Commit Graph
563 Commits
Author SHA1 Message Date
Neil 46b467d847 feat(cloud): workspace numbers (W-123), org slug links and synced sidebar prefs on the control plane (G13, G14) 2026-11-10 19:22:39 -08:00
Neil a86a8d6850 feat(cloud-create): automatic environments — repos come prebuilt after the first create
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).
2026-11-10 19:10:29 -08:00
Neil 06075e682f feat(cloud): POST /v1/agent-credentials/claude/verify for the desktop's pre-launch check
Runs the confirm-before-revoke auth check on demand: a pass within 10 min answers at once (verified_at, migration 0048), otherwise one check rate-limited to one a minute per person, revoking (audited) only on a definite authentication_error. Storing a new token forgets the old verification.
2026-11-10 18:10:43 -08:00
Neil ca73c6c8dc fix(cloud): confirm a provider 401 with one auth check before revoking an Orca account
A single 401 on an Orca-account request no longer revokes the person's Claude account everywhere. The hook only fires after the real credential was swapped in and sent. The control plane then makes one free auth check with the real token (Anthropic count_tokens, oauth beta) and revokes only on a definite authentication_error; anything else (passes, 429, 5xx, other 401 types, network) keeps the account. Checks are rate-limited to one per person and agent per minute (agent_credentials.checked_at), and every keep/revoke is audited (agent_credential_audit, migration 0047). Codex keeps its lease-refresh path.
2026-11-10 17:47:58 -08:00
Neil ddf6cfe09f fix(cloud): a provider 401 drops the dead account placeholder from all the person's running sandboxes 2026-11-10 17:40:14 -08:00
Neil c2e250bcfe fix(cloud-start-gate): a sandbox going to sleep or an abandoned create no longer refuses a start
The team and per-member concurrency counts (billing's checkStart input and the guardrail backstop)
counted 'suspending' rows, which drain for up to 5 min after someone chose Sleep and read as
asleep everywhere, and 'creating' rows whose Worker was cut off (until the cron released them).
Both could refuse a start that the user's view says fits. The global capacity cap still counts them.
2026-11-10 17:02:49 -08:00
Neil 19701b604c Merge pull request #311 from nwparker/create-speed/clone
fix(cloud-create): repo creates no longer time out cloning; clone progress API; failures say why
2026-11-10 15:57:44 -08:00
Neil 793292a6fb fix(cloud-create): clone percent covers remote counting; blob fetch reads as checkout, held 2026-11-10 15:55:35 -08:00
Neil 19cb3f2eac fix(cloud-create): blobless repo clone, optional background clone with progress, bounded create failures
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)
2026-11-10 15:51:46 -08:00
Neil 70f0ed4217 test(billing): every billing wrangler config that names a control plane binds it as SANDBOX_CONTROL
#305 moved billing's GitHub install-url call onto a service binding because same-account Workers
can't fetch each other (Cloudflare error 1042). No prod billing config exists yet (launch runbook
step 1.2); this test fails if the new wrangler.prod.jsonc names SANDBOX_CONTROL_URL without the
binding, or binds another environment's control plane.
2026-11-10 15:19:35 -08:00
Neil ff64af20b0 fix(cloud-setup): the composer follows its setup session; billing reaches the control plane for GitHub
- The composer read the setup session with a bare readCloudSetupSession() call in render. The React
  Compiler memoizes that on the session id, so the panel kept its first (empty) read: rows never
  moved past 'In browser', the finished session never auto-started the workspace, and the GitHub
  row stayed 'Needed' after the browser said 'You're all set'. Read it through
  useCloudSetupSession (useSyncExternalStore with a per-poll cached snapshot) instead.
- Billing asked the control plane for the GitHub install URL with a plain fetch to its workers.dev
  origin. Same-account Workers can't do that (Cloudflare error 1042), so every setup GitHub step
  showed 'Orca Cloud setup is unavailable'. Bind the control plane as a service (SANDBOX_CONTROL)
  in the dev config and send the request through it when bound.

