@frockbot/kernel-composition 0.3.0 → 0.3.2
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-wrapper.ts +28 -1
- package/src/manifest.ts +442 -85
package/src/index.test.ts
CHANGED
|
@@ -125,6 +125,63 @@ describe("PackageCatalog", () => {
|
|
|
125
125
|
).toThrow('manifest v2 desktop execution must be "sandboxed-renderer"');
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
// Constitution — Computer and Workspace: "durable roots, declared by the
|
|
129
|
+
// Computer Package's Workspace layout **and by Package manifests**". This is
|
|
130
|
+
// the manifest half; `image` and `applets` are its two callers today.
|
|
131
|
+
test("decodes the durable roots a Package declares, and refuses a malformed one", () => {
|
|
132
|
+
const base = {
|
|
133
|
+
schemaVersion: 4 as const,
|
|
134
|
+
id: "image",
|
|
135
|
+
displayName: "Image generation",
|
|
136
|
+
version: "1.0.0",
|
|
137
|
+
compatibility: { frockbot: "*" },
|
|
138
|
+
contributions: { runtime: { entry: "./agent" } },
|
|
139
|
+
permissions: [],
|
|
140
|
+
};
|
|
141
|
+
expect(
|
|
142
|
+
decodeFrockBotManifest({
|
|
143
|
+
...base,
|
|
144
|
+
roots: [{ id: "generated", scope: "user" }],
|
|
145
|
+
}).roots,
|
|
146
|
+
).toEqual([{ id: "generated", scope: "user" }]);
|
|
147
|
+
// Absent stays absent: a Package that declares no root has none, and the
|
|
148
|
+
// sync must not invent one.
|
|
149
|
+
expect(decodeFrockBotManifest(base).roots).toBeUndefined();
|
|
150
|
+
for (const [roots, message] of [
|
|
151
|
+
[[], "non-empty bounded array"],
|
|
152
|
+
[[{ id: "Generated", scope: "user" }], "id is invalid"],
|
|
153
|
+
[[{ id: "-bad", scope: "user" }], "id is invalid"],
|
|
154
|
+
// A `package-declared` root names no Bot, so a Bot-scoped one would be a
|
|
155
|
+
// root `WorkspaceRootV1` cannot address.
|
|
156
|
+
[[{ id: "generated", scope: "bot" }], 'scope must be "user"'],
|
|
157
|
+
[[{ id: "generated" }], 'scope must be "user"'],
|
|
158
|
+
[
|
|
159
|
+
[
|
|
160
|
+
{ id: "generated", scope: "user" },
|
|
161
|
+
{ id: "generated", scope: "user" },
|
|
162
|
+
],
|
|
163
|
+
"duplicate ids",
|
|
164
|
+
],
|
|
165
|
+
] as const) {
|
|
166
|
+
expect(() => decodeFrockBotManifest({ ...base, roots })).toThrow(message);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("keeps a declared root out of reach of a v2 manifest", () => {
|
|
171
|
+
expect(() =>
|
|
172
|
+
decodeFrockBotManifest({
|
|
173
|
+
schemaVersion: 2,
|
|
174
|
+
id: "legacy",
|
|
175
|
+
displayName: "Legacy",
|
|
176
|
+
version: "1.0.0",
|
|
177
|
+
compatibility: { frockbot: "*" },
|
|
178
|
+
contributions: { runtime: { entry: "./agent" } },
|
|
179
|
+
permissions: [],
|
|
180
|
+
roots: [{ id: "generated", scope: "user" }],
|
|
181
|
+
}),
|
|
182
|
+
).toThrow('manifest has unknown field "roots"');
|
|
183
|
+
});
|
|
184
|
+
|
|
128
185
|
test("keeps backend Contributions unavailable to v2 manifests", () => {
|
|
129
186
|
expect(() =>
|
|
130
187
|
decodeFrockBotManifest({
|
|
@@ -328,9 +385,22 @@ describe("decodeFrockBotManifest", () => {
|
|
|
328
385
|
const decoded = decodeFrockBotManifest(manifest);
|
|
329
386
|
const client = decoded.contributions.client;
|
|
330
387
|
expect(client && "kind" in client ? client.kind : undefined).toBe("iframe");
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
388
|
+
// Migration: the v3 single-page record decodes to the one v5 shape.
|
|
389
|
+
expect(client && "pages" in client ? client.pages : undefined).toEqual([
|
|
390
|
+
{
|
|
391
|
+
id: "main",
|
|
392
|
+
artifact: {
|
|
393
|
+
contentHash: "a".repeat(64),
|
|
394
|
+
size: 123,
|
|
395
|
+
mediaType: "text/html",
|
|
396
|
+
bundlerVersion: "frockbot-inline-html@1",
|
|
397
|
+
},
|
|
398
|
+
mounts: [
|
|
399
|
+
{ slot: "frockbot.tool-result:weather_lookup" },
|
|
400
|
+
{ slot: "frockbot.bot-settings-sections", order: 10 },
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
]);
|
|
334
404
|
expect(decoded.hooks).toEqual(["agent/tool-exposure"]);
|
|
335
405
|
expect(() =>
|
|
336
406
|
decodeFrockBotManifest({
|
|
@@ -373,6 +443,290 @@ describe("decodeFrockBotManifest", () => {
|
|
|
373
443
|
).toThrow("256 KB quota");
|
|
374
444
|
});
|
|
375
445
|
|
|
446
|
+
// The literal previous released shape of a stored iframe Contribution. It
|
|
447
|
+
// is written out rather than derived so a change to the v5 decoder that
|
|
448
|
+
// silently stopped migrating it would fail here.
|
|
449
|
+
const releasedV4IframeManifest = () => ({
|
|
450
|
+
schemaVersion: 4,
|
|
451
|
+
id: "weather-page",
|
|
452
|
+
displayName: "Weather page",
|
|
453
|
+
version: "0.0.1",
|
|
454
|
+
compatibility: { frockbot: ">=0.0.1" },
|
|
455
|
+
dependencies: {},
|
|
456
|
+
contributions: {
|
|
457
|
+
runtime: { entry: "./package.js", host: "bot-isolate" },
|
|
458
|
+
client: {
|
|
459
|
+
kind: "iframe",
|
|
460
|
+
artifact: {
|
|
461
|
+
contentHash: "b".repeat(64),
|
|
462
|
+
size: 42,
|
|
463
|
+
mediaType: "text/html",
|
|
464
|
+
bundlerVersion: "frockbot-inline-html@1",
|
|
465
|
+
},
|
|
466
|
+
mounts: [{ slot: "frockbot.tool-result:weather_lookup", order: 3 }],
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
tools: [
|
|
470
|
+
{ name: "weather_lookup", description: "Weather", inputSchema: {} },
|
|
471
|
+
],
|
|
472
|
+
permissions: [],
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("migrates a released v4 single-page iframe record to v5 pages", () => {
|
|
476
|
+
const decoded = decodeFrockBotManifest(releasedV4IframeManifest());
|
|
477
|
+
const client = decoded.contributions.client;
|
|
478
|
+
expect(client && "pages" in client ? client.pages : undefined).toEqual([
|
|
479
|
+
{
|
|
480
|
+
id: "main",
|
|
481
|
+
artifact: {
|
|
482
|
+
contentHash: "b".repeat(64),
|
|
483
|
+
size: 42,
|
|
484
|
+
mediaType: "text/html",
|
|
485
|
+
bundlerVersion: "frockbot-inline-html@1",
|
|
486
|
+
},
|
|
487
|
+
mounts: [{ slot: "frockbot.tool-result:weather_lookup", order: 3 }],
|
|
488
|
+
},
|
|
489
|
+
]);
|
|
490
|
+
expect(client && "entries" in client ? client.entries : undefined).toBe(
|
|
491
|
+
undefined,
|
|
492
|
+
);
|
|
493
|
+
// One in-memory shape: the migrated record round-trips as v5.
|
|
494
|
+
const roundTripped = decodeFrockBotManifest({
|
|
495
|
+
...releasedV4IframeManifest(),
|
|
496
|
+
schemaVersion: 5,
|
|
497
|
+
contributions: {
|
|
498
|
+
runtime: { entry: "./package.js", host: "bot-isolate" },
|
|
499
|
+
client: {
|
|
500
|
+
kind: "iframe",
|
|
501
|
+
pages: client && "pages" in client ? client.pages : [],
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
expect(roundTripped.contributions.client).toEqual(
|
|
506
|
+
decoded.contributions.client!,
|
|
507
|
+
);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test("refuses a v4 record that declares v5 pages or an instance", () => {
|
|
511
|
+
expect(() =>
|
|
512
|
+
decodeFrockBotManifest({
|
|
513
|
+
...releasedV4IframeManifest(),
|
|
514
|
+
contributions: {
|
|
515
|
+
runtime: { entry: "./package.js", host: "bot-isolate" },
|
|
516
|
+
client: { kind: "iframe", pages: [] },
|
|
517
|
+
},
|
|
518
|
+
}),
|
|
519
|
+
).toThrow("unknown field");
|
|
520
|
+
expect(() =>
|
|
521
|
+
decodeFrockBotManifest({
|
|
522
|
+
...releasedV4IframeManifest(),
|
|
523
|
+
contributions: {
|
|
524
|
+
...releasedV4IframeManifest().contributions,
|
|
525
|
+
instance: {
|
|
526
|
+
contract: 1,
|
|
527
|
+
server: {
|
|
528
|
+
contentHash: "c".repeat(64),
|
|
529
|
+
size: 10,
|
|
530
|
+
mediaType: "application/javascript",
|
|
531
|
+
bundlerVersion: "frockbot-esbuild@1",
|
|
532
|
+
},
|
|
533
|
+
ui: {
|
|
534
|
+
contentHash: "d".repeat(64),
|
|
535
|
+
size: 10,
|
|
536
|
+
mediaType: "text/html",
|
|
537
|
+
bundlerVersion: "frockbot-inline-html@1",
|
|
538
|
+
},
|
|
539
|
+
tools: [{ name: "add_todo", description: "Add", inputSchema: {} }],
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
}),
|
|
543
|
+
).toThrow("schema version 5");
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const v5Manifest = () => ({
|
|
547
|
+
schemaVersion: 5,
|
|
548
|
+
id: "applets",
|
|
549
|
+
displayName: "Applets",
|
|
550
|
+
version: "0.0.1",
|
|
551
|
+
compatibility: { frockbot: ">=0.0.1" },
|
|
552
|
+
dependencies: {},
|
|
553
|
+
contributions: {
|
|
554
|
+
runtime: { entry: "./package.js", host: "bot-isolate" },
|
|
555
|
+
client: {
|
|
556
|
+
kind: "iframe",
|
|
557
|
+
pages: [
|
|
558
|
+
{
|
|
559
|
+
id: "list",
|
|
560
|
+
artifact: {
|
|
561
|
+
contentHash: "a".repeat(64),
|
|
562
|
+
size: 10,
|
|
563
|
+
mediaType: "text/html",
|
|
564
|
+
bundlerVersion: "frockbot-inline-html@1",
|
|
565
|
+
},
|
|
566
|
+
mounts: [{ slot: "frockbot.surface:list" }],
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
id: "canvas",
|
|
570
|
+
artifact: {
|
|
571
|
+
contentHash: "b".repeat(64),
|
|
572
|
+
size: 10,
|
|
573
|
+
mediaType: "text/html",
|
|
574
|
+
bundlerVersion: "frockbot-inline-html@1",
|
|
575
|
+
},
|
|
576
|
+
mounts: [{ slot: "frockbot.right-panel" }],
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
entries: [
|
|
580
|
+
{
|
|
581
|
+
id: "open",
|
|
582
|
+
slot: "frockbot.sidebar-actions",
|
|
583
|
+
order: 5,
|
|
584
|
+
label: "Applets",
|
|
585
|
+
icon: "applets",
|
|
586
|
+
opens: { kind: "surface", page: "list" },
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
},
|
|
590
|
+
instance: {
|
|
591
|
+
contract: 1,
|
|
592
|
+
server: {
|
|
593
|
+
contentHash: "c".repeat(64),
|
|
594
|
+
size: 10,
|
|
595
|
+
mediaType: "application/javascript",
|
|
596
|
+
bundlerVersion: "frockbot-esbuild@1",
|
|
597
|
+
},
|
|
598
|
+
ui: {
|
|
599
|
+
contentHash: "d".repeat(64),
|
|
600
|
+
size: 10,
|
|
601
|
+
mediaType: "text/html",
|
|
602
|
+
bundlerVersion: "frockbot-inline-html@1",
|
|
603
|
+
},
|
|
604
|
+
tools: [{ name: "add_todo", description: "Add", inputSchema: {} }],
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
tools: [{ name: "applet_list", description: "List", inputSchema: {} }],
|
|
608
|
+
permissions: [],
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test("decodes a v5 multi-page client, entries, and Instance Contribution", () => {
|
|
612
|
+
const decoded = decodeFrockBotManifest(v5Manifest());
|
|
613
|
+
expect(decoded.schemaVersion).toBe(5);
|
|
614
|
+
const client = decoded.contributions.client;
|
|
615
|
+
expect(
|
|
616
|
+
client && "pages" in client ? client.pages.map((p) => p.id) : [],
|
|
617
|
+
).toEqual(["list", "canvas"]);
|
|
618
|
+
expect(client && "entries" in client ? client.entries : undefined).toEqual([
|
|
619
|
+
{
|
|
620
|
+
id: "open",
|
|
621
|
+
slot: "frockbot.sidebar-actions",
|
|
622
|
+
order: 5,
|
|
623
|
+
label: "Applets",
|
|
624
|
+
icon: "applets",
|
|
625
|
+
opens: { kind: "surface", page: "list" },
|
|
626
|
+
},
|
|
627
|
+
]);
|
|
628
|
+
expect(decoded.contributions.instance?.contract).toBe(1);
|
|
629
|
+
expect(decoded.contributions.instance?.tools.map((t) => t.name)).toEqual([
|
|
630
|
+
"add_todo",
|
|
631
|
+
]);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test("refuses v5 pages and entries that break their bounds", () => {
|
|
635
|
+
const base = v5Manifest();
|
|
636
|
+
const withPages = (pages: unknown) => ({
|
|
637
|
+
...base,
|
|
638
|
+
contributions: {
|
|
639
|
+
...base.contributions,
|
|
640
|
+
client: { ...base.contributions.client, pages },
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
expect(() => decodeFrockBotManifest(withPages([]))).toThrow(
|
|
644
|
+
"non-empty bounded array",
|
|
645
|
+
);
|
|
646
|
+
expect(() =>
|
|
647
|
+
decodeFrockBotManifest(
|
|
648
|
+
withPages(
|
|
649
|
+
Array.from({ length: 9 }, (_unused, index) => ({
|
|
650
|
+
...base.contributions.client.pages[0]!,
|
|
651
|
+
id: `page-${index}`,
|
|
652
|
+
mounts: [{ slot: "frockbot.right-panel" }],
|
|
653
|
+
})),
|
|
654
|
+
),
|
|
655
|
+
),
|
|
656
|
+
).toThrow("non-empty bounded array");
|
|
657
|
+
expect(() =>
|
|
658
|
+
decodeFrockBotManifest(
|
|
659
|
+
withPages([
|
|
660
|
+
base.contributions.client.pages[0]!,
|
|
661
|
+
{ ...base.contributions.client.pages[1]!, id: "list" },
|
|
662
|
+
]),
|
|
663
|
+
),
|
|
664
|
+
).toThrow("duplicate ids");
|
|
665
|
+
expect(() =>
|
|
666
|
+
decodeFrockBotManifest(
|
|
667
|
+
withPages([
|
|
668
|
+
{ ...base.contributions.client.pages[0]!, id: "List" },
|
|
669
|
+
base.contributions.client.pages[1]!,
|
|
670
|
+
]),
|
|
671
|
+
),
|
|
672
|
+
).toThrow("id is invalid");
|
|
673
|
+
expect(() =>
|
|
674
|
+
decodeFrockBotManifest(
|
|
675
|
+
withPages([
|
|
676
|
+
{
|
|
677
|
+
...base.contributions.client.pages[0]!,
|
|
678
|
+
mounts: [{ slot: "frockbot.surface:missing" }],
|
|
679
|
+
},
|
|
680
|
+
base.contributions.client.pages[1]!,
|
|
681
|
+
]),
|
|
682
|
+
),
|
|
683
|
+
).toThrow("not iframe-safe");
|
|
684
|
+
const withEntries = (entries: unknown) => ({
|
|
685
|
+
...base,
|
|
686
|
+
contributions: {
|
|
687
|
+
...base.contributions,
|
|
688
|
+
client: { ...base.contributions.client, entries },
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
expect(() =>
|
|
692
|
+
decodeFrockBotManifest(
|
|
693
|
+
withEntries([
|
|
694
|
+
{
|
|
695
|
+
...base.contributions.client.entries[0]!,
|
|
696
|
+
opens: { kind: "surface", page: "canvas" },
|
|
697
|
+
},
|
|
698
|
+
]),
|
|
699
|
+
),
|
|
700
|
+
).toThrow("declares no surface mount");
|
|
701
|
+
expect(() =>
|
|
702
|
+
decodeFrockBotManifest(
|
|
703
|
+
withEntries([
|
|
704
|
+
{
|
|
705
|
+
...base.contributions.client.entries[0]!,
|
|
706
|
+
label: "x".repeat(33),
|
|
707
|
+
},
|
|
708
|
+
]),
|
|
709
|
+
),
|
|
710
|
+
).toThrow("label is too long");
|
|
711
|
+
expect(() =>
|
|
712
|
+
decodeFrockBotManifest(
|
|
713
|
+
withEntries([
|
|
714
|
+
{ ...base.contributions.client.entries[0]!, slot: "frockbot.root" },
|
|
715
|
+
]),
|
|
716
|
+
),
|
|
717
|
+
).toThrow("slot is invalid");
|
|
718
|
+
expect(() =>
|
|
719
|
+
decodeFrockBotManifest(
|
|
720
|
+
withEntries(
|
|
721
|
+
Array.from({ length: 5 }, (_unused, index) => ({
|
|
722
|
+
...base.contributions.client.entries[0]!,
|
|
723
|
+
id: `open-${index}`,
|
|
724
|
+
})),
|
|
725
|
+
),
|
|
726
|
+
),
|
|
727
|
+
).toThrow("bounded array");
|
|
728
|
+
});
|
|
729
|
+
|
|
376
730
|
test("keeps trusted Electron main execution exclusive to manifest v3", () => {
|
|
377
731
|
const contribution = {
|
|
378
732
|
desktop: {
|
|
@@ -761,7 +1115,7 @@ describe("decodeFrockBotManifest", () => {
|
|
|
761
1115
|
test("rejects an unsupported manifest version", () => {
|
|
762
1116
|
expect(() =>
|
|
763
1117
|
decodeFrockBotManifest({
|
|
764
|
-
schemaVersion:
|
|
1118
|
+
schemaVersion: 6,
|
|
765
1119
|
id: "future",
|
|
766
1120
|
displayName: "Future",
|
|
767
1121
|
version: "1.0.0",
|
package/src/isolate-wrapper.ts
CHANGED
|
@@ -200,6 +200,33 @@ const BOT_ISOLATE_CONTEXT_PROPERTY_SOURCE_V1 = {
|
|
|
200
200
|
return capabilities.workspaceDelete(request);
|
|
201
201
|
},
|
|
202
202
|
}`,
|
|
203
|
+
applets: `{
|
|
204
|
+
list: function () {
|
|
205
|
+
return capabilities.applets({ op: "list" });
|
|
206
|
+
},
|
|
207
|
+
create: function (input) {
|
|
208
|
+
return capabilities.applets({ op: "create", displayName: input.displayName });
|
|
209
|
+
},
|
|
210
|
+
publish: function (input) {
|
|
211
|
+
return capabilities.applets({ op: "publish", appletId: input.appletId });
|
|
212
|
+
},
|
|
213
|
+
revert: function (input) {
|
|
214
|
+
return capabilities.applets({
|
|
215
|
+
op: "revert",
|
|
216
|
+
appletId: input.appletId,
|
|
217
|
+
generationId: input.generationId,
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
delete: function (input) {
|
|
221
|
+
return capabilities.applets({ op: "delete", appletId: input.appletId });
|
|
222
|
+
},
|
|
223
|
+
focus: function (input) {
|
|
224
|
+
return capabilities.applets({ op: "focus", appletId: input.appletId });
|
|
225
|
+
},
|
|
226
|
+
generations: function (input) {
|
|
227
|
+
return capabilities.applets({ op: "generations", appletId: input.appletId });
|
|
228
|
+
},
|
|
229
|
+
}`,
|
|
203
230
|
connection:
|
|
204
231
|
"function (connectionId) { return capabilities.connection(connectionId); }",
|
|
205
232
|
notify: "function (request) { return capabilities.notify(request); }",
|
|
@@ -354,7 +381,7 @@ export default class extends WorkerEntrypoint {
|
|
|
354
381
|
`;
|
|
355
382
|
|
|
356
383
|
/** Bumped with any change to the wrapper text; folded into the loader id. */
|
|
357
|
-
export const BOT_ISOLATE_WRAPPER_VERSION = "wrapper-
|
|
384
|
+
export const BOT_ISOLATE_WRAPPER_VERSION = "wrapper-v5";
|
|
358
385
|
|
|
359
386
|
export const BOT_ISOLATE_MAIN_MODULE = "index.js";
|
|
360
387
|
export const BOT_ISOLATE_PACKAGE_MODULE = "package.js";
|