Skip to content

feat(loop): route tool dispatch to a hand sandbox (brain↔hand isolation)#1

Merged
changkun merged 1 commit into
mainfrom
feat/loop-hand-dispatch
Jul 6, 2026
Merged

feat(loop): route tool dispatch to a hand sandbox (brain↔hand isolation)#1
changkun merged 1 commit into
mainfrom
feat/loop-hand-dispatch

Conversation

@changkun

@changkun changkun commented Jul 6, 2026

Copy link
Copy Markdown
Member

What

Adds loop.Config.ToolSandboxID. When set, the agentic loop dispatches tool calls to that sandbox (the hand) while the run's own SandboxID (the brain) stays compute-only. This is the loop-side half of brain-hand-spawn (leg 4): the brain reasons; a separate hand sandbox — created under the user's policy by the control plane — executes untrusted tool code, so tool code never shares the brain's pod/credentials.

Why this is the whole seam

runtime/loop/loop.go:441 is topos's only tool-execution site (verified: grep '.Invoke(' across the tree returns just this line; delegated/mesh sub-runs re-enter the same loop). So this one change routes all tool dispatch, including delegated sub-runs — the isolation is complete, not partial.

Empty ToolSandboxID preserves today's behaviour byte-for-byte (tools run in SandboxID).

Tests

  • TestLoopDispatchesToolsToHandSandbox: a capturing tool records the sandbox id it ran in; with ToolSandboxID set it sees the hand, not the brain. Verified to fail without the routing (runs in the brain), pass with it.
  • TestLoopWithoutHandRunsToolsInRunSandbox: default (unset) still runs tools in the run's own sandbox.
  • runtime/loop package coverage 99.2%; -race clean; golangci-lint 0 issues.

Closes

Satisfies brain-hand-spawn AC-2 ("a tool call dispatched by the loop runs in the hand sandbox, not the brain sandbox") at the SDK level. The control-plane consumer (latere.ai/x/agents, HandBroker + POST /v1/sessions/{id}/hands) wires the hand id into the loop config once a topos release carrying this field is cut.

Note

make all's cover-check reports total 89.7% < 90%, but this is pre-existing on main (identical total with and without this change) — the recently-added examples/graph + examples/delegation packages sit at 0% and drag the total. This PR's own package is 99.2%; fixing example coverage (or excluding ./examples/... from the coverage scope) is a separate concern.

Add Config.ToolSandboxID: when set, tool calls dispatch to that sandbox (the
"hand") while the run's own SandboxID (the "brain") stays compute-only. This is
the loop-side half of brain-hand-spawn (leg 4) — the brain reasons; a separate
hand sandbox, created under the user's policy, executes untrusted tool code.

loop.go:441 is topos's only tool-execution site (delegated sub-runs re-enter the
same loop), so one seam covers all dispatch. Empty ToolSandboxID preserves
today's behaviour exactly.

Tests: a capturing tool records the sandbox id it ran in — with ToolSandboxID
set it sees the hand, not the brain (fails without the routing); the default
(unset) still runs in the run's own sandbox. loop pkg coverage 99.2%.
@changkun changkun merged commit 5afdfbb into main Jul 6, 2026
1 of 2 checks passed
@changkun changkun deleted the feat/loop-hand-dispatch branch July 6, 2026 09:48
changkun added a commit that referenced this pull request Jul 6, 2026
…eleton)

The walking-skeleton leaf for interactive-session-modes mode 2 (the
laptop as a sandbox the control plane drives): let a sandbox.Provider be
served over any byte stream, with the ratified trust protections built in.

- sandbox/rpc: Serve(conn, provider) + NewClient(conn) sandbox.Provider,
  a JSON-framed codec for the unary methods (Create/Destroy/Exec/ReadFile/
  WriteFile/ListFiles/HealthCheck). Typed errors (ErrNotFound/ErrConflict/
  ErrConfined/*APIError) round-trip so callers errors.Is them across the
  wire. StreamExec is a named follow-on (client returns ErrStreamUnsupported).
- sandbox.Confine(provider, root): path-root confinement + the
  non-overridable secret deny-list (mode-2 trust protections #1/#2),
  refusing escapes/secrets with ErrConfined before the inner provider.

Exercised over an in-memory net.Pipe (no network): full round-trip against
sandbox/local, typed-error round-trips, and confined-refused-server-side.
sandbox 100% / rpc 95.4%; topos gate green (total 90.4%). Transport wiring
onto the tunnel, per-call exec consent, and StreamExec are follow-on leaves.
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