@frockbot/plugin-shell 0.3.23 → 0.3.24

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.23",
3
+ "version": "0.3.24",
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.23",
56
- "@frockbot/application-foundation": "0.3.23",
57
- "@frockbot/catalog-core": "0.3.23",
58
- "@frockbot/client-core": "0.3.23",
59
- "@frockbot/client-ui": "0.3.23",
60
- "@frockbot/computer-core": "0.3.23",
61
- "@frockbot/computer-host-protocol": "0.3.23",
62
- "@frockbot/configuration-core": "0.3.23",
63
- "@frockbot/connection-core": "0.3.23",
64
- "@frockbot/kernel-agent-loop": "0.3.23",
65
- "@frockbot/kernel-composition": "0.3.23",
66
- "@frockbot/kernel-contracts": "0.3.23",
67
- "@frockbot/kernel-do": "0.3.23",
68
- "@frockbot/machine-protocol": "0.3.23",
69
- "@frockbot/plugin-applets": "0.3.23",
70
- "@frockbot/plugin-authoring": "0.3.23",
71
- "@frockbot/plugin-bot-template": "0.3.23",
72
- "@frockbot/plugin-computer": "0.3.23",
73
- "@frockbot/plugin-flock": "0.3.23",
74
- "@frockbot/plugin-image": "0.3.23",
75
- "@frockbot/plugin-machine-messages": "0.3.23",
76
- "@frockbot/plugin-mcp": "0.3.23",
77
- "@frockbot/plugin-memory": "0.3.23",
78
- "@frockbot/plugin-package-catalog": "0.3.23",
79
- "@frockbot/plugin-routines": "0.3.23",
80
- "@frockbot/plugin-skills": "0.3.23",
81
- "@frockbot/plugin-subagents": "0.3.23",
82
- "@frockbot/plugin-user-machine": "0.3.23",
83
- "@frockbot/protocol": "0.3.23",
55
+ "@frockbot/agent-runtime": "0.3.24",
56
+ "@frockbot/application-foundation": "0.3.24",
57
+ "@frockbot/catalog-core": "0.3.24",
58
+ "@frockbot/client-core": "0.3.24",
59
+ "@frockbot/client-ui": "0.3.24",
60
+ "@frockbot/computer-core": "0.3.24",
61
+ "@frockbot/computer-host-protocol": "0.3.24",
62
+ "@frockbot/configuration-core": "0.3.24",
63
+ "@frockbot/connection-core": "0.3.24",
64
+ "@frockbot/kernel-agent-loop": "0.3.24",
65
+ "@frockbot/kernel-composition": "0.3.24",
66
+ "@frockbot/kernel-contracts": "0.3.24",
67
+ "@frockbot/kernel-do": "0.3.24",
68
+ "@frockbot/machine-protocol": "0.3.24",
69
+ "@frockbot/plugin-applets": "0.3.24",
70
+ "@frockbot/plugin-authoring": "0.3.24",
71
+ "@frockbot/plugin-bot-template": "0.3.24",
72
+ "@frockbot/plugin-computer": "0.3.24",
73
+ "@frockbot/plugin-flock": "0.3.24",
74
+ "@frockbot/plugin-image": "0.3.24",
75
+ "@frockbot/plugin-machine-messages": "0.3.24",
76
+ "@frockbot/plugin-mcp": "0.3.24",
77
+ "@frockbot/plugin-memory": "0.3.24",
78
+ "@frockbot/plugin-package-catalog": "0.3.24",
79
+ "@frockbot/plugin-routines": "0.3.24",
80
+ "@frockbot/plugin-skills": "0.3.24",
81
+ "@frockbot/plugin-subagents": "0.3.24",
82
+ "@frockbot/plugin-user-machine": "0.3.24",
83
+ "@frockbot/protocol": "0.3.24",
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.23",
90
- "@frockbot/plugin-tools": "0.3.23",
89
+ "@frockbot/plugin-prompt": "0.3.24",
90
+ "@frockbot/plugin-tools": "0.3.24",
91
91
  "@types/bun": "1.3.6",
92
92
  "@types/node": "26.2.0",
93
93
  "@vitejs/plugin-vue": "6.0.8",
