TutorialsManaged Agents

Brain and Hands

Split an agent into the part that decides and the sandbox its tools act on — the architecture, the three bindings, and where the reference implementation lives.

An agent harness has two jobs: decide what to do, and do it. The first is a model loop with your prompt and your tools. The second is bash, read, write, edit, grep, glob, apply_patch — and today it happens on whatever machine the harness is running on.

Brain and Hands separates those two. The brain keeps running wherever you run it; the hands become a sandbox bound to the conversation.

Why the split is worth it

Without itWith it
the work lands in someone's home directory and stays thereit lands in a sandbox that is reclaimed on a timer
a restarted process loses its scratch spacethe sandbox is bound to the conversation, not the process
the agent's rm -rf is aimed at the machine you develop onit is aimed at a machine that exists for this task
the filesystem is invisible unless you go and lookit is inspectable: the same sandbox is a page in the console

This is confinement, not a security boundary

The agent process still runs on your machine, with your files, your environment and your credentials. What moves into the sandbox is where the agent's tools act. An agent that can install a package or load a plugin can reach the host again — cooperative confinement, not isolation. If you need the stronger thing, run the harness itself in a container or a sandbox; the two compose, and E2B Kata is how you get the sandbox half to carry its own kernel.

Three layers, and the rule that holds them together

sdk/hands/ is one package with a deliberate seam:

LayerPathWhat it does
coretypescript/src/core/the seven tools and their behaviour contract — harness-neutral, knows nothing about harnesses
bindingtypescript/src/harness/{claude-code,opencode,mcp}/expresses "replace these built-ins with these tools" in one harness's vocabulary
daemonpython/agentbox_hands/binds a stable session id to a sandbox, and serves the workspace file API

The rule: a binding must not reimplement behaviour from core. A binding that does drifts from the contract, and the drift is invisible from the signatures.

How completely the built-ins can be taken away differs by harness, and it is worth knowing which one you are on before trusting the confinement:

HarnessMechanismHow complete
Claude Codetools: [] + disallowedTools + mcpServers + toolAliases, togethercomplete; subagents inherit it, and a test drives a real session to check
OpenCodea tool whose name matches a built-in replaces itunverified — the override appears to register alongside the built-in
Anything over MCPthe generic bindingMCP can add tools; it cannot remove a harness's built-ins

That second row is the reason the console's own assistant runs on Claude Code: an override that registers alongside the built-in leaves the built-in in play, which is the one outcome this package exists to prevent.

Plugging it in

import { sandboxToolOptions } from '@scitix/agentbox-hands/claude-code'

for await (const msg of query({
  prompt,
  options: { ...sandboxToolOptions({ sessionKey: threadId }) },
})) { … }

sandboxToolOptions returns all five tool-related options as one object on purpose: applying four of five loses the guarantee and nothing complains.

Point the config directory's tools/<name>.ts at the binding — one line each, and the filename is what does the overriding:

// ~/.config/opencode/tools/bash.ts
export { default } from '@scitix/agentbox-hands/opencode/tools/bash'

Raise tool_output in opencode.json at the same time, or the confinement leaks: OpenCode truncates an oversized tool result by writing the full text to a file on the machine running the harness and handing the agent that path. The binding's own offload writes into the sandbox instead, and needs the limit out of the way.

import { createServer } from 'node:http'
import { handsMcpHttpHandler } from '@scitix/agentbox-hands/mcp/http'

createServer(handsMcpHttpHandler()).listen(8766)

Every request must carry the conversation's own stable id in X-Hands-Session. The MCP transport's session id looks right and is not: it changes on reconnect (the conversation quietly moves to a new sandbox) and is shared when one connection serves several conversations (they end up on one filesystem).

Where the code is

The package is in sdk/hands/, with Claude Agent SDK, OpenCode and MCP bindings and its own README — which is more precise about the open questions than this page can be.

The platform's own assistant is the reference implementation, and it runs this architecture end to end: the console's floating assistant is a brain in the browser, a gateway, and a sandbox per conversation.

PieceWhere
The brain's instructionsbrain/AGENTS.md — the prompt a sandbox-backed agent runs with
The gateway that drives the harnessbrain/gateway/
The conversation UI and its tool cardsdashboard/components/assistant-ui/
The session → sandbox bindingsdk/hands/python/agentbox_hands/
The pool its sandboxes come frominstaller/helm/agent-sandbox-hub/values.yaml — assistant.sandbox names the Env and the image

Reading those five together is the shortest path from "I have a harness" to "my harness's tools act on a sandbox": the prompt shows what the brain is told, the gateway shows the loop, and the hands package shows the tool layer underneath.

The patterns behind it

The split is not ours to invent — it is where the ecosystem has been heading, and these are the pieces worth reading alongside it:

See also

On this page