@frockbot/kernel-composition 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.
- package/package.json +2 -2
- package/src/compiler.test.ts +17 -0
- package/src/compiler.ts +12 -2
- package/src/generation.test.ts +55 -0
- package/src/generation.ts +111 -7
- package/src/index.test.ts +233 -7
- package/src/isolate-host.test.ts +342 -14
- package/src/isolate-host.ts +303 -22
- package/src/isolate-wrapper.test.ts +35 -0
- package/src/isolate-wrapper.ts +155 -32
- package/src/manifest.ts +345 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/kernel-composition",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"cordis": "4.0.0-rc.8",
|
|
22
22
|
"semver": "7.8.5",
|
|
23
|
-
"@frockbot/kernel-contracts": "0.
|
|
23
|
+
"@frockbot/kernel-contracts": "0.2.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "1.4.0",
|
package/src/compiler.test.ts
CHANGED
|
@@ -12,6 +12,7 @@ function runtimeManifest(
|
|
|
12
12
|
version?: string;
|
|
13
13
|
permissions?: string[];
|
|
14
14
|
dependencies?: Record<string, string>;
|
|
15
|
+
defaultEnablement?: "enabled" | "disabled";
|
|
15
16
|
compatibility?: string;
|
|
16
17
|
} = {},
|
|
17
18
|
) {
|
|
@@ -22,6 +23,7 @@ function runtimeManifest(
|
|
|
22
23
|
version: options.version ?? "1.0.0",
|
|
23
24
|
compatibility: { frockbot: options.compatibility ?? ">=0.0.1" },
|
|
24
25
|
dependencies: options.dependencies,
|
|
26
|
+
defaultEnablement: options.defaultEnablement,
|
|
25
27
|
contributions: { runtime: { entry: "./runtime" } },
|
|
26
28
|
permissions: options.permissions ?? [],
|
|
27
29
|
};
|
|
@@ -191,6 +193,21 @@ describe("compileApplicationPlan", () => {
|
|
|
191
193
|
);
|
|
192
194
|
});
|
|
193
195
|
|
|
196
|
+
test("allows a disabled Package to depend on a Catalog Package", async () => {
|
|
197
|
+
const plan = await compileApplicationPlan(
|
|
198
|
+
{ schemaVersion: 1, packages: [selection("@fixture/feature")] },
|
|
199
|
+
resolver({
|
|
200
|
+
"@fixture/feature": runtimeManifest("feature", {
|
|
201
|
+
dependencies: { optional: "^1.0.0" },
|
|
202
|
+
defaultEnablement: "disabled",
|
|
203
|
+
}),
|
|
204
|
+
}),
|
|
205
|
+
{ frockbotVersion: "1.0.0" },
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
expect(plan.packages[0]?.manifest.defaultEnablement).toBe("disabled");
|
|
209
|
+
});
|
|
210
|
+
|
|
194
211
|
test("validates client roots and declared outlets", async () => {
|
|
195
212
|
const clientManifest = (
|
|
196
213
|
id: string,
|
package/src/compiler.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
decodeFrockBotManifest,
|
|
3
|
+
isClientIframeContribution,
|
|
4
|
+
type FrockBotManifest,
|
|
5
|
+
} from "./manifest.ts";
|
|
2
6
|
import { satisfies, valid } from "semver";
|
|
3
7
|
|
|
4
8
|
export type JsonValue =
|
|
@@ -139,6 +143,10 @@ function orderedPackages(
|
|
|
139
143
|
).sort(([left], [right]) => left.localeCompare(right))) {
|
|
140
144
|
const dependency = packages.get(dependencyId);
|
|
141
145
|
if (!dependency) {
|
|
146
|
+
// A disabled-by-default Package may depend on a Package available only
|
|
147
|
+
// from the User's Catalog. Settings refuses to enable it until an
|
|
148
|
+
// installed dependency row exists; enabled built-ins still fail here.
|
|
149
|
+
if (pkg.manifest.defaultEnablement === "disabled") continue;
|
|
142
150
|
throw new Error(
|
|
143
151
|
`package "${pkg.id}" requires missing package "${dependencyId}"`,
|
|
144
152
|
);
|
|
@@ -162,7 +170,9 @@ function orderedPackages(
|
|
|
162
170
|
function validateClientComposition(packages: readonly CompiledPackage[]): void {
|
|
163
171
|
const clients = packages.flatMap((pkg) => {
|
|
164
172
|
const client = pkg.manifest.contributions.client;
|
|
165
|
-
return client
|
|
173
|
+
return client && !isClientIframeContribution(client)
|
|
174
|
+
? [{ id: pkg.id, client }]
|
|
175
|
+
: [];
|
|
166
176
|
});
|
|
167
177
|
const roots = clients.flatMap(({ id, client }) =>
|
|
168
178
|
client.mounts.filter((mount) => mount.slot === "root").map(() => id),
|
package/src/generation.test.ts
CHANGED
|
@@ -200,4 +200,59 @@ describe("Composition generation v1", () => {
|
|
|
200
200
|
}),
|
|
201
201
|
).toThrow("mediaType is invalid");
|
|
202
202
|
});
|
|
203
|
+
|
|
204
|
+
test("decodes a Catalog isolate member and its plain-language generation summary", async () => {
|
|
205
|
+
const generation = await bootstrap();
|
|
206
|
+
const member = {
|
|
207
|
+
packageId: "parcel-tracking",
|
|
208
|
+
specifier: "catalog:parcel-tracking",
|
|
209
|
+
version: "0.0.1",
|
|
210
|
+
manifestHash: "c".repeat(64),
|
|
211
|
+
provenance: {
|
|
212
|
+
kind: "catalog" as const,
|
|
213
|
+
packageId: "parcel-tracking",
|
|
214
|
+
version: "0.0.1",
|
|
215
|
+
catalogId: "parcel-tracking",
|
|
216
|
+
catalogGeneration: "catalog-1",
|
|
217
|
+
contentHash: "d".repeat(64),
|
|
218
|
+
},
|
|
219
|
+
artifact: {
|
|
220
|
+
contentHash: "d".repeat(64),
|
|
221
|
+
size: 512,
|
|
222
|
+
mediaType: "application/javascript" as const,
|
|
223
|
+
bundlerVersion: "catalog-test@1",
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
const decoded = decodeCompositionGenerationV1({
|
|
227
|
+
...generation,
|
|
228
|
+
members: [member],
|
|
229
|
+
artifactSetHash: await compositionArtifactSetHashV1([member]),
|
|
230
|
+
origin: {
|
|
231
|
+
kind: "bot-catalog",
|
|
232
|
+
action: "install",
|
|
233
|
+
packageId: "parcel-tracking",
|
|
234
|
+
catalogId: "parcel-tracking",
|
|
235
|
+
botId: "primary",
|
|
236
|
+
runId: "run-1",
|
|
237
|
+
sessionId: "user-1:primary",
|
|
238
|
+
turnId: "turn-1",
|
|
239
|
+
},
|
|
240
|
+
summary: "Added parcel tracking",
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(decoded.summary).toBe("Added parcel tracking");
|
|
244
|
+
expect(decoded.members[0]?.provenance.kind).toBe("catalog");
|
|
245
|
+
expect(() =>
|
|
246
|
+
decodeCompositionGenerationV1({
|
|
247
|
+
...decoded,
|
|
248
|
+
members: [{ ...member, artifact: undefined }],
|
|
249
|
+
}),
|
|
250
|
+
).toThrow("must match its Bot-isolate artifact");
|
|
251
|
+
expect(() =>
|
|
252
|
+
decodeCompositionGenerationV1({
|
|
253
|
+
...decoded,
|
|
254
|
+
summary: "Added\nparcel tracking",
|
|
255
|
+
}),
|
|
256
|
+
).toThrow("must be one trimmed line");
|
|
257
|
+
});
|
|
203
258
|
});
|
package/src/generation.ts
CHANGED
|
@@ -7,6 +7,14 @@ import { canonicalJson, sha256 } from "./compiler.ts";
|
|
|
7
7
|
|
|
8
8
|
export type PackageProvenanceV1 =
|
|
9
9
|
| { kind: "first-party"; packageId: string; version: string }
|
|
10
|
+
| {
|
|
11
|
+
kind: "catalog";
|
|
12
|
+
packageId: string;
|
|
13
|
+
version: string;
|
|
14
|
+
catalogId: string;
|
|
15
|
+
catalogGeneration: string;
|
|
16
|
+
contentHash: string;
|
|
17
|
+
}
|
|
10
18
|
| {
|
|
11
19
|
kind: "user";
|
|
12
20
|
packageId: string;
|
|
@@ -46,8 +54,25 @@ export interface CompositionMemberV1 {
|
|
|
46
54
|
export type CompositionOriginV1 =
|
|
47
55
|
| { kind: "bootstrap" }
|
|
48
56
|
| { kind: "bot-authored"; runId: string; sessionId: string; turnId: string }
|
|
57
|
+
| {
|
|
58
|
+
kind: "bot-catalog";
|
|
59
|
+
action: "install" | "update" | "remove";
|
|
60
|
+
packageId: string;
|
|
61
|
+
catalogId: string;
|
|
62
|
+
botId: string;
|
|
63
|
+
runId: string;
|
|
64
|
+
sessionId: string;
|
|
65
|
+
turnId: string;
|
|
66
|
+
}
|
|
49
67
|
| { kind: "user-install"; userId: string }
|
|
50
|
-
| { kind: "revert"; revertsTo: string; userId: string }
|
|
68
|
+
| { kind: "revert"; revertsTo: string; userId: string }
|
|
69
|
+
| {
|
|
70
|
+
kind: "revert";
|
|
71
|
+
revertsTo: string;
|
|
72
|
+
botId: string;
|
|
73
|
+
runId: string;
|
|
74
|
+
turnId: string;
|
|
75
|
+
};
|
|
51
76
|
|
|
52
77
|
export type CompositionGenerationStatusV1 =
|
|
53
78
|
"pending" | "active" | "superseded" | "failed" | "quarantined";
|
|
@@ -59,6 +84,8 @@ export interface CompositionGenerationV1 {
|
|
|
59
84
|
/** sha-256 over the canonical member list — the loader identity. */
|
|
60
85
|
artifactSetHash: string;
|
|
61
86
|
parentGenerationId?: string;
|
|
87
|
+
/** Bot-written one-line audit copy for a setup change. */
|
|
88
|
+
summary?: string;
|
|
62
89
|
createdAt: string;
|
|
63
90
|
origin: CompositionOriginV1;
|
|
64
91
|
members: CompositionMemberV1[];
|
|
@@ -83,6 +110,7 @@ export interface CompositionStore {
|
|
|
83
110
|
revert(
|
|
84
111
|
toGenerationId: string,
|
|
85
112
|
origin: Extract<CompositionOriginV1, { kind: "revert" }>,
|
|
113
|
+
options?: { createdAt?: string },
|
|
86
114
|
): Promise<CompositionGenerationV1>;
|
|
87
115
|
list(query: {
|
|
88
116
|
limit: number;
|
|
@@ -115,7 +143,7 @@ const GENERATION_REQUIRED_KEYS = [
|
|
|
115
143
|
"members",
|
|
116
144
|
"status",
|
|
117
145
|
] as const;
|
|
118
|
-
const GENERATION_OPTIONAL_KEYS = ["parentGenerationId"] as const;
|
|
146
|
+
const GENERATION_OPTIONAL_KEYS = ["parentGenerationId", "summary"] as const;
|
|
119
147
|
const MEMBER_REQUIRED_KEYS = [
|
|
120
148
|
"packageId",
|
|
121
149
|
"specifier",
|
|
@@ -132,6 +160,7 @@ const ARTIFACT_KEYS = [
|
|
|
132
160
|
] as const;
|
|
133
161
|
const MAX_COMPOSITION_MEMBERS = 512;
|
|
134
162
|
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
163
|
+
export const MAX_COMPOSITION_SUMMARY_V1 = 160;
|
|
135
164
|
|
|
136
165
|
function record(value: unknown, label: string): Record<string, unknown> {
|
|
137
166
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -195,6 +224,17 @@ function decodePackageProvenanceV1(
|
|
|
195
224
|
if (kind === "first-party") {
|
|
196
225
|
exactKeys(value, common, [], label);
|
|
197
226
|
identity();
|
|
227
|
+
} else if (kind === "catalog") {
|
|
228
|
+
exactKeys(
|
|
229
|
+
value,
|
|
230
|
+
[...common, "catalogId", "catalogGeneration", "contentHash"],
|
|
231
|
+
[],
|
|
232
|
+
label,
|
|
233
|
+
);
|
|
234
|
+
identity();
|
|
235
|
+
boundedString(value.catalogId, `${label}.catalogId`, 64);
|
|
236
|
+
boundedString(value.catalogGeneration, `${label}.catalogGeneration`, 64);
|
|
237
|
+
hashString(value.contentHash, `${label}.contentHash`);
|
|
198
238
|
} else if (kind === "user") {
|
|
199
239
|
exactKeys(value, [...common, "userId", "authoredAt"], [], label);
|
|
200
240
|
identity();
|
|
@@ -251,15 +291,25 @@ function decodeCompositionMemberV1(
|
|
|
251
291
|
if (provenance.packageId !== packageId || provenance.version !== version) {
|
|
252
292
|
throw new Error(`${label}.provenance does not match its member`);
|
|
253
293
|
}
|
|
294
|
+
const artifact =
|
|
295
|
+
value.artifact === undefined
|
|
296
|
+
? undefined
|
|
297
|
+
: decodeArtifactRefV1(value.artifact, `${label}.artifact`);
|
|
298
|
+
if (
|
|
299
|
+
provenance.kind === "catalog" &&
|
|
300
|
+
(!artifact || artifact.contentHash !== provenance.contentHash)
|
|
301
|
+
) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`${label}.catalog provenance must match its Bot-isolate artifact`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
254
306
|
return {
|
|
255
307
|
packageId,
|
|
256
308
|
specifier,
|
|
257
309
|
version,
|
|
258
310
|
manifestHash,
|
|
259
311
|
provenance,
|
|
260
|
-
...(
|
|
261
|
-
? {}
|
|
262
|
-
: { artifact: decodeArtifactRefV1(value.artifact, `${label}.artifact`) }),
|
|
312
|
+
...(artifact === undefined ? {} : { artifact }),
|
|
263
313
|
};
|
|
264
314
|
}
|
|
265
315
|
|
|
@@ -276,13 +326,54 @@ function decodeCompositionOriginV1(
|
|
|
276
326
|
boundedString(value.runId, `${label}.runId`, 128);
|
|
277
327
|
boundedString(value.sessionId, `${label}.sessionId`, 257);
|
|
278
328
|
boundedString(value.turnId, `${label}.turnId`, 128);
|
|
329
|
+
} else if (kind === "bot-catalog") {
|
|
330
|
+
exactKeys(
|
|
331
|
+
value,
|
|
332
|
+
[
|
|
333
|
+
"kind",
|
|
334
|
+
"action",
|
|
335
|
+
"packageId",
|
|
336
|
+
"catalogId",
|
|
337
|
+
"botId",
|
|
338
|
+
"runId",
|
|
339
|
+
"sessionId",
|
|
340
|
+
"turnId",
|
|
341
|
+
],
|
|
342
|
+
[],
|
|
343
|
+
label,
|
|
344
|
+
);
|
|
345
|
+
if (
|
|
346
|
+
value.action !== "install" &&
|
|
347
|
+
value.action !== "update" &&
|
|
348
|
+
value.action !== "remove"
|
|
349
|
+
) {
|
|
350
|
+
throw new Error(`${label}.action is invalid`);
|
|
351
|
+
}
|
|
352
|
+
boundedString(value.packageId, `${label}.packageId`, 128);
|
|
353
|
+
boundedString(value.catalogId, `${label}.catalogId`, 64);
|
|
354
|
+
boundedString(value.botId, `${label}.botId`, 256);
|
|
355
|
+
boundedString(value.runId, `${label}.runId`, 128);
|
|
356
|
+
boundedString(value.sessionId, `${label}.sessionId`, 257);
|
|
357
|
+
boundedString(value.turnId, `${label}.turnId`, 128);
|
|
279
358
|
} else if (kind === "user-install") {
|
|
280
359
|
exactKeys(value, ["kind", "userId"], [], label);
|
|
281
360
|
boundedString(value.userId, `${label}.userId`, 256);
|
|
282
361
|
} else if (kind === "revert") {
|
|
283
|
-
exactKeys(value, ["kind", "revertsTo", "userId"], [], label);
|
|
284
362
|
boundedString(value.revertsTo, `${label}.revertsTo`, 256);
|
|
285
|
-
|
|
363
|
+
if (Object.hasOwn(value, "userId")) {
|
|
364
|
+
exactKeys(value, ["kind", "revertsTo", "userId"], [], label);
|
|
365
|
+
boundedString(value.userId, `${label}.userId`, 256);
|
|
366
|
+
} else {
|
|
367
|
+
exactKeys(
|
|
368
|
+
value,
|
|
369
|
+
["kind", "revertsTo", "botId", "runId", "turnId"],
|
|
370
|
+
[],
|
|
371
|
+
label,
|
|
372
|
+
);
|
|
373
|
+
boundedString(value.botId, `${label}.botId`, 256);
|
|
374
|
+
boundedString(value.runId, `${label}.runId`, 128);
|
|
375
|
+
boundedString(value.turnId, `${label}.turnId`, 128);
|
|
376
|
+
}
|
|
286
377
|
} else {
|
|
287
378
|
throw new Error(`${label}.kind is invalid`);
|
|
288
379
|
}
|
|
@@ -331,6 +422,16 @@ export function decodeCompositionGenerationV1(
|
|
|
331
422
|
if (value.parentGenerationId !== undefined) {
|
|
332
423
|
boundedString(value.parentGenerationId, `${label}.parentGenerationId`, 256);
|
|
333
424
|
}
|
|
425
|
+
if (value.summary !== undefined) {
|
|
426
|
+
const summary = boundedString(
|
|
427
|
+
value.summary,
|
|
428
|
+
`${label}.summary`,
|
|
429
|
+
MAX_COMPOSITION_SUMMARY_V1,
|
|
430
|
+
);
|
|
431
|
+
if (summary.trim() !== summary || /[\r\n]/u.test(summary)) {
|
|
432
|
+
throw new Error(`${label}.summary must be one trimmed line`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
334
435
|
return {
|
|
335
436
|
schemaVersion: 1,
|
|
336
437
|
generationId,
|
|
@@ -342,6 +443,9 @@ export function decodeCompositionGenerationV1(
|
|
|
342
443
|
...(value.parentGenerationId === undefined
|
|
343
444
|
? {}
|
|
344
445
|
: { parentGenerationId: value.parentGenerationId as string }),
|
|
446
|
+
...(value.summary === undefined
|
|
447
|
+
? {}
|
|
448
|
+
: { summary: value.summary as string }),
|
|
345
449
|
};
|
|
346
450
|
}
|
|
347
451
|
|
package/src/index.test.ts
CHANGED
|
@@ -295,6 +295,84 @@ describe("PackageCatalog", () => {
|
|
|
295
295
|
});
|
|
296
296
|
|
|
297
297
|
describe("decodeFrockBotManifest", () => {
|
|
298
|
+
test("accepts iframe UI only in settings or its own declared tool-result slots", () => {
|
|
299
|
+
const manifest = {
|
|
300
|
+
schemaVersion: 3,
|
|
301
|
+
id: "weather-page",
|
|
302
|
+
displayName: "Weather page",
|
|
303
|
+
version: "0.0.1",
|
|
304
|
+
compatibility: { frockbot: ">=0.0.1" },
|
|
305
|
+
dependencies: {},
|
|
306
|
+
contributions: {
|
|
307
|
+
runtime: { entry: "./package.js", host: "bot-isolate" },
|
|
308
|
+
client: {
|
|
309
|
+
kind: "iframe",
|
|
310
|
+
artifact: {
|
|
311
|
+
contentHash: "a".repeat(64),
|
|
312
|
+
size: 123,
|
|
313
|
+
mediaType: "text/html",
|
|
314
|
+
bundlerVersion: "frockbot-inline-html@1",
|
|
315
|
+
},
|
|
316
|
+
mounts: [
|
|
317
|
+
{ slot: "frockbot.tool-result:weather_lookup" },
|
|
318
|
+
{ slot: "frockbot.bot-settings-sections", order: 10 },
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
tools: [
|
|
323
|
+
{ name: "weather_lookup", description: "Weather", inputSchema: {} },
|
|
324
|
+
],
|
|
325
|
+
hooks: ["agent/tool-exposure"],
|
|
326
|
+
permissions: [],
|
|
327
|
+
};
|
|
328
|
+
const decoded = decodeFrockBotManifest(manifest);
|
|
329
|
+
const client = decoded.contributions.client;
|
|
330
|
+
expect(client && "kind" in client ? client.kind : undefined).toBe("iframe");
|
|
331
|
+
expect(client?.mounts[0]).toEqual({
|
|
332
|
+
slot: "frockbot.tool-result:weather_lookup",
|
|
333
|
+
});
|
|
334
|
+
expect(decoded.hooks).toEqual(["agent/tool-exposure"]);
|
|
335
|
+
expect(() =>
|
|
336
|
+
decodeFrockBotManifest({
|
|
337
|
+
...manifest,
|
|
338
|
+
contributions: {
|
|
339
|
+
...manifest.contributions,
|
|
340
|
+
client: {
|
|
341
|
+
...manifest.contributions.client,
|
|
342
|
+
mounts: [{ slot: "root" }],
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
}),
|
|
346
|
+
).toThrow("not iframe-safe");
|
|
347
|
+
expect(() =>
|
|
348
|
+
decodeFrockBotManifest({
|
|
349
|
+
...manifest,
|
|
350
|
+
contributions: {
|
|
351
|
+
...manifest.contributions,
|
|
352
|
+
client: {
|
|
353
|
+
...manifest.contributions.client,
|
|
354
|
+
mounts: [{ slot: "frockbot.tool-result:package_author" }],
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
}),
|
|
358
|
+
).toThrow("undeclared tool");
|
|
359
|
+
expect(() =>
|
|
360
|
+
decodeFrockBotManifest({
|
|
361
|
+
...manifest,
|
|
362
|
+
contributions: {
|
|
363
|
+
...manifest.contributions,
|
|
364
|
+
client: {
|
|
365
|
+
...manifest.contributions.client,
|
|
366
|
+
artifact: {
|
|
367
|
+
...manifest.contributions.client.artifact,
|
|
368
|
+
size: 256 * 1024 + 1,
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
}),
|
|
373
|
+
).toThrow("256 KB quota");
|
|
374
|
+
});
|
|
375
|
+
|
|
298
376
|
test("keeps trusted Electron main execution exclusive to manifest v3", () => {
|
|
299
377
|
const contribution = {
|
|
300
378
|
desktop: {
|
|
@@ -427,11 +505,95 @@ describe("decodeFrockBotManifest", () => {
|
|
|
427
505
|
});
|
|
428
506
|
});
|
|
429
507
|
|
|
508
|
+
test("decodes the exact provider-neutral model setting role", () => {
|
|
509
|
+
const decoded = decodeFrockBotManifest({
|
|
510
|
+
...manifest("custom-models"),
|
|
511
|
+
configuration: {
|
|
512
|
+
settings: [
|
|
513
|
+
{
|
|
514
|
+
id: "model",
|
|
515
|
+
schemaVersion: 1,
|
|
516
|
+
scopes: ["user", "bot"],
|
|
517
|
+
role: "model",
|
|
518
|
+
schema: {
|
|
519
|
+
type: "object",
|
|
520
|
+
properties: {
|
|
521
|
+
connectionId: { type: "string" },
|
|
522
|
+
providerModelId: { type: "string" },
|
|
523
|
+
},
|
|
524
|
+
required: ["connectionId", "providerModelId"],
|
|
525
|
+
additionalProperties: false,
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
expect(decoded.configuration?.settings).toEqual([
|
|
533
|
+
{
|
|
534
|
+
id: "model",
|
|
535
|
+
schemaVersion: 1,
|
|
536
|
+
scopes: ["user", "bot"],
|
|
537
|
+
role: "model",
|
|
538
|
+
schema: {
|
|
539
|
+
type: "object",
|
|
540
|
+
properties: {
|
|
541
|
+
connectionId: { type: "string" },
|
|
542
|
+
providerModelId: { type: "string" },
|
|
543
|
+
},
|
|
544
|
+
required: ["connectionId", "providerModelId"],
|
|
545
|
+
additionalProperties: false,
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
]);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("rejects any other schema for a model-role setting", () => {
|
|
552
|
+
const exact = {
|
|
553
|
+
type: "object",
|
|
554
|
+
properties: {
|
|
555
|
+
connectionId: { type: "string" },
|
|
556
|
+
providerModelId: { type: "string" },
|
|
557
|
+
},
|
|
558
|
+
required: ["connectionId", "providerModelId"],
|
|
559
|
+
additionalProperties: false,
|
|
560
|
+
};
|
|
561
|
+
for (const schema of [
|
|
562
|
+
{ ...exact, additionalProperties: true },
|
|
563
|
+
{ ...exact, required: ["connectionId"] },
|
|
564
|
+
{
|
|
565
|
+
...exact,
|
|
566
|
+
properties: {
|
|
567
|
+
...exact.properties,
|
|
568
|
+
providerModelId: { type: "number" },
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
{ ...exact, title: "Choose a model" },
|
|
572
|
+
]) {
|
|
573
|
+
expect(() =>
|
|
574
|
+
decodeFrockBotManifest({
|
|
575
|
+
...manifest("custom-models"),
|
|
576
|
+
configuration: {
|
|
577
|
+
settings: [
|
|
578
|
+
{
|
|
579
|
+
id: "model",
|
|
580
|
+
schemaVersion: 1,
|
|
581
|
+
scopes: ["user"],
|
|
582
|
+
role: "model",
|
|
583
|
+
schema,
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
},
|
|
587
|
+
}),
|
|
588
|
+
).toThrow(/model setting schema must be exactly/);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
430
592
|
test("decodes an ambient-native Connection without an authorization driver", () => {
|
|
431
593
|
const decoded = decodeFrockBotManifest({
|
|
432
594
|
schemaVersion: 4,
|
|
433
|
-
id: "
|
|
434
|
-
displayName: "
|
|
595
|
+
id: "flock-ai",
|
|
596
|
+
displayName: "Flock AI",
|
|
435
597
|
version: "1.0.0",
|
|
436
598
|
compatibility: { frockbot: ">=0.0.1" },
|
|
437
599
|
contributions: { runtime: { entry: "./runtime" } },
|
|
@@ -439,18 +601,18 @@ describe("decodeFrockBotManifest", () => {
|
|
|
439
601
|
configuration: {
|
|
440
602
|
connectionTypes: [
|
|
441
603
|
{
|
|
442
|
-
id: "
|
|
443
|
-
displayName: "
|
|
604
|
+
id: "flock-ai-account",
|
|
605
|
+
displayName: "Flock AI",
|
|
444
606
|
allowMultiple: false,
|
|
445
607
|
authorization: { kind: "ambient-native" },
|
|
446
|
-
capabilities: ["
|
|
608
|
+
capabilities: ["flock-ai-models"],
|
|
447
609
|
},
|
|
448
610
|
],
|
|
449
611
|
capabilities: [
|
|
450
612
|
{
|
|
451
|
-
id: "
|
|
613
|
+
id: "flock-ai-models",
|
|
452
614
|
kind: "model",
|
|
453
|
-
connectionTypes: ["
|
|
615
|
+
connectionTypes: ["flock-ai-account"],
|
|
454
616
|
},
|
|
455
617
|
],
|
|
456
618
|
},
|
|
@@ -1043,6 +1205,70 @@ describe("decodeFrockBotManifest", () => {
|
|
|
1043
1205
|
).toThrow("manifest setting schema is too large");
|
|
1044
1206
|
});
|
|
1045
1207
|
|
|
1208
|
+
test("decodes the exact Bot isolate runtime and tool declaration", () => {
|
|
1209
|
+
const decoded = decodeFrockBotManifest({
|
|
1210
|
+
schemaVersion: 3,
|
|
1211
|
+
id: "authored",
|
|
1212
|
+
displayName: "Authored",
|
|
1213
|
+
version: "0.0.1",
|
|
1214
|
+
compatibility: { frockbot: ">=0.0.1" },
|
|
1215
|
+
dependencies: {},
|
|
1216
|
+
contributions: {
|
|
1217
|
+
runtime: { entry: "./package.js", host: "bot-isolate" },
|
|
1218
|
+
},
|
|
1219
|
+
tools: [
|
|
1220
|
+
{
|
|
1221
|
+
name: "look_up",
|
|
1222
|
+
description: "Looks up a value",
|
|
1223
|
+
inputSchema: { type: "object" },
|
|
1224
|
+
},
|
|
1225
|
+
],
|
|
1226
|
+
hooks: ["agent/tool-exposure", "tools/post-execute"],
|
|
1227
|
+
permissions: [],
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
expect(decoded.contributions.runtime?.host).toBe("bot-isolate");
|
|
1231
|
+
expect(decoded.tools?.map((tool) => tool.name)).toEqual(["look_up"]);
|
|
1232
|
+
expect(decoded.hooks).toEqual([
|
|
1233
|
+
"agent/tool-exposure",
|
|
1234
|
+
"tools/post-execute",
|
|
1235
|
+
]);
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
test("requires Bot isolate runtime and tools declarations together", () => {
|
|
1239
|
+
const base = {
|
|
1240
|
+
schemaVersion: 3,
|
|
1241
|
+
id: "authored",
|
|
1242
|
+
displayName: "Authored",
|
|
1243
|
+
version: "0.0.1",
|
|
1244
|
+
compatibility: { frockbot: ">=0.0.1" },
|
|
1245
|
+
dependencies: {},
|
|
1246
|
+
permissions: [],
|
|
1247
|
+
};
|
|
1248
|
+
expect(() =>
|
|
1249
|
+
decodeFrockBotManifest({
|
|
1250
|
+
...base,
|
|
1251
|
+
contributions: {
|
|
1252
|
+
runtime: { entry: "./package.js", host: "bot-isolate" },
|
|
1253
|
+
},
|
|
1254
|
+
}),
|
|
1255
|
+
).toThrow(/must appear together/);
|
|
1256
|
+
expect(() =>
|
|
1257
|
+
decodeFrockBotManifest({
|
|
1258
|
+
...base,
|
|
1259
|
+
contributions: { runtime: { entry: "./package.js" } },
|
|
1260
|
+
hooks: ["agent/tool-exposure"],
|
|
1261
|
+
}),
|
|
1262
|
+
).toThrow(/hooks require a bot-isolate/);
|
|
1263
|
+
expect(() =>
|
|
1264
|
+
decodeFrockBotManifest({
|
|
1265
|
+
...base,
|
|
1266
|
+
contributions: { runtime: { entry: "./package.js" } },
|
|
1267
|
+
tools: [{ name: "look_up", description: "Looks", inputSchema: {} }],
|
|
1268
|
+
}),
|
|
1269
|
+
).toThrow(/must appear together/);
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1046
1272
|
test("orders normalized contribution kinds", () => {
|
|
1047
1273
|
const decoded = decodeFrockBotManifest({
|
|
1048
1274
|
schemaVersion: 3,
|