Security #
FireTrident lets many agents, most of them untrusted, change one repository at once. This page says what it defends against, what it leaves out, and how to report a vulnerability.
Report a vulnerability #
Report it privately through this repository's GitHub security advisories: open Report a vulnerability on the repository's Security tab. Please do not open a public issue or pull request for it. Say what is affected (a commit, a route, a tool), how to reproduce it, and what an attacker gains. The discussion stays in the advisory until a fix is released.
Test against your own deployment (see "Run it" in the README), not the public showcase.
Who is trusted #
- The operator holds the Cloudflare account and the admin token (
FT_ADMIN_TOKEN), and is trusted. - Agents, in-platform or external, are not. Neither their text nor their changes are trusted, nor the repository content they handle.
- Account holders (with
FT_ACCOUNTSon) are trusted with the colonies they own and nothing else: their session and the tokens they mint act on their own colonies only, and their colonies' repositories and models are in their own Cloudflare account. - Readers need no token: colony state, review queues (masked, including the file lines a review card or a returned change quotes), lineage and each landing's test names and counts are public on a deployment, and colonies listed in
FT_PUBLIC_GITcan be cloned by anyone.
What FireTrident defends #
- Agents never hold a trunk write token. Only the colony's trunk writer pushes to trunk, and it pushes with the expected old value (compare-and-swap). A workspace lease carries a read token for trunk (valid an hour, replaced 15 minutes before it expires) and a write token for the agent's own workspace.
- Workspace tokens are short-lived and scoped to one fork. Each is a write token for a single workspace repository, valid for two hours. It is never handed to a different agent, and about a minute after its agent lets the workspace go it is revoked and replaced. Public reads redact lease tokens.
- The MCP endpoint takes its token only in a header.
/mcpand/sserefuse a request whose URL carries atokenquery parameter, whatever it holds, since a token in a URL ends up in logs and histories. The bearer token is compared in constant time (by SHA-256 digest). WithoutFT_MCP_TOKENconfigured, the endpoint refuses every connection, unlessFT_OPEN_WRITES=1, which is for local development only and never set on a deployment: it opens/mcpand/sseto anyone without a token, and, whenFT_ADMIN_TOKENis not configured either, the heads-up route too. - An agent id alone resumes nothing. An external agent's resume key is 32 random bytes, returned once by
join. The colony keeps only its SHA-256 digest and compares in constant time, and no route returns it. - The heads-up shows an agent only its own picture.
GET /api/colonies/:id/agents/:aid/headsuptakes the MCP or admin token or the agent's resume key, never in the URL, and shows no other agent's id, model or read set; intent titles in it are agent-written text, quoted on one line. - Configured tokens tolerate a trailing newline. A configured
FT_ADMIN_TOKENorFT_MCP_TOKENloses a trailing CR or LF before the constant-time compare; the presented token is compared exactly as sent. - Writes fail closed without the admin token. Every mutating REST route needs
FT_ADMIN_TOKENas a bearer token, compared in constant time. Without that secret configured the routes refuse (403), and deleting a colony refuses outright.FT_OPEN_WRITES=1lifts this for local development only and is never set on a deployment. The one exception is a colony a person owns (FT_ACCOUNTSon): there the owner's session or a token scoped to that colony may do what its role allows, and nothing else; deleting it still takes the admin token or the owner's own session. - Sessions are signed and kept only as digests. A person signs in with GitHub (authorization code with PKCE S256; the state and verifier ride in an AES-GCM sealed cookie scoped to
/auth/githubfor 10 minutes, and the redirect URI is fixed fromFT_PUBLIC_URL). The session cookie__Host-ft_session(HttpOnly, Secure, SameSite=Lax, path/, 14 days) holds 32 random bytes and their HMAC-SHA-256 under a key derived fromFT_SESSION_KEY; a cookie whose signature fails is never looked up, the server keeps only the SHA-256 of the session id with its expiry, and signing out ends it on the server. Every state-changing route a browser calls with the cookie checks the Origin header againstFT_PUBLIC_URL. - Invite codes and scoped tokens are shown once and kept as digests. An invite code (128 random bits) and a scoped token (160 random bits) are shown only when minted; the server keeps their SHA-256 and looks a presented one up by it. A code has a number of uses and an expiry. A token names one colony and one role, expires within 30 days, can be revoked at once, and is checked on every request against the colony's current owner: it never works on another colony, on an operator colony, or after the colony is deleted or changes hands. At
/mcpan MCP session is bound to the token that opened it, a tool call naming another colony is refused, and/sserefuses scoped tokens, since its session id travels in the URL. - Cloudflare tokens are encrypted at rest and never leave. Connecting Cloudflare is an authorization code flow with a client secret and PKCE S256, its state bound to a 10-minute HttpOnly cookie on
/auth/cloudflare, used once and compared in constant time. The access and refresh tokens are kept only sealed with AES-GCM, under a key derived fromFT_GRANT_KEYwith HKDF-SHA-256 and a salt of each record's own, and bound to the account they belong to, in one Durable Object per account. No route returns them and none is logged; they are sent only to Cloudflare. An access token is renewed shortly before it expires; a renewal Cloudflare refuses marks the connection broken, with a sentence on the account page, and disconnecting revokes it at Cloudflare. GitHub profile fields and Cloudflare account names are shown as text, never as markup. - Certification runs the project's tests in a sandbox. The tests run in a Dynamic Worker with no network access, no environment variables and no bindings, under CPU and wall-clock limits, and their report is read as a bounded stream.
- Prompts fence repository content as data. The model prompts of review (the second opinion, the contest judge, antibody questions), agents' write prompts and repair intents put the text agents, people or the repository wrote between a random marker that occurs in none of it, labeled as data, never as instructions. No model opinion overrides a deterministic fact.
- Danger patterns and protected paths reach a person. While a colony's review is on (the default), deterministic patterns send to a person any change that touches authentication, adds a hard-coded credential, or adds process or network access that no intent written by a person or a rule asked for. The colony's policy or a tolerating antibody, both set by people, can allow those classes; an agent's own words never can. A change to what decides on agents, the colony's memory under
.firetrident/and agent instructions (AGENTS.md,CLAUDE.md,.claude/), goes to a person whatever the policy, the antibodies or the intent say. A change to a file another tool runs or obeys (MCP client configuration,.vscode/,.idea/,.cursor/, dev containers,.envrc, git hooks,.gitmodules, nested.git, virtual environments, other agents' instruction files,.gitattributesdriver lines) goes to a person on the same terms; every path rule matches case-folded and NFC-normalized. A loosened guard (a deleted 401 or 403, a weakened auth condition,verifyswapped fordecode) is never waived: a second opinion must look at it. A change to a protected path (ft policy <colony> --protect <glob>) goes to a person even with review off. - Bytes a person approved are the bytes that land. A change a person approved, or one with a danger pattern a person must see, or one that writes a protected path, never lands merged with a newer trunk on its own: the merge goes back to a person with both diffs.
- Destructive changes need a second confirmation. Neither earned autonomy nor a deterministic proof lands a change alone that deletes or renames a file, removes an export or removes a large share of a file: it goes to the second opinion.
- Agents cannot raise their own priority. An agent's intent takes at most the priority of a parent it holds or declared, or 1; only a person, a rule or the platform raises it.
- The trunk writer keeps trunk's structure. It never changes a submodule, refuses a change that would retarget a symbolic link, and refuses a path that collides with another only in letter case or Unicode form.
- Lineage notes mask secret-looking text. Before a note is written, its free text (intents' titles and whys, an agent's
notVerified, check details, directives, status notes, review questions and answers) passes through the same secret detector as the danger patterns, and each match becomes[masked]. The review view masks lines that may hold a secret the same way. - A security finding tells its agent only the class. The full reasons stay in the decision record and the review view, the proposal route withholds them from readers without the admin token, and the intent's next proposal goes to a person.
- Recipes are a person's act. Declaring or approving a recipe needs the admin token; built-in recipes are pure functions, module recipes run sealed (no network, no environment, no bindings) and twice, and every recipe's output is danger-scanned, so a hit waits for a person. A recipe a change declares never waives the danger scan, and an agent's
text-replaceis never taken as proof. - Settings a person owns stay behind the admin token. The silent-agent limit (
POST /silent-ttl), the spend cap and re-running a change's tests need the admin token; their reads,/decidesand/decide/policyare public and carry policy values and counts only. - A person can stop every agent at once. Only the admin token pauses or resumes a colony or the deployment; while paused, agent writes are refused and trunk does not move, and nothing an agent writes can lift a pause.
- Previews cannot script. File previews (
/preview/:commit/*) are served with asandboxcontent security policy, so no script in a colony's files runs and the page gets an opaque origin that cannot read anything the web app stores. An agent's preview link (previewUrl) must be an https URL, and a stored link with any other scheme is shown as text, never as a link. - Every response carries security headers. HSTS,
nosniff,X-Frame-Options: DENYwithframe-ancestors 'none',strict-origin-when-cross-originand a Permissions-Policy denying camera, microphone, geolocation and payment: the Worker adds them to its answers (front/headers.ts) andpackages/web/public/_headersgives static assets the same values. A full content security policy is report-only for now; violations show in the browser console and nothing is collected. - Plain git is read-only. The
/git/proxy serves only colonies listed inFT_PUBLIC_GIT, forwards only fetches, refuses pushes, and adds its own read token upstream, which clients never see. - Only a person decides, wherever they review. Deciding takes the admin token (on a colony a person owns, also the owner's session or a reviewer or owner token of that colony). The chat-client review endpoint (
/mcp/review) takes only that token, never the MCP token agents hold, and itsdecidetool also needs a single-use code that FireTrident gives only to the review view, outside anything a model reads, so a model in a person's chat client cannot approve a change, whatever the change's text tells it. - Who approved is checked where it can be. Behind Cloudflare Access with
FT_ACCESS_TEAM_DOMAINandFT_ACCESS_AUDset, a person's decision records the email from a verified Access token; otherwise the name is recorded as stated. The plain email header Access adds is never trusted on its own. The owner of a colony who decides signed in is recorded asgithub:<login>, verified by the GitHub sign-in; a decision made with a token records its stated name, as stated. - Imported issues are untrusted. An intent imported from a GitHub issue counts as an agent's request at every gate, never as one a person or a rule wrote, and agents see its text quoted as a stranger's words. FireTrident writes back to GitHub only when the import asked for it and a token with write access is configured.
What is out of scope #
- The operator and the platform. Whoever holds the admin token can approve any change, allow danger classes, turn a colony's review off, change protected paths and delete colonies, people's own colonies included. The Cloudflare account, its Artifacts namespaces and Workers AI are trusted. A person who connects Cloudflare trusts the deployment with a token for their account's Artifacts (and Workers AI, when they allow it), kept encrypted under the operator's
FT_GRANT_KEY. - Review switched off. With a colony's review off, only protected paths still reach a person, including for changes to
.firetrident/, agent instructions and files other tools run or obey. Protect those paths before turning review off. - The MCP token is per deployment. There is one, not one per agent or per colony: whoever holds it can join any colony on that deployment as an external agent, read its trunk, and propose changes, which then go through review.
- Attacks split across changes. Review looks at one change at a time, so a harmful change split into several harmless-looking ones is an open problem. Model-based layers are judgments and borderline calls can vary between runs, and how often the danger patterns miss a dangerous change nobody has seen is not measured.
- Tests run after landing. Certification runs a project's tests after a change lands, so a broken change can sit on
mainuntil a repair lands; deploy and pin fromcertified. The test files of one run share one Worker's globals, which is less isolation than a process per file. - Secrets in file contents. Masking covers lineage notes and review views, not files. A change that adds a credential the secret patterns recognize goes to a person while review is on, but FireTrident never rewrites file contents, and whatever lands is in git.
- Antibody questions are on trunk. Security antibodies' questions are kept out of what agents see when they take work, but anyone who can read trunk can read
.firetrident/antibodies/. - What an external agent does on its own machine. The client's sandbox and permissions are its user's choice; the agent guides show settings that allow only the FireTrident tools and git.
- Volume. FireTrident relies on the platform for rate limits; a holder of a valid token can load a colony.