@frockbot/plugin-shell 0.1.3 → 0.2.0

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 (41) hide show
  1. package/frockbot.json +4 -0
  2. package/package.json +30 -28
  3. package/src/agent.ts +10 -13
  4. package/src/backend-authoring.test.ts +356 -2
  5. package/src/backend-authoring.ts +585 -20
  6. package/src/backend-composition-input.test.ts +65 -0
  7. package/src/backend-composition-input.ts +58 -0
  8. package/src/backend-composition.ts +23 -5
  9. package/src/backend-configuration.test.ts +549 -1468
  10. package/src/backend-contracts.test.ts +28 -0
  11. package/src/backend-contracts.ts +3 -1
  12. package/src/backend-execution.ts +0 -4
  13. package/src/backend-iframe-ui.test.ts +131 -0
  14. package/src/backend-image.test.ts +8 -8
  15. package/src/backend-image.ts +13 -13
  16. package/src/backend-isolate.test.ts +230 -89
  17. package/src/backend-isolate.ts +103 -200
  18. package/src/backend-package-catalog.test.ts +451 -0
  19. package/src/backend-package-catalog.ts +923 -0
  20. package/src/backend-recovery-integration.test.ts +17 -67
  21. package/src/backend-routines.ts +1 -1
  22. package/src/backend-runner-iframe.test.ts +74 -0
  23. package/src/backend-runner.ts +192 -0
  24. package/src/backend.ts +1198 -1781
  25. package/src/client/FrockBotApp.vue +44 -73
  26. package/src/client/PackageIframeHost.vue +218 -0
  27. package/src/client/PackageIframeSettings.vue +52 -0
  28. package/src/client/SendPayloadView.vue +0 -60
  29. package/src/client/index.test.ts +439 -380
  30. package/src/client/index.ts +163 -257
  31. package/src/client/model-presentation.test.ts +21 -8
  32. package/src/client/model-presentation.ts +14 -7
  33. package/src/client/package-iframe-host-message.test.ts +41 -0
  34. package/src/client/package-iframe-host-message.ts +27 -0
  35. package/src/client/styles.css +0 -27
  36. package/src/composition-views.ts +54 -0
  37. package/src/settings-links.test.ts +2 -10
  38. package/src/settings-links.ts +1 -13
  39. package/src/shared.ts +10 -13
  40. package/src/backend-assignment.test.ts +0 -161
  41. package/src/backend-assignment.ts +0 -274
@@ -20,6 +20,34 @@ function storedRun(): StoredRun {
20
20
  }
21
21
 