Found by the NP-4 end-to-end run (np4-e2e) on m4air against staging.
2026-11-10 14:32:37 -08:00
Neil 5883d7eaf6 fix(cloud): tell a provider that was never offered (flag off) from the kill switch
GET /v1/agent-credentials adds providerState {claude, codex: on|off|disabled} next to providerEnabled.
Clients read it through orcaCloudProviderState (older servers: disabled = kill switch; a state that
contradicts providerEnabled is never trusted). Flag off is a new 'unavailable' kind: Settings says
it isn't available in Orca Cloud yet (no 'right now / until it's back'), and the composer hides the
Agent row like an agent without an account path. Kill-switch copy is unchanged.
2026-11-10 09:02:08 -08:00
Neil 062c05d2fe fix(billing): grant the trial to the desktop's active team when setup starts on it
Live m4air run: a signed-in owner whose active team never had a trial was sent
to T3/Checkout. Only teams bound in the browser got the trial. Session creation
now runs the same grant (N11 limits) for the active team, so the chain skips
the plan page.
2026-11-10 08:49:41 -08:00
Neil e6cdedd45e Merge pull request #247 from nwparker/cloud/agents-aws-badge
fix(cloud): W10-2: AWS keys marked 'Real value in sandbox' with a focusable tooltip; fixtures
2026-11-10 08:21:14 -08:00
Neil 5a0eabd864 fix(cloud): compare the boot env inline; refresh in the background only when it changed 2026-11-10 07:29:12 -08:00
Neil b20120d4d0 fix(cloud): secrets saved while a sandbox boots reach its session
The boot resolves the session env before the VM is up, and a secret change only refreshes running sandboxes, so a value saved from the build-sheet Secrets row during the boot never reached the workspace (found in the W6 live e2e). Once running, the boot re-resolves the env and refreshes the session when it changed.
2026-11-10 07:26:29 -08:00
Neil f094594d0e merge integration; design-squad copy: generic 'this key' sentence for non-AWS keys, subscription-token refusal points to Settings → Agent accounts 2026-11-10 06:58:16 -08:00
Neil 576ea3ae4f fix(cloud): D13 fails closed when a workspace's sharing is unknown
Server: only a row known to be private counts as unshared. Desktop panel: a failed sharing read leaves out the caller's Personal values and the Personal save scope, as for a teammate.
2026-11-10 06:49:40 -08:00
Neil 264ac40bcb fix(billing): e2e keeps each org's Stripe email (customer lookup), trial check uses it 2026-11-10 06:30:39 -08:00
Neil 5a877a01d7 feat(billing): setup sessions use the one-time trial (N11) through the TrialPort
A team that setup creates or picks gets the trial when it binds, with the email,
GitHub user and ip abuse keys. Teams that can't get one go through T3 and
Checkout.
2026-11-10 06:27:45 -08:00
Neil 51ace4d05f feat(billing): one-time Orca Cloud trial (N11) and its desktop surfaces (T1)
Billing Worker:
- trials + trial_claims (migration 0005): 2 sandbox hours per new team, no card, no Stripe.
- POST /v1/billing/trial grants it (idempotent, any member); grantTrial/trialOffer exported for
  the setup session. offers.trial on GET entitlements only when this caller could still claim it.
- Abuse limits: once per team, Orca user, normalized verified email, GitHub user (token claim)
  and GitHub owner (POST /v1/internal/trial/github from the binding; a reused identity revokes),
  disposable-domain blocklist, 3 grants per IP per day. Keys stored as SHA-256 hashes.
- Entitlement trial {includedSeconds, usedSeconds, remainingSeconds, exhausted}; metered from
  usage_events, concurrency 1, never overage; trial_used_up (suspendRunning) / trial_unavailable.
- Any subscription ends the trial; unused trial hours lapse.
- e2e: trial path (grant, no Stripe customer, concurrency, metering to used up, once per user,
  plan ends it) with cleanup of the run's abuse keys.

