- N9: orca://cloud/setup/return?session=cs_… is routed (open-url + second
instance), validated, focuses Orca and fans out to renderer listeners
(onCloudSetupReturn) so the setup session re-reads at once.
- N10: cloud_setup_step_{started,done,failed}, cloud_setup_first_sandbox and
cloud_setup_plan_chosen, enum-only, derived from the setup rows.
- X1: setup error kinds (timed_out, checkout_canceled, bank_step,
trial_unavailable, team_create_failed, wrong_account) and their actions.
- B2: billing POST /v1/billing/team-plans returns other teams' plan state with
the caller's own token; auth confirms membership from that token (no new
credential). Desktop IPC billingTeamPlans + useSwitchToPlannedTeam. The team
switch logic moves out of SidebarTeamSubmenu so the composer can reuse it.
Verified on a dev sandbox (Claude Code 2.1.319, codex-cli 0.166.0): runCommand runs as ubuntu with
HOME=/vercel (same user as orcad); `claude auth logout` and `codex logout` exist and exit 0; the
script reported {loggedOut:2,left:0}, removed both credential files and kept other ~/.claude.json keys.
With nothing configured, OpenCode picked its GitHub Copilot provider (GITHUB_TOKEN is the workspace
GitHub App stand-in: 'Authorization header is badly formatted') or OpenCode Zen (opencode.ai is not
on the egress allowlist). The VM seeding keeps both in OpenCode's disabled_providers unless the user
connected them in OpenCode (auth.json); the user's own list is kept (ledger of what Orca added).
Checked on a dev VM: the composer opens on 'Connect a provider'.
- M1: a permanent refresh failure revokes only the login it refreshed (CAS on updated_at).
- M2: dead logins (refresh_token_expired/reused/invalidated) are never retried; a new login clears it.
- M3: the Codex account route only carries the token to /backend-api/codex/*, the plugin catalog and
wham/usage (GET), seen live; plain segments only.
- M4: one atomic INSERT enforces one live broker per person (409 broker_in_progress) and 6 starts/hour;
broker sandboxes use 2 vCPUs.
- L5: auth.json is read only after Codex exited 0.
- L6: the 401 hook respects the refresher's backoff and dead logins.
- L7: a cancel while the broker is starting wins: the sandbox goes and POST answers 409 cancelled.
- L8: orca-sandbox-env writes the marker before the swap, recognises any Orca placeholder, and never
overwrites a login already set aside.
Codex signs in once via the person's Orca account:
- POST /v1/agent-credentials/codex/broker starts a short-lived broker sandbox (golden image, egress
auth.openai.com + chatgpt.com only, non-persistent, tagged orcaCodexBroker/orcaInfra so the workspace
orphan sweep and billing never see it) running `codex login --device-auth` (Codex 0.166), and returns
{brokerId, verificationUrl, userCode}. GET .../broker/:id -> {status: pending|active|failed}; DELETE
cancels. On completion the Worker reads auth.json through the files API, stores the tokens
envelope-encrypted and Personal-only in agent_credentials, and deletes the broker sandbox.
- Lease refresher (cron, every 5 min): refreshes the access token with the refresh token before expiry,
single-flight per person (D1 lease) with a compare-and-swap write, so a refresh token is spent once and
a delete or new login is never overwritten (no refresh_token_reused). Failures mark expiring/expired
(by the real access-token expiry) or revoked (refresh_token_reused/invalidated). A chatgpt.com 401
triggers a refresh instead of revoking.
- codexAccount AccountProvider: placeholder access token bound to the sandbox id (D14), swapped for the
real one on chatgpt.com with ChatGPT-Account-Id; never in a shared workspace (D13); kill switch
AGENT_ACCOUNTS_CODEX_DISABLED=1.
- VM: orca-sandbox-env writes a placeholder ~/.codex/auth.json (no refresh token) from
ORCA_CODEX_AUTH_PLACEHOLDER, sets a managed HTTPS ChatGPT provider (Codex's Responses WebSocket can't
pass the egress proxy; verified live), and sets a person's own login aside / restores it.
- Migration 0045_codex_broker (codex_broker_sessions, codex_leases; no token columns).
- Flag S2 on in wrangler.dev.jsonc; absent in prod.
- Overage stays $1.00 per sandbox hour (ORCA_CLOUD_OVERAGE_CENTS_PER_HOUR) and is now billed by
the minute: a per-minute metered TEST price (unit_amount_decimal = rate / 60 to 12 decimals,
transform_quantity divide_by 60, round up), created the same way by stripe-test-setup.ts.
capSeconds rounds the cap down to whole minutes and spentCents mirrors the invoice, so the
cap math matches Stripe. The meter still counts seconds.
- Opting in re-prices an overage item still on an older price (e.g. $0.75 / h) instead of adding
a second item on the same meter, and removes any extra stale one.
- Alerts at 75%, 90% and 100% of included hours (warnAtPercent).
- New top-level entitlement.suspendRunning: payment grace expired, or at the limit (out of hours
with overage off, or at the spend cap). The start check and usage answers carry it, so the
control plane puts running sandboxes to sleep (snapshot kept, never deleted). Payment fields and
/v1/internal/suspensions stay payment-only, so no data-retention clock starts at a cap.
- Copy (desktop billing panel, plan card, build sheet, phone, CLI): "billed by the minute" after
the rate; at the limit "Running sandboxes go to sleep and new ones can't start. Nothing is
deleted, and your files are safe."
- run.nonconcurrent: echo-loop started in B stops only A's echo-loop; web keeps serving in A and B
- run.ports (cloud): ORCA_PORT in the first workspace's terminal and run action, before and after
Sleep -> wake, with the VM's port registry; whether a second worktree on the same VM is offered
* fix(sandbox-control): unblock dev deploy: apply the build-runner delete-class migration without the binding (step 1 of 2)
* test(sandbox-control): build-runner config test allows the binding-less step of the rollout
Cloudflare generates no preview URLs for a Worker that implements a Durable Object, so every
--preview-alias upload of orca-sandbox-control-dev 404ed. The runner DO moves to
orca-sandbox-build-runner-dev (wrangler.build-runner.dev.jsonc); the control plane binds it with
script_name and deletes its own class (migration env-build-runner-v2). The runner only keeps the
alarm: each step calls back into the control plane's BuildTicker RPC entrypoint, which runs the same
tickBuild with the control plane's D1, provider and secrets, so nothing is duplicated.
- Worker: run orca-agent-signout (each agent's logout, then credential files) on a running sandbox
before the provider delete; bounded (20 s) and best effort, never blocks the delete.
- Environment builds: scrub agent auth (--scrub-only) before the snapshot; fail closed.
- New VM script registered in VM_SCRIPTS + provision loop; vm-scripts regenerated.
- Main IPC ephemeralVm:forwardedEnvSummary returns names only, grouped by credential-looking.
- P-4 warning component (+ fixture) shown under Settings > recipes; delete dialog line for awake
Orca Cloud sandboxes.
- local.run-actions: autoRunAfterSetup after a good setup and not after a failed one, Run button,
one trust prompt, Stop -> Stopped, menu Restart replaces the tab, Cmd+Opt+Enter start/restart
- run.nonconcurrent: echo-loop started in B stops A's
- run.ports: ORCA_PORT/ORCA_PORT_COUNT/CONDUCTOR_PORT per terminal, distinct blocks, stable across
an app restart, served locally; cloud half: the action in the VM reachable here via auto-forward
- run.cloud-resume: action back after Sleep/wake and after Restart sandbox
Dry routine run on dev: Copilot CLI answered 'No authentication information found' with
COPILOT_GITHUB_TOKEN=orca-ph-…; it only takes GitHub-shaped tokens (github_pat_/gho_/ghu_; classic
ghp_ refused). The stand-in for that one credential is now github_pat_orcaph_<sandbox>, still
swapped by exact match on api.github.com (Copilot endpoints only) and the Copilot API.
DS-34:
- One billing store (billing-summary-store) replaces the summary, the hook's local state and the
Run-on picker cache; it keeps the last good result through a failed read and still drops answers
from before a team or account switch (DS-17).
- Skeleton while Plan & billing loads.
- '{{count}} sandbox hours left' and '{{count}} sandbox hours over the included amount this month'
as i18n plurals; no bare 'hours'; the unreachable 100% branch is gone.
- Usage warning driven by usageWarningPercent ('Your team has used {{percent}}% of this month's
sandbox hours.'), in Settings, the plans page and the account menu.
- capTooHigh states the maximum ('The spend cap can be at most {{max}}.'), from offers.maxSpendCapCents.
- Plans dialog focuses its heading on open, so Tab reaches the interval toggle before the cards.
- Payment notice uses InlineError; the build card's overage buttons land on the overage section.
- Account menu names the team once and adds the hours-left line.
DS-44: warning tone at 90%+, '$' as an input prefix, neutral paid check, 24px dialog close hit
area, no <p> inside the CheckoutPanel <dl>.