Skip to content

feat(pull): platform-injected execution id; refuse unguarded effects on pull agents (#2392) - #3086

Merged
vybe merged 3 commits into
devfrom
feature/2392-inject-execution-id
Sep 29, 2026
Merged

vybe merged 3 commits into
devfrom
feature/2392-inject-execution-id

Conversation

@obasilakis

@obasilakis obasilakis commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • The execution id now reaches every side-effect sink from the platform. The agent's Trinity MCP entry sends X-Trinity-Execution-Id with each spawned process's own TRINITY_EXECUTION_ID (manual when unset: a person's terminal session). The MCP server reads it per request and it wins over the agent-typed tool arg, which stays as the fallback for older images.
  • On a pull-mode agent, an effect with no usable id (absent, unknown, or another agent's execution) is refused: 422 {"reason": "effect_unguarded"} plus an effect_unguarded operator alarm through create_bounded_alert. manual and a DB lookup error proceed.
  • Every other agent behaves as before, with a new effect_guard.degraded warning whenever a send skips de-duplication.

Policy per TARGET_ARCHITECTURE.md §Re-Delivery and Side-Effect Recovery. The manual exception for terminal sessions was agreed during plan review.

Changes

  • Agent server: trinity_mcp.py writes the header for Claude Code and Gemini (${TRINITY_EXECUTION_ID:-manual}) and for Codex (env_http_headers). mcp_validator.py accepts exactly one extra header with that exact literal; both vendored copies are byte-identical.
  • MCP server: authenticate parses the header; tools/execution_id.ts resolves header-then-param for send_message, call_user, send_voice_reply, share_file and call_a2a_agent.
  • Backend: idempotency_service.effect_guard refuses on pull-mode agents (effect_dedup_required, the one predicate Phase 5 flips). The messages, voip, agent_files, a2a and internal share routers map the refusal to 422. Alert type registered, id prefix reserved.
  • Docs: effect-idempotency flow, execution.md, operating-room.md, and T6.3 plus the remaining-work lists in the pull migration docs.

Verification

  • Live: Claude Code 2.1.250 in an agent container against a request-logging MCP server. In 13 runs each request carried the spawning process's id; with the variable unset it carried manual. An earlier batch of 3 runs sent manual with the variable set and did not reproduce. If it recurs, the effect proceeds unguarded and logged, which is today's behaviour.
  • MCP transport: a real fastmcp server and client, one session, two requests with different headers: the backend received both ids.
  • Mutation: with the pull-mode predicate forced to False, 5 refusal tests go red.
  • Unit: backend island 18,932 passed / 45 skipped (Python 3.13); MCP server 586/586; tsc clean.

Test Plan

  • cd tests && pytest unit/test_2392_effect_guard_fail_closed.py unit/test_2392_execution_id_header.py unit/test_idempotency.py unit/test_736_a2a_outbound_call.py -v
  • cd src/mcp-server && npm test && npx tsc --noEmit
  • After deploy: rebuild the base image, restart a pilot, and confirm effect:* rows carry the turn id on the eu2 emitting arm (oracle-7-ai-semi)

Known limits

  • Codex terminal sessions on a pull agent send no header, so they are refused. Codex omits the header when the variable is unset. The same applies to a user-scoped key acting for a pull agent.
  • An agent calling a sink directly over REST with execution_id: "manual" skips the refusal. The guard stops omission, not a hostile agent. Through the MCP tools, manual counts only when it comes from the header.
  • An id in the header that doesn't resolve overrides a valid tool arg. Reachable only if the chat path fails to insert its execution row; the agent then runs under a random id.
  • Rollout: pilots must run a base image with this change, or every effect whose model omits the id is refused (with an alarm).
  • Untested 422 paths: the voip and messages routes (voip also releases its claim) have no route-level test, because neither has a route harness today. The a2a route's 422 is tested.

/cso --diff: 0 findings (docs/security-reports/cso-diff-2026-09-29-2392-execution-id-injection.md).

Fixes #2392

🤖 Generated with Claude Code

…on pull agents (#2392)

The effect guard de-duplicates outbound messages, calls, shares and A2A
calls on the execution id, which reached it only as an optional value the
agent typed. Pull re-delivers the same execution after a lease expiry, so a
missing id meant a duplicate reaching a real recipient.

- Agent MCP config sends X-Trinity-Execution-Id per process:
  ${TRINITY_EXECUTION_ID:-manual} for Claude Code and Gemini,
  env_http_headers for Codex. Validator accepts that one extra shape.
- MCP server reads the header on every POST; it wins over the tool arg,
  which stays as the older-image fallback.
- effect_guard: on a pull-mode agent an effect with no usable id (absent,
  unknown, another agent's) is refused with a 422 effect_unguarded and a
  budgeted operator alarm. `manual` (a person's terminal session) and a
  lookup error proceed. Other agents proceed and log effect_guard.degraded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@obasilakis obasilakis added the ui PR touches the frontend UI — triggers Playwright e2e tests label Sep 29, 2026
obasilakis and others added 2 commits September 29, 2026 15:30
#2392)

A model typing execution_id="manual" as a tool argument (Codex, which sends no
header when unset, or an older image) would skip the pull-mode refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…L_MIGRATION_TESTING.md (#2392) — mechanical, per the merge-train note on the PR

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vybe

vybe commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

merge-train: on this train. One mechanical fix pushed to your branch (bed8db878): docs/planning/PULL_MIGRATION_TESTING.md T6.3 named an internal agent (oracle-7-ai-semi) that appears nowhere else in the public repo — replaced with "a pull-mode pilot agent".

Non-blocking notes from validation: the 422 refusal paths in routers/messages.py, voip.py (incl. the claim release), agent_files.py and internal.py are tested only at the service layer (a2a is the one route-level test); the sync test_alarm_type_registered_and_prefix_reserved is swept up by the module-level pytestmark = asyncio and warns.

@vybe vybe left a comment

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.

merge-train: batch validated on train/20260929-1926 (#3095)

@vybe
vybe merged commit b3a20b2 into dev Sep 29, 2026
25 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ui PR touches the frontend UI — triggers Playwright e2e tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants