agi-cos
Security

Assume the model is the attacker.

A coding agent reads untrusted repositories, fetches untrusted pages, and produces output that other parts of the system will act on. Treating any of that as trustworthy is the mistake this design is built around not making. The model gets to propose; it never gets to decide, and it never gets the keys.

The trust boundary

There is exactly one untrusted component, and it is named as such in the architecture: the runner. Everything about it is arranged so that the worst case is a wasted container.

Isolation

The runner is the only untrusted component

Agent code executes in a throwaway container: non-root, no network, read-only root filesystem, no capabilities, and only the task's worktree mounted. It is destroyed when the task ends.

Credentials

Keys never reach the sandbox

Model credentials stay in the worker process. The runner receives a work package and a mounted worktree and nothing else. Its environment is built from empty rather than filtered from the host's — forgetting to add a variable breaks a build; forgetting to remove one leaks a key.

Injection

Repository text is data, never an instruction

Anything a model, a repository or an outside party wrote is fenced as untrusted content with its provenance attached. A client's own words in a support ticket are quoted to the agent as data and labelled as such on screen.

Authority

A model's output cannot mutate state

Agents return proposals. Deterministic code validates each one against that employee's permissions and commits it, or refuses it. A model writing “budget approved” produces a row for a person to look at.

Shell

There is no shell to inject into

Commands are argv arrays and the runner image contains no shell. An employee asks to run something and code decides: programs are limited, git is read-only, and every path is re-resolved against the worktree root.

Approval

Anything ungranted raises a question, not an error

A command with no matching grant creates an approval request bound to a SHA-256 of the exact argv. Approving “npm ci” is not approving “npm ci --ignore-scripts”, and consuming an approval is a conditional update so two workers cannot both use it.

Data

Self-hosted, with no telemetry

It runs on your machine against your database. There is no account, no hosted control plane, and nothing phones home. Your code, your briefs and your model keys do not leave your infrastructure.

Preview

Model-written files are served sandboxed

A captured preview is served under Content-Security-Policy sandbox in an origin of its own, with same-origin deliberately absent. Its manifest is the allow-list, and every file is re-verified against its recorded hash before it is served.

What the container actually gets

PropertySetting
UserNon-root
NetworkNone
Root filesystemRead-only
Linux capabilitiesAll dropped
MountsThe task's worktree, and nothing else
EnvironmentBuilt from empty, never filtered from the host
ShellNot present in the image
Database driverNot present in the image
Provider SDKsNot present in the image
LifetimeDestroyed when the task ends

The last three are asserted by tests that fail the build if a database driver or a provider SDK ever appears inside the image. Building the environment from empty rather than filtering the host’s is the deliberate direction of that trade: forgetting to add a variable breaks a build loudly, forgetting to remove one leaks a key quietly.

Prompt injection, specifically

Untrusted text is fenced and labelled

Anything written by a model, a repository or an outside party is wrapped in an untrusted-content block carrying its provenance. A client’s own words in a support ticket are quoted to the agent as data, and drawn on screen as a quotation marked treated as data, never as an instruction.

A successful injection still cannot do anything

This is the part that matters. Suppose the injection works and the model is fully persuaded. It still only emits proposals, into a container with no network and no credentials, and deterministic code decides whether each one is permitted. The blast radius of a perfect prompt injection is a refused proposal and a wasted attempt.

It cannot pick its own reviewer

Reviewers come from the org chart. A planner that could name its own reviewer could name itself, and the evidence gate would be decorative. The same applies to who hears about a blocker: an employee escalates to its manager and cannot choose a friendlier audience.

It cannot email your customer

Client replies are drafted and then wait for a person. The database refuses an outbound message that is unapproved or unkeyed, so “the agent sent something odd to a customer” is not a failure mode that exists.

Your data

Where it livesYour machine, your PostgreSQL, your git repositories. There is no hosted control plane in the self-hosted product.What leavesOnly the requests you cause, to the model provider you configured, with the key you supplied. Nothing else is sent anywhere.TelemetryNone. The application does not phone home.AccountsThere are none. There is one user — you — and you are already signed in.AuditabilityEvery run, every proposal, every refusal and every token is a row you can query. You do not have to take the interface’s word for any of it.

What we are not claiming

This is a young open-source project, not a certified platform. It has not been through a third-party penetration test or a compliance audit, and it holds no certifications. Saying otherwise would be the same category of claim the product exists to refuse.

The container isolation is Docker’s, which is a strong boundary and not a hypervisor one; microVM isolation is deliberately out of scope for now. RUNNER_MODE=local exists so a first run does not need Docker, and it runs commands on your machine with your privileges and no sandbox — it warns loudly and it should only ever meet code you already trust.