Desktop: trial_used_up / trial_unavailable classify as 'plan'; copy in billing, phone and CLI;
"Trial" badge, TrialPlanSummary in Plan & billing and the plans page strip, account menu row
"Trial · {{count}} sandbox hours left" (Choose plan for owners/admins); billingClaimTrial IPC
and claimOrcaCloudTrial() for the composer.
2026-11-10 06:20:22 -08:00
Neil 91e82b870d feat(sandbox-control): bind a GitHub install by the Orca sign-in's GitHub identity (N5/O3)
Billing's setup chain now passes the GitHub identity that Orca Cloud auth
verified from a GitHub sign-in (githubUserId, githubLogin). The install URL's
signed state carries it, and the setup URL checks it against the installation
with the App's own credentials: the user's own account, or an active org admin
(members:read installation token). A match binds with method 'idp' and skips
GitHub's separate Authorize page. Anything else falls back to the OAuth step
(A4b) as before.
2026-11-10 06:14:02 -08:00
Neil 33950fa436 feat(cloud-setup): desktop side of the one-tab setup session (N1), team-first plans (O1)
- Signed out, Set up and start opens one tab. The authorize page shows the
  setup account step (A2). Once the code is exchanged, the loopback holds the tab,
  creates the billing setup session and 302s the same tab into the setup pages.
- Signed in, the session is created and its start URL is opened.
- IPC: orcaProfiles.cloudSetupStart / cloudSetupGet / cloudSetupLink (Copy setup
  link mints a fresh one-time link).
- Renderer: cloud-setup-session.ts polls the session (2 s for 3 min, then 10 s),
  keeps a snapshot for the composer and tracker, and switches the profile to the
  team setup chose.
- Plans dialog: Checkout joins the open setup session. A personal account
  (409 team_required) goes through setup instead, which makes or picks a team
  first.
