@frockbot/plugin-shell 0.3.25 → 0.3.26

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/plugin-shell",
3
- "version": "0.3.25",
3
+ "version": "0.3.26",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -52,42 +52,42 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@cordisjs/plugin-webui": "0.8.2",
55
- "@frockbot/agent-runtime": "0.3.25",
56
- "@frockbot/application-foundation": "0.3.25",
57
- "@frockbot/catalog-core": "0.3.25",
58
- "@frockbot/client-core": "0.3.25",
59
- "@frockbot/client-ui": "0.3.25",
60
- "@frockbot/computer-core": "0.3.25",
61
- "@frockbot/computer-host-protocol": "0.3.25",
62
- "@frockbot/configuration-core": "0.3.25",
63
- "@frockbot/connection-core": "0.3.25",
64
- "@frockbot/kernel-agent-loop": "0.3.25",
65
- "@frockbot/kernel-composition": "0.3.25",
66
- "@frockbot/kernel-contracts": "0.3.25",
67
- "@frockbot/kernel-do": "0.3.25",
68
- "@frockbot/machine-protocol": "0.3.25",
69
- "@frockbot/plugin-applets": "0.3.25",
70
- "@frockbot/plugin-authoring": "0.3.25",
71
- "@frockbot/plugin-bot-template": "0.3.25",
72
- "@frockbot/plugin-computer": "0.3.25",
73
- "@frockbot/plugin-flock": "0.3.25",
74
- "@frockbot/plugin-image": "0.3.25",
75
- "@frockbot/plugin-machine-messages": "0.3.25",
76
- "@frockbot/plugin-mcp": "0.3.25",
77
- "@frockbot/plugin-memory": "0.3.25",
78
- "@frockbot/plugin-package-catalog": "0.3.25",
79
- "@frockbot/plugin-routines": "0.3.25",
80
- "@frockbot/plugin-skills": "0.3.25",
81
- "@frockbot/plugin-subagents": "0.3.25",
82
- "@frockbot/plugin-user-machine": "0.3.25",
83
- "@frockbot/protocol": "0.3.25",
55
+ "@frockbot/agent-runtime": "0.3.26",
56
+ "@frockbot/application-foundation": "0.3.26",
57
+ "@frockbot/catalog-core": "0.3.26",
58
+ "@frockbot/client-core": "0.3.26",
59
+ "@frockbot/client-ui": "0.3.26",
60
+ "@frockbot/computer-core": "0.3.26",
61
+ "@frockbot/computer-host-protocol": "0.3.26",
62
+ "@frockbot/configuration-core": "0.3.26",
63
+ "@frockbot/connection-core": "0.3.26",
64
+ "@frockbot/kernel-agent-loop": "0.3.26",
65
+ "@frockbot/kernel-composition": "0.3.26",
66
+ "@frockbot/kernel-contracts": "0.3.26",
67
+ "@frockbot/kernel-do": "0.3.26",
68
+ "@frockbot/machine-protocol": "0.3.26",
69
+ "@frockbot/plugin-applets": "0.3.26",
70
+ "@frockbot/plugin-authoring": "0.3.26",
71
+ "@frockbot/plugin-bot-template": "0.3.26",
72
+ "@frockbot/plugin-computer": "0.3.26",
73
+ "@frockbot/plugin-flock": "0.3.26",
74
+ "@frockbot/plugin-image": "0.3.26",
75
+ "@frockbot/plugin-machine-messages": "0.3.26",
76
+ "@frockbot/plugin-mcp": "0.3.26",
77
+ "@frockbot/plugin-memory": "0.3.26",
78
+ "@frockbot/plugin-package-catalog": "0.3.26",
79
+ "@frockbot/plugin-routines": "0.3.26",
80
+ "@frockbot/plugin-skills": "0.3.26",
81
+ "@frockbot/plugin-subagents": "0.3.26",
82
+ "@frockbot/plugin-user-machine": "0.3.26",
83
+ "@frockbot/protocol": "0.3.26",
84
84
  "cordis": "4.0.0-rc.8",
85
85
  "vue": "3.5.41"
86
86
  },
