Boomship can act. So control matters.
It edits code, commits, runs commands, reads your sources and updates records. Each of those is a power, so each has a boundary you can read: in the code, on the receipt, and here.
scroll the diagram sideways →
Every run has a scope.
A scope is a permission, set for one run from the composer’s scope chip. Shipping, which means branch, commit, push and opening a pull request, is off unless you allow it for that run. The budget is a hard stop: tokens, cost and minutes are charged on every model turn, and when a limit is reached the run stops and the receipt says which limit stopped it.
Before a run starts, the capability gate reads the goal. If it asks for something this release cannot do (images, video, sending email or messages, calls) the composer says so in one sentence, with what Enter will do instead. It never pretends.
Sending email is not in this release: Boomship reads Gmail threads you add to Sources but cannot send. Enter drafts the email in its reply; you send it.
computed by lib/agent/capability-gate.ts for that sentence · the run still starts; the composer says this firstYour keys stay yours.
A provider key you paste is encrypted with AES-256-GCM before it is stored, and only its last four characters are shown again, in Settings → Intelligence. Your provider bills you directly; Boomship’s subscription is flat and separate.
Managed keys, where a deployment has them, never reach the app server. They are Edge Function secrets, and the model relay adds them to the call. The app only learns whether a provider is available.
- stored
- AES-256-GCM, in the
credentialstable, with a random salt and IV per value. - shown
- Only the last four characters, ever again. The value is decrypted on the server for a model call and nowhere else.
- billed
- By your provider, on your account. Boomship never marks inference up: it is not on its invoice.
- managed
- Where a deployment has its own keys, they live in Supabase Edge Function secrets and are used only through
supabase/functions/model-relay. The app asks the relay which providers it can serve and sends the call; it never reads the key.
No key, no secrets.
ENCRYPTION_KEY is required everywhere except NODE_ENV=development. Without it, the routes that would store a key answer 503 “encryption not configured” and store nothing. The web app and the worker share the same key, so a value saved through one decrypts on the other.
- production
- ENCRYPTION_KEY set · every credential, connector token and webhook secret is encrypted with it.
- production, unset
- 503 “encryption not configured” · the key and trigger routes refuse to store anything, instead of a 500 or a silent fallback.
- development only
- A local fallback key, with a warning in the log, so
npm run devworks without setup.NODE_ENV=developmentis never a production setting. - rotation
- A key that was ever committed is rotated.
.env*files are untracked.
Tools declare what they may do.
Every connector declares its capabilities in the registry, and this table is read from it. A capability is absent when no code does it; nothing claims trigger yet. Airtable is the one connector that writes, and only to the tables you allow on the Connections page; each update is a step on the run.
GitHub: runs commit only to boomship/<name> branches and open pull requests only into the base branch. On a host with no shell, the GitHub runner edits through the API and refuses exec, so tests stay “not run” on the receipt rather than being guessed.
| Connector | read | write | trigger | sync |
|---|---|---|---|---|
| Notion | yes | — | — | yes |
| Linear | yes | — | — | yes |
| Slack | yes | — | — | yes |
| Gmail | yes | — | — | yes |
| Google Drive | yes | — | — | yes |
| Airtable | yes | allowed tables | — | yes |
| GitHub | yes | boomship/ branches | — | — |
- AirtableRuns can find and update records, field by field, only in the tables you allow here. Each update is a step on the run.
- GitHubRuns commit only to boomship/<name> branches and open pull requests only into the base branch. No existing branch outside boomship/ is ever updated. Webhook-triggered runs never ship.
It waits for a person.
A chain runs in order. When a run does not meet its condition, everything behind it is blocked and says why, until a person looks. One switch pauses everything automatic: the dispatcher holds ready runs, and webhook triggers answer 423 until it is turned back on.
A steer sent while a run works is a step on its own ledger: received at the next turn, with that turn written on the step. A steer the run never read is closed as skipped, never dropped silently. A webhook delivery is accepted only with a valid signature over the timestamp and body, inside a five-minute window, and its text reaches the builder labelled as untrusted input.
Every check is named.
A Work Receipt ends every run: files changed, what was inspected, each check’s state, the reviewers’ findings, cost and duration. A check that did not run is written as not run, never left out, and confidence is rated from what ran. The receipt is written by the verification stage, which collects the diff itself and does not trust the builder’s summary.
Every tool call leaves a hash.
Each step on a run’s ledger records which tool ran, a hash of what went in and what came out, and whether the result was observed (a real command, file or model result) or only asserted by the model.
After a run ends, outcomes are appended by the action that caused them, never inferred: you undid it, a later Prove it confirmed or contradicted it, a later graph run resumed it.
Rows belong to the project.
What the Brain indexes, remembers and runs is stored as rows that belong to a project. Row-level security reads membership through functions in an unexposed schema (private.user_project_ids() and its organization twins), so no policy selects from its own table and no member sees another project’s rows.
What reaches a model is the compiled context for that run. The Context card shows every item in it with the reason it was included, which memories the compiler doubted, and what the goal named that the index does not know.
Commands run in a container.
With the Docker runner, each project gets its own long-lived container on a bind mount of its workspace. The network is off unless RUNNER_NETWORK=1; every capability is dropped and no new privileges can be gained; every path is resolved inside the workspace; binaries are allowlisted and nothing runs through a shell.
The repository is fetched with host-side git into a private bare mirror, so the GitHub token never enters the workspace or the container. The workspace takes commits from that mirror, and push and pull request go back through it. A .git whose configuration could run a program (hooks, fsmonitor, filters, includes and the like) is refused.
Someone tries to break it.
At high and critical risk an adversarial security review reads the diff as an attacker would: authorisation bypass, row-level-security gaps, injection, SSRF, secrets logged or committed, webhooks without signature checks. Its verdict is fail when any critical or high finding exists. The reviewer is a model that did not build the change, whenever another is eligible.
Before any commit, the staged diff is scanned for credentials: known token formats, private keys, connection strings with passwords, and secret-named values with high entropy. A finding unstages everything and refuses the commit, with the value masked; a push scans each commit the same way.
Refused to commit: 1 possible secret in the staged changes (values masked): <file>:<line> <kind> (<masked>) Remove them (read the value from the environment instead) and commit again.
Verdict: "fail" when any critical or high finding exists, "concerns" for medium or low only, "pass" when you found nothing exploitable.