@frockbot/plugin-shell 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.
Files changed (33) hide show
  1. package/package.json +32 -29
  2. package/src/backend-applets.test.ts +581 -0
  3. package/src/backend-applets.ts +959 -0
  4. package/src/backend-authoring.test.ts +61 -19
  5. package/src/backend-authoring.ts +52 -26
  6. package/src/backend-composition.ts +64 -0
  7. package/src/backend-computer.test.ts +128 -0
  8. package/src/backend-computer.ts +81 -0
  9. package/src/backend-configuration.test.ts +8 -1
  10. package/src/backend-iframe-ui.test.ts +29 -12
  11. package/src/backend-isolate.ts +31 -5
  12. package/src/backend-package-catalog.test.ts +13 -8
  13. package/src/backend-package-catalog.ts +8 -6
  14. package/src/backend-recovery-integration.test.ts +23 -0
  15. package/src/backend.ts +508 -5
  16. package/src/client/AppletCanvas.vue +679 -0
  17. package/src/client/FrockBotApp.vue +169 -15
  18. package/src/client/PackageEntryTrigger.vue +77 -0
  19. package/src/client/PackageIframeHost.vue +148 -47
  20. package/src/client/PackageIframeSettings.vue +8 -6
  21. package/src/client/PackageSurfacePage.vue +39 -0
  22. package/src/client/applets-client.test.ts +204 -0
  23. package/src/client/applets-client.ts +139 -0
  24. package/src/client/applets-state.ts +64 -0
  25. package/src/client/index.test.ts +13 -7
  26. package/src/client/index.ts +308 -1
  27. package/src/client/package-iframe-entries.test.ts +122 -0
  28. package/src/client/package-iframe-entries.ts +112 -0
  29. package/src/client/package-iframe-host-message.test.ts +3 -3
  30. package/src/client/package-iframe-host-message.ts +3 -3
  31. package/src/client/styles.css +107 -1
  32. package/src/composition-views.ts +31 -6
  33. package/src/shared.ts +52 -0
@@ -354,18 +354,27 @@ describe("a Bot authoring a Package", () => {
354
354
  });
355
355
  });
356
356
 