87
87
  "devDependencies": {
88
88
  "@cloudflare/workers-types": "latest",
89
- "@frockbot/plugin-prompt": "0.3.25",
90
- "@frockbot/plugin-tools": "0.3.25",
89
+ "@frockbot/plugin-prompt": "0.3.26",
90
+ "@frockbot/plugin-tools": "0.3.26",
91
91
  "@types/bun": "1.3.6",
92
92
  "@types/node": "26.2.0",
93
93
  "@vitejs/plugin-vue": "6.0.8",
@@ -148,8 +148,13 @@ export interface AppletInstanceBindingV1 {
148
148
  diagnostics: string[];
149
149
  }
150
150
  >;
151
+ /**
152
+ * `generationId` is the Applet generation the calling Turn pinned. The
153
+ * instance runs that generation or refuses the call (ADR 0038).
154
+ */
151
155
  invokeTool(input: {
152
156
  appletId: string;
157
+ generationId: string;
153
158
  tool: string;
154
159
  input: unknown;
155
160
  }): Promise<{ status: "ok" | "error"; content: string }>;
@@ -249,7 +254,11 @@ export function createAppletInstanceBindingV1(
249
254
  const answer = JSON.parse(
250
255
  JSON.stringify(
251
256
  await rpc.invokeTool(
252
- envelope({ tool: input.tool, toolInput: input.input ?? null }),
257
+ envelope({
258
+ generationId: input.generationId,
259
+ tool: input.tool,
260
+ toolInput: input.input ?? null,
261
+ }),
253
262
  ),
254
263
  ),
255
264
  ) as { status?: unknown; content?: unknown };
@@ -1,13 +1,26 @@
1
+ // Two things this seam owns, tested together because they share a Composition.
2
+ //
3
+ // First: which built-in members a Bot follows across a deployment.
4
+ //
5
+ // Second, the Applet half. An Applet member contributes no module and nothing
6
+ // to health-check, so the only thing this seam does with it is register its
7
+ // tools. What matters is *which* generation those tools reach: the description
8
+ // handed to the model names the pinned generation, and ADR 0038 requires the
9
+ // call to name it too, so the Applet Durable Object executes that generation or
10
+ // refuses. A DTO that carried only the Applet id let a publish landing mid-Turn
11
+ // run new code behind the schema and provenance the model was shown.
1
12
  import { describe, expect, test } from "bun:test";
2
13
  import type { ApplicationPlan } from "@frockbot/kernel-composition/compiler";
3
14
  import {
4
15
  compositionArtifactSetHashV1,
5
16
  decodeCompositionGenerationV1,
17
+ type CompositionAppletMemberV1,
6
18
  type CompositionGenerationV1,
7
19
  type CompositionMemberV1,
8
20
  } from "@frockbot/kernel-composition/generation";
9
21
  import {
10
22
  bootstrapCompositionGeneration,
23
+ createShellCompositionHost,
11
24
  resolveDeploymentCompositionV1,
12
25
  DEPLOYMENT_FOLLOW_SUMMARY_V1,
13
26
  } from "./backend-composition.js";
@@ -219,3 +232,114 @@ describe("a Bot's built-in members follow the deployment", () => {
219
232
  ).toBeUndefined();
220
233
  });
221
234
  });
