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.

Tools, MCP, and what an agent may reach

The fastest way to give an agent real capability is to connect it to tools, and it is also the fastest way to give an attacker one. This is the part of the design most worth reading before you trust it with anything.

The agent has no tools by default

The CLI agents this drives — Claude Code, Codex, the Gemini CLI — are run with no tools granted at all. They are used to get a structured proposal back, not to run an agent session. Every file change is applied afterwards by code we control, into a throwaway checkout.

MCP is not supported yet, on purpose

Those same CLIs support the Model Context Protocol, and an MCP server configured in yours will not be reachable from an agi-cos run. That is a position rather than an oversight: an MCP tool that can act directly routes around the rule the whole architecture rests on, which is that a model produces proposals and deterministic code decides.

What MCP support will have to look like

A permissioned connection, scoped to an employee, whose calls come back as proposals and are committed by the same code path as everything else — with the same approval request, bound to a hash of the exact call, when there is no grant covering it. Anything looser would be a second way to change the world that the evidence gate never sees.

An MCP server is untrusted input twice over

It is code you did not write, returning text a model will read. Both halves matter: its output has to be fenced as untrusted the way repository text and client messages already are, and its ability to act has to be enumerated rather than assumed. Neither is free, which is why it is not shipped half-done.

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.