@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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
compositionFailureDurableInputV1,
|
|
4
|
+
compositionFailureTurnTextV1,
|
|
5
|
+
} from "./backend-composition-input.ts";
|
|
6
|
+
|
|
7
|
+
describe("Composition failure durable Bot input", () => {
|
|
8
|
+
test("includes the authored Package, phase, diagnostics, and quarantine", () => {
|
|
9
|
+
const text = compositionFailureDurableInputV1({
|
|
10
|
+
attemptedGenerationId: "generation-broken",
|
|
11
|
+
generation: {
|
|
12
|
+
schemaVersion: 1,
|
|
13
|
+
generationId: "generation-broken",
|
|
14
|
+
artifactSetHash: "a".repeat(64),
|
|
15
|
+
createdAt: "2026-09-02T00:00:00.000Z",
|
|
16
|
+
origin: {
|
|
17
|
+
kind: "bot-authored",
|
|
18
|
+
runId: "run-1",
|
|
19
|
+
sessionId: "session-1",
|
|
20
|
+
turnId: "turn-1",
|
|
21
|
+
},
|
|
22
|
+
members: [
|
|
23
|
+
{
|
|
24
|
+
packageId: "weather-lookup",
|
|
25
|
+
specifier: "bot-authored:weather-lookup",
|
|
26
|
+
version: "0.0.1",
|
|
27
|
+
manifestHash: "b".repeat(64),
|
|
28
|
+
provenance: {
|
|
29
|
+
kind: "bot",
|
|
30
|
+
packageId: "weather-lookup",
|
|
31
|
+
version: "0.0.1",
|
|
32
|
+
botId: "bot-1",
|
|
33
|
+
sessionId: "session-1",
|
|
34
|
+
turnId: "turn-1",
|
|
35
|
+
runId: "run-1",
|
|
36
|
+
authoredAt: "2026-09-02T00:00:00.000Z",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
status: "quarantined",
|
|
41
|
+
},
|
|
42
|
+
failure: {
|
|
43
|
+
generationId: "generation-broken",
|
|
44
|
+
attempt: 3,
|
|
45
|
+
at: "2026-09-02T00:01:00.000Z",
|
|
46
|
+
phase: "health",
|
|
47
|
+
message: "declared tools do not match",
|
|
48
|
+
diagnostics: ["declared=weather_lookup", "reported=other"],
|
|
49
|
+
},
|
|
50
|
+
quarantined: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(text).toContain("Generation: generation-broken");
|
|
54
|
+
expect(text).toContain("Package: weather-lookup");
|
|
55
|
+
expect(text).toContain("Phase: health");
|
|
56
|
+
expect(text).toContain("- reported=other");
|
|
57
|
+
expect(text).toContain("Status: quarantined");
|
|
58
|
+
expect(
|
|
59
|
+
compositionFailureTurnTextV1("please continue", {
|
|
60
|
+
attemptedGenerationId: "generation-broken",
|
|
61
|
+
quarantined: false,
|
|
62
|
+
}),
|
|
63
|
+
).toEndWith("\n\nplease continue");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CompositionFailureV1 } from "@frockbot/kernel-composition/activation";
|
|
2
|
+
import type { CompositionGenerationV1 } from "@frockbot/kernel-composition/generation";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Model-visible durable input for the Turn that fell back from a broken
|
|
6
|
+
* Composition. The caller places this exact text in `user/message`, from
|
|
7
|
+
* which the exact recorded `model/request` is reconstructed.
|
|
8
|
+
*/
|
|
9
|
+
export function compositionFailureDurableInputV1(input: {
|
|
10
|
+
attemptedGenerationId: string;
|
|
11
|
+
generation?: CompositionGenerationV1;
|
|
12
|
+
failure?: CompositionFailureV1;
|
|
13
|
+
quarantined: boolean;
|
|
14
|
+
}): string {
|
|
15
|
+
const origin = input.generation?.origin;
|
|
16
|
+
const authoredPackageId =
|
|
17
|
+
origin?.kind === "bot-authored"
|
|
18
|
+
? input.generation?.members.find(
|
|
19
|
+
(member) =>
|
|
20
|
+
member.provenance.kind === "bot" &&
|
|
21
|
+
member.provenance.runId === origin.runId,
|
|
22
|
+
)?.packageId
|
|
23
|
+
: undefined;
|
|
24
|
+
const failure = input.failure;
|
|
25
|
+
const packageId =
|
|
26
|
+
authoredPackageId ??
|
|
27
|
+
failure?.message.match(/package "([^"]+)"/i)?.[1] ??
|
|
28
|
+
(input.generation?.members.filter(
|
|
29
|
+
(member) => member.provenance.kind === "bot",
|
|
30
|
+
).length === 1
|
|
31
|
+
? input.generation.members.find(
|
|
32
|
+
(member) => member.provenance.kind === "bot",
|
|
33
|
+
)?.packageId
|
|
34
|
+
: undefined);
|
|
35
|
+
const diagnostics = failure?.diagnostics.length
|
|
36
|
+
? failure.diagnostics.map((entry) => `- ${entry}`).join("\n")
|
|
37
|
+
: "- none";
|
|
38
|
+
return [
|
|
39
|
+
"[Durable Package activation failure]",
|
|
40
|
+
`Generation: ${input.attemptedGenerationId}`,
|
|
41
|
+
`Package: ${packageId ?? "unknown"}`,
|
|
42
|
+
`Phase: ${failure?.phase ?? "resolve"}`,
|
|
43
|
+
`Message: ${failure?.message ?? "This quarantined generation remains unavailable."}`,
|
|
44
|
+
"Diagnostics:",
|
|
45
|
+
diagnostics,
|
|
46
|
+
input.quarantined
|
|
47
|
+
? "Status: quarantined after repeated activation failures; the last working Package setup was mounted. Author a repair or use package_undo."
|
|
48
|
+
: "Status: activation failed; the last working Package setup was mounted for this Turn. Author a repair or use package_undo.",
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The exact command text handed to the Agent loop on a fallback Turn. */
|
|
53
|
+
export function compositionFailureTurnTextV1(
|
|
54
|
+
ordinaryInput: string,
|
|
55
|
+
failure: Parameters<typeof compositionFailureDurableInputV1>[0],
|
|
56
|
+
): string {
|
|
57
|
+
return `${compositionFailureDurableInputV1(failure)}\n\n${ordinaryInput}`;
|
|
58
|
+
}
|
|
@@ -65,6 +65,8 @@ export interface ShellIsolateMountOptions {
|
|
|
65
65
|
turnId: string;
|
|
66
66
|
loader: BotIsolateLoader;
|
|
67
67
|
artifacts: BotIsolateArtifactStore;
|
|
68
|
+
/** Reads the exact manifest whose hash the member records. */
|
|
69
|
+
manifestFor(member: CompositionMemberV1): Promise<unknown>;
|
|
68
70
|
/**
|
|
69
71
|
* Mints the loopback `CAPABILITIES` service binding for one Package —
|
|
70
72
|
* `ctx.exports.BotCapabilities({ props })` in the Durable Object.
|
|
@@ -177,12 +179,27 @@ export function createShellCompositionHost(
|
|
|
177
179
|
loader: isolate.loader,
|
|
178
180
|
artifacts: isolate.artifacts,
|
|
179
181
|
tools: runtime.root.tools,
|
|
182
|
+
loop: runtime.root,
|
|
180
183
|
userId: isolate.userId,
|
|
181
184
|
botId: options.botId,
|
|
182
185
|
sessionId: options.sessionId,
|
|
183
186
|
runId: isolate.runId,
|
|
184
187
|
turnId: isolate.turnId,
|
|
185
188
|
generationId: generation.generationId,
|
|
189
|
+
turnType: options.turnType ?? "chat",
|
|
190
|
+
...(options.subagentRole === undefined
|
|
191
|
+
? {}
|
|
192
|
+
: { subagentRole: options.subagentRole }),
|
|
193
|
+
recordHookFailure: async (failure) => {
|
|
194
|
+
const session = runtime.root.sessions.get(options.sessionId);
|
|
195
|
+
if (!session) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`session "${options.sessionId}" is unavailable for hook failure recording`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
session.append({ type: "package/hook-failed", ...failure });
|
|
201
|
+
await session.flush();
|
|
202
|
+
},
|
|
186
203
|
capabilities: isolate.capabilitiesFor(member),
|
|
187
204
|
compatibilityDate: isolate.compatibilityDate,
|
|
188
205
|
bindingDigest: isolate.bindingDigest,
|
|
@@ -192,8 +209,9 @@ export function createShellCompositionHost(
|
|
|
192
209
|
: { deadlineMs: isolate.deadlineMs }),
|
|
193
210
|
});
|
|
194
211
|
// Mount and health-check are one guarded phase (Worker Loader spike).
|
|
212
|
+
const storedManifest = await isolate.manifestFor(member);
|
|
195
213
|
const prepared = await host.prepare(
|
|
196
|
-
botIsolatePackageDescriptorV1(member),
|
|
214
|
+
await botIsolatePackageDescriptorV1(member, storedManifest),
|
|
197
215
|
);
|
|
198
216
|
if (!prepared) {
|
|
199
217
|
failures.push({
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
} from "@frockbot/configuration-core";
|
|
8
8
|
import type { PackageSettingDefinition } from "@frockbot/kernel-composition";
|
|
9
9
|
import { createShellBotBackendContribution } from "./backend.js";
|
|
10
|
+
import { createIsolateCapabilityHost } from "./backend-isolate.js";
|
|
10
11
|
|
|
11
12
|
class MemoryStorage {
|
|
12
13
|
readonly values = new Map<string, unknown>();
|
|
@@ -218,6 +219,58 @@ function request(command: BotConfigurationCommandV1) {
|
|
|
218
219
|
}
|
|
219
220
|
|
|
220
221
|
describe("Bot configuration admission", () => {
|
|
222
|
+
test("reads old Bot settings purely and writes the migrated shape on the next command", async () => {
|
|
223
|
+
const storage = new MemoryStorage();
|
|
224
|
+
// Literal durable shape from eb0283edcce5daea976a21a9f6a6414bedc6e2bc,
|
|
225
|
+
// the first parent of PR #134's merge commit.
|
|
226
|
+
const historical = {
|
|
227
|
+
schemaVersion: 1,
|
|
228
|
+
botId: "primary",
|
|
229
|
+
revision: 4,
|
|
230
|
+
profile: { name: "Primary" },
|
|
231
|
+
notifications: { enabled: true },
|
|
232
|
+
assignments: [],
|
|
233
|
+
assignmentOperations: [],
|
|
234
|
+
model: {
|
|
235
|
+
connectionId: "ollama-1",
|
|
236
|
+
providerModelId: "glm-5.3-flash:cloud",
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
await storage.put({
|
|
240
|
+
identity: { userId: "user-1", botId: "primary" },
|
|
241
|
+
"bot-configuration": historical,
|
|
242
|
+
});
|
|
243
|
+
const contribution = host(storage, configuredUser);
|
|
244
|
+
const identity = { userId: "user-1", botId: "primary" };
|
|
245
|
+
|
|
246
|
+
await expect(contribution.getSettings(identity)).resolves.toMatchObject({
|
|
247
|
+
revision: 4,
|
|
248
|
+
packageValues: {},
|
|
249
|
+
});
|
|
250
|
+
expect(await storage.get<unknown>("bot-configuration")).toEqual(historical);
|
|
251
|
+
|
|
252
|
+
await contribution.executeConfiguration(
|
|
253
|
+
request({
|
|
254
|
+
schemaVersion: 1,
|
|
255
|
+
type: "bot/update-profile",
|
|
256
|
+
commandId: "migrate-bot-settings",
|
|
257
|
+
botId: "primary",
|
|
258
|
+
expectedRevision: 4,
|
|
259
|
+
profile: { name: "Migrated Bot" },
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
const written =
|
|
263
|
+
await storage.get<Record<string, unknown>>("bot-configuration");
|
|
264
|
+
expect(written).toMatchObject({
|
|
265
|
+
revision: 5,
|
|
266
|
+
profile: { name: "Migrated Bot" },
|
|
267
|
+
packageValues: {},
|
|
268
|
+
});
|
|
269
|
+
expect(written).not.toHaveProperty("assignments");
|
|
270
|
+
expect(written).not.toHaveProperty("assignmentOperations");
|
|
271
|
+
expect(written).not.toHaveProperty("model");
|
|
272
|
+
});
|
|
273
|
+
|
|
221
274
|
test("rejects an unmaterialized Bot without writing durable state", async () => {
|
|
222
275
|
const storage = new MemoryStorage();
|
|
223
276
|
const contribution = host(storage, configuredUser);
|
|
@@ -414,6 +467,65 @@ describe("Bot configuration admission", () => {
|
|
|
414
467
|
});
|
|
415
468
|
|
|
416
469
|
describe("generic per-Turn model resolution", () => {
|
|
470
|
+
test("projects the platform model into an otherwise unconfigured Bot's isolate list", async () => {
|
|
471
|
+
const storage = new MemoryStorage();
|
|
472
|
+
const contribution = host(storage, configuredUser);
|
|
473
|
+
const identity = { userId: "user-1", botId: "primary" };
|
|
474
|
+
const settings = await contribution.materializeSettings(identity, {
|
|
475
|
+
name: "Primary",
|
|
476
|
+
});
|
|
477
|
+
const snapshot = await (
|
|
478
|
+
contribution as unknown as {
|
|
479
|
+
isolateAuthoritySnapshot(
|
|
480
|
+
identity: { userId: string; botId: string },
|
|
481
|
+
settings: BotSettingsViewV1,
|
|
482
|
+
): Promise<{
|
|
483
|
+
connections: Array<{
|
|
484
|
+
connectionId: string;
|
|
485
|
+
packageId: string;
|
|
486
|
+
connectionTypeId: string;
|
|
487
|
+
displayName: string;
|
|
488
|
+
generation: string;
|
|
489
|
+
safeMetadata: Record<string, unknown>;
|
|
490
|
+
}>;
|
|
491
|
+
model?: {
|
|
492
|
+
connectionId: string;
|
|
493
|
+
packageId: string;
|
|
494
|
+
provider: string;
|
|
495
|
+
providerModelId: string;
|
|
496
|
+
connectionGeneration: string;
|
|
497
|
+
catalogGeneration?: string;
|
|
498
|
+
};
|
|
499
|
+
memory: boolean;
|
|
500
|
+
workspace: boolean;
|
|
501
|
+
}>;
|
|
502
|
+
}
|
|
503
|
+
).isolateAuthoritySnapshot(identity, settings);
|
|
504
|
+
const listed = await createIsolateCapabilityHost({
|
|
505
|
+
storage: {
|
|
506
|
+
put: () => Promise.resolve(),
|
|
507
|
+
list: () => Promise.resolve(new Map()),
|
|
508
|
+
},
|
|
509
|
+
botId: identity.botId,
|
|
510
|
+
packageId: "bot-authored",
|
|
511
|
+
generationId: "composition-1",
|
|
512
|
+
connections: snapshot.connections,
|
|
513
|
+
...(snapshot.model ? { modelBinding: snapshot.model } : {}),
|
|
514
|
+
memory: snapshot.memory,
|
|
515
|
+
workspace: snapshot.workspace,
|
|
516
|
+
}).list();
|
|
517
|
+
|
|
518
|
+
expect(listed.model).toEqual({
|
|
519
|
+
connectionId: "flock-ai-ambient",
|
|
520
|
+
packageId: "provider-flock-ai",
|
|
521
|
+
provider: "flock-ai",
|
|
522
|
+
providerModelId: "@flock/auto",
|
|
523
|
+
connectionGeneration: "foundation-generation-1",
|
|
524
|
+
catalogGeneration: "catalog-1",
|
|
525
|
+
});
|
|
526
|
+
expect(Object.hasOwn(settings, "model")).toBe(false);
|
|
527
|
+
});
|
|
528
|
+
|
|
417
529
|
test("runs on the platform model without creating a per-Bot model record", async () => {
|
|
418
530
|
const storage = new MemoryStorage();
|
|
419
531
|
const contribution = host(storage, configuredUser);
|
|
@@ -429,11 +541,65 @@ describe("generic per-Turn model resolution", () => {
|
|
|
429
541
|
});
|
|
430
542
|
|
|
431
543
|
expect(result.text).toBe("Cordis runtime: hello");
|
|
544
|
+
const durableEvents = storage.values.get("latest-events") as
|
|
545
|
+
Array<{ seq: number }> | undefined;
|
|
546
|
+
expect(durableEvents?.length).toBeGreaterThan(0);
|
|
547
|
+
expect(durableEvents?.map((event) => event.seq)).toEqual(
|
|
548
|
+
durableEvents?.map((_, index) => index),
|
|
549
|
+
);
|
|
432
550
|
const settings = await contribution.getSettings(identity);
|
|
433
551
|
expect(settings).toMatchObject({ revision: 0, packageValues: {} });
|
|
434
552
|
expect(Object.hasOwn(settings, "model")).toBe(false);
|
|
435
553
|
});
|
|
436
554
|
|
|
555
|
+
test("a Connection disabled after admission is unavailable and records a visible failure", async () => {
|
|
556
|
+
const storage = new MemoryStorage();
|
|
557
|
+
const user = configuredUser();
|
|
558
|
+
const contribution = host(storage, () => user);
|
|
559
|
+
const identity = { userId: "user-1", botId: "primary" };
|
|
560
|
+
await contribution.materializeSettings(identity, { name: "Primary" });
|
|
561
|
+
(
|
|
562
|
+
contribution as unknown as {
|
|
563
|
+
activeTurn: unknown;
|
|
564
|
+
}
|
|
565
|
+
).activeTurn = {
|
|
566
|
+
runId: "run-1",
|
|
567
|
+
sessionId: "session-1",
|
|
568
|
+
turnId: "turn-1",
|
|
569
|
+
generationId: "composition-1",
|
|
570
|
+
mounted: {
|
|
571
|
+
generation: {
|
|
572
|
+
members: [
|
|
573
|
+
{ packageId: "bot-authored", artifact: { contentHash: "hash" } },
|
|
574
|
+
],
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
user.connections[0] = { ...user.connections[0]!, state: "disabled" };
|
|
579
|
+
|
|
580
|
+
await expect(
|
|
581
|
+
contribution.isolateConnection({
|
|
582
|
+
...identity,
|
|
583
|
+
runId: "run-1",
|
|
584
|
+
sessionId: "session-1",
|
|
585
|
+
turnId: "turn-1",
|
|
586
|
+
packageId: "bot-authored",
|
|
587
|
+
generationId: "composition-1",
|
|
588
|
+
request: "flock-ai-ambient",
|
|
589
|
+
}),
|
|
590
|
+
).resolves.toEqual({
|
|
591
|
+
status: "unavailable",
|
|
592
|
+
reason: "the Connection is unavailable",
|
|
593
|
+
});
|
|
594
|
+
await expect(contribution.listNotifications()).resolves.toEqual([
|
|
595
|
+
expect.objectContaining({
|
|
596
|
+
notificationId:
|
|
597
|
+
"package-connection-unavailable:run-1:bot-authored:flock-ai-ambient",
|
|
598
|
+
title: "Connection unavailable",
|
|
599
|
+
}),
|
|
600
|
+
]);
|
|
601
|
+
});
|
|
602
|
+
|
|
437
603
|
test("uses an enabled Bot-scoped model value and preserves it while disabled", async () => {
|
|
438
604
|
const storage = new MemoryStorage();
|
|
439
605
|
let user = configuredUser();
|
|
@@ -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" }),
|
package/src/backend-contracts.ts
CHANGED
|
@@ -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:
|
|
40
|
+
decodeConfigurationSnapshot: (stored) =>
|
|
41
|
+
decodeBotSettingsViewV1(migrateStoredBotSettingsV1(stored)),
|
|
40
42
|
});
|
|
41
43
|
|
|
42
44
|
export function requireStoredRunV1(input: unknown): StoredRun {
|
|
@@ -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
|
+
});
|