235
+
236
+ const USER = "user-42";
237
+ const APPLET = `${USER}.${"a".repeat(32)}`;
238
+
239
+ const APPLET_MEMBER: CompositionAppletMemberV1 = {
240
+ kind: "applet",
241
+ appletId: APPLET,
242
+ generationId: "2026-09-05T00:00:00.000Z:A",
243
+ tools: [
244
+ {
245
+ name: "add_todo",
246
+ description: "Add a todo",
247
+ inputSchema: {
248
+ type: "object",
249
+ properties: { title: { type: "string" } },
250
+ },
251
+ },
252
+ ],
253
+ provenance: {
254
+ kind: "user",
255
+ packageId: APPLET,
256
+ version: "2026-09-05T00:00:00.000Z:A",
257
+ userId: USER,
258
+ authoredAt: "2026-09-05T00:00:00.000Z",
259
+ },
260
+ };
261
+
262
+ async function generationWithApplet(): Promise<CompositionGenerationV1> {
263
+ const members = [
264
+ {
265
+ packageId: "shell",
266
+ specifier: "@frockbot/plugin-shell",
267
+ version: "1.0.0",
268
+ manifestHash: "c".repeat(64),
269
+ provenance: {
270
+ kind: "first-party" as const,
271
+ packageId: "shell",
272
+ version: "1.0.0",
273
+ },
274
+ },
275
+ ];
276
+ const applets = [APPLET_MEMBER];
277
+ const artifactSetHash = await compositionArtifactSetHashV1(members, applets);
278
+ return decodeCompositionGenerationV1({
279
+ schemaVersion: 1,
280
+ generationId: `2026-09-05T00:00:00.000Z:${artifactSetHash.slice(0, 16)}`,
281
+ artifactSetHash,
282
+ createdAt: "2026-09-05T00:00:00.000Z",
283
+ origin: { kind: "bootstrap" },
284
+ members,
285
+ applets,
286
+ status: "active",
287
+ });
288
+ }
289
+
290
+ describe("Applet tools mounted into a Turn's Composition", () => {
291
+ test("the call carries the generation the Turn pinned, not just the Applet id", async () => {
292
+ const calls: Array<{ appletId: string; generationId: string }> = [];
293
+ const generation = await generationWithApplet();
294
+ const { signal } = new AbortController();
295
+ const mounted = await createShellCompositionHost({
296
+ botId: "bot-1",
297
+ sessionId: `${USER}:bot-1`,
298
+ sessionEvents: [],
299
+ admitEffect: () => Promise.resolve(true),
300
+ applets: {
301
+ invokeTool: (request) => {
302
+ calls.push({
303
+ appletId: request.appletId,
304
+ generationId: request.generationId,
305
+ });
306
+ return Promise.resolve({ status: "ok" as const, content: "added" });
307
+ },
308
+ },
309
+ }).mount(generation, signal);
310
+ try {
311
+ await mounted.verify(signal);
312
+ const schema = mounted.root.tools
313
+ .schemas({ turnType: "chat" })
314
+ .find((entry) => entry.name === "add_todo");
315
+ expect(schema).toBeDefined();
316
+ // The pin the model is shown, and the pin the call carries, are one
317
+ // string. If they ever diverge the Turn stops being reconstructable.
318
+ expect(schema?.description).toContain(APPLET_MEMBER.generationId);
319
+
320
+ const call = { id: "call-1", name: "add_todo", input: { title: "milk" } };
321
+ const context = {
322
+ botId: "bot-1",
323
+ agentId: "bot-1",
324
+ sessionId: `${USER}:bot-1`,
325
+ compositionGenerationId: generation.generationId,
326
+ effectId: "effect-1",
327
+ toolCall: call,
328
+ turnType: "chat" as const,
329
+ signal,
330
+ };
331
+ const prepared = await mounted.root.tools.prepare(call, context);
332
+ expect(prepared.kind).toBe("ready");
333
+ const outcome = await mounted.root.tools.executePrepared(
334
+ prepared as Extract<typeof prepared, { kind: "ready" }>,
335
+ context,
336
+ );
337
+ expect(outcome).toMatchObject({ isError: false });
338
+ expect(calls).toEqual([
339
+ { appletId: APPLET, generationId: APPLET_MEMBER.generationId },
340
+ ]);
341
+ } finally {
342
+ await mounted.dispose();
343
+ }
344
+ });
345
+ });
@@ -185,10 +185,16 @@ export interface ShellIsolateMountOptions {
185
185
  * (`docs/research/spike-applet-facets.md` §8). Absent when the Bot Durable
186
186
  * Object has no Applet binding: an Applet member's tools are then simply not
187
187
  * registered, exactly as an isolate member fails without a loader.
188
+ *
189
+ * `generationId` is the Applet generation this Turn's Composition pinned, and
190
+ * it is not decoration: the Applet Durable Object runs that generation or
191
+ * refuses the call (ADR 0038). Without it a publish landing mid-Turn would
192
+ * execute new code behind the schema and provenance the model was shown.
188
193
  */
189
194
  export interface ShellAppletMountOptions {
190
195
  invokeTool(request: {
191
196
  appletId: string;
197
+ generationId: string;
192
198
  tool: string;
193
199
  input: unknown;
194
200
  }): Promise<{ status: "ok" | "error"; content: string }>;
@@ -385,6 +391,9 @@ export function createShellCompositionHost(
385
391
  execute: async (input) => {
386
392
  const outcome = await routing.invokeTool({
387
393
  appletId: applet.appletId,
394
+ // The pin the description above advertises, carried into the
395
+ // call so the instance executes it or refuses.
396
+ generationId: applet.generationId,
388
397
  tool: tool.name,
389
398
  input: input ?? null,
390
399
  });