Merge pull request #241 from nwparker/cloud/w9-eval

feat(cloud): acceptance — phone UI read-asleep (sim), cloud run.ports headless, nonconcurrent per #233
This commit is contained in:
Neil
2026-11-10 04:41:08 -08:00
committed by GitHub
6 changed files with 504 additions and 7 deletions
+203 -2
View File
@@ -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
+3 -3
View File
@@ -1,7 +1,8 @@
{
"findings": [
"<b>Wake delivery (target p95 &lt; 6 s) — MET in pdx1:</b> 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).",
"<b>Read while asleep (gap 5, #189/#218), verified by W9 live:</b> 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).",
"<b>run.ports (cloud) passes on a #233 golden (iad1 d323e4a):</b> 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).",
"<b>Read while asleep (gap 5, #189/#218), verified by W9 live:</b> 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.",
"<b>Read-asleep access, triggered by W9 itself:</b> 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.",
"<b>Independent audit corrections (2026-11-10, reports/conductor-parity-audit.html):</b> 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).",
"<b>Agents and machine size (2026-11-10):</b> 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 +29,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."
}
}
+101
View File
@@ -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 <file> 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 <needle> rendered text strings containing <needle> (read-only)
// node cdp.mjs eval '<expr>'
// 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 <file> | identity-clear | texts <needle> | eval <expr>')
}
} finally {
s.close()
clearTimeout(timer)
}
process.exit(0)
@@ -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<string, any> = 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<string, unknown> = {}
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')
}
+50 -1
View File
@@ -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<string, Check> = {
}
},
'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 }, ...(process.env.ORCA_ACCEPTANCE_REGION ? { region: process.env.ORCA_ACCEPTANCE_REGION } : {}) })
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')
+12 -1
View File
@@ -32,7 +32,18 @@ function contains(commit: string, ancestor: string): boolean {
}
for (;;) {
const a = active()
let a: ReturnType<typeof active> = 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) {