- Billing: POST /v1/cloud-setup/sessions/:id/link.
2026-11-10 06:07:35 -08:00
Neil c14d5856f2 feat(billing): cloud setup sessions (N1) and team-first Checkout (N4/O1)
Setup session API on the billing Worker: the desktop creates a session after
sign-in and polls its steps (account, team, plan/trial, github); the setup pages
on Orca Cloud auth redeem a one-time browser ticket, bind the team (granting the
trial through a port np4-trial's module plugs into), open Checkout that returns
to the setup pages, and ask the control plane for the GitHub install URL. The
control plane flips the GitHub step via POST /v1/internal/cloud-setup/github.

Checkout now refuses personal accounts with 409 team_required: a user: plan
could never move to a team later.
2026-11-10 06:07:27 -08:00
Neil 95bb1dd372 Merge pull request #249 from nwparker/cloud/w6-a9-recipe
A9 sign-out before delete + recipe env-forwarding warning (P-4)
2026-11-10 06:03:08 -08:00
Neil 2f06814020 test: imports before the preset-name list 2026-11-10 05:57:40 -08:00
Neil 99c52ba889 Merge remote-tracking branch 'private/cloud/integration' into cloud/w6-a9-recipe
# Conflicts:
#	cloud/apps/sandbox-control/image/vm-scripts.generated.ts
2026-11-10 05:53:41 -08:00
Neil 21ac87b972 feat(cloud-start): setup return link, setup telemetry, X1 error model, B2 team-plans lookup (NP-4)
- 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.
2026-11-10 05:50:29 -08:00
Neil 0606c25105 Merge pull request #250 from nwparker/cloud/w6-codex-broker
feat(sandbox-control): Codex account broker (contract D11, flag S2)
2026-11-10 05:49:20 -08:00
Neil 4030deeba8 fix(cloud): every agent credential a sandbox gets in full is marked 'Real value in sandbox', not only AWS keys
A shared list of agent and model-provider credential names (AWS, Bedrock, OpenRouter, Groq, xAI,
Mistral, DeepSeek, Together, Fireworks, Perplexity, Cerebras, Moonshot/Kimi, DashScope, Z.ai,
MiniMax, NVIDIA, Cohere, Azure OpenAI, Hugging Face, Amp, Factory, ...) marks every one that has no
egress swap. AWS keeps its own sentence; others read 'Orca can't swap {{name}} at the network edge,
...'. The control-plane test now walks the server's agent keys, this list and the desktop's Agent
API key presets: each is swapped, or flagged.
2026-11-10 05:43:43 -08:00
Neil 732c04ddd7 Merge remote-tracking branch 'private/cloud/integration' into cloud/w6-a9-recipe
# Conflicts:
#	cloud/apps/sandbox-control/image/vm-scripts.generated.ts
2026-11-10 05:43:06 -08:00
Neil 7fa51b6a0b Merge pull request #246 from nwparker/np4/github-repo-access
NP-4: GitHub repo access, preselected install URLs, install requests, webhook auto-start, repository list (N2/N3/N12, D2/D3)
2026-11-10 05:42:57 -08:00
Neil 4c1ee40ffe Merge remote-tracking branch 'private/cloud/integration' into cloud/w6-codex-broker
# Conflicts:
#	cloud/apps/sandbox-control/image/vm-scripts.generated.ts
2026-11-10 05:32:29 -08:00
Neil 849a12773b fix(cloud): find agent CLIs in /opt/orca/tools/bin; delete note only for running sandboxes; test retry skip
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.
2026-11-10 05:27:41 -08:00
Neil 5b9d2735e0 fix(cloud): OpenCode with no credentials shows 'Connect a provider' instead of failing on Copilot or Zen
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'.
2026-11-10 05:23:31 -08:00
Neil 1bca45b536 fix(sandbox-control): Codex broker review fixes (M1-M4, L5-L8)
- 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.
2026-11-10 05:22:43 -08:00
Neil 100b9d4699 feat(sandbox-control): tell billing the GitHub owner/user ids of each bind (trial abuse keys, POST /v1/internal/trial/github) 2026-11-10 05:19:45 -08:00
Neil 521c9fe3c5 feat(cloud): desktop repo access hook, GitHub row model and repo list plumbing (NP-4 N2/N12, D2/D3) 2026-11-10 05:19:45 -08:00
Neil a4e0b7a656 feat(sandbox-control): GitHub repo access, preselected install URLs, install requests, repository list (NP-4 N2/N3/N12, D2) 2026-11-10 05:19:44 -08:00
Neil 4af8f7fe04 feat(sandbox-control): Codex account broker (contract D11, flag S2)
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.
2026-11-10 05:03:50 -08:00
Neil a81afb57e0 Merge remote-tracking branch 'private/cloud/integration' into cloud/w6-a9-recipe
# Conflicts:
#	cloud/apps/sandbox-control/image/vm-scripts.generated.ts
2026-11-10 04:49:31 -08:00
Neil 40b33811b5 Merge pull request #242 from nwparker/cloud/w6-agent-accounts
W6: sign in once via your Orca account — Claude account store + egress-time Source (D10/D11/D13/D14)
2026-11-10 04:48:11 -08:00
Neil aedf0809eb Merge remote-tracking branch 'private/cloud/integration' into cloud/w6-a9-recipe
# Conflicts:
#	src/renderer/src/i18n/locales/en.json
2026-11-10 04:43:10 -08:00
Neil de8a65b7f1 Merge pull request #241 from nwparker/cloud/w9-eval
feat(cloud): acceptance — phone UI read-asleep (sim), cloud run.ports headless, nonconcurrent per #233
2026-11-10 04:41:08 -08:00
Neil 0832550290 feat(cloud): provider-specific onUnauthorized hook (default: revoke) 2026-11-10 04:39:25 -08:00
Neil 5e1a5d7ac5 refactor(cloud): drop env-build scrub-on-clone (spec v4 D14); keep sign-out before delete 2026-11-10 04:33:45 -08:00
Neil dd34314dc0 feat(billing): bill overage by the minute, put sandboxes to sleep at the cap (U22, U24)
- 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."
2026-11-10 04:33:07 -08:00
Neil ab106b93cc feat(cloud): cloud run.ports passes on the #233 golden (3000 + second worktree block, stable across sleep) 2026-11-10 04:29:04 -08:00
Neil 8b2701636c feat(cloud): acceptance headless cloud run.ports (first workspace 3000, second worktree block, sleep/wake) 2026-11-10 04:29:04 -08:00
Neil 2a0790deef docs(cloud): run.ports cloud fail is desktop/golden skew pending #233 golden 2026-11-10 04:29:04 -08:00