Concepts
The trust boundary
Here is the question the whole architecture answers: the agent has to do real things — send mail, move money, write to a client's ledger — and you cannot trust it with the credentials that make those things possible. Not because it is malicious, but because anything it reads can instruct it, and everything it does is one bad inference away from being wrong in public.
The usual answer is to make the model better behaved. Mycel's answer is to move the capability out of its reach. The agent gets intent. The harness holds the capability. A human sits on the line between them.
SANDBOX (untrusted) │ HARNESS (trusted)
│
agent + skills + knowledge │ connections, vault, policy
opaque nonce ──── POST /v1/internal/actions/:capability
│ │
│ approval gate ── human approves / edits
│ │
│ real credential ──► the providerWhat crosses the line, and what does not
| Into the sandbox | Never into the sandbox |
|---|---|
| Skills and knowledge for this wedge | Connection secrets — Stripe keys, mailbox tokens |
| The task input, and uploaded documents | MYCEL_DATABASE_URL, MYCEL_SECRET_KEY, DAYTONA_API_KEY, any observability keys |
| An action-grant nonce and a set of proxy URLs | The founder API key (msk_…) |
| A gate token for the OpenCode plugin | Any other project's data |
The local sandbox does not simply inherit the process environment — it is given HOME, PATH, LANG, TERM and a handful of equally boring variables. printenv inside the sandbox is deliberately dull.
The exception you have to know about. By default the model provider key — ANTHROPIC_API_KEY or equivalent — is passed into the sandbox, because OpenCode calls the model directly. Set MYCEL_PROXY_MODE=1 and model calls go through /v1/internal/llm/* instead: the sandbox holds a nonce, the harness holds the key, pins the model, and caps max_tokens at MYCEL_MAX_TOKENS. Connection secrets are unaffected either way — those never cross the line at all.
Three doors through the boundary
Everything the agent can do to the outside world goes through one of three endpoints, and they differ in exactly one dimension: how much damage they can do.
| Reads | Actions | Tool gate | |
|---|---|---|---|
| Endpoint | /v1/internal/reads/:capability | /v1/internal/actions/:capability | /v1/internal/gate |
| Human gate | No | Always, unless policy matches | Always, unless policy matches |
| Constrained by | GET only, host fixed by the connection, 256KB cap, 200 per task | Granted connections for this task only | Tool name matching a pattern list |
| Reached by | Agent, directly | Agent, directly | OpenCode plugin, automatically |
Reads are ungated because reading is recoverable and because gating them would make every task unusable. They are still scoped: the agent supplies a relative path, never a host, so it cannot point a credentialled request at a server it chose. Absolute URLs, protocol-relative URLs, .. and header-injection attempts are all rejected.
The tool gate is the belt to the action proxy's braces. A plugin inside the sandbox intercepts tool calls whose name contains send, email, message, pay, charge, refund, delete, deploy, book or transfer, plus any action your wedge marked required. Matching is substring and case-insensitive, so it over-triggers rather than under-triggers — a tool called notebook_read will stop for approval. That is the correct direction to be wrong in.
If the gate cannot be reached at all, the plugin throws and tells the agent not to retry. It fails closed.
Which sandbox you are actually running
MYCEL_SANDBOX | What it is | Use for |
|---|---|---|
local | A temp directory and bash -lc on your host kernel | Development. Not a security boundary. |
docker | A container per task, port bound to 127.0.0.1, removed on completion | The realistic default for self-hosting |
daytona | Remote sandbox with auto-stop | Cloud, and the least exercised of the three |
If MYCEL_SANDBOX is unset, the presence of DAYTONA_API_KEY selects daytona, otherwise local. A typo in the value falls through to local without complaint — so read the boot banner, which prints sandbox=…, rather than trusting your env file.
What the boundary does not give you
- It is not a claim that the agent is safe. It is a claim that the blast radius of a bad decision is bounded by what a human approved.
task.toolsis recorded, not enforced. Tool restriction is the gate plus a coarse bash denylist, not a fine-grained ACL.- The agent can still be wrong inside its permissions. A read it is entitled to make can return text that manipulates it. That output still has to clear the gate before it becomes an action — which is the point.
North star versus what ships
kernel/docs/ARCHITECTURE.md describes a larger system — durable mid-run resume, an MCP hub, task-scoped JWTs, deeper RBAC. That is the destination. Where those docs and server.ts disagree, the code wins. Build against routes that exist.
What to read next
Approvals — the human half of the boundary, and the only part of it your customers will ever notice.