@frockbot/plugin-shell 0.1.4 → 0.2.1
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/frockbot.json +4 -0
- package/package.json +30 -28
- package/src/agent.ts +10 -13
- package/src/backend-authoring.test.ts +356 -2
- package/src/backend-authoring.ts +585 -20
- package/src/backend-composition-input.test.ts +65 -0
- package/src/backend-composition-input.ts +58 -0
- package/src/backend-composition.ts +19 -1
- package/src/backend-configuration.test.ts +166 -0
- package/src/backend-contracts.test.ts +28 -0
- package/src/backend-contracts.ts +3 -1
- package/src/backend-iframe-ui.test.ts +131 -0
- package/src/backend-isolate.test.ts +198 -203
- package/src/backend-isolate.ts +96 -231
- package/src/backend-package-catalog.test.ts +451 -0
- package/src/backend-package-catalog.ts +923 -0
- package/src/backend-runner-iframe.test.ts +74 -0
- package/src/backend-runner.ts +192 -0
- package/src/backend.ts +947 -206
- package/src/client/FrockBotApp.vue +38 -0
- package/src/client/PackageIframeHost.vue +218 -0
- package/src/client/PackageIframeSettings.vue +52 -0
- package/src/client/SendPayloadView.vue +0 -60
- package/src/client/index.test.ts +119 -0
- package/src/client/index.ts +78 -1
- package/src/client/package-iframe-host-message.test.ts +41 -0
- package/src/client/package-iframe-host-message.ts +27 -0
- package/src/composition-views.ts +54 -0
- package/src/shared.ts +10 -0
package/frockbot.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-shell",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"./backend-image": "./src/backend-image.ts",
|
|
17
17
|
"./backend-isolate": "./src/backend-isolate.ts",
|
|
18
18
|
"./backend-memory": "./src/backend-memory.ts",
|
|
19
|
+
"./backend-package-catalog": "./src/backend-package-catalog.ts",
|
|
19
20
|
"./backend-recovery": "./src/backend-recovery.ts",
|
|
20
21
|
"./backend-runner": "./src/backend-runner.ts",
|
|
21
22
|
"./backend-skills": "./src/backend-skills.ts",
|
|
@@ -45,38 +46,39 @@
|
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@cordisjs/plugin-webui": "0.8.2",
|
|
48
|
-
"@frockbot/agent-runtime": "0.1
|
|
49
|
-
"@frockbot/application-foundation": "0.1
|
|
50
|
-
"@frockbot/catalog-core": "0.1
|
|
51
|
-
"@frockbot/client-core": "0.1
|
|
52
|
-
"@frockbot/client-ui": "0.1
|
|
53
|
-
"@frockbot/computer-core": "0.1
|
|
54
|
-
"@frockbot/computer-host-protocol": "0.1
|
|
55
|
-
"@frockbot/configuration-core": "0.1
|
|
56
|
-
"@frockbot/connection-core": "0.1
|
|
57
|
-
"@frockbot/kernel-agent-loop": "0.1
|
|
58
|
-
"@frockbot/kernel-composition": "0.1
|
|
59
|
-
"@frockbot/kernel-contracts": "0.1
|
|
60
|
-
"@frockbot/kernel-do": "0.1
|
|
61
|
-
"@frockbot/machine-protocol": "0.1
|
|
62
|
-
"@frockbot/plugin-authoring": "0.1
|
|
63
|
-
"@frockbot/plugin-
|
|
64
|
-
"@frockbot/plugin-
|
|
65
|
-
"@frockbot/plugin-
|
|
66
|
-
"@frockbot/plugin-
|
|
67
|
-
"@frockbot/plugin-
|
|
68
|
-
"@frockbot/plugin-
|
|
69
|
-
"@frockbot/plugin-
|
|
70
|
-
"@frockbot/plugin-
|
|
71
|
-
"@frockbot/plugin-
|
|
72
|
-
"@frockbot/plugin-
|
|
73
|
-
"@frockbot/
|
|
49
|
+
"@frockbot/agent-runtime": "0.2.1",
|
|
50
|
+
"@frockbot/application-foundation": "0.2.1",
|
|
51
|
+
"@frockbot/catalog-core": "0.2.1",
|
|
52
|
+
"@frockbot/client-core": "0.2.1",
|
|
53
|
+
"@frockbot/client-ui": "0.2.1",
|
|
54
|
+
"@frockbot/computer-core": "0.2.1",
|
|
55
|
+
"@frockbot/computer-host-protocol": "0.2.1",
|
|
56
|
+
"@frockbot/configuration-core": "0.2.1",
|
|
57
|
+
"@frockbot/connection-core": "0.2.1",
|
|
58
|
+
"@frockbot/kernel-agent-loop": "0.2.1",
|
|
59
|
+
"@frockbot/kernel-composition": "0.2.1",
|
|
60
|
+
"@frockbot/kernel-contracts": "0.2.1",
|
|
61
|
+
"@frockbot/kernel-do": "0.2.1",
|
|
62
|
+
"@frockbot/machine-protocol": "0.2.1",
|
|
63
|
+
"@frockbot/plugin-authoring": "0.2.1",
|
|
64
|
+
"@frockbot/plugin-package-catalog": "0.2.1",
|
|
65
|
+
"@frockbot/plugin-bot-template": "0.2.1",
|
|
66
|
+
"@frockbot/plugin-flock": "0.2.1",
|
|
67
|
+
"@frockbot/plugin-image": "0.2.1",
|
|
68
|
+
"@frockbot/plugin-machine-messages": "0.2.1",
|
|
69
|
+
"@frockbot/plugin-mcp": "0.2.1",
|
|
70
|
+
"@frockbot/plugin-memory": "0.2.1",
|
|
71
|
+
"@frockbot/plugin-routines": "0.2.1",
|
|
72
|
+
"@frockbot/plugin-skills": "0.2.1",
|
|
73
|
+
"@frockbot/plugin-subagents": "0.2.1",
|
|
74
|
+
"@frockbot/plugin-user-machine": "0.2.1",
|
|
75
|
+
"@frockbot/protocol": "0.2.1",
|
|
74
76
|
"cordis": "4.0.0-rc.8",
|
|
75
77
|
"vue": "3.5.41"
|
|
76
78
|
},
|
|
77
79
|
"devDependencies": {
|
|
78
80
|
"@cloudflare/workers-types": "latest",
|
|
79
|
-
"@frockbot/plugin-tools": "0.1
|
|
81
|
+
"@frockbot/plugin-tools": "0.2.1",
|
|
80
82
|
"@types/bun": "1.3.6",
|
|
81
83
|
"@types/node": "26.2.0",
|
|
82
84
|
"@vitejs/plugin-vue": "6.0.8",
|
package/src/agent.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// `widget` payload ends the Turn; row 53's `approval` payload is the only
|
|
10
10
|
// other one that does, and for the same reason — the Bot has nothing left
|
|
11
11
|
// to do until a person answers.
|
|
12
|
-
// 3. One `agent/
|
|
12
|
+
// 3. One `agent/message-window` handler, which is where the transcript seam is: a
|
|
13
13
|
// chat Turn's request carries only chat Turns, and an automation Turn's
|
|
14
14
|
// carries its own Turn and a pointer to the parent it may not read. See
|
|
15
15
|
// `history.ts`.
|
|
@@ -120,8 +120,6 @@ function sendAcknowledgement(payload: SendToUserPayloadV1): string {
|
|
|
120
120
|
return "Secret request sent to the user.";
|
|
121
121
|
case "agent-card":
|
|
122
122
|
return "Agent card sent to the user.";
|
|
123
|
-
case "connect-card":
|
|
124
|
-
return "Connect card sent to the user. Only they can complete the authorization; you have recorded the request, not granted it.";
|
|
125
123
|
case "approval":
|
|
126
124
|
// Deliberately not "requested permission": nothing has been granted, and
|
|
127
125
|
// the Turn is over whatever the answer turns out to be.
|
|
@@ -137,7 +135,6 @@ const SEND_TO_USER_DESCRIPTION = [
|
|
|
137
135
|
'{"type":"widget","widget":{"prompt":"…","helpText":"…","options":["…"],"allowCustom":false,"dismissOnMoveOn":false}}',
|
|
138
136
|
'{"type":"secret-request","prompt":"…","secretName":"…"}',
|
|
139
137
|
'{"type":"agent-card","agentId":"…","title":"…","body":"…"}',
|
|
140
|
-
'{"type":"connect-card","connectionId":"…","title":"…","body":"…"}',
|
|
141
138
|
'{"type":"approval","approvalId":"…","action":"…","rationale":"…","risk":"low|medium|high","expiresInSeconds":86400}',
|
|
142
139
|
"A widget asks the user a question with 1 to 6 options and ends your Turn;",
|
|
143
140
|
"their answer arrives as a new Turn. An approval asks the user to allow one",
|
|
@@ -313,18 +310,18 @@ export const shellAgentPlugin: Plugin.Function = (ctx) => {
|
|
|
313
310
|
// Applied after the rest of the chain, so this Package has the last word on
|
|
314
311
|
// what history a request carries — the one rule the visible transcript
|
|
315
312
|
// rests on.
|
|
316
|
-
ctx.on(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
313
|
+
ctx.on(
|
|
314
|
+
"agent/message-window",
|
|
315
|
+
async (agent, _messages, _turn, _step, _signal, next) => {
|
|
316
|
+
const proposed = await next();
|
|
317
|
+
return turnScopedMessagesV1({
|
|
321
318
|
events: agent.session.events,
|
|
322
|
-
messages: proposed
|
|
319
|
+
messages: proposed,
|
|
323
320
|
pointer: automationParentPointerV1,
|
|
324
321
|
sessionId: agent.session.id,
|
|
325
|
-
})
|
|
326
|
-
}
|
|
327
|
-
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
),
|
|
328
325
|
];
|
|
329
326
|
return () => {
|
|
330
327
|
for (const dispose of disposers.toReversed()) dispose();
|
|
@@ -7,17 +7,23 @@ import {
|
|
|
7
7
|
type CompositionGenerationV1,
|
|
8
8
|
type CompositionMemberV1,
|
|
9
9
|
} from "@frockbot/kernel-composition/generation";
|
|
10
|
+
import { decodeFrockBotManifest } from "@frockbot/kernel-composition";
|
|
10
11
|
import type { PackageBundleResultV1 } from "@frockbot/kernel-contracts";
|
|
11
12
|
import {
|
|
12
13
|
artifactKey,
|
|
13
14
|
authorshipArtifactKey,
|
|
14
15
|
authorshipFailureKey,
|
|
15
16
|
authorshipIntentKey,
|
|
17
|
+
authorshipManifestKey,
|
|
18
|
+
authorshipUndoIntentKey,
|
|
19
|
+
authorshipUndoOutcomeKey,
|
|
16
20
|
AUTHORSHIP_FAILURE_PREFIX,
|
|
17
21
|
type AuthoredArtifactRecordV1,
|
|
22
|
+
type AuthoredManifestRecordV1,
|
|
18
23
|
type AuthoringFailureRecordV1,
|
|
19
24
|
type AuthorPackageRequestV1,
|
|
20
25
|
type AuthorshipIntentV1,
|
|
26
|
+
sha256HexV1,
|
|
21
27
|
} from "@frockbot/plugin-authoring";
|
|
22
28
|
import {
|
|
23
29
|
createPackageAuthoringHost,
|
|
@@ -34,7 +40,7 @@ function requestFor(overrides: Partial<AuthorPackageRequestV1> = {}) {
|
|
|
34
40
|
input: {
|
|
35
41
|
packageId: "hello-world",
|
|
36
42
|
displayName: "Hello world",
|
|
37
|
-
|
|
43
|
+
tools: [{ name: "hello", description: "Says hi", inputSchema: {} }],
|
|
38
44
|
source: SOURCE,
|
|
39
45
|
},
|
|
40
46
|
sourceHash: "a".repeat(64),
|
|
@@ -56,6 +62,12 @@ function memoryStorage() {
|
|
|
56
62
|
}
|
|
57
63
|
return Promise.resolve();
|
|
58
64
|
},
|
|
65
|
+
list: <T>({ prefix }: { prefix: string }) =>
|
|
66
|
+
Promise.resolve(
|
|
67
|
+
new Map(
|
|
68
|
+
[...values.entries()].filter(([key]) => key.startsWith(prefix)),
|
|
69
|
+
) as Map<string, T>,
|
|
70
|
+
),
|
|
59
71
|
};
|
|
60
72
|
return storage;
|
|
61
73
|
}
|
|
@@ -79,13 +91,28 @@ async function memoryComposition() {
|
|
|
79
91
|
[bootstrap.generationId, bootstrap],
|
|
80
92
|
]);
|
|
81
93
|
let current = bootstrap.generationId;
|
|
94
|
+
let lastKnownGood = bootstrap.generationId;
|
|
82
95
|
const store: AuthoringCompositionStore & {
|
|
83
96
|
generations: Map<string, CompositionGenerationV1>;
|
|
84
97
|
currentId(): string;
|
|
98
|
+
activate(generationId: string): void;
|
|
99
|
+
fail(generationId: string): void;
|
|
85
100
|
} = {
|
|
86
101
|
generations,
|
|
87
102
|
currentId: () => current,
|
|
88
103
|
current: () => Promise.resolve(generations.get(current)!),
|
|
104
|
+
lastKnownGood: () => Promise.resolve(generations.get(lastKnownGood)!),
|
|
105
|
+
activate: (generationId) => {
|
|
106
|
+
const generation = generations.get(generationId)!;
|
|
107
|
+
generations.set(generationId, { ...generation, status: "active" });
|
|
108
|
+
current = generationId;
|
|
109
|
+
lastKnownGood = generationId;
|
|
110
|
+
},
|
|
111
|
+
fail: (generationId) => {
|
|
112
|
+
const generation = generations.get(generationId)!;
|
|
113
|
+
generations.set(generationId, { ...generation, status: "failed" });
|
|
114
|
+
current = generationId;
|
|
115
|
+
},
|
|
89
116
|
read: (generationId) => Promise.resolve(generations.get(generationId)),
|
|
90
117
|
propose: (generation, options) => {
|
|
91
118
|
if (generations.has(generation.generationId)) {
|
|
@@ -96,6 +123,33 @@ async function memoryComposition() {
|
|
|
96
123
|
return Promise.resolve();
|
|
97
124
|
},
|
|
98
125
|
retainedCount: () => Promise.resolve(generations.size),
|
|
126
|
+
list: ({ limit }) =>
|
|
127
|
+
Promise.resolve({
|
|
128
|
+
generations: [...generations.values()].reverse().slice(0, limit),
|
|
129
|
+
}),
|
|
130
|
+
revert: async (toGenerationId, origin, options) => {
|
|
131
|
+
const target = generations.get(toGenerationId);
|
|
132
|
+
if (!target) throw new Error("unknown target");
|
|
133
|
+
const createdAt = options?.createdAt ?? "2026-08-31T02:00:00.000Z";
|
|
134
|
+
const generation = decodeCompositionGenerationV1({
|
|
135
|
+
schemaVersion: 1,
|
|
136
|
+
generationId: compositionGenerationIdV1(
|
|
137
|
+
createdAt,
|
|
138
|
+
target.artifactSetHash,
|
|
139
|
+
),
|
|
140
|
+
artifactSetHash: target.artifactSetHash,
|
|
141
|
+
parentGenerationId: current,
|
|
142
|
+
createdAt,
|
|
143
|
+
origin,
|
|
144
|
+
members: target.members,
|
|
145
|
+
status: "pending",
|
|
146
|
+
});
|
|
147
|
+
const existing = generations.get(generation.generationId);
|
|
148
|
+
if (existing) return existing;
|
|
149
|
+
generations.set(generation.generationId, generation);
|
|
150
|
+
current = generation.generationId;
|
|
151
|
+
return generation;
|
|
152
|
+
},
|
|
99
153
|
};
|
|
100
154
|
return store;
|
|
101
155
|
}
|
|
@@ -160,12 +214,18 @@ describe("a Bot authoring a Package", () => {
|
|
|
160
214
|
let storage: ReturnType<typeof memoryStorage>;
|
|
161
215
|
let composition: Awaited<ReturnType<typeof memoryComposition>>;
|
|
162
216
|
let written: string[];
|
|
217
|
+
let writtenSources: string[];
|
|
218
|
+
let writtenUi: string[];
|
|
219
|
+
let sourceContents: Map<string, string>;
|
|
163
220
|
let reservations: unknown[];
|
|
164
221
|
|
|
165
222
|
beforeEach(async () => {
|
|
166
223
|
storage = memoryStorage();
|
|
167
224
|
composition = await memoryComposition();
|
|
168
225
|
written = [];
|
|
226
|
+
writtenSources = [];
|
|
227
|
+
writtenUi = [];
|
|
228
|
+
sourceContents = new Map();
|
|
169
229
|
reservations = [];
|
|
170
230
|
});
|
|
171
231
|
|
|
@@ -173,6 +233,7 @@ describe("a Bot authoring a Package", () => {
|
|
|
173
233
|
bundler: ReturnType<typeof countingBundler>;
|
|
174
234
|
quota?: (request: unknown) => unknown;
|
|
175
235
|
runId?: string;
|
|
236
|
+
currentToolNames?: readonly string[];
|
|
176
237
|
}) {
|
|
177
238
|
let ids = 0;
|
|
178
239
|
return createPackageAuthoringHost({
|
|
@@ -184,6 +245,17 @@ describe("a Bot authoring a Package", () => {
|
|
|
184
245
|
written.push(contentHash);
|
|
185
246
|
return Promise.resolve();
|
|
186
247
|
},
|
|
248
|
+
putPackageSource: (sourceHash, source) => {
|
|
249
|
+
writtenSources.push(sourceHash);
|
|
250
|
+
sourceContents.set(sourceHash, source);
|
|
251
|
+
return Promise.resolve();
|
|
252
|
+
},
|
|
253
|
+
putPackageUiArtifact: (contentHash) => {
|
|
254
|
+
writtenUi.push(contentHash);
|
|
255
|
+
return Promise.resolve();
|
|
256
|
+
},
|
|
257
|
+
loadPackageSource: (sourceHash) =>
|
|
258
|
+
Promise.resolve(sourceContents.get(sourceHash)),
|
|
187
259
|
headPackageArtifact: () => Promise.resolve(undefined),
|
|
188
260
|
},
|
|
189
261
|
quota: {
|
|
@@ -206,6 +278,7 @@ describe("a Bot authoring a Package", () => {
|
|
|
206
278
|
runId: options.runId ?? "run-1",
|
|
207
279
|
turnId: options.runId ?? "run-1",
|
|
208
280
|
compatibilityDate: "2026-08-27",
|
|
281
|
+
currentToolNames: () => options.currentToolNames ?? [],
|
|
209
282
|
now: () => new Date("2026-08-31T01:00:00.000Z"),
|
|
210
283
|
newId: () => `id-${(ids += 1)}`,
|
|
211
284
|
});
|
|
@@ -242,7 +315,93 @@ describe("a Bot authoring a Package", () => {
|
|
|
242
315
|
authoredAt: "2026-08-31T01:00:00.000Z",
|
|
243
316
|
});
|
|
244
317
|
expect(artifact.r2Key).toBe(`packages/${"b".repeat(64)}.mjs`);
|
|
318
|
+
expect(artifact.sourceR2Key).toBe(`packages/${"a".repeat(64)}.ts`);
|
|
319
|
+
expect(artifact.sourceHash).toBe("a".repeat(64));
|
|
245
320
|
expect(written).toEqual(["b".repeat(64)]);
|
|
321
|
+
expect(writtenSources).toEqual(["a".repeat(64)]);
|
|
322
|
+
const manifestRecord = storage.values.get(
|
|
323
|
+
authorshipManifestKey(artifact.manifestHash),
|
|
324
|
+
) as AuthoredManifestRecordV1;
|
|
325
|
+
expect(decodeFrockBotManifest(manifestRecord.manifest)).toMatchObject({
|
|
326
|
+
id: "hello-world",
|
|
327
|
+
version: "0.0.1",
|
|
328
|
+
contributions: { runtime: { host: "bot-isolate" } },
|
|
329
|
+
tools: [{ name: "hello" }],
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("stores declared loop hooks in the authored manifest", async () => {
|
|
334
|
+
const bundler = countingBundler((effectId) => bundledResult(effectId));
|
|
335
|
+
const outcome = await host({ bundler }).author(
|
|
336
|
+
requestFor({
|
|
337
|
+
input: {
|
|
338
|
+
...requestFor().input,
|
|
339
|
+
hooks: ["agent/tool-exposure", "tools/post-execute"],
|
|
340
|
+
},
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
if (outcome.status !== "authored") throw new Error(outcome.reason);
|
|
344
|
+
|
|
345
|
+
const artifact = storage.values.get(
|
|
346
|
+
artifactKey(outcome.contentHash),
|
|
347
|
+
) as AuthoredArtifactRecordV1;
|
|
348
|
+
const manifestRecord = storage.values.get(
|
|
349
|
+
authorshipManifestKey(artifact.manifestHash),
|
|
350
|
+
) as AuthoredManifestRecordV1;
|
|
351
|
+
|
|
352
|
+
expect(manifestRecord.manifest).toMatchObject({
|
|
353
|
+
hooks: ["agent/tool-exposure", "tools/post-execute"],
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("stores the second immutable artifact and records it in the same manifest", async () => {
|
|
358
|
+
const html = "<!doctype html><h1>Hello</h1>";
|
|
359
|
+
const contentHash = await sha256HexV1(html);
|
|
360
|
+
const bundler = countingBundler((effectId) => ({
|
|
361
|
+
...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,
|
|
369
|
+
}));
|
|
370
|
+
const request = requestFor();
|
|
371
|
+
const outcome = await host({ bundler }).author({
|
|
372
|
+
...request,
|
|
373
|
+
input: {
|
|
374
|
+
...request.input,
|
|
375
|
+
hooks: ["agent/tool-exposure", "tools/post-execute"],
|
|
376
|
+
ui: {
|
|
377
|
+
html,
|
|
378
|
+
mounts: [{ slot: "frockbot.tool-result:hello" }],
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
expect(outcome.status).toBe("authored");
|
|
383
|
+
expect(writtenUi).toEqual([contentHash]);
|
|
384
|
+
const artifact = storage.values.get(
|
|
385
|
+
artifactKey("b".repeat(64)),
|
|
386
|
+
) as AuthoredArtifactRecordV1;
|
|
387
|
+
const stored = storage.values.get(
|
|
388
|
+
authorshipManifestKey(artifact.manifestHash),
|
|
389
|
+
) as AuthoredManifestRecordV1;
|
|
390
|
+
const manifest = decodeFrockBotManifest(stored.manifest);
|
|
391
|
+
expect(manifest.contributions.client).toEqual({
|
|
392
|
+
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" }],
|
|
400
|
+
});
|
|
401
|
+
expect(manifest.hooks).toEqual([
|
|
402
|
+
"agent/tool-exposure",
|
|
403
|
+
"tools/post-execute",
|
|
404
|
+
]);
|
|
246
405
|
});
|
|
247
406
|
|
|
248
407
|
test("proposes a pending generation pinned for the next Turn", async () => {
|
|
@@ -287,7 +446,13 @@ describe("a Bot authoring a Package", () => {
|
|
|
287
446
|
),
|
|
288
447
|
);
|
|
289
448
|
const first = await host({ bundler }).author(requestFor());
|
|
290
|
-
|
|
449
|
+
if (first.status !== "authored") throw new Error(first.reason);
|
|
450
|
+
composition.activate(first.generationId);
|
|
451
|
+
const second = await host({
|
|
452
|
+
bundler,
|
|
453
|
+
runId: "run-2",
|
|
454
|
+
currentToolNames: ["hello"],
|
|
455
|
+
}).author(
|
|
291
456
|
requestFor({ effectId: "author-second", sourceHash: "d".repeat(64) }),
|
|
292
457
|
);
|
|
293
458
|
if (first.status !== "authored" || second.status !== "authored") {
|
|
@@ -318,6 +483,105 @@ describe("a Bot authoring a Package", () => {
|
|
|
318
483
|
).toBe(first.generationId);
|
|
319
484
|
});
|
|
320
485
|
|
|
486
|
+
test("a failed pending generation is never inherited by later authoring", async () => {
|
|
487
|
+
const bundler = countingBundler((effectId) =>
|
|
488
|
+
bundledResult(
|
|
489
|
+
effectId,
|
|
490
|
+
effectId === "author-first" ? "b".repeat(64) : "c".repeat(64),
|
|
491
|
+
),
|
|
492
|
+
);
|
|
493
|
+
const first = await host({ bundler, runId: "run-1" }).author(
|
|
494
|
+
requestFor({ effectId: "author-first" }),
|
|
495
|
+
);
|
|
496
|
+
if (first.status !== "authored") throw new Error(first.reason);
|
|
497
|
+
composition.fail(first.generationId);
|
|
498
|
+
|
|
499
|
+
const second = await host({ bundler, runId: "run-2" }).author(
|
|
500
|
+
requestFor({
|
|
501
|
+
effectId: "author-second",
|
|
502
|
+
sourceHash: "d".repeat(64),
|
|
503
|
+
input: {
|
|
504
|
+
...requestFor().input,
|
|
505
|
+
packageId: "second-package",
|
|
506
|
+
tools: [
|
|
507
|
+
{ name: "second_tool", description: "Second", inputSchema: {} },
|
|
508
|
+
],
|
|
509
|
+
},
|
|
510
|
+
}),
|
|
511
|
+
);
|
|
512
|
+
if (second.status !== "authored") throw new Error(second.reason);
|
|
513
|
+
const generation = composition.generations.get(second.generationId)!;
|
|
514
|
+
expect(generation.parentGenerationId).toBe(
|
|
515
|
+
(await composition.lastKnownGood()).generationId,
|
|
516
|
+
);
|
|
517
|
+
expect(generation.members.map((member) => member.packageId)).toEqual([
|
|
518
|
+
"second-package",
|
|
519
|
+
"shell",
|
|
520
|
+
]);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test("re-authoring the same package repairs a failed proposal from last-known-good", async () => {
|
|
524
|
+
const bundler = countingBundler((effectId) =>
|
|
525
|
+
bundledResult(
|
|
526
|
+
effectId,
|
|
527
|
+
effectId === "author-broken" ? "b".repeat(64) : "c".repeat(64),
|
|
528
|
+
),
|
|
529
|
+
);
|
|
530
|
+
const broken = await host({ bundler, runId: "run-broken" }).author(
|
|
531
|
+
requestFor({ effectId: "author-broken" }),
|
|
532
|
+
);
|
|
533
|
+
if (broken.status !== "authored") throw new Error(broken.reason);
|
|
534
|
+
composition.fail(broken.generationId);
|
|
535
|
+
|
|
536
|
+
const repaired = await host({ bundler, runId: "run-repair" }).author(
|
|
537
|
+
requestFor({
|
|
538
|
+
effectId: "author-repair",
|
|
539
|
+
sourceHash: "d".repeat(64),
|
|
540
|
+
input: { ...requestFor().input, source: `${SOURCE}// repaired\n` },
|
|
541
|
+
}),
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
if (repaired.status !== "authored") throw new Error(repaired.reason);
|
|
545
|
+
const generation = composition.generations.get(repaired.generationId)!;
|
|
546
|
+
expect(generation.parentGenerationId).toBe(
|
|
547
|
+
(await composition.lastKnownGood()).generationId,
|
|
548
|
+
);
|
|
549
|
+
expect(
|
|
550
|
+
generation.members.find((member) => member.packageId === "hello-world")
|
|
551
|
+
?.version,
|
|
552
|
+
).toBe("0.0.2");
|
|
553
|
+
expect(generation.members).toHaveLength(2);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("a registered tool-name collision is refused before quota or bundling", async () => {
|
|
557
|
+
const bundler = countingBundler((effectId) => bundledResult(effectId));
|
|
558
|
+
const outcome = await host({
|
|
559
|
+
bundler,
|
|
560
|
+
currentToolNames: ["memory_write", "package_author"],
|
|
561
|
+
}).author(
|
|
562
|
+
requestFor({
|
|
563
|
+
input: {
|
|
564
|
+
...requestFor().input,
|
|
565
|
+
tools: [
|
|
566
|
+
{
|
|
567
|
+
name: "memory_write",
|
|
568
|
+
description: "Would collide",
|
|
569
|
+
inputSchema: {},
|
|
570
|
+
},
|
|
571
|
+
],
|
|
572
|
+
},
|
|
573
|
+
}),
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
expect(outcome.status).toBe("refused");
|
|
577
|
+
if (outcome.status !== "refused") throw new Error("unreachable");
|
|
578
|
+
expect(outcome.reason).toContain(
|
|
579
|
+
'Tool "memory_write" is already registered',
|
|
580
|
+
);
|
|
581
|
+
expect(reservations).toEqual([]);
|
|
582
|
+
expect(bundler.calls).toBe(0);
|
|
583
|
+
});
|
|
584
|
+
|
|
321
585
|
test("shadowing a first-party Package is refused before any durable effect", async () => {
|
|
322
586
|
const bundler = countingBundler((effectId) => bundledResult(effectId));
|
|
323
587
|
const outcome = await host({ bundler }).author(
|
|
@@ -440,6 +704,7 @@ describe("a Bot authoring a Package", () => {
|
|
|
440
704
|
packageId: "hello-world",
|
|
441
705
|
version: "0.0.1",
|
|
442
706
|
sourceHash: "a".repeat(64),
|
|
707
|
+
manifestHash: "f".repeat(64),
|
|
443
708
|
sourceBytes: SOURCE.length,
|
|
444
709
|
recordedAt: "2026-08-31T00:30:00.000Z",
|
|
445
710
|
status: "recorded",
|
|
@@ -471,6 +736,7 @@ describe("a Bot authoring a Package", () => {
|
|
|
471
736
|
packageId: "hello-world",
|
|
472
737
|
version: "0.0.1",
|
|
473
738
|
sourceHash: "a".repeat(64),
|
|
739
|
+
manifestHash: "f".repeat(64),
|
|
474
740
|
sourceBytes: SOURCE.length,
|
|
475
741
|
recordedAt: "2026-08-31T00:30:00.000Z",
|
|
476
742
|
status: "recorded",
|
|
@@ -515,4 +781,92 @@ describe("a Bot authoring a Package", () => {
|
|
|
515
781
|
expect(outcome.status).toBe("refused");
|
|
516
782
|
expect(composition.generations.size).toBe(1);
|
|
517
783
|
});
|
|
784
|
+
|
|
785
|
+
test("package undo records intent before an idempotent Bot-origin revert without moving last-known-good", async () => {
|
|
786
|
+
const bundler = countingBundler((effectId) => bundledResult(effectId));
|
|
787
|
+
const authored = await host({ bundler }).author(requestFor());
|
|
788
|
+
if (authored.status !== "authored") throw new Error("authoring failed");
|
|
789
|
+
composition.activate(authored.generationId);
|
|
790
|
+
const goodBeforeUndo = await composition.lastKnownGood();
|
|
791
|
+
const undoHost = host({ bundler, runId: "run-undo" });
|
|
792
|
+
const effectId = await undoHost.undoEffectIdFor({});
|
|
793
|
+
const request = {
|
|
794
|
+
input: {},
|
|
795
|
+
effectId,
|
|
796
|
+
sessionId: "user-1:bot-1",
|
|
797
|
+
position: { turn: 2, step: 1 },
|
|
798
|
+
} as const;
|
|
799
|
+
|
|
800
|
+
const first = await undoHost.undo(request);
|
|
801
|
+
const replay = await undoHost.undo(request);
|
|
802
|
+
|
|
803
|
+
expect(first).toEqual(replay);
|
|
804
|
+
expect(first.status).toBe("recorded");
|
|
805
|
+
if (first.status !== "recorded") throw new Error("undo was refused");
|
|
806
|
+
const reverted = composition.generations.get(first.generationId)!;
|
|
807
|
+
expect(reverted.origin).toEqual({
|
|
808
|
+
kind: "revert",
|
|
809
|
+
revertsTo: first.targetGenerationId,
|
|
810
|
+
botId: "bot-1",
|
|
811
|
+
runId: "run-undo",
|
|
812
|
+
turnId: "run-undo",
|
|
813
|
+
});
|
|
814
|
+
expect(reverted.members.map((member) => member.packageId)).toEqual([
|
|
815
|
+
"shell",
|
|
816
|
+
]);
|
|
817
|
+
expect((await composition.lastKnownGood()).generationId).toBe(
|
|
818
|
+
goodBeforeUndo.generationId,
|
|
819
|
+
);
|
|
820
|
+
expect(storage.values.has(authorshipUndoIntentKey(effectId))).toBe(true);
|
|
821
|
+
expect(storage.values.has(authorshipUndoOutcomeKey(effectId))).toBe(true);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test("package_inspect_self returns generated contract, declared tools, stored source, and latest failure", async () => {
|
|
825
|
+
const bundler = countingBundler((effectId) => bundledResult(effectId));
|
|
826
|
+
const authored = await host({ bundler }).author(requestFor());
|
|
827
|
+
if (authored.status !== "authored") throw new Error("authoring failed");
|
|
828
|
+
composition.activate(authored.generationId);
|
|
829
|
+
const inspecting = host({
|
|
830
|
+
bundler,
|
|
831
|
+
runId: "run-inspect",
|
|
832
|
+
currentToolNames: ["hello", "memory_write"],
|
|
833
|
+
});
|
|
834
|
+
await inspecting.author(
|
|
835
|
+
requestFor({
|
|
836
|
+
effectId: "author-collision",
|
|
837
|
+
sourceHash: "c".repeat(64),
|
|
838
|
+
input: {
|
|
839
|
+
...requestFor().input,
|
|
840
|
+
tools: [
|
|
841
|
+
{
|
|
842
|
+
name: "memory_write",
|
|
843
|
+
description: "collides",
|
|
844
|
+
inputSchema: {},
|
|
845
|
+
},
|
|
846
|
+
],
|
|
847
|
+
},
|
|
848
|
+
}),
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
const view = await inspecting.inspectSelf();
|
|
852
|
+
|
|
853
|
+
expect(view.contextContract).toContain(
|
|
854
|
+
"interface BotPackageExecutionContextV1",
|
|
855
|
+
);
|
|
856
|
+
expect(view.contextContract).toContain("interface FrockBotIframeBridgeV1");
|
|
857
|
+
expect(view.contextContract).toContain("window.frockbot=");
|
|
858
|
+
expect(view.composition.members).toContainEqual(
|
|
859
|
+
expect.objectContaining({
|
|
860
|
+
packageId: "hello-world",
|
|
861
|
+
declaredTools: ["hello"],
|
|
862
|
+
source: SOURCE,
|
|
863
|
+
}),
|
|
864
|
+
);
|
|
865
|
+
expect(view.failures).toContainEqual(
|
|
866
|
+
expect.objectContaining({
|
|
867
|
+
packageId: "hello-world",
|
|
868
|
+
authoring: expect.objectContaining({ phase: "compose" }),
|
|
869
|
+
}),
|
|
870
|
+
);
|
|
871
|
+
});
|
|
518
872
|
});
|