Skip to main content

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 /workspace as 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.

See also​