@frockbot/plugin-authoring 0.3.1 → 0.3.3
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 +20 -4
- package/src/agent.ts +12 -4
- package/src/shared.test.ts +154 -12
- package/src/shared.ts +258 -69
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-authoring",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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.3.
|
|
24
|
+
"@frockbot/kernel-contracts": "0.3.3",
|
|
25
25
|
"cordis": "4.0.0-rc.8"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
package/src/agent.test.ts
CHANGED
|
@@ -115,7 +115,7 @@ describe("the package_author tool", () => {
|
|
|
115
115
|
await dispose();
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
test("includes
|
|
118
|
+
test("includes every UI page hash and declared hooks in the effect identity", async () => {
|
|
119
119
|
const { sessions, dispose } = await openSession();
|
|
120
120
|
const effectInputs: Parameters<PackageAuthoringHost["effectIdFor"]>[0][] =
|
|
121
121
|
[];
|
|
@@ -138,8 +138,18 @@ describe("the package_author tool", () => {
|
|
|
138
138
|
...INPUT,
|
|
139
139
|
hooks: ["agent/tool-exposure"],
|
|
140
140
|
ui: {
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
pages: [
|
|
142
|
+
{
|
|
143
|
+
id: "main",
|
|
144
|
+
html,
|
|
145
|
+
mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "board",
|
|
149
|
+
html: "<!doctype html><h1>Board</h1>",
|
|
150
|
+
mounts: [{ slot: "frockbot.right-panel" }],
|
|
151
|
+
},
|
|
152
|
+
],
|
|
143
153
|
},
|
|
144
154
|
},
|
|
145
155
|
CONTEXT,
|
|
@@ -149,7 +159,13 @@ describe("the package_author tool", () => {
|
|
|
149
159
|
{
|
|
150
160
|
packageId: "weather-lookup",
|
|
151
161
|
sourceHash: await sha256HexV1(INPUT.source),
|
|
152
|
-
|
|
162
|
+
uiPageHashes: [
|
|
163
|
+
{ id: "main", htmlHash: await sha256HexV1(html) },
|
|
164
|
+
{
|
|
165
|
+
id: "board",
|
|
166
|
+
htmlHash: await sha256HexV1("<!doctype html><h1>Board</h1>"),
|
|
167
|
+
},
|
|
168
|
+
],
|
|
153
169
|
hooks: ["agent/tool-exposure"],
|
|
154
170
|
},
|
|
155
171
|
]);
|
package/src/agent.ts
CHANGED
|
@@ -57,7 +57,7 @@ export interface PackageAuthoringHost {
|
|
|
57
57
|
effectIdFor(input: {
|
|
58
58
|
packageId: string;
|
|
59
59
|
sourceHash: string;
|
|
60
|
-
|
|
60
|
+
uiPageHashes?: Array<{ id: string; htmlHash: string }>;
|
|
61
61
|
hooks?: AuthorPackageInputV1["hooks"];
|
|
62
62
|
}): Promise<string>;
|
|
63
63
|
author(request: AuthorPackageRequestV1): Promise<AuthorPackageOutcomeV1>;
|
|
@@ -119,6 +119,7 @@ export function createPackageAuthorTool(
|
|
|
119
119
|
): ToolDefinition {
|
|
120
120
|
return {
|
|
121
121
|
name: "package_author",
|
|
122
|
+
namespace: "frockbot",
|
|
122
123
|
// A general work tool: the full toolset an `executor` subagent gets, and
|
|
123
124
|
// not part of the narrow reach of `browserUse`, `computerUse`, or the two
|
|
124
125
|
// video roles. See `@frockbot/plugin-subagents` `SUBAGENT_TOOL_REACH_V1`.
|
|
@@ -154,13 +155,18 @@ export function createPackageAuthorTool(
|
|
|
154
155
|
};
|
|
155
156
|
}
|
|
156
157
|
const sourceHash = await sha256HexV1(decoded.source);
|
|
157
|
-
const
|
|
158
|
-
? await
|
|
158
|
+
const uiPageHashes = decoded.ui
|
|
159
|
+
? await Promise.all(
|
|
160
|
+
decoded.ui.pages.map(async (page) => ({
|
|
161
|
+
id: page.id,
|
|
162
|
+
htmlHash: await sha256HexV1(page.html),
|
|
163
|
+
})),
|
|
164
|
+
)
|
|
159
165
|
: undefined;
|
|
160
166
|
const effectId = await host.effectIdFor({
|
|
161
167
|
packageId: decoded.packageId,
|
|
162
168
|
sourceHash,
|
|
163
|
-
...(
|
|
169
|
+
...(uiPageHashes === undefined ? {} : { uiPageHashes }),
|
|
164
170
|
...(decoded.hooks === undefined ? {} : { hooks: decoded.hooks }),
|
|
165
171
|
});
|
|
166
172
|
const position = openTurnPositionV1(session);
|
|
@@ -207,6 +213,7 @@ export function createPackageUndoTool(
|
|
|
207
213
|
): ToolDefinition {
|
|
208
214
|
return {
|
|
209
215
|
name: "package_undo",
|
|
216
|
+
namespace: "frockbot",
|
|
210
217
|
admission: { subagentRoles: ["executor"] },
|
|
211
218
|
description:
|
|
212
219
|
"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.",
|
|
@@ -285,6 +292,7 @@ export function createPackageInspectSelfTool(
|
|
|
285
292
|
): ToolDefinition {
|
|
286
293
|
return {
|
|
287
294
|
name: "package_inspect_self",
|
|
295
|
+
namespace: "frockbot",
|
|
288
296
|
admission: { subagentRoles: ["executor"] },
|
|
289
297
|
description:
|
|
290
298
|
"Read your exact Package execution context contract, current Composition (including your stored Package source), and latest authoring or activation failure per authored Package.",
|
package/src/shared.test.ts
CHANGED
|
@@ -83,14 +83,36 @@ describe("decodeAuthorPackageInputV1", () => {
|
|
|
83
83
|
).toThrow();
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
test("accepts
|
|
87
|
-
const
|
|
86
|
+
test("accepts inline iframe pages with entries and refuses external resources or an oversized page", () => {
|
|
87
|
+
const main = {
|
|
88
|
+
id: "main",
|
|
88
89
|
html: "<!doctype html><style>body{color:red}</style><script>window.frockbot.resize()</script>",
|
|
89
90
|
mounts: [
|
|
90
91
|
{ slot: "frockbot.tool-result:weather_lookup", order: 10 },
|
|
91
92
|
{ slot: "frockbot.bot-settings-sections" },
|
|
92
93
|
],
|
|
93
94
|
};
|
|
95
|
+
const board = {
|
|
96
|
+
id: "board",
|
|
97
|
+
html: "<!doctype html><h1>Board</h1>",
|
|
98
|
+
mounts: [
|
|
99
|
+
{ slot: "frockbot.surface:board" },
|
|
100
|
+
{ slot: "frockbot.right-panel" },
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
const ui = {
|
|
104
|
+
pages: [main, board],
|
|
105
|
+
entries: [
|
|
106
|
+
{
|
|
107
|
+
id: "open",
|
|
108
|
+
slot: "frockbot.sidebar-actions" as const,
|
|
109
|
+
order: 5,
|
|
110
|
+
label: "Weather board",
|
|
111
|
+
icon: "sparkle",
|
|
112
|
+
opens: { kind: "surface" as const, page: "board" },
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
94
116
|
expect(
|
|
95
117
|
decodeAuthorPackageInputV1({
|
|
96
118
|
...VALID,
|
|
@@ -98,18 +120,70 @@ describe("decodeAuthorPackageInputV1", () => {
|
|
|
98
120
|
ui,
|
|
99
121
|
}),
|
|
100
122
|
).toMatchObject({ hooks: ["agent/tool-exposure"], ui });
|
|
123
|
+
const withPages = (pages: unknown) => ({ ...VALID, ui: { pages } });
|
|
124
|
+
expect(() =>
|
|
125
|
+
decodeAuthorPackageInputV1(
|
|
126
|
+
withPages([
|
|
127
|
+
{
|
|
128
|
+
...main,
|
|
129
|
+
html: '<script src="https://example.com/x.js"></script>',
|
|
130
|
+
},
|
|
131
|
+
]),
|
|
132
|
+
),
|
|
133
|
+
).toThrow("inline resources only");
|
|
134
|
+
expect(() =>
|
|
135
|
+
decodeAuthorPackageInputV1(
|
|
136
|
+
withPages([{ ...main, html: "a".repeat(256 * 1024 + 1) }]),
|
|
137
|
+
),
|
|
138
|
+
).toThrow();
|
|
139
|
+
expect(() => decodeAuthorPackageInputV1(withPages([]))).toThrow(
|
|
140
|
+
"non-empty bounded array",
|
|
141
|
+
);
|
|
142
|
+
expect(() =>
|
|
143
|
+
decodeAuthorPackageInputV1(withPages([main, { ...board, id: "main" }])),
|
|
144
|
+
).toThrow("duplicate ids");
|
|
145
|
+
expect(() =>
|
|
146
|
+
decodeAuthorPackageInputV1(withPages([{ ...main, id: "Main" }])),
|
|
147
|
+
).toThrow("id is invalid");
|
|
148
|
+
expect(() =>
|
|
149
|
+
decodeAuthorPackageInputV1(
|
|
150
|
+
withPages([{ ...main, mounts: [{ slot: "frockbot.surface:absent" }] }]),
|
|
151
|
+
),
|
|
152
|
+
).toThrow("not iframe-safe");
|
|
153
|
+
expect(() =>
|
|
154
|
+
decodeAuthorPackageInputV1(
|
|
155
|
+
withPages([
|
|
156
|
+
{ ...main, mounts: [{ slot: "frockbot.tool-result:unknown_tool" }] },
|
|
157
|
+
]),
|
|
158
|
+
),
|
|
159
|
+
).toThrow("not iframe-safe");
|
|
101
160
|
expect(() =>
|
|
102
161
|
decodeAuthorPackageInputV1({
|
|
103
162
|
...VALID,
|
|
104
|
-
ui: {
|
|
163
|
+
ui: {
|
|
164
|
+
pages: [main, board],
|
|
165
|
+
entries: [
|
|
166
|
+
{ ...ui.entries[0]!, opens: { kind: "surface", page: "main" } },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
105
169
|
}),
|
|
106
|
-
).toThrow("
|
|
170
|
+
).toThrow("names no page with a matching surface mount");
|
|
171
|
+
expect(() =>
|
|
172
|
+
decodeAuthorPackageInputV1({
|
|
173
|
+
...VALID,
|
|
174
|
+
ui: {
|
|
175
|
+
pages: [main, board],
|
|
176
|
+
entries: [{ ...ui.entries[0]!, label: "x".repeat(33) }],
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
).toThrow("label must be a bounded non-empty string");
|
|
180
|
+
// The retired single-page shape is not honoured backward.
|
|
107
181
|
expect(() =>
|
|
108
182
|
decodeAuthorPackageInputV1({
|
|
109
183
|
...VALID,
|
|
110
|
-
ui: {
|
|
184
|
+
ui: { html: main.html, mounts: main.mounts },
|
|
111
185
|
}),
|
|
112
|
-
).toThrow();
|
|
186
|
+
).toThrow("has invalid fields");
|
|
113
187
|
});
|
|
114
188
|
});
|
|
115
189
|
|
|
@@ -140,8 +214,38 @@ describe("authoring identity", () => {
|
|
|
140
214
|
runId: "run-1",
|
|
141
215
|
packageId: VALID.packageId,
|
|
142
216
|
sourceHash,
|
|
143
|
-
|
|
217
|
+
uiPageHashes: [
|
|
218
|
+
{
|
|
219
|
+
id: "main",
|
|
220
|
+
htmlHash: await sha256HexV1("<!doctype html><h1>Weather</h1>"),
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
});
|
|
224
|
+
const otherUiPageId = await authoringEffectIdV1({
|
|
225
|
+
runId: "run-1",
|
|
226
|
+
packageId: VALID.packageId,
|
|
227
|
+
sourceHash,
|
|
228
|
+
uiPageHashes: [
|
|
229
|
+
{
|
|
230
|
+
id: "board",
|
|
231
|
+
htmlHash: await sha256HexV1("<!doctype html><h1>Weather</h1>"),
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
});
|
|
235
|
+
expect(otherUi).not.toBe(otherUiPageId);
|
|
236
|
+
const twoPages = await authoringEffectIdV1({
|
|
237
|
+
runId: "run-1",
|
|
238
|
+
packageId: VALID.packageId,
|
|
239
|
+
sourceHash,
|
|
240
|
+
uiPageHashes: [
|
|
241
|
+
{
|
|
242
|
+
id: "main",
|
|
243
|
+
htmlHash: await sha256HexV1("<!doctype html><h1>Weather</h1>"),
|
|
244
|
+
},
|
|
245
|
+
{ id: "board", htmlHash: await sha256HexV1("<!doctype html>b") },
|
|
246
|
+
],
|
|
144
247
|
});
|
|
248
|
+
expect(twoPages).not.toBe(otherUi);
|
|
145
249
|
const otherHooks = await authoringEffectIdV1({
|
|
146
250
|
runId: "run-1",
|
|
147
251
|
packageId: VALID.packageId,
|
|
@@ -215,18 +319,56 @@ describe("authoring identity", () => {
|
|
|
215
319
|
version: "0.0.1",
|
|
216
320
|
tools: VALID.tools,
|
|
217
321
|
ui: {
|
|
218
|
-
|
|
219
|
-
|
|
322
|
+
pages: [
|
|
323
|
+
{
|
|
324
|
+
id: "main",
|
|
325
|
+
artifact,
|
|
326
|
+
mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: "board",
|
|
330
|
+
artifact: { ...artifact, contentHash: "b".repeat(64) },
|
|
331
|
+
mounts: [{ slot: "frockbot.surface:board" }],
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
entries: [
|
|
335
|
+
{
|
|
336
|
+
id: "open",
|
|
337
|
+
slot: "frockbot.sidebar-actions",
|
|
338
|
+
label: "Weather board",
|
|
339
|
+
icon: "sparkle",
|
|
340
|
+
opens: { kind: "surface", page: "board" },
|
|
341
|
+
},
|
|
342
|
+
],
|
|
220
343
|
},
|
|
221
344
|
});
|
|
222
345
|
expect(manifest).toMatchObject({
|
|
223
|
-
schemaVersion:
|
|
346
|
+
schemaVersion: 5,
|
|
224
347
|
contributions: {
|
|
225
348
|
runtime: { host: "bot-isolate" },
|
|
226
349
|
client: {
|
|
227
350
|
kind: "iframe",
|
|
228
|
-
|
|
229
|
-
|
|
351
|
+
pages: [
|
|
352
|
+
{
|
|
353
|
+
id: "main",
|
|
354
|
+
artifact,
|
|
355
|
+
mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: "board",
|
|
359
|
+
artifact: { ...artifact, contentHash: "b".repeat(64) },
|
|
360
|
+
mounts: [{ slot: "frockbot.surface:board" }],
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
entries: [
|
|
364
|
+
{
|
|
365
|
+
id: "open",
|
|
366
|
+
slot: "frockbot.sidebar-actions",
|
|
367
|
+
label: "Weather board",
|
|
368
|
+
icon: "sparkle",
|
|
369
|
+
opens: { kind: "surface", page: "board" },
|
|
370
|
+
},
|
|
371
|
+
],
|
|
230
372
|
},
|
|
231
373
|
},
|
|
232
374
|
});
|
package/src/shared.ts
CHANGED
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
import {
|
|
10
10
|
BOT_ISOLATE_HOOK_EVENTS_V1,
|
|
11
11
|
isBotIsolateHookEventNameV1,
|
|
12
|
+
iframePageSlotAllowedV1,
|
|
12
13
|
PACKAGE_BUNDLE_MAX_SOURCE_BYTES,
|
|
14
|
+
PACKAGE_IFRAME_ENTRY_LABEL_MAX_V1,
|
|
15
|
+
PACKAGE_IFRAME_ENTRY_SLOT_V1,
|
|
16
|
+
PACKAGE_IFRAME_ID_V1,
|
|
17
|
+
PACKAGE_IFRAME_MAX_ENTRIES_V1,
|
|
18
|
+
PACKAGE_IFRAME_MAX_PAGES_V1,
|
|
13
19
|
type BotIsolateHookEventNameV1,
|
|
14
20
|
} from "@frockbot/kernel-contracts";
|
|
15
21
|
|
|
@@ -24,10 +30,21 @@ export interface AuthorPackageInputV1 {
|
|
|
24
30
|
hooks?: BotIsolateHookEventNameV1[];
|
|
25
31
|
/** TypeScript text; exactly one `package.ts`. */
|
|
26
32
|
source: string;
|
|
27
|
-
/** Optional sandboxed
|
|
33
|
+
/** Optional sandboxed pages. All CSS and JavaScript must be inline. */
|
|
28
34
|
ui?: {
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
pages: Array<{
|
|
36
|
+
id: string;
|
|
37
|
+
html: string;
|
|
38
|
+
mounts: Array<{ slot: string; order?: number }>;
|
|
39
|
+
}>;
|
|
40
|
+
entries?: Array<{
|
|
41
|
+
id: string;
|
|
42
|
+
slot: "frockbot.sidebar-actions";
|
|
43
|
+
order?: number;
|
|
44
|
+
label: string;
|
|
45
|
+
icon: string;
|
|
46
|
+
opens: { kind: "surface"; page: string };
|
|
47
|
+
}>;
|
|
31
48
|
};
|
|
32
49
|
}
|
|
33
50
|
|
|
@@ -259,62 +276,165 @@ export function decodeAuthorPackageInputV1(
|
|
|
259
276
|
let ui: AuthorPackageInputV1["ui"];
|
|
260
277
|
if (value.ui !== undefined) {
|
|
261
278
|
const rawUi = record(value.ui, `${label}.ui`);
|
|
262
|
-
exactKeys(rawUi, ["
|
|
263
|
-
const html = boundedString(
|
|
264
|
-
rawUi.html,
|
|
265
|
-
`${label}.ui.html`,
|
|
266
|
-
PACKAGE_BUNDLE_MAX_SOURCE_BYTES,
|
|
267
|
-
);
|
|
279
|
+
exactKeys(rawUi, ["pages"], ["entries"], `${label}.ui`);
|
|
268
280
|
if (
|
|
269
|
-
|
|
270
|
-
|
|
281
|
+
!Array.isArray(rawUi.pages) ||
|
|
282
|
+
rawUi.pages.length === 0 ||
|
|
283
|
+
rawUi.pages.length > PACKAGE_IFRAME_MAX_PAGES_V1
|
|
271
284
|
) {
|
|
272
|
-
throw new Error(`${label}.ui.
|
|
285
|
+
throw new Error(`${label}.ui.pages must be a non-empty bounded array`);
|
|
273
286
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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`);
|
|
287
|
+
const pageIds = rawUi.pages.map((candidate, index) => {
|
|
288
|
+
const page = record(candidate, `${label}.ui.pages[${index}]`);
|
|
289
|
+
const id = boundedString(page.id, `${label}.ui.pages[${index}].id`, 32);
|
|
290
|
+
if (!PACKAGE_IFRAME_ID_V1.test(id)) {
|
|
291
|
+
throw new Error(`${label}.ui.pages[${index}].id is invalid`);
|
|
292
|
+
}
|
|
293
|
+
return id;
|
|
294
|
+
});
|
|
295
|
+
if (new Set(pageIds).size !== pageIds.length) {
|
|
296
|
+
throw new Error(`${label}.ui.pages contains duplicate ids`);
|
|
293
297
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
298
|
+
const declaredTools = tools.map((tool) => tool.name);
|
|
299
|
+
const pages = rawUi.pages.map((candidate, index) => {
|
|
300
|
+
const pageLabel = `${label}.ui.pages[${index}]`;
|
|
301
|
+
const page = record(candidate, pageLabel);
|
|
302
|
+
exactKeys(page, ["id", "html", "mounts"], [], pageLabel);
|
|
303
|
+
const html = boundedString(
|
|
304
|
+
page.html,
|
|
305
|
+
`${pageLabel}.html`,
|
|
306
|
+
PACKAGE_BUNDLE_MAX_SOURCE_BYTES,
|
|
301
307
|
);
|
|
302
308
|
if (
|
|
303
|
-
|
|
304
|
-
|
|
309
|
+
new TextEncoder().encode(html).byteLength >
|
|
310
|
+
PACKAGE_BUNDLE_MAX_SOURCE_BYTES
|
|
311
|
+
) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`${pageLabel}.html exceeds the per-Package source quota`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
if (
|
|
317
|
+
/<(?:script|iframe|img|audio|video|source|embed|input)\b[^>]*\bsrc\s*=\s*["'](?!data:)/i.test(
|
|
318
|
+
html,
|
|
319
|
+
) ||
|
|
320
|
+
/<object\b[^>]*\bdata\s*=\s*["'](?!data:)/i.test(html) ||
|
|
321
|
+
/\bsrcset\s*=/i.test(html) ||
|
|
322
|
+
/<link\b/i.test(html) ||
|
|
323
|
+
/@import\b/i.test(html) ||
|
|
324
|
+
/<meta\b[^>]*http-equiv\s*=\s*["']?refresh/i.test(html) ||
|
|
325
|
+
/url\(\s*["']?(?!data:|["']?\s*\))/i.test(html)
|
|
305
326
|
) {
|
|
306
|
-
throw new Error(`${
|
|
327
|
+
throw new Error(`${pageLabel}.html may contain inline resources only`);
|
|
307
328
|
}
|
|
308
|
-
const order = mount.order;
|
|
309
329
|
if (
|
|
310
|
-
|
|
311
|
-
|
|
330
|
+
!Array.isArray(page.mounts) ||
|
|
331
|
+
page.mounts.length === 0 ||
|
|
332
|
+
page.mounts.length > 64
|
|
312
333
|
) {
|
|
313
|
-
throw new Error(
|
|
334
|
+
throw new Error(
|
|
335
|
+
`${pageLabel}.mounts must be a non-empty bounded array`,
|
|
336
|
+
);
|
|
314
337
|
}
|
|
315
|
-
|
|
338
|
+
const mounts = page.mounts.map((candidateMount, mountIndex) => {
|
|
339
|
+
const mount = record(
|
|
340
|
+
candidateMount,
|
|
341
|
+
`${pageLabel}.mounts[${mountIndex}]`,
|
|
342
|
+
);
|
|
343
|
+
exactKeys(
|
|
344
|
+
mount,
|
|
345
|
+
["slot"],
|
|
346
|
+
["order"],
|
|
347
|
+
`${pageLabel}.mounts[${mountIndex}]`,
|
|
348
|
+
);
|
|
349
|
+
const slot = boundedString(
|
|
350
|
+
mount.slot,
|
|
351
|
+
`${pageLabel}.mounts[${mountIndex}].slot`,
|
|
352
|
+
160,
|
|
353
|
+
);
|
|
354
|
+
if (!iframePageSlotAllowedV1(slot, { declaredTools, pageIds })) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`${pageLabel}.mounts[${mountIndex}].slot is not iframe-safe`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const order = mount.order;
|
|
360
|
+
if (
|
|
361
|
+
order !== undefined &&
|
|
362
|
+
(typeof order !== "number" || !Number.isFinite(order))
|
|
363
|
+
) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`${pageLabel}.mounts[${mountIndex}].order must be finite`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return { slot, ...(order === undefined ? {} : { order }) };
|
|
369
|
+
});
|
|
370
|
+
return { id: pageIds[index]!, html, mounts };
|
|
316
371
|
});
|
|
317
|
-
|
|
372
|
+
let entries: NonNullable<AuthorPackageInputV1["ui"]>["entries"];
|
|
373
|
+
if (rawUi.entries !== undefined) {
|
|
374
|
+
if (
|
|
375
|
+
!Array.isArray(rawUi.entries) ||
|
|
376
|
+
rawUi.entries.length > PACKAGE_IFRAME_MAX_ENTRIES_V1
|
|
377
|
+
) {
|
|
378
|
+
throw new Error(`${label}.ui.entries must be a bounded array`);
|
|
379
|
+
}
|
|
380
|
+
entries = rawUi.entries.map((candidate, index) => {
|
|
381
|
+
const entryLabel = `${label}.ui.entries[${index}]`;
|
|
382
|
+
const entry = record(candidate, entryLabel);
|
|
383
|
+
exactKeys(
|
|
384
|
+
entry,
|
|
385
|
+
["id", "slot", "label", "icon", "opens"],
|
|
386
|
+
["order"],
|
|
387
|
+
entryLabel,
|
|
388
|
+
);
|
|
389
|
+
const id = boundedString(entry.id, `${entryLabel}.id`, 32);
|
|
390
|
+
if (!PACKAGE_IFRAME_ID_V1.test(id)) {
|
|
391
|
+
throw new Error(`${entryLabel}.id is invalid`);
|
|
392
|
+
}
|
|
393
|
+
if (entry.slot !== PACKAGE_IFRAME_ENTRY_SLOT_V1) {
|
|
394
|
+
throw new Error(`${entryLabel}.slot is invalid`);
|
|
395
|
+
}
|
|
396
|
+
const order = entry.order;
|
|
397
|
+
if (
|
|
398
|
+
order !== undefined &&
|
|
399
|
+
(typeof order !== "number" || !Number.isFinite(order))
|
|
400
|
+
) {
|
|
401
|
+
throw new Error(`${entryLabel}.order must be finite`);
|
|
402
|
+
}
|
|
403
|
+
const opens = record(entry.opens, `${entryLabel}.opens`);
|
|
404
|
+
exactKeys(opens, ["kind", "page"], [], `${entryLabel}.opens`);
|
|
405
|
+
if (opens.kind !== "surface") {
|
|
406
|
+
throw new Error(`${entryLabel}.opens.kind is invalid`);
|
|
407
|
+
}
|
|
408
|
+
const page = boundedString(opens.page, `${entryLabel}.opens.page`, 32);
|
|
409
|
+
const target = pages.find((candidatePage) => candidatePage.id === page);
|
|
410
|
+
if (
|
|
411
|
+
!target ||
|
|
412
|
+
!target.mounts.some(
|
|
413
|
+
(mount) => mount.slot === `frockbot.surface:${page}`,
|
|
414
|
+
)
|
|
415
|
+
) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`${entryLabel}.opens.page names no page with a matching surface mount`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
id,
|
|
422
|
+
slot: PACKAGE_IFRAME_ENTRY_SLOT_V1 as "frockbot.sidebar-actions",
|
|
423
|
+
...(order === undefined ? {} : { order }),
|
|
424
|
+
label: boundedString(
|
|
425
|
+
entry.label,
|
|
426
|
+
`${entryLabel}.label`,
|
|
427
|
+
PACKAGE_IFRAME_ENTRY_LABEL_MAX_V1,
|
|
428
|
+
),
|
|
429
|
+
icon: boundedString(entry.icon, `${entryLabel}.icon`, 64),
|
|
430
|
+
opens: { kind: "surface" as const, page },
|
|
431
|
+
};
|
|
432
|
+
});
|
|
433
|
+
if (new Set(entries.map((entry) => entry.id)).size !== entries.length) {
|
|
434
|
+
throw new Error(`${label}.ui.entries contains duplicate ids`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
ui = { pages, ...(entries ? { entries } : {}) };
|
|
318
438
|
}
|
|
319
439
|
return {
|
|
320
440
|
packageId,
|
|
@@ -370,28 +490,81 @@ export const AUTHOR_PACKAGE_INPUT_SCHEMA_V1 = {
|
|
|
370
490
|
ui: {
|
|
371
491
|
type: "object",
|
|
372
492
|
additionalProperties: false,
|
|
373
|
-
required: ["
|
|
493
|
+
required: ["pages"],
|
|
374
494
|
properties: {
|
|
375
|
-
|
|
376
|
-
type: "
|
|
495
|
+
pages: {
|
|
496
|
+
type: "array",
|
|
497
|
+
minItems: 1,
|
|
498
|
+
maxItems: PACKAGE_IFRAME_MAX_PAGES_V1,
|
|
377
499
|
description:
|
|
378
|
-
"
|
|
500
|
+
"Up to 8 sandboxed HTML pages, each mounted into one or more slots.",
|
|
501
|
+
items: {
|
|
502
|
+
type: "object",
|
|
503
|
+
additionalProperties: false,
|
|
504
|
+
required: ["id", "html", "mounts"],
|
|
505
|
+
properties: {
|
|
506
|
+
id: {
|
|
507
|
+
type: "string",
|
|
508
|
+
description:
|
|
509
|
+
"Lowercase page id, /^[a-z][a-z0-9-]{0,31}$/, unique in this Package.",
|
|
510
|
+
},
|
|
511
|
+
html: {
|
|
512
|
+
type: "string",
|
|
513
|
+
description:
|
|
514
|
+
"One HTML page (maximum 256 KB). CSS and JavaScript must be inline; images may use data: URLs.",
|
|
515
|
+
},
|
|
516
|
+
mounts: {
|
|
517
|
+
type: "array",
|
|
518
|
+
minItems: 1,
|
|
519
|
+
maxItems: 64,
|
|
520
|
+
items: {
|
|
521
|
+
type: "object",
|
|
522
|
+
additionalProperties: false,
|
|
523
|
+
required: ["slot"],
|
|
524
|
+
properties: {
|
|
525
|
+
slot: {
|
|
526
|
+
type: "string",
|
|
527
|
+
description:
|
|
528
|
+
"frockbot.bot-settings-sections, frockbot.tool-result:<declaredToolName>, frockbot.right-panel, or frockbot.surface:<pageId>",
|
|
529
|
+
},
|
|
530
|
+
order: { type: "number" },
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
},
|
|
379
536
|
},
|
|
380
|
-
|
|
537
|
+
entries: {
|
|
381
538
|
type: "array",
|
|
382
|
-
|
|
383
|
-
|
|
539
|
+
maxItems: PACKAGE_IFRAME_MAX_ENTRIES_V1,
|
|
540
|
+
description:
|
|
541
|
+
"Up to 4 sidebar launchers. Each opens one of your pages as an overlay surface; that page must mount frockbot.surface:<its own id>.",
|
|
384
542
|
items: {
|
|
385
543
|
type: "object",
|
|
386
544
|
additionalProperties: false,
|
|
387
|
-
required: ["slot"],
|
|
545
|
+
required: ["id", "slot", "label", "icon", "opens"],
|
|
388
546
|
properties: {
|
|
389
|
-
|
|
547
|
+
id: {
|
|
390
548
|
type: "string",
|
|
391
549
|
description:
|
|
392
|
-
"
|
|
550
|
+
"Lowercase entry id, /^[a-z][a-z0-9-]{0,31}$/, unique in this Package.",
|
|
393
551
|
},
|
|
552
|
+
slot: { type: "string", enum: [PACKAGE_IFRAME_ENTRY_SLOT_V1] },
|
|
394
553
|
order: { type: "number" },
|
|
554
|
+
label: {
|
|
555
|
+
type: "string",
|
|
556
|
+
maxLength: PACKAGE_IFRAME_ENTRY_LABEL_MAX_V1,
|
|
557
|
+
},
|
|
558
|
+
icon: { type: "string", description: "A FrockBot UiIcon name." },
|
|
559
|
+
opens: {
|
|
560
|
+
type: "object",
|
|
561
|
+
additionalProperties: false,
|
|
562
|
+
required: ["kind", "page"],
|
|
563
|
+
properties: {
|
|
564
|
+
kind: { type: "string", enum: ["surface"] },
|
|
565
|
+
page: { type: "string" },
|
|
566
|
+
},
|
|
567
|
+
},
|
|
395
568
|
},
|
|
396
569
|
},
|
|
397
570
|
},
|
|
@@ -449,7 +622,8 @@ export async function authoringEffectIdV1(input: {
|
|
|
449
622
|
runId: string;
|
|
450
623
|
packageId: string;
|
|
451
624
|
sourceHash: string;
|
|
452
|
-
|
|
625
|
+
/** Every page the bundler is asked for, in declaration order. */
|
|
626
|
+
uiPageHashes?: Array<{ id: string; htmlHash: string }>;
|
|
453
627
|
hooks?: BotIsolateHookEventNameV1[];
|
|
454
628
|
}): Promise<string> {
|
|
455
629
|
const digest = await sha256HexV1(
|
|
@@ -457,7 +631,7 @@ export async function authoringEffectIdV1(input: {
|
|
|
457
631
|
input.runId,
|
|
458
632
|
input.packageId,
|
|
459
633
|
input.sourceHash,
|
|
460
|
-
input.
|
|
634
|
+
(input.uiPageHashes ?? []).map((page) => [page.id, page.htmlHash]),
|
|
461
635
|
input.hooks ?? [],
|
|
462
636
|
]),
|
|
463
637
|
);
|
|
@@ -502,17 +676,21 @@ export function authoredManifestV1(input: {
|
|
|
502
676
|
tools: AuthorPackageInputV1["tools"];
|
|
503
677
|
hooks?: AuthorPackageInputV1["hooks"];
|
|
504
678
|
ui?: {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
679
|
+
pages: Array<{
|
|
680
|
+
id: string;
|
|
681
|
+
artifact: {
|
|
682
|
+
contentHash: string;
|
|
683
|
+
size: number;
|
|
684
|
+
mediaType: "text/html";
|
|
685
|
+
bundlerVersion: string;
|
|
686
|
+
};
|
|
687
|
+
mounts: Array<{ slot: string; order?: number }>;
|
|
688
|
+
}>;
|
|
689
|
+
entries?: NonNullable<AuthorPackageInputV1["ui"]>["entries"];
|
|
512
690
|
};
|
|
513
691
|
}): Record<string, unknown> {
|
|
514
692
|
return {
|
|
515
|
-
schemaVersion:
|
|
693
|
+
schemaVersion: 5,
|
|
516
694
|
id: input.packageId,
|
|
517
695
|
displayName: input.displayName,
|
|
518
696
|
version: input.version,
|
|
@@ -524,8 +702,19 @@ export function authoredManifestV1(input: {
|
|
|
524
702
|
? {
|
|
525
703
|
client: {
|
|
526
704
|
kind: "iframe",
|
|
527
|
-
|
|
528
|
-
|
|
705
|
+
pages: input.ui.pages.map((page) => ({
|
|
706
|
+
id: page.id,
|
|
707
|
+
artifact: { ...page.artifact },
|
|
708
|
+
mounts: page.mounts.map((mount) => ({ ...mount })),
|
|
709
|
+
})),
|
|
710
|
+
...(input.ui.entries
|
|
711
|
+
? {
|
|
712
|
+
entries: input.ui.entries.map((entry) => ({
|
|
713
|
+
...entry,
|
|
714
|
+
opens: { ...entry.opens },
|
|
715
|
+
})),
|
|
716
|
+
}
|
|
717
|
+
: {}),
|
|
529
718
|
},
|
|
530
719
|
}
|
|
531
720
|
: {}),
|