0141 — A browser per errand
Status: accepted · 2026-10-19
- Status: accepted
- Date: 2026-10-19
- Builds on: ADR 0014, ADR 0080, ADR 0130, ADR 0033, ADR 0129, ADR 0025
Problem
Every chat and every background task had a tab in one browser context
(BrowserRuntime.context()). Tabs were per conversation, but cookies, local
storage and sign-ins were shared. Errands that ran side by side collided: two
price comparisons fanned out by delegate filled one shop's basket, one
signed the other out, and a site's "you're already looking at this in another
tab" logic saw them as one person.
Decision
Each errand gets its own browser session. Every task, and every helper
(both run as a conversation with origin.kind === 'task'), browses in a
browser context of its own: cookies, storage and tabs that no other chat or
task sees. BrowserService.tools(ctx) marks the conversation when a task's
turn builds its tools; the agent can't choose this, and a task can't leave it.
Ordinary chats keep the main context, signed in where the person signed in
("use my logins" is what people expect). A person can give a chat its own
from the live view (the A browser of its own for this chat toggle, a live
command session), and take it back.
Sign-ins are lent, never shared
An isolated context still needs the person's sign-ins for some errands. Copying cookies is copying credentials (a session cookie is a bearer token: OWASP Session Management Cheat Sheet), so the rule is least privilege and a person's press:
- A new errand is seeded with a copy of the main context's storage state
(
context.storageState()) for exactly the sites the person always allows (Settings › Browser,browser.jsonsites), filtered by registrable domain (errands.tsloginsFor, the samesiteOfa site permission uses:.shop.examplebelongs toshop.example,alice.github.iois notgithub.io). Never the whole jar. These are sites the errand could already act on with the person's sign-in in the shared context before this change, so lending them widens nothing. - Another site is lent only when the person presses Allow on that site's
question in the errand (
tools.tspermit→BrowserService.lendSignIn): its cookies are added to the errand's context. The card says so (BrowserPermission.lendsSignIn). Auto and Full trust don't ask about an ordinary site, and therefore never lend by themselves; there the errand starts signed out and signs in with Passwords (ADR 0130), which works in any context because filling acts on the page. - Never from your own Chrome (ADR 0080): Conch doesn't copy cookies out of the person's own browser into another process. Beside your Chrome, errands run in Conch's own browser, signed in nowhere; the prompt and the panel say so.
- Never passkeys or IndexedDB (
storageState()withoutcredentialsorindexedDB), and never back: what an errand signs in to stays in the errand. - Revoking a site (no longer always allowed) takes its cookies back from every errand it was lent to, open or set aside. Sign out of every site wipes every errand too.
Threat model: a prompt-injected errand (Greshake et al., 2023) gets the sign-ins of the sites the person trusts and the ones they allowed in that errand, and nothing else; before, it had all of them. Another errand's session is out of its reach. What it holds never enters the agent's context, a log or a URL.
Lifecycle
- Ends with the task. Done, unverified or stopped (
task.changed→BrowserService.taskChanged): the context closes and its session and saved tabs are forgotten. Deleting the chat does the same. - Waits for Resume. Interrupted or failed: the context is set aside
(
Errands.park): its storage state is kept, and its tabs inbrowser/tabs.jsonas for any chat. Resume runs in the same conversation, so it opens the same session and tabs again. Shutdown, Repair and an update set every open errand aside the same way. - Sealed at rest. Sessions live in
browser.errands.secrets.json, one of theSEALED_FILES(AES-256-GCM under the device key, ADR 0025), protected from the agent's file tools (protect.ts), andderivedin the backup manifest: copies of sign-ins aren't carried to another computer. At most 40 are kept, for 14 days. The main profile (browser/profile) stays as before: Chromium's own store. - A cap.
errandLimit(): about one open context per 2 GB of memory, from 2 to 6. When it's full, the open errand used least recently that nobody is driving or watching is set aside to make room (it comes back as it was). When all are busy, the step fails in words a model can act on ("wait a minute and try again"). Idle errands are set aside by the browser's idle sweep. The task scheduler (ADR 0129) keeps its room checks as they are: its gateway reserve already counts the browser, an errand's context is far smaller than a task, and setting one aside is cheap and lossless, so the browser enforces its own cap instead of the scheduler guessing which tasks will browse. - Heals. What an errand holds is snapshotted a few seconds after each page loads. A context or the errands' browser that crashes is relaunched on next use (or at once when someone is watching), from the last snapshot, with its tabs, and Health says "Reopened a task's browser as it was".
Where it runs
BrowserRuntime.errandContext makes the contexts and contains each one like
the main one (every request and WebSocket past the guard, ADR 0014
"Containment"):
- Conch's own browser here: a second, non-persistent Chromium (the same program as the main one), one context per errand, started when the first needs it.
- A browser elsewhere (a DevTools address, Browserbase, Steel): a new context of that browser when it gives one; otherwise (some clouds keep one context per session) Conch's own browser here, said once in Health. The main browser isn't let go of for idleness while an errand uses a context in it.
- Your Chrome: Conch's own browser here, for the reason above.
The live view
The live view was already per conversation, and a task has its own: it shows
the right context's tabs because each conversation's tab now opens in its own
context. BrowserTab.session carries reason and the sites lent; the window
shows Its own browser (the sites in its tooltip).
Consequences
- Parallel errands no longer collide, and a task's reach to the person's sign-ins is narrower than before.
- A task that needs a site the person hasn't always allowed signs in again (from Passwords, or a handoff), unless they allow it when asked. That's the price of not copying the whole jar.
- One more browser process while tasks browse, bounded by the cap.
- Tests:
browser/errands.test.ts(parallel tasks apart, seeding only permitted sites, revoking, resume across a restart, the cap, crash heal, a chat's toggle).