@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/computer-host-runtime",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -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 contains only the runtime and reference phases", () => {
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-01.1";
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