@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/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
- expect(client?.mounts[0]).toEqual({
332
- slot: "frockbot.tool-result:weather_lookup",
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: 5,
1118
+ schemaVersion: 6,
765
1119
  id: "future",
766
1120
  displayName: "Future",
767
1121
  version: "1.0.0",
@@ -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-v4";
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";