@frockbot/computer-host-runtime 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1047 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import {
4
+ chmod,
5
+ mkdir,
6
+ mkdtemp,
7
+ readFile,
8
+ rm,
9
+ utimes,
10
+ writeFile,
11
+ } from "node:fs/promises";
12
+ import { tmpdir } from "node:os";
13
+ import { dirname, join } from "node:path";
14
+ import { describe, expect, test } from "bun:test";
15
+ import {
16
+ BIN_ROOT,
17
+ BOTS_ROOT,
18
+ boxDoctorScript,
19
+ browserHelper,
20
+ CHROME_LAUNCHER,
21
+ chromeLauncherScript,
22
+ CHROMIUM_PATH,
23
+ COMPUTER_GUI_SHELL_COMMANDS,
24
+ COMPUTER_RUNTIME_FILES,
25
+ computerGuiRefusalV1,
26
+ SHIMS_ROOT,
27
+ DOCTOR_BROWSER_IDENTITY_ACTION,
28
+ DOCTOR_LOG,
29
+ DOCTOR_MARKER,
30
+ DOCTOR_REPORT_SCHEMA_VERSION,
31
+ DOCTOR_SCRIPT,
32
+ guiShimScript,
33
+ REFERENCE_DOCS,
34
+ REFERENCE_DOCS_VERSION,
35
+ REFERENCE_ROOT,
36
+ SCRATCH_ROOT,
37
+ shellGuiCommandV1,
38
+ computerSpriteNameSourceV1,
39
+ computerSpriteNameV1,
40
+ CONTROL_SCRIPT,
41
+ DATA_ROOT,
42
+ DESKTOP_GUI_LEASE_KEY,
43
+ ENSURE_AGENT_SCRIPT,
44
+ HOME_ROOT,
45
+ PROVISION_LOCK,
46
+ PROVISION_DIGEST,
47
+ PROVISION_PHASES,
48
+ PROVISION_SCRIPT,
49
+ PROVISION_TASK,
50
+ PLAYWRIGHT_PLATFORM,
51
+ PLAYWRIGHT_VERSION,
52
+ DESKTOP_PACKAGES,
53
+ SPRITE_API_SOCKET,
54
+ provisionLaunchScript,
55
+ provisionPollScript,
56
+ BOUNDED_LOG_SCRIPT,
57
+ BOUNDED_LOG_HEAD_BYTES,
58
+ provisionScript,
59
+ RUNTIME_DOCUMENT_FILES,
60
+ runtimeDocumentDigestV1,
61
+ RUNTIME_ROOT,
62
+ base64,
63
+ installFile,
64
+ shellQuote,
65
+ SLOT_IDLE_SECONDS,
66
+ UPDATE_PHASES,
67
+ updateLaunchScript,
68
+ WORKSPACES_ROOT,
69
+ } from "./runtime.ts";
70
+
71
+ function installedScript(provision: string, path: string): string {
72
+ const line = provision
73
+ .split("\n")
74
+ .find(
75
+ (candidate) =>
76
+ candidate.includes(`> ${path}`) || candidate.includes(`> ${path}.tmp`),
77
+ );
78
+ const encoded = line ? /printf %s '([^']+)'/.exec(line)?.[1] : undefined;
79
+ if (!encoded) throw new Error(`installed script not found: ${path}`);
80
+ return Buffer.from(encoded, "base64").toString();
81
+ }
82
+
83
+ async function expectValidShell(script: string): Promise<void> {
84
+ const process = Bun.spawn(["bash", "-n"], {
85
+ stdin: new Blob([script]),
86
+ stdout: "ignore",
87
+ stderr: "pipe",
88
+ });
89
+ const [exitCode, stderr] = await Promise.all([
90
+ process.exited,
91
+ new Response(process.stderr).text(),
92
+ ]);
93
+ expect(stderr).toBe("");
94
+ expect(exitCode).toBe(0);
95
+ }
96
+
97
+ async function runControl(
98
+ scriptPath: string,
99
+ action: string,
100
+ key: string,
101
+ owner: string,
102
+ maxAge = "90",
103
+ ): Promise<{ exitCode: number; stderr: string }> {
104
+ const args =
105
+ action === "assert-agent"
106
+ ? [scriptPath, action, key, DESKTOP_GUI_LEASE_KEY, owner, maxAge]
107
+ : [scriptPath, action, key, owner, maxAge];
108
+ const child = Bun.spawn(args, {
109
+ env: {
110
+ ...process.env,
111
+ PATH: `${dirname(scriptPath)}:${process.env.PATH ?? ""}`,
112
+ },
113
+ stdout: "ignore",
114
+ stderr: "pipe",
115
+ });
116
+ const [exitCode, stderr] = await Promise.all([
117
+ child.exited,
118
+ new Response(child.stderr).text(),
119
+ ]);
120
+ return { exitCode, stderr };
121
+ }
122
+
123
+ function spriteName(userId: string, base = "frockbot"): string {
124
+ return computerSpriteNameV1(
125
+ userId,
126
+ createHash("sha256")
127
+ .update(computerSpriteNameSourceV1(userId))
128
+ .digest("hex"),
129
+ base,
130
+ );
131
+ }
132
+
133
+ describe("layout", () => {
134
+ test("the Computer is laid out under the GrokBot home", () => {
135
+ expect(HOME_ROOT).toBe("/home/box");
136
+ expect(DATA_ROOT).toBe("/home/box/agent-data");
137
+ expect(RUNTIME_ROOT).toBe("/home/box/.frockbot");
138
+ expect(BOTS_ROOT).toBe("/home/box/.frockbot/bots");
139
+ expect(WORKSPACES_ROOT).toBe("/workspaces");
140
+ });
141
+ });
142
+
143
+ describe("runtime files", () => {
144
+ test("every declared file is the one the provisioning script installs", () => {
145
+ for (const file of COMPUTER_RUNTIME_FILES) {
146
+ expect(provisionScript).toContain(
147
+ installFile(`${file.path}.tmp`, file.content),
148
+ );
149
+ }
150
+ });
151
+
152
+ test("the inventory covers every file the provisioning script installs", () => {
153
+ const installs = provisionScript
154
+ .split("\n")
155
+ .filter(
156
+ (line) =>
157
+ line.trimStart().startsWith("printf %s '") &&
158
+ line.includes("base64 -d >"),
159
+ );
160
+ const paths = new Set(
161
+ installs.map((line) =>
162
+ line
163
+ .slice(line.indexOf("base64 -d >") + "base64 -d >".length)
164
+ .trim()
165
+ .split(/[ &]/, 1)[0]!
166
+ .replace(/\.tmp$/, ""),
167
+ ),
168
+ );
169
+ expect(paths).toEqual(
170
+ new Set(RUNTIME_DOCUMENT_FILES.slice(1).map((file) => file.path)),
171
+ );
172
+ });
173
+
174
+ test("every declared file receives its mode before the atomic rename", () => {
175
+ // Found live: the shims moved to their own directory and the `chmod` that
176
+ // follows them kept the old path, so provisioning failed at phase 3 with
177
+ // "cannot access /home/box/bin/xdotool". An install and a mode are one
178
+ // fact about a file, and this is what keeps them from drifting apart.
179
+ for (const file of COMPUTER_RUNTIME_FILES) {
180
+ const installed = provisionScript.indexOf(
181
+ installFile(`${file.path}.tmp`, file.content),
182
+ );
183
+ const mode = provisionScript.indexOf(
184
+ `chmod ${file.mode.toString(8)} ${file.path}.tmp`,
185
+ installed,
186
+ );
187
+ const renamed = provisionScript.indexOf(
188
+ `mv ${file.path}.tmp ${file.path}`,
189
+ mode,
190
+ );
191
+ expect(installed, file.path).toBeGreaterThan(-1);
192
+ expect(mode, file.path).toBeGreaterThan(installed);
193
+ expect(renamed, file.path).toBeGreaterThan(mode);
194
+ }
195
+ });
196
+
197
+ test("the control and ensure scripts are installed where the provider calls them", () => {
198
+ const paths = COMPUTER_RUNTIME_FILES.map((file) => file.path);
199
+ expect(paths).toContain(CONTROL_SCRIPT);
200
+ expect(paths).toContain(ENSURE_AGENT_SCRIPT);
201
+ });
202
+ });
203
+
204
+ describe("provisioning script", () => {
205
+ test("is far larger than the argv budget that produced the measured 431", () => {
206
+ // ADR 0004: Fly answered a ~2.5 KB `cmd=` query with 431. The script must
207
+ // reach the Sprite on stdin, and this asserts the size that makes argv
208
+ // delivery impossible rather than merely unwise.
209
+ expect(provisionScript.length).toBeGreaterThan(3_000);
210
+ });
211
+
212
+ test("installs the desktop, sync, and gateway runtime", () => {
213
+ expect(provisionScript).toContain(
214
+ "apt-get install -y --no-install-recommends",
215
+ );
216
+ // `computer_screenshot` runs `scrot` under the tenant's own display, so
217
+ // provisioning installs it and the capability probe asks for it. Without
218
+ // the probe, an already-provisioned Computer would never gain it.
219
+ expect(DESKTOP_PACKAGES).toContain("scrot");
220
+ expect(provisionScript).toContain("! command -v scrot >/dev/null");
221
+ expect(provisionScript).toContain(`playwright-core@${PLAYWRIGHT_VERSION}`);
222
+ expect(provisionScript).toContain(`chmod 600 ${RUNTIME_ROOT}/tokens`);
223
+ });
224
+
225
+ test("installs no browser from the distribution", () => {
226
+ // ADR 0004: on the Sprite base image `chromium` is a snap transitional
227
+ // package. Installing it pulls `snapd` and `systemd` and had not finished
228
+ // after 25 minutes, which is the whole reason a cold Computer could not
229
+ // open. The browser is Playwright's own build instead, and the way that
230
+ // stays true is that the package list never names one again.
231
+ expect(DESKTOP_PACKAGES).not.toContain("chromium");
232
+ expect(provisionScript).not.toMatch(/apt-get install[^\n]*\bchromium\b/);
233
+ expect(provisionScript).toContain("cli.js install chromium");
234
+ expect(provisionScript).toContain(
235
+ `PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=${PLAYWRIGHT_PLATFORM}`,
236
+ );
237
+ });
238
+
239
+ test("holds the Sprite awake for the whole detached run", () => {
240
+ // The defect that made every package list look too heavy: a Sprite is
241
+ // active while there is "a command running, a session producing output, an
242
+ // open TCP connection to its URL, a service handling traffic", and a
243
+ // `setsid nohup` provisioner is none of those. Measured, the Sprite's own
244
+ // clock advanced ~4 minutes across ~25 minutes of wall time. The task is
245
+ // the documented hold, and it is released on EXIT so a failed run stops
246
+ // paying for the Sprite rather than pinning it awake.
247
+ expect(provisionScript).toContain(SPRITE_API_SOCKET);
248
+ expect(provisionScript).toContain(`http://sprite/v1/tasks`);
249
+ expect(provisionScript).toContain(
250
+ `-X DELETE http://sprite/v1/tasks/${PROVISION_TASK}`,
251
+ );
252
+ expect(provisionScript).toContain("trap release EXIT");
253
+ });
254
+
255
+ test("puts the real toolchain on PATH before it runs node", () => {
256
+ // `/.sprite/bin/node` is an nvm shim whose last resort is `command -v
257
+ // node` — itself, in a non-login shell — so a detached `node` re-execs for
258
+ // ever. Measured on a real Sprite: it never returned.
259
+ const preamble = provisionScript.indexOf("/etc/profile.d/languages_paths");
260
+ const firstNode = provisionScript.indexOf("npm install --prefix");
261
+ expect(preamble).toBeGreaterThan(-1);
262
+ expect(preamble).toBeLessThan(firstNode);
263
+ });
264
+
265
+ test("guards every resumable phase with its own marker", () => {
266
+ // A half-provisioned Computer is completed, never started over: the phase
267
+ // a container restart interrupted is the phase the next run begins at.
268
+ for (const phase of PROVISION_PHASES.filter((entry) => !entry.always)) {
269
+ expect(provisionScript).toContain(`[ ! -f "$MARKERS/${phase.name}" ]`);
270
+ expect(provisionScript).toContain(`touch "$MARKERS/${phase.name}"`);
271
+ }
272
+ });
273
+
274
+ test("the reference phase is version-guarded rather than marker-guarded", () => {
275
+ // A marker would make the reference set writable exactly once in a
276
+ // Computer's life, which is the defect this version exists to fix.
277
+ const reference = PROVISION_PHASES.find(
278
+ (phase) => phase.name === "reference",
279
+ );
280
+ expect(reference?.always).toBe(true);
281
+ expect(provisionScript).not.toContain('[ ! -f "$MARKERS/reference" ]');
282
+ expect(provisionScript).toContain(`${REFERENCE_ROOT}/.version 2>/dev/null`);
283
+ expect(provisionScript).toContain(REFERENCE_DOCS_VERSION);
284
+ });
285
+
286
+ test("creates the shared scratch, which no durable root covers", () => {
287
+ expect(provisionScript).toContain(`chmod 0775 ${SCRATCH_ROOT}`);
288
+ expect(provisionScript).toContain(`chown box:box ${SCRATCH_ROOT}`);
289
+ });
290
+
291
+ test("records the phase it is in before it begins it", () => {
292
+ // The progress `open` reports. Written before the work, or a phase that
293
+ // never finishes would never be named.
294
+ for (const [position, phase] of PROVISION_PHASES.entries()) {
295
+ expect(provisionScript).toContain(`INDEX=${position + 1}
296
+ NAME=${phase.name}
297
+ LABEL=${shellQuote(phase.label)}
298
+ state running`);
299
+ }
300
+ expect(provisionScript).toContain("state complete");
301
+ expect(provisionScript).toContain("trap 'state failed' ERR");
302
+ });
303
+
304
+ test("the launcher detaches the run and the poll starts nothing", async () => {
305
+ // The defect in one assertion: `@fly/sprites@0.1.0` declares a WebSocket
306
+ // dead 45 s after the last inbound message and never pings, so the exec
307
+ // that installs a desktop stack must not be the exec that waits for it.
308
+ expect(provisionLaunchScript).toContain("setsid nohup");
309
+ expect(provisionLaunchScript).toContain(PROVISION_SCRIPT);
310
+ expect(provisionPollScript).not.toContain("setsid");
311
+ expect(provisionPollScript).not.toContain("apt-get");
312
+ // Short enough that it cannot be the thing that is quiet.
313
+ expect(provisionPollScript.length).toBeLessThan(1_000);
314
+ await expectValidShell(provisionLaunchScript);
315
+ await expectValidShell(updateLaunchScript);
316
+ await expectValidShell(provisionPollScript);
317
+ await expectValidShell(provisionScript);
318
+ });
319
+
320
+ test("the launcher probes the run lock once, before it starts anything", () => {
321
+ // Measured: a second probe after the launch takes the lock the
322
+ // provisioner is trying to take, and `flock -n` makes the provisioner
323
+ // die silently. One probe, and the provisioner waits rather than refusing.
324
+ expect(
325
+ provisionLaunchScript.split(`flock -n ${PROVISION_LOCK}`),
326
+ ).toHaveLength(2);
327
+ expect(provisionLaunchScript).toContain(`flock -w 30 ${PROVISION_LOCK}`);
328
+ });
329
+
330
+ test("writes the runtime digest only after the complete state", () => {
331
+ const complete = provisionScript.indexOf("state complete");
332
+ const digest = provisionScript.indexOf(
333
+ `mv \"$DIGEST_TMP\" ${PROVISION_DIGEST}`,
334
+ );
335
+ expect(complete).toBeGreaterThan(-1);
336
+ expect(digest).toBeGreaterThan(complete);
337
+ expect(provisionScript.slice(digest).trim()).toBe(
338
+ `mv \"$DIGEST_TMP\" ${PROVISION_DIGEST}`,
339
+ );
340
+ });
341
+
342
+ test("the update runner contains only the runtime and reference phases", () => {
343
+ expect(UPDATE_PHASES.map((phase) => phase.name)).toEqual([
344
+ "runtime",
345
+ "reference",
346
+ ]);
347
+ for (const phase of UPDATE_PHASES) {
348
+ expect(phase.label).toStartWith("Updating ");
349
+ }
350
+ const updateDocument = UPDATE_PHASES.map((phase) => phase.body).join("\n");
351
+ expect(updateDocument).not.toContain("apt-get");
352
+ expect(updateDocument).not.toContain("playwright-core/cli.js install");
353
+ });
354
+ });
355
+
356
+ describe("runtime document digest", () => {
357
+ test("is stable across runs and is sha-256 hex", () => {
358
+ const digest = runtimeDocumentDigestV1();
359
+ const framed = RUNTIME_DOCUMENT_FILES.map(
360
+ (file) => `${Buffer.byteLength(file.content)}\0${file.content}`,
361
+ ).join("");
362
+ expect(digest).toBe(runtimeDocumentDigestV1());
363
+ expect(digest).toBe(createHash("sha256").update(framed).digest("hex"));
364
+ expect(digest).toMatch(/^[0-9a-f]{64}$/);
365
+ });
366
+
367
+ test("moves on a one-byte change to every installed file", () => {
368
+ const stable = runtimeDocumentDigestV1();
369
+ for (const file of RUNTIME_DOCUMENT_FILES) {
370
+ const mutable = file as { content: string };
371
+ const original = mutable.content;
372
+ try {
373
+ mutable.content = `${original}x`;
374
+ expect(runtimeDocumentDigestV1(), file.path).not.toBe(stable);
375
+ } finally {
376
+ mutable.content = original;
377
+ }
378
+ }
379
+ expect(runtimeDocumentDigestV1()).toBe(stable);
380
+ });
381
+ });
382
+
383
+ describe("shell helpers", () => {
384
+ test("quotes a value that would otherwise break out of its argument", () => {
385
+ expect(shellQuote("it's")).toBe(`'it'"'"'s'`);
386
+ });
387
+
388
+ test("round-trips content through the base64 installer", () => {
389
+ const line = installFile("/tmp/x", "hello");
390
+ expect(line).toBe(`printf %s '${base64("hello")}' | base64 -d > /tmp/x`);
391
+ });
392
+ });
393
+
394
+ describe("Sprite naming", () => {
395
+ test("one Computer per User: the name derives from the User alone", () => {
396
+ expect(spriteName("user-1")).toBe(spriteName("user-1"));
397
+ expect(spriteName("user-1")).not.toBe(spriteName("user-2"));
398
+ });
399
+
400
+ test("the digest source is keyed so another owner kind cannot collide", () => {
401
+ expect(computerSpriteNameSourceV1("user-1")).toBe('["user","user-1"]');
402
+ });
403
+
404
+ test("the name is a legal Sprite name with a twelve-character digest", () => {
405
+ const name = spriteName("user-1");
406
+ expect(name).toMatch(/^frockbot-[0-9a-f]{12}$/);
407
+ expect(name.length).toBeLessThanOrEqual(63);
408
+ });
409
+
410
+ test("a long base name is trimmed so the result still fits", () => {
411
+ const name = spriteName("user-1", `a${"b".repeat(60)}`);
412
+ expect(name.length).toBeLessThanOrEqual(63);
413
+ });
414
+
415
+ test("refuses a base name that is not a legal Sprite name", () => {
416
+ expect(() => spriteName("user-1", "Frockbot")).toThrow(/base name/);
417
+ expect(() => spriteName("user-1", "-leading")).toThrow(/base name/);
418
+ });
419
+
420
+ test("refuses an empty User", () => {
421
+ expect(() => spriteName(" ")).toThrow(/non-empty userId/);
422
+ });
423
+ });
424
+
425
+ // The Computer's shell scripts, run for real.
426
+ //
427
+ // These live here rather than beside the provider because the scripts do: a
428
+ // Bot Durable Object no longer installs them, the Computer host does, and a
429
+ // test that had to stand up a provider to read a string out of a provisioning
430
+ // command was testing the wrong module. Each one installs production's own
431
+ // script into a temp tree, stubs only `flock` and GNU `stat`, and runs it.
432
+ describe("installed shell scripts", () => {
433
+ test("all of a User's Bots share one browser profile", () => {
434
+ // ADR 0012: one Computer per User, and "all Bots share the User's browser
435
+ // profile". One directory, not one per Bot — the assertion lives here
436
+ // because the provisioning document is what creates it.
437
+ expect(provisionScript).toContain(`${HOME_ROOT}/chrome-profile `);
438
+ expect(provisionScript).not.toContain("chrome-profiles");
439
+ // The flag set moved into the launcher (parity row 33); the desktop
440
+ // starter calls it and holds no flags of its own.
441
+ expect(installedScript(provisionScript, CHROME_LAUNCHER)).toContain(
442
+ `--user-data-dir=${HOME_ROOT}/chrome-profile`,
443
+ );
444
+ expect(
445
+ installedScript(provisionScript, `${RUNTIME_ROOT}/start-desktop.sh`),
446
+ ).toContain(`${CHROME_LAUNCHER} "$KEY"`);
447
+ });
448
+
449
+ test("every script the provisioning document installs is valid bash", async () => {
450
+ for (const path of [
451
+ `${RUNTIME_ROOT}/start-desktop.sh`,
452
+ ENSURE_AGENT_SCRIPT,
453
+ CONTROL_SCRIPT,
454
+ BOUNDED_LOG_SCRIPT,
455
+ `${RUNTIME_ROOT}/start-gateway.sh`,
456
+ ]) {
457
+ await expectValidShell(installedScript(provisionScript, path));
458
+ }
459
+ });
460
+
461
+ test("atomically grants an expired lease to one concurrent replacement", async () => {
462
+ const installed = installedScript(provisionScript, CONTROL_SCRIPT);
463
+ const directory = await mkdtemp(join(tmpdir(), "frockbot-control-"));
464
+ const runtimeRoot = join(directory, "runtime");
465
+ const scriptPath = join(directory, "control.sh");
466
+ const flockPath = join(directory, "flock");
467
+ const statPath = join(directory, "stat");
468
+ const helper = installed.replaceAll("/home/box/.frockbot", runtimeRoot);
469
+ await writeFile(scriptPath, helper);
470
+ await writeFile(
471
+ flockPath,
472
+ [
473
+ "#!/usr/bin/env python3",
474
+ "import fcntl, subprocess, sys",
475
+ "lock_path = sys.argv[2]",
476
+ "with open(lock_path, 'a') as lock:",
477
+ " fcntl.flock(lock, fcntl.LOCK_EX)",
478
+ " result = subprocess.run(sys.argv[3:])",
479
+ " raise SystemExit(result.returncode)",
480
+ "",
481
+ ].join("\n"),
482
+ );
483
+ await writeFile(
484
+ statPath,
485
+ // `stat -c %Y` is GNU; the shim answers with the host's own stat in one
486
+ // exec. A scripting-language shim here was the slow half of a hundred
487
+ // tenant scans and flaked the suite under load.
488
+ [
489
+ "#!/usr/bin/env bash",
490
+ 'if /usr/bin/stat -f %m / >/dev/null 2>&1; then exec /usr/bin/stat -f %m "${@: -1}"; fi',
491
+ 'exec /usr/bin/stat -c %Y "${@: -1}"',
492
+ "",
493
+ ].join("\n"),
494
+ );
495
+ await Promise.all([
496
+ chmod(scriptPath, 0o700),
497
+ chmod(flockPath, 0o700),
498
+ chmod(statPath, 0o700),
499
+ ]);
500
+ const key = "general-0123456789ab";
501
+ try {
502
+ expect(
503
+ (await runControl(scriptPath, "acquire", key, "owner-1", "90"))
504
+ .exitCode,
505
+ ).toBe(0);
506
+ const leasePath = join(runtimeRoot, "bots", key, "human-control");
507
+ const expiredAt = new Date(Date.now() - 120_000);
508
+ await utimes(leasePath, expiredAt, expiredAt);
509
+
510
+ const contenders = await Promise.all([
511
+ runControl(scriptPath, "acquire", key, "owner-2", "90"),
512
+ runControl(scriptPath, "acquire", key, "owner-3", "90"),
513
+ ]);
514
+
515
+ expect(contenders.map(({ exitCode }) => exitCode).sort()).toEqual([
516
+ 0, 73,
517
+ ]);
518
+ const winner = contenders[0]?.exitCode === 0 ? "owner-2" : "owner-3";
519
+ expect(
520
+ (await runControl(scriptPath, "renew", key, winner)).exitCode,
521
+ ).toBe(0);
522
+ expect(
523
+ (await runControl(scriptPath, "assert-agent", key, "agent-runtime"))
524
+ .exitCode,
525
+ ).toBe(73);
526
+ expect(
527
+ (await runControl(scriptPath, "release", key, winner)).exitCode,
528
+ ).toBe(0);
529
+ expect(
530
+ (await runControl(scriptPath, "assert-agent", key, "agent-runtime"))
531
+ .exitCode,
532
+ ).toBe(0);
533
+
534
+ expect(
535
+ (
536
+ await runControl(
537
+ scriptPath,
538
+ "acquire",
539
+ DESKTOP_GUI_LEASE_KEY,
540
+ "human-session",
541
+ )
542
+ ).exitCode,
543
+ ).toBe(0);
544
+ const fenced = await runControl(
545
+ scriptPath,
546
+ "assert-agent",
547
+ key,
548
+ "agent-runtime",
549
+ );
550
+ expect(fenced.exitCode).toBe(73);
551
+ expect(fenced.stderr).toContain("human-session");
552
+ const desktopLease = join(
553
+ runtimeRoot,
554
+ "bots",
555
+ DESKTOP_GUI_LEASE_KEY,
556
+ "human-control",
557
+ );
558
+ await utimes(desktopLease, expiredAt, expiredAt);
559
+ expect(
560
+ (await runControl(scriptPath, "assert-agent", key, "agent-runtime"))
561
+ .exitCode,
562
+ ).toBe(0);
563
+ } finally {
564
+ await rm(directory, { recursive: true, force: true });
565
+ }
566
+ });
567
+ });
568
+
569
+ describe("desktop slots are reclaimed from idle tenants only", () => {
570
+ /**
571
+ * Installs the ensure script into a temp tree, with `flock` and GNU `stat`
572
+ * stubbed the way the control-script test does: the script is production's,
573
+ * only its roots and its two coreutils are local.
574
+ */
575
+ async function installEnsureScript(): Promise<{
576
+ directory: string;
577
+ runtimeRoot: string;
578
+ run: (key: string) => Promise<{ exitCode: number; stdout: string }>;
579
+ }> {
580
+ const installed = installedScript(provisionScript, ENSURE_AGENT_SCRIPT);
581
+ const directory = await mkdtemp(join(tmpdir(), "frockbot-slots-"));
582
+ const runtimeRoot = join(directory, "runtime");
583
+ const scriptPath = join(directory, "ensure-agent.sh");
584
+ await writeFile(
585
+ scriptPath,
586
+ installed
587
+ .replaceAll("/home/box/.frockbot", runtimeRoot)
588
+ .replaceAll("/home/box", join(directory, "home"))
589
+ .replaceAll("/workspaces", join(directory, "workspaces")),
590
+ );
591
+ await writeFile(
592
+ join(directory, "flock"),
593
+ ["#!/usr/bin/env bash", "exit 0", ""].join("\n"),
594
+ );
595
+ await writeFile(
596
+ join(directory, "stat"),
597
+ // `stat -c %Y` is GNU; the shim answers with the host's own stat in one
598
+ // exec. A scripting-language shim here was the slow half of a hundred
599
+ // tenant scans and flaked the suite under load.
600
+ [
601
+ "#!/usr/bin/env bash",
602
+ 'if /usr/bin/stat -f %m / >/dev/null 2>&1; then exec /usr/bin/stat -f %m "${@: -1}"; fi',
603
+ 'exec /usr/bin/stat -c %Y "${@: -1}"',
604
+ "",
605
+ ].join("\n"),
606
+ );
607
+ await Promise.all([
608
+ chmod(scriptPath, 0o700),
609
+ chmod(join(directory, "flock"), 0o700),
610
+ chmod(join(directory, "stat"), 0o700),
611
+ ]);
612
+ return {
613
+ directory,
614
+ runtimeRoot,
615
+ run: async (key: string) => {
616
+ const child = Bun.spawn(
617
+ [scriptPath, key, Buffer.from("{}").toString("base64")],
618
+ {
619
+ env: { ...process.env, PATH: `${directory}:${process.env.PATH}` },
620
+ stdout: "pipe",
621
+ stderr: "pipe",
622
+ },
623
+ );
624
+ const [exitCode, stdout] = await Promise.all([
625
+ child.exited,
626
+ new Response(child.stdout).text(),
627
+ ]);
628
+ return { exitCode, stdout };
629
+ },
630
+ };
631
+ }
632
+
633
+ /** A tenant holding one slot, last seen `idleSeconds` ago. */
634
+ async function seedTenant(
635
+ runtimeRoot: string,
636
+ slot: number,
637
+ idleSeconds: number,
638
+ lease?: number,
639
+ ): Promise<string> {
640
+ const key = `tenant-${String(slot).padStart(3, "0")}`;
641
+ const bot = join(runtimeRoot, "bots", key);
642
+ await mkdir(bot, { recursive: true });
643
+ await writeFile(join(bot, "slot"), `${slot}\n`);
644
+ await writeFile(join(bot, "last-seen"), "");
645
+ const seenAt = new Date(Date.now() - idleSeconds * 1000);
646
+ await utimes(join(bot, "last-seen"), seenAt, seenAt);
647
+ await utimes(join(bot, "slot"), seenAt, seenAt);
648
+ if (lease !== undefined) {
649
+ await writeFile(join(bot, "human-control"), "viewer-1\n");
650
+ const leasedAt = new Date(Date.now() - lease * 1000);
651
+ await utimes(join(bot, "human-control"), leasedAt, leasedAt);
652
+ }
653
+ return key;
654
+ }
655
+
656
+ test("reclaims an idle tenant's display and never a live one", async () => {
657
+ const { directory, runtimeRoot, run } = await installEnsureScript();
658
+ try {
659
+ for (let slot = 0; slot < 100; slot += 1) {
660
+ // Slot 7's tenant went quiet long ago; every other tenant is one this
661
+ // provider ran something for moments ago.
662
+ await seedTenant(
663
+ runtimeRoot,
664
+ slot,
665
+ slot === 7 ? SLOT_IDLE_SECONDS + 600 : 5,
666
+ );
667
+ }
668
+
669
+ const ensured = await run("newcomer");
670
+
671
+ expect(ensured.exitCode).toBe(0);
672
+ expect(
673
+ (
674
+ await readFile(join(runtimeRoot, "bots/newcomer/slot"), "utf8")
675
+ ).trim(),
676
+ ).toBe("7");
677
+ // The idle tenant lost its slot; the live ones kept theirs.
678
+ expect(existsSync(join(runtimeRoot, "bots/tenant-007/slot"))).toBe(false);
679
+ expect(existsSync(join(runtimeRoot, "bots/tenant-008/slot"))).toBe(true);
680
+ } finally {
681
+ await rm(directory, { recursive: true, force: true });
682
+ }
683
+ }, 30_000);
684
+
685
+ test("refuses the new tenant when every display is live, rather than sharing one", async () => {
686
+ const { directory, runtimeRoot, run } = await installEnsureScript();
687
+ try {
688
+ for (let slot = 0; slot < 100; slot += 1) {
689
+ await seedTenant(runtimeRoot, slot, 5);
690
+ }
691
+
692
+ const ensured = await run("newcomer");
693
+
694
+ expect(ensured.exitCode).toBe(75);
695
+ expect(ensured.stdout).toContain("__FROCKBOT_NO_SLOTS__");
696
+ expect(existsSync(join(runtimeRoot, "bots/newcomer/slot"))).toBe(false);
697
+ } finally {
698
+ await rm(directory, { recursive: true, force: true });
699
+ }
700
+ }, 30_000);
701
+
702
+ test("an idle tenant under human control keeps its display", async () => {
703
+ const { directory, runtimeRoot, run } = await installEnsureScript();
704
+ try {
705
+ for (let slot = 0; slot < 100; slot += 1) {
706
+ // The only idle tenant is the one a human is watching right now.
707
+ await seedTenant(
708
+ runtimeRoot,
709
+ slot,
710
+ slot === 3 ? SLOT_IDLE_SECONDS + 600 : 5,
711
+ slot === 3 ? 5 : undefined,
712
+ );
713
+ }
714
+
715
+ const ensured = await run("newcomer");
716
+
717
+ expect(ensured.exitCode).toBe(75);
718
+ expect(existsSync(join(runtimeRoot, "bots/tenant-003/slot"))).toBe(true);
719
+ } finally {
720
+ await rm(directory, { recursive: true, force: true });
721
+ }
722
+ }, 30_000);
723
+
724
+ test("a fresh User-wide desktop lease keeps every idle display", async () => {
725
+ const { directory, runtimeRoot, run } = await installEnsureScript();
726
+ try {
727
+ for (let slot = 0; slot < 100; slot += 1) {
728
+ await seedTenant(runtimeRoot, slot, SLOT_IDLE_SECONDS + 600);
729
+ }
730
+ const leaseRoot = join(runtimeRoot, "bots", DESKTOP_GUI_LEASE_KEY);
731
+ await mkdir(leaseRoot, { recursive: true });
732
+ await writeFile(join(leaseRoot, "human-control"), "human-session\n");
733
+
734
+ const ensured = await run("newcomer");
735
+
736
+ expect(ensured.exitCode).toBe(75);
737
+ expect(existsSync(join(runtimeRoot, "bots/tenant-003/slot"))).toBe(true);
738
+ } finally {
739
+ await rm(directory, { recursive: true, force: true });
740
+ }
741
+ }, 30_000);
742
+
743
+ test("skips a tenant whose viewer just renewed last-seen", async () => {
744
+ const { directory, runtimeRoot, run } = await installEnsureScript();
745
+ try {
746
+ for (let slot = 0; slot < 100; slot += 1) {
747
+ await seedTenant(runtimeRoot, slot, SLOT_IDLE_SECONDS + 600);
748
+ }
749
+ // Viewer open/renew touches this existing registry fact. The reclaim
750
+ // scan needs no viewer-specific file: a watcher is simply a live tenant.
751
+ const watched = join(runtimeRoot, "bots/tenant-003/last-seen");
752
+ const renewedAt = new Date();
753
+ await utimes(watched, renewedAt, renewedAt);
754
+
755
+ const ensured = await run("newcomer");
756
+
757
+ expect(ensured.exitCode).toBe(0);
758
+ expect(existsSync(join(runtimeRoot, "bots/tenant-003/slot"))).toBe(true);
759
+ } finally {
760
+ await rm(directory, { recursive: true, force: true });
761
+ }
762
+ }, 30_000);
763
+ });
764
+
765
+ describe("the background-process logger", () => {
766
+ /**
767
+ * A process that outlives its Turn can write for hours. The cap is what
768
+ * keeps its log from becoming an unbounded write to a disk the User pays
769
+ * for, and keeping both ends is what keeps the log useful: a long job says
770
+ * what it set out to do at the start and what went wrong at the end.
771
+ */
772
+ test("keeps the head and the tail and drops the middle", async () => {
773
+ const installed = installedScript(provisionScript, BOUNDED_LOG_SCRIPT);
774
+ const directory = await mkdtemp(join(tmpdir(), "frockbot-log-"));
775
+ const scriptPath = join(directory, "bounded-log.sh");
776
+ await writeFile(scriptPath, installed);
777
+ await chmod(scriptPath, 0o700);
778
+ const out = join(directory, "log");
779
+
780
+ // Small caps, so the test writes kilobytes rather than megabytes and the
781
+ // trimming path runs many times rather than never.
782
+ const input = Array.from(
783
+ { length: 500 },
784
+ (_, index) => `line-${String(index).padStart(4, "0")}\n`,
785
+ ).join("");
786
+ const child = Bun.spawn([scriptPath, out, "200", "200"], {
787
+ stdin: new TextEncoder().encode(input),
788
+ stdout: "pipe",
789
+ stderr: "pipe",
790
+ });
791
+ expect(await child.exited, await new Response(child.stderr).text()).toBe(0);
792
+
793
+ const head = await readFile(`${out}.head`, "utf8");
794
+ const tail = await readFile(`${out}.tail`, "utf8");
795
+ expect(head).toContain("line-0000");
796
+ expect(head.length).toBeLessThan(400);
797
+ expect(tail).toContain("line-0499");
798
+ expect(tail.length).toBeLessThanOrEqual(400);
799
+ // The middle really is gone: neither half holds it.
800
+ expect(head + tail).not.toContain("line-0250");
801
+ });
802
+
803
+ test("declares a 256 KiB cap by default", () => {
804
+ expect(BOUNDED_LOG_HEAD_BYTES * 2).toBe(262_144);
805
+ expect(installedScript(provisionScript, BOUNDED_LOG_SCRIPT)).toContain(
806
+ `HEAD_BYTES=${"${2:-"}${BOUNDED_LOG_HEAD_BYTES}}`,
807
+ );
808
+ });
809
+ });
810
+
811
+ // Parity row 33: "a launcher that enforces correct browser flags; GUI never
812
+ // driven from the shell". Two layers, both policy and neither a boundary —
813
+ // which is exactly why the refusal has to say what to use instead.
814
+ describe("the GUI is never driven from the shell", () => {
815
+ test("names the command a shell string would actually run", () => {
816
+ for (const command of [
817
+ "chromium --headless",
818
+ "xdotool key Return",
819
+ "cd /tmp && scrot out.png",
820
+ "true; sudo x11vnc -display :1",
821
+ "DISPLAY=:1 import -window root shot.png",
822
+ "/usr/bin/chromium about:blank",
823
+ "ls | wmctrl -l",
824
+ "Xvfb :3",
825
+ ]) {
826
+ expect(shellGuiCommandV1(command), command).toBeDefined();
827
+ }
828
+ });
829
+
830
+ test("leaves a command that merely mentions one alone", () => {
831
+ for (const command of [
832
+ "echo 'chromium is not installed'",
833
+ "grep -r import ./src",
834
+ "python3 -c 'import os'",
835
+ "cat /home/box/chromium.log",
836
+ "ls /home/box/bin/xdotool",
837
+ "printf '%s' scrotum",
838
+ ]) {
839
+ expect(shellGuiCommandV1(command), command).toBeUndefined();
840
+ }
841
+ });
842
+
843
+ test("both layers print the same sentence, naming the sanctioned surface", () => {
844
+ const refusal = computerGuiRefusalV1("xdotool");
845
+ expect(refusal).toContain("computer_browser");
846
+ expect(refusal).toContain("computer_screenshot");
847
+ expect(refusal).toContain(CHROME_LAUNCHER);
848
+ expect(guiShimScript("xdotool")).toContain(shellQuote(refusal));
849
+ expect(guiShimScript("xdotool")).toContain("exit 64");
850
+ });
851
+
852
+ test("a shim steps aside for the Computer's own sanctioned scripts", async () => {
853
+ // The shims sit on the tenant's PATH, and the desktop starter and the
854
+ // screenshot exec run the very binaries they cover. Without this the
855
+ // policy would break the Computer rather than the shell habit.
856
+ const directory = await mkdtemp(join(tmpdir(), "frockbot-shim-"));
857
+ try {
858
+ const binDirectory = join(directory, "bin");
859
+ const realDirectory = join(directory, "real");
860
+ await mkdir(binDirectory, { recursive: true });
861
+ await mkdir(realDirectory, { recursive: true });
862
+ const shimPath = join(binDirectory, "xdotool");
863
+ await writeFile(
864
+ shimPath,
865
+ guiShimScript("xdotool").replaceAll(SHIMS_ROOT, binDirectory),
866
+ );
867
+ await writeFile(
868
+ join(realDirectory, "xdotool"),
869
+ ["#!/usr/bin/env bash", "echo real-xdotool", ""].join("\n"),
870
+ );
871
+ await chmod(shimPath, 0o755);
872
+ await chmod(join(realDirectory, "xdotool"), 0o755);
873
+ // The shim dir leads, as it does on a tenant's PATH; the system
874
+ // directories follow so `bash` itself is still findable.
875
+ const path = `${binDirectory}:${realDirectory}:/usr/bin:/bin`;
876
+
877
+ const refused = Bun.spawn([shimPath], {
878
+ env: { PATH: path },
879
+ stdout: "pipe",
880
+ stderr: "pipe",
881
+ });
882
+ const [refusedCode, refusedError] = await Promise.all([
883
+ refused.exited,
884
+ new Response(refused.stderr).text(),
885
+ ]);
886
+ expect(refusedCode).toBe(64);
887
+ expect(refusedError).toContain("never driven from the shell");
888
+
889
+ const allowed = Bun.spawn([shimPath], {
890
+ env: { PATH: path, FROCKBOT_SANCTIONED_SURFACE: "1" },
891
+ stdout: "pipe",
892
+ stderr: "pipe",
893
+ });
894
+ const [allowedCode, allowedOut] = await Promise.all([
895
+ allowed.exited,
896
+ new Response(allowed.stdout).text(),
897
+ ]);
898
+ expect(allowedCode).toBe(0);
899
+ expect(allowedOut.trim()).toBe("real-xdotool");
900
+ } finally {
901
+ await rm(directory, { recursive: true, force: true });
902
+ }
903
+ });
904
+ });
905
+
906
+ // Parity row 27: "a self-check the Bot runs and reads a log from".
907
+ describe("box-doctor", () => {
908
+ test("prints GrokBot's log lines and one machine-readable report", async () => {
909
+ const directory = await mkdtemp(join(tmpdir(), "frockbot-doctor-"));
910
+ try {
911
+ const logPath = join(directory, "box-doctor.log");
912
+ const scriptPath = join(directory, "box-doctor.sh");
913
+ await writeFile(
914
+ scriptPath,
915
+ installedScript(provisionScript, DOCTOR_SCRIPT).replaceAll(
916
+ DOCTOR_LOG,
917
+ logPath,
918
+ ),
919
+ );
920
+ await chmod(scriptPath, 0o755);
921
+
922
+ const child = Bun.spawn([scriptPath, "doctor-bot", "7"], {
923
+ stdout: "pipe",
924
+ stderr: "pipe",
925
+ });
926
+ const [exitCode, stdout] = await Promise.all([
927
+ child.exited,
928
+ new Response(child.stdout).text(),
929
+ ]);
930
+
931
+ // A Computer with failing checks is still a Computer that answered:
932
+ // the report is the outcome, and a non-zero exit would make an
933
+ // unhealthy box indistinguishable from an unreachable one.
934
+ expect(exitCode).toBe(0);
935
+ const line = stdout
936
+ .split("\n")
937
+ .find((candidate) => candidate.startsWith(DOCTOR_MARKER));
938
+ expect(line).toBeDefined();
939
+ const report = JSON.parse(line!.slice(DOCTOR_MARKER.length)) as {
940
+ schemaVersion: number;
941
+ generation: number;
942
+ capturedAt: string;
943
+ checks: { name: string; status: string; detail: string }[];
944
+ browserIdentity: unknown;
945
+ summary: string;
946
+ };
947
+ expect(report.schemaVersion).toBe(DOCTOR_REPORT_SCHEMA_VERSION);
948
+ expect(report.generation).toBe(7);
949
+ expect(report.capturedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
950
+ expect(report.summary).toMatch(/^\d+ checks, \d+ passed, \d+ failed$/);
951
+ // Every check the plan names, on a box that has none of them: what is
952
+ // asserted is that each one is *reported*, not that it passes.
953
+ expect(report.checks.map((check) => check.name)).toEqual([
954
+ "disk-root",
955
+ "disk-home",
956
+ "scratch",
957
+ "desktop-gateway",
958
+ "sync-watcher",
959
+ "tenant-display",
960
+ "browser",
961
+ "browser-profile",
962
+ "browser-identity",
963
+ "sync-signal",
964
+ "reference-docs",
965
+ "launcher",
966
+ "clock",
967
+ "dns",
968
+ "sprite-hold",
969
+ ]);
970
+ for (const check of report.checks) {
971
+ expect(["pass", "fail"]).toContain(check.status);
972
+ expect(check.detail.length).toBeGreaterThan(0);
973
+ }
974
+ // Parity row 34b: nothing on this box is a browser, so the check fails
975
+ // legibly and the report carries no measurement rather than an empty
976
+ // one. The measured shape is proven at the decoder and on a live Sprite.
977
+ expect(report.browserIdentity).toBeNull();
978
+ expect(
979
+ report.checks.find((check) => check.name === "browser-identity"),
980
+ ).toMatchObject({ status: "fail" });
981
+
982
+ const log = await readFile(logPath, "utf8");
983
+ for (const check of report.checks) {
984
+ expect(log).toContain(
985
+ `[box-doctor] ${check.status === "pass" ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`,
986
+ );
987
+ }
988
+ expect(log).toContain(`[box-doctor] SUMMARY ${report.summary}`);
989
+ } finally {
990
+ await rm(directory, { recursive: true, force: true });
991
+ }
992
+ }, 30_000);
993
+
994
+ // Parity row 34b. The action is a literal in the runtime because the module
995
+ // builds shell documents in a Worker, so the encoding is asserted here
996
+ // rather than trusted.
997
+ test("asks the browser helper for an identity it understands", () => {
998
+ expect(
999
+ JSON.parse(
1000
+ Buffer.from(DOCTOR_BROWSER_IDENTITY_ACTION, "base64url").toString(
1001
+ "utf8",
1002
+ ),
1003
+ ),
1004
+ ).toEqual({ action: "identity" });
1005
+ expect(browserHelper).toContain('action.action === "identity"');
1006
+ expect(browserHelper).toContain("navigator.webdriver");
1007
+ expect(boxDoctorScript).toContain(DOCTOR_BROWSER_IDENTITY_ACTION);
1008
+ // A tell is a FAIL, and both tells are named in the script rather than
1009
+ // inferred by whatever reads the report.
1010
+ expect(boxDoctorScript).toContain("HeadlessChrome");
1011
+ expect(boxDoctorScript).toContain('"webdriver":true');
1012
+ });
1013
+
1014
+ test("reports the scratch, the launcher, and the reference version it expects", () => {
1015
+ expect(boxDoctorScript).toContain(SCRATCH_ROOT);
1016
+ expect(boxDoctorScript).toContain(CHROME_LAUNCHER);
1017
+ // The browser is Playwright's own build behind a stable symlink, and the
1018
+ // Sprite hold is the thing that must *not* still be held once
1019
+ // provisioning is done.
1020
+ expect(boxDoctorScript).toContain(CHROMIUM_PATH);
1021
+ expect(boxDoctorScript).toContain(SPRITE_API_SOCKET);
1022
+ expect(boxDoctorScript).toContain(PROVISION_TASK);
1023
+ expect(boxDoctorScript).toContain(REFERENCE_DOCS_VERSION);
1024
+ for (const command of COMPUTER_GUI_SHELL_COMMANDS) {
1025
+ expect(boxDoctorScript).toContain(`${SHIMS_ROOT}/${command}`);
1026
+ }
1027
+ });
1028
+ });
1029
+
1030
+ describe("the shipped reference set", () => {
1031
+ test("covers the four documents a Bot debugs its Computer with", () => {
1032
+ expect(REFERENCE_DOCS.map((document) => document.name)).toEqual([
1033
+ "README.md",
1034
+ "layout.md",
1035
+ "browser.md",
1036
+ "debugging-the-box.md",
1037
+ ]);
1038
+ });
1039
+
1040
+ test("says once, in layout.md, that the shared scratch is not durable", () => {
1041
+ const layout = REFERENCE_DOCS.find(
1042
+ (document) => document.name === "layout.md",
1043
+ );
1044
+ expect(layout?.content).toContain(SCRATCH_ROOT);
1045
+ expect(layout?.content).toContain("not** a durable root");
1046
+ });
1047
+ });