From d009ec750c2560948465e1338c7d61a4c901618c Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 10 Nov 2026 04:15:14 -0800 Subject: [PATCH 1/5] chore(acceptance): W9 headless iOS sim run for life.read-asleep-phone (pass), lift audit cap --- cloud/apps/acceptance/findings.json | 7 +- cloud/apps/acceptance/mobile/cdp.mjs | 101 +++++++++++++ .../apps/acceptance/mobile/read-asleep-sim.ts | 135 ++++++++++++++++++ 3 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 cloud/apps/acceptance/mobile/cdp.mjs create mode 100644 cloud/apps/acceptance/mobile/read-asleep-sim.ts diff --git a/cloud/apps/acceptance/findings.json b/cloud/apps/acceptance/findings.json index 738775c3c29..77a1dabee90 100644 --- a/cloud/apps/acceptance/findings.json +++ b/cloud/apps/acceptance/findings.json @@ -1,7 +1,7 @@ { "findings": [ "Wake delivery (target p95 < 6 s) — MET in pdx1: after core U17/U18 (default region pdx1, Standard 8 vCPU/16 GB, golden 63b6999), n=20, persistent connection. Wake to echo p50 5.04 s, p95 5.85 s, p99 5.94 s; 0/20 slow hosts. Pinned to iad1 at 8/16: p50 5.31, p95 10.59, p99 11.80 s, with 4/20 slow hosts. BEFORE (iad1 4/8): 5.28 / 8.99 / 9.40 s. Region, not size, removes the tail. From this Mac the first terminal list (0.18 vs 0.44 s) and echo (0.07 vs 0.15 s) are also faster in pdx1. Create p50: pdx1 2.03 s, iad1-pinned 4.26 s (pinned creates seem to skip the warm pool).", - "Read while asleep (gap 5, #189/#218), verified by W9 live: a teammate decrypted a sleeping team workspace (git incl. a new file, PR, sessions) with no wake: the Vercel session stayed stopped. 20 reads p50 89 ms, max 131 ms, vs about 5-6 s to wake. An outsider gets 404, and the plaintext marker is not in D1. Removal/private access control and the phone view are owner-reported (capped).", + "Read while asleep (gap 5, #189/#218), verified by W9 live: a teammate decrypted a sleeping team workspace (git incl. a new file, PR, sessions) with no wake: the Vercel session stayed stopped. 20 reads p50 89 ms, max 131 ms, vs about 5-6 s to wake. An outsider gets 404, and the plaintext marker is not in D1. Removal/private access control is covered by life.read-asleep-access. The phone view was checked in W9's own headless simulator run: “Latest from this workspace · As of just now” showed the marker branch and NOTES.md, and Vercel stayed stopped.", "Read-asleep access, triggered by W9 itself: the operator members/removed route revokes the member; their pre-removal token and a fresh token both get 401, and their wrapped keys go 1 to 0, while the other teammate still reads. Making the workspace private gives that teammate 404 and 0 keys. Phone protocol: device token plus mobile reader key reads a sleeping private workspace in 125 ms, and unpairing revokes access.", "Independent audit corrections (2026-11-10, reports/conductor-parity-audit.html): five rows are capped at partial because they passed on a weaker bar than the Conductor column: real-model agent run, subscription sign-in, default machine size, and the download and remote-desktop UIs. Four rows Conductor has that our bar left out are added as not landed: local core workflow, Run actions with per-workspace ports, read-while-asleep from any device, and per-turn code checkpoints. Our 'pass' counts measure the team's own bar. Every cloud result is on dev Workers, Stripe TEST mode and dev/staging tokens; nothing is shipped (mvp-tracker §5 launch items are all open).", "Agents and machine size (2026-11-10): image.agents: Claude Code, Codex, Cursor, OpenCode, Gemini and Copilot all answer --version on a fresh VM. agent.keys-swap: stand-in Gemini/Cursor keys exist on the VM only as placeholders; the provider sees the swapped key (Gemini 400 API_KEY_INVALID, Cursor 401); a foreign key or a disallowed path is refused (403); the stand-in values appear in 0 files and 0 process environments. sub.size cap lifted: Standard is now 8 vCPU / 16 GB (core U18, #194), and machineSize large maps to standard.", @@ -28,7 +28,6 @@ "auditCaps": { "agent.real-task": "Proven only with moonshotai/kimi-k2 through the Claude Code CLI. Per the user decision (2026-11-10) there are no AI Gateway real-model runs; lifted by the user's own e2e session with their own credentials, and on the coordinator's e2e checklist. Image now also has Cursor, OpenCode, Gemini and Copilot (image.agents); W9 has shown Cursor and Gemini launch with their keys swapped at egress (agent.keys-swap), but no real-model run for those two.", "agent.signin": "Auth folders persist across sleep/wake and rollover, but subscription OAuth on a VM was never exercised. This cap is lifted by the user's own e2e session with their own credentials (after their review of #40228). It is on the coordinator's e2e checklist.", - "agent.more-agents": "Owner-reported (gap-agents) real-task runs on kimi-k2 via the gateway. W9 is not re-running them (no gateway spend per user decision). Lifted by the user e2e session with their own credentials.", - "life.read-asleep-phone": "Phone protocol verified by W9 (device token, mobile reader key, decrypt, no wake, revoked on unpair). The phone UI rendering (\"Latest from this workspace\") is still owner-reported (gap5 simulator screenshots); a W9 simulator UI run is pending." + "agent.more-agents": "Owner-reported (gap-agents) real-task runs on kimi-k2 via the gateway. W9 is not re-running them (no gateway spend per user decision). Lifted by the user e2e session with their own credentials." } -} \ No newline at end of file +} diff --git a/cloud/apps/acceptance/mobile/cdp.mjs b/cloud/apps/acceptance/mobile/cdp.mjs new file mode 100644 index 00000000000..33083975541 --- /dev/null +++ b/cloud/apps/acceptance/mobile/cdp.mjs @@ -0,0 +1,101 @@ +// Headless helpers over Metro's CDP for the Orca mobile dev client on a simulator (no GUI). +// Adapted from W8/gap5's /tmp/w8/cdp-*.mjs. Port: W9M_METRO_PORT (default 8131). +// node cdp.mjs identity-install stores {seedB64, enrollment} as the app's Orca Cloud identity +// node cdp.mjs identity-clear removes the stored identity (the app makes a fresh, unenrolled key) +// node cdp.mjs texts rendered text strings containing (read-only) +// node cdp.mjs eval '' +// Hermes's CDP does not await promises: the result is parked on a global and read back. +import { readFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import { fileURLToPath } from 'node:url' +import { resolve, dirname } from 'node:path' + +const here = dirname(fileURLToPath(import.meta.url)) +const require = createRequire(resolve(here, '../../../../mobile/package.json')) +const WebSocket = require('ws') +const PORT = process.env.W9M_METRO_PORT ?? '8131' +const KEY = 'orca.cloud-device.v1' +const [mode, arg] = process.argv.slice(2) + +async function session() { + const targets = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json() + const target = targets.find((t) => t.appId === 'com.stably.orca.mobile') ?? targets[0] + if (!target) { + throw new Error('no CDP target (is the app running?)') + } + const ws = new WebSocket(target.webSocketDebuggerUrl) + await new Promise((ok, fail) => { + ws.on('open', ok) + ws.on('error', fail) + }) + let n = 0 + const pending = new Map() + ws.on('message', (data) => { + const msg = JSON.parse(String(data)) + pending.get(msg.id)?.(msg) + }) + const evaluate = (expression) => + new Promise((ok) => { + const id = ++n + pending.set(id, ok) + ws.send(JSON.stringify({ id, method: 'Runtime.evaluate', params: { expression, returnByValue: true } })) + }).then((m) => m.result?.result?.value ?? m.result?.exceptionDetails?.text ?? null) + return { evaluate, close: () => ws.close() } +} + +const timer = setTimeout(() => { + console.log('"cdp timeout"') + process.exit(2) +}, 20000) +const s = await session() +try { + if (mode === 'identity-install' || mode === 'identity-clear') { + const value = mode === 'identity-install' ? readFileSync(arg, 'utf8') : null + await s.evaluate(`(function () { + globalThis.__w9Identity = { pending: true } + var m = globalThis.expo && globalThis.expo.modules && globalThis.expo.modules.ExpoSecureStore + if (!m) { globalThis.__w9Identity = { error: 'no ExpoSecureStore' }; return } + var options = { keychainAccessible: m.WHEN_UNLOCKED_THIS_DEVICE_ONLY } + var work = ${JSON.stringify(value)} === null + ? m.deleteValueWithKeyAsync(${JSON.stringify(KEY)}, options) + : m.setValueWithKeyAsync(${JSON.stringify(value)}, ${JSON.stringify(KEY)}, options) + work.then(function () { globalThis.__w9Identity = { ok: true } }, function (e) { globalThis.__w9Identity = { error: String(e) } }) + })()`) + await new Promise((r) => setTimeout(r, 1500)) + console.log(await s.evaluate('JSON.stringify(globalThis.__w9Identity)')) + } else if (mode === 'texts') { + console.log( + await s.evaluate(`(() => { + const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ + if (!hook) return '"no hook"' + const out = new Set() + const take = (v) => { + if (typeof v === 'string' && v.includes(${JSON.stringify(arg ?? '')})) out.add(v) + else if (Array.isArray(v)) v.forEach(take) + } + for (const id of hook.renderers.keys()) { + const roots = hook.getFiberRoots(id) + roots && roots.forEach((r) => { + const stack = [r.current] + while (stack.length) { + const f = stack.pop() + if (!f) continue + if (f.memoizedProps) { take(f.memoizedProps.children); take(f.memoizedProps.text); take(f.memoizedProps.accessibilityLabel) } + if (f.child) stack.push(f.child) + if (f.sibling) stack.push(f.sibling) + } + }) + } + return JSON.stringify([...out].slice(0, 80)) + })()`) + ) + } else if (mode === 'eval') { + console.log(JSON.stringify(await s.evaluate(arg))) + } else { + console.log('usage: cdp.mjs identity-install | identity-clear | texts | eval ') + } +} finally { + s.close() + clearTimeout(timer) +} +process.exit(0) diff --git a/cloud/apps/acceptance/mobile/read-asleep-sim.ts b/cloud/apps/acceptance/mobile/read-asleep-sim.ts new file mode 100644 index 00000000000..d20f8b10c6a --- /dev/null +++ b/cloud/apps/acceptance/mobile/read-asleep-sim.ts @@ -0,0 +1,135 @@ +// W9 simulator run for life.read-asleep-phone: the Orca mobile app shows a SLEEPING workspace's +// "Latest from this workspace" without waking it. Driven in phases so the sim steps (CDP identity +// install, deep link, screenshot) run between them; see run-read-asleep-sim.sh. +// tsx mobile/read-asleep-sim.ts enroll device session (Ed25519 phone key) for the owner; writes the identity file (0600) +// tsx mobile/read-asleep-sim.ts readerkey waits for the app's own mobile reader key (GET /v1/reader-keys) +// tsx mobile/read-asleep-sim.ts workspace creates the workspace, writes NOTES.md + a commit with a marker, sleeps it +// tsx mobile/read-asleep-sim.ts provider Vercel GET of the sandbox (no-wake check) +// tsx mobile/read-asleep-sim.ts cleanup destroys the sandbox, revokes the device session + reader key, Vercel 404 check +// State (ids, timings; no secrets) lives in $W9M_DIR/state.json; the phone seed only in identity.json (0600). +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { createPrivateKey, createPublicKey, randomBytes } from 'node:crypto' +import { control, vercel, vercelRest } from '../src/clients' +import { config } from '../src/env' +import type { Identity } from '../src/tokens' + +const DIR = process.env.W9M_DIR ?? '/tmp/w9m' +const STATE = join(DIR, 'state.json') +const owner: Identity = { sub: process.env.W9M_OWNER ?? 'w9-mobile' } +const state: Record = existsSync(STATE) ? JSON.parse(readFileSync(STATE, 'utf8')) : {} +const save = () => writeFileSync(STATE, JSON.stringify(state, null, 2)) +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) +const providerStatus = async (id: string) => { + const r = await vercelRest('GET', `/v2/sandboxes/orca-dev-${id}`, { projectId: config.vercelProjectId }) + return { http: r.status, sandbox: r.json?.sandbox?.status ?? null, session: r.json?.session?.status ?? null } +} + +const phase = process.argv[2] +if (phase === 'enroll') { + // The app's identity is a 32-byte Ed25519 seed (tweetnacl fromSeed == RFC 8032 private key). + const seed = randomBytes(32) + const der = Buffer.concat([Buffer.from('302e020100300506032b657004220420', 'hex'), seed]) + const pub = Buffer.from(createPublicKey(createPrivateKey({ key: der, format: 'der', type: 'pkcs8' })).export({ format: 'jwk' }).x!, 'base64') + const t0 = performance.now() + const r = await control.as(owner, 'POST', '/v1/device-sessions', { label: 'W9 eval iPhone (sim)', platform: 'ios', publicKeyB64: pub.toString('base64') }) + const ds = r.body?.deviceSession?.id + console.log('enroll', r.status, ds ?? JSON.stringify(r.body).slice(0, 200)) + if (!ds) process.exit(1) + writeFileSync( + join(DIR, 'identity.json'), + JSON.stringify({ seedB64: seed.toString('base64'), enrollment: { controlPlaneUrl: config.controlPlaneUrl, deviceSessionId: ds, label: 'W9 eval iPhone (sim)', enrolledAt: Date.now() } }), + { mode: 0o600 } + ) + Object.assign(state, { owner: `dev:${owner.sub}`, deviceSessionId: ds, enrollMs: Math.round(performance.now() - t0) }) + save() +} else if (phase === 'readerkey') { + for (let i = 0; i < 40; i++) { + const r = await control.as(owner, 'GET', '/v1/reader-keys') + const keys: any[] = r.body?.keys ?? r.body?.readerKeys ?? [] + const mobile = keys.find((k) => (k.deviceKind ?? k.kind) === 'mobile') + if (mobile) { + state.readerKeyId = mobile.id + save() + console.log('reader key', mobile.id, mobile.deviceKind ?? mobile.kind, mobile.deviceLabel ?? mobile.label) + process.exit(0) + } + if (i === 0) console.log('reader-keys', r.status, `${keys.length} keys`) + await sleep(3000) + } + console.log('no mobile reader key registered by the app') + process.exit(1) +} else if (phase === 'workspace') { + const marker = `W9MOB${randomBytes(3).toString('hex').toUpperCase()}` + const t0 = performance.now() + const created = await control.as(owner, 'POST', '/v1/sandboxes', { + workspaceId: `w9-mobile-${marker.toLowerCase()}`, + machineSize: 'standard', + title: `W9 phone read ${marker}`, + repo: { url: 'https://github.com/octocat/Hello-World' } + }) + const id = created.body?.sandbox?.id + console.log('create', created.status, created.ms, 'ms', id ?? JSON.stringify(created.body).slice(0, 200)) + if (!id) process.exit(1) + Object.assign(state, { marker, sandboxId: id, createMs: created.ms }) + save() + const root = created.body.connection.projectRoot + const script = [ + `cd ${root}`, + `git checkout -q -b w9-${marker}`, + `printf '# Notes\\n\\nW9 phone read-asleep marker: ${marker}\\n' > NOTES.md`, + `git add NOTES.md`, + `git -c user.name='W9 eval' -c user.email='w9@example.invalid' commit -q -m 'Add NOTES.md (${marker})'`, + `printf 'Edited after the commit: ${marker}\\n' >> NOTES.md`, + `git log --oneline -1; git status --short; git branch --show-current` + ].join(' && ') + const ex: any = await vercel().runCommand(created.body.sandbox.session!.id, { command: 'bash', args: ['-c', script], timeoutMs: 60_000 }) + const out = String(ex?.stdout ?? ex?.output ?? JSON.stringify(ex)).trim() + console.log('vm:', out.slice(0, 400)) + state.vmOutput = out.slice(0, 400) + const s0 = performance.now() + const sus = await control.as(owner, 'POST', `/v1/sandboxes/${id}/suspend`) + state.suspendStatus = sus.status + state.suspendMs = Math.round(performance.now() - s0) + state.readSnapshotAt = sus.body?.sandbox?.readSnapshotAt ?? null + state.afterSuspend = await providerStatus(id) + state.workspaceSetupMs = Math.round(performance.now() - t0) + save() + console.log('suspend', sus.status, state.suspendMs, 'ms; provider', JSON.stringify(state.afterSuspend)) + const g = await control.as(owner, 'GET', `/v1/sandboxes/${id}`) + console.log('control-plane view', g.body?.sandbox?.status, 'readSnapshotAt', g.body?.sandbox?.readSnapshotAt ?? g.body?.readSnapshotAt ?? '?') +} else if (phase === 'provider') { + const p = await providerStatus(state.sandboxId) + const g = await control.as(owner, 'GET', `/v1/sandboxes/${state.sandboxId}`) + const label = process.argv[3] ?? 'check' + state.provider = { ...(state.provider ?? {}), [label]: { ...p, controlPlane: g.body?.sandbox?.status, at: new Date().toISOString() } } + save() + console.log(label, JSON.stringify(state.provider[label])) +} else if (phase === 'cleanup') { + const out: Record = {} + if (state.sandboxId) { + const d = await control.as(owner, 'DELETE', `/v1/sandboxes/${state.sandboxId}`) + out.destroy = d.status + for (let i = 0; i < 20; i++) { + const p = await providerStatus(state.sandboxId) + out.vercelAfterDestroy = p.http + if (p.http === 404) break + await sleep(3000) + } + } + if (state.deviceSessionId) { + out.revokeDeviceSession = (await control.as(owner, 'DELETE', `/v1/device-sessions/${state.deviceSessionId}`)).status + } + if (state.readerKeyId) { + out.readerKeyDelete = (await control.as(owner, 'DELETE', `/v1/reader-keys/${state.readerKeyId}`)).status + } + const keys = await control.as(owner, 'GET', '/v1/reader-keys') + out.liveReaderKeysLeft = (keys.body?.keys ?? keys.body?.readerKeys ?? []).length + const list = await control.as(owner, 'GET', '/v1/sandboxes') + out.liveSandboxesLeft = (list.body?.sandboxes ?? []).filter((s: any) => !['destroyed', 'deleted'].includes(s.status)).length + state.cleanup = out + save() + console.log(JSON.stringify(out)) +} else { + console.log('phases: enroll | readerkey | workspace | provider [label] | cleanup') +} From c6484c005bf4e9f4185bf93c416618f9da6288be Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 10 Nov 2026 04:20:10 -0800 Subject: [PATCH 2/5] feat(cloud): desktop check for #233 run-action semantics and cloud port block (W9D_ONLY=run-ports2) - 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 --- cloud/apps/acceptance/desktop/run-desktop.mjs | 205 +++++++++++++++++- 1 file changed, 203 insertions(+), 2 deletions(-) diff --git a/cloud/apps/acceptance/desktop/run-desktop.mjs b/cloud/apps/acceptance/desktop/run-desktop.mjs index dd0b5d787a3..8ce06a5a754 100644 --- a/cloud/apps/acceptance/desktop/run-desktop.mjs +++ b/cloud/apps/acceptance/desktop/run-desktop.mjs @@ -1029,7 +1029,7 @@ async function main() { results.notes.push('test repo not clonable here; used a local repo with the same origin URL') } created.tmp = tmp - if (process.env.W9D_ONLY === 'run-actions') { + if (process.env.W9D_ONLY === 'run-actions' || process.env.W9D_ONLY === 'run-ports2') { writeFileSync(path.join(repoDir, 'orca.yaml'), runActionsYaml(tmp)) execFileSync('git', ['-C', repoDir, 'add', 'orca.yaml'], { stdio: 'ignore' }) execFileSync('git', ['-C', repoDir, '-c', 'user.email=w9@orca.dev', '-c', 'user.name=W9', 'commit', '-qm', 'orca.yaml run actions'], { stdio: 'ignore' }) @@ -2024,7 +2024,7 @@ async function runMenuItem(step, actionName, item) { const isItem = el.getAttribute('role') === 'menuitem' const text = (el.textContent ?? '').replace(/\s+/g, ' ').trim() if (!isItem && el.children.length <= 3 && text.startsWith(actionName) && !text.startsWith(`${actionName} `.repeat(2))) under = true - else if (!isItem && under && el.children.length <= 3 && /^(Web|Echo loop)\b/.test(text) && !text.startsWith(actionName)) under = false + else if (!isItem && under && el.children.length <= 3 && /^(Web|Echo loop|Env)\b/.test(text) && !text.startsWith(actionName)) under = false if (under && isItem && text === item) { el.setAttribute('data-w9d-run-item', '1') return 'ok' @@ -2036,6 +2036,203 @@ async function runMenuItem(step, actionName, item) { await page.locator('[data-w9d-run-item="1"]').first().click() } +/** #233: nonconcurrent stops only the same action elsewhere; cloud port block is 3000 and stable. */ +async function runPorts2Flow() { + const A = `${wsName}-a` + const B = `${wsName}-b` + const wsA = {} + const wsB = {} + await runStep('run.nonconcurrent', 'echo-loop (nonconcurrent) started in B stops echo-loop in A; concurrent web keeps running and serving in A and B (#233)', async (step) => { + Object.assign(wsA, await createLocalWorktree(step, A)) + await waitFor(step, async () => (await runOf(wsA.id, 'web'))?.status === 'running', 90_000) + Object.assign(wsB, await createLocalWorktree(step, B)) + await waitFor(step, async () => (await runOf(wsB.id, 'web'))?.status === 'running', 90_000) + const envA = await terminalEnv(step, A) + const envB = await terminalEnv(step, B) + wsA.port = Number(envA?.ORCA_PORT) + wsB.port = Number(envB?.ORCA_PORT) + step.ports = { A: wsA.port, B: wsB.port } + step.before = { webA: (await runOf(wsA.id, 'web'))?.status, webB: (await runOf(wsB.id, 'web'))?.status, serveA: await httpOk(wsA.port), serveB: await httpOk(wsB.port) } + await (await rowOf(A, 'local')).click() + await sleep(800) + await runMenuItem(step, 'Echo loop', 'Run') + const aOn = await waitFor(step, async () => (await runOf(wsA.id, 'echo-loop'))?.status === 'running', 20_000) + await (await rowOf(B, 'local')).click() + await sleep(800) + const t0 = Date.now() + await runMenuItem(step, 'Echo loop', 'Run') + const res = await waitFor(step, async () => { + const a = await runOf(wsA.id, 'echo-loop') + const b = await runOf(wsB.id, 'echo-loop') + return b?.status === 'running' && (!a || a.status === 'stopped' || a.status === 'exited') ? true : null + }, 20_000) + step.timings.bStartToAStoppedMs = res.v ? Date.now() - t0 : null + await sleep(3000) + step.after = { + echoA: (await runOf(wsA.id, 'echo-loop'))?.status ?? 'gone', + echoB: (await runOf(wsB.id, 'echo-loop'))?.status ?? 'gone', + webA: (await runOf(wsA.id, 'web'))?.status ?? 'gone', + webB: (await runOf(wsB.id, 'web'))?.status ?? 'gone', + serveA: await httpOk(wsA.port), + serveB: await httpOk(wsB.port) + } + await runMenu(step) + await shot(step, 'b-menu') + await page.keyboard.press('Escape') + const webKept = step.after.webA === 'running' && step.after.webB === 'running' && step.after.serveA === 200 && step.after.serveB === 200 + step.status = aOn.v && res.v && webKept ? 'pass' : 'fail' + step.detail = `ports A ${wsA.port}, B ${wsB.port}; before ${JSON.stringify(step.before)}; echo-loop in A running: ${Boolean(aOn.v)}; started in B -> A's echo-loop stopped in ${step.timings.bStartToAStoppedMs}ms; after ${JSON.stringify(step.after)} (web kept running and serving in both: ${webKept})` + }) + + // ---- cloud: ORCA_PORT=3000 for the first workspace's terminal and run action, also after Sleep -> wake. + await store(() => window.__store.getState().connectCurrentOrcaProfile()) + await sleep(1500) + let cws = null + await runStep('run.ports', 'Cloud: the first workspace terminal and run action see ORCA_PORT=3000, still 3000 after Sleep -> wake; a second worktree on the same VM gets another block (if the UI allows)', async (step) => { + const sub = {} + step.substeps = sub + const w = await createExtraWorkspace(step, `${wsName}-cl`, { blankAgent: true }) + const wt = await worktreeByName(w.name) + cws = { ...w, id: wt?.id } + step.sandboxId = w.sandboxId + const repoPath = await store((id) => { const s = window.__store.getState(); const r = s.repos.find((x) => (s.worktreesByRepo?.[x.id] ?? []).some((t) => t.id === id)); return r?.path ?? null }, cws.id) + step.cloudRepoPath = repoPath + // orca.yaml in the VM's primary checkout (committed there, so worktrees made from it have it too). + const yaml = [ + 'scripts:', + ' setup: "echo SETUP P=$ORCA_PORT C=$ORCA_PORT_COUNT K=$CONDUCTOR_PORT > /tmp/w9-setup-env-$(basename $PWD)"', + ' run:', + ' env:', + ' command: "echo RUNENV P=$ORCA_PORT C=$ORCA_PORT_COUNT K=$CONDUCTOR_PORT; sleep 600"', + ' default: true', + '' + ].join('\n') + const b64 = Buffer.from(yaml).toString('base64') + await openNewTerminalTab() + await waitShellPrompt(90_000) + await focusTerminal() + const rp = repoPath ?? '/vercel/sandbox/project' + await page.keyboard.type(`clear; echo ${b64} | base64 -d > ${rp}/orca.yaml && git -C ${rp} add orca.yaml && git -C ${rp} -c user.email=w9@orca.dev -c user.name=W9 commit -qm w9-orca-yaml && cp ${rp}/orca.yaml ./orca.yaml && echo YAMLOK; echo W9ENV P=$ORCA_PORT C=$ORCA_PORT_COUNT K=$CONDUCTOR_PORT\n`) + // The VM's port registry and orcad build, to explain which workspace holds the launcher's block. + await sleep(1500) + await page.keyboard.type('clear; echo W9REG; cat /vercel/.orcad-data/workspace-port-blocks.json; echo; echo W9REGEND\n') + const reg = await waitFor(step, async () => (await visibleTerminals()).join('\n').match(/W9REG\s*\n([\s\S]*?)\n\s*W9REGEND/)?.[1]?.replace(/\s+/g, ' ') ?? null, 15_000) + step.vmRegistry = reg.v + await focusTerminal() + await page.keyboard.type('clear; echo W9ENV P=$ORCA_PORT C=$ORCA_PORT_COUNT K=$CONDUCTOR_PORT\n') + const env1 = await waitFor(step, async () => { + const m = (await visibleTerminals()).join('\n').match(/W9ENV P=(\d*) C=(\d*) K=(\d*)\s*$/m) + return m ? { ORCA_PORT: m[1], ORCA_PORT_COUNT: m[2], CONDUCTOR_PORT: m[3] } : null + }, 30_000) + step.terminalEnv = env1.v + sub.terminal3000 = env1.v?.ORCA_PORT === '3000' && env1.v?.ORCA_PORT_COUNT === '10' && env1.v?.CONDUCTOR_PORT === '3000' + // Run action env. + await (await rowOf(A, 'local')).click() + await sleep(800) + await (await rowOf(cws.name)).click() + await page.evaluate(() => window.dispatchEvent(new Event('focus'))) + await waitFor(step, async () => /Env/.test(await runButton().innerText().catch(() => '')), 30_000) + const readRunEnv = async () => { + const r = await waitFor(step, async () => { + const m = (await visibleTerminals()).join('\n').match(/RUNENV P=(\d*) C=(\d*) K=(\d*)/) + return m ? { ORCA_PORT: m[1], ORCA_PORT_COUNT: m[2], CONDUCTOR_PORT: m[3] } : null + }, 30_000) + return r.v + } + await runButton().getByRole('button', { name: /^Run Env/ }).click() + await waitFor(step, async () => (await runOf(cws.id, 'env'))?.status === 'running', 60_000) + step.runActionEnv = await readRunEnv() + sub.runAction3000 = step.runActionEnv?.ORCA_PORT === '3000' + await shot(step, 'run-env') + step.setupEnvFirst = 'not testable: the first workspace is created from the GitHub test repo, which has no orca.yaml, so no setup runs for it' + // Sleep -> wake, then terminal + run action again. + await openRowMenu(cws.name) + await page.getByRole('menuitem', { name: /^(Suspend VM|Sleep)$/ }).first().click() + await page.getByRole('dialog').getByRole('button', { name: /^(Sleep|Suspend)/ }).click({ timeout: 1500 }).catch(() => {}) + const asleep = await waitSandboxStatus(cws.sandboxId, ['suspended', 'sleeping', 'stopped'], 180_000) + step.timings.sleepMs = asleep.ms + await sleep(2000) + const tabBeforeSleep = (await runOf(cws.id, 'env'))?.tabId + const t0 = Date.now() + await (await rowOf(cws.name)).click() + // Back = relaunched in a new tab after the wake. + const back = await waitFor(step, async () => { const r = await runOf(cws.id, 'env'); return r?.status === 'running' && r.tabId !== tabBeforeSleep }, 180_000) + step.timings.wakeClickToRunActionBackMs = back.v ? Date.now() - t0 : null + await page.getByText('Run: Env', { exact: true }).first().click().catch(() => {}) + await sleep(1000) + step.runActionEnvAfterWake = await readRunEnv() + await openNewTerminalTab() + await waitShellPrompt(90_000) + await focusTerminal() + await page.keyboard.type('clear; echo W9ENV P=$ORCA_PORT C=$ORCA_PORT_COUNT K=$CONDUCTOR_PORT\n') + const env2 = await waitFor(step, async () => { + const m = (await visibleTerminals()).join('\n').match(/W9ENV P=(\d*) C=(\d*) K=(\d*)\s*$/m) + return m ? { ORCA_PORT: m[1], ORCA_PORT_COUNT: m[2], CONDUCTOR_PORT: m[3] } : null + }, 30_000) + step.terminalEnvAfterWake = env2.v + sub.stillAfterWake = env2.v?.ORCA_PORT === '3000' && step.runActionEnvAfterWake?.ORCA_PORT === '3000' + await shot(step, 'after-wake') + // A second worktree on the same VM: offered by the composer? + const d = await openComposer(step, 'second-worktree-composer') + await d.getByRole('combobox', { name: 'Run on' }).click() + const targets = page.getByRole('listbox', { name: 'Run targets' }) + await targets.waitFor() + step.runTargetsWithCloud = (await targets.getByRole('option').allInnerTexts()).map((t) => t.replace(/\s+/g, ' ').trim()) + await shot(step, 'run-targets') + // Only a target for the existing VM counts (not "Orca Cloud", which starts a new sandbox). + const sameVm = targets.getByRole('option').filter({ hasText: new RegExp(`${cws.name}|sandbox ·|environment`, 'i') }).filter({ hasNotText: /Orca Cloud|Local Mac|Connect/ }).first() + if (await sameVm.isVisible().catch(() => false)) { + step.secondWorktree = 'offered' + await sameVm.click() + await selectBlankAgent(d) + const n2 = `${wsName}-cl2` + await fillName(d, n2) + await d.getByRole('button', { name: /^Create/ }).click() + const w2 = await waitFor(step, () => worktreeByName(n2), 120_000) + if (w2.v) { + await openNewTerminalTab() + await waitShellPrompt(90_000) + await focusTerminal() + await page.keyboard.type('clear; echo W9ENV P=$ORCA_PORT C=$ORCA_PORT_COUNT K=$CONDUCTOR_PORT; cat /tmp/w9-setup-env-* 2>/dev/null\n') + const env3 = await waitFor(step, async () => { + const m = (await visibleTerminals()).join('\n').match(/W9ENV P=(\d*) C=(\d*) K=(\d*)/) + return m ? { ORCA_PORT: m[1], ORCA_PORT_COUNT: m[2], CONDUCTOR_PORT: m[3] } : null + }, 30_000) + step.secondWorktreeEnv = env3.v + step.setupEnvLines = ((await visibleTerminals()).join('\n').match(/SETUP P=\S* C=\S* K=\S*/g) ?? []) + sub.secondWorktreeOtherBlock = Boolean(env3.v?.ORCA_PORT) && env3.v.ORCA_PORT !== '3000' + } else { + step.secondWorktree = 'offered, but no worktree appeared within 120s' + } + } else { + step.secondWorktree = 'not testable: the composer offers no target for an existing cloud VM (only a new Orca Cloud sandbox)' + await page.keyboard.press('Escape').catch(() => {}) + await page.keyboard.press('Escape').catch(() => {}) + } + step.status = Object.values(sub).every(Boolean) ? 'pass' : 'fail' + step.detail = `cloud first workspace: terminal ${JSON.stringify(step.terminalEnv)}, run action ${JSON.stringify(step.runActionEnv)}; Sleep -> ${asleep.status} ${asleep.ms}ms; click to wake -> run action back ${step.timings.wakeClickToRunActionBackMs}ms, then terminal ${JSON.stringify(step.terminalEnvAfterWake)}, run action ${JSON.stringify(step.runActionEnvAfterWake)}; setup: ${step.setupEnvFirst}; second worktree on the same VM: ${step.secondWorktree}${step.secondWorktreeEnv ? ` -> ${JSON.stringify(step.secondWorktreeEnv)}, setup env ${JSON.stringify(step.setupEnvLines)}` : ''}; run targets ${JSON.stringify(step.runTargetsWithCloud)}; ${JSON.stringify(sub)}` + }) + + if (cws) { + await runStep('life.destroy', 'Delete the cloud workspace', async (step) => { + step.sandboxId = cws.sandboxId + await page.keyboard.press('Escape').catch(() => {}) + const o = await uiDelete(step, cws.name, 'delete', 120_000) + const w = await waitSandboxStatus(cws.sandboxId, ['destroyed'], 120_000) + step.status = o.kind === 'removed' && w.status === 'destroyed' ? 'pass' : 'fail' + step.detail = `UI Delete -> ${o.kind} in ${o.ms}ms; sandbox -> ${w.status}` + }) + } + for (const n of [A, B]) { + if (await rowShown(n, 'local')) { + await openRowMenu(n, 'local').catch(() => {}) + await page.getByRole('menuitem', { name: /^Delete/ }).click().catch(() => {}) + await page.getByRole('dialog').getByRole('button', { name: /^(Force )?Delete( Workspace)?$/ }).first().click().catch(() => {}) + await waitRowGone(n, 'local', 30_000).catch(() => {}) + } + } +} + async function runActionsFlow() { const A = `${wsName}-a` const B = `${wsName}-b` @@ -2309,6 +2506,10 @@ async function runActionsFlow() { * and a normal Delete. */ async function targetedFlow() { + if (process.env.W9D_ONLY === 'run-ports2') { + await runPorts2Flow() + return + } if (process.env.W9D_ONLY === 'run-actions') { await runActionsFlow() return From 2a0790deef146c657c3fe2906d2f8fb65a7a53d3 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 10 Nov 2026 04:20:49 -0800 Subject: [PATCH 3/5] docs(cloud): run.ports cloud fail is desktop/golden skew pending #233 golden --- cloud/apps/acceptance/findings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/apps/acceptance/findings.json b/cloud/apps/acceptance/findings.json index 77a1dabee90..144e7547032 100644 --- a/cloud/apps/acceptance/findings.json +++ b/cloud/apps/acceptance/findings.json @@ -1,6 +1,7 @@ { "findings": [ "Wake delivery (target p95 < 6 s) — MET in pdx1: after core U17/U18 (default region pdx1, Standard 8 vCPU/16 GB, golden 63b6999), n=20, persistent connection. Wake to echo p50 5.04 s, p95 5.85 s, p99 5.94 s; 0/20 slow hosts. Pinned to iad1 at 8/16: p50 5.31, p95 10.59, p99 11.80 s, with 4/20 slow hosts. BEFORE (iad1 4/8): 5.28 / 8.99 / 9.40 s. Region, not size, removes the tail. From this Mac the first terminal list (0.18 vs 0.44 s) and echo (0.07 vs 0.15 s) are also faster in pdx1. Create p50: pdx1 2.03 s, iad1-pinned 4.26 s (pinned creates seem to skip the warm pool).", + "run.ports (cloud) currently FAILS (2026-11-10): with the #233 desktop (ceea524) against the active goldens, which predate #233 (pdx1 2d99d9b, iad1 3f2b8ea), the first cloud workspace gets a hash port block (22740, stable across sleep/wake) instead of the launcher's 3000. On be26e2f both sides matched and it was 3000, so this is desktop/VM version skew. W9 re-runs automatically once a golden with ceea524 is active. Version skew between desktop and golden image is itself a risk to watch at launch. run.nonconcurrent passes with #233: only the same action stops (471 ms); the concurrent web keeps serving.", "Read while asleep (gap 5, #189/#218), verified by W9 live: a teammate decrypted a sleeping team workspace (git incl. a new file, PR, sessions) with no wake: the Vercel session stayed stopped. 20 reads p50 89 ms, max 131 ms, vs about 5-6 s to wake. An outsider gets 404, and the plaintext marker is not in D1. Removal/private access control is covered by life.read-asleep-access. The phone view was checked in W9's own headless simulator run: “Latest from this workspace · As of just now” showed the marker branch and NOTES.md, and Vercel stayed stopped.", "Read-asleep access, triggered by W9 itself: the operator members/removed route revokes the member; their pre-removal token and a fresh token both get 401, and their wrapped keys go 1 to 0, while the other teammate still reads. Making the workspace private gives that teammate 404 and 0 keys. Phone protocol: device token plus mobile reader key reads a sleeping private workspace in 125 ms, and unpairing revokes access.", "Independent audit corrections (2026-11-10, reports/conductor-parity-audit.html): five rows are capped at partial because they passed on a weaker bar than the Conductor column: real-model agent run, subscription sign-in, default machine size, and the download and remote-desktop UIs. Four rows Conductor has that our bar left out are added as not landed: local core workflow, Run actions with per-workspace ports, read-while-asleep from any device, and per-turn code checkpoints. Our 'pass' counts measure the team's own bar. Every cloud result is on dev Workers, Stripe TEST mode and dev/staging tokens; nothing is shipped (mvp-tracker §5 launch items are all open).", @@ -30,4 +31,4 @@ "agent.signin": "Auth folders persist across sleep/wake and rollover, but subscription OAuth on a VM was never exercised. This cap is lifted by the user's own e2e session with their own credentials (after their review of #40228). It is on the coordinator's e2e checklist.", "agent.more-agents": "Owner-reported (gap-agents) real-task runs on kimi-k2 via the gateway. W9 is not re-running them (no gateway spend per user decision). Lifted by the user e2e session with their own credentials." } -} +} \ No newline at end of file From 8b2701636c648b4689622045c33b4874c17e9f17 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 10 Nov 2026 04:26:16 -0800 Subject: [PATCH 4/5] feat(cloud): acceptance headless cloud run.ports (first workspace 3000, second worktree block, sleep/wake) --- cloud/apps/acceptance/src/checks/lifecycle.ts | 51 ++++++++++++++++++- cloud/apps/acceptance/src/golden-status.ts | 13 ++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/cloud/apps/acceptance/src/checks/lifecycle.ts b/cloud/apps/acceptance/src/checks/lifecycle.ts index 75b36de0f89..3eb294d4602 100644 --- a/cloud/apps/acceptance/src/checks/lifecycle.ts +++ b/cloud/apps/acceptance/src/checks/lifecycle.ts @@ -8,7 +8,7 @@ import { join } from 'node:path' import { control } from '../clients' import { config } from '../env' import { log, type Check, type Ctx } from '../context' -import { orca, openTerminal } from '../orca-cli' +import { orca, openTerminal, typeAndSee } from '../orca-cli' import { sandboxToken } from '../tokens' const ARCHIVE = { 'x-orca-sandbox-features': 'archive' } @@ -318,6 +318,55 @@ export const lifecycleChecks: Record = { } }, + + 'run.ports': async (ctx) => { + // Cloud half of per-workspace ports (#216/#233), headless over the pairing: the VM's first workspace + // gets the launcher block (3000), a second worktree on the same VM gets its own registry block, both + // are persisted and survive sleep/wake. The local half (distinct blocks, stable across app restart) + // was proven in the real app on m4air (desktop-20261110-033734). + const box = await ctx.sandboxes.create(ctx.solo, { workspaceId: `w9-ports-${ctx.run.file.runId}`, repo: { url: config.testRepo } }) + if (box.status !== 200) { + return { status: 'fail', detail: `create ${box.status}` } + } + const id = box.body.sandbox.id + let code = box.body.connection.pairingCode as string + const root = box.body.connection.projectRoot as string + try { + const first = await openTerminal(code, root) + const worktree: string | undefined = first.term.json?.result?.terminal?.worktreeId + const repoId = worktree?.split('::')[0] + const second = await orca(code, ['worktree', 'create', '--repo', `id:${repoId}`, '--name', 'w9-second']) + const secondPath: string | undefined = second.json?.result?.worktree?.path + const portsIn = async (path: string, label: string) => { + const t = await orca(code, ['terminal', 'create', '--worktree', `path:${path}`, '--title', label]) + const handle = t.json?.result?.terminal?.handle + if (!handle) { + return `no-terminal(${t.stderr.slice(0, 80)})` + } + const seen = await typeAndSee(code, handle, `echo PORTS_\${ORCA_PORT}_\${ORCA_PORT_COUNT}_\${CONDUCTOR_PORT}_END`, '_END') + return /PORTS_(\d+)_(\d+)_(\d+)_END/.exec(seen.output)?.slice(1).join('/') ?? `unseen(${seen.error})` + } + const a1 = await portsIn(root, 'first') + const b1 = secondPath ? await portsIn(secondPath, 'second') : `no second worktree (${second.stderr.slice(0, 120) || JSON.stringify(second.json?.error ?? '').slice(0, 120)})` + const registry = (await ctx.exec((await ctx.sandboxes.get(id)).body.sandbox.session!.id, 'cat /vercel/.orcad-data/workspace-port-blocks.json 2>/dev/null')).stdout.trim() + await ctx.sandboxes.suspend(id) + const w = await ctx.sandboxes.resume(id) + code = w.body.connection.pairingCode + const a2 = await portsIn(root, 'first-after') + const b2 = secondPath ? await portsIn(secondPath, 'second-after') : 'n/a' + const aPort = Number(a1.split('/')[0]) + const bPort = Number(b1.split('/')[0]) + const ok = + aPort === 3000 && a1.endsWith('/10/3000') && bPort >= 20000 && bPort <= 31999 && bPort !== aPort && a1 === a2 && b1 === b2 + return { + status: ok ? 'pass' : 'fail', + detail: `cloud VM ports (ORCA_PORT/COUNT/CONDUCTOR_PORT): first workspace ${a1}, second worktree ${b1}; after sleep/wake ${a2} and ${b2}; registry ${registry.slice(0, 200)}. Local blocks (distinct, stable across restart) proven on m4air desktop-20261110-033734.`, + evidence: [log('port registry', registry)] + } + } finally { + await ctx.sandboxes.destroy(id) + } + }, 'acc.ssh': async (ctx) => { const b = await refresh(ctx) const { publicKey } = generateKeyPairSync('ed25519') diff --git a/cloud/apps/acceptance/src/golden-status.ts b/cloud/apps/acceptance/src/golden-status.ts index fc9cc9b6cb0..ac01e674d6a 100644 --- a/cloud/apps/acceptance/src/golden-status.ts +++ b/cloud/apps/acceptance/src/golden-status.ts @@ -32,7 +32,18 @@ function contains(commit: string, ancestor: string): boolean { } for (;;) { - const a = active() + let a: ReturnType = null + try { + a = active() + } catch (error) { + // Transient Cloudflare API/auth errors: keep waiting rather than dying. + console.log(JSON.stringify({ at: new Date().toISOString(), error: String(error).split('\n')[0]!.slice(0, 160) })) + if (!wait) { + break + } + await new Promise((r) => setTimeout(r, 120_000)) + continue + } const ok = a?.commit && required ? contains(a.commit, required) : null console.log(JSON.stringify({ at: new Date().toISOString(), ...a, containsRequired: ok })) if (!wait || ok) { From ab106b93cc093520d905f0e7e16caf631d70557c Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 10 Nov 2026 04:29:00 -0800 Subject: [PATCH 5/5] feat(cloud): cloud run.ports passes on the #233 golden (3000 + second worktree block, stable across sleep) --- cloud/apps/acceptance/findings.json | 2 +- cloud/apps/acceptance/src/checks/lifecycle.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/apps/acceptance/findings.json b/cloud/apps/acceptance/findings.json index 144e7547032..366d33600b2 100644 --- a/cloud/apps/acceptance/findings.json +++ b/cloud/apps/acceptance/findings.json @@ -1,7 +1,7 @@ { "findings": [ "Wake delivery (target p95 < 6 s) — MET in pdx1: after core U17/U18 (default region pdx1, Standard 8 vCPU/16 GB, golden 63b6999), n=20, persistent connection. Wake to echo p50 5.04 s, p95 5.85 s, p99 5.94 s; 0/20 slow hosts. Pinned to iad1 at 8/16: p50 5.31, p95 10.59, p99 11.80 s, with 4/20 slow hosts. BEFORE (iad1 4/8): 5.28 / 8.99 / 9.40 s. Region, not size, removes the tail. From this Mac the first terminal list (0.18 vs 0.44 s) and echo (0.07 vs 0.15 s) are also faster in pdx1. Create p50: pdx1 2.03 s, iad1-pinned 4.26 s (pinned creates seem to skip the warm pool).", - "run.ports (cloud) currently FAILS (2026-11-10): with the #233 desktop (ceea524) against the active goldens, which predate #233 (pdx1 2d99d9b, iad1 3f2b8ea), the first cloud workspace gets a hash port block (22740, stable across sleep/wake) instead of the launcher's 3000. On be26e2f both sides matched and it was 3000, so this is desktop/VM version skew. W9 re-runs automatically once a golden with ceea524 is active. Version skew between desktop and golden image is itself a risk to watch at launch. run.nonconcurrent passes with #233: only the same action stops (471 ms); the concurrent web keeps serving.", + "run.ports (cloud) passes on a #233 golden (iad1 d323e4a): first workspace 3000/10/3000, a second worktree on the same VM 26600/10/26600, both unchanged after sleep/wake, both persisted in the VM registry. pdx1 still runs 2d99d9b (pre-#233) and gives a hash block until its golden rotates. Desktop/golden version skew is a launch risk to watch. run.nonconcurrent passes with #233 (only the same action stops, 471 ms).", "Read while asleep (gap 5, #189/#218), verified by W9 live: a teammate decrypted a sleeping team workspace (git incl. a new file, PR, sessions) with no wake: the Vercel session stayed stopped. 20 reads p50 89 ms, max 131 ms, vs about 5-6 s to wake. An outsider gets 404, and the plaintext marker is not in D1. Removal/private access control is covered by life.read-asleep-access. The phone view was checked in W9's own headless simulator run: “Latest from this workspace · As of just now” showed the marker branch and NOTES.md, and Vercel stayed stopped.", "Read-asleep access, triggered by W9 itself: the operator members/removed route revokes the member; their pre-removal token and a fresh token both get 401, and their wrapped keys go 1 to 0, while the other teammate still reads. Making the workspace private gives that teammate 404 and 0 keys. Phone protocol: device token plus mobile reader key reads a sleeping private workspace in 125 ms, and unpairing revokes access.", "Independent audit corrections (2026-11-10, reports/conductor-parity-audit.html): five rows are capped at partial because they passed on a weaker bar than the Conductor column: real-model agent run, subscription sign-in, default machine size, and the download and remote-desktop UIs. Four rows Conductor has that our bar left out are added as not landed: local core workflow, Run actions with per-workspace ports, read-while-asleep from any device, and per-turn code checkpoints. Our 'pass' counts measure the team's own bar. Every cloud result is on dev Workers, Stripe TEST mode and dev/staging tokens; nothing is shipped (mvp-tracker §5 launch items are all open).", diff --git a/cloud/apps/acceptance/src/checks/lifecycle.ts b/cloud/apps/acceptance/src/checks/lifecycle.ts index 3eb294d4602..ddb423f80c7 100644 --- a/cloud/apps/acceptance/src/checks/lifecycle.ts +++ b/cloud/apps/acceptance/src/checks/lifecycle.ts @@ -324,7 +324,7 @@ export const lifecycleChecks: Record = { // gets the launcher block (3000), a second worktree on the same VM gets its own registry block, both // are persisted and survive sleep/wake. The local half (distinct blocks, stable across app restart) // was proven in the real app on m4air (desktop-20261110-033734). - const box = await ctx.sandboxes.create(ctx.solo, { workspaceId: `w9-ports-${ctx.run.file.runId}`, repo: { url: config.testRepo } }) + const box = await ctx.sandboxes.create(ctx.solo, { workspaceId: `w9-ports-${ctx.run.file.runId}`, repo: { url: config.testRepo }, ...(process.env.ORCA_ACCEPTANCE_REGION ? { region: process.env.ORCA_ACCEPTANCE_REGION } : {}) }) if (box.status !== 200) { return { status: 'fail', detail: `create ${box.status}` } }