Skip to content

Commit 3c5b48d

Browse files
GiniGini
authored andcommitted
feat: add repeatable OneComputer live e2e harness
1 parent 1496d99 commit 3c5b48d

3 files changed

Lines changed: 86 additions & 1 deletion

File tree

docs/ONECOMPUTER-LIVE-E2E.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,15 @@ Run the following after the provider lifecycle change:
7474
4. Cancel a separate task during bootstrap and prove provider-side deletion with no surviving container or sandbox row.
7575
5. Complete a normal task and prove automatic destruction within the lifecycle SLO.
7676
6. Verify the browser has only server-proxied PNG frames and never has runtime, VNC, CDP, API-key, or project-header access.
77+
78+
## Repeatable harness
79+
80+
Once the provider lifecycle repair is deployed and ONEVibe has its server-only ONEComputer configuration, run the controlled Website proof from the ONEVibe repository:
81+
82+
```sh
83+
ONEVIBE_E2E_URL=https://onevibe.example \
84+
ONEVIBE_E2E_REQUIRE_GATEWAY=true \
85+
npm run e2e:onecomputer
86+
```
87+
88+
The harness refuses to run if the ONEComputer provider is unavailable. It creates one disposable Website task, waits for a terminal result, and verifies the recorded sandbox boundary, optional gateway attestation, ephemeral destruction, readiness evidence, optional X11 frame, extracted `index.html`, and evidence-chain validity. It intentionally does not send credentials to the browser or attempt a provider-side cancellation stress test; retain that as the separate controlled proof in the required-success list.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"lint": "oxlint src server",
1414
"test": "vitest run",
1515
"preview": "vite preview",
16-
"wallet": "tsx server/wallet-cli.ts"
16+
"wallet": "tsx server/wallet-cli.ts",
17+
"e2e:onecomputer": "tsx scripts/onecomputer-live-e2e.ts"
1718
},
1819
"dependencies": {
1920
"@anthropic-ai/claude-agent-sdk": "^0.3.210",

scripts/onecomputer-live-e2e.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Controlled live proof for a configured ONEVibe + ONEComputer deployment.
3+
* It is intentionally never run as part of unit tests: it creates a real
4+
* disposable sandbox and requires server-side deployment credentials.
5+
*/
6+
const baseUrl = (process.env.ONEVIBE_E2E_URL ?? 'http://127.0.0.1:5173').replace(/\/$/, '')
7+
const timeoutMs = Math.max(60_000, Number(process.env.ONEVIBE_E2E_TIMEOUT_MS ?? 20 * 60_000))
8+
const requireGateway = process.env.ONEVIBE_E2E_REQUIRE_GATEWAY === 'true'
9+
const requireVisual = process.env.ONEVIBE_E2E_REQUIRE_VISUAL !== 'false'
10+
11+
type Snapshot = {
12+
id: string
13+
status: string
14+
securityContext?: { executionBoundary?: string; gatewayEnforced?: boolean; sandboxState?: string }
15+
events: Array<{ type: string; label?: string; payload: Record<string, unknown> }>
16+
}
17+
18+
const request = async <T>(pathname: string, init?: RequestInit) => {
19+
let response: Response
20+
try {
21+
response = await fetch(`${baseUrl}${pathname}`, { ...init, headers: { 'Content-Type': 'application/json', ...init?.headers } })
22+
} catch (error) {
23+
throw new Error(`Cannot reach ONEVibe at ${baseUrl}${pathname}: ${error instanceof Error ? error.message : 'network failure'}`)
24+
}
25+
const body = await response.json().catch(() => ({})) as T & { error?: string }
26+
if (!response.ok) throw new Error(`${pathname} returned ${response.status}${body.error ? `: ${body.error}` : ''}`)
27+
return body
28+
}
29+
30+
const waitForTerminalSnapshot = async (taskId: string) => {
31+
const deadline = Date.now() + timeoutMs
32+
let latest: Snapshot | undefined
33+
while (Date.now() < deadline) {
34+
latest = await request<Snapshot>(`/api/tasks/${encodeURIComponent(taskId)}`)
35+
if (['completed', 'failed', 'cancelled'].includes(latest.status)) return latest
36+
await new Promise((resolve) => setTimeout(resolve, 2_000))
37+
}
38+
throw new Error(`Task ${taskId} did not reach a terminal state within ${timeoutMs}ms (last state: ${latest?.status ?? 'unreadable'})`)
39+
}
40+
41+
const main = async () => {
42+
const readiness = await request<{ providers: Array<{ id: string; available: boolean; detail: string }> }>('/api/runtime')
43+
const sandbox = readiness.providers.find((provider) => provider.id === 'onecomputer')
44+
if (!sandbox?.available) throw new Error(`ONEComputer is not available at ${baseUrl}: ${sandbox?.detail ?? 'runtime status unavailable'}`)
45+
const created = await request<{ id: string }>('/api/tasks', {
46+
method: 'POST',
47+
body: JSON.stringify({
48+
prompt: 'Create a small accessible governed website with index.html and a concise README. Do not publish or call external services.',
49+
provider: 'onecomputer',
50+
mode: 'website',
51+
projectId: 'project_onevibe',
52+
references: [], attachments: [], skills: ['web_build', 'security_review'],
53+
}),
54+
})
55+
const task = await waitForTerminalSnapshot(created.id)
56+
if (task.status !== 'completed') throw new Error(`ONEComputer task ${task.id} ended ${task.status}`)
57+
if (task.securityContext?.executionBoundary !== 'onecomputer_sandbox') throw new Error('Task did not record the ONEComputer sandbox execution boundary')
58+
if (requireGateway && task.securityContext?.gatewayEnforced !== true) throw new Error('Gateway attestation was required but not recorded')
59+
if (task.securityContext?.sandboxState !== 'destroyed') throw new Error(`Expected ephemeral sandbox destruction, found ${task.securityContext?.sandboxState ?? 'unknown'}`)
60+
if (!task.events.some((event) => event.label === 'ONEComputer sandbox ready')) throw new Error('Sandbox readiness event missing')
61+
if (requireVisual && !task.events.some((event) => event.payload.kind === 'visual_frame')) throw new Error('Required X11 visual evidence missing')
62+
const preview = await request<{ content: string }>(`/api/tasks/${encodeURIComponent(task.id)}/file?path=index.html`)
63+
if (!preview.content.includes('<')) throw new Error('Expected portable index.html was not extracted')
64+
const evidence = await request<{ valid: boolean }>(`/api/tasks/${encodeURIComponent(task.id)}/evidence`)
65+
if (!evidence.valid) throw new Error('Evidence chain verification failed')
66+
console.log(JSON.stringify({ taskId: task.id, status: task.status, gatewayEnforced: task.securityContext?.gatewayEnforced === true, visualEvidence: task.events.filter((event) => event.payload.kind === 'visual_frame').length, evidenceValid: evidence.valid }, null, 2))
67+
}
68+
69+
main().catch((error: unknown) => {
70+
console.error(error instanceof Error ? error.message : error)
71+
process.exitCode = 1
72+
})

0 commit comments

Comments
 (0)