Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

feat(chat): resolve delegate identity via per-turn token, not a dedicated MCP server

merged#2735CedarCopilot

CedarCopilot wants to merge 3 commits into staging from feat/delegate-token-attribution

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Sep 8, 2026, 4:02 PM
  2. Sep 8, 2026, 5:54 PM
  3. Merged
    Sep 8, 2026, 6:26 PM
  4. Live on prod
    Sep 8, 2026, 6:26 PM
  5. Observed 2 days
    Sep 8, 2026, 6:26 PM
  6. Pipelines steady after this deploy
    Sep 8, 2026, 6:26 PM
  7. Unobserved

    Live on prod, no production signal yet

    Sep 11, 2026, 5:47 PM

Behaviors Libra is checking

Each named playbook delegate receives a fresh unique per-turn token in only its own prompt and is instructed to include it as `_delegateToken` on every Cedar tool call.Degradedlow confidence

Strict CloudWatch fallback found 2 prod failure log lines for [run-chat-agent-sdk] query() failed (Claude Code process aborted by user) in the last 24h while checking buildsubagentdefinitions, runchatviaagentsdk, _delegatetoken, agent-bound-chat.test, delegate-tokens.test.

prod, checked Sep 11, 2026, 5:02 PM
Cedar family tools, conversation analysis, and skill loading accept `_delegateToken` only as internal routing metadata and never pass it into the underlying family-tool input contract.Degradedlow confidence

Strict CloudWatch fallback found 2 prod failure log lines for [run-chat-agent-sdk] query() failed (Claude Code process aborted by user) in the last 24h while checking buildcedarsdkmcpserver, _delegatetoken, agent-bound-chat.test, delegate-tokens.test, delegate-tool-scoping.test.

prod, checked Sep 11, 2026, 5:02 PM
Delegate tokens are removed from tool-call arguments before they are streamed to the frontend or persisted in `chat_messages`.Inconclusivelow confidence

Verification reached its budget before a verdict.

prod, checked Sep 11, 2026, 5:45 PM
A valid delegate token resolves each Cedar tool call to that delegate's real playbook `agentId`, allowing ownership-gated conversation and document writes to be attributed to and authorized for the correct delegate.Inconclusivelow confidence

Verification reached its 120s wall-clock budget before a tied verdict. Libra is keeping this intent verifying until the next check.

prod, checked Sep 11, 2026, 5:47 PM
Missing, malformed, stale, or unrecognized delegate tokens resolve to no delegate identity without throwing or granting elevated access, while tokenless main-thread and general-purpose calls retain the bound-agent or `chat-agent-sdk` identity.Not checked
prod, checked Sep 11, 2026, 5:42 PM
A user-connected MCP server named exactly `cedar` cannot shadow Cedar's shared server, while names matching the removed `cedar_<agentId>` pattern are no longer treated as reserved.Not checked
prod

Libra has verdicts on 0 of 7 tracked behaviors on prod; 7 are still being checked. Libra has 2 low-confidence degraded verdicts it is still confirming. Libra checks hourly for 3 days after each deploy.

What was wrong

delegate-tool-scoping.ts's scopeNamedDelegatesToOwnServers (introduced 5825c46e0, 2026-09-06 , a real, necessary fix for silently-broken field-ownership attribution on the chat harness, PR #2699) gave every Task-delegatable named subagent its own dedicated MCP server, each registering the full 23-tool family-tool schema set from scratch. On an account with N subagents, that's N complete copies of ~25 tool schemas configured for the SDK to connect to at session start , which is what caused real "Prompt is too long" / context-overflow incidents on accounts with many subagents (Sherlock, 2026-09-08). A same-day fix (a502820d7) addressed a different contributor (full subagent instruction bodies inlined into prompt) but never touched this one, which is why the failure kept recurring on the one real account with enough subagents (12) to hit it.

The fix

