feat(slack-app): bind sandbox MCP identity to each message's actor - #69618
Closed
VojtechBartos wants to merge 7 commits into
Closed
feat(slack-app): bind sandbox MCP identity to each message's actor#69618VojtechBartos wants to merge 7 commits into
VojtechBartos wants to merge 7 commits into
Conversation
…s package Move the per-message orchestration out of PostHogCodeSlackMentionWorkflow into helpers/process_mention_message.py: process_mention_message plus the MentionSignalState holder and a MentionSignalHandlersMixin carrying the interactive signal handlers. The workflow class becomes a thin shell with an unchanged defn name, signals, and command sequence, so in-flight histories replay identically. helpers.py becomes the helpers package __init__ so activity-side imports are unchanged. No behavior change.
… flag One SlackAppMentionWorkflow per thread/DM serializes message processing: the webhook signal-with-starts the conversation workflow, messages queue as signals carrying their own routing-time-resolved user, and the loop feeds them one at a time through the shared mention orchestration. The workflow idles out after 30s with an empty queue; the next message starts a fresh instance that picks the conversation back up via SlackThreadTaskMapping. Gated by the slack-app-queue-workflow flag; off, dispatch stays one workflow per message.
…sing Mentions and DMs get an ⌛ reaction when they land in the conversation queue and an 👀 swap when their turn starts. Untagged thread followups stay reaction-free since most are dropped by the chitchat classifier. The reaction activity takes a single pydantic input model; reactions are best-effort and never stall the queue.
Adds rebind_sandbox_identity_for_user to the tasks facade: mint a PostHog MCP OAuth token for the given user and push fresh MCP configs into the run's live sandbox, so subsequent agent actions attribute to that user. Best-effort by contract — failures are logged, never raised. The sandbox's current identity is tracked per run in the tasks cache so transitions always bypass the token-freshness rate limit (including switching back to the task creator in ping-pong threads), and every sandbox-creation path clears the marks — a fresh sandbox boots with creator credentials, so a mark surviving a mid-run workflow retry or dead-restore fallback would block the actor's next rebind.
Messages dispatched to the per-conversation queue workflow carry per_message_identity: the sandbox connection JWT is minted for the message's actor and the facade rebind runs before delivery, so insights, dashboards, and other PostHog writes attribute to whoever actually spoke. Rebind failures are best-effort and never drop the message. The legacy per-message workflow leaves the field False and keeps every credential bound to the task creator, exactly as before.
VojtechBartos
force-pushed
the
vojtab/slack-app-per-message-identity
branch
from
July 10, 2026 13:02
3530bd2 to
ad4731e
Compare
2 tasks
The freshness and identity marks describe what a sandbox holds, so key them by sandbox id: a replacement sandbox (fresh provision, snapshot restore, mid-run workflow retry) starts unmarked by construction and defaults back to the boot-time creator identity — deleting all the creation-path clear calls and the clear helper outright. Make the cache helpers own the composite key as (scope, user_id) signatures; the boot-time mark in start_agent_server had silently kept the old single-run shape after the rekey, orphaning the write and losing the post-boot refresh suppression. Stamp per_message_identity inside the queue workflow loop instead of at dispatch, so no future dispatcher into the workflow can forget it. Mint the rebind token via create_oauth_access_token with a user override rather than re-composing its body, and record both marks on the no-configs bail so hosts with no resolvable MCP URL stop re-minting on every cross-user message.
Contributor
🤖 CI reportℹ️ ClickHouse migration SQL — 1 migration(s)ClickHouse migration SQL per cloud environment
|
VojtechBartos
force-pushed
the
vojtab/slack-app-queue-workflow
branch
2 times, most recently
from
July 13, 2026 13:26
f94df18 to
1819f04
Compare
2 tasks
VojtechBartos
force-pushed
the
vojtab/slack-app-queue-workflow
branch
5 times, most recently
from
July 13, 2026 16:13
8959499 to
45bfaca
Compare
Member
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Stacked on #68894. With the queue workflow, teammates take turns talking to one running agent — but the sandbox's PostHog MCP token stays bound to whoever created the task. A teammate's followup mints insights and dashboards as the original mentioner:
Changes
Every message the queue workflow processes rebinds the sandbox's MCP identity to that message's actor before the message is delivered:
The transition check is what makes ping-pong threads correct: after Bob's turn, Alice's next message is detected as a swap back and always refreshes — "actor == creator" alone is no proof the sandbox is currently authed as the creator.
How the sandbox's current identity is tracked — two self-expiring cache entries, keyed by sandbox id:
Keying by sandbox means a replacement sandbox (fresh provision, snapshot restore, mid-run workflow retry) starts unmarked by construction — it boots with creator credentials and the cache agrees, with zero clearing code. Absence always means "assume boot state", so an evicted entry degrades to one extra refresh, never to wrong behavior.
Everything is gated to the queue workflow: the legacy per-message workflow leaves
per_message_identityFalse and keeps creator-bound credentials exactly as today. Rebinds are best-effort by contract — a failure logs and the message still goes through under the previous identity.How did you test this code?
Automated only; the two-user Slack thread verification is planned as part of the queue-workflow dev rollout (the identity transitions are fully visible in worker logs:
refresh_mcp_identity_transition/refresh_mcp_delivered).test_send_followup_to_sandbox.py: identity transitions bypass the freshness rate limit; switching back to the creator refreshes (the ping-pong case); same-identity messages inside the window are no-ops; failed pushes don't record marks; no-config hosts don't re-mint per message.test_followup_forwarding.py: same-user and cross-user followups rebind to the right actor;test_legacy_dispatch_keeps_creator_identitypins the flag-off path byte-identical to master (creator JWT, zero rebind calls).per_message_identitystamped True.Automatic notifications
Docs update
N/A, internal credential mechanics behind the queue-workflow flag.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Authored with Claude Code, starting from an unmerged June branch of Vojta's that re-authed MCP for cross-user followups; adapted so every message rebinds with the tasks layer detecting transitions (fixing ping-pong), and gated after review showed the shared activity would otherwise change legacy-workflow behavior. Three /simplify passes shaped the final form — the notable ones: identity marks re-keyed from run id to sandbox id (deleting all creation-path clearing code), cache-key composition moved inside the helpers after a hand-built key at one call site silently orphaned the boot-time rate-limit mark, and the
per_message_identitystamp moved from the dispatch branch into the workflow loop so future dispatchers can't forget it. Split out of a combined MCP+GitHub PR; the GitHub/git-author half is stacked as #70024. Known boundaries, deliberate: identity lives in cache rather thanTaskRun.state(eviction degrades safely to creator); web/PostHog-Code followups still rebind to the task creator — acceptable today because task control there is creator-only, and a named follow-up if web multiplayer opens up.