The security model
Why a secret never enters a sandbox, a container or an agent's own environment.
The engine trusts the configuration and nothing it runs. A model, a command, a tool or an external agent may be wrong or hostile. A secret that reaches one of them has leaked. Every rule below follows from that.
The token
A private repository is cloned with the token named by Workspace.tokenEnv. The token reaches
only the git process on the host, through its environment and GIT_ASKPASS. It never appears
in an argv, in a remote URL written to disk, in a log, in an event or in a sandbox.
Push is a deterministic engine step on the host too. builtin:open_pr pushes the integration
branch with the same GIT_ASKPASS, and the forge opens the pull request (see
Git and pull requests).
The sandbox
No agent-generated command runs outside the Docker sandbox. The container:
- has no network by default.
- has all capabilities dropped and
no-new-privileges, with CPU, memory and pids limits. - runs as a non-root user.
- mounts
/workspaceas its only writable path, and never mounts the Docker socket.
It gets no environment variable and no secret, except the variable names an acp agent
declares. The Docker adapter passes -e NAME and reads the value from its own environment, so the
value never enters an argv.
Egress
Egress is an allowlist. A sandbox without sandbox.egress keeps --network none. With egress, it
joins a per-ticket internal network and reaches only the listed hosts, through a squid proxy
attached to the default bridge.
Every other host is refused, and so is a direct connection that ignores the proxy. The sandbox,
the proxy and the network carry the afe.ticket label, and go away with the ticket (see
Docker sandbox).
The forge and the agents
Git and forge tokens never enter containers. The token reaches only the git process on the
host, through its environment and GIT_ASKPASS. It never appears in an argv, in a remote URL
written to .git/config, in a log, in an event or in a returned output.
The forge token (a fine-grained GitHub PAT named by Workspace.tokenEnv) reaches the GitHub API
only in the Authorization header. Push, pull requests and merges are deterministic engine steps
on the host. Agents never get gh, forge CLIs or forge tokens, and a merge happens only after a
human approval.
Tool paths
.. and absolute paths are refused. Every path component is opened from the node's root with
dir_fd and O_NOFOLLOW. Every symlink on a path is refused, even one that points inside the
root, or one created inside the sandbox while a call runs.
The browser session
POST /auth/login compares the token with hmac.compare_digest, and sets a session cookie
signed with a key derived from the token. The cookie is HttpOnly, SameSite=Strict, Path=/,
and Secure behind HTTPS. Nothing is kept server-side, a new token ends every session, and the
cookie never reaches a log.
The /rpc handshake accepts the cookie only when the request's Origin is the server's own. The
CLI keeps using the bearer token (see Web UI).
Operations
/livez, /readyz and /metrics ask for no token, so they bind to 127.0.0.1 unless
--ops-host says otherwise. Readiness names the failing dependency, never its error text. No
metric label carries a ticket id, an input or a secret.
OTLP header values go only into the request, never into a span, an error or a log.
Secrets
No secret is written in a manifest, a log, an event or a metric. A manifest names the environment
variable that holds it (apiKeyEnv, tokenEnv). Connection strings are read at startup and never
echoed. Values that look like secrets are dropped from memory and redacted from errors.