357
- test("stores the second immutable artifact and records it in the same manifest", async () => {
357
+ test("stores every immutable page artifact and records them in the same manifest", async () => {
358
358
  const html = "<!doctype html><h1>Hello</h1>";
359
+ const boardHtml = "<!doctype html><h1>Board</h1>";
359
360
  const contentHash = await sha256HexV1(html);
361
+ const boardHash = await sha256HexV1(boardHtml);
362
+ const uiArtifact = (hash: string, text: string) => ({
363
+ contentHash: hash,
364
+ size: new TextEncoder().encode(text).byteLength,
365
+ mediaType: "text/html" as const,
366
+ bundlerVersion: "frockbot-inline-html@1",
367
+ });
360
368
  const bundler = countingBundler((effectId) => ({
361
369
  ...bundledResult(effectId),
362
- uiArtifact: {
363
- contentHash,
364
- size: new TextEncoder().encode(html).byteLength,
365
- mediaType: "text/html",
366
- bundlerVersion: "frockbot-inline-html@1",
367
- },
368
- uiHtml: html,
370
+ uiArtifacts: [
371
+ { id: "main", artifact: uiArtifact(contentHash, html), html },
372
+ {
373
+ id: "board",
374
+ artifact: uiArtifact(boardHash, boardHtml),
375
+ html: boardHtml,
376
+ },
377
+ ],
369
378
  }));
370
379
  const request = requestFor();
371
380
  const outcome = await host({ bundler }).author({
@@ -374,13 +383,32 @@ describe("a Bot authoring a Package", () => {
374
383
  ...request.input,
375
384
  hooks: ["agent/tool-exposure", "tools/post-execute"],
376
385
  ui: {
377
- html,
378
- mounts: [{ slot: "frockbot.tool-result:hello" }],
386
+ pages: [
387
+ {
388
+ id: "main",
389
+ html,
390
+ mounts: [{ slot: "frockbot.tool-result:hello" }],
391
+ },
392
+ {
393
+ id: "board",
394
+ html: boardHtml,
395
+ mounts: [{ slot: "frockbot.surface:board" }],
396
+ },
397
+ ],
398
+ entries: [
399
+ {
400
+ id: "open",
401
+ slot: "frockbot.sidebar-actions",
402
+ label: "Board",
403
+ icon: "sparkle",
404
+ opens: { kind: "surface", page: "board" },
405
+ },
406
+ ],
379
407
  },
380
408
  },
381
409
  });
382
410
  expect(outcome.status).toBe("authored");
383
- expect(writtenUi).toEqual([contentHash]);
411
+ expect(writtenUi).toEqual([contentHash, boardHash]);
384
412
  const artifact = storage.values.get(
385
413
  artifactKey("b".repeat(64)),
386
414
  ) as AuthoredArtifactRecordV1;
@@ -390,13 +418,27 @@ describe("a Bot authoring a Package", () => {
390
418
  const manifest = decodeFrockBotManifest(stored.manifest);
391
419
  expect(manifest.contributions.client).toEqual({
392
420
  kind: "iframe",
393
- artifact: {
394
- contentHash,
395
- size: new TextEncoder().encode(html).byteLength,
396
- mediaType: "text/html",
397
- bundlerVersion: "frockbot-inline-html@1",
398
- },
399
- mounts: [{ slot: "frockbot.tool-result:hello" }],
421
+ pages: [
422
+ {
423
+ id: "main",
424
+ artifact: uiArtifact(contentHash, html),
425
+ mounts: [{ slot: "frockbot.tool-result:hello" }],
426
+ },
427
+ {
428
+ id: "board",
429
+ artifact: uiArtifact(boardHash, boardHtml),
430
+ mounts: [{ slot: "frockbot.surface:board" }],
431
+ },
432
+ ],
433
+ entries: [
434
+ {
435
+ id: "open",
436
+ slot: "frockbot.sidebar-actions",
437
+ label: "Board",
438
+ icon: "sparkle",
439
+ opens: { kind: "surface", page: "board" },
440
+ },
441
+ ],
400
442
  });
401
443
  expect(manifest.hooks).toEqual([
402
444
  "agent/tool-exposure",
@@ -853,7 +895,7 @@ describe("a Bot authoring a Package", () => {
853
895
  expect(view.contextContract).toContain(
854
896
  "interface BotPackageExecutionContextV1",
855
897
  );
856
- expect(view.contextContract).toContain("interface FrockBotIframeBridgeV1");
898
+ expect(view.contextContract).toContain("interface FrockBotIframeBridgeV2");
857
899
  expect(view.contextContract).toContain("window.frockbot=");
858
900
  expect(view.composition.members).toContainEqual(
859
901
  expect.objectContaining({
@@ -437,9 +437,9 @@ export function createPackageAuthoringHost(
437
437
  runId: options.runId,
438
438
  packageId: input.packageId,
439
439
  sourceHash: input.sourceHash,
440
- ...(input.uiHtmlHash === undefined
440
+ ...(input.uiPageHashes === undefined
441
441
  ? {}
442
- : { uiHtmlHash: input.uiHtmlHash }),
442
+ : { uiPageHashes: input.uiPageHashes }),
443
443
  ...(input.hooks === undefined ? {} : { hooks: input.hooks }),
444
444
  }),
445
445
 
@@ -560,13 +560,20 @@ export function createPackageAuthoringHost(
560
560
  );
561
561
  const ordinal = (previous?.ordinal ?? 0) + 1;
562
562
  const version = authoredVersionV1(ordinal);
563
- const uiArtifact = request.input.ui
564
- ? {
565
- contentHash: await sha256(request.input.ui.html),
566
- size: new TextEncoder().encode(request.input.ui.html).byteLength,
567
- mediaType: "text/html" as const,
568
- bundlerVersion: PACKAGE_UI_ARTIFACT_VERSION,
569
- }
563
+ const uiPages = request.input.ui
564
+ ? await Promise.all(
565
+ request.input.ui.pages.map(async (page) => ({
566
+ id: page.id,
567
+ html: page.html,
568
+ mounts: page.mounts,
569
+ artifact: {
570
+ contentHash: await sha256(page.html),
571
+ size: new TextEncoder().encode(page.html).byteLength,
572
+ mediaType: "text/html" as const,
573
+ bundlerVersion: PACKAGE_UI_ARTIFACT_VERSION,
574
+ },
575
+ })),
576
+ )
570
577
  : undefined;
571
578
  const rawManifest = authoredManifestV1({
572
579
  packageId,
@@ -574,11 +581,17 @@ export function createPackageAuthoringHost(
574
581
  version,
575
582
  tools: request.input.tools,
576
583
  hooks: request.input.hooks,
577
- ...(request.input.ui && uiArtifact
584
+ ...(request.input.ui && uiPages
578
585
  ? {
579
586
  ui: {
580
- artifact: uiArtifact,
581
- mounts: request.input.ui.mounts,
587
+ pages: uiPages.map((page) => ({
588
+ id: page.id,
589
+ artifact: page.artifact,
590
+ mounts: page.mounts,
591
+ })),
592
+ ...(request.input.ui.entries
593
+ ? { entries: request.input.ui.entries }
594
+ : {}),
582
595
  },
583
596
  }
584
597
  : {}),
@@ -653,8 +666,13 @@ export function createPackageAuthoringHost(
653
666
  compatibilityDate: options.compatibilityDate,
654
667
  entry: "package.ts",
655
668
  sources: [{ path: "package.ts", text: request.input.source }],
656
- ...(request.input.ui
657
- ? { ui: { path: "ui.html" as const, html: request.input.ui.html } }
669
+ ...(uiPages
670
+ ? {
671
+ uiPages: uiPages.map((page) => ({
672
+ id: page.id,
673
+ html: page.html,
674
+ })),
675
+ }
658
676
  : {}),
659
677
  };
660
678
  let bundled;
@@ -696,15 +714,23 @@ export function createPackageAuthoringHost(
696
714
  }
697
715
 
698
716
  const { artifact } = bundled;
699
- if (
700
- uiArtifact &&
701
- (!bundled.uiArtifact ||
702
- bundled.uiHtml !== request.input.ui?.html ||
703
- bundled.uiArtifact.contentHash !== uiArtifact.contentHash ||
704
- bundled.uiArtifact.size !== uiArtifact.size ||
705
- bundled.uiArtifact.mediaType !== uiArtifact.mediaType ||
706
- bundled.uiArtifact.bundlerVersion !== uiArtifact.bundlerVersion)
707
- ) {
717
+ const returnedPages = bundled.uiArtifacts ?? [];
718
+ const uiMismatch =
719
+ uiPages !== undefined &&
720
+ (returnedPages.length !== uiPages.length ||
721
+ uiPages.some((page, index) => {
722
+ const returned = returnedPages[index];
723
+ return (
724
+ !returned ||
725
+ returned.id !== page.id ||
726
+ returned.html !== page.html ||
727
+ returned.artifact.contentHash !== page.artifact.contentHash ||
728
+ returned.artifact.size !== page.artifact.size ||
729
+ returned.artifact.mediaType !== page.artifact.mediaType ||
730
+ returned.artifact.bundlerVersion !== page.artifact.bundlerVersion
731
+ );
732
+ }));
733
+ if (uiMismatch || (uiPages === undefined && returnedPages.length > 0)) {
708
734
  return refused(
709
735
  await recordFailure({
710
736
  effectId,
@@ -721,10 +747,10 @@ export function createPackageAuthoringHost(
721
747
  artifact.contentHash,
722
748
  bundled.module,
723
749
  );
724
- if (bundled.uiArtifact && bundled.uiHtml) {
750
+ for (const page of returnedPages) {
725
751
  await options.artifacts.putPackageUiArtifact!(
726
- bundled.uiArtifact.contentHash,
727
- bundled.uiHtml,
752
+ page.artifact.contentHash,
753
+ page.html,
728
754
  );
729
755
  }
730
756
  await options.artifacts.putPackageSource(
@@ -49,6 +49,9 @@ export function bootstrapCompositionGeneration(
49
49
  specifier: pkg.specifier,
50
50
  version: pkg.version,
51
51
  manifest: pkg.manifest,
52
+ // A first-party member the application declared an artifact for loads
53
+ // through the isolate host, not the application's Contribution table.
54
+ ...(pkg.artifact ? { artifact: pkg.artifact } : {}),
52
55
  })),
53
56
  { createdAt },
54
57
  );
@@ -84,6 +87,23 @@ export interface ShellIsolateMountOptions {
84
87
  deadlineMs?: number;
85
88
  }
86
89
 
90
+ /**
91
+ * How an Applet member's tools reach their instance.
92
+ *
93
+ * The Applet Durable Object forwards to the facet; a facet stub is not
94
+ * serializable and never leaves that object, so this is a call, never a stub
95
+ * (`docs/research/spike-applet-facets.md` §8). Absent when the Bot Durable
96
+ * Object has no Applet binding: an Applet member's tools are then simply not
97
+ * registered, exactly as an isolate member fails without a loader.
98
+ */
99
+ export interface ShellAppletMountOptions {
100
+ invokeTool(request: {
101
+ appletId: string;
102
+ tool: string;
103
+ input: unknown;
104
+ }): Promise<{ status: "ok" | "error"; content: string }>;
105
+ }
106
+
87
107
  export interface ShellCompositionMountOptions {
88
108
  botId: string;
89
109
  sessionId: string;
@@ -110,6 +130,8 @@ export interface ShellCompositionMountOptions {
110
130
  subagentRole?: string;
111
131
  /** Absent when the host cannot load isolates; isolate members then fail verify. */
112
132
  isolate?: ShellIsolateMountOptions;
133
+ /** Absent when the host cannot reach Applet instances. */
134
+ applets?: ShellAppletMountOptions;
113
135
  }
114
136
 
115
137
  export interface ShellCompositionHost extends CompositionHost {
@@ -226,7 +248,49 @@ export function createShellCompositionHost(
226
248
  }
227
249
  }
228
250
 
251
+ // Applet members. Their tools are ordinary tools in this Bot's catalog
252
+ // (ADR 0022 decision 4), pinned to this generation like every other
253
+ // member, and routed to the Applet Durable Object. An Applet contributes
254
+ // no module and no manifest, so there is nothing here to mount, load, or
255
+ // health-check: the instance's own health check ran when its generation
256
+ // was published, and its failure is recorded there.
257
+ const unregisterApplets: (() => void)[] = [];
258
+ for (const applet of generation.applets ?? []) {
259
+ if (!options.applets) {
260
+ failures.push({
261
+ phase: "resolve",
262
+ message: `Applet "${applet.appletId}" needs an Applet binding and this host has none`,
263
+ });
264
+ continue;
265
+ }
266
+ const routing = options.applets;
267
+ for (const tool of applet.tools) {
268
+ unregisterApplets.push(
269
+ runtime.root.tools.register({
270
+ name: tool.name,
271
+ // Provenance travels into the catalog the model reads, so a Bot
272
+ // can tell an Applet's tool from a Package's.
273
+ description: `${tool.description} (Applet "${applet.appletId}", generation ${applet.generationId})`,
274
+ inputSchema: tool.inputSchema,
275
+ idempotent: false,
276
+ execute: async (input) => {
277
+ const outcome = await routing.invokeTool({
278
+ appletId: applet.appletId,
279
+ tool: tool.name,
280
+ input: input ?? null,
281
+ });
282
+ return {
283
+ content: outcome.content,
284
+ isError: outcome.status === "error",
285
+ };
286
+ },
287
+ }),
288
+ );
289
+ }
290
+ }
291
+
229
292
  const dispose = async () => {
293
+ for (const unregister of unregisterApplets.toReversed()) unregister();
230
294
  for (const contribution of active.toReversed()) {
231
295
  await contribution.dispose();
232
296
  }
@@ -0,0 +1,128 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { FrockBotManifest } from "@frockbot/kernel-composition";
3
+ import {
4
+ createBotComputerSyncHost,
5
+ declaredPackageRootsV1,
6
+ } from "./backend-computer.ts";
7
+
8
+ function pkg(
9
+ id: string,
10
+ version: string,
11
+ roots?: FrockBotManifest["roots"],
12
+ ): { id: string; version: string; manifest: FrockBotManifest } {
13
+ return {
14
+ id,
15
+ version,
16
+ manifest: {
17
+ schemaVersion: 4,
18
+ id,
19
+ displayName: id,
20
+ version,
21
+ compatibility: { frockbot: "*" },
22
+ dependencies: {},
23
+ contributions: {},
24
+ permissions: [],
25
+ ...(roots ? { roots } : {}),
26
+ },
27
+ };
28
+ }
29
+
30
+ const packages = [
31
+ pkg("image", "0.0.1", [{ id: "generated", scope: "user" }]),
32
+ pkg("applets", "0.0.1", [{ id: "source", scope: "user" }]),
33
+ pkg("clock", "0.0.1"),
34
+ ];
35
+
36
+ describe("the durable roots a User's Packages declare", () => {
37
+ test("supplies every enabled Package's declared roots, sorted", () => {
38
+ expect(
39
+ declaredPackageRootsV1({
40
+ installations: [
41
+ { packageId: "applets", version: "0.0.1", state: "installed" },
42
+ { packageId: "image", version: "0.0.1", state: "installed" },
43
+ { packageId: "clock", version: "0.0.1", state: "installed" },
44
+ ],
45
+ packages,
46
+ }),
47
+ ).toEqual([
48
+ { packageId: "applets", rootId: "source" },
49
+ { packageId: "image", rootId: "generated" },
50
+ ]);
51
+ });
52
+
53
+ test("a Package that declares no root contributes none", () => {
54
+ expect(
55
+ declaredPackageRootsV1({
56
+ installations: [
57
+ { packageId: "clock", version: "0.0.1", state: "installed" },
58
+ ],
59
+ packages,
60
+ }),
61
+ ).toEqual([]);
62
+ });
63
+
64
+ test("enablement decides: a disabled or failed install syncs nothing", () => {
65
+ // Materializing files for a Package that cannot run would leave
66
+ // directories on a Computer that no Bot on it could explain, and would
67
+ // keep syncing them after an uninstall.
68
+ for (const state of ["disabled", "failed"] as const) {
69
+ expect(
70
+ declaredPackageRootsV1({
71
+ installations: [{ packageId: "image", version: "0.0.1", state }],
72
+ packages,
73
+ }),
74
+ ).toEqual([]);
75
+ }
76
+ });
77
+
78
+ test("the installed version decides which manifest declares the roots", () => {
79
+ expect(
80
+ declaredPackageRootsV1({
81
+ installations: [
82
+ { packageId: "image", version: "0.0.2", state: "installed" },
83
+ ],
84
+ packages,
85
+ }),
86
+ ).toEqual([]);
87
+ });
88
+
89
+ test("one entry per root, whatever the installations say", () => {
90
+ expect(
91
+ declaredPackageRootsV1({
92
+ installations: [
93
+ { packageId: "image", version: "0.0.1", state: "installed" },
94
+ { packageId: "image", version: "0.0.1", state: "installed" },
95
+ ],
96
+ packages,
97
+ }),
98
+ ).toEqual([{ packageId: "image", rootId: "generated" }]);
99
+ });
100
+ });
101
+
102
+ describe("the Computer sync seam", () => {
103
+ const store = {} as never;
104
+
105
+ test("carries the declared roots to the provider", () => {
106
+ const host = createBotComputerSyncHost({ WORKSPACE_SYNC_FILES: store }, [
107
+ { packageId: "applets", rootId: "source" },
108
+ ]);
109
+ expect(host?.packageRoots).toEqual([
110
+ { packageId: "applets", rootId: "source" },
111
+ ]);
112
+ });
113
+
114
+ test("omits the field entirely when no Package declared a root", () => {
115
+ // Absent rather than empty: a provider then behaves exactly as it did
116
+ // before any host supplied a list.
117
+ const host = createBotComputerSyncHost({ WORKSPACE_SYNC_FILES: store });
118
+ expect(host && "packageRoots" in host).toBe(false);
119
+ });
120
+
121
+ test("no store binding is no sync at all, roots or not", () => {
122
+ expect(
123
+ createBotComputerSyncHost({}, [
124
+ { packageId: "applets", rootId: "source" },
125
+ ]),
126
+ ).toBeUndefined();
127
+ });
128
+ });
@@ -33,6 +33,81 @@ import type {
33
33
  WorkspaceSyncEffectsV1,
34
34
  } from "@frockbot/kernel-contracts";
35
35
  import type { ComputerSyncHostV1 } from "@frockbot/computer-core";
36
+ import type { FrockBotManifest } from "@frockbot/kernel-composition";
37
+
38
+ /**
39
+ * One installed Package, as the durable-root supplier needs to see it: what
40
+ * the User installed, and what that exact version's manifest declares.
41
+ */
42
+ export interface DeclaredPackageRootSourceV1 {
43
+ /** The User's installations, from the User configuration view. */
44
+ installations: readonly {
45
+ packageId: string;
46
+ version: string;
47
+ state: "installed" | "disabled" | "failed";
48
+ }[];
49
+ /** The application's compiled Packages, each with the manifest it shipped. */
50
+ packages: readonly {
51
+ id: string;
52
+ version: string;
53
+ manifest: FrockBotManifest;
54
+ }[];
55
+ }
56
+
57
+ /**
58
+ * The `package-declared` durable roots this User's *enabled* Packages declare.
59
+ *
60
+ * "Durable roots, declared by the Computer Package's Workspace layout and by
61
+ * Package manifests." Until this existed the second half of that sentence had
62
+ * no supplier: `declaredWorkspaceRootsV1` took a `packageRoots` argument that
63
+ * nothing in production passed, so `image/generated` — a root the Image
64
+ * Package has written since it shipped — never reached the durable-root sync
65
+ * and never appeared on a Computer. This is the missing half, and
66
+ * `applets/source` (ADR 0022) is the reason it could no longer be missing.
67
+ *
68
+ * ENABLEMENT decides membership, by the same `state === "installed"` test
69
+ * `resolveBotExecutionPlanV1` applies to Capabilities and against the same
70
+ * exact version the application compiled. A root of a Package the User
71
+ * disabled or never installed is not synchronized: materializing files for a
72
+ * Package that cannot run would grow directories on a Computer that no Bot on
73
+ * it could explain, and would keep syncing them after an uninstall.
74
+ *
75
+ * SCOPE is the User's, always. A `package-declared` root names no Bot, so this
76
+ * takes no Bot and answers the same for every tenant on one Computer.
77
+ */
78
+ export function declaredPackageRootsV1(
79
+ source: DeclaredPackageRootSourceV1,
80
+ ): { packageId: string; rootId: string }[] {
81
+ const roots: { packageId: string; rootId: string }[] = [];
82
+ for (const installation of source.installations) {
83
+ if (installation.state !== "installed") continue;
84
+ const declared = source.packages.find(
85
+ (candidate) =>
86
+ candidate.id === installation.packageId &&
87
+ candidate.version === installation.version,
88
+ );
89
+ for (const root of declared?.manifest.roots ?? []) {
90
+ const entry = { packageId: installation.packageId, rootId: root.id };
91
+ // One entry per root: the sync walks this list, and a duplicate would
92
+ // reconcile the same root twice in a single pass.
93
+ if (
94
+ !roots.some(
95
+ (candidate) =>
96
+ candidate.packageId === entry.packageId &&
97
+ candidate.rootId === entry.rootId,
98
+ )
99
+ ) {
100
+ roots.push(entry);
101
+ }
102
+ }
103
+ }
104
+ roots.sort(
105
+ (left, right) =>
106
+ left.packageId.localeCompare(right.packageId) ||
107
+ left.rootId.localeCompare(right.rootId),
108
+ );
109
+ return roots;
110
+ }
36
111
 
37
112
  /**
38
113
  * The narrow slice of the Durable Object environment this module reads. Named
@@ -55,9 +130,14 @@ export interface BotComputerSyncEnv {
55
130
  * above. What a sync finds on the Computer is attributed to nobody, so knowing
56
131
  * which Bot, Session, and Turn asked for the sync would only be an invitation
57
132
  * to record a writer this seam cannot support.
133
+ *
134
+ * `packageRoots` is the one thing it does take beyond the bindings, because a
135
+ * Package's declared roots are a fact about the *User's* installation rather
136
+ * than about the Turn: see {@link declaredPackageRootsV1}.
58
137
  */
59
138
  export function createBotComputerSyncHost(
60
139
  env: object,
140
+ packageRoots: readonly { packageId: string; rootId: string }[] = [],
61
141
  ): ComputerSyncHostV1 | undefined {
62
142
  // SAFETY: these surfaces are constructed onto the Durable Object environment
63
143
  // rather than declared in the generated `Env`; they are not Worker bindings.
@@ -72,5 +152,6 @@ export function createBotComputerSyncHost(
72
152
  ...(bound.WORKSPACE_SYNC_GENERATIONS
73
153
  ? { generations: bound.WORKSPACE_SYNC_GENERATIONS }
74
154
  : {}),
155
+ ...(packageRoots.length > 0 ? { packageRoots } : {}),
75
156
  };
76
157
  }
@@ -88,8 +88,15 @@ async function compileModelTestApplication(): ReturnType<
88
88
  return {
89
89
  ...application,
90
90
  packages: [
91
+ // Artifact-backed members are dropped with them: `bun test` gives this
92
+ // host no Worker Loader, so an isolate member has nowhere to load from
93
+ // and the Composition would fail verification closed — correct, and not
94
+ // what this suite is about. workerd's suites mount the real thing.
91
95
  ...application.packages.filter(
92
- (pkg) => pkg.id !== provider.id && pkg.id !== "custom-models",
96
+ (pkg) =>
97
+ pkg.artifact === undefined &&
98
+ pkg.id !== provider.id &&
99
+ pkg.id !== "custom-models",
93
100
  ),
94
101
  provider,
95
102
  {
@@ -15,13 +15,19 @@ describe("Package iframe server admission", () => {
15
15
  packageId: "weather-page",
16
16
  displayName: "Weather page",
17
17
  provenance: "Bot-authored",
18
- artifact: {
19
- contentHash: "a".repeat(64),
20
- size: 123,
21
- mediaType: "text/html",
22
- bundlerVersion: "frockbot-inline-html@1",
23
- },
24
- mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
18
+ pages: [
19
+ {
20
+ id: "main",
21
+ artifact: {
22
+ contentHash: "a".repeat(64),
23
+ size: 123,
24
+ mediaType: "text/html",
25
+ bundlerVersion: "frockbot-inline-html@1",
26
+ },
27
+ mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
28
+ },
29
+ ],
30
+ entries: [],
25
31
  declaredTools: ["weather_lookup"],
26
32
  },
27
33
  ],
@@ -56,7 +62,7 @@ describe("Package iframe server admission", () => {
56
62
  bundlerVersion: "frockbot-inline-html@1",
57
63
  };
58
64
  const manifest: FrockBotManifest = {
59
- schemaVersion: 3,
65
+ schemaVersion: 5,
60
66
  id: "weather-page",
61
67
  displayName: "Weather page",
62
68
  version: "0.0.1",
@@ -66,8 +72,13 @@ describe("Package iframe server admission", () => {
66
72
  runtime: { entry: "./package.js", host: "bot-isolate" },
67
73
  client: {
68
74
  kind: "iframe",
69
- artifact: uiArtifact,
70
- mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
75
+ pages: [
76
+ {
77
+ id: "main",
78
+ artifact: uiArtifact,
79
+ mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
80
+ },
81
+ ],
71
82
  },
72
83
  },
73
84
  tools: [
@@ -122,8 +133,14 @@ describe("Package iframe server admission", () => {
122
133
  packageId: "weather-page",
123
134
  displayName: "Weather page",
124
135
  provenance: "User-installed",
125
- artifact: uiArtifact,
126
- mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
136
+ pages: [
137
+ {
138
+ id: "main",
139
+ artifact: uiArtifact,
140
+ mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
141
+ },
142
+ ],
143
+ entries: [],
127
144
  declaredTools: ["weather_lookup"],
128
145
  },
129
146
  ]);