Skip to content

trunk-merge/pr-87346/2c072ef9-b7fa-4ff7-877e-f48418c7cc6b-bisection - #90042

Closed
trunk-io[bot] wants to merge 51 commits into
masterfrom
trunk-merge/pr-87346/2c072ef9-b7fa-4ff7-877e-f48418c7cc6b-bisection
Closed

trunk-merge/pr-87346/2c072ef9-b7fa-4ff7-877e-f48418c7cc6b-bisection#90042
trunk-io[bot] wants to merge 51 commits into
masterfrom
trunk-merge/pr-87346/2c072ef9-b7fa-4ff7-877e-f48418c7cc6b-bisection

Conversation

@trunk-io

@trunk-io trunk-io Bot commented Aug 27, 2026

Copy link
Copy Markdown
Trunk Merge Pull Request Banner

This pull request was created and is being managed by Trunk Merge.

This pull request is based on the master branch at SHA 7eced17f1684d6461a693c4621acaa75336840e9.

See more details here.

When CI completes, this pull request will be closed automatically.

Pull Requests Being Tested

This pull request is testing the changes from pull request 87346.

Batch Bisection

This pull request is in a batch bisection. Pull requests successfully tested by this PR will re-enter the main queue.

a-lider and others added 30 commits August 20, 2026 12:52
Adds AccessCeiling, an org-scoped per-channel cap (channel, optional resource,
max_level), with channel_ceiling()/classify_channel() in the access_control facade.
APIScopePermission denies write-scoped actions for MCP-channel requests (classified
by the MCP server user agent on token auth) when the org caps the channel at viewer,
before the wildcard-scope early return so *-scoped tokens are clamped too. Gated on
the organization security settings feature. No settings UI yet; rows are the API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
APIScopePermission now only translates DRF vocabulary and applies the verdict;
classification, the entitlement gate, row lookup and the denial copy live in
ceiling_denial_for_request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…n class

ChannelCeilingPermission joins the mixin stack (including the dangerously_get_permissions
branch, like domain enforcement) instead of living inside APIScopePermission. DRF evaluates
permission classes with AND semantics, so the wildcard-scope early return can no longer
matter, and session-authenticated channels become cappable later.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
AccessCeiling becomes SurfaceAccessLimit, channel becomes surface, and the permission
class becomes WithinSurfaceLimits, so the vocabulary reads without a glossary: a surface
is how the request arrived (MCP now; personal API keys, share links, impersonation later)
and a limit is the max level the org allows through it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…lish

One statement per sentence, no semicolons, explicit subjects, hedges kept. Also
removes the test class docstring per the no-doc-comments-in-tests house rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
resource="*" replaces a nullable resource: a null one column away from
max_level="none" read as two different nones. Also drops the nulls_distinct
special case from the unique constraint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…de modules

The surface_limits module docstring kept its pre-rewrite wording. Function docstrings
now start with a verb, and the user-agent comment loses the mint-time jargon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…mitPermission

The repo's stack convention is the Permission suffix, and the exact model-name prefix
makes one grep find the storage, the policy and the enforcement together.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…IDOR rules

The coverage check requires every org-scoped model in the semgrep taint rules, so
lookups without an organization filter get flagged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
The column is not nullable since the wildcard change; the row now takes the "*" default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
… signatures

The writes early-return ran before the row lookup, so a disabled surface still served
reads, against the model's documented semantics. Adds the regression test and types
the permission and policy entry points with concrete request and view classes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…tion column

Replaces the SurfaceAccessLimit table with Organization.mcp_access_read_only, matching
the sibling security settings, per review. The enforcement layer keeps its shape: the
policy lives in facade/mcp_access.py and MCPAccessPermission enforces it in every
viewset stack. Drops the per-resource wildcard and the none level; read-only is the
only cap. The hot-table migration is acknowledged and needs a coordinated deploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
A non-member probing another project with an MCP-marked write got the policy message
before the membership check ran, disclosing that organization's security setting.
The cap now runs after TeamMemberAccessPermission and domain enforcement, so
non-members get the generic denial. Enforcement is unchanged: DRF combines permission
classes with AND semantics, so order only decides which denial message wins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…ilings

# Conflicts:
#	posthog/migrations/max_migration.txt
…annotation

Master added an import-linter contract while this branch was in flight: facade modules
must not import DRF. The permission class moves to presentation/ (exposed via tach),
and the facade policy types its request parameter with a structural protocol instead
of the DRF Request. Also drops a wrong HttpResponse annotation mypy caught.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…rwards

A write forwarded through a PostHog connection reached the target with the connection
token and no MCP user agent, so a target organization with MCP read-only could still
be written to on behalf of an MCP client in the source organization. The forwarder now
marks outbound requests with the MCP user agent when the originating request is MCP,
and the target classifies them like any other MCP traffic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…ents

One statement per sentence, no compound colon clauses, plain words, and the stale
facade path in the policy docstring corrected to presentation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…he ack header

