Skip to content

trunk-merge/pr-89909/f5757c3c-8331-4c1b-9208-c1d14326f6d7 - #89965

Closed
trunk-io[bot] wants to merge 60 commits into
masterfrom
trunk-merge/pr-89909/f5757c3c-8331-4c1b-9208-c1d14326f6d7
Closed

trunk-merge/pr-89909/f5757c3c-8331-4c1b-9208-c1d14326f6d7#89965
trunk-io[bot] wants to merge 60 commits into
masterfrom
trunk-merge/pr-89909/f5757c3c-8331-4c1b-9208-c1d14326f6d7

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 d788f6229b9f0f51530ec0e6c7f5a46158df17f5.

See more details about each PR in the batch here:

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

Pull Requests Being Tested

This pull request is testing a batch with the changes from pull requests 89909 and 89911 - batching documentation.

Dependencies

This pull request depends on the changes from pull requests 89686, 87881, 89715, and 86299.

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
Expose list, create, and update operations for error tracking severity rules through the PostHog MCP server.

Generated-By: PostHog Desktop
Task-Id: 12089906-88c9-4363-aa68-4705d0f4c5f4
…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
Validate that each affected tool remains reachable through the compact domain index instead of requiring one exact domain spelling.

Generated-By: PostHog Desktop
Task-Id: bc15d8bc-972e-4aea-8ad5-bebb38b8b6b1
…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
hpouillot and others added 26 commits August 26, 2026 13:36
Regenerate the MCP tool-schema snapshots after the generated error tracking API gained behavioral filters.

Generated-By: PostHog Desktop
Task-Id: 7c8792a2-2d15-4047-9067-ca7fb2c72e8f
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
…ands

migrations:down, migrations:up and migrations:sync each carried their own copy
of orphan classification, cache recovery, confirmation, rollback, record
removal and pending apply. migrations_sync scored 27 on Ruff's cyclomatic
complexity check and migrations_down scored 17, against a repository limit
of 10.

Extract the shared steps into helpers and leave each Click command as a thin
sequence of calls. All three commands now score under the limit.

Two output strings are unified in the process: the --force warning and the
uncached record removal line now read the same in both commands.

Add command-level tests for dry run, cancellation, --force, a failed rollback,
duplicate schema recovery and an unrecoverable apply error.

Generated-By: PostHog Desktop
Task-Id: 78eac211-3c34-4783-b4a6-e0fb10dba557
`_classify_orphans` built a frozen `OrphanRollbackPlan` and then appended to its
lists, and `_fetch_uncached_from_git` appended recovered migrations into the
caller's `cached` list, so `replace()` returned a plan aliasing the mutated list.
Both now build their lists first and construct the plan once.

Add two cases: a git-recovered orphan gets a real schema rollback rather than a
bare record delete, and `--force --dry-run` passes `dry_run=True` down to the
record-removal path.

Generated-By: PostHog Desktop
Task-Id: 78eac211-3c34-4783-b4a6-e0fb10dba557
Generated-By: PostHog Desktop
Task-Id: 78eac211-3c34-4783-b4a6-e0fb10dba557
The help said --force removes DB records without a schema rollback. It does
that only for uncached migrations. Cached orphans are rolled back for real,
with DROP COLUMN and DROP TABLE against the local database, whether or not
--force is set. What --force actually does is let the run continue past the
abort that uncached migrations would otherwise raise.

The warning shown at the confirmation prompt already says this. The option
help now agrees with it.

Generated-By: PostHog Desktop
Task-Id: 78eac211-3c34-4783-b4a6-e0fb10dba557
The hint shown when uncached migrations block a rollback repeated the claim
that --force skips the schema rollback. Cached orphans in the same run are
still rolled back for real. The hint now says what --force does, which
matches the option help and the confirmation warning.

Generated-By: PostHog Desktop
Task-Id: 78eac211-3c34-4783-b4a6-e0fb10dba557
_fetch_uncached_from_git returned tuple[list[MigrationInfo], list[MigrationInfo]], whose two same-typed halves can be swapped silently at the call site. Return a frozen GitRecoveryResult with newly_cached and still_uncached fields instead.

This clears the blocking semgrep devex tuple-return-prefer-dataclass finding.

Generated-By: PostHog Desktop
Task-Id: 78eac211-3c34-4783-b4a6-e0fb10dba557
Generated-By: PostHog Desktop
Task-Id: 2adfe509-5e60-4089-b1d8-0d6ab686c67c
…ilings

# Conflicts:
#	posthog/migrations/max_migration.txt
Metabase credential validation refuses to follow redirects as an SSRF guard. When the Instance URL responds with a 3xx, validation returned the raw "Metabase host is not allowed", which tells the user nothing they can act on.

Return a message that names the redirect and points at the fix: enter the direct instance URL, then reconnect.

Generated-By: PostHog Desktop
Task-Id: ae9fe31b-7111-4d9b-893c-3d3ffe64ce00
A user who pastes an HTTP method in front of the URL (for example a "POST https://..." copied from API docs) into the custom source base URL failed validation with "is missing a hostname", even though the URL plainly has one. urlparse reads no host once a method prefix is present.

Detect a leading HTTP verb and return a message that names the fix: remove the method and enter just the address.

Generated-By: PostHog Desktop
Task-Id: ae9fe31b-7111-4d9b-893c-3d3ffe64ce00
@trunk-io

trunk-io Bot commented Aug 27, 2026

Copy link
Copy Markdown
Author

Static BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Django migration SQL — 1 new migration to review

We've detected new migrations on this PR. Review the SQL output for each migration:

posthog/migrations/1322_organization_read_only_mcp_access.py

BEGIN;
--
-- Add field read_only_mcp_access to organization
--
ALTER TABLE "posthog_organization" ADD COLUMN "read_only_mcp_access" boolean DEFAULT false NULL;
ALTER TABLE "posthog_organization" ALTER COLUMN "read_only_mcp_access" DROP DEFAULT;
COMMIT;

Last updated: 2026-08-27 10:08 UTC (8da25af)

Django migration risk — migration analysis complete

We've analyzed your migrations for potential risks.

Summary: 1 Safe | 0 Needs Review | 0 Blocked

✅ Safe

Brief or no lock, backwards compatible

posthog.1322_organization_read_only_mcp_access
  └─ #1 ✅ AddField
     Adding nullable field requires brief lock
     model: organization, field: read_only_mcp_access

📚 How to Deploy These Changes Safely

AddField:

This operation acquires a brief lock but doesn't rewrite the table.

Deployment uses lock timeouts with automatic retries, so lock contention will cause retries rather than connection pile-up.

Last updated: 2026-08-27 10:09 UTC (8da25af)

@trunk-io trunk-io Bot closed this Aug 27, 2026
@trunk-io
trunk-io Bot deleted the trunk-merge/pr-89909/f5757c3c-8331-4c1b-9208-c1d14326f6d7 branch August 27, 2026 10:10
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.

4 participants