Identity now travels via a per-turn unforgeable token, not via which server a call arrives on:

  • delegate-tokens.ts (new): mints one random token per named delegate (mintDelegateTokens) and the exact instruction line spliced into that delegate's own prompt (delegateTokenPromptLine). Full trust-model writeup in the file header , unforgeable (never appears anywhere two delegates can both see it), fail-safe (an unresolved token degrades to today's un-attributed identity, never an elevated one).
  • subagents.ts: mints tokens after agentIdsByName is known, splices each delegate's own token into ONLY that delegate's own prompt. Delegates no longer get tools/mcpServers overrides at all , they inherit the parent's one shared server, same as general-purpose always did.
  • mcp-server.ts: buildCedarSdkMcpServer takes a resolveDelegateAgentId resolver instead of a fixed agentIdOverride + serverName. Every family tool's shape gains an optional _delegateToken field (one line, not touched in any of the 23 individual tool files); a new resolveCallAgentId helper strips it from the call's args and resolves it to a real agentId, computed fresh per call instead of baked into the server at construction time.
  • run-chat-agent-sdk.ts: builds exactly ONE Cedar server per turn (moved after agentIdByToken is known), wired with the resolver.
  • delegate-tool-scoping.ts: the per-agent-server multiplication is gone , scopeNamedDelegatesToOwnServers/perAgentServers deleted. What's left (buildScopedMcpServers, findReservedCedarServerNameCollisions, cedarToolNamesForServer) simplifies to match: one server, one reserved name.
  • legacy-tool-name-map.ts: simplified the frontend tool-name-stripping regex , it used to handle both the shared server and a per-delegate-shaped name; only the former exists now.

No changes needed to the actual gated tools (updateConversationFieldsTool.ts, writeDocumentTool.ts, task tools) , they already just read context.requestContext.get('agentId'); the fix is entirely upstream of them.

Self-review (thermo-review) , findings and fixes

Ran a structural/security review against this diff before opening. Two real findings, both fixed in the second commit:

  • A dangling reference to a test file that was never created , pointed the trust-model doc at the actual isolation test instead.
  • An undocumented residual gap: a delegate's own final answer (not its prompt) returns to the parent thread, and nothing stopped a delegate from narrating its own token back into it. Documented the gap explicitly (blast radius: same user's own session, so worst case is a misattributed write to data the caller already holds , a coordination bug, not a tenant-isolation one) and added a one-line prompt mitigation ("never repeat this token in your final answer").
  • Also removed three non-null assertions (!) I'd introduced in the new isolation test, per this repo's no-cast policy.

How to test

  • `pnpm --filter @zero/server exec vitest run src/mastra/workflows/c
Show production surfaces and changed-file mapping

Production surfaces

Libra has not measured any production surfaces for this change yet.

Changed files → surfaces

  • apps/server/src/mastra/workflows/chat/__tests__/agent-bound-chat.test.tsno production surface mapped
  • apps/server/src/mastra/workflows/chat/harness/__tests__/delegate-tokens.test.tsno production surface mapped
  • apps/server/src/mastra/workflows/chat/harness/__tests__/delegate-tool-scoping.test.tsno production surface mapped
  • apps/server/src/mastra/workflows/chat/harness/__tests__/legacy-tool-name-map.test.tsno production surface mapped
  • apps/server/src/mastra/workflows/chat/harness/__tests__/resolve-call-agent-id.test.tsno production surface mapped
  • apps/server/src/mastra/workflows/chat/harness/delegate-tokens.tsno production surface mapped
  • apps/server/src/mastra/workflows/chat/harness/delegate-tool-scoping.tsno production surface mapped
  • apps/server/src/mastra/workflows/chat/harness/legacy-tool-name-map.tsno production surface mapped
  • apps/server/src/mastra/workflows/chat/harness/mcp-server.tsno production surface mapped
  • apps/server/src/mastra/workflows/chat/harness/request-context.tsno production surface mapped
  • apps/server/src/mastra/workflows/chat/harness/subagents.tsno production surface mapped
  • apps/server/src/mastra/workflows/chat/run-chat-agent-sdk.tsno production surface mapped