@frockbot/plugin-authoring 0.1.4 → 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 +2 -2
- package/src/agent.test.ts +101 -2
- package/src/agent.ts +135 -6
- package/src/records.test.ts +1 -0
- package/src/records.ts +70 -1
- package/src/shared.test.ts +122 -26
- package/src/shared.ts +308 -74
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-authoring",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/records.test.ts
CHANGED
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
|
|
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>`.
|
package/src/shared.test.ts
CHANGED
|
@@ -10,23 +10,44 @@ import {
|
|
|
10
10
|
const VALID = {
|
|
11
11
|
packageId: "weather-lookup",
|
|
12
12
|
displayName: "Weather lookup",
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
28
|
-
})
|
|
29
|
-
).
|
|
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,
|
|
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,
|
|
66
|
+
{ ...VALID, tools: [{ ...VALID.tools[0]!, inputSchema: "object" }] },
|
|
46
67
|
],
|
|
68
|
+
["a removed model declaration", { ...VALID, model: { providerId: "x" } }],
|
|
47
69
|
[
|
|
48
|
-
"
|
|
49
|
-
{ ...VALID,
|
|
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
|
|
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
|
-
|
|
107
|
-
|
|
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)
|
|
179
|
+
expect(Object.keys(contributions)).toEqual(["runtime"]);
|
|
114
180
|
expect(contributions.runtime?.host).toBe("bot-isolate");
|
|
115
|
-
expect(
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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", "
|
|
118
|
-
["
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
159
|
-
if (value.
|
|
160
|
-
const
|
|
161
|
-
exactKeys(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
322
|
+
tools,
|
|
323
|
+
...(hooks === undefined ? {} : { hooks }),
|
|
175
324
|
source,
|
|
176
|
-
...(
|
|
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", "
|
|
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
|
-
|
|
193
|
-
type: "
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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.
|
|
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
|
-
|
|
370
|
+
ui: {
|
|
208
371
|
type: "object",
|
|
209
372
|
additionalProperties: false,
|
|
210
|
-
required: ["
|
|
211
|
-
description:
|
|
212
|
-
"Declare a model Contribution that forwards to the kernel model binding.",
|
|
373
|
+
required: ["html", "mounts"],
|
|
213
374
|
properties: {
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
234
|
-
*
|
|
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([
|
|
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
|
|
264
|
-
*
|
|
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
|
-
|
|
271
|
-
|
|
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.
|
|
523
|
+
...(input.ui
|
|
283
524
|
? {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
}
|