@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/src/manifest.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isBotIsolateHookEventNameV1,
|
|
3
|
+
decodeTurnTypeV1,
|
|
4
|
+
type BotIsolateHookEventNameV1,
|
|
5
|
+
type TurnTypeV1,
|
|
6
|
+
} from "@frockbot/kernel-contracts";
|
|
2
7
|
|
|
3
8
|
/** The Contribution kinds a Package manifest can declare. */
|
|
4
9
|
export type ManifestContributionKind =
|
|
5
10
|
"backend" | "runtime" | "client" | "desktop" | "mobile";
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
|
-
* Every execution host a Contribution can be mounted in.
|
|
9
|
-
* manifest
|
|
10
|
-
*
|
|
11
|
-
*
|
|
13
|
+
* Every execution host a Contribution can be mounted in. A Bot-authored
|
|
14
|
+
* manifest declares `bot-isolate`, but provenance and an immutable artifact
|
|
15
|
+
* still decide whether the host accepts it; a manifest never grants itself
|
|
16
|
+
* that execution authority.
|
|
12
17
|
*/
|
|
13
18
|
export type ContributionKind = ManifestContributionKind | "bot-isolate";
|
|
14
19
|
|
|
@@ -19,6 +24,15 @@ export interface BackendContribution {
|
|
|
19
24
|
|
|
20
25
|
export interface RuntimeContribution {
|
|
21
26
|
entry: string;
|
|
27
|
+
/** Present only on a Bot-authored manifest; provenance still decides host authority. */
|
|
28
|
+
host?: "bot-isolate";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A Bot-authored manifest's durable declaration; isolate health supplies details at mount. */
|
|
32
|
+
export interface ManifestToolDeclaration {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
inputSchema: Record<string, unknown>;
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
export interface ClientMount {
|
|
@@ -26,12 +40,37 @@ export interface ClientMount {
|
|
|
26
40
|
order?: number;
|
|
27
41
|
}
|
|
28
42
|
|
|
29
|
-
|
|
43
|
+
/** A reviewed first-party client module compiled into the hosted bundle. */
|
|
44
|
+
export interface ClientModuleContribution {
|
|
30
45
|
entry: string;
|
|
31
46
|
mounts: ClientMount[];
|
|
32
47
|
outlets: string[];
|
|
33
48
|
}
|
|
34
49
|
|
|
50
|
+
/** Immutable HTML bytes rendered only by the first-party sandbox host. */
|
|
51
|
+
export interface ClientIframeArtifactV1 {
|
|
52
|
+
contentHash: string;
|
|
53
|
+
size: number;
|
|
54
|
+
mediaType: "text/html";
|
|
55
|
+
bundlerVersion: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A non-first-party page. It never becomes JavaScript in the app origin. */
|
|
59
|
+
export interface ClientIframeContribution {
|
|
60
|
+
kind: "iframe";
|
|
61
|
+
artifact: ClientIframeArtifactV1;
|
|
62
|
+
mounts: ClientMount[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type ClientContribution =
|
|
66
|
+
ClientModuleContribution | ClientIframeContribution;
|
|
67
|
+
|
|
68
|
+
export function isClientIframeContribution(
|
|
69
|
+
contribution: ClientContribution,
|
|
70
|
+
): contribution is ClientIframeContribution {
|
|
71
|
+
return "kind" in contribution && contribution.kind === "iframe";
|
|
72
|
+
}
|
|
73
|
+
|
|
35
74
|
export interface DesktopContribution {
|
|
36
75
|
entry: string;
|
|
37
76
|
execution: "sandboxed-renderer" | "trusted-main";
|
|
@@ -81,6 +120,12 @@ export interface PackageSettingDefinition {
|
|
|
81
120
|
id: string;
|
|
82
121
|
schemaVersion: number;
|
|
83
122
|
scopes: SettingScope[];
|
|
123
|
+
/**
|
|
124
|
+
* A kernel-consumed semantic role. The model role is deliberately generic:
|
|
125
|
+
* ADR 0019 lets a Package opt the User into model choice without teaching
|
|
126
|
+
* the kernel that Package's identity or policy.
|
|
127
|
+
*/
|
|
128
|
+
role?: "model";
|
|
84
129
|
schema: PackageSettingSchema;
|
|
85
130
|
}
|
|
86
131
|
|
|
@@ -130,6 +175,7 @@ export interface FrockBotManifest {
|
|
|
130
175
|
version: string;
|
|
131
176
|
compatibility: { frockbot: string };
|
|
132
177
|
dependencies: Record<string, string>;
|
|
178
|
+
defaultEnablement?: "enabled" | "disabled";
|
|
133
179
|
contributions: {
|
|
134
180
|
backend?: BackendContribution[];
|
|
135
181
|
runtime?: RuntimeContribution;
|
|
@@ -139,6 +185,10 @@ export interface FrockBotManifest {
|
|
|
139
185
|
};
|
|
140
186
|
permissions: string[];
|
|
141
187
|
configuration?: PackageConfiguration;
|
|
188
|
+
/** Present exactly when `contributions.runtime.host` is `bot-isolate`. */
|
|
189
|
+
tools?: ManifestToolDeclaration[];
|
|
190
|
+
/** Bot-isolate waterfalls the immutable artifact declares it exports. */
|
|
191
|
+
hooks?: BotIsolateHookEventNameV1[];
|
|
142
192
|
}
|
|
143
193
|
|
|
144
194
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -159,6 +209,88 @@ function exactFields(
|
|
|
159
209
|
}
|
|
160
210
|
}
|
|
161
211
|
|
|
212
|
+
function validateManifestJson(value: unknown, label: string, depth = 0): void {
|
|
213
|
+
if (depth > 16) throw new Error(`${label} is too deeply nested`);
|
|
214
|
+
if (
|
|
215
|
+
value === null ||
|
|
216
|
+
typeof value === "string" ||
|
|
217
|
+
typeof value === "boolean" ||
|
|
218
|
+
(typeof value === "number" && Number.isFinite(value))
|
|
219
|
+
) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (Array.isArray(value)) {
|
|
223
|
+
for (const entry of value) validateManifestJson(entry, label, depth + 1);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (!isRecord(value)) throw new Error(`${label} must contain only JSON`);
|
|
227
|
+
for (const entry of Object.values(value)) {
|
|
228
|
+
validateManifestJson(entry, label, depth + 1);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function decodeManifestTools(
|
|
233
|
+
value: unknown,
|
|
234
|
+
): ManifestToolDeclaration[] | undefined {
|
|
235
|
+
if (value === undefined) return undefined;
|
|
236
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 64) {
|
|
237
|
+
throw new Error("manifest tools must be a non-empty bounded array");
|
|
238
|
+
}
|
|
239
|
+
const tools = value.map((candidate, index) => {
|
|
240
|
+
if (!isRecord(candidate)) {
|
|
241
|
+
throw new Error(`manifest tools[${index}] must be an object`);
|
|
242
|
+
}
|
|
243
|
+
exactFields(
|
|
244
|
+
candidate,
|
|
245
|
+
["name", "description", "inputSchema"],
|
|
246
|
+
`manifest tools[${index}]`,
|
|
247
|
+
);
|
|
248
|
+
const name = requiredString(candidate, "name");
|
|
249
|
+
if (!/^[a-z][a-z0-9_]{0,63}$/.test(name)) {
|
|
250
|
+
throw new Error(`manifest tools[${index}] name is invalid`);
|
|
251
|
+
}
|
|
252
|
+
const description = requiredString(candidate, "description");
|
|
253
|
+
if (description.length > 1_024) {
|
|
254
|
+
throw new Error(`manifest tools[${index}] description is too long`);
|
|
255
|
+
}
|
|
256
|
+
if (!isRecord(candidate.inputSchema)) {
|
|
257
|
+
throw new Error(`manifest tools[${index}] inputSchema must be an object`);
|
|
258
|
+
}
|
|
259
|
+
validateManifestJson(
|
|
260
|
+
candidate.inputSchema,
|
|
261
|
+
`manifest tools[${index}] inputSchema`,
|
|
262
|
+
);
|
|
263
|
+
return {
|
|
264
|
+
name,
|
|
265
|
+
description,
|
|
266
|
+
inputSchema: structuredClone(candidate.inputSchema),
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
if (new Set(tools.map((tool) => tool.name)).size !== tools.length) {
|
|
270
|
+
throw new Error("manifest tools contains duplicate names");
|
|
271
|
+
}
|
|
272
|
+
return tools;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function decodeManifestHooks(
|
|
276
|
+
value: unknown,
|
|
277
|
+
): BotIsolateHookEventNameV1[] | undefined {
|
|
278
|
+
if (value === undefined) return undefined;
|
|
279
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 16) {
|
|
280
|
+
throw new Error("manifest hooks must be a non-empty bounded array");
|
|
281
|
+
}
|
|
282
|
+
const hooks = value.map((hook, index) => {
|
|
283
|
+
if (!isBotIsolateHookEventNameV1(hook)) {
|
|
284
|
+
throw new Error(`manifest hooks[${index}] is invalid`);
|
|
285
|
+
}
|
|
286
|
+
return hook;
|
|
287
|
+
});
|
|
288
|
+
if (new Set(hooks).size !== hooks.length) {
|
|
289
|
+
throw new Error("manifest hooks contains duplicates");
|
|
290
|
+
}
|
|
291
|
+
return hooks;
|
|
292
|
+
}
|
|
293
|
+
|
|
162
294
|
function requiredString(record: Record<string, unknown>, key: string): string {
|
|
163
295
|
const value = record[key];
|
|
164
296
|
if (typeof value !== "string" || !value.trim()) {
|
|
@@ -221,6 +353,18 @@ function decodeDependencies(value: unknown): Record<string, string> {
|
|
|
221
353
|
return dependencies;
|
|
222
354
|
}
|
|
223
355
|
|
|
356
|
+
function decodeDefaultEnablement(
|
|
357
|
+
value: unknown,
|
|
358
|
+
): FrockBotManifest["defaultEnablement"] {
|
|
359
|
+
if (value === undefined) return undefined;
|
|
360
|
+
if (value !== "enabled" && value !== "disabled") {
|
|
361
|
+
throw new Error(
|
|
362
|
+
'manifest defaultEnablement must be "enabled" or "disabled"',
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
return value;
|
|
366
|
+
}
|
|
367
|
+
|
|
224
368
|
function decodeIdentity(
|
|
225
369
|
value: Record<string, unknown>,
|
|
226
370
|
): Pick<FrockBotManifest, "id" | "displayName" | "version" | "permissions"> {
|
|
@@ -301,6 +445,7 @@ function isV3OrLater(value: Record<string, unknown>): boolean {
|
|
|
301
445
|
|
|
302
446
|
function decodeV2(value: Record<string, unknown>): FrockBotManifest {
|
|
303
447
|
const identity = decodeIdentity(value);
|
|
448
|
+
const defaultEnablement = decodeDefaultEnablement(value.defaultEnablement);
|
|
304
449
|
if (!isRecord(value.compatibility)) {
|
|
305
450
|
throw new Error("manifest compatibility must be an object");
|
|
306
451
|
}
|
|
@@ -348,43 +493,102 @@ function decodeV2(value: Record<string, unknown>): FrockBotManifest {
|
|
|
348
493
|
}
|
|
349
494
|
exactFields(
|
|
350
495
|
value.contributions.runtime,
|
|
351
|
-
["entry"],
|
|
496
|
+
["entry", ...(isV3OrLater(value) ? ["host"] : [])],
|
|
352
497
|
"manifest runtime contribution",
|
|
353
498
|
);
|
|
499
|
+
const host = value.contributions.runtime.host;
|
|
500
|
+
if (host !== undefined && host !== "bot-isolate") {
|
|
501
|
+
throw new Error("manifest runtime host is invalid");
|
|
502
|
+
}
|
|
354
503
|
contributions.runtime = {
|
|
355
504
|
entry: relativeEntry(value.contributions.runtime, "entry"),
|
|
505
|
+
...(host === "bot-isolate" ? { host } : {}),
|
|
356
506
|
};
|
|
357
507
|
}
|
|
358
508
|
if (value.contributions.client !== undefined) {
|
|
359
509
|
const client = value.contributions.client;
|
|
360
510
|
if (!isRecord(client))
|
|
361
511
|
throw new Error("manifest client contribution must be an object");
|
|
362
|
-
exactFields(
|
|
363
|
-
client,
|
|
364
|
-
["entry", "mounts", "outlets"],
|
|
365
|
-
"manifest client contribution",
|
|
366
|
-
);
|
|
367
512
|
const mounts = client.mounts;
|
|
368
513
|
if (!Array.isArray(mounts)) {
|
|
369
514
|
throw new Error("manifest client mounts must be an array");
|
|
370
515
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
|
|
516
|
+
const decodedMounts = mounts.map((mount) => {
|
|
517
|
+
if (!isRecord(mount))
|
|
518
|
+
throw new Error("manifest client mount must be an object");
|
|
519
|
+
exactFields(mount, ["slot", "order"], "manifest client mount");
|
|
520
|
+
const order = mount.order;
|
|
521
|
+
if (
|
|
522
|
+
order !== undefined &&
|
|
523
|
+
(typeof order !== "number" || !Number.isFinite(order))
|
|
524
|
+
) {
|
|
525
|
+
throw new Error("manifest client mount order must be finite");
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
slot: requiredString(mount, "slot"),
|
|
529
|
+
...(order === undefined ? {} : { order }),
|
|
530
|
+
};
|
|
531
|
+
});
|
|
532
|
+
if (client.kind === "iframe") {
|
|
533
|
+
if (!isV3OrLater(value)) {
|
|
534
|
+
throw new Error("manifest iframe client requires schema version 3");
|
|
535
|
+
}
|
|
536
|
+
exactFields(
|
|
537
|
+
client,
|
|
538
|
+
["kind", "artifact", "mounts"],
|
|
539
|
+
"manifest iframe client contribution",
|
|
540
|
+
);
|
|
541
|
+
if (!isRecord(client.artifact)) {
|
|
542
|
+
throw new Error("manifest iframe client artifact must be an object");
|
|
543
|
+
}
|
|
544
|
+
exactFields(
|
|
545
|
+
client.artifact,
|
|
546
|
+
["contentHash", "size", "mediaType", "bundlerVersion"],
|
|
547
|
+
"manifest iframe client artifact",
|
|
548
|
+
);
|
|
549
|
+
const artifact = client.artifact;
|
|
550
|
+
if (
|
|
551
|
+
typeof artifact.contentHash !== "string" ||
|
|
552
|
+
!/^[0-9a-f]{64}$/.test(artifact.contentHash)
|
|
553
|
+
) {
|
|
554
|
+
throw new Error(
|
|
555
|
+
"manifest iframe client artifact contentHash must be a sha-256 digest",
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
if (
|
|
559
|
+
!Number.isSafeInteger(artifact.size) ||
|
|
560
|
+
(artifact.size as number) < 0 ||
|
|
561
|
+
(artifact.size as number) > 256 * 1024
|
|
562
|
+
) {
|
|
563
|
+
throw new Error(
|
|
564
|
+
"manifest iframe client artifact size must be within the 256 KB quota",
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
if (artifact.mediaType !== "text/html") {
|
|
568
|
+
throw new Error("manifest iframe client artifact mediaType is invalid");
|
|
569
|
+
}
|
|
570
|
+
contributions.client = {
|
|
571
|
+
kind: "iframe",
|
|
572
|
+
artifact: {
|
|
573
|
+
contentHash: artifact.contentHash,
|
|
574
|
+
size: artifact.size as number,
|
|
575
|
+
mediaType: "text/html",
|
|
576
|
+
bundlerVersion: requiredString(artifact, "bundlerVersion"),
|
|
577
|
+
},
|
|
578
|
+
mounts: decodedMounts,
|
|
579
|
+
};
|
|
580
|
+
} else {
|
|
581
|
+
exactFields(
|
|
582
|
+
client,
|
|
583
|
+
["entry", "mounts", "outlets"],
|
|
584
|
+
"manifest client contribution",
|
|
585
|
+
);
|
|
586
|
+
contributions.client = {
|
|
587
|
+
entry: relativeEntry(client, "entry"),
|
|
588
|
+
mounts: decodedMounts,
|
|
589
|
+
outlets: optionalStringArray(client, "outlets"),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
388
592
|
}
|
|
389
593
|
if (value.contributions.mobile !== undefined) {
|
|
390
594
|
const mobile = value.contributions.mobile;
|
|
@@ -437,6 +641,7 @@ function decodeV2(value: Record<string, unknown>): FrockBotManifest {
|
|
|
437
641
|
frockbot: requiredString(value.compatibility, "frockbot"),
|
|
438
642
|
},
|
|
439
643
|
dependencies: decodeDependencies(value.dependencies),
|
|
644
|
+
...(defaultEnablement ? { defaultEnablement } : {}),
|
|
440
645
|
contributions,
|
|
441
646
|
};
|
|
442
647
|
}
|
|
@@ -905,6 +1110,44 @@ function safeSchema(value: unknown): PackageSettingSchema {
|
|
|
905
1110
|
return decodeSafeSchema(value, 0);
|
|
906
1111
|
}
|
|
907
1112
|
|
|
1113
|
+
/**
|
|
1114
|
+
* The one object contract the kernel interprets from a setting value. Keeping
|
|
1115
|
+
* this exact prevents a Package from smuggling provider policy into the model
|
|
1116
|
+
* seam while still letting ordinary settings use the supported schema subset.
|
|
1117
|
+
*/
|
|
1118
|
+
function assertModelBindingSchema(schema: PackageSettingSchema): void {
|
|
1119
|
+
const fields = Reflect.ownKeys(schema);
|
|
1120
|
+
const properties = schema.properties;
|
|
1121
|
+
const required = schema.required;
|
|
1122
|
+
if (
|
|
1123
|
+
fields.length !== 4 ||
|
|
1124
|
+
!fields.every((field) =>
|
|
1125
|
+
["type", "properties", "required", "additionalProperties"].includes(
|
|
1126
|
+
String(field),
|
|
1127
|
+
),
|
|
1128
|
+
) ||
|
|
1129
|
+
schema.type !== "object" ||
|
|
1130
|
+
schema.additionalProperties !== false ||
|
|
1131
|
+
!properties ||
|
|
1132
|
+
Reflect.ownKeys(properties).length !== 2 ||
|
|
1133
|
+
!Object.hasOwn(properties, "connectionId") ||
|
|
1134
|
+
!Object.hasOwn(properties, "providerModelId") ||
|
|
1135
|
+
Reflect.ownKeys(properties.connectionId ?? {}).length !== 1 ||
|
|
1136
|
+
properties.connectionId?.type !== "string" ||
|
|
1137
|
+
Reflect.ownKeys(properties.providerModelId ?? {}).length !== 1 ||
|
|
1138
|
+
properties.providerModelId?.type !== "string" ||
|
|
1139
|
+
!required ||
|
|
1140
|
+
required.length !== 2 ||
|
|
1141
|
+
new Set(required).size !== 2 ||
|
|
1142
|
+
!required.includes("connectionId") ||
|
|
1143
|
+
!required.includes("providerModelId")
|
|
1144
|
+
) {
|
|
1145
|
+
throw new Error(
|
|
1146
|
+
'manifest model setting schema must be exactly an object with required string properties "connectionId" and "providerModelId" and no additional properties',
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
908
1151
|
function decodeCapabilityAdmission(value: unknown): {
|
|
909
1152
|
turnTypes: TurnTypeV1[];
|
|
910
1153
|
subagentRoles?: string[];
|
|
@@ -984,7 +1227,13 @@ function settingDefinitions(
|
|
|
984
1227
|
// that round-trips through this decoder decodes again unchanged.
|
|
985
1228
|
exactFields(
|
|
986
1229
|
setting,
|
|
987
|
-
[
|
|
1230
|
+
[
|
|
1231
|
+
"id",
|
|
1232
|
+
"schemaVersion",
|
|
1233
|
+
"schema",
|
|
1234
|
+
"scopes",
|
|
1235
|
+
...(scope === "package" ? ["role"] : []),
|
|
1236
|
+
],
|
|
988
1237
|
"manifest setting definition",
|
|
989
1238
|
);
|
|
990
1239
|
const schemaVersion = setting.schemaVersion;
|
|
@@ -1019,11 +1268,17 @@ function settingDefinitions(
|
|
|
1019
1268
|
) {
|
|
1020
1269
|
throw new Error("manifest setting scopes must contain user or bot");
|
|
1021
1270
|
}
|
|
1271
|
+
if (setting.role !== undefined && setting.role !== "model") {
|
|
1272
|
+
throw new Error('manifest setting role must be "model"');
|
|
1273
|
+
}
|
|
1274
|
+
const schema = safeSchema(setting.schema);
|
|
1275
|
+
if (setting.role === "model") assertModelBindingSchema(schema);
|
|
1022
1276
|
return {
|
|
1023
1277
|
id: definitionId(setting),
|
|
1024
1278
|
schemaVersion: schemaVersion as number,
|
|
1025
1279
|
scopes: scopes as SettingScope[],
|
|
1026
|
-
|
|
1280
|
+
...(setting.role === undefined ? {} : { role: setting.role }),
|
|
1281
|
+
schema,
|
|
1027
1282
|
};
|
|
1028
1283
|
});
|
|
1029
1284
|
}
|
|
@@ -1147,23 +1402,79 @@ function decodeConfiguration(
|
|
|
1147
1402
|
|
|
1148
1403
|
function decodeV3(value: Record<string, unknown>): FrockBotManifest {
|
|
1149
1404
|
const base = decodeV2(value);
|
|
1405
|
+
const tools = decodeManifestTools(value.tools);
|
|
1406
|
+
const hooks = decodeManifestHooks(value.hooks);
|
|
1407
|
+
const botIsolate = base.contributions.runtime?.host === "bot-isolate";
|
|
1408
|
+
if (botIsolate !== (tools !== undefined)) {
|
|
1409
|
+
throw new Error(
|
|
1410
|
+
"manifest bot-isolate runtime and tools declaration must appear together",
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
if (!botIsolate && hooks !== undefined) {
|
|
1414
|
+
throw new Error("manifest hooks require a bot-isolate runtime");
|
|
1415
|
+
}
|
|
1416
|
+
validateIframeClientContribution(base.contributions.client, tools);
|
|
1150
1417
|
return {
|
|
1151
1418
|
...base,
|
|
1152
1419
|
schemaVersion: 3,
|
|
1153
1420
|
configuration: decodeConfiguration(value.configuration, false),
|
|
1421
|
+
...(tools ? { tools } : {}),
|
|
1422
|
+
...(hooks ? { hooks } : {}),
|
|
1154
1423
|
};
|
|
1155
1424
|
}
|
|
1156
1425
|
|
|
1157
1426
|
/** v4 is v3 plus the Capability admission ceiling, and nothing else. */
|
|
1158
1427
|
function decodeV4(value: Record<string, unknown>): FrockBotManifest {
|
|
1159
1428
|
const base = decodeV2(value);
|
|
1429
|
+
const tools = decodeManifestTools(value.tools);
|
|
1430
|
+
const hooks = decodeManifestHooks(value.hooks);
|
|
1431
|
+
const botIsolate = base.contributions.runtime?.host === "bot-isolate";
|
|
1432
|
+
if (botIsolate !== (tools !== undefined)) {
|
|
1433
|
+
throw new Error(
|
|
1434
|
+
"manifest bot-isolate runtime and tools declaration must appear together",
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
if (!botIsolate && hooks !== undefined) {
|
|
1438
|
+
throw new Error("manifest hooks require a bot-isolate runtime");
|
|
1439
|
+
}
|
|
1440
|
+
validateIframeClientContribution(base.contributions.client, tools);
|
|
1160
1441
|
return {
|
|
1161
1442
|
...base,
|
|
1162
1443
|
schemaVersion: 4,
|
|
1163
1444
|
configuration: decodeConfiguration(value.configuration, true),
|
|
1445
|
+
...(tools ? { tools } : {}),
|
|
1446
|
+
...(hooks ? { hooks } : {}),
|
|
1164
1447
|
};
|
|
1165
1448
|
}
|
|
1166
1449
|
|
|
1450
|
+
function validateIframeClientContribution(
|
|
1451
|
+
client: ClientContribution | undefined,
|
|
1452
|
+
tools: ManifestToolDeclaration[] | undefined,
|
|
1453
|
+
): void {
|
|
1454
|
+
if (!client || !isClientIframeContribution(client)) return;
|
|
1455
|
+
if (client.mounts.length === 0 || client.mounts.length > 64) {
|
|
1456
|
+
throw new Error(
|
|
1457
|
+
"manifest iframe client mounts must be a non-empty bounded array",
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
const toolNames = new Set((tools ?? []).map((tool) => tool.name));
|
|
1461
|
+
for (const mount of client.mounts) {
|
|
1462
|
+
if (mount.slot === "frockbot.bot-settings-sections") continue;
|
|
1463
|
+
const prefix = "frockbot.tool-result:";
|
|
1464
|
+
if (!mount.slot.startsWith(prefix)) {
|
|
1465
|
+
throw new Error(
|
|
1466
|
+
`manifest iframe client slot "${mount.slot}" is not iframe-safe`,
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
const toolName = mount.slot.slice(prefix.length);
|
|
1470
|
+
if (!toolNames.has(toolName)) {
|
|
1471
|
+
throw new Error(
|
|
1472
|
+
`manifest iframe client tool-result slot names undeclared tool "${toolName}"`,
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1167
1478
|
export function decodeFrockBotManifest(value: unknown): FrockBotManifest {
|
|
1168
1479
|
if (!isRecord(value)) throw new Error("manifest must be an object");
|
|
1169
1480
|
if (value.schemaVersion === 1) {
|
|
@@ -1196,8 +1507,11 @@ export function decodeFrockBotManifest(value: unknown): FrockBotManifest {
|
|
|
1196
1507
|
"permissions",
|
|
1197
1508
|
"compatibility",
|
|
1198
1509
|
"dependencies",
|
|
1510
|
+
"defaultEnablement",
|
|
1199
1511
|
"contributions",
|
|
1200
1512
|
...(isV3OrLater(value) ? ["configuration"] : []),
|
|
1513
|
+
...(isV3OrLater(value) ? ["tools"] : []),
|
|
1514
|
+
...(isV3OrLater(value) ? ["hooks"] : []),
|
|
1201
1515
|
],
|
|
1202
1516
|
"manifest",
|
|
1203
1517
|
);
|