@frockbot/kernel-composition 0.3.1 → 0.3.3
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 +50 -0
- package/src/compiler.ts +23 -0
- package/src/generation.ts +194 -8
- package/src/index.test.ts +358 -4
- package/src/isolate-host.test.ts +24 -1
- package/src/isolate-host.ts +9 -1
- package/src/isolate-wrapper.ts +28 -1
- package/src/manifest.ts +442 -85
package/src/manifest.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isBotIsolateHookEventNameV1,
|
|
3
3
|
decodeTurnTypeV1,
|
|
4
|
+
iframePageSlotAllowedV1,
|
|
5
|
+
PACKAGE_IFRAME_ENTRY_LABEL_MAX_V1,
|
|
6
|
+
PACKAGE_IFRAME_ENTRY_SLOT_V1,
|
|
7
|
+
PACKAGE_IFRAME_ID_V1,
|
|
8
|
+
PACKAGE_IFRAME_MAX_ENTRIES_V1,
|
|
9
|
+
PACKAGE_IFRAME_MAX_PAGES_V1,
|
|
4
10
|
type BotIsolateHookEventNameV1,
|
|
5
11
|
type TurnTypeV1,
|
|
6
12
|
} from "@frockbot/kernel-contracts";
|
|
13
|
+
import type { ArtifactRefV1 } from "./generation.ts";
|
|
7
14
|
|
|
8
15
|
/** The Contribution kinds a Package manifest can declare. */
|
|
9
16
|
export type ManifestContributionKind =
|
|
@@ -55,13 +62,54 @@ export interface ClientIframeArtifactV1 {
|
|
|
55
62
|
bundlerVersion: string;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
/**
|
|
59
|
-
export interface
|
|
60
|
-
|
|
65
|
+
/** One non-first-party page. It never becomes JavaScript in the app origin. */
|
|
66
|
+
export interface ClientIframePageV1 {
|
|
67
|
+
/** `/^[a-z][a-z0-9-]{0,31}$/`, unique within the Contribution. */
|
|
68
|
+
id: string;
|
|
61
69
|
artifact: ClientIframeArtifactV1;
|
|
62
70
|
mounts: ClientMount[];
|
|
63
71
|
}
|
|
64
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Manifest v5. A declarative launcher for one of the Package's pages. The
|
|
75
|
+
* shell renders it; the Package never scripts the app origin to place it.
|
|
76
|
+
*/
|
|
77
|
+
export interface ClientEntryV1 {
|
|
78
|
+
id: string;
|
|
79
|
+
slot: "frockbot.sidebar-actions";
|
|
80
|
+
order?: number;
|
|
81
|
+
/** At most 32 characters. */
|
|
82
|
+
label: string;
|
|
83
|
+
/** A `UiIcon` name. */
|
|
84
|
+
icon: string;
|
|
85
|
+
opens: { kind: "surface"; page: string };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Manifest v5. The multi-page iframe client. A v3/v4 record carrying one
|
|
90
|
+
* top-level `artifact` and `mounts` is migrated to `pages: [{ id: "main" }]`
|
|
91
|
+
* at the decoder; there is one in-memory shape, never two.
|
|
92
|
+
*/
|
|
93
|
+
export interface ClientIframeContribution {
|
|
94
|
+
kind: "iframe";
|
|
95
|
+
/** 1..8 pages. */
|
|
96
|
+
pages: ClientIframePageV1[];
|
|
97
|
+
/** 0..4 entries. */
|
|
98
|
+
entries?: ClientEntryV1[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Manifest v5. What a Package contributes as one durable instance per User —
|
|
103
|
+
* an Applet. The kernel mounts `server` as a facet under its own Applet
|
|
104
|
+
* Durable Object; `tools` is what that instance offers the User's Bots.
|
|
105
|
+
*/
|
|
106
|
+
export interface InstanceContributionV1 {
|
|
107
|
+
contract: 1;
|
|
108
|
+
server: ArtifactRefV1;
|
|
109
|
+
ui: ClientIframeArtifactV1;
|
|
110
|
+
tools: ManifestToolDeclaration[];
|
|
111
|
+
}
|
|
112
|
+
|
|
65
113
|
export type ClientContribution =
|
|
66
114
|
ClientModuleContribution | ClientIframeContribution;
|
|
67
115
|
|
|
@@ -162,6 +210,27 @@ export interface CapabilityDefinition {
|
|
|
162
210
|
admission?: { turnTypes: TurnTypeV1[]; subagentRoles?: string[] };
|
|
163
211
|
}
|
|
164
212
|
|
|
213
|
+
/**
|
|
214
|
+
* One durable Workspace root a Package declares for itself.
|
|
215
|
+
*
|
|
216
|
+
* "Durable roots, declared by the Computer Package's Workspace layout and by
|
|
217
|
+
* Package manifests, survive hibernation, cold start, host migration, and
|
|
218
|
+
* image rebuild." The Computer half of that sentence has always been real —
|
|
219
|
+
* `FLY_WORKSPACE_LAYOUT` carries the `package-declared` mount template — but
|
|
220
|
+
* the manifest half had nowhere to be written, so no Package-declared root
|
|
221
|
+
* ever reached the durable-root sync. This is that half.
|
|
222
|
+
*
|
|
223
|
+
* `scope` is `user` and only `user`: `WorkspaceRootV1` names a
|
|
224
|
+
* `package-declared` root by User and Package with no Bot in it, and Package
|
|
225
|
+
* availability is a User-level fact (ADR 0019). A Bot-scoped Package root
|
|
226
|
+
* would be a root the kernel's own root type cannot address.
|
|
227
|
+
*/
|
|
228
|
+
export interface ManifestDeclaredRootV1 {
|
|
229
|
+
/** The `rootId`, in the kernel's `package-declared` root-id shape. */
|
|
230
|
+
id: string;
|
|
231
|
+
scope: "user";
|
|
232
|
+
}
|
|
233
|
+
|
|
165
234
|
export interface PackageConfiguration {
|
|
166
235
|
settings: PackageSettingDefinition[];
|
|
167
236
|
connectionTypes: ConnectionTypeDefinition[];
|
|
@@ -169,7 +238,7 @@ export interface PackageConfiguration {
|
|
|
169
238
|
}
|
|
170
239
|
|
|
171
240
|
export interface FrockBotManifest {
|
|
172
|
-
schemaVersion: 2 | 3 | 4;
|
|
241
|
+
schemaVersion: 2 | 3 | 4 | 5;
|
|
173
242
|
id: string;
|
|
174
243
|
displayName: string;
|
|
175
244
|
version: string;
|
|
@@ -182,6 +251,8 @@ export interface FrockBotManifest {
|
|
|
182
251
|
client?: ClientContribution;
|
|
183
252
|
desktop?: DesktopContribution;
|
|
184
253
|
mobile?: MobileContribution;
|
|
254
|
+
/** Manifest v5 only; refused below it. */
|
|
255
|
+
instance?: InstanceContributionV1;
|
|
185
256
|
};
|
|
186
257
|
permissions: string[];
|
|
187
258
|
configuration?: PackageConfiguration;
|
|
@@ -189,6 +260,8 @@ export interface FrockBotManifest {
|
|
|
189
260
|
tools?: ManifestToolDeclaration[];
|
|
190
261
|
/** Bot-isolate waterfalls the immutable artifact declares it exports. */
|
|
191
262
|
hooks?: BotIsolateHookEventNameV1[];
|
|
263
|
+
/** The durable Workspace roots this Package declares. Manifest v3 onward. */
|
|
264
|
+
roots?: ManifestDeclaredRootV1[];
|
|
192
265
|
}
|
|
193
266
|
|
|
194
267
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -291,6 +364,45 @@ function decodeManifestHooks(
|
|
|
291
364
|
return hooks;
|
|
292
365
|
}
|
|
293
366
|
|
|
367
|
+
/**
|
|
368
|
+
* The `package-declared` root-id shape, kept identical to the kernel's own
|
|
369
|
+
* `ROOT_ID` in `kernel-contracts/src/workspace.ts`.
|
|
370
|
+
*
|
|
371
|
+
* Restated rather than imported because `kernel-composition` decodes a
|
|
372
|
+
* manifest without depending on the Workspace contracts; the round-trip test
|
|
373
|
+
* beside this decoder is what keeps the two from drifting.
|
|
374
|
+
*/
|
|
375
|
+
const MANIFEST_ROOT_ID = /^[a-z][a-z0-9-]{0,127}$/;
|
|
376
|
+
|
|
377
|
+
function decodeManifestRoots(
|
|
378
|
+
value: unknown,
|
|
379
|
+
): ManifestDeclaredRootV1[] | undefined {
|
|
380
|
+
if (value === undefined) return undefined;
|
|
381
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 8) {
|
|
382
|
+
throw new Error("manifest roots must be a non-empty bounded array");
|
|
383
|
+
}
|
|
384
|
+
const roots = value.map((entry, index) => {
|
|
385
|
+
if (!isRecord(entry)) {
|
|
386
|
+
throw new Error(`manifest roots[${index}] must be an object`);
|
|
387
|
+
}
|
|
388
|
+
exactFields(entry, ["id", "scope"], `manifest roots[${index}]`);
|
|
389
|
+
const id = requiredString(entry, "id");
|
|
390
|
+
if (!MANIFEST_ROOT_ID.test(id)) {
|
|
391
|
+
throw new Error(`manifest roots[${index}] id is invalid`);
|
|
392
|
+
}
|
|
393
|
+
// User and only User: a `package-declared` root names no Bot, so a
|
|
394
|
+
// Bot-scoped one would be a root the kernel cannot address.
|
|
395
|
+
if (entry.scope !== "user") {
|
|
396
|
+
throw new Error(`manifest roots[${index}] scope must be "user"`);
|
|
397
|
+
}
|
|
398
|
+
return { id, scope: "user" as const };
|
|
399
|
+
});
|
|
400
|
+
if (new Set(roots.map((root) => root.id)).size !== roots.length) {
|
|
401
|
+
throw new Error("manifest roots contains duplicate ids");
|
|
402
|
+
}
|
|
403
|
+
return roots;
|
|
404
|
+
}
|
|
405
|
+
|
|
294
406
|
function requiredString(record: Record<string, unknown>, key: string): string {
|
|
295
407
|
const value = record[key];
|
|
296
408
|
if (typeof value !== "string" || !value.trim()) {
|
|
@@ -440,7 +552,187 @@ function decodeV1(value: Record<string, unknown>): FrockBotManifest {
|
|
|
440
552
|
* Manifest v4 extends v3, so every v3 rule applies unchanged to a v4 body.
|
|
441
553
|
*/
|
|
442
554
|
function isV3OrLater(value: Record<string, unknown>): boolean {
|
|
443
|
-
return
|
|
555
|
+
return (
|
|
556
|
+
value.schemaVersion === 3 ||
|
|
557
|
+
value.schemaVersion === 4 ||
|
|
558
|
+
value.schemaVersion === 5
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function isV5(value: Record<string, unknown>): boolean {
|
|
563
|
+
return value.schemaVersion === 5;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function decodeClientMounts(value: unknown, label: string): ClientMount[] {
|
|
567
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
|
568
|
+
return value.map((mount) => {
|
|
569
|
+
if (!isRecord(mount)) throw new Error(`${label} entry must be an object`);
|
|
570
|
+
exactFields(mount, ["slot", "order"], label);
|
|
571
|
+
const order = mount.order;
|
|
572
|
+
if (
|
|
573
|
+
order !== undefined &&
|
|
574
|
+
(typeof order !== "number" || !Number.isFinite(order))
|
|
575
|
+
) {
|
|
576
|
+
throw new Error(`${label} order must be finite`);
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
slot: requiredString(mount, "slot"),
|
|
580
|
+
...(order === undefined ? {} : { order }),
|
|
581
|
+
};
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function decodeClientIframeArtifact(
|
|
586
|
+
value: unknown,
|
|
587
|
+
label: string,
|
|
588
|
+
): ClientIframeArtifactV1 {
|
|
589
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
590
|
+
exactFields(
|
|
591
|
+
value,
|
|
592
|
+
["contentHash", "size", "mediaType", "bundlerVersion"],
|
|
593
|
+
label,
|
|
594
|
+
);
|
|
595
|
+
if (
|
|
596
|
+
typeof value.contentHash !== "string" ||
|
|
597
|
+
!/^[0-9a-f]{64}$/.test(value.contentHash)
|
|
598
|
+
) {
|
|
599
|
+
throw new Error(`${label} contentHash must be a sha-256 digest`);
|
|
600
|
+
}
|
|
601
|
+
if (
|
|
602
|
+
!Number.isSafeInteger(value.size) ||
|
|
603
|
+
(value.size as number) < 0 ||
|
|
604
|
+
(value.size as number) > 256 * 1024
|
|
605
|
+
) {
|
|
606
|
+
throw new Error(`${label} size must be within the 256 KB quota`);
|
|
607
|
+
}
|
|
608
|
+
if (value.mediaType !== "text/html") {
|
|
609
|
+
throw new Error(`${label} mediaType is invalid`);
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
contentHash: value.contentHash,
|
|
613
|
+
size: value.size as number,
|
|
614
|
+
mediaType: "text/html",
|
|
615
|
+
bundlerVersion: requiredString(value, "bundlerVersion"),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function decodeInstanceContribution(value: unknown): InstanceContributionV1 {
|
|
620
|
+
if (!isRecord(value)) {
|
|
621
|
+
throw new Error("manifest instance Contribution must be an object");
|
|
622
|
+
}
|
|
623
|
+
exactFields(
|
|
624
|
+
value,
|
|
625
|
+
["contract", "server", "ui", "tools"],
|
|
626
|
+
"manifest instance Contribution",
|
|
627
|
+
);
|
|
628
|
+
if (value.contract !== 1) {
|
|
629
|
+
throw new Error("manifest instance Contribution contract is unsupported");
|
|
630
|
+
}
|
|
631
|
+
if (!isRecord(value.server)) {
|
|
632
|
+
throw new Error("manifest instance Contribution server must be an object");
|
|
633
|
+
}
|
|
634
|
+
exactFields(
|
|
635
|
+
value.server,
|
|
636
|
+
["contentHash", "size", "mediaType", "bundlerVersion"],
|
|
637
|
+
"manifest instance Contribution server",
|
|
638
|
+
);
|
|
639
|
+
const server = value.server;
|
|
640
|
+
if (
|
|
641
|
+
typeof server.contentHash !== "string" ||
|
|
642
|
+
!/^[0-9a-f]{64}$/.test(server.contentHash)
|
|
643
|
+
) {
|
|
644
|
+
throw new Error(
|
|
645
|
+
"manifest instance Contribution server contentHash must be a sha-256 digest",
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
if (!Number.isSafeInteger(server.size) || (server.size as number) < 0) {
|
|
649
|
+
throw new Error(
|
|
650
|
+
"manifest instance Contribution server size must be a non-negative integer",
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
if (server.mediaType !== "application/javascript") {
|
|
654
|
+
throw new Error(
|
|
655
|
+
"manifest instance Contribution server mediaType is invalid",
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
const tools = decodeManifestTools(value.tools);
|
|
659
|
+
if (!tools) {
|
|
660
|
+
throw new Error(
|
|
661
|
+
"manifest instance Contribution must declare its instance tools",
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
contract: 1,
|
|
666
|
+
server: {
|
|
667
|
+
contentHash: server.contentHash,
|
|
668
|
+
size: server.size as number,
|
|
669
|
+
mediaType: "application/javascript",
|
|
670
|
+
bundlerVersion: requiredString(server, "bundlerVersion"),
|
|
671
|
+
},
|
|
672
|
+
ui: decodeClientIframeArtifact(
|
|
673
|
+
value.ui,
|
|
674
|
+
"manifest instance Contribution ui",
|
|
675
|
+
),
|
|
676
|
+
tools,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function decodeClientEntries(value: unknown): ClientEntryV1[] | undefined {
|
|
681
|
+
if (value === undefined) return undefined;
|
|
682
|
+
if (!Array.isArray(value) || value.length > PACKAGE_IFRAME_MAX_ENTRIES_V1) {
|
|
683
|
+
throw new Error("manifest iframe client entries must be a bounded array");
|
|
684
|
+
}
|
|
685
|
+
const entries = value.map((candidate, index) => {
|
|
686
|
+
const label = `manifest iframe client entries[${index}]`;
|
|
687
|
+
if (!isRecord(candidate)) throw new Error(`${label} must be an object`);
|
|
688
|
+
exactFields(
|
|
689
|
+
candidate,
|
|
690
|
+
["id", "slot", "order", "label", "icon", "opens"],
|
|
691
|
+
label,
|
|
692
|
+
);
|
|
693
|
+
const id = requiredString(candidate, "id");
|
|
694
|
+
if (!PACKAGE_IFRAME_ID_V1.test(id)) {
|
|
695
|
+
throw new Error(`${label} id is invalid`);
|
|
696
|
+
}
|
|
697
|
+
if (candidate.slot !== PACKAGE_IFRAME_ENTRY_SLOT_V1) {
|
|
698
|
+
throw new Error(`${label} slot is invalid`);
|
|
699
|
+
}
|
|
700
|
+
const order = candidate.order;
|
|
701
|
+
if (
|
|
702
|
+
order !== undefined &&
|
|
703
|
+
(typeof order !== "number" || !Number.isFinite(order))
|
|
704
|
+
) {
|
|
705
|
+
throw new Error(`${label} order must be finite`);
|
|
706
|
+
}
|
|
707
|
+
const entryLabel = requiredString(candidate, "label");
|
|
708
|
+
if (entryLabel.length > PACKAGE_IFRAME_ENTRY_LABEL_MAX_V1) {
|
|
709
|
+
throw new Error(`${label} label is too long`);
|
|
710
|
+
}
|
|
711
|
+
const icon = requiredString(candidate, "icon");
|
|
712
|
+
if (icon.length > 64) throw new Error(`${label} icon is too long`);
|
|
713
|
+
if (!isRecord(candidate.opens)) {
|
|
714
|
+
throw new Error(`${label} opens must be an object`);
|
|
715
|
+
}
|
|
716
|
+
exactFields(candidate.opens, ["kind", "page"], `${label} opens`);
|
|
717
|
+
if (candidate.opens.kind !== "surface") {
|
|
718
|
+
throw new Error(`${label} opens.kind is invalid`);
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
id,
|
|
722
|
+
slot: PACKAGE_IFRAME_ENTRY_SLOT_V1 as "frockbot.sidebar-actions",
|
|
723
|
+
...(order === undefined ? {} : { order }),
|
|
724
|
+
label: entryLabel,
|
|
725
|
+
icon,
|
|
726
|
+
opens: {
|
|
727
|
+
kind: "surface" as const,
|
|
728
|
+
page: requiredString(candidate.opens, "page"),
|
|
729
|
+
},
|
|
730
|
+
};
|
|
731
|
+
});
|
|
732
|
+
if (new Set(entries.map((entry) => entry.id)).size !== entries.length) {
|
|
733
|
+
throw new Error("manifest iframe client entries contain duplicate ids");
|
|
734
|
+
}
|
|
735
|
+
return entries;
|
|
444
736
|
}
|
|
445
737
|
|
|
446
738
|
function decodeV2(value: Record<string, unknown>): FrockBotManifest {
|
|
@@ -456,7 +748,7 @@ function decodeV2(value: Record<string, unknown>): FrockBotManifest {
|
|
|
456
748
|
exactFields(
|
|
457
749
|
value.contributions,
|
|
458
750
|
isV3OrLater(value)
|
|
459
|
-
? ["backend", "runtime", "client", "desktop", "mobile"]
|
|
751
|
+
? ["backend", "runtime", "client", "desktop", "mobile", "instance"]
|
|
460
752
|
: ["runtime", "client", "desktop", "mobile"],
|
|
461
753
|
"manifest contributions",
|
|
462
754
|
);
|
|
@@ -509,74 +801,77 @@ function decodeV2(value: Record<string, unknown>): FrockBotManifest {
|
|
|
509
801
|
const client = value.contributions.client;
|
|
510
802
|
if (!isRecord(client))
|
|
511
803
|
throw new Error("manifest client contribution must be an object");
|
|
512
|
-
const mounts = client.mounts;
|
|
513
|
-
if (!Array.isArray(mounts)) {
|
|
514
|
-
throw new Error("manifest client mounts must be an array");
|
|
515
|
-
}
|
|
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
804
|
if (client.kind === "iframe") {
|
|
533
805
|
if (!isV3OrLater(value)) {
|
|
534
806
|
throw new Error("manifest iframe client requires schema version 3");
|
|
535
807
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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",
|
|
808
|
+
if (isV5(value)) {
|
|
809
|
+
exactFields(
|
|
810
|
+
client,
|
|
811
|
+
["kind", "pages", "entries"],
|
|
812
|
+
"manifest iframe client contribution",
|
|
556
813
|
);
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
814
|
+
if (
|
|
815
|
+
!Array.isArray(client.pages) ||
|
|
816
|
+
client.pages.length === 0 ||
|
|
817
|
+
client.pages.length > PACKAGE_IFRAME_MAX_PAGES_V1
|
|
818
|
+
) {
|
|
819
|
+
throw new Error(
|
|
820
|
+
"manifest iframe client pages must be a non-empty bounded array",
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const pages = client.pages.map((candidate, index) => {
|
|
824
|
+
const label = `manifest iframe client pages[${index}]`;
|
|
825
|
+
if (!isRecord(candidate))
|
|
826
|
+
throw new Error(`${label} must be an object`);
|
|
827
|
+
exactFields(candidate, ["id", "artifact", "mounts"], label);
|
|
828
|
+
const id = requiredString(candidate, "id");
|
|
829
|
+
if (!PACKAGE_IFRAME_ID_V1.test(id)) {
|
|
830
|
+
throw new Error(`${label} id is invalid`);
|
|
831
|
+
}
|
|
832
|
+
return {
|
|
833
|
+
id,
|
|
834
|
+
artifact: decodeClientIframeArtifact(
|
|
835
|
+
candidate.artifact,
|
|
836
|
+
`${label} artifact`,
|
|
837
|
+
),
|
|
838
|
+
mounts: decodeClientMounts(candidate.mounts, `${label} mounts`),
|
|
839
|
+
};
|
|
840
|
+
});
|
|
841
|
+
if (new Set(pages.map((page) => page.id)).size !== pages.length) {
|
|
842
|
+
throw new Error("manifest iframe client pages contain duplicate ids");
|
|
843
|
+
}
|
|
844
|
+
const entries = decodeClientEntries(client.entries);
|
|
845
|
+
contributions.client = {
|
|
846
|
+
kind: "iframe",
|
|
847
|
+
pages,
|
|
848
|
+
...(entries ? { entries } : {}),
|
|
849
|
+
};
|
|
850
|
+
} else {
|
|
851
|
+
// Migration, not compatibility: the v3/v4 single-page record becomes
|
|
852
|
+
// the one in-memory multi-page shape and is written back as v5.
|
|
853
|
+
exactFields(
|
|
854
|
+
client,
|
|
855
|
+
["kind", "artifact", "mounts"],
|
|
856
|
+
"manifest iframe client contribution",
|
|
565
857
|
);
|
|
858
|
+
contributions.client = {
|
|
859
|
+
kind: "iframe",
|
|
860
|
+
pages: [
|
|
861
|
+
{
|
|
862
|
+
id: "main",
|
|
863
|
+
artifact: decodeClientIframeArtifact(
|
|
864
|
+
client.artifact,
|
|
865
|
+
"manifest iframe client artifact",
|
|
866
|
+
),
|
|
867
|
+
mounts: decodeClientMounts(
|
|
868
|
+
client.mounts,
|
|
869
|
+
"manifest client mount",
|
|
870
|
+
),
|
|
871
|
+
},
|
|
872
|
+
],
|
|
873
|
+
};
|
|
566
874
|
}
|
|
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
875
|
} else {
|
|
581
876
|
exactFields(
|
|
582
877
|
client,
|
|
@@ -585,7 +880,7 @@ function decodeV2(value: Record<string, unknown>): FrockBotManifest {
|
|
|
585
880
|
);
|
|
586
881
|
contributions.client = {
|
|
587
882
|
entry: relativeEntry(client, "entry"),
|
|
588
|
-
mounts:
|
|
883
|
+
mounts: decodeClientMounts(client.mounts, "manifest client mount"),
|
|
589
884
|
outlets: optionalStringArray(client, "outlets"),
|
|
590
885
|
};
|
|
591
886
|
}
|
|
@@ -625,12 +920,23 @@ function decodeV2(value: Record<string, unknown>): FrockBotManifest {
|
|
|
625
920
|
commands: optionalStringArray(desktop, "commands"),
|
|
626
921
|
};
|
|
627
922
|
}
|
|
923
|
+
if (value.contributions.instance !== undefined) {
|
|
924
|
+
if (!isV5(value)) {
|
|
925
|
+
throw new Error(
|
|
926
|
+
"manifest instance Contribution requires schema version 5",
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
contributions.instance = decodeInstanceContribution(
|
|
930
|
+
value.contributions.instance,
|
|
931
|
+
);
|
|
932
|
+
}
|
|
628
933
|
if (
|
|
629
934
|
!contributions.backend &&
|
|
630
935
|
!contributions.runtime &&
|
|
631
936
|
!contributions.client &&
|
|
632
937
|
!contributions.desktop &&
|
|
633
|
-
!contributions.mobile
|
|
938
|
+
!contributions.mobile &&
|
|
939
|
+
!contributions.instance
|
|
634
940
|
) {
|
|
635
941
|
throw new Error("manifest has no contributions");
|
|
636
942
|
}
|
|
@@ -1404,6 +1710,7 @@ function decodeV3(value: Record<string, unknown>): FrockBotManifest {
|
|
|
1404
1710
|
const base = decodeV2(value);
|
|
1405
1711
|
const tools = decodeManifestTools(value.tools);
|
|
1406
1712
|
const hooks = decodeManifestHooks(value.hooks);
|
|
1713
|
+
const roots = decodeManifestRoots(value.roots);
|
|
1407
1714
|
const botIsolate = base.contributions.runtime?.host === "bot-isolate";
|
|
1408
1715
|
if (botIsolate !== (tools !== undefined)) {
|
|
1409
1716
|
throw new Error(
|
|
@@ -1420,6 +1727,7 @@ function decodeV3(value: Record<string, unknown>): FrockBotManifest {
|
|
|
1420
1727
|
configuration: decodeConfiguration(value.configuration, false),
|
|
1421
1728
|
...(tools ? { tools } : {}),
|
|
1422
1729
|
...(hooks ? { hooks } : {}),
|
|
1730
|
+
...(roots ? { roots } : {}),
|
|
1423
1731
|
};
|
|
1424
1732
|
}
|
|
1425
1733
|
|
|
@@ -1428,6 +1736,7 @@ function decodeV4(value: Record<string, unknown>): FrockBotManifest {
|
|
|
1428
1736
|
const base = decodeV2(value);
|
|
1429
1737
|
const tools = decodeManifestTools(value.tools);
|
|
1430
1738
|
const hooks = decodeManifestHooks(value.hooks);
|
|
1739
|
+
const roots = decodeManifestRoots(value.roots);
|
|
1431
1740
|
const botIsolate = base.contributions.runtime?.host === "bot-isolate";
|
|
1432
1741
|
if (botIsolate !== (tools !== undefined)) {
|
|
1433
1742
|
throw new Error(
|
|
@@ -1444,6 +1753,40 @@ function decodeV4(value: Record<string, unknown>): FrockBotManifest {
|
|
|
1444
1753
|
configuration: decodeConfiguration(value.configuration, true),
|
|
1445
1754
|
...(tools ? { tools } : {}),
|
|
1446
1755
|
...(hooks ? { hooks } : {}),
|
|
1756
|
+
...(roots ? { roots } : {}),
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
/**
|
|
1761
|
+
* v5 is v4 plus the multi-page iframe client, declarative entries, and the
|
|
1762
|
+
* Instance Contribution. A v3 or v4 record decodes to the same in-memory
|
|
1763
|
+
* shape through the single-page migration in `decodeV2`.
|
|
1764
|
+
*/
|
|
1765
|
+
function decodeV5(value: Record<string, unknown>): FrockBotManifest {
|
|
1766
|
+
const base = decodeV2(value);
|
|
1767
|
+
const tools = decodeManifestTools(value.tools);
|
|
1768
|
+
const hooks = decodeManifestHooks(value.hooks);
|
|
1769
|
+
// Declared roots are v3 onward, so a v5 manifest carries them too. Dropping
|
|
1770
|
+
// them here would silently unmount the durable root a v5 Package declares —
|
|
1771
|
+
// for the Applets Package, the directory its source lives in.
|
|
1772
|
+
const roots = decodeManifestRoots(value.roots);
|
|
1773
|
+
const botIsolate = base.contributions.runtime?.host === "bot-isolate";
|
|
1774
|
+
if (botIsolate !== (tools !== undefined)) {
|
|
1775
|
+
throw new Error(
|
|
1776
|
+
"manifest bot-isolate runtime and tools declaration must appear together",
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
if (!botIsolate && hooks !== undefined) {
|
|
1780
|
+
throw new Error("manifest hooks require a bot-isolate runtime");
|
|
1781
|
+
}
|
|
1782
|
+
validateIframeClientContribution(base.contributions.client, tools);
|
|
1783
|
+
return {
|
|
1784
|
+
...base,
|
|
1785
|
+
schemaVersion: 5,
|
|
1786
|
+
configuration: decodeConfiguration(value.configuration, true),
|
|
1787
|
+
...(tools ? { tools } : {}),
|
|
1788
|
+
...(hooks ? { hooks } : {}),
|
|
1789
|
+
...(roots ? { roots } : {}),
|
|
1447
1790
|
};
|
|
1448
1791
|
}
|
|
1449
1792
|
|
|
@@ -1452,24 +1795,35 @@ function validateIframeClientContribution(
|
|
|
1452
1795
|
tools: ManifestToolDeclaration[] | undefined,
|
|
1453
1796
|
): void {
|
|
1454
1797
|
if (!client || !isClientIframeContribution(client)) return;
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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)) {
|
|
1798
|
+
const declaredTools = (tools ?? []).map((tool) => tool.name);
|
|
1799
|
+
const pageIds = client.pages.map((page) => page.id);
|
|
1800
|
+
for (const page of client.pages) {
|
|
1801
|
+
if (page.mounts.length === 0 || page.mounts.length > 64) {
|
|
1465
1802
|
throw new Error(
|
|
1466
|
-
`manifest iframe client
|
|
1803
|
+
`manifest iframe client page "${page.id}" mounts must be a non-empty bounded array`,
|
|
1467
1804
|
);
|
|
1468
1805
|
}
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1806
|
+
for (const mount of page.mounts) {
|
|
1807
|
+
if (!iframePageSlotAllowedV1(mount.slot, { declaredTools, pageIds })) {
|
|
1808
|
+
const toolResult = "frockbot.tool-result:";
|
|
1809
|
+
throw new Error(
|
|
1810
|
+
mount.slot.startsWith(toolResult)
|
|
1811
|
+
? `manifest iframe client tool-result slot names undeclared tool "${mount.slot.slice(toolResult.length)}"`
|
|
1812
|
+
: `manifest iframe client slot "${mount.slot}" is not iframe-safe`,
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
for (const entry of client.entries ?? []) {
|
|
1818
|
+
const target = client.pages.find((page) => page.id === entry.opens.page);
|
|
1819
|
+
if (
|
|
1820
|
+
!target ||
|
|
1821
|
+
!target.mounts.some(
|
|
1822
|
+
(mount) => mount.slot === `frockbot.surface:${entry.opens.page}`,
|
|
1823
|
+
)
|
|
1824
|
+
) {
|
|
1471
1825
|
throw new Error(
|
|
1472
|
-
`manifest iframe client
|
|
1826
|
+
`manifest iframe client entry "${entry.id}" opens page "${entry.opens.page}", which declares no surface mount`,
|
|
1473
1827
|
);
|
|
1474
1828
|
}
|
|
1475
1829
|
}
|
|
@@ -1495,7 +1849,8 @@ export function decodeFrockBotManifest(value: unknown): FrockBotManifest {
|
|
|
1495
1849
|
if (
|
|
1496
1850
|
value.schemaVersion === 2 ||
|
|
1497
1851
|
value.schemaVersion === 3 ||
|
|
1498
|
-
value.schemaVersion === 4
|
|
1852
|
+
value.schemaVersion === 4 ||
|
|
1853
|
+
value.schemaVersion === 5
|
|
1499
1854
|
) {
|
|
1500
1855
|
exactFields(
|
|
1501
1856
|
value,
|
|
@@ -1512,12 +1867,14 @@ export function decodeFrockBotManifest(value: unknown): FrockBotManifest {
|
|
|
1512
1867
|
...(isV3OrLater(value) ? ["configuration"] : []),
|
|
1513
1868
|
...(isV3OrLater(value) ? ["tools"] : []),
|
|
1514
1869
|
...(isV3OrLater(value) ? ["hooks"] : []),
|
|
1870
|
+
...(isV3OrLater(value) ? ["roots"] : []),
|
|
1515
1871
|
],
|
|
1516
1872
|
"manifest",
|
|
1517
1873
|
);
|
|
1518
1874
|
if (value.schemaVersion === 2) return decodeV2(value);
|
|
1519
1875
|
if (value.schemaVersion === 3) return decodeV3(value);
|
|
1520
|
-
return decodeV4(value);
|
|
1876
|
+
if (value.schemaVersion === 4) return decodeV4(value);
|
|
1877
|
+
return decodeV5(value);
|
|
1521
1878
|
}
|
|
1522
1879
|
throw new Error("unsupported FrockBot manifest version");
|
|
1523
1880
|
}
|