@frockbot/computer-host-runtime 0.3.1 → 0.3.2
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.
- package/package.json +1 -1
- package/src/runtime.test.ts +90 -1
- package/src/runtime.ts +146 -1
package/package.json
CHANGED
package/src/runtime.test.ts
CHANGED
|
@@ -13,6 +13,13 @@ import { tmpdir } from "node:os";
|
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import { describe, expect, test } from "bun:test";
|
|
15
15
|
import {
|
|
16
|
+
APPLET_SDK_VERSION,
|
|
17
|
+
APPLET_SHIM_PATH,
|
|
18
|
+
appletShimScript,
|
|
19
|
+
APPLETS_ROOT,
|
|
20
|
+
APPLETS_RUNTIME_FILES,
|
|
21
|
+
APPLETS_SDK_FAILURE_PATH,
|
|
22
|
+
MINIFLARE_VERSION,
|
|
16
23
|
BIN_ROOT,
|
|
17
24
|
BOTS_ROOT,
|
|
18
25
|
boxDoctorScript,
|
|
@@ -424,9 +431,10 @@ state running`);
|
|
|
424
431
|
);
|
|
425
432
|
});
|
|
426
433
|
|
|
427
|
-
test("the update runner
|
|
434
|
+
test("the update runner replaces files and installs nothing", () => {
|
|
428
435
|
expect(UPDATE_PHASES.map((phase) => phase.name)).toEqual([
|
|
429
436
|
"runtime",
|
|
437
|
+
"applets",
|
|
430
438
|
"reference",
|
|
431
439
|
]);
|
|
432
440
|
for (const phase of UPDATE_PHASES) {
|
|
@@ -435,6 +443,85 @@ state running`);
|
|
|
435
443
|
const updateDocument = UPDATE_PHASES.map((phase) => phase.body).join("\n");
|
|
436
444
|
expect(updateDocument).not.toContain("apt-get");
|
|
437
445
|
expect(updateDocument).not.toContain("playwright-core/cli.js install");
|
|
446
|
+
// An in-place update swaps names over files it owns and reaches no
|
|
447
|
+
// network: the `applets` phase installs the shim and leaves the SDK's
|
|
448
|
+
// dependency tree — which an `applet dev` may be running out of — alone.
|
|
449
|
+
expect(updateDocument).not.toContain("npm install");
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
describe("the applets phase", () => {
|
|
454
|
+
test("installs the Applets runtime after the browser", () => {
|
|
455
|
+
const names = PROVISION_PHASES.map((phase) => phase.name);
|
|
456
|
+
expect(names).toEqual([
|
|
457
|
+
"layout",
|
|
458
|
+
"packages",
|
|
459
|
+
"runtime",
|
|
460
|
+
"browser",
|
|
461
|
+
"applets",
|
|
462
|
+
"reference",
|
|
463
|
+
]);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("installs miniflare and the SDK into a prefix of their own", () => {
|
|
467
|
+
const applets = PROVISION_PHASES.find((phase) => phase.name === "applets")!;
|
|
468
|
+
// Not the runtime root: the browser driver and the Applets runtime are two
|
|
469
|
+
// dependency trees, and one resolution over both would let an Applets
|
|
470
|
+
// upgrade move `playwright-core`.
|
|
471
|
+
expect(applets.body).toContain(
|
|
472
|
+
`npm install --prefix ${APPLETS_ROOT} --no-audit --no-fund miniflare@${MINIFLARE_VERSION}`,
|
|
473
|
+
);
|
|
474
|
+
expect(applets.body).toContain(
|
|
475
|
+
`npm install --prefix ${APPLETS_ROOT} --no-audit --no-fund @frockbot/applet-sdk@${APPLET_SDK_VERSION}`,
|
|
476
|
+
);
|
|
477
|
+
expect(APPLETS_ROOT.startsWith(`${RUNTIME_ROOT}/`)).toBe(true);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("an SDK that cannot be fetched leaves a record and not a failed run", () => {
|
|
481
|
+
// `@frockbot/applet-sdk` is not on npm yet. A Computer whose SDK could not
|
|
482
|
+
// be installed still browses, execs, and syncs, so the phase records the
|
|
483
|
+
// failure for the doctor rather than failing provisioning.
|
|
484
|
+
const applets = PROVISION_PHASES.find((phase) => phase.name === "applets")!;
|
|
485
|
+
expect(applets.body).toContain(`> ${APPLETS_SDK_FAILURE_PATH}`);
|
|
486
|
+
expect(applets.body).toContain(`rm -f ${APPLETS_SDK_FAILURE_PATH}`);
|
|
487
|
+
expect(boxDoctorScript).toContain("record applets-sdk fail");
|
|
488
|
+
expect(boxDoctorScript).toContain("record applets-sdk pass");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("runs again rather than once, because the SDK has not shipped", () => {
|
|
492
|
+
const applets = PROVISION_PHASES.find((phase) => phase.name === "applets")!;
|
|
493
|
+
expect(applets.always).toBe(true);
|
|
494
|
+
expect(provisionScript).not.toContain('[ ! -f "$MARKERS/applets"');
|
|
495
|
+
// Both installs are guarded, so a second run on a provisioned Computer is
|
|
496
|
+
// two directory tests.
|
|
497
|
+
expect(applets.body).toContain(`[ ! -d ${APPLETS_ROOT}/node_modules/`);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("puts `applet` on the tenant's PATH, execing the SDK's own binary", () => {
|
|
501
|
+
// `bin` leads a tenant's PATH after `shims`, and `shims` holds refusals —
|
|
502
|
+
// this is a real command, so it belongs in `bin`.
|
|
503
|
+
expect(APPLET_SHIM_PATH).toBe(`${BIN_ROOT}/applet`);
|
|
504
|
+
expect(appletShimScript).toContain('exec "$APPLET" "$@"');
|
|
505
|
+
expect(appletShimScript).toContain(
|
|
506
|
+
`APPLET=${APPLETS_ROOT}/node_modules/.bin/applet`,
|
|
507
|
+
);
|
|
508
|
+
// The node shim on the base image re-execs itself for ever without this.
|
|
509
|
+
expect(appletShimScript).toContain("/etc/profile.d/languages_paths");
|
|
510
|
+
expect(APPLETS_RUNTIME_FILES[0]!.mode).toBe(0o755);
|
|
511
|
+
// Declared, so the digest moves when the shim does and an existing
|
|
512
|
+
// Computer is actually reached by the change.
|
|
513
|
+
expect(RUNTIME_DOCUMENT_FILES.map((file) => file.path)).toContain(
|
|
514
|
+
APPLET_SHIM_PATH,
|
|
515
|
+
);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("the reference set tells a Bot the command exists and where it is", () => {
|
|
519
|
+
const layout = REFERENCE_DOCS.find(
|
|
520
|
+
(document) => document.name === "layout.md",
|
|
521
|
+
);
|
|
522
|
+
expect(layout?.content).toContain("applet build");
|
|
523
|
+
expect(layout?.content).toContain(APPLETS_ROOT);
|
|
524
|
+
expect(layout?.content).toContain("user-packages/applets/source");
|
|
438
525
|
});
|
|
439
526
|
});
|
|
440
527
|
|
|
@@ -1046,6 +1133,8 @@ describe("box-doctor", () => {
|
|
|
1046
1133
|
"browser-profile",
|
|
1047
1134
|
"browser-identity",
|
|
1048
1135
|
"sync-signal",
|
|
1136
|
+
"applets",
|
|
1137
|
+
"applets-sdk",
|
|
1049
1138
|
"reference-docs",
|
|
1050
1139
|
"launcher",
|
|
1051
1140
|
"clock",
|
package/src/runtime.ts
CHANGED
|
@@ -112,6 +112,41 @@ export const PLAYWRIGHT_VERSION = "1.55.0";
|
|
|
112
112
|
*/
|
|
113
113
|
export const PLAYWRIGHT_PLATFORM = "ubuntu24.04-x64";
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Where the Applets SDK and its runtime are installed (ADR 0022 decision 7).
|
|
117
|
+
*
|
|
118
|
+
* A prefix of its own rather than the runtime root's `node_modules`, because
|
|
119
|
+
* the browser's `playwright-core` and the SDK's `miniflare` are two unrelated
|
|
120
|
+
* dependency trees on two unrelated release cadences, and one `npm install`
|
|
121
|
+
* resolving both would let an Applets upgrade move the browser driver.
|
|
122
|
+
*/
|
|
123
|
+
export const APPLETS_ROOT = `${RUNTIME_ROOT}/applets`;
|
|
124
|
+
/**
|
|
125
|
+
* The `applet` shim on a tenant's PATH.
|
|
126
|
+
*
|
|
127
|
+
* In `bin` rather than `shims`: `shims` holds refusals — the sanctioned-surface
|
|
128
|
+
* shims that decline a GUI command — and this is the opposite, a real command
|
|
129
|
+
* a Bot is meant to run. `bin` leads a tenant's PATH after `shims`
|
|
130
|
+
* (`plugin-fly-sprite/src/computer.ts`, `tenantEnvironment`), so `applet` is
|
|
131
|
+
* reachable by name from the working directory the Bot already has.
|
|
132
|
+
*/
|
|
133
|
+
export const APPLET_SHIM_PATH = `${BIN_ROOT}/applet`;
|
|
134
|
+
/**
|
|
135
|
+
* Written when the SDK install failed, and read by the doctor.
|
|
136
|
+
*
|
|
137
|
+
* `@frockbot/applet-sdk` is not on npm yet. Its install is therefore guarded:
|
|
138
|
+
* a Computer whose SDK could not be fetched is a Computer that still browses,
|
|
139
|
+
* execs, and syncs, so the phase leaves this file and succeeds rather than
|
|
140
|
+
* failing provisioning over a Package that has not shipped. The doctor turns
|
|
141
|
+
* the file into a named check, which is where an unshipped SDK belongs — a
|
|
142
|
+
* reported fact, not a Computer nobody can open.
|
|
143
|
+
*/
|
|
144
|
+
export const APPLETS_SDK_FAILURE_PATH = `${APPLETS_ROOT}/.sdk-unavailable`;
|
|
145
|
+
/** The Applets SDK the Computer authors and previews an Applet with. */
|
|
146
|
+
export const APPLET_SDK_VERSION = "0.1.0";
|
|
147
|
+
/** Pinned with the SDK: `applet dev` embeds this Miniflare, never wrangler. */
|
|
148
|
+
export const MINIFLARE_VERSION = "5.20260828.0-alpha";
|
|
149
|
+
|
|
115
150
|
/**
|
|
116
151
|
* Everything the Computer's desktop needs from the distribution, and nothing
|
|
117
152
|
* that merely recommends itself.
|
|
@@ -848,7 +883,7 @@ export const CLOCK_FLOOR_EPOCH = 1_756_684_800;
|
|
|
848
883
|
* corrected. The version is compared on every adoption instead, and the whole
|
|
849
884
|
* set is rewritten when it moves. Bump it whenever a document below changes.
|
|
850
885
|
*/
|
|
851
|
-
export const REFERENCE_DOCS_VERSION = "2026-09-
|
|
886
|
+
export const REFERENCE_DOCS_VERSION = "2026-09-03.1";
|
|
852
887
|
|
|
853
888
|
/**
|
|
854
889
|
* What a Bot reads to debug its own Computer.
|
|
@@ -919,6 +954,24 @@ on an image rebuild, a Computer reset, and a host migration. Put working files
|
|
|
919
954
|
here, never the only copy of anything.
|
|
920
955
|
|
|
921
956
|
\`/tmp\` is the same story with a shorter life: assume a restart empties it.
|
|
957
|
+
|
|
958
|
+
## Applets — written here, run somewhere else
|
|
959
|
+
|
|
960
|
+
Applet source is a durable root like any other:
|
|
961
|
+
\`${DATA_ROOT}/user-packages/applets/source/<appletId>/\`. Write it with the
|
|
962
|
+
ordinary file tools, then use \`applet\` — it is on your PATH:
|
|
963
|
+
|
|
964
|
+
| Command | What |
|
|
965
|
+
|---|---|
|
|
966
|
+
| \`applet check\` | type-check and lint one Applet |
|
|
967
|
+
| \`applet build\` | write \`<appletId>/dist/\` — this is what a publish reads |
|
|
968
|
+
| \`applet dev\` | run it locally on this Computer and print a URL to open in the browser |
|
|
969
|
+
|
|
970
|
+
The SDK and its runtime live under \`${APPLETS_ROOT}\`, installed when this
|
|
971
|
+
Computer was provisioned. An Applet **never runs for real here**: \`applet dev\`
|
|
972
|
+
is a preview, and publishing hands the built artifact to the kernel, which runs
|
|
973
|
+
it. If \`applet\` says the SDK is not installed, run the self-check and read the
|
|
974
|
+
\`applets-sdk\` line.
|
|
922
975
|
`,
|
|
923
976
|
},
|
|
924
977
|
{
|
|
@@ -1160,6 +1213,37 @@ export const provisionPathPreamble = `if [ -r /etc/profile.d/languages_paths ];
|
|
|
1160
1213
|
export PATH
|
|
1161
1214
|
fi`;
|
|
1162
1215
|
|
|
1216
|
+
/**
|
|
1217
|
+
* `applet` on a tenant's PATH (ADR 0022 decision 7).
|
|
1218
|
+
*
|
|
1219
|
+
* A shim rather than a symlink, for the same reason every other node entry
|
|
1220
|
+
* point here is: `/.sprite/bin/node` is a bash re-exec shim whose last resort
|
|
1221
|
+
* is `command -v node`, which in a non-login shell finds the shim again and
|
|
1222
|
+
* loops for ever. The preamble puts the real toolchain on PATH first, so the
|
|
1223
|
+
* SDK's own `#!/usr/bin/env node` resolves to a binary.
|
|
1224
|
+
*
|
|
1225
|
+
* A missing SDK is a sentence, not a stack trace: the install is guarded (see
|
|
1226
|
+
* {@link APPLETS_SDK_FAILURE_PATH}), so this is the state a Bot will meet
|
|
1227
|
+
* until `@frockbot/applet-sdk` is published, and it says where to look.
|
|
1228
|
+
*/
|
|
1229
|
+
export const appletShimScript = `#!/usr/bin/env bash
|
|
1230
|
+
set -u
|
|
1231
|
+
${provisionPathPreamble}
|
|
1232
|
+
APPLET=${APPLETS_ROOT}/node_modules/.bin/applet
|
|
1233
|
+
if [ ! -x "$APPLET" ]; then
|
|
1234
|
+
echo "the Applets SDK is not installed at ${APPLETS_ROOT}; run computer_doctor and read the applets-sdk check" >&2
|
|
1235
|
+
exit 127
|
|
1236
|
+
fi
|
|
1237
|
+
exec "$APPLET" "$@"
|
|
1238
|
+
`;
|
|
1239
|
+
|
|
1240
|
+
/** The files the `applets` phase installs, with their modes. */
|
|
1241
|
+
export const APPLETS_RUNTIME_FILES: readonly {
|
|
1242
|
+
readonly path: string;
|
|
1243
|
+
readonly content: string;
|
|
1244
|
+
readonly mode: number;
|
|
1245
|
+
}[] = [{ path: APPLET_SHIM_PATH, content: appletShimScript, mode: 0o755 }];
|
|
1246
|
+
|
|
1163
1247
|
/**
|
|
1164
1248
|
* The Computer's self-check (parity row 27).
|
|
1165
1249
|
*
|
|
@@ -1324,6 +1408,23 @@ elif [ "$CONFLICTS" -gt 0 ]; then
|
|
|
1324
1408
|
else
|
|
1325
1409
|
record sync-signal pass "signal $(cat "$SIGNAL" 2>/dev/null), last moved $((NOW - $(stat -c %Y "$SIGNAL"))) s ago, no conflicts"
|
|
1326
1410
|
fi
|
|
1411
|
+
if [ -x ${APPLET_SHIM_PATH} ] && [ -d ${APPLETS_ROOT}/node_modules/miniflare ]; then
|
|
1412
|
+
record applets pass "the Applets runtime is installed under ${APPLETS_ROOT} and \\"applet\\" is on your PATH"
|
|
1413
|
+
elif [ -x ${APPLET_SHIM_PATH} ]; then
|
|
1414
|
+
record applets fail "\\"applet\\" is on your PATH but no miniflare is installed under ${APPLETS_ROOT}; \\"applet dev\\" cannot run an Applet"
|
|
1415
|
+
else
|
|
1416
|
+
record applets fail "no ${APPLET_SHIM_PATH}; provisioning installs it, and no Applet can be checked, built, or previewed without it"
|
|
1417
|
+
fi
|
|
1418
|
+
# Named and non-fatal on purpose: \`@frockbot/applet-sdk\` is not published
|
|
1419
|
+
# yet, so the provisioning phase's guarded install is expected to fail and
|
|
1420
|
+
# leave this file rather than fail the whole run. This is where that shows up.
|
|
1421
|
+
if [ -d ${APPLETS_ROOT}/node_modules/@frockbot/applet-sdk ]; then
|
|
1422
|
+
record applets-sdk pass "the Applets SDK is installed under ${APPLETS_ROOT}"
|
|
1423
|
+
elif [ -f ${APPLETS_SDK_FAILURE_PATH} ]; then
|
|
1424
|
+
record applets-sdk fail "$(head -n 1 ${APPLETS_SDK_FAILURE_PATH} 2>/dev/null); everything else on this Computer is unaffected"
|
|
1425
|
+
else
|
|
1426
|
+
record applets-sdk fail "no Applets SDK under ${APPLETS_ROOT} and no record of an attempt; it installs when this Computer is next provisioned"
|
|
1427
|
+
fi
|
|
1327
1428
|
INSTALLED=$(cat ${REFERENCE_ROOT}/.version 2>/dev/null || echo none)
|
|
1328
1429
|
if [ "$INSTALLED" = "${REFERENCE_DOCS_VERSION}" ]; then
|
|
1329
1430
|
record reference-docs pass "${REFERENCE_ROOT} holds version ${REFERENCE_DOCS_VERSION}"
|
|
@@ -1501,6 +1602,39 @@ if [ ! -x ${CHROMIUM_PATH} ]; then
|
|
|
1501
1602
|
# start-desktop.sh and an upgrade is one relink rather than a script change.
|
|
1502
1603
|
ln -sfn "$CHROMIUM_BUILD" ${CHROMIUM_PATH}
|
|
1503
1604
|
fi`,
|
|
1605
|
+
},
|
|
1606
|
+
{
|
|
1607
|
+
name: "applets",
|
|
1608
|
+
label: "installing the Applets SDK",
|
|
1609
|
+
// Version-guarded by its own `[ ! -d ]` tests rather than by a marker, for
|
|
1610
|
+
// the same reason the reference phase is: a marker would make this run
|
|
1611
|
+
// exactly once in a Computer's life, and `@frockbot/applet-sdk` is not on
|
|
1612
|
+
// npm yet — the first run on a Computer provisioned today is expected to
|
|
1613
|
+
// fail its guarded step. Once both trees are present this phase is two
|
|
1614
|
+
// directory tests and a file install, so running it again costs nothing.
|
|
1615
|
+
always: true,
|
|
1616
|
+
// ADR 0022 decision 7: an Applet is authored on the Computer and run in
|
|
1617
|
+
// the loader, so this installs what authoring needs and nothing that runs
|
|
1618
|
+
// an Applet for real. Everything is `npm install --prefix` into a prefix
|
|
1619
|
+
// of this phase's own: no `apt`, no distribution package, no daemon, and
|
|
1620
|
+
// nothing that outlives the command.
|
|
1621
|
+
body: `mkdir -p ${APPLETS_ROOT}
|
|
1622
|
+
if [ ! -d ${APPLETS_ROOT}/node_modules/miniflare ]; then
|
|
1623
|
+
npm install --prefix ${APPLETS_ROOT} --no-audit --no-fund miniflare@${MINIFLARE_VERSION}
|
|
1624
|
+
fi
|
|
1625
|
+
# Guarded, and deliberately not fatal. \`@frockbot/applet-sdk\` is not on npm
|
|
1626
|
+
# yet, and a Computer that cannot fetch it is still a Computer: it browses,
|
|
1627
|
+
# execs, and syncs. The failure is recorded as a file the doctor reports under
|
|
1628
|
+
# \`applets-sdk\` rather than as a phase that fails and leaves the whole
|
|
1629
|
+
# provisioning run resumable-but-unfinished.
|
|
1630
|
+
if [ ! -d ${APPLETS_ROOT}/node_modules/@frockbot/applet-sdk ]; then
|
|
1631
|
+
if npm install --prefix ${APPLETS_ROOT} --no-audit --no-fund @frockbot/applet-sdk@${APPLET_SDK_VERSION}; then
|
|
1632
|
+
rm -f ${APPLETS_SDK_FAILURE_PATH}
|
|
1633
|
+
else
|
|
1634
|
+
printf '%s\\n' "npm could not install @frockbot/applet-sdk@${APPLET_SDK_VERSION}" > ${APPLETS_SDK_FAILURE_PATH}
|
|
1635
|
+
fi
|
|
1636
|
+
fi
|
|
1637
|
+
${installDeclaredFiles(APPLETS_RUNTIME_FILES)}`,
|
|
1504
1638
|
},
|
|
1505
1639
|
{
|
|
1506
1640
|
name: "reference",
|
|
@@ -1533,6 +1667,16 @@ export const UPDATE_PHASES: readonly {
|
|
|
1533
1667
|
label: "Updating the Computer runtime",
|
|
1534
1668
|
body: PROVISION_PHASES.find((phase) => phase.name === "runtime")!.body,
|
|
1535
1669
|
},
|
|
1670
|
+
{
|
|
1671
|
+
name: "applets",
|
|
1672
|
+
label: "Updating the Applets command",
|
|
1673
|
+
// The shim and nothing else. The provisioning phase beside it also runs
|
|
1674
|
+
// `npm install`, which is a network fetch into a dependency tree an
|
|
1675
|
+
// `applet dev` may be running out of right now; an in-place update
|
|
1676
|
+
// replaces files atomically and must not do that. A Computer picks up a
|
|
1677
|
+
// newly published SDK on its next provisioning adoption, not here.
|
|
1678
|
+
body: installDeclaredFiles(APPLETS_RUNTIME_FILES),
|
|
1679
|
+
},
|
|
1536
1680
|
{
|
|
1537
1681
|
name: "reference",
|
|
1538
1682
|
label: "Updating the Computer reference",
|
|
@@ -1687,6 +1831,7 @@ export const RUNTIME_DOCUMENT_FILES: readonly {
|
|
|
1687
1831
|
}[] = [
|
|
1688
1832
|
{ path: PROVISION_SCRIPT, content: provisionScript },
|
|
1689
1833
|
...COMPUTER_RUNTIME_FILES,
|
|
1834
|
+
...APPLETS_RUNTIME_FILES,
|
|
1690
1835
|
...REFERENCE_RUNTIME_FILES,
|
|
1691
1836
|
];
|
|
1692
1837
|
|