Skip to content

Security model ​

The design assumes the agent can be wrong or hostile. So the agent is given no power: it cannot hold a key, push code, widen its own permissions or mark its own work as passing. Everything that matters is checked by the platform against signed records, and anything missing or unclear is refused.

The rules, in plain words ​

  • Who is calling is proven by certificate. Runners, services and operators connect with a client certificate (mutual TLS). The certificate says who they are and which workspace they belong to, and the request body cannot choose another.
  • People sign what they approve. A merge needs a person's signature on the exact commit. The signature is bound to that version of the record, so changing the work invalidates it. Signing needs a fresh passkey check.
  • The agent may only touch its scope. Each task has allowed paths. A write outside them stops the task. Scope can grow only by a signed approval, and only up to a signed ceiling.
  • Untrusted code runs in a box. Agents and gates run in gVisor containers on a private network with no internet. Dependencies come from the mirror.
  • Results are signed evidence. A gate result counts only if the runner signed it, with a key an administrator approved, bound to the exact commit, base and forelead.env hash. A status the worker merely reports is never trusted.
  • Credentials never enter the sandbox. Model keys stay in the broker. GitHub access stays in the control plane. The agent gets no tokens and no push rights.

Certificates and sign-in ​

  • Runner certificate: at most 24 hours, one URI SAN spiffe://forelead/runners/<runner-id>/workspaces/<workspace-id>. Renewed certificates are read on the next handshake.
  • Operator certificate: 24 hours, SAN spiffe://forelead/workspaces/<workspace-id>, reissued before expiry (see Certificates).
  • Task certificate: issued by the broker against an active lease, bound to the task and workspace, valid until the lease expires. The model bridge uses it to reach the gateway. The key stays outside the task container.
  • People: the browser needs a client certificate and a session. Sign-in is an OpenID Connect provider first, then a passkey with user verification; no session exists until both pass. An identity links to a user only through an invite.

Records, signatures and evidence ​

  • Records are append-only: each change is a new version with a content hash. A signature names the signer, meaning (for example merge:approved or gate:pass), version and hash. The audit log is a hash chain per workspace. See Data model.
  • Gates run once against a fresh copy of the committed tree, with a fresh cache each. The workspace has no .git, so a gate cannot depend on commit metadata. The published tree must equal the gated tree.
  • A runner without an approved evidence key has everything it signs refused. Keys are enrolled, rotated and revoked by an administrator, and each action is audited.
  • Shadow review findings never block publish. "Blocked by review" in the UI is a display rule; the merge control is the enforcement, and it fails closed if reviews cannot be read.

The job network rule ​

Every job gets its own private Docker bridge network that is internal (no route out). Its only allowed peers are the job's proxy and the current gate's own sidecars (for example the test database). Before a service joins, sandbox.VerifyNetwork inspects the network and refuses anything else.

The consequence: all gates of a job share that one network, so they run one at a time. Two gates at once would each look like an unexpected peer to the other and be refused. Running them together would need one network per gate. This is the rule issue #1124 ran into.

Technical notes ​

  • The runner runs the gate and agent containers with --runtime=runsc and --pull=never; a missing image pin fails the job. The runner checks os.OpenRoot symlink confinement at startup and refuses to run if it fails.
  • Host-side Git on an agent workspace goes through internal/gitsecure (no hooks, filters, fsmonitor or replace refs; gitlinks refused).
  • The per-record signature POST returns 501 by design (decision 0018). Signing goes through POST /v1/signatures, which needs a verified passkey event.
  • Decisions are in the design repository (forelead-ai/design, decisions/), for example 0009 (governance), 0010 (foundations), 0011 (human approval on every merge).

Sources ​