@@ -0,0 +1,221 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ApplicationPlan } from "@frockbot/kernel-composition/compiler";
3
+ import {
4
+ compositionArtifactSetHashV1,
5
+ decodeCompositionGenerationV1,
6
+ type CompositionGenerationV1,
7
+ type CompositionMemberV1,
8
+ } from "@frockbot/kernel-composition/generation";
9
+ import {
10
+ bootstrapCompositionGeneration,
11
+ resolveDeploymentCompositionV1,
12
+ DEPLOYMENT_FOLLOW_SUMMARY_V1,
13
+ } from "./backend-composition.js";
14
+
15
+ const ARTIFACT_A = {
16
+ contentHash: "a".repeat(64),
17
+ size: 10,
18
+ mediaType: "application/javascript" as const,
19
+ bundlerVersion: "1",
20
+ };
21
+ const ARTIFACT_B = { ...ARTIFACT_A, contentHash: "b".repeat(64) };
22
+
23
+ /** A compiled application: the shell in-process, the Applets as an artifact. */
24
+ function plan(input: {
25
+ appletsManifest: unknown;
26
+ appletsArtifact?: typeof ARTIFACT_A;
27
+ extra?: boolean;
28
+ }): ApplicationPlan {
29
+ return {
30
+ packages: [
31
+ {
32
+ id: "shell",
33
+ specifier: "@frockbot/plugin-shell",
34
+ version: "1.0.0",
35
+ manifest: { id: "shell" },
36
+ },
37
+ {
38
+ id: "applets",
39
+ specifier: "@frockbot/plugin-applets",
40
+ version: "1.0.0",
41
+ manifest: input.appletsManifest,
42
+ artifact: input.appletsArtifact ?? ARTIFACT_A,
43
+ },
44
+ ...(input.extra
45
+ ? [
46
+ {
47
+ id: "extra",
48
+ specifier: "@frockbot/plugin-extra",
49
+ version: "1.0.0",
50
+ manifest: { id: "extra" },
51
+ },
52
+ ]
53
+ : []),
54
+ ],
55
+ } as unknown as ApplicationPlan;
56
+ }
57
+
58
+ function store(current: CompositionGenerationV1) {
59
+ const proposed: CompositionGenerationV1[] = [];
60
+ return {
61
+ proposed,
62
+ current: () => Promise.resolve(current),
63
+ propose: (generation: CompositionGenerationV1) => {
64
+ proposed.push(generation);
65
+ return Promise.resolve();
66
+ },
67
+ };
68
+ }
69
+
70
+ /** The generation a Bot pinned under yesterday's deployment, plus a User member. */
71
+ async function pinned(
72
+ deployment: ApplicationPlan,
73
+ extraMembers: CompositionMemberV1[] = [],
74
+ ): Promise<CompositionGenerationV1> {
75
+ const bootstrap = await bootstrapCompositionGeneration(
76
+ deployment,
77
+ "2026-09-04T09:00:00.000Z",
78
+ );
79
+ const members = [...bootstrap.members, ...extraMembers].sort((a, b) =>
80
+ a.packageId.localeCompare(b.packageId),
81
+ );
82
+ const applets = [
83
+ {
84
+ kind: "applet" as const,
85
+ appletId: "user-1.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
86
+ generationId: "g1",
87
+ tools: [
88
+ {
89
+ name: "add_todo",
90
+ description: "Add",
91
+ inputSchema: { type: "object" },
92
+ },
93
+ ],
94
+ provenance: {
95
+ kind: "user" as const,
96
+ packageId: "user-1.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
97
+ version: "g1",
98
+ userId: "user-1",
99
+ authoredAt: "2026-09-04T09:00:00.000Z",
100
+ },
101
+ },
102
+ ];
103
+ const artifactSetHash = await compositionArtifactSetHashV1(members, applets);
104
+ return decodeCompositionGenerationV1({
105
+ schemaVersion: 1,
106
+ generationId: `2026-09-04T09:00:00.000Z:${artifactSetHash.slice(0, 16)}`,
107
+ artifactSetHash,
108
+ createdAt: "2026-09-04T09:00:00.000Z",
109
+ origin: { kind: "bootstrap" },
110
+ members,
111
+ applets,
112
+ status: "active",
113
+ });
114
+ }
115
+
116
+ const USER_MEMBER: CompositionMemberV1 = {
117
+ packageId: "user-1.notes",
118
+ specifier: "frockbot:user-1.notes",
119
+ version: "3",
120
+ manifestHash: "d".repeat(64),
121
+ provenance: {
122
+ kind: "user",
123
+ packageId: "user-1.notes",
124
+ version: "3",
125
+ userId: "user-1",
126
+ authoredAt: "2026-09-04T10:00:00.000Z",
127
+ },
128
+ artifact: { ...ARTIFACT_A, contentHash: "e".repeat(64) },
129
+ };
130
+
131
+ describe("a Bot's built-in members follow the deployment", () => {
132
+ test("an unchanged deployment proposes nothing", async () => {
133
+ const yesterday = plan({ appletsManifest: { id: "applets", v: 1 } });
134
+ const composition = store(await pinned(yesterday, [USER_MEMBER]));
135
+ const result = await resolveDeploymentCompositionV1({
136
+ plan: yesterday,
137
+ composition,
138
+ now: new Date("2026-09-05T07:30:00.000Z"),
139
+ });
140
+ expect(result).toBeUndefined();
141
+ expect(composition.proposed).toHaveLength(0);
142
+ });
143
+
144
+ test("a changed manifest or artifact is a new pinned generation with the old one as parent", async () => {
145
+ const yesterday = plan({ appletsManifest: { id: "applets", v: 1 } });
146
+ const current = await pinned(yesterday, [USER_MEMBER]);
147
+ const composition = store(current);
148
+ const today = plan({
149
+ appletsManifest: { id: "applets", v: 2 },
150
+ appletsArtifact: ARTIFACT_B,
151
+ });
152
+ const result = await resolveDeploymentCompositionV1({
153
+ plan: today,
154
+ composition,
155
+ now: new Date("2026-09-05T07:30:00.000Z"),
156
+ });
157
+ expect(result).toBeDefined();
158
+ expect(composition.proposed).toEqual([result!]);
159
+ expect(result!.status).toBe("pending");
160
+ expect(result!.parentGenerationId).toBe(current.generationId);
161
+ expect(result!.summary).toBe(DEPLOYMENT_FOLLOW_SUMMARY_V1);
162
+ const deployed = await bootstrapCompositionGeneration(
163
+ today,
164
+ result!.createdAt,
165
+ );
166
+ const applets = result!.members.find((m) => m.packageId === "applets");
167
+ expect(applets).toEqual(
168
+ deployed.members.find((m) => m.packageId === "applets"),
169
+ );
170
+ // The shell did not change, the User's own member is carried over
171
+ // untouched, and the Applet members stay pinned.
172
+ expect(result!.members.find((m) => m.packageId === "shell")).toEqual(
173
+ current.members.find((m) => m.packageId === "shell"),
174
+ );
175
+ expect(result!.members.find((m) => m.packageId === "user-1.notes")).toEqual(
176
+ USER_MEMBER,
177
+ );
178
+ expect(result!.applets).toEqual(current.applets);
179
+ expect(result!.artifactSetHash).toBe(
180
+ await compositionArtifactSetHashV1(result!.members, result!.applets),
181
+ );
182
+ });
183
+
184
+ test("a built-in Package the deployment added or dropped changes the set", async () => {
185
+ const yesterday = plan({ appletsManifest: { id: "applets", v: 1 } });
186
+ const composition = store(await pinned(yesterday));
187
+ const today = plan({
188
+ appletsManifest: { id: "applets", v: 1 },
189
+ extra: true,
190
+ });
191
+ const added = await resolveDeploymentCompositionV1({
192
+ plan: today,
193
+ composition,
194
+ });
195
+ expect(added!.members.map((m) => m.packageId)).toEqual([
196
+ "applets",
197
+ "extra",
198
+ "shell",
199
+ ]);
200
+
201
+ const dropped = await resolveDeploymentCompositionV1({
202
+ plan: yesterday,
203
+ composition: store(await pinned(today)),
204
+ });
205
+ expect(dropped!.members.map((m) => m.packageId)).toEqual([
206
+ "applets",
207
+ "shell",
208
+ ]);
209
+ });
210
+
211
+ test("a version bump alone does not spend a generation", async () => {
212
+ const yesterday = plan({ appletsManifest: { id: "applets", v: 1 } });
213
+ const composition = store(await pinned(yesterday));
214
+ const bumped = {
215
+ packages: yesterday.packages.map((pkg) => ({ ...pkg, version: "1.0.1" })),
216
+ } as unknown as ApplicationPlan;
217
+ expect(
218
+ await resolveDeploymentCompositionV1({ plan: bumped, composition }),
219
+ ).toBeUndefined();
220
+ });
221
+ });
@@ -18,9 +18,13 @@ import {
18
18
  import type { ApplicationPlan } from "@frockbot/kernel-composition/compiler";
19
19
  import {
20
20
  bootstrapGeneration,
21
+ compositionArtifactSetHashV1,
22
+ compositionGenerationIdV1,
23
+ decodeCompositionGenerationV1,
21
24
  type CompositionGenerationV1,
22
25
  type CompositionHost,
23
26
  type CompositionMemberV1,
27
+ type CompositionStore,
24
28
  type MountedComposition,
25
29
  } from "@frockbot/kernel-composition/generation";
26
30
  import {
@@ -57,6 +61,92 @@ export function bootstrapCompositionGeneration(
57
61
  );
58
62
  }
59
63
 
64
+ /** The audit line a deployment-following generation carries. */
65
+ export const DEPLOYMENT_FOLLOW_SUMMARY_V1 =
66
+ "Updated the built-in Packages to this deployment's";
67
+
68
+ /**
69
+ * Resolve the deployment's built-in Packages into this Bot's next generation.
70
+ *
71
+ * A first-party member's manifest and artifact live in the compiled
72
+ * application, keyed by hash. A deploy that changes one of them (2026-09-05:
73
+ * the Applets list page) leaves every pinned generation naming a manifest this
74
+ * deployment no longer ships, and every Turn of every Bot fails to mount. A
75
+ * built-in member is the deployment's to update, never the Bot's, so before a
76
+ * Turn is admitted the Bot's first-party members are brought up to the
77
+ * application's, as a new generation with the old one as its parent. Members
78
+ * the Bot or its User put in are carried over exactly as they are; Applet
79
+ * members stay pinned; a deployment that changes nothing proposes nothing.
80
+ *
81
+ * Only the manifest and the artifact decide. A version string that moved with
82
+ * a release while both stayed the same mounts exactly as before, and a
83
+ * generation per Bot per release would eat the User's retention quota for
84
+ * nothing.
85
+ */
86
+ export async function resolveDeploymentCompositionV1(options: {
87
+ plan: ApplicationPlan;
88
+ composition: Pick<CompositionStore, "current" | "propose">;
89
+ now?: Date;
90
+ }): Promise<CompositionGenerationV1 | undefined> {
91
+ const current = await options.composition.current();
92
+ const createdAt = (options.now ?? new Date()).toISOString();
93
+ const deployed = await bootstrapCompositionGeneration(
94
+ options.plan,
95
+ createdAt,
96
+ );
97
+ const shipped = new Map(
98
+ deployed.members.map((member) => [member.packageId, member]),
99
+ );
100
+ const members: CompositionMemberV1[] = [];
101
+ let changed = false;
102
+ for (const member of current.members) {
103
+ if (member.provenance.kind !== "first-party") {
104
+ members.push(member);
105
+ continue;
106
+ }
107
+ const replacement = shipped.get(member.packageId);
108
+ shipped.delete(member.packageId);
109
+ if (!replacement) {
110
+ // The deployment no longer ships it; nothing could mount it anyway.
111
+ changed = true;
112
+ continue;
113
+ }
114
+ if (
115
+ replacement.manifestHash !== member.manifestHash ||
116
+ replacement.artifact?.contentHash !== member.artifact?.contentHash
117
+ ) {
118
+ changed = true;
119
+ members.push(replacement);
120
+ } else {
121
+ members.push(member);
122
+ }
123
+ }
124
+ for (const added of shipped.values()) {
125
+ changed = true;
126
+ members.push(added);
127
+ }
128
+ if (!changed) return undefined;
129
+ const ordered = members.sort((left, right) =>
130
+ left.packageId.localeCompare(right.packageId),
131
+ );
132
+ const applets = current.applets ?? [];
133
+ const artifactSetHash = await compositionArtifactSetHashV1(ordered, applets);
134
+ const generation = decodeCompositionGenerationV1({
135
+ schemaVersion: 1,
136
+ generationId: compositionGenerationIdV1(createdAt, artifactSetHash),
137
+ artifactSetHash,
138
+ parentGenerationId: current.generationId,
139
+ summary: DEPLOYMENT_FOLLOW_SUMMARY_V1,
140
+ createdAt,
141
+ origin: { kind: "bootstrap" },
142
+ members: ordered,
143
+ ...(applets.length === 0 ? {} : { applets }),
144
+ status: "pending",
145
+ });
146
+ await options.composition.propose(generation, { pin: true });
147
+ return generation;
148
+ }
149
+
60
150
  export interface ShellMountedComposition extends MountedComposition {
61
151
  readonly runtime: FoundationRuntime;
62
152
  }
package/src/backend.ts CHANGED
@@ -126,12 +126,14 @@ import {
126
126
  planBotRunRecovery,
127
127
  planInterruptedRunRecoveryV1,
128
128
  } from "./backend-recovery.js";
129
+ import { COMPOSITION_CURRENT_KEY } from "@frockbot/kernel-do";
129
130
  import {
130
131
  bootstrapCompositionGeneration,
131
132
  createShellCompositionHost,
132
133
  type ShellAppletMountOptions,
133
134
  type ShellIsolateMountOptions,
134
135
  type ShellMountedComposition,
136
+ resolveDeploymentCompositionV1,
135
137
  } from "./backend-composition.js";
136
138
  import {
137
139
  createAppletCapabilityHostV1,
@@ -1230,7 +1232,8 @@ export class ShellBotBackendContribution {
1230
1232
  // from the previous Turn has already handed the log back (ADR 0030).
1231
1233
  await yieldCompactionWorkV1(command.sessionId);
1232
1234
  // Before admission, so the pin this Turn takes already carries whatever the
1233
- // User's Applet directory says now.
1235
+ // deployment ships and whatever the User's Applet directory says now.
1236
+ await this.followDeploymentComposition();
1234
1237
  await this.resolveAppletComposition(
1235
1238
  { userId: command.userId, botId: command.botId },
1236
1239
  command,
@@ -1245,6 +1248,7 @@ export class ShellBotBackendContribution {
1245
1248
  await this.validateIdentity(identity);
1246
1249
  const catalog = await this.listPackageUi(identity);
1247
1250
  const contribution = requirePackageUiToolDeclarationV1(catalog, command);
1251
+ await this.followDeploymentComposition();
1248
1252
  return projectClientTurnV1(
1249
1253
  await this.authority.run({
1250
1254
  ...identity,
@@ -2723,6 +2727,39 @@ export class ShellBotBackendContribution {
2723
2727
  return focused;
2724
2728
  }
2725
2729
 
2730
+ /**
2731
+ * Bring this Bot's built-in members up to the deployment before a Turn is
2732
+ * admitted, so a release that changed a first-party manifest or artifact is
2733
+ * a new generation rather than a Bot that cannot mount (2026-09-05, when
2734
+ * every Bot failed every Turn after the Applets list page changed). Outside
2735
+ * the admission transaction like the Applet resolve below; a failure here
2736
+ * leaves the Bot on the generation it has and is visible where that
2737
+ * generation fails to mount.
2738
+ */
2739
+ private async followDeploymentComposition(): Promise<void> {
2740
+ try {
2741
+ // A Bot with nothing pinned yet bootstraps from this deployment when
2742
+ // its first Turn is admitted; reading the current generation here would
2743
+ // materialize that bootstrap early, and a refused command must leave
2744
+ // storage exactly as it found it.
2745
+ if (
2746
+ (await this.ctx.storage.get<unknown>(COMPOSITION_CURRENT_KEY)) ===
2747
+ undefined
2748
+ )
2749
+ return;
2750
+ await resolveDeploymentCompositionV1({
2751
+ plan: await this.compileApplication(),
2752
+ composition: {
2753
+ current: () => this.authority.composition.current(),
2754
+ propose: (generation, options) =>
2755
+ this.authority.composition.propose(generation, options),
2756
+ },
2757
+ });
2758
+ } catch {
2759
+ // The mount records why; never a wedged Turn on its own.
2760
+ }
2761
+ }
2762
+
2726
2763
  /**
2727
2764
  * Resolve the User's Applet directory into this Bot's next Composition
2728
2765
  * generation, before a Turn is admitted.
@@ -3262,6 +3299,7 @@ export class ShellBotBackendContribution {
3262
3299
  fire: RoutineFireV1,
3263
3300
  ): Promise<RoutineFireOutcomeV1> {
3264
3301
  try {
3302
+ await this.followDeploymentComposition();
3265
3303
  await this.authority.run(
3266
3304
  routineTurnCommandV1(identity, fire, new Date().toISOString()),
3267
3305
  );
@@ -4197,6 +4235,7 @@ export class ShellBotBackendContribution {
4197
4235
  await this.ctx.storage.put(key, { ...context, status: "running" });
4198
4236
  let outcome: TaskOutcomeV1;
4199
4237
  try {
4238
+ await this.followDeploymentComposition();
4200
4239
  await this.authority.run({
4201
4240
  ...identity,
4202
4241
  runId: context.taskId,