Skip to content

fix(agent): let MCP publish channel CONTEXT.md and canvases#3451

Open
jakesciotto wants to merge 3 commits into
mainfrom
posthog-code/allow-mcp-write-channel-context-md
Open

fix(agent): let MCP publish channel CONTEXT.md and canvases#3451
jakesciotto wants to merge 3 commits into
mainfrom
posthog-code/allow-mcp-write-channel-context-md

Conversation

@jakesciotto

@jakesciotto jakesciotto commented Jul 14, 2026

Copy link
Copy Markdown

Problem

A channel's CONTEXT.md and its freeform canvases are not files on disk — they live in PostHog, and the agent publishes them via the PostHog MCP sub-tools desktop-file-system-instructions-partial-update and desktop-file-system-canvas-partial-update.

The destructive-sub-tool permission gate classified every *-partial-update as destructive, so after the user already approved the generation plan, publishing hit a second, redundant "modify live PostHog data" approval (and behaved inconsistently in cloud runs). That gate exists to protect the user's PostHog product data (flags, experiments, notebooks) — not the app's own artifacts, which are the whole point of the generation task.

The hard part: the write must stay bound to the artifact the task actually owns. A blanket exemption by sub-tool name would let the agent publish to any id — silently overwriting an arbitrary channel's CONTEXT.md or any canvas. And the target id can't be threaded through structured session metadata without PostHog backend changes, because these tasks default to cloud runs whose session metadata is rebuilt server-side.

Changes

  • The generation prompts embed a machine-readable authorized-write marker (sub-tool + id) via a shared helper (@posthog/shared, rendered as an invisible HTML comment). This is the one trusted, in-repo source of the target id that works on both local and cloud runs.
  • The agent captures those ids only from user messages (which it cannot forge) onto the session.
  • The permission gate skips the destructive prompt for a sanctioned publish only when the call's id matches an authorized id — and never in plan mode, so CONTEXT.md co-design is preserved. Any other id still prompts. isPostHogDestructiveSubTool stays a pure classifier (these publishes are destructive; the id-scoped exemption lives in the handler).
  • Fails safe: an unrecognised/malformed id, or a resumed session that never re-received the marker, falls back to the normal approval prompt.

How did you test this?

  • posthog-exec-gate.test.ts — id extraction from the exec command, the sanctioned-sub-tool predicate, and destructive classification (18 passed).
  • permission-handlers.test.ts — matching id auto-allows without a prompt; a different id still prompts; plan mode never auto-allows (30 passed).
  • authorized-write-marker.test.ts (shared) — build/parse round-trip, multiple markers, malformed markers (4 passed).
  • pnpm --filter @posthog/{shared,agent,ui} typecheck and Biome on all changed files — clean.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Created with PostHog Code

A channel's CONTEXT.md and freeform canvases are not files on disk — they're
stored in PostHog and published by the agent via the PostHog MCP sub-tools
`desktop-file-system-instructions-partial-update` and
`desktop-file-system-canvas-partial-update`.

The destructive-sub-tool gate classified any `*-partial-update` sub-tool as
destructive, so these publishes were forced behind an extra "modify live PostHog
data" approval on top of the plan approval the user already gave — redundant on
desktop and inconsistent in cloud runs. That gate exists to protect PostHog
product data (flags/experiments/notebooks), but these two sub-tools write the
app's own artifacts, which are the explicit output of the generation task.

Exempt exactly those two sub-tool names from `isPostHogDestructiveSubTool` so
they ride the same silent-allow path as create-family exec sub-tools. Uses an
exact-name allowlist, not a prefix, so the broader
`desktop-file-system-{partial-update,destroy}` sub-tools that rename/delete whole
channels stay gated. Plan mode still blocks the write until the plan is approved,
so CONTEXT.md co-design is unchanged.

Generated-By: PostHog Code
Task-Id: ab56d0cc-fb60-4554-a576-88307e823e67
@trunk-io

trunk-io Bot commented Jul 14, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit d0d8b83.

@jakesciotto jakesciotto marked this pull request as ready for review July 14, 2026 21:45
@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Security Review

The changed allowlist removes the destructive prompt for two live PostHog writes without checking that the target artifact id matches the approved generation task.

Reviews (1): Last reviewed commit: "fix(agent): let MCP publish channel CONT..." | Re-trigger Greptile

Comment on lines +26 to +27
"desktop-file-system-canvas-partial-update", // freeform canvas publish
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Artifact Id Bypasses Approval

When mcp__posthog__exec is already allowed, these exact subtool names now skip the destructive prompt for any model-supplied id. A call to desktop-file-system-instructions-partial-update or desktop-file-system-canvas-partial-update can publish to a different channel or canvas than the approved generation task, so the live PostHog write is no longer scoped to the user-approved artifact.

Rule Used: When implementing new features, ensure that owners... (source)

Learned From
PostHog/posthog#31236

…ifact id

The previous approach exempted the CONTEXT.md/canvas publish sub-tools from the
destructive gate by name, which let the agent publish to ANY artifact id without
approval — it could silently overwrite an arbitrary channel's CONTEXT.md or any
canvas.

Bind the auto-allow to the exact artifact the task owns instead:

- Revert `isPostHogDestructiveSubTool` to a pure classifier (these publishes are
  destructive again).
- The generation prompts now embed a machine-readable authorized-write marker
  (sub-tool + id) via a shared helper. The agent captures those ids off the
  *user* message only (it can't forge one) onto the session.
- The permission gate skips the destructive prompt for a sanctioned publish only
  when the call's `id` matches an authorized id — and never in plan mode, so
  CONTEXT.md co-design is preserved. Any other id still prompts.

This carries the target id through the app-authored prompt, the one trusted
source available on both local and cloud runs without backend changes.

Generated-By: PostHog Code
Task-Id: ab56d0cc-fb60-4554-a576-88307e823e67
…ctions tag

The freeform authorized-write marker was appended after
`</canvas_generation_instructions>`, so it leaked into
`extractCanvasInstructions(...).stripped` and broke freeformPrompt.test.ts,
which asserts the stripped text is exactly the user's instruction.

Move the marker inside the tag body. It's still in the raw prompt text the agent
scans, and the conversation UI already collapses that tag — so the visible
message stays the bare instruction.

Generated-By: PostHog Code
Task-Id: ab56d0cc-fb60-4554-a576-88307e823e67
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