- orcad: only the owner's desktop may attach/settle; devices paired through a grant are listed
by orca-sandbox-grant and refused (forks keep their offer's origin id)
- desktop: per-workspace limits (3 waiting, 60/hour), always-allow runs one at a time and 30/hour,
one banner per burst, refusal floods logged once a minute
- dialog: commands over 20 lines refused; long ones must be scrolled to the end; agent name muted;
Turn off for this workspace; read delay counts from when a prompt reaches the front
- lookalike spaces and combining marks refused; settled events only close their own workspace's prompt
- executor reports soon after the shell exits even if an escaped process holds the pipes; hard stop
- audit log keeps 3 files, long commands stored as prefix + sha256
- rules expire after 30 days and are cleared when a shared workspace is allowed
Vercel's sandbox-init listens on 23456 as root (pid invisible to the user); the
VM declares it via ORCA_HOST_MANAGED_UNOWNED_PORTS. Any other pid-less
listener (root-run postgres, Docker-published ports) stays under External.
The first create of a repo that no environment of the account lists makes a default environment for it
(just the repo, cloned blobless, no install script) and builds it in the background with the existing
environment build pipeline (EnvironmentBuildRunner). Later creates boot from its active build and fetch
only what changed, in the background, shown as 'Update repository' with git's real progress.
- environments.kind 'org' | 'auto' + last_used_at (migration 0049, applied to dev); an admin's edit
adopts an auto environment; an admin environment listing the repo always wins
- auto environments unused for 14 days are deleted with their snapshots (cron); per account only
- golden refresh, staleness, pruning and the warm pool apply as for any environment
- the caller's machine size stands for auto environments; an explicit other region skips them
- AUTO_ENVIRONMENTS=off disables it
- desktop: feature repo-update, 'Update repository' / 'Fetching changes…' / sidebar 'Updating the
repository…'
Measured on dev (stablyai/orca, staging OWNER): before 6 s to ready + 58 s clone; the background build
took 79 s; after, ready in 6.6-8.7 s with the repo current 0.7 s later (7.4-9.4 s total).
A repo-backed create of stablyai/orca (2.7 GB) did a full git clone inside the 120 s orcad start
command, failed at ~127 s with a bare create_failed, then the pooled path retried on a fresh VM
(~255 s total). When the desktop gave up, the cut-off Worker left the retry row in 'creating' for
15 min.
- workspace prelude clones --filter=blob:none (stablyai/orca: 68 s instead of >120 s timeout)
- feature clone-progress: the clone runs detached after ready; GET /v1/sandboxes/:id/clone reports
{state, percent, stage} and records a failed clone's reason in last_error; 15 min clone timeout
- failed creates record why (orcad_start_failed, provider_<status>, ...) instead of create_failed
- a pooled boot that failed after 30 s is not retried on a fresh VM
- 'creating' rows whose Worker was cut off are released after 5 min (create_abandoned)
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.
- 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.
- Agent and worktree come from the terminal orcad verifies from launch
evidence (verifiedCallerTerminalHandle, shared with turn-sender); the
terminalHandle param is gone and a worktree hint is kept only when the
host knows it.
- secrets.* are accepted, and their events sent, only on cloud orcad
(ORCA_IS_CLOUD=1 + ORCA_SANDBOX_ID); already-set never reads orcad's env.
- touchRequest emits secrets.requestUpdated; natural expiry emits
secrets.requestSettled {status: expired}.
- orca-env skips the control plane's reserved names and prefixes, with a
test that reads them from secrets.ts.
- reason drops bidi controls and turns C0/C1 controls into spaces.
Live on dev the first capture sealed zero sessions: agentSession.subscribeStatus needs a streaming
transport and the unix socket refuses it, and worktree.ps lists only agents live right now. The
collector now reads orcad's own session records (agent-session-journal.db, read-only): provider,
name and last turn, open tabs first; live worktree.ps rows override status; history still comes
from agentSession.history, with the stored last turn as fallback.
orcad keeps agent-initiated secret requests in memory with the contract's
limits (3 pending, 10/hour, 15 min TTL, touch capped at 60 min, dedupe per
name), derives the agent from the calling terminal, and announces
secrets.requested on runtime.clientEvents (replayed on attach; phones get
status only). The CLI waits on it and prints the D4 reload hint; the VM
ships orca-env and makes zsh read the session env hook.
- COPILOT_GITHUB_TOKEN: placeholder in the VM, swapped on api.github.com (Bearer or token form, next to
the workspace token's own route) and on the Copilot API hosts (left open for Copilot's own session
token). GOOGLE_API_KEY: swapped on generativelanguage.googleapis.com and aiplatform.googleapis.com
(Vertex express). A credential may now route to several hosts.
- Routines accept claude, codex, opencode, gemini, cursor and copilot; the in-VM runner has a
headless command for each (checked against a local stand-in model: prompts starting with '-' stay
prompts). The editor picks the agent from a list.
- AWS_* keys (SigV4, signed in the VM) and Bedrock API keys can't be swapped: the secrets UI marks
them 'Stored on workspace' with the reason, in the list, the .env preview, the effective view and
the editor.
- Gemini's auth method is seeded for GOOGLE_API_KEY too (vertex-ai with GOOGLE_GENAI_USE_VERTEXAI);
headless Gemini refuses to start without one.
Copilot prefers GH_TOKEN/GITHUB_TOKEN over a stored /login. In Orca Cloud those hold the workspace
GitHub App placeholder, so every Copilot start said 'Resource not accessible by integration' and a
user who signed in could never use it. A wrapper drops the two placeholders for Copilot only, unless
BYOK is configured (no GitHub sign-in needed; gh in Copilot's shell keeps working).
Hosts with credential rules (model APIs, api.github.com) are answered by Vercel's egress proxy with a
certificate from its CA. Vercel's command env trusts it (NODE_USE_SYSTEM_CA, NODE_EXTRA_CA_CERTS,
SSL_CERT_FILE) but orcad starts under env -i, so Node clients in terminals used Node's bundled roots:
Gemini CLI retried forever ('This is taking a bit longer') while the same call from a command
returned at once. Found on the live proof of the new agents; NODE_USE_SYSTEM_CA=1 in the terminal
fixed it there (1 s to the provider's answer).
Image: pinned opencode-ai 1.19.5, @google/gemini-cli 0.67.0, @github/copilot 1.0.108 (npm) and
cursor-agent 2026.11.05-86047df (checksummed tarball). opencode's unused linux build variant is
dropped (~205 MB). First-run screens are answered at every boot/secret refresh (folder trust for
Gemini, Cursor and Copilot; Gemini's auth picker when a Gemini key is set; OpenCode's gateway model
from ANTHROPIC_MODEL/OPENAI_MODEL); self-update is off for all four.
Egress: GEMINI_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY (generativelanguage.googleapis.com,
x-goog-api-key), CURSOR_API_KEY (api2.cursor.sh, only POST /auth/exchange_user_api_key; the
refresh token is stripped so nothing long-lived lands on the VM) and COPILOT_PROVIDER_API_KEY /
BEARER_TOKEN (host of COPILOT_PROVIDER_BASE_URL, same scope only) are placeholders in the VM and
swapped by the proxy. Allowlist adds the agents' API and sign-in hosts. API/MCP accept the image's
agents; routines stay on claude/codex.
The wake phase "orcad ready" (W9) runs from orcad's spawn to the control plane
having the pairing offer. After orca-sandbox-start exits, the control plane
made a second Vercel round trip (files API, ~120 ms from here) to read the
offer, because command stdout is kept as logs.
startOrcad now passes a fresh 32-byte key per call in env (env is not kept).
The start script adds "sealedOffer": "<iv>.<base64 AES-256-CTR(offer line)>"
to the pointer line it already prints, so the logs only hold ciphertext and
the control plane opens the offer locally. The key is unset first thing in the
script and in the prelude, so nothing the script starts inherits it. If the
offer is missing or does not open (older image, no openssl, wrong key), the
control plane reads the file as before. Older Workers send no key, and
current images then print the plain pointer.
The ready poll in the start script drops from 50 ms to 10 ms (~20 ms mean),
keeping the 60 s minimum timeout.
Verified on a VM from snap_aQmW, driving the start script like the control
plane: all four starts, including an idempotent second start, opened without
the files API, and the stdout never held the pairing code.
NODE_COMPILE_CACHE only stores what V8 compiles before a script runs: 5.4 MB of
orcad-server.js's 15 MB. Every function startup runs still compiled lazily on
every boot. Measured on a VM from golden snap_aQmW, orcad ready went from
0.95-1.06 s to 0.70-0.78 s (5 pairs, alternated), and from ~1.25 s to ~0.95 s
on a slower host.
image/vm/orcad-boot runs orcad-server.js through a vm.Script whose code cache
is taken as the image's smoke run exits, so it covers what startup compiled.
orcad sees the same argv, __filename, require and import() resolution as under
plain node. The cache file starts with a header (Node version, arch, bundle
length and crc32): V8 itself only checks the source length, so a cache from
other bundle bytes would otherwise run stale code. Without a matching cache
orcad loads through require exactly as before.
Verified in that VM: the cache is accepted, the experimental-loader warning
stays out of orcad.log, and a terminal created through the CLI runs (node-pty
resolves via import() from the bundle directory).
On every Orca Cloud start and wake, orcad ran its disposable startup preflight child. That child is a
second full load of the 15 MB bundle plus the SQLite and node-pty probes, and it ran before main().
The cloud slot could never be qualified: qualification needed the managed-activation env, and the
marker lives in the slot directory, which is root-owned in the image. In-VM profiles showed the main
thread idle for about 580 ms waiting on that child.
- ORCA_ORCAD_STARTUP_QUALIFICATION_DIR names a writable directory for the startup-qualified marker
and opts the slot into qualification. The key is unchanged: artifact version, file stat identity,
runtime, ABI, kernel and launch env. It survives a stop and resume: inode, mtime and ctime are
identical after resume, and the first launch after resume skipped the preflight.
- The image creates /opt/orca/startup-qualification. The provisioning smoke run records the marker,
and orca-sandbox-start passes the env.
- Agent-state-rules live updates and speech-model warm-up now start after the ready line instead of
before it.
- Once the ready line is published, a terminal list warms the terminal path (worktree catalog,
daemon inventory) before the reconnecting desktop asks for it.
Real 4-vCPU VMs, readyMs median of 5: idle 1475 -> 903 ms; with 3 busy cores 1921 -> 1200 ms.
The Worker-started source build used the VM scripts, provision script and manifest pins embedded in
the Worker that started it, so a golden built from a newer commit could miss that commit's new VM
scripts (seen live: snap_HTsU from 19f900e has #122's orcad but not orca-desktop-proxy or
orca-desktop-page.js). source-build.sh now sources image/commit-provision-env.mjs from the fetched
checkout (no dependencies; mirrors provisionEnv, enforced by a test), so the image ships exactly that
commit. Tests also require every image/vm file to be a declared VM script and installed by provision.
* feat(cli,sandbox-control): orca repo add --display-name; cloud sandboxes name the project after its repository
The VM checkout lives at /vercel/sandbox/project, so boot-time registration (#104) named the repo
'project'. repo.add already accepts displayName; the CLI now exposes it as --display-name and
orca-sandbox-start passes the repository basename from ORCA_REPO_URL.
* fix(sandbox-control): golden builder polls detached builds with a bounded wait=true read; image-only merges trigger a rebuild
The provider reports a detached command's exit code only to a wait=true read (W1's finding, #123):
stepBuild's plain GET is what kept finished golden builds 'running' until the deadline. stepBuild and
the operator rotation now use the same bounded wait probe (4 s); the exit-file marker stays as a
fallback. golden-source's VM path list now includes cloud/apps/sandbox-control/image/ (VM scripts,
provision, manifest), so a script-only merge (e.g. orca-desktop-proxy) rebuilds the golden too.
* feat(cloud): remote desktop single-use tickets, fit, clipboard and keyboard release
- VM: Xvnc on a 0600 socket with no VNC auth plus an X cookie; orca-desktop-proxy (websockify
subclass) admits one WebSocket per unused 60 s ticket from the same localhost origin, refuses
rebinding Hosts, scrubs tickets from logs, serves the page with connect-src 'self'.
- Worker: POST /desktop mints a fresh ticket per call (schemaVersion 2).
- Desktop: no secret in the tab URL; main hands the page tickets on load and reconnect (backoff,
floor, pause with Reconnect), only for the exact URL of a live forward it opened; keyboard stays
in the desktop until Ctrl+Cmd / Ctrl+Alt; a paste key pushes this computer's clipboard; sandbox
copies reach this computer after a copy gesture.
- UI: keyboard hint, asleep/starting neutral tone and mapping, persistent error toasts, Beta badge.
* fix(cloud): remote desktop review fixes
- Security review: noVNC host/port/encrypt pinned to the page's origin, CSP connect-src 'self',
exact-URL + live-forward trust, mint floor and per-minute cap, copy gate on copy gestures only,
X display cookie.
- Design review: archived is its own neutral state, starting covers waking, page messages say
"Wake it in Orca", pause overlay is a status for calm states and focuses Reconnect, hint wraps
and hides from assistive tech when faded.
- FE review: paste follows the keyboard layout, AltGr never releases, page calls settle on
navigation, state notices only on change, hint once per load/release, paste always pushes,
no committed bytecode, session tests.
* fix(cloud): remote desktop archived page message, hint layout, fixtures
* fix(cloud): remote desktop copy gate counts double-clicks (pointer events have no click count)
* chore(cloud): remote desktop lint (exhaustive pause reasons, no test casts or wall clock)