Authorising a call
Two credentials reach a sandbox, and the difference between them is whether a person is present. Everything else about authorisation follows from that.
On this page(5 sections)
Two kinds of caller
A browser has a signed-in human behind it and can be sent through a sign-in flow. A script cannot. So there are two credentials, one for each, and every route on this site accepts either.
# A program, anywhere: the control token
curl "$SANDBOX/git/root/status" -H "x-intentic-control: $INTENTIC_TOKEN"
# A person, in a browser: the session minted from their sign-in
curl "$SANDBOX/git/root/status" -H "authorization: Bearer $SESSION"A person, in a browser
The sandbox authenticates the end user directly against Google: the browser presents a Google identity token and the daemon verifies its signature against Google's published keys. The platform never holds or signs it, which is why a compromise of the platform cannot command your sandbox. The first verified identity to arrive becomes the owner, and the owner grants everyone else a role that decides how far they reach.
POST /system/session exchanges that token for a session the daemon minted, and that session is what every steady-state call carries as authorization: Bearer …. Calling the same route again with a session that has not expired renews it, so a long-lived tab never has to sign in twice.
A program, anywhere
Anything outside a browser presents a control token in the x-intentic-control header instead. A control token is shown once, when it is minted. The daemon keeps only its hash, so it cannot be recovered and it can be revoked at any moment.
curl -X POST "$SANDBOX/system/control/tokens" \
-H "authorization: Bearer $OWNER_SESSION" \
-H "content-type: application/json" \
-d '{"label":"nightly CI","scope":"read"}'
{ "id": "6f1c…", "token": "ict_9wQ…" }The app mints one for you under Sandbox → Computers → Editor bridge, at editor scope, together with the settings an editor needs to use it. Any other scope is minted through the route above, which requires an owner session.
What a scope reaches
A scope is chosen when the token is minted and stored with it, rather than worked out from the caller. That is deliberate: the daemon cannot tell an editor from a build job from a cron script, because all three are just a program holding a secret. The only honest moment to decide how far one reaches is when a person decides it.
| Scope | Reaches |
|---|---|
editor | One conversation: run a turn, answer a card it parked on, read transcripts, search the tree. What an editor bridge holds. It cannot see the fleet and it cannot land work. |
read | Observation only: the fleet, past sessions, workspace search, listening ports. The one genuinely narrow rung, which is why it exists separately rather than as a politeness. |
drive | Everything read sees, plus making an agent work: start, answer, steer and stop a turn. Stops short of anything that moves code into the main tree. A stolen token at this rung is the agent's reach. |
land | Everything drive does, plus merging a conversation's worktree into the main tree, and discarding one. Separate because the usual arrangement is a program that works and a person who decides. |
Worth saying plainly: at drive and above, a stolen token is the agent's reach, because driving an agent means editing files and running commands in this sandbox. read exists separately for exactly that reason rather than as a politeness. A token presented on a route outside its scope gets a 403 that says so, never a confusing 401.
Open on purpose
A handful of routes check no credential, because the caller provably cannot present one: /health, the web-chat widget's routes, an automation's webhook, a forge's pipeline webhook, a workflow gate, and the enrolment routes a new computer redeems a one-time pairing token on. Each carries its own narrower check instead of the general one.
Only /health is missing from this reference, and that is on purpose too: it is not part of the contract. It exists so a script can tell a live sandbox from a dead port, and it deliberately checks nothing.