@frockbot/plugin-shell 0.3.0 → 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
@@ -251,6 +251,16 @@ export function isolateModelEventStreamV1(
251
251
  * identity. User, Bot, the resolved model binding, and the pinned Composition
252
252
  * generation complete the digest. Package id is deliberately absent: two
253
253
  * Packages of one Bot receive the same authority projection.
254
+ *
255
+ * `runId` is here because `CAPABILITIES` is one of those bindings and it is
256
+ * scoped to one Turn: the stub carries that Turn's run, session, and turn id,
257
+ * and the Bot Durable Object refuses a capability call whose scope is not the
258
+ * Turn it is currently running. The loader hands back the *cached* worker for a
259
+ * repeated id, env and all, so leaving the Turn out of the digest gave a second
260
+ * Turn on one Composition generation an isolate holding the first Turn's stub —
261
+ * every `ctx.memory`, `ctx.workspace`, and `ctx.applets` call in it answered
262
+ * "the Package is not running in this Bot's active Composition". The digest is
263
+ * a statement about what is bound; a per-Turn binding belongs in it.
254
264
  */
255
265
  export async function isolateBindingDigestV1(input: {
256
266
  userId: string;
@@ -261,6 +271,8 @@ export async function isolateBindingDigestV1(input: {
261
271
  >[];
262
272
  model?: IsolateModelBindingV1;
263
273
  compositionGenerationId: string;
274
+ /** The Turn whose `CAPABILITIES` stub this isolate's env carries. */
275
+ runId?: string;
264
276
  }): Promise<string> {
265
277
  const connections = [...input.connections]
266
278
  .map(({ connectionId, generation }) => ({ connectionId, generation }))
@@ -270,6 +282,7 @@ export async function isolateBindingDigestV1(input: {
270
282
  userId: input.userId,
271
283
  botId: input.botId,
272
284
  compositionGenerationId: input.compositionGenerationId,
285
+ runId: input.runId ?? null,
273
286
  connections,
274
287
  model: input.model ?? null,
275
288
  }),
@@ -290,17 +303,30 @@ async function sha256Hex(value: string): Promise<string> {
290
303
  * Reads a Bot Package artifact from object storage and verifies its content
291
304
  * address before a byte of it becomes code. Artifacts are immutable content,
292
305
  * not state; the hash is the only thing that makes them safe to mount.
306
+ *
307
+ * `bundled` is a second *place* to find the same immutable bytes, not a second
308
+ * kind of thing. A first-party Package that ships as an artifact-backed member
309
+ * (ADR 0022 decision 8) is built at build time and travels inside this bundle,
310
+ * so its bytes are already here and object storage never has to be seeded for
311
+ * a deploy to be correct. Object storage still wins when it holds the object,
312
+ * the digest is still verified either way, and nothing is ever *built* here —
313
+ * which is what "Composition consumes immutable content-addressed artifacts and
314
+ * never builds them" asks of this seam.
293
315
  */
294
316
  export function createR2PackageArtifactStore(
295
317
  bucket: R2Bucket,
318
+ bundled?: ReadonlyMap<string, string>,
296
319
  ): BotIsolateArtifactStore {
297
320
  return {
298
321
  async loadPackageArtifact(contentHash: string): Promise<string> {
299
- const object = await bucket.get(`packages/${contentHash}.mjs`);
300
- if (!object) {
301
- throw new Error(`package artifact "${contentHash}" is missing`);
302
- }
303
- const module = await object.text();
322
+ const key = `packages/${contentHash}.mjs`;
323
+ const object = await bucket.get(key);
324
+ const module = object
325
+ ? await object.text()
326
+ : (bundled?.get(key) ??
327
+ (() => {
328
+ throw new Error(`package artifact "${contentHash}" is missing`);
329
+ })());
304
330
  if ((await sha256Hex(module)) !== contentHash) {
305
331
  throw new Error(
306
332
  `package artifact "${contentHash}" failed hash verification`,
@@ -28,7 +28,7 @@ const UI_SIZE = 42;
28
28
  const CREATED_AT = "2026-09-02T00:00:00.000Z";
29
29
 
30
30
  const manifest = {
31
- schemaVersion: 3 as const,
31
+ schemaVersion: 5 as const,
32
32
  id: "parcel-tracking",
33
33
  displayName: "Parcel tracking",
34
34
  version: "0.0.1",
@@ -38,13 +38,18 @@ const manifest = {
38
38
  runtime: { entry: "./package.js", host: "bot-isolate" as const },
39
39
  client: {
40
40
  kind: "iframe" as const,
41
- artifact: {
42
- contentHash: UI_CONTENT_HASH,
43
- size: UI_SIZE,
44
- mediaType: "text/html" as const,
45
- bundlerVersion: "frockbot-inline-html@1",
46
- },
47
- mounts: [{ slot: "frockbot.tool-result:track_parcel" }],
41
+ pages: [
42
+ {
43
+ id: "main",
44
+ artifact: {
45
+ contentHash: UI_CONTENT_HASH,
46
+ size: UI_SIZE,
47
+ mediaType: "text/html" as const,
48
+ bundlerVersion: "frockbot-inline-html@1",
49
+ },
50
+ mounts: [{ slot: "frockbot.tool-result:track_parcel" }],
51
+ },
52
+ ],
48
53
  },
49
54
  },
50
55
  configuration: {
@@ -259,13 +259,15 @@ export function createPackageCatalogHost(
259
259
  }
260
260
  const client = entry.bundle.manifest.contributions.client;
261
261
  if (client && isClientIframeContribution(client)) {
262
- const uiArtifact = await options.catalog.headUiArtifact(
263
- client.artifact.contentHash,
264
- );
265
- if (!uiArtifact || uiArtifact.size !== client.artifact.size) {
266
- throw new Error(
267
- `Catalog iframe artifact "${client.artifact.contentHash}" is absent or has the wrong size`,
262
+ for (const page of client.pages) {
263
+ const uiArtifact = await options.catalog.headUiArtifact(
264
+ page.artifact.contentHash,
268
265
  );
266
+ if (!uiArtifact || uiArtifact.size !== page.artifact.size) {
267
+ throw new Error(
268
+ `Catalog iframe artifact "${page.artifact.contentHash}" is absent or has the wrong size`,
269
+ );
270
+ }
269
271
  }
270
272
  }
271
273
  }
@@ -8,6 +8,7 @@ import {
8
8
  initializeBotSettingsV1,
9
9
  type UserSettingsViewV1,
10
10
  } from "@frockbot/configuration-core";
11
+ import { compileFoundationApplication } from "@frockbot/application-foundation/runtime";
11
12
  import { createShellBotBackendContribution } from "./backend.js";
12
13
  import {
13
14
  botTurnCommandFingerprintV1,
@@ -85,6 +86,27 @@ class MemoryStorage {
85
86
  }
86
87
  }
87
88
 
89
+ /*
90
+ * The application as a host with no Worker Loader can mount it.
91
+ *
92
+ * This suite runs under `bun test`, where there is no `BOT_PACKAGES` binding
93
+ * and no `BotCapabilities` loopback, so an artifact-backed member — the
94
+ * Applets Package, ADR 0022 decision 8 — has nowhere to load from and the
95
+ * Composition fails verification closed, which is correct and is not what
96
+ * these tests are about. Dropping those members is the honest way to say
97
+ * "this host cannot run isolate code"; it is not a switch over any Package's
98
+ * identity, and workerd's suites mount the real thing.
99
+ */
100
+ async function compileWithoutIsolateMembers(): ReturnType<
101
+ typeof compileFoundationApplication
102
+ > {
103
+ const application = await compileFoundationApplication();
104
+ return {
105
+ ...application,
106
+ packages: application.packages.filter((pkg) => pkg.artifact === undefined),
107
+ };
108
+ }
109
+
88
110
  describe("Bot recovery", () => {
89
111
  test("executes and reconstructs an Ollama-bound Bot without Foundation fallback", async () => {
90
112
  const storage = new MemoryStorage();
@@ -216,6 +238,7 @@ describe("Bot recovery", () => {
216
238
  typeof createShellBotBackendContribution
217
239
  >[0]["env"],
218
240
  outboundFetch,
241
+ compileApplication: compileWithoutIsolateMembers,
219
242
  });
220
243
 
221
244
  const configured = host();