Containment is structural, never a prompt rule. The exceptions are known and accepted.
The invariants
- Per-run App tokensEvery LLM stage gets a one-hour, repo-scoped token. Granted permissions must equal requested ones at mint time.
- No
workflows: writeCI runs agent code with repo secrets before a person sees it, so agents can never edit workflows. - You control the mergeYour branch rules decide how code reaches
mainand release branches: approvals, CODEOWNERS, green CI. The App authors each PR, so it cannot approve it. - The repo selects, the operator permitsMCP servers, E2B runtimes and egress need an allowlist entry in the database. A repo file alone grants nothing.
- Workspaces stay in their rootAgents run in worktrees under the adapter's
local_root. Any path that escapes it is refused. - One secret storeThe database's
secrettable, envelope-encrypted. The App key lives at~/.config/factorial/app.pem, mode 0600.
Per-run tokens
Every run gets a new GitHub token. The token covers one repository and lasts one hour. Factorial asks GitHub for a set of permissions and then reads the token back. If the token carries more or less than the request, Factorial refuses to start the run.
An agent manifest can request its own set of permissions. An operator can set a ceiling for a project. Factorial refuses a manifest that asks for more than the ceiling. An interactive run that lasts longer than one hour parks and resumes with a new token.
What stops a bad merge
Two rules stop a bad merge. Neither depends on the agent.
- The branch
mainneeds a human approval before a merge. You set this protection on GitHub, as Protect the default branch shows. Factorial does not set it. - The App authors every agent pull request. GitHub does not let an author approve its own pull request, so the App cannot approve the pull request it opened.
A gate never counts a review from the App, even when approvers names it. Another bot counts only when approvers names it. The App does not hold workflows: write. CI runs agent code with the secrets of the repository before a person sees it. An agent that could edit workflows could run any code there.
Secrets
The secrets block of an agent manifest lists what to mint for that agent. It is not a boundary. A runtime can put every stored secret in front of every run. Store only the secrets that you accept in every run.
MCP servers and egress
An agent can use MCP servers. The repository selects a server, and the operator allows it. The allowlist is in the project settings, in the database. No file in the repository can change it. An agent on a host runtime can reach the database, as Costs the design accepts says. An empty allowlist allows nothing. An entry must match the server file, by the rules that MCP servers gives.
A header or an environment value can refer to a secret as {secrets.KEY}, and never holds the value. Factorial refuses a server that points at its own MCP address. A local command runs beside the keys of the run, so pin its version. Prefer an HTTP server. MCP servers explains how to allow one.
On E2B, the egress allowlist limits the hosts that a sandbox can reach. E2B adds the model key and the GitHub token to the requests of the agent, so the agent cannot read them.
Who may decide a gate
A gate counts a review only from a reviewer that it qualifies. That is a person named in approvers, or a person who holds the approverPermission role or a stronger one on the repository. GitHub reports the role at every poll, so removing access takes effect at once. An empty approvers list means anyone only when approverPermission is not set.
A review that does not count is not silent. The ticket gets a comment that names the reason. An agent cannot change these settings. They are in the config, and sync accepts only a commit that Factorial wrote or that a person merged. See What guards the branch.
Previews and served pages
A preview shows work from an agent inside the dashboard. The page comes from another origin, so its scripts never run with the cookies of the dashboard. The frame allows scripts, forms and the same origin, and nothing else. It allows no top navigation, no popups and no dialogs.
A page that an agent writes is inert. It runs no script, loads nothing from the network and submits nothing. It has an opaque origin, so it holds no dashboard cookie.
Never type a credential into a preview or into a page. An agent that follows an injected instruction can draw a fake sign-in form. A link to a page works for at most two hours. With Google sign-in on, the reader must also sign in.
A sandbox that serves a preview is public while it lives, but only on the ports that the step declares. The services run as a separate user behind a firewall that keeps them from the credentials of the run.
The terminal
An interactive run lets a person watch the agent and type into it. An operator turns the terminal on or off for the deployment.
- Every viewer attaches read-only. A run has one writer seat. A viewer takes it on purpose, and it frees after two minutes without typing.
- Factorial records who typed and how many bytes, and never the content.
- The connection must come from the dashboard origin. A ticket for one viewer of one run lasts 30 seconds and works once.
- A viewer never wakes a paused sandbox.
- Factorial stores nothing from the screen.
On a host runtime, write access is a shell as the user of the worker. Give it only to people you trust with that user.
Costs the design accepts
These costs are known and accepted.
- Agents and Factorial share one permission profile. The rule that agents never add labels is a prompt rule, not an enforced one. The merge stays blocked either way.
- The sandbox of a preview is public for its life, on the declared ports only. A live agent that an injection has taken over can relay its own credentials to anyone who holds the link, until the run parks.
- An MCP client can press Commit. The check on the server refuses any change under
.factorial/agents/or.factorial/skills/from an MCP client. It also refuses any change to the approvers of a gate. Those stay with the dashboard. - On a host runtime, an agent runs as the same user as Factorial. It can read the App key, the master key and the database. It can also reach the sessions and the keys of the other runs on the same machine. Use an E2B runtime for containment.