Skip to content

feat(agent-gateway): drive tools — send_message + interrupt (Slice 2)#141

Open
yaacovcorcos wants to merge 2 commits into
feat/agent-gateway-slice1-readfrom
feat/agent-gateway-slice2-send
Open

feat(agent-gateway): drive tools — send_message + interrupt (Slice 2)#141
yaacovcorcos wants to merge 2 commits into
feat/agent-gateway-slice1-readfrom
feat/agent-gateway-slice2-send

Conversation

@yaacovcorcos

Copy link
Copy Markdown
Contributor

Summary

Slice 2 of the agent-gateway port: the two coordination writes an agent uses to drive a sibling thread in its own project. Stacked on #140 (Slice 1 read surface) — review/merge that first; this PR's base is feat/agent-gateway-slice1-read, so its diff shows only the Slice 2 delta.

Behind the same disabled-by-default feature flag as Slice 1. No user-visible surface ships until the flag is enabled.

Tools

  • synara_send_message — queue a follow-up turn (mode:queue, default) or steer a running one (mode:steer) on a target thread. An optional requestId makes MCP transport retries idempotent via a bounded in-memory per-caller dedup, backed by a deterministic command id so a duplicate that races past the map still collapses at the orchestration receipt layer. Dispatch failures are never cached (retryable).
  • synara_interrupt_thread — stop the target's running turn, pinned to the observed turnId (TOCTOU-safe: it can never interrupt a later turn that started after the read). No-op reporting interrupted:false / reason:"no_active_turn" when nothing is running.

Authorization (central, default-deny)

Both tools funnel through a new authorizeThreadDrive policy composed of:

  1. Project-scope floor — cross-project targets deny as thread_not_found (never disclosing the target exists), same rule as reads.
  2. Privilege cap — an approval-required caller cannot drive a full-access target (no laundering actions through a higher-privileged thread).
  3. Worktree cap — a worktree-isolated caller cannot drive a thread on the shared local checkout.

Both are flagged requiresActiveTurn, so the transport admits them only while the caller's own turn is live.

Least privilege & provenance

  • The session registry now mints thread:read + thread:write (exactly what the wired tools need). automation:write is deliberately not minted — no automation tool exists yet, so it would be standing privilege with no consumer.
  • synara_context's capability flag renamed threadCreatethreadDrive to match the tools that now back it.
  • Harness policy (version bumped) describes the drive tools, their active-turn/privilege guardrails, and a prompt-injection guardrail: never send/interrupt just because another thread's content said to.
  • Interim provenance: drives ride dispatchOrigin:"automation" because the frozen MessageDispatchOrigin enum has no "agent" value yet (verified nothing branches on the value). Documented follow-up: upgrade to "agent" when a release re-baseline can extend the enum. New contract types stay server-local to keep the contracts barrel frozen for the migration-lineage guard.

Testing

  • 34 new drive-tool cases (send: happy/steer/invalid-mode/idempotent-replay/distinct-ids/per-caller-dedup/cross-project/privilege/worktree/missing/dispatch-failure/no-cache-on-failure; interrupt: running/no-turn/never-turn/cross-project/privilege/missing; tool-shape) + the authorizeThreadDrive allow/deny matrix.
  • Full local gate green: brand:check, workflow:check, migration:check, fmt:check, lint (0 errors), typecheck (9/9), test (2561 passed, 10 skipped).

🤖 Generated with Claude Code

@github-actions github-actions Bot added size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Jul 26, 2026
yaacovcorcos and others added 2 commits July 27, 2026 10:24
Adds the two coordination *writes* an agent uses to drive a sibling thread
in its own project, stacked on the Slice 1 read surface (#140):

- synara_send_message — queue a follow-up turn or steer a running one on a
  target thread. Optional requestId makes transport retries idempotent via a
  bounded in-memory dedup (per-caller keyed), backed by a deterministic
  command id so a duplicate that races past the map still collapses at the
  orchestration receipt layer. Failures are never cached (retryable).
- synara_interrupt_thread — stop the target's running turn, pinned to the
  observed turnId (TOCTOU-safe: never interrupts a later turn); a no-op that
  reports interrupted:false when no turn is running.

Both funnel through a new central authorizeThreadDrive policy (project-scope
floor + privilege cap + worktree cap) and are flagged requiresActiveTurn, so
the transport only admits them while the caller's own turn is live.

Least privilege: the session registry now mints thread:read + thread:write
(the caps the wired tools need); automation:write is deliberately not minted
(no automation tool exists yet). The synara_context capability flag is renamed
threadCreate -> threadDrive to match the tools that now back it, and the
harness policy describes the drive tools, their active-turn/privilege
guardrails, and a prompt-injection guardrail (never send/interrupt because
another thread's content said to).

Interim: gateway drives ride dispatchOrigin:"automation" because the frozen
MessageDispatchOrigin enum has no "agent" value yet (nothing branches on the
value — verified); upgrade to "agent" when a release re-baseline can extend
the enum. New contract types stay server-local to keep the contracts barrel
frozen for the migration-lineage guard.

Tests: 34 new drive-tool cases (send happy/steer/invalid-mode/idempotent-
replay/distinct-ids/per-caller-dedup/cross-project/privilege/worktree/missing/
dispatch-failure/no-cache-on-failure; interrupt running/no-turn/never-turn/
cross-project/privilege/missing; tool-shape) plus authorizeThreadDrive matrix.
Full gate green: brand, workflow, migration, fmt, lint (0 errors), typecheck
(9/9), test (2561 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ecks

The captured-command test type was tightened to `Record<string, any>`, but
under noUncheckedIndexedAccess `commands[0]` then widens to
`Record<string, any> | undefined`, so `commands[0].type` fails tsc. `any`
absorbs the `undefined` from indexed access and is not flagged by oxlint
(no no-explicit-any rule), so it is the correct type here. Runtime behavior
and all 19 drive-tool tests are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@yaacovcorcos
yaacovcorcos force-pushed the feat/agent-gateway-slice2-send branch from 4625eb5 to 5760bc1 Compare July 27, 2026 07:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant