@frockbot/plugin-shell 0.1.4 → 0.2.1

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.
@@ -0,0 +1,41 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { PackageIframeHostMessageV1 } from "@frockbot/kernel-contracts";
3
+ import { postPackageIframeHostMessage } from "./package-iframe-host-message.js";
4
+
5
+ describe("Package iframe host messages", () => {
6
+ test("posts unchanged state once and posts changed state again", () => {
7
+ const posted: PackageIframeHostMessageV1[] = [];
8
+ const target = {
9
+ postMessage(message: PackageIframeHostMessageV1): void {
10
+ posted.push(message);
11
+ },
12
+ } as Pick<Window, "postMessage">;
13
+ const lastStateWireByName = new Map<string, string>();
14
+
15
+ const postSettings = (value: unknown): void =>
16
+ postPackageIframeHostMessage(
17
+ target,
18
+ { schemaVersion: 1, type: "state", name: "settings", value },
19
+ lastStateWireByName,
20
+ );
21
+
22
+ postSettings({ temperatureUnit: "celsius" });
23
+ postSettings({ temperatureUnit: "celsius" });
24
+ postSettings({ temperatureUnit: "fahrenheit" });
25
+
26
+ expect(posted).toEqual([
27
+ {
28
+ schemaVersion: 1,
29
+ type: "state",
30
+ name: "settings",
31
+ value: { temperatureUnit: "celsius" },
32
+ },
33
+ {
34
+ schemaVersion: 1,
35
+ type: "state",
36
+ name: "settings",
37
+ value: { temperatureUnit: "fahrenheit" },
38
+ },
39
+ ]);
40
+ });
41
+ });
@@ -0,0 +1,27 @@
1
+ import type { PackageIframeHostMessageV1 } from "@frockbot/kernel-contracts";
2
+
3
+ type MessageTarget = Pick<Window, "postMessage">;
4
+
5
+ export function postPackageIframeHostMessage(
6
+ target: MessageTarget,
7
+ message: PackageIframeHostMessageV1,
8
+ lastStateWireByName: Map<string, string>,
9
+ ): void {
10
+ const wire = JSON.stringify(message);
11
+ if (new TextEncoder().encode(wire).byteLength > 64 * 1024) {
12
+ throw new Error("Package page state exceeds the bridge limit");
13
+ }
14
+ if (
15
+ message.type === "state" &&
16
+ lastStateWireByName.get(message.name) === wire
17
+ ) {
18
+ return;
19
+ }
20
+
21
+ // Vue settings values may be reactive proxies, which structured clone
22
+ // rejects. JSON is also the bridge's declared value domain.
23
+ target.postMessage(JSON.parse(wire) as PackageIframeHostMessageV1, "*");
24
+ if (message.type === "state") {
25
+ lastStateWireByName.set(message.name, wire);
26
+ }
27
+ }
@@ -9,6 +9,11 @@ import {
9
9
  type CompositionMemberViewV1,
10
10
  type CompositionProvenanceViewV1,
11
11
  } from "@frockbot/configuration-core";
12
+ import type { PackageIframeCompositionV1 } from "@frockbot/kernel-contracts";
13
+ import {
14
+ isClientIframeContribution,
15
+ type FrockBotManifest,
16
+ } from "@frockbot/kernel-composition";
12
17
  import type {
13
18
  CompositionFailureV1,
14
19
  CompositionQuarantineV1,
@@ -29,11 +34,57 @@ export type CompositionMemberSourceReaderV1 = (
29
34
  member: CompositionMemberV1,
30
35
  ) => Promise<string | undefined>;
31
36
 
37
+ /** One decoded manifest lookup shared by isolate mount and hosted UI views. */
38
+ export type CompositionMemberManifestReaderV1 = (
39
+ member: CompositionMemberV1,
40
+ ) => Promise<FrockBotManifest | undefined>;
41
+
42
+ /** Project iframe Contributions from either Bot or Catalog provenance. */
43
+ export async function projectPackageIframeCompositionV1(input: {
44
+ botId: string;
45
+ generation: CompositionGenerationV1;
46
+ readMemberManifest: CompositionMemberManifestReaderV1;
47
+ }): Promise<PackageIframeCompositionV1> {
48
+ const contributions: PackageIframeCompositionV1["contributions"] = [];
49
+ for (const member of input.generation.members) {
50
+ if (member.provenance.kind === "first-party") continue;
51
+ const manifest = await input.readMemberManifest(member);
52
+ if (!manifest) continue;
53
+ const client = manifest.contributions.client;
54
+ if (!client || !isClientIframeContribution(client)) continue;
55
+ contributions.push({
56
+ packageId: member.packageId,
57
+ displayName: manifest.displayName,
58
+ provenance:
59
+ member.provenance.kind === "bot" ? "Bot-authored" : "User-installed",
60
+ artifact: { ...client.artifact },
61
+ mounts: client.mounts.map((mount) => ({ ...mount })),
62
+ declaredTools: (manifest.tools ?? []).map((tool) => tool.name),
63
+ });
64
+ }
65
+ contributions.sort((left, right) =>
66
+ left.packageId.localeCompare(right.packageId),
67
+ );
68
+ return {
69
+ schemaVersion: 1,
70
+ botId: input.botId,
71
+ generationId: input.generation.generationId,
72
+ contributions,
73
+ };
74
+ }
75
+
32
76
  function provenanceView(
33
77
  member: CompositionMemberV1,
34
78
  ): CompositionProvenanceViewV1 {
35
79
  const provenance = member.provenance;
36
80
  if (provenance.kind === "first-party") return { kind: "first-party" };
81
+ if (provenance.kind === "catalog") {
82
+ return {
83
+ kind: "catalog",
84
+ catalogId: provenance.catalogId,
85
+ catalogGeneration: provenance.catalogGeneration,
86
+ };
87
+ }
37
88
  if (provenance.kind === "user") {
38
89
  return {
39
90
  kind: "user",
@@ -102,6 +153,9 @@ export async function projectCompositionGenerationV1(
102
153
  isCurrent: input.generation.generationId === input.currentGenerationId,
103
154
  members,
104
155
  failures,
156
+ ...(input.generation.summary === undefined
157
+ ? {}
158
+ : { summary: input.generation.summary }),
105
159
  ...(input.quarantine === undefined
106
160
  ? {}
107
161
  : {
package/src/shared.ts CHANGED
@@ -14,6 +14,8 @@ import type {
14
14
  CatalogIndexEntryV1,
15
15
  } from "@frockbot/catalog-core";
16
16
  import type {
17
+ PackageIframeCatalogV1,
18
+ PackageIframeContributionViewV1,
17
19
  SendToUserPayloadV1,
18
20
  SkillRefV1,
19
21
  } from "@frockbot/kernel-contracts";
@@ -187,6 +189,8 @@ export interface FrockBotWebData {
187
189
  * without the run's own events being rewritten.
188
190
  */
189
191
  tasks: TaskViewV1[];
192
+ /** Sandboxed pages in the selected Bot's active fail-closed Composition. */
193
+ packageUi?: PackageIframeCatalogV1;
190
194
  /**
191
195
  * The User's MCP servers: state, tool count, last handshake, instructions,
192
196
  * failure, and the durable refusal ledger. Absent until it is loaded, and
@@ -240,6 +244,12 @@ export interface FrockBotWebData {
240
244
  loadApprovals(): Promise<void>;
241
245
  /** Refreshes {@link FrockBotWebData.tasks} for the active Bot. */
242
246
  loadTasks(): Promise<void>;
247
+ loadPackageUi(): Promise<void>;
248
+ callPackageUiTool(
249
+ contribution: PackageIframeContributionViewV1,
250
+ name: string,
251
+ input: unknown,
252
+ ): Promise<unknown>;
243
253
  /**
244
254
  * Cancels one subagent, explicitly and with the User's authentication. The
245
255
  * backend is the authority: the task this replaces in the list is the record