Pi auth on sandboxes
How pi gets LLM creds and a model when it runs on a sandbox — the detached path (daytona runtime) and manual arbe x. In-process bot dispatch does not run pi on the sandbox; it replies via pi-ai with OpenRouter by default (pi) and reaches the sandbox only through the run_command tool. Decisions, not mechanics — wire-up lives in environments, secrets.
Decisions
- Creds live in
arbe secret. Environments bind names; the dispatcher resolves them and passes the values to the box as process environment — forrun_command’s exec, and for the coding-agent run behinddelegate_task. House keys are env-var keys; a person’s own ChatGPT plan is the one OAuth path (below). - OpenRouter is the canonical sandbox credential. Store and bind
OPENROUTER_API_KEY; pi receives it as an env var, no persistent pi login on the sandbox. Provider-specific keys (ANTHROPIC_API_KEY,OPENAI_API_KEY, …) work when paired with an explicit model. Default model isDEFAULT_BOT_MODEL(packages/core/schemas/agent-model.ts) — a pinned cheap OpenRouter ref, notopenrouter/auto. See llm-keys. - Setup is two commands.
arbe secret set, thenarbe env create --only-secret <NAME>; Daytona boxes need no per-box setup — see “Typical setup workflow” in runtime.
A person’s ChatGPT plan
Someone who connects ChatGPT (arbe connect chatgpt, or “Continue with ChatGPT” under Account → Connections) pays for their own turns with it, in any house. Whoever asked pays: a bot turn bills the human who wrote the trigger, a bot-to-bot chain the latest human before it in the window, a coding run whoever the delegating turn billed (or the human who wrote the follow-up). A run has exactly one payer: another human’s message in its thread waits for it to end, then starts a run billed to them (Daytona runtime). No human in view (workflows, schedules) and no plan both fall back to the house key. A plan that is connected but failing fails the turn visibly; it never quietly switches to the house’s money.
web / CLI ──POST /api/me/plans/chatgpt/login──▶ API: PKCE verifier, state, nonce ──▶ plan_logins(waiting)web / CLI ◀──────────────── authorize URL ◀───────┘browser consent ──▶ http://127.0.0.1:1455/auth/callback (CLI listens; on the web, paste it)web / CLI ──POST …/login/:id {callbackUrl}──▶ API: check state, exchange code at OpenAI ▼ plan_connections row + Vault credential (one per person)turn ──resolvePlanAuth──▶ fresh access token (one refresher, row lease) ──▶ pi-aibox: pi ──run's tools token──▶ /api/threads/:id/model/responses ──resolvePlanAuth──▶ OpenAI- Sign-in is two stateless API requests. Start stores the PKCE verifier, state and nonce on the
plan_loginsrow (service_role only) and returns OpenAI’s authorize URL; complete checks the pasted address, claims the row, and makes OpenAI’s documented code exchange, storing the credential in pi-ai’s ChatGPT shape so pi-ai’srefreshrenews it. Nothing is held in a process, so a deploy between the two costs nothing. A sign-in expires after 10 minutes; every failure ends the row with a plain reason, and the API answers with that reason rather than a 500. The newest sign-in wins: starting another or disconnecting ends the pending ones, andsave_plan_loginstores a credential only for the person’s newest sign-in that is still exchanging. - Tokens live in Vault, refreshed in one place.
plan_connectionsholds status, the account’s model catalog and the opaque host id; the credential (access, refresh, client id) is one Vault secret only service_role decrypts. Refresh tokens rotate and a reused one locks the person out, soresolvePlanAuthrefreshes only under a row lease (claim_plan_refresh/store_plan_refresh); a refused refresh marks the connectionneeds_login. Deleting an account drops both plan tables with the tombstone, revokes at OpenAI best effort, andresolve_plan_connectionnever returns a deleted person’s credential. - The model never changes; only the route does. The configured model runs exactly as set (model rule):
openai/<slug>runs on the asker’s plan when it serves the slug, else on OpenRouter with the house key;openrouter/…always on OpenRouter. Only with nothing configured does the default follow the payer:openai/gpt-6-lunaon a plan,DEFAULT_BOT_MODELotherwise. The route is picked before the plan is read, and only anopenai/model or no model at all reads it, so an expired login never blocks an OpenRouter model; coding runs check the house key for that route before cutting a box. Code:routeModel/routeTurninpackages/core/plan-auth.ts. - Boxes never hold a plan token. A coding run on a plan runs pi with
--model openai/<slug>and a per-runmodels.jsonthat points pi’sopenaiprovider atPOST /api/threads/:id/model/responses, keyed with the run’s tools token (read from$ARBE_TOOLS_TOKEN_FILEper call, so renewals carry over). That token names the payer; the route checks the run is still the one running, reads the payer’s token withresolvePlanAuth(refreshing it there), strips the request fields a plan refuses, and streams OpenAI’s reply back. A leaked tools token works only through arbe and only while its run runs; a plan that fails mid-run fails the run, never moving to the house key. Code:packages/core/dispatch/coding-run-model.ts. - Usage. Plan turns are ledgered with
key_source = 'plan': never against a house’s allowance.
Code: packages/core/plan-auth.ts, packages/core/plans.ts, packages/core/dispatch/plan-turn.ts, apps/www/src/routes/api/me/plans/.
Failure narration
Pi auth/billing/quota errors are hoisted into a signal.thread.pi_failed entry alongside the failed status flip. Carries errorClass (missing_key / access_denied / auth / billing / quota / network / transient / pi_exit / unknown, sorted the way pi-ai’s retry rules sort provider errors; a ChatGPT plan’s subscription_sharing_usage_limit_exceeded / _unavailable are quota, the first never retried), a short message, and provider/model when known. Additive — the pi.assistant payload is unchanged. Source: decideAgentOutcome + classifyProviderError (packages/core/pi/outcome.ts), shared with in-thread replies.
Open
- CLI’s
~/.config/arbe/sandboxes/<name>.envoverlaps witharbe secret; not yet retired. - Claude subscriptions for coding runs (arbe-b2b0) reuse the plan connection.
- OpenAI says plan usage is for open-source and locally hosted apps; arbe is hosted, so this stays dogfood-only until OpenAI answers the interest form.
See also
dispatch · environments · secrets · pi