Review findings on the draft: the acknowledgment file header got alphabetized by the
entry-sorting edit, and permission chains from dangerously_get_permissions can omit
APIScopePermission, so an unclassified action there collapsed to writes=False and
passed the cap. Unclassified actions now fall back to the HTTP method. Adds a test
that the organization viewset (a dangerously-defined chain) is capped, and corrects
the RequestLike justification to cite the import-linter contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…ly property

HttpRequest.headers is a cached_property, and mypy rejects a read-only descriptor
where a protocol declares a settable attribute.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…nd enforcement

is_mcp_request and the user-agent marker move to posthog/auth.py, next to the
authenticator classes they inspect. The facade keeps only the decision:
mcp_access_denial(organization, is_mcp, writes), with no request type and no
RequestLike protocol. MCPAccessPermission moves to posthog/permissions.py, next to
its base class and the sibling stack permissions, and the tach carve-out for
presentation.permissions is reverted. posthog_connection now imports the classifier
from posthog.auth, which removes that posthog-to-products import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…and docstrings

The user-agent constant cannot be shared across the two runtimes, so both sides now
carry a plain mirror note. The moved class and facade docstrings get one statement
per sentence and explicit subjects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…ent org

On root viewsets (/api/environments/{id}/, /api/projects/{id}/) the target is the
detail pk, so get_organization_from_view fell back to the caller current organization,
a UI preference. A member of two organizations could then have the cap evaluated
against the wrong one, failing open when the current org is uncapped. MCPAccessPermission
now uses the same guard as VerifiedDomainEnforcementPermission: defer to
has_object_permission when the target is not URL-derivable, and resolve the org from
the fetched object there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…ilings

# Conflicts:
#	posthog/migrations/max_migration.txt
#	posthog/permissions.py
Drops the mcp_origin parameter. A connection forward always carries the MCP user
agent now, so the target organization applies its read-only policy to any forwarded
write. The target-metadata read is a GET, which the policy always allows, so the
unconditional marker is simpler and has no downside.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…ot creates

Reverts the always-mark: a forward is stamped as MCP only when the originating request
is MCP (mcp_origin=is_mcp_request(request)). A plain session user forwarding a write is
no longer capped by the target org - that org restricts MCP, not connections.

Also caps root creates. POST /api/projects/ and /api/environments/ have no object for
has_object_permission to check, and they land in the resolved (current) organization,
so has_permission now caps creates against that org. Creating a new organization stays
uncapped, since its target org cannot be resolved before it exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…embership gates

Promotes the enforcement-test user to owner so the membership permissions pass and
MCPAccessPermission is the permission that denies, which also proves the cap binds
owners. Adds the multi-project plan feature so the root-create test reaches the cap
instead of PremiumMultiProjectPermission. Drops the organization-creation test:
OrganizationViewSet builds its own permission list for create without this class, so
MCP does not cap organization creation, and it is not a reachable MCP tool anyway.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
is_mcp_request only recognized personal API keys and OAuth tokens. ID-JAG (XAA) tokens
are served from the same OAuth endpoint, carry scopes, and can reach the API through the
MCP server, so a write on that pathway skipped the read-only cap. Adds them to the
classifier, with a unit test over all three token types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
a-lider and others added 21 commits August 26, 2026 00:12
…ttings

A new 'AI and MCP access' section in organization Security settings, before personal
API keys, with one switch: 'Restrict MCP access to read-only'. The switch writes and
deletes the org-wide SurfaceAccessLimit row through the organization API
(mcp_access_read_only serializer field), keeps per-resource exception rows, and needs
the organization security settings entitlement, like its sibling toggles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
Rename Organization.mcp_access_read_only to read_only_mcp_access per review,
including the migration module. Cite the OrganizationMemberPermissions
delegate-to-object split in the MCP guard comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…ilings

# Conflicts:
#	posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr
#	posthog/migrations/max_migration.txt
…the API and UI

Follow the L1 rename in the serializer, the settings scene, and the
generated API types.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…pdate payload

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
Say the setting restricts the PostHog MCP server itself, and note that
each member's permissions still apply via access control.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
PostHog AI also connects through MCP internally and is not capped, so
the AI framing was wrong.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…ilings

# Conflicts:
#	posthog/migrations/max_migration.txt
CI regenerates the typegen interfaces and diffs them. The organization
update payload gained read_only_mcp_access, and the recording-viewed
logic type moved to SessionRecordingViewedResult on master.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…e generator

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEer2y9uGMkADPjMfWG8WK
…s-settings-ui

# Conflicts:
#	products/access_control/backend/tests/test_mcp_access.py
@trunk-io

trunk-io Bot commented Aug 27, 2026

Copy link
Copy Markdown
Author

Static BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@trunk-io trunk-io Bot closed this Aug 27, 2026
@trunk-io
trunk-io Bot deleted the trunk-merge/pr-87346/2c072ef9-b7fa-4ff7-877e-f48418c7cc6b-bisection branch August 27, 2026 12:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant