@frockbot/plugin-authoring 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-authoring",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,7 +21,7 @@
21
21
  "typecheck": "tsc --noEmit -p tsconfig.json"
22
22
  },
23
23
  "dependencies": {
24
- "@frockbot/kernel-contracts": "0.1.3",
24
+ "@frockbot/kernel-contracts": "0.2.0",
25
25
  "cordis": "4.0.0-rc.8"
26
26
  },
27
27
  "devDependencies": {
package/src/agent.test.ts CHANGED
@@ -3,16 +3,18 @@ import { SessionStore, type Session } from "@frockbot/kernel-contracts";
3
3
  import { Context } from "cordis";
4
4
  import {
5
5
  createPackageAuthorTool,
6
+ createPackageInspectSelfTool,
7
+ createPackageUndoTool,
6
8
  openTurnPositionV1,
7
9
  type AuthorPackageRequestV1,
8
10
  type PackageAuthoringHost,
9
11
  } from "./agent.ts";
10
- import type { AuthorPackageOutcomeV1 } from "./shared.ts";
12
+ import { sha256HexV1, type AuthorPackageOutcomeV1 } from "./shared.ts";
11
13
 
12
14
  const INPUT = {
13
15
  packageId: "weather-lookup",
14
16
  displayName: "Weather lookup",
15
- tool: { name: "weather_lookup", description: "Looks up", inputSchema: {} },
17
+ tools: [{ name: "weather_lookup", description: "Looks up", inputSchema: {} }],
16
18
  source: "export const tools = [];\nexport async function execute() {}\n",
17
19
  };
18
20
 
@@ -55,6 +57,24 @@ function stubHost(
55
57
  seen.push(request);
56
58
  return Promise.resolve(outcome);
57
59
  },
60
+ undoEffectIdFor: () => Promise.resolve("undo-0123456789abcdef"),
61
+ undo: () =>
62
+ Promise.resolve({
63
+ status: "recorded",
64
+ effectId: "undo-0123456789abcdef",
65
+ generationId: "revert-generation",
66
+ targetGenerationId: "target-generation",
67
+ }),
68
+ inspectSelf: () =>
69
+ Promise.resolve({
70
+ contextContract: "interface BotPackageExecutionContextV1 {}",
71
+ composition: {
72
+ generationId: "current-generation",
73
+ status: "active",
74
+ members: [],
75
+ },
76
+ failures: [],
77
+ }),
58
78
  };
59
79
  }
60
80
 
@@ -95,6 +115,47 @@ describe("the package_author tool", () => {
95
115
  await dispose();
96
116
  });
97
117
 
118
+ test("includes the UI HTML hash and declared hooks in the effect identity", async () => {
119
+ const { sessions, dispose } = await openSession();
120
+ const effectInputs: Parameters<PackageAuthoringHost["effectIdFor"]>[0][] =
121
+ [];
122
+ const host = stubHost({
123
+ status: "authored",
124
+ packageId: "weather-lookup",
125
+ version: "0.0.1",
126
+ contentHash: "b".repeat(64),
127
+ generationId: "2026-08-31T01:00:00.000Z:fedcba9876543210",
128
+ });
129
+ host.effectIdFor = (input) => {
130
+ effectInputs.push(input);
131
+ return Promise.resolve("author-0123456789abcdef");
132
+ };
133
+ const tool = createPackageAuthorTool(host, sessions);
134
+ const html = "<!doctype html><h1>Weather</h1>";
135
+
136
+ await tool.execute(
137
+ {
138
+ ...INPUT,
139
+ hooks: ["agent/tool-exposure"],
140
+ ui: {
141
+ html,
142
+ mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
143
+ },
144
+ },
145
+ CONTEXT,
146
+ );
147
+
148
+ expect(effectInputs).toEqual([
149
+ {
150
+ packageId: "weather-lookup",
151
+ sourceHash: await sha256HexV1(INPUT.source),
152
+ uiHtmlHash: await sha256HexV1(html),
153
+ hooks: ["agent/tool-exposure"],
154
+ },
155
+ ]);
156
+ await dispose();
157
+ });
158
+
98
159
  test("a refusal leaves the intent recorded and no authored event", async () => {
99
160
  const { session, sessions, dispose } = await openSession();
100
161
  const tool = createPackageAuthorTool(
@@ -160,3 +221,41 @@ describe("the package_author tool", () => {
160
221
  await root.fiber.dispose();
161
222
  });
162
223
  });
224
+
225
+ describe("the Package setup companion tools", () => {
226
+ test("package_undo records intent before the durable revert outcome", async () => {
227
+ const { session, sessions, dispose } = await openSession();
228
+ const host = stubHost({
229
+ status: "refused",
230
+ reason: "unused",
231
+ failureId: "unused",
232
+ });
233
+ const tool = createPackageUndoTool(host, sessions);
234
+
235
+ const result = await tool.execute({}, CONTEXT);
236
+
237
+ expect(result.isError).toBe(false);
238
+ expect(result.content).toContain("activates on the next Turn");
239
+ expect(result.content).toContain("did not undo any action");
240
+ expect(session.events.slice(-2).map((event) => event.type)).toEqual([
241
+ "package/undo-intent",
242
+ "package/undo-recorded",
243
+ ]);
244
+ await dispose();
245
+ });
246
+
247
+ test("package_inspect_self returns the host's generated catalog read-only", async () => {
248
+ const host = stubHost({
249
+ status: "refused",
250
+ reason: "unused",
251
+ failureId: "unused",
252
+ });
253
+ const tool = createPackageInspectSelfTool(host);
254
+
255
+ const result = await tool.execute({}, CONTEXT);
256
+
257
+ expect(result.isError).toBe(false);
258
+ expect(result.content).toContain("BotPackageExecutionContextV1");
259
+ expect(tool.idempotent).toBe(true);
260
+ });
261
+ });
package/src/agent.ts CHANGED
@@ -1,4 +1,4 @@
1
- // The Package Authoring runtime Contribution: one tool, `package_author`.
1
+ // The Package Authoring runtime Contribution: author, undo, and self-inspect.
2
2
  //
3
3
  // The Package holds no authority of its own. It decodes the model's input at
4
4
  // the seam, appends the two session events that make the effect visible in the
@@ -10,12 +10,18 @@ import type {
10
10
  ToolDefinition,
11
11
  ToolExecutionContext,
12
12
  } from "@frockbot/kernel-contracts";
13
+ import { BOT_ISOLATE_CONTEXT_SUMMARY_V1 } from "@frockbot/kernel-contracts";
13
14
  import type { Plugin } from "cordis";
14
15
  import {
15
16
  AUTHOR_PACKAGE_INPUT_SCHEMA_V1,
17
+ PACKAGE_UNDO_INPUT_SCHEMA_V1,
16
18
  type AuthorPackageInputV1,
17
19
  type AuthorPackageOutcomeV1,
20
+ type PackageInspectSelfOutcomeV1,
21
+ type PackageUndoInputV1,
22
+ type PackageUndoOutcomeV1,
18
23
  decodeAuthorPackageInputV1,
24
+ decodePackageUndoInputV1,
19
25
  sha256HexV1,
20
26
  } from "./shared.js";
21
27
 
@@ -33,6 +39,13 @@ export interface AuthorPackageRequestV1 {
33
39
  position: AuthoringTurnPositionV1;
34
40
  }
35
41
 
42
+ export interface PackageUndoRequestV1 {
43
+ input: PackageUndoInputV1;
44
+ effectId: string;
45
+ sessionId: string;
46
+ position: AuthoringTurnPositionV1;
47
+ }
48
+
36
49
  /**
37
50
  * The kernel-hosted seam this Package receives. Implemented by the Durable
38
51
  * Object host (`@frockbot/plugin-shell/backend-authoring`), which owns the
@@ -44,8 +57,13 @@ export interface PackageAuthoringHost {
44
57
  effectIdFor(input: {
45
58
  packageId: string;
46
59
  sourceHash: string;
60
+ uiHtmlHash?: string;
61
+ hooks?: AuthorPackageInputV1["hooks"];
47
62
  }): Promise<string>;
48
63
  author(request: AuthorPackageRequestV1): Promise<AuthorPackageOutcomeV1>;
64
+ undoEffectIdFor(input: PackageUndoInputV1): Promise<string>;
65
+ undo(request: PackageUndoRequestV1): Promise<PackageUndoOutcomeV1>;
66
+ inspectSelf(): Promise<PackageInspectSelfOutcomeV1>;
49
67
  }
50
68
 
51
69
  /**
@@ -105,8 +123,7 @@ export function createPackageAuthorTool(
105
123
  // not part of the narrow reach of `browserUse`, `computerUse`, or the two
106
124
  // video roles. See `@frockbot/plugin-subagents` `SUBAGENT_TOOL_REACH_V1`.
107
125
  admission: { subagentRoles: ["executor"] },
108
- description:
109
- "Author a Package for yourself: one tool implemented in a single TypeScript file that runs in your own isolate. The Package is recorded as a new Composition generation and activates on your next Turn.",
126
+ description: `Author a Package for yourself: tools implemented in one TypeScript file that runs in your own isolate. Declare every exported tool in tools; the names must match exactly. The Package is recorded as a new Composition generation and activates on your next Turn. ${BOT_ISOLATE_CONTEXT_SUMMARY_V1}`,
110
127
  inputSchema: AUTHOR_PACKAGE_INPUT_SCHEMA_V1,
111
128
  idempotent: false,
112
129
  validate: (input: unknown) => {
@@ -137,9 +154,14 @@ export function createPackageAuthorTool(
137
154
  };
138
155
  }
139
156
  const sourceHash = await sha256HexV1(decoded.source);
157
+ const uiHtmlHash = decoded.ui
158
+ ? await sha256HexV1(decoded.ui.html)
159
+ : undefined;
140
160
  const effectId = await host.effectIdFor({
141
161
  packageId: decoded.packageId,
142
162
  sourceHash,
163
+ ...(uiHtmlHash === undefined ? {} : { uiHtmlHash }),
164
+ ...(decoded.hooks === undefined ? {} : { hooks: decoded.hooks }),
143
165
  });
144
166
  const position = openTurnPositionV1(session);
145
167
  // Intent before effect: the session event and the durable intent record
@@ -179,12 +201,119 @@ export function createPackageAuthorTool(
179
201
  };
180
202
  }
181
203
 
182
- /** The runtime Contribution. Registers `package_author` and nothing else. */
204
+ export function createPackageUndoTool(
205
+ host: PackageAuthoringHost,
206
+ sessions: { get(sessionId: string): Session | undefined },
207
+ ): ToolDefinition {
208
+ return {
209
+ name: "package_undo",
210
+ admission: { subagentRoles: ["executor"] },
211
+ description:
212
+ "Undo your latest Package setup change, or restore an earlier Composition generation. This only changes Package setup; it never undoes actions taken through Connections. The resulting generation activates on the next Turn.",
213
+ inputSchema: PACKAGE_UNDO_INPUT_SCHEMA_V1,
214
+ idempotent: false,
215
+ validate: (input) => {
216
+ try {
217
+ decodePackageUndoInputV1(input);
218
+ return true;
219
+ } catch {
220
+ return false;
221
+ }
222
+ },
223
+ execute: async (input: unknown, context: ToolExecutionContext) => {
224
+ let decoded: PackageUndoInputV1;
225
+ try {
226
+ decoded = decodePackageUndoInputV1(input);
227
+ } catch (error) {
228
+ return {
229
+ content: `package_undo input was rejected: ${errorMessage(error)}`,
230
+ isError: true,
231
+ };
232
+ }
233
+ const session = sessions.get(context.sessionId);
234
+ if (!session) {
235
+ return {
236
+ content: `package_undo cannot record its intent: session "${context.sessionId}" is unavailable`,
237
+ isError: true,
238
+ };
239
+ }
240
+ const effectId = await host.undoEffectIdFor(decoded);
241
+ const position = openTurnPositionV1(session);
242
+ session.append({
243
+ type: "package/undo-intent",
244
+ ...position,
245
+ effectId,
246
+ ...(decoded.generationId
247
+ ? { requestedGenerationId: decoded.generationId }
248
+ : {}),
249
+ });
250
+ await session.flush();
251
+ const outcome = await host.undo({
252
+ input: decoded,
253
+ effectId,
254
+ sessionId: context.sessionId,
255
+ position,
256
+ });
257
+ if (outcome.status === "refused") {
258
+ return {
259
+ content: `Package undo was refused: ${outcome.reason} A durable failure record "${outcome.failureId}" was written. No connection action was undone.`,
260
+ isError: true,
261
+ };
262
+ }
263
+ session.append({
264
+ type: "package/undo-recorded",
265
+ ...position,
266
+ effectId,
267
+ generationId: outcome.generationId,
268
+ targetGenerationId: outcome.targetGenerationId,
269
+ });
270
+ await session.flush();
271
+ return {
272
+ content: `Package setup will return to Composition generation "${outcome.targetGenerationId}" through new pending generation "${outcome.generationId}". It activates on the next Turn. This changed Package setup only; it did not undo any action taken through a Connection.`,
273
+ isError: false,
274
+ };
275
+ },
276
+ };
277
+ }
278
+
279
+ function errorMessage(error: unknown): string {
280
+ return error instanceof Error ? error.message : String(error);
281
+ }
282
+
283
+ export function createPackageInspectSelfTool(
284
+ host: PackageAuthoringHost,
285
+ ): ToolDefinition {
286
+ return {
287
+ name: "package_inspect_self",
288
+ admission: { subagentRoles: ["executor"] },
289
+ description:
290
+ "Read your exact Package execution context contract, current Composition (including your stored Package source), and latest authoring or activation failure per authored Package.",
291
+ inputSchema: { type: "object", additionalProperties: false },
292
+ idempotent: true,
293
+ validate: (input) =>
294
+ Boolean(input && typeof input === "object" && !Array.isArray(input)) &&
295
+ Object.keys(input as Record<string, unknown>).length === 0,
296
+ execute: async () => ({
297
+ content: JSON.stringify(await host.inspectSelf(), null, 2),
298
+ isError: false,
299
+ }),
300
+ };
301
+ }
302
+
303
+ /** The runtime Contribution. Registers the three chat-first setup tools. */
183
304
  export function createAuthoringRuntimePlugin(
184
305
  host: PackageAuthoringHost,
185
306
  ): Plugin.Function {
186
- const plugin: Plugin.Function = (ctx) =>
187
- ctx.tools.register(createPackageAuthorTool(host, ctx.sessions));
307
+ const plugin: Plugin.Function = (ctx) => {
308
+ const disposers = [
309
+ ctx.tools.register(createPackageAuthorTool(host, ctx.sessions)),
310
+ ctx.tools.register(createPackageUndoTool(host, ctx.sessions)),
311
+ ctx.tools.register(createPackageInspectSelfTool(host)),
312
+ ];
313
+ return () => {
314
+ for (const dispose of disposers.reverse()) dispose();
315
+ };
316
+ };
188
317
  plugin.inject = ["tools", "sessions"];
189
318
  return plugin;
190
319
  }
@@ -19,6 +19,7 @@ const INTENT: AuthorshipIntentV1 = {
19
19
  packageId: "weather-lookup",
20
20
  version: "0.0.1",
21
21
  sourceHash: "a".repeat(64),
22
+ manifestHash: "b".repeat(64),
22
23
  sourceBytes: 64,
23
24
  recordedAt: "2026-08-31T00:00:00.000Z",
24
25
  status: "recorded",
package/src/records.ts CHANGED
@@ -11,6 +11,10 @@ export const AUTHORSHIP_INTENT_PREFIX = "authorship:intent:";
11
11
  export const AUTHORSHIP_ARTIFACT_PREFIX = "authorship:artifact:";
12
12
  export const AUTHORSHIP_FAILURE_PREFIX = "authorship:failure:";
13
13
  export const AUTHORSHIP_PACKAGE_PREFIX = "authorship:package:";
14
+ export const AUTHORSHIP_MANIFEST_PREFIX = "authorship:manifest:";
15
+ export const AUTHORSHIP_LATEST_FAILURE_PREFIX = "authorship:latest-failure:";
16
+ export const AUTHORSHIP_UNDO_INTENT_PREFIX = "authorship:undo-intent:";
17
+ export const AUTHORSHIP_UNDO_OUTCOME_PREFIX = "authorship:undo-outcome:";
14
18
  export const ARTIFACT_PREFIX = "artifact:";
15
19
 
16
20
  export function authorshipIntentKey(effectId: string): string {
@@ -30,6 +34,22 @@ export function authorshipPackageKey(packageId: string): string {
30
34
  return `${AUTHORSHIP_PACKAGE_PREFIX}${packageId}`;
31
35
  }
32
36
 
37
+ export function authorshipManifestKey(manifestHash: string): string {
38
+ return `${AUTHORSHIP_MANIFEST_PREFIX}${manifestHash}`;
39
+ }
40
+
41
+ export function authorshipLatestFailureKey(packageId: string): string {
42
+ return `${AUTHORSHIP_LATEST_FAILURE_PREFIX}${packageId}`;
43
+ }
44
+
45
+ export function authorshipUndoIntentKey(effectId: string): string {
46
+ return `${AUTHORSHIP_UNDO_INTENT_PREFIX}${effectId}`;
47
+ }
48
+
49
+ export function authorshipUndoOutcomeKey(effectId: string): string {
50
+ return `${AUTHORSHIP_UNDO_OUTCOME_PREFIX}${effectId}`;
51
+ }
52
+
33
53
  export function artifactKey(contentHash: string): string {
34
54
  return `${ARTIFACT_PREFIX}${contentHash}`;
35
55
  }
@@ -43,8 +63,10 @@ export interface AuthorshipIntentV1 {
43
63
  turnId: string;
44
64
  packageId: string;
45
65
  version: string;
46
- /** sha-256 of the source text. The source itself is never durable state. */
66
+ /** sha-256 of the source text stored in the immutable Package content store. */
47
67
  sourceHash: string;
68
+ /** sha-256 of the exact manifest recorded before bundling starts. */
69
+ manifestHash: string;
48
70
  sourceBytes: number;
49
71
  recordedAt: string;
50
72
  status: "recorded";
@@ -59,6 +81,9 @@ export interface AuthoredArtifactRecordV1 {
59
81
  bundlerVersion: string;
60
82
  effectId: string;
61
83
  r2Key: string;
84
+ sourceHash: string;
85
+ sourceR2Key: string;
86
+ manifestHash: string;
62
87
  provenance: {
63
88
  kind: "bot";
64
89
  packageId: string;
@@ -72,6 +97,16 @@ export interface AuthoredArtifactRecordV1 {
72
97
  createdAt: string;
73
98
  }
74
99
 
100
+ /** The exact immutable manifest mounted for one Bot-authored member. */
101
+ export interface AuthoredManifestRecordV1 {
102
+ schemaVersion: 1;
103
+ manifestHash: string;
104
+ packageId: string;
105
+ version: string;
106
+ manifest: unknown;
107
+ createdAt: string;
108
+ }
109
+
75
110
  /** The latest recorded version of one authored Package identity. */
76
111
  export interface AuthoredPackageRecordV1 {
77
112
  schemaVersion: 1;
@@ -96,10 +131,44 @@ export interface AuthoringFailureRecordV1 {
96
131
  recordedAt: string;
97
132
  }
98
133
 
134
+ export interface PackageUndoIntentV1 {
135
+ schemaVersion: 1;
136
+ effectId: string;
137
+ botId: string;
138
+ runId: string;
139
+ turnId: string;
140
+ requestedGenerationId?: string;
141
+ targetGenerationId: string;
142
+ recordedAt: string;
143
+ status: "recorded";
144
+ }
145
+
146
+ export type PackageUndoRecordV1 =
147
+ | {
148
+ schemaVersion: 1;
149
+ effectId: string;
150
+ generationId: string;
151
+ targetGenerationId: string;
152
+ recordedAt: string;
153
+ status: "recorded";
154
+ }
155
+ | {
156
+ schemaVersion: 1;
157
+ effectId: string;
158
+ failureId: string;
159
+ reason: string;
160
+ recordedAt: string;
161
+ status: "refused";
162
+ };
163
+
99
164
  export function artifactR2KeyV1(contentHash: string): string {
100
165
  return `packages/${contentHash}.mjs`;
101
166
  }
102
167
 
168
+ export function sourceR2KeyV1(sourceHash: string): string {
169
+ return `packages/${sourceHash}.ts`;
170
+ }
171
+
103
172
  /**
104
173
  * The durable outcome of one authoring effect, written under
105
174
  * `authorship:artifact:<effectId>` in the same put as `artifact:<contentHash>`.
@@ -10,23 +10,44 @@ import {
10
10
  const VALID = {
11
11
  packageId: "weather-lookup",
12
12
  displayName: "Weather lookup",
13
- tool: {
14
- name: "weather_lookup",
15
- description: "Looks up the weather",
16
- inputSchema: { type: "object", properties: { city: { type: "string" } } },
17
- },
13
+ tools: [
14
+ {
15
+ name: "weather_lookup",
16
+ description: "Looks up the weather",
17
+ inputSchema: {
18
+ type: "object",
19
+ properties: { city: { type: "string" } },
20
+ },
21
+ },
22
+ ],
18
23
  source: "export const tools = [];\nexport async function execute() {}\n",
19
24
  };
20
25
 
21
26
  describe("decodeAuthorPackageInputV1", () => {
22
- test("accepts the exact v1 shape and the optional model Contribution", () => {
27
+ test("accepts the exact plural shape and canonicalizes the legacy singular shape", () => {
23
28
  expect(decodeAuthorPackageInputV1(VALID)).toEqual(VALID);
29
+ const { tools: _tools, ...rest } = VALID;
24
30
  expect(
31
+ decodeAuthorPackageInputV1({ ...rest, tool: VALID.tools[0] }),
32
+ ).toEqual(VALID);
33
+ });
34
+
35
+ test("accepts only declared public waterfall hooks", () => {
36
+ expect(
37
+ decodeAuthorPackageInputV1({
38
+ ...VALID,
39
+ hooks: ["agent/tool-exposure", "tools/post-execute"],
40
+ }).hooks,
41
+ ).toEqual(["agent/tool-exposure", "tools/post-execute"]);
42
+ expect(() =>
43
+ decodeAuthorPackageInputV1({ ...VALID, hooks: ["agent/request"] }),
44
+ ).toThrow(/hooks\[0\] is invalid/);
45
+ expect(() =>
25
46
  decodeAuthorPackageInputV1({
26
47
  ...VALID,
27
- model: { providerId: "ollama-cloud", modelId: "qwen3-coder:480b" },
28
- }).model,
29
- ).toEqual({ providerId: "ollama-cloud", modelId: "qwen3-coder:480b" });
48
+ hooks: ["agent/tool-exposure", "agent/tool-exposure"],
49
+ }),
50
+ ).toThrow(/duplicate events/);
30
51
  });
31
52
 
32
53
  test.each([
@@ -37,16 +58,17 @@ describe("decodeAuthorPackageInputV1", () => {
37
58
  ["a one-character package id", { ...VALID, packageId: "a" }],
38
59
  [
39
60
  "a tool name with a dash",
40
- { ...VALID, tool: { ...VALID.tool, name: "a-b" } },
61
+ { ...VALID, tools: [{ ...VALID.tools[0]!, name: "a-b" }] },
41
62
  ],
42
63
  ["an empty source", { ...VALID, source: "" }],
43
64
  [
44
65
  "a non-object input schema",
45
- { ...VALID, tool: { ...VALID.tool, inputSchema: "object" } },
66
+ { ...VALID, tools: [{ ...VALID.tools[0]!, inputSchema: "object" }] },
46
67
  ],
68
+ ["a removed model declaration", { ...VALID, model: { providerId: "x" } }],
47
69
  [
48
- "a partial model Contribution",
49
- { ...VALID, model: { providerId: "ollama-cloud" } },
70
+ "duplicate tool names",
71
+ { ...VALID, tools: [VALID.tools[0], VALID.tools[0]] },
50
72
  ],
51
73
  ])("rejects %s", (_label, input) => {
52
74
  expect(() => decodeAuthorPackageInputV1(input)).toThrow();
@@ -60,10 +82,39 @@ describe("decodeAuthorPackageInputV1", () => {
60
82
  }),
61
83
  ).toThrow();
62
84
  });
85
+
86
+ test("accepts one inline iframe page and refuses external resources or an oversized page", () => {
87
+ const ui = {
88
+ html: "<!doctype html><style>body{color:red}</style><script>window.frockbot.resize()</script>",
89
+ mounts: [
90
+ { slot: "frockbot.tool-result:weather_lookup", order: 10 },
91
+ { slot: "frockbot.bot-settings-sections" },
92
+ ],
93
+ };
94
+ expect(
95
+ decodeAuthorPackageInputV1({
96
+ ...VALID,
97
+ hooks: ["agent/tool-exposure"],
98
+ ui,
99
+ }),
100
+ ).toMatchObject({ hooks: ["agent/tool-exposure"], ui });
101
+ expect(() =>
102
+ decodeAuthorPackageInputV1({
103
+ ...VALID,
104
+ ui: { ...ui, html: '<script src="https://example.com/x.js"></script>' },
105
+ }),
106
+ ).toThrow("inline resources only");
107
+ expect(() =>
108
+ decodeAuthorPackageInputV1({
109
+ ...VALID,
110
+ ui: { ...ui, html: "a".repeat(256 * 1024 + 1) },
111
+ }),
112
+ ).toThrow();
113
+ });
63
114
  });
64
115
 
65
116
  describe("authoring identity", () => {
66
- test("the effect id is deterministic in the run and the exact source", async () => {
117
+ test("the effect id is deterministic in the run, source, UI, and hooks", async () => {
67
118
  const sourceHash = await sha256HexV1(VALID.source);
68
119
  const first = await authoringEffectIdV1({
69
120
  runId: "run-1",
@@ -85,10 +136,25 @@ describe("authoring identity", () => {
85
136
  packageId: VALID.packageId,
86
137
  sourceHash: await sha256HexV1(`${VALID.source}//`),
87
138
  });
139
+ const otherUi = await authoringEffectIdV1({
140
+ runId: "run-1",
141
+ packageId: VALID.packageId,
142
+ sourceHash,
143
+ uiHtmlHash: await sha256HexV1("<!doctype html><h1>Weather</h1>"),
144
+ });
145
+ const otherHooks = await authoringEffectIdV1({
146
+ runId: "run-1",
147
+ packageId: VALID.packageId,
148
+ sourceHash,
149
+ hooks: ["agent/tool-exposure"],
150
+ });
88
151
 
89
152
  expect(first).toBe(second);
90
153
  expect(first).not.toBe(otherRun);
91
154
  expect(first).not.toBe(otherSource);
155
+ expect(first).not.toBe(otherUi);
156
+ expect(first).not.toBe(otherHooks);
157
+ expect(otherUi).not.toBe(otherHooks);
92
158
  expect(first.length).toBeLessThanOrEqual(200);
93
159
  });
94
160
 
@@ -98,41 +164,71 @@ describe("authoring identity", () => {
98
164
  expect(() => authoredVersionV1(0)).toThrow();
99
165
  });
100
166
 
101
- test("the synthesized manifest declares only the isolate host", () => {
167
+ test("the synthesized manifest declares only the isolate host, tools and hooks", () => {
102
168
  const manifest = authoredManifestV1({
103
169
  packageId: VALID.packageId,
104
170
  displayName: VALID.displayName,
105
171
  version: "0.0.1",
106
- tool: VALID.tool,
107
- model: { providerId: "ollama-cloud", modelId: "qwen3-coder:480b" },
172
+ tools: VALID.tools,
173
+ hooks: ["agent/tool-exposure"],
108
174
  });
109
175
  const contributions = manifest.contributions as Record<
110
176
  string,
111
177
  { host?: string; binding?: string }
112
178
  >;
113
- expect(Object.keys(contributions).toSorted()).toEqual(["model", "runtime"]);
179
+ expect(Object.keys(contributions)).toEqual(["runtime"]);
114
180
  expect(contributions.runtime?.host).toBe("bot-isolate");
115
- expect(contributions.model?.host).toBe("bot-isolate");
116
- // A Bot-authored model adapter is a translation layer over a kernel
117
- // binding, never a network client.
118
- expect(contributions.model?.binding).toBe("capabilities.invokeModel");
181
+ expect(manifest.tools).toEqual(VALID.tools);
182
+ expect(manifest.hooks).toEqual(["agent/tool-exposure"]);
119
183
  expect(manifest.permissions).toEqual([]);
120
184
  });
121
185
 
122
- test("the manifest hash moves when the declared model Contribution moves", () => {
186
+ test("the manifest moves when its declared tool set moves", () => {
123
187
  const base = authoredManifestV1({
124
188
  packageId: VALID.packageId,
125
189
  displayName: VALID.displayName,
126
190
  version: "0.0.1",
127
- tool: VALID.tool,
191
+ tools: VALID.tools,
128
192
  });
129
193
  const withModel = authoredManifestV1({
130
194
  packageId: VALID.packageId,
131
195
  displayName: VALID.displayName,
132
196
  version: "0.0.1",
133
- tool: VALID.tool,
134
- model: { providerId: "ollama-cloud", modelId: "qwen3-coder:480b" },
197
+ tools: [
198
+ ...VALID.tools,
199
+ { name: "forecast", description: "Forecasts", inputSchema: {} },
200
+ ],
135
201
  });
136
202
  expect(JSON.stringify(base)).not.toBe(JSON.stringify(withModel));
137
203
  });
204
+
205
+ test("extends the same manifest with a content-addressed iframe contribution", () => {
206
+ const artifact = {
207
+ contentHash: "a".repeat(64),
208
+ size: 42,
209
+ mediaType: "text/html" as const,
210
+ bundlerVersion: "frockbot-inline-html@1",
211
+ };
212
+ const manifest = authoredManifestV1({
213
+ packageId: VALID.packageId,
214
+ displayName: VALID.displayName,
215
+ version: "0.0.1",
216
+ tools: VALID.tools,
217
+ ui: {
218
+ artifact,
219
+ mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
220
+ },
221
+ });
222
+ expect(manifest).toMatchObject({
223
+ schemaVersion: 3,
224
+ contributions: {
225
+ runtime: { host: "bot-isolate" },
226
+ client: {
227
+ kind: "iframe",
228
+ artifact,
229
+ mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
230
+ },
231
+ },
232
+ });
233
+ });
138
234
  });
package/src/shared.ts CHANGED
@@ -6,23 +6,29 @@
6
6
  // immutable artifact and a pending Composition generation. Activation is a
7
7
  // separate event, at the next admitted Turn. A model never overwrites a
8
8
  // version; re-authoring the same `packageId` appends the next one.
9
- import { PACKAGE_BUNDLE_MAX_SOURCE_BYTES } from "@frockbot/kernel-contracts";
9
+ import {
10
+ BOT_ISOLATE_HOOK_EVENTS_V1,
11
+ isBotIsolateHookEventNameV1,
12
+ PACKAGE_BUNDLE_MAX_SOURCE_BYTES,
13
+ type BotIsolateHookEventNameV1,
14
+ } from "@frockbot/kernel-contracts";
10
15
 
11
16
  /** The `package_author` tool input. */
12
17
  export interface AuthorPackageInputV1 {
13
18
  /** Stable Plugin identity; re-authoring appends a version. */
14
19
  packageId: string;
15
20
  displayName: string;
16
- tool: { name: string; description: string; inputSchema: unknown };
21
+ /** Every tool the immutable Package artifact is expected to export. */
22
+ tools: Array<{ name: string; description: string; inputSchema: unknown }>;
23
+ /** Waterfall events the immutable artifact is expected to hook. */
24
+ hooks?: BotIsolateHookEventNameV1[];
17
25
  /** TypeScript text; exactly one `package.ts`. */
18
26
  source: string;
19
- /**
20
- * D6 addendum. The authored Package declares a model Contribution: an
21
- * adapter that forwards to `CAPABILITIES.invokeModel`. It is a translation
22
- * layer over a kernel-declared binding, never a network client, and it is
23
- * callable only where an enabled model Assignment matches.
24
- */
25
- model?: { providerId: string; modelId: string };
27
+ /** Optional sandboxed page. All CSS and JavaScript must be inline. */
28
+ ui?: {
29
+ html: string;
30
+ mounts: Array<{ slot: string; order?: number }>;
31
+ };
26
32
  }
27
33
 
28
34
  export type AuthorPackageOutcomeV1 =
@@ -42,8 +48,61 @@ export type AuthorPackageOutcomeV1 =
42
48
  failureId: string;
43
49
  };
44
50
 
51
+ export interface PackageUndoInputV1 {
52
+ /** Absent means the generation before the most recent authored change. */
53
+ generationId?: string;
54
+ }
55
+
56
+ export type PackageUndoOutcomeV1 =
57
+ | {
58
+ status: "recorded";
59
+ effectId: string;
60
+ generationId: string;
61
+ targetGenerationId: string;
62
+ }
63
+ | { status: "refused"; reason: string; failureId: string };
64
+
65
+ export interface PackageInspectMemberV1 {
66
+ packageId: string;
67
+ version: string;
68
+ provenance: Record<string, unknown>;
69
+ declaredTools: string[];
70
+ source?: string;
71
+ }
72
+
73
+ export interface PackageInspectFailureV1 {
74
+ packageId: string;
75
+ authoring?: {
76
+ failureId: string;
77
+ phase: string;
78
+ reason: string;
79
+ diagnostics: string[];
80
+ recordedAt: string;
81
+ };
82
+ activation?: {
83
+ generationId: string;
84
+ attempt: number;
85
+ phase: string;
86
+ message: string;
87
+ diagnostics: string[];
88
+ at: string;
89
+ quarantined: boolean;
90
+ };
91
+ }
92
+
93
+ export interface PackageInspectSelfOutcomeV1 {
94
+ contextContract: string;
95
+ composition: {
96
+ generationId: string;
97
+ status: string;
98
+ members: PackageInspectMemberV1[];
99
+ };
100
+ failures: PackageInspectFailureV1[];
101
+ }
102
+
45
103
  export const AUTHORED_PACKAGE_ID = /^[a-z][a-z0-9-]{2,63}$/;
46
104
  export const AUTHORED_TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/;
105
+ export const AUTHORED_TOOLS_MAX = 64;
47
106
  /**
48
107
  * The shape of an authored id, not its authority: a Bot may not shadow a
49
108
  * first-party or User Package, and that rule is enforced against the Bot's
@@ -112,12 +171,20 @@ export function decodeAuthorPackageInputV1(
112
171
  label = "package_author input",
113
172
  ): AuthorPackageInputV1 {
114
173
  const value = record(input, label);
174
+ // `tool` is accepted for one compatibility release, but the decoded shape
175
+ // is always the plural declaration the manifest and mount path enforce.
115
176
  exactKeys(
116
177
  value,
117
- ["packageId", "displayName", "tool", "source"],
118
- ["model"],
178
+ ["packageId", "displayName", "source"],
179
+ ["tools", "tool", "hooks", "ui"],
119
180
  label,
120
181
  );
182
+ if (
183
+ (value.tools === undefined && value.tool === undefined) ||
184
+ (value.tools !== undefined && value.tool !== undefined)
185
+ ) {
186
+ throw new Error(`${label} must declare exactly one of tools or tool`);
187
+ }
121
188
  const packageId = boundedString(
122
189
  value.packageId,
123
190
  `${label}.packageId`,
@@ -131,19 +198,53 @@ export function decodeAuthorPackageInputV1(
131
198
  `${label}.displayName`,
132
199
  128,
133
200
  );
134
- const tool = record(value.tool, `${label}.tool`);
135
- exactKeys(tool, ["name", "description", "inputSchema"], [], `${label}.tool`);
136
- const name = boundedString(tool.name, `${label}.tool.name`, 64);
137
- if (!AUTHORED_TOOL_NAME.test(name)) {
138
- throw new Error(`${label}.tool.name is invalid`);
201
+ const declaredTools = value.tools === undefined ? [value.tool] : value.tools;
202
+ if (
203
+ !Array.isArray(declaredTools) ||
204
+ declaredTools.length === 0 ||
205
+ declaredTools.length > AUTHORED_TOOLS_MAX
206
+ ) {
207
+ throw new Error(`${label}.tools must be a non-empty bounded array`);
208
+ }
209
+ const tools = declaredTools.map((candidate, index) => {
210
+ const toolLabel = `${label}.tools[${index}]`;
211
+ const tool = record(candidate, toolLabel);
212
+ exactKeys(tool, ["name", "description", "inputSchema"], [], toolLabel);
213
+ const name = boundedString(tool.name, `${toolLabel}.name`, 64);
214
+ if (!AUTHORED_TOOL_NAME.test(name)) {
215
+ throw new Error(`${toolLabel}.name is invalid`);
216
+ }
217
+ const description = boundedString(
218
+ tool.description,
219
+ `${toolLabel}.description`,
220
+ 1_024,
221
+ );
222
+ const inputSchema = record(tool.inputSchema, `${toolLabel}.inputSchema`);
223
+ requireJsonValue(inputSchema, `${toolLabel}.inputSchema`);
224
+ return { name, description, inputSchema };
225
+ });
226
+ if (new Set(tools.map((tool) => tool.name)).size !== tools.length) {
227
+ throw new Error(`${label}.tools contains duplicate names`);
228
+ }
229
+ let hooks: BotIsolateHookEventNameV1[] | undefined;
230
+ if (value.hooks !== undefined) {
231
+ if (
232
+ !Array.isArray(value.hooks) ||
233
+ value.hooks.length === 0 ||
234
+ value.hooks.length > BOT_ISOLATE_HOOK_EVENTS_V1.length
235
+ ) {
236
+ throw new Error(`${label}.hooks must be a non-empty bounded array`);
237
+ }
238
+ hooks = value.hooks.map((hook, index) => {
239
+ if (!isBotIsolateHookEventNameV1(hook)) {
240
+ throw new Error(`${label}.hooks[${index}] is invalid`);
241
+ }
242
+ return hook;
243
+ });
244
+ if (new Set(hooks).size !== hooks.length) {
245
+ throw new Error(`${label}.hooks contains duplicate events`);
246
+ }
139
247
  }
140
- const description = boundedString(
141
- tool.description,
142
- `${label}.tool.description`,
143
- 1_024,
144
- );
145
- const inputSchema = record(tool.inputSchema, `${label}.tool.inputSchema`);
146
- requireJsonValue(inputSchema, `${label}.tool.inputSchema`);
147
248
  const source = boundedString(
148
249
  value.source,
149
250
  `${label}.source`,
@@ -155,25 +256,73 @@ export function decodeAuthorPackageInputV1(
155
256
  ) {
156
257
  throw new Error(`${label}.source exceeds the per-Package source quota`);
157
258
  }
158
- let model: AuthorPackageInputV1["model"];
159
- if (value.model !== undefined) {
160
- const declared = record(value.model, `${label}.model`);
161
- exactKeys(declared, ["providerId", "modelId"], [], `${label}.model`);
162
- model = {
163
- providerId: boundedString(
164
- declared.providerId,
165
- `${label}.model.providerId`,
166
- 128,
167
- ),
168
- modelId: boundedString(declared.modelId, `${label}.model.modelId`, 128),
169
- };
259
+ let ui: AuthorPackageInputV1["ui"];
260
+ if (value.ui !== undefined) {
261
+ const rawUi = record(value.ui, `${label}.ui`);
262
+ exactKeys(rawUi, ["html", "mounts"], [], `${label}.ui`);
263
+ const html = boundedString(
264
+ rawUi.html,
265
+ `${label}.ui.html`,
266
+ PACKAGE_BUNDLE_MAX_SOURCE_BYTES,
267
+ );
268
+ if (
269
+ new TextEncoder().encode(html).byteLength >
270
+ PACKAGE_BUNDLE_MAX_SOURCE_BYTES
271
+ ) {
272
+ throw new Error(`${label}.ui.html exceeds the per-Package source quota`);
273
+ }
274
+ if (
275
+ /<(?:script|iframe|img|audio|video|source|embed|input)\b[^>]*\bsrc\s*=\s*["'](?!data:)/i.test(
276
+ html,
277
+ ) ||
278
+ /<object\b[^>]*\bdata\s*=\s*["'](?!data:)/i.test(html) ||
279
+ /\bsrcset\s*=/i.test(html) ||
280
+ /<link\b/i.test(html) ||
281
+ /@import\b/i.test(html) ||
282
+ /<meta\b[^>]*http-equiv\s*=\s*["']?refresh/i.test(html) ||
283
+ /url\(\s*["']?(?!data:|["']?\s*\))/i.test(html)
284
+ ) {
285
+ throw new Error(`${label}.ui.html may contain inline resources only`);
286
+ }
287
+ if (
288
+ !Array.isArray(rawUi.mounts) ||
289
+ rawUi.mounts.length === 0 ||
290
+ rawUi.mounts.length > 64
291
+ ) {
292
+ throw new Error(`${label}.ui.mounts must be a non-empty bounded array`);
293
+ }
294
+ const mounts = rawUi.mounts.map((candidate, index) => {
295
+ const mount = record(candidate, `${label}.ui.mounts[${index}]`);
296
+ exactKeys(mount, ["slot"], ["order"], `${label}.ui.mounts[${index}]`);
297
+ const slot = boundedString(
298
+ mount.slot,
299
+ `${label}.ui.mounts[${index}].slot`,
300
+ 160,
301
+ );
302
+ if (
303
+ slot !== "frockbot.bot-settings-sections" &&
304
+ !slot.startsWith("frockbot.tool-result:")
305
+ ) {
306
+ throw new Error(`${label}.ui.mounts[${index}].slot is not iframe-safe`);
307
+ }
308
+ const order = mount.order;
309
+ if (
310
+ order !== undefined &&
311
+ (typeof order !== "number" || !Number.isFinite(order))
312
+ ) {
313
+ throw new Error(`${label}.ui.mounts[${index}].order must be finite`);
314
+ }
315
+ return { slot, ...(order === undefined ? {} : { order }) };
316
+ });
317
+ ui = { html, mounts };
170
318
  }
171
319
  return {
172
320
  packageId,
173
321
  displayName,
174
- tool: { name, description, inputSchema },
322
+ tools,
323
+ ...(hooks === undefined ? {} : { hooks }),
175
324
  source,
176
- ...(model ? { model } : {}),
325
+ ...(ui ? { ui } : {}),
177
326
  };
178
327
  }
179
328
 
@@ -181,7 +330,7 @@ export function decodeAuthorPackageInputV1(
181
330
  export const AUTHOR_PACKAGE_INPUT_SCHEMA_V1 = {
182
331
  type: "object",
183
332
  additionalProperties: false,
184
- required: ["packageId", "displayName", "tool", "source"],
333
+ required: ["packageId", "displayName", "tools", "source"],
185
334
  properties: {
186
335
  packageId: {
187
336
  type: "string",
@@ -189,35 +338,97 @@ export const AUTHOR_PACKAGE_INPUT_SCHEMA_V1 = {
189
338
  "Stable lowercase Package identity. Re-authoring it appends a version.",
190
339
  },
191
340
  displayName: { type: "string" },
192
- tool: {
193
- type: "object",
194
- additionalProperties: false,
195
- required: ["name", "description", "inputSchema"],
196
- properties: {
197
- name: { type: "string" },
198
- description: { type: "string" },
199
- inputSchema: { type: "object" },
341
+ tools: {
342
+ type: "array",
343
+ minItems: 1,
344
+ maxItems: AUTHORED_TOOLS_MAX,
345
+ items: {
346
+ type: "object",
347
+ additionalProperties: false,
348
+ required: ["name", "description", "inputSchema"],
349
+ properties: {
350
+ name: { type: "string" },
351
+ description: { type: "string" },
352
+ inputSchema: { type: "object" },
353
+ },
200
354
  },
201
355
  },
356
+ hooks: {
357
+ type: "array",
358
+ minItems: 1,
359
+ maxItems: BOT_ISOLATE_HOOK_EVENTS_V1.length,
360
+ uniqueItems: true,
361
+ items: { type: "string", enum: BOT_ISOLATE_HOOK_EVENTS_V1 },
362
+ description:
363
+ "Waterfall loop events exported from `hooks`; names must match exactly.",
364
+ },
202
365
  source: {
203
366
  type: "string",
204
367
  description:
205
- "TypeScript for one package.ts that exports `tools` and `execute(tool, input, ctx)`. No imports: the isolate has no network and no npm. `ctx.invokeModel(request)` is the only model path.",
368
+ "TypeScript for one package.ts that exports `tools` and `execute(tool, input, ctx)`. No imports: the isolate has no network and no npm. `ctx.model.invoke(request)` uses the Bot's configured model binding.",
206
369
  },
207
- model: {
370
+ ui: {
208
371
  type: "object",
209
372
  additionalProperties: false,
210
- required: ["providerId", "modelId"],
211
- description:
212
- "Declare a model Contribution that forwards to the kernel model binding.",
373
+ required: ["html", "mounts"],
213
374
  properties: {
214
- providerId: { type: "string" },
215
- modelId: { type: "string" },
375
+ html: {
376
+ type: "string",
377
+ description:
378
+ "One ui.html page (maximum 256 KB). CSS and JavaScript must be inline; images may use data: URLs.",
379
+ },
380
+ mounts: {
381
+ type: "array",
382
+ minItems: 1,
383
+ maxItems: 64,
384
+ items: {
385
+ type: "object",
386
+ additionalProperties: false,
387
+ required: ["slot"],
388
+ properties: {
389
+ slot: {
390
+ type: "string",
391
+ description:
392
+ "frockbot.bot-settings-sections or frockbot.tool-result:<declaredToolName>",
393
+ },
394
+ order: { type: "number" },
395
+ },
396
+ },
397
+ },
216
398
  },
217
399
  },
218
400
  },
219
401
  } as const;
220
402
 
403
+ export function decodePackageUndoInputV1(
404
+ input: unknown,
405
+ label = "package_undo input",
406
+ ): PackageUndoInputV1 {
407
+ const value = record(input, label);
408
+ exactKeys(value, [], ["generationId"], label);
409
+ return value.generationId === undefined
410
+ ? {}
411
+ : {
412
+ generationId: boundedString(
413
+ value.generationId,
414
+ `${label}.generationId`,
415
+ 256,
416
+ ),
417
+ };
418
+ }
419
+
420
+ export const PACKAGE_UNDO_INPUT_SCHEMA_V1 = {
421
+ type: "object",
422
+ additionalProperties: false,
423
+ properties: {
424
+ generationId: {
425
+ type: "string",
426
+ description:
427
+ "Optional earlier Composition generation. Omit to undo your most recent Package change.",
428
+ },
429
+ },
430
+ } as const;
431
+
221
432
  export async function sha256HexV1(value: string): Promise<string> {
222
433
  const digest = await crypto.subtle.digest(
223
434
  "SHA-256",
@@ -230,20 +441,40 @@ export async function sha256HexV1(value: string): Promise<string> {
230
441
 
231
442
  /**
232
443
  * The idempotency key for one authoring effect. Deterministic in the admitted
233
- * run and the exact source, so a resumed Turn that re-executes the same tool
234
- * call lands on the same effect instead of bundling a second time.
444
+ * run and every independently bundled or declared execution dimension, so a
445
+ * resumed Turn that re-executes the same tool call lands on the same effect
446
+ * instead of bundling a second time.
235
447
  */
236
448
  export async function authoringEffectIdV1(input: {
237
449
  runId: string;
238
450
  packageId: string;
239
451
  sourceHash: string;
452
+ uiHtmlHash?: string;
453
+ hooks?: BotIsolateHookEventNameV1[];
240
454
  }): Promise<string> {
241
455
  const digest = await sha256HexV1(
242
- JSON.stringify([input.runId, input.packageId, input.sourceHash]),
456
+ JSON.stringify([
457
+ input.runId,
458
+ input.packageId,
459
+ input.sourceHash,
460
+ input.uiHtmlHash ?? null,
461
+ input.hooks ?? [],
462
+ ]),
243
463
  );
244
464
  return `author-${digest.slice(0, 32)}`;
245
465
  }
246
466
 
467
+ /** One undo effect per admitted run and requested target (or default target). */
468
+ export async function packageUndoEffectIdV1(input: {
469
+ runId: string;
470
+ generationId?: string;
471
+ }): Promise<string> {
472
+ const digest = await sha256HexV1(
473
+ JSON.stringify([input.runId, input.generationId ?? "latest"]),
474
+ );
475
+ return `undo-${digest.slice(0, 32)}`;
476
+ }
477
+
247
478
  /** `0.0.1`, `0.0.2`, … — a version is appended, never overwritten. */
248
479
  export function authoredVersionV1(ordinal: number): string {
249
480
  if (!Number.isSafeInteger(ordinal) || ordinal < 1) {
@@ -260,15 +491,25 @@ export function authoredSpecifierV1(packageId: string): string {
260
491
  /**
261
492
  * The manifest an authored Package is content-addressed by. It is synthesized
262
493
  * rather than authored so a Bot cannot declare a Contribution host the kernel
263
- * did not offer it: exactly one Bot isolate runtime Contribution, plus the
264
- * declared model binding when the Package asked for one.
494
+ * did not offer it: exactly one Bot isolate runtime Contribution and the exact
495
+ * tool names mount health must report. Model access is a method on the narrow
496
+ * Package context, not a separate manifest Contribution.
265
497
  */
266
498
  export function authoredManifestV1(input: {
267
499
  packageId: string;
268
500
  displayName: string;
269
501
  version: string;
270
- tool: AuthorPackageInputV1["tool"];
271
- model?: AuthorPackageInputV1["model"];
502
+ tools: AuthorPackageInputV1["tools"];
503
+ hooks?: AuthorPackageInputV1["hooks"];
504
+ ui?: {
505
+ artifact: {
506
+ contentHash: string;
507
+ size: number;
508
+ mediaType: "text/html";
509
+ bundlerVersion: string;
510
+ };
511
+ mounts: Array<{ slot: string; order?: number }>;
512
+ };
272
513
  }): Record<string, unknown> {
273
514
  return {
274
515
  schemaVersion: 3,
@@ -279,25 +520,18 @@ export function authoredManifestV1(input: {
279
520
  dependencies: {},
280
521
  contributions: {
281
522
  runtime: { entry: "./package.js", host: "bot-isolate" },
282
- ...(input.model
523
+ ...(input.ui
283
524
  ? {
284
- model: {
285
- entry: "./package.js",
286
- host: "bot-isolate",
287
- binding: "capabilities.invokeModel",
288
- providerId: input.model.providerId,
289
- modelId: input.model.modelId,
525
+ client: {
526
+ kind: "iframe",
527
+ artifact: { ...input.ui.artifact },
528
+ mounts: input.ui.mounts.map((mount) => ({ ...mount })),
290
529
  },
291
530
  }
292
531
  : {}),
293
532
  },
294
- tools: [
295
- {
296
- name: input.tool.name,
297
- description: input.tool.description,
298
- inputSchema: input.tool.inputSchema,
299
- },
300
- ],
533
+ tools: input.tools.map((tool) => ({ ...tool })),
534
+ ...(input.hooks === undefined ? {} : { hooks: [...input.hooks] }),
301
535
  permissions: [],
302
536
  };
303
537
  }