22
22
  describe("StoredRun durable contract", () => {
23
+ test("migrates a historical configuration snapshot before strict decoding", () => {
24
+ // Literal Bot settings shape from eb0283edcce5daea976a21a9f6a6414bedc6e2bc.
25
+ const decoded = requireStoredRunV1({
26
+ ...storedRun(),
27
+ configurationSnapshot: {
28
+ schemaVersion: 1,
29
+ botId: "primary",
30
+ revision: 4,
31
+ profile: { name: "Primary" },
32
+ notifications: { enabled: true },
33
+ assignments: [],
34
+ assignmentOperations: [],
35
+ model: {
36
+ connectionId: "ollama-1",
37
+ providerModelId: "glm-5.3-flash:cloud",
38
+ },
39
+ },
40
+ });
41
+ expect(decoded.configurationSnapshot).toEqual({
42
+ schemaVersion: 1,
43
+ botId: "primary",
44
+ revision: 4,
45
+ profile: { name: "Primary", description: undefined },
46
+ notifications: { enabled: true },
47
+ packageValues: {},
48
+ });
49
+ });
50
+
23
51
  test("uses the public run identifier grammar", () => {
24
52
  expect(() =>
25
53
  requireStoredRunV1({ ...storedRun(), runId: "run:1" }),
@@ -8,6 +8,7 @@ import {
8
8
  import {
9
9
  decodeBotSettingsViewV1,
10
10
  isPublicIdentifier,
11
+ migrateStoredBotSettingsV1,
11
12
  type BotSettingsViewV1,
12
13
  } from "@frockbot/configuration-core";
13
14
 
@@ -36,7 +37,8 @@ export function decodeRunIdV1(value: unknown): string {
36
37
  export const storedRunCodecV1: StoredRunCodecV1<BotSettingsViewV1> =
37
38
  createStoredRunCodecV1<BotSettingsViewV1>({
38
39
  decodeRunId: decodeRunIdV1,
39
- decodeConfigurationSnapshot: decodeBotSettingsViewV1,
40
+ decodeConfigurationSnapshot: (stored) =>
41
+ decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored)),
40
42
  });
41
43
 
42
44
  export function requireStoredRunV1(input: unknown): StoredRun {
@@ -6,7 +6,6 @@ import type {
6
6
  import type {
7
7
  BotExecutionPlanV1,
8
8
  BotSettingsViewV1,
9
- ConnectionView,
10
9
  } from "@frockbot/configuration-core";
11
10
  import type { BotTurnCommand, BotTurnCompletion } from "./backend-contracts.js";
12
11
 
@@ -17,9 +16,6 @@ export interface BotResidentProjection {
17
16
  settings: BotSettingsViewV1;
18
17
  executionPlan: BotExecutionPlanV1;
19
18
  systemPromptSection: string;
20
- authorizeConnection(
21
- assignment: BotSettingsViewV1["assignments"][number],
22
- ): Promise<ConnectionView>;
23
19
  }
24
20
 
25
21
  export interface BotResidentTurnExecution {
@@ -0,0 +1,131 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { PackageIframeCompositionV1 } from "@frockbot/kernel-contracts";
3
+ import type { FrockBotManifest } from "@frockbot/kernel-composition";
4
+ import type { CompositionGenerationV1 } from "@frockbot/kernel-composition/generation";
5
+ import { requirePackageUiToolDeclarationV1 } from "./backend.js";
6
+ import { projectPackageIframeCompositionV1 } from "./composition-views.js";
7
+
8
+ describe("Package iframe server admission", () => {
9
+ const catalog: PackageIframeCompositionV1 = {
10
+ schemaVersion: 1,
11
+ botId: "bot",
12
+ generationId: "generation-1",
13
+ contributions: [
14
+ {
15
+ packageId: "weather-page",
16
+ displayName: "Weather page",
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" }],
25
+ declaredTools: ["weather_lookup"],
26
+ },
27
+ ],
28
+ };
29
+
30
+ test("refuses an undeclared tool before admitting a durable Turn", () => {
31
+ expect(() =>
32
+ requirePackageUiToolDeclarationV1(catalog, {
33
+ generationId: "generation-1",
34
+ packageId: "weather-page",
35
+ name: "package_author",
36
+ }),
37
+ ).toThrow('did not declare tool "package_author"');
38
+ });
39
+
40
+ test("refuses a stale page after its Composition generation changed", () => {
41
+ expect(() =>
42
+ requirePackageUiToolDeclarationV1(catalog, {
43
+ generationId: "generation-old",
44
+ packageId: "weather-page",
45
+ name: "weather_lookup",
46
+ }),
47
+ ).toThrow('generation "generation-old"');
48
+ });
49
+
50
+ test("projects a Catalog member through the same manifest reader", async () => {
51
+ const manifestHash = "c".repeat(64);
52
+ const uiArtifact = {
53
+ contentHash: "d".repeat(64),
54
+ size: 42,
55
+ mediaType: "text/html" as const,
56
+ bundlerVersion: "frockbot-inline-html@1",
57
+ };
58
+ const manifest: FrockBotManifest = {
59
+ schemaVersion: 3,
60
+ id: "weather-page",
61
+ displayName: "Weather page",
62
+ version: "0.0.1",
63
+ compatibility: { frockbot: "*" },
64
+ dependencies: {},
65
+ contributions: {
66
+ runtime: { entry: "./package.js", host: "bot-isolate" },
67
+ client: {
68
+ kind: "iframe",
69
+ artifact: uiArtifact,
70
+ mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
71
+ },
72
+ },
73
+ tools: [
74
+ { name: "weather_lookup", description: "Looks up", inputSchema: {} },
75
+ ],
76
+ permissions: [],
77
+ };
78
+ const generation: CompositionGenerationV1 = {
79
+ schemaVersion: 1,
80
+ generationId: "generation-1",
81
+ artifactSetHash: "a".repeat(64),
82
+ createdAt: "2026-09-02T00:00:00.000Z",
83
+ origin: { kind: "bootstrap" },
84
+ members: [
85
+ {
86
+ packageId: "weather-page",
87
+ specifier: "catalog:weather-page",
88
+ version: "0.0.1",
89
+ manifestHash,
90
+ provenance: {
91
+ kind: "catalog",
92
+ packageId: "weather-page",
93
+ version: "0.0.1",
94
+ catalogId: "weather-page",
95
+ catalogGeneration: "catalog-1",
96
+ contentHash: "b".repeat(64),
97
+ },
98
+ artifact: {
99
+ contentHash: "b".repeat(64),
100
+ size: 512,
101
+ mediaType: "application/javascript",
102
+ bundlerVersion: "catalog-test@1",
103
+ },
104
+ },
105
+ ],
106
+ status: "active",
107
+ };
108
+ const requested: string[] = [];
109
+
110
+ const projected = await projectPackageIframeCompositionV1({
111
+ botId: "bot",
112
+ generation,
113
+ readMemberManifest: (member) => {
114
+ requested.push(member.manifestHash);
115
+ return Promise.resolve(manifest);
116
+ },
117
+ });
118
+
119
+ expect(requested).toEqual([manifestHash]);
120
+ expect(projected.contributions).toEqual([
121
+ {
122
+ packageId: "weather-page",
123
+ displayName: "Weather page",
124
+ provenance: "User-installed",
125
+ artifact: uiArtifact,
126
+ mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
127
+ declaredTools: ["weather_lookup"],
128
+ },
129
+ ]);
130
+ });
131
+ });
@@ -5,8 +5,8 @@ import {
5
5
  } from "@frockbot/plugin-image/testing";
6
6
  import {
7
7
  createBotImageHost,
8
- createWorkersAiImageModelV1,
9
- decodeWorkersAiImageV1,
8
+ createNativeAiImageModelV1,
9
+ decodeNativeAiImageV1,
10
10
  } from "./backend-image.ts";
11
11
 
12
12
  const IDENTITY = { userId: "user-1", botId: "bot-1" };
@@ -56,7 +56,7 @@ describe("normalizing what Workers AI answered", () => {
56
56
  const png = fakePngBytesV1(64, 32);
57
57
 
58
58
  test("accepts the base64 envelope the FLUX models answer", async () => {
59
- const buffer = await decodeWorkersAiImageV1({ image: base64(png) });
59
+ const buffer = await decodeNativeAiImageV1({ image: base64(png) });
60
60
  expect([...new Uint8Array(buffer)]).toEqual([...png]);
61
61
  });
62
62
 
@@ -68,23 +68,23 @@ describe("normalizing what Workers AI answered", () => {
68
68
  controller.close();
69
69
  },
70
70
  });
71
- expect([...new Uint8Array(await decodeWorkersAiImageV1(stream))]).toEqual([
71
+ expect([...new Uint8Array(await decodeNativeAiImageV1(stream))]).toEqual([
72
72
  ...png,
73
73
  ]);
74
74
  });
75
75
 
76
76
  test("accepts a raw buffer or view", async () => {
77
77
  expect([
78
- ...new Uint8Array(await decodeWorkersAiImageV1(png.slice().buffer)),
78
+ ...new Uint8Array(await decodeNativeAiImageV1(png.slice().buffer)),
79
79
  ]).toEqual([...png]);
80
- expect([...new Uint8Array(await decodeWorkersAiImageV1(png))]).toEqual([
80
+ expect([...new Uint8Array(await decodeNativeAiImageV1(png))]).toEqual([
81
81
  ...png,
82
82
  ]);
83
83
  });
84
84
 
85
85
  test("refuses anything else rather than storing it", async () => {
86
86
  for (const answer of [undefined, null, 7, "hello", { image: 3 }, {}]) {
87
- await expect(decodeWorkersAiImageV1(answer)).rejects.toThrow(
87
+ await expect(decodeNativeAiImageV1(answer)).rejects.toThrow(
88
88
  "not an image",
89
89
  );
90
90
  }
@@ -92,7 +92,7 @@ describe("normalizing what Workers AI answered", () => {
92
92
 
93
93
  test("passes the requested size through to the binding", async () => {
94
94
  const calls: Array<[string, Record<string, unknown>]> = [];
95
- const model = createWorkersAiImageModelV1({
95
+ const model = createNativeAiImageModelV1({
96
96
  run: (name, input) => {
97
97
  calls.push([name, input]);
98
98
  return Promise.resolve({ image: base64(png) });
@@ -1,15 +1,15 @@
1
1
  // The Bot Durable Object's half of the image-generation seam.
2
2
  //
3
3
  // The Image Package consumes a narrow `ImageModelV1` — one method, answering
4
- // raw image bytes — and knows nothing about Workers AI. This module is the
4
+ // raw image bytes — and knows nothing about Cloudflare's native AI binding. This module is the
5
5
  // adapter: it takes the `AI` binding off the Durable Object's environment,
6
- // normalizes the three shapes a Workers AI text-to-image model answers in, and
6
+ // normalizes the three shapes a native text-to-image model answers in, and
7
7
  // hands the Package a seam with no platform vocabulary in it. "Electron,
8
8
  // Cloudflare, provider SDK, and Computer implementation types remain inside
9
9
  // their adapters."
10
10
  //
11
11
  // The binding is optional, exactly as `PACKAGE_BUNDLER` is. A deployment with
12
- // no Workers AI binding still mounts the Package, and `generate_image` then
12
+ // no native AI binding still mounts the Package, and `generate_image` then
13
13
  // refuses visibly on the Turn that calls it — which is a better answer than a
14
14
  // tool that silently vanishes from the catalog, and a far better one than a
15
15
  // `TypeError` inside the Agent loop.
@@ -38,11 +38,11 @@ export interface BotImageTurn {
38
38
  }
39
39
 
40
40
  /**
41
- * The Workers AI binding, declared structurally so this module names no
41
+ * The native AI binding, declared structurally so this module names no
42
42
  * Cloudflare type it does not have to. `run` is the whole of what an image
43
43
  * model needs from it.
44
44
  */
45
- export interface WorkersAiBindingV1 {
45
+ export interface NativeAiBindingV1 {
46
46
  run(model: string, input: Record<string, unknown>): Promise<unknown>;
47
47
  }
48
48
 
@@ -51,7 +51,7 @@ export interface WorkersAiBindingV1 {
51
51
  * as its own type so the binding's absence is a typed state, not a cast.
52
52
  */
53
53
  export interface BotImageEnv {
54
- AI?: WorkersAiBindingV1;
54
+ AI?: NativeAiBindingV1;
55
55
  WORKSPACE_FILES?: WorkspaceFilesV1;
56
56
  }
57
57
 
@@ -88,7 +88,7 @@ async function readStream(
88
88
  }
89
89
 
90
90
  /**
91
- * The bytes inside whatever a Workers AI text-to-image model answered.
91
+ * The bytes inside whatever a native text-to-image model answered.
92
92
  *
93
93
  * The catalog does not agree with itself: `flux-1-schnell` and the FLUX.2
94
94
  * models answer `{ image: "<base64>" }`, the Stable Diffusion models answer a
@@ -96,7 +96,7 @@ async function readStream(
96
96
  * directly. Every one of those is an image; none of them is the Package's
97
97
  * problem.
98
98
  */
99
- export async function decodeWorkersAiImageV1(
99
+ export async function decodeNativeAiImageV1(
100
100
  answer: unknown,
101
101
  ): Promise<ArrayBuffer> {
102
102
  if (answer instanceof ArrayBuffer) return answer;
@@ -123,15 +123,15 @@ export async function decodeWorkersAiImageV1(
123
123
  }
124
124
 
125
125
  /**
126
- * The Workers AI binding as an {@link ImageModelV1}.
126
+ * The native AI binding as an {@link ImageModelV1}.
127
127
  *
128
128
  * `width` and `height` are passed through: the Stable Diffusion models accept
129
129
  * them, and the FLUX models ignore unknown fields rather than refusing. The
130
130
  * Package never trusts them to have been honoured — it reads the dimensions
131
131
  * back out of the bytes.
132
132
  */
133
- export function createWorkersAiImageModelV1(
134
- binding: WorkersAiBindingV1,
133
+ export function createNativeAiImageModelV1(
134
+ binding: NativeAiBindingV1,
135
135
  ): ImageModelV1 {
136
136
  return {
137
137
  async run(model: string, input: ImageModelInputV1): Promise<ArrayBuffer> {
@@ -140,7 +140,7 @@ export function createWorkersAiImageModelV1(
140
140
  width: input.width,
141
141
  height: input.height,
142
142
  });
143
- return await decodeWorkersAiImageV1(answer);
143
+ return await decodeNativeAiImageV1(answer);
144
144
  },
145
145
  };
146
146
  }
@@ -174,7 +174,7 @@ export function createBotImageHost(
174
174
  runId: turn.runId,
175
175
  },
176
176
  files,
177
- ...(bindings.AI ? { model: createWorkersAiImageModelV1(bindings.AI) } : {}),
177
+ ...(bindings.AI ? { model: createNativeAiImageModelV1(bindings.AI) } : {}),
178
178
  ...(modelId ? { modelId } : {}),
179
179
  };
180
180
  }