@frockbot/kernel-contracts 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/applets.test.ts +299 -0
- package/src/applets.ts +902 -0
- package/src/authoring.test.ts +47 -0
- package/src/authoring.ts +57 -21
- package/src/contributions.ts +102 -0
- package/src/iframe-ui.test.ts +238 -37
- package/src/iframe-ui.ts +430 -89
- package/src/index.ts +2 -0
- package/src/isolate-context-catalog.generated.ts +3 -2
- package/src/isolate.ts +98 -0
- package/src/types.ts +10 -3
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/kernel-contracts",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
|
-
".": "./src/index.ts"
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./contributions": "./src/contributions.ts",
|
|
9
|
+
"./package.json": "./package.json"
|
|
8
10
|
},
|
|
9
11
|
"scripts": {
|
|
10
12
|
"test": "bun test src",
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
decodeAppletDirectoryEntryV1,
|
|
4
|
+
decodeAppletGenerationSummaryV1,
|
|
5
|
+
decodeAppletGenerationV1,
|
|
6
|
+
decodeAppletPublishResultV1,
|
|
7
|
+
decodeAppletBuildViewV1,
|
|
8
|
+
decodeAppletListViewV1,
|
|
9
|
+
decodeAppletSourceViewV1,
|
|
10
|
+
decodeAppletSummaryV1,
|
|
11
|
+
decodeAppletUiViewV1,
|
|
12
|
+
decodeAppletViewerTokenV1,
|
|
13
|
+
} from "./applets.js";
|
|
14
|
+
|
|
15
|
+
const tool = {
|
|
16
|
+
name: "add_todo",
|
|
17
|
+
description: "Add a todo",
|
|
18
|
+
inputSchema: { type: "object" },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const server = {
|
|
22
|
+
contentHash: "a".repeat(64),
|
|
23
|
+
size: 128,
|
|
24
|
+
mediaType: "application/javascript",
|
|
25
|
+
bundlerVersion: "frockbot-esbuild@1",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ui = {
|
|
29
|
+
contentHash: "b".repeat(64),
|
|
30
|
+
size: 64,
|
|
31
|
+
mediaType: "text/html",
|
|
32
|
+
bundlerVersion: "frockbot-inline-html@1",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe("Applet directory entry v1", () => {
|
|
36
|
+
const entry = {
|
|
37
|
+
schemaVersion: 1,
|
|
38
|
+
appletId: "u1abc.todo",
|
|
39
|
+
displayName: "Todo",
|
|
40
|
+
currentGenerationId: "generation-2",
|
|
41
|
+
tools: [tool],
|
|
42
|
+
provenance: {
|
|
43
|
+
kind: "bot",
|
|
44
|
+
botId: "bot-1",
|
|
45
|
+
sessionId: "session-1",
|
|
46
|
+
turnId: "turn-1",
|
|
47
|
+
},
|
|
48
|
+
createdAt: "2026-09-03T00:00:00.000Z",
|
|
49
|
+
status: "published",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
test("round-trips the exact record", () => {
|
|
53
|
+
expect(decodeAppletDirectoryEntryV1(entry)).toEqual(entry as never);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("accepts a draft with no current generation and User provenance", () => {
|
|
57
|
+
const { currentGenerationId: _unused, ...draft } = entry;
|
|
58
|
+
const decoded = decodeAppletDirectoryEntryV1({
|
|
59
|
+
...draft,
|
|
60
|
+
provenance: { kind: "user" },
|
|
61
|
+
status: "draft",
|
|
62
|
+
});
|
|
63
|
+
expect(Object.hasOwn(decoded, "currentGenerationId")).toBe(false);
|
|
64
|
+
expect(decoded.provenance).toEqual({ kind: "user" });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("fails closed on an unknown field, id, status, or provenance", () => {
|
|
68
|
+
expect(() =>
|
|
69
|
+
decodeAppletDirectoryEntryV1({ ...entry, userId: "u1" }),
|
|
70
|
+
).toThrow("invalid fields");
|
|
71
|
+
expect(() =>
|
|
72
|
+
decodeAppletDirectoryEntryV1({ ...entry, appletId: "todo" }),
|
|
73
|
+
).toThrow("appletId is invalid");
|
|
74
|
+
expect(() =>
|
|
75
|
+
decodeAppletDirectoryEntryV1({ ...entry, status: "archived" }),
|
|
76
|
+
).toThrow("status is invalid");
|
|
77
|
+
expect(() =>
|
|
78
|
+
decodeAppletDirectoryEntryV1({
|
|
79
|
+
...entry,
|
|
80
|
+
provenance: { kind: "catalog" },
|
|
81
|
+
}),
|
|
82
|
+
).toThrow("provenance.kind is invalid");
|
|
83
|
+
expect(() =>
|
|
84
|
+
decodeAppletDirectoryEntryV1({ ...entry, schemaVersion: 2 }),
|
|
85
|
+
).toThrow("schemaVersion is unsupported");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("Applet generation v1", () => {
|
|
90
|
+
const generation = {
|
|
91
|
+
schemaVersion: 1,
|
|
92
|
+
generationId: "generation-2",
|
|
93
|
+
parentGenerationId: "generation-1",
|
|
94
|
+
server,
|
|
95
|
+
ui,
|
|
96
|
+
tools: [tool],
|
|
97
|
+
contract: 1,
|
|
98
|
+
origin: "publish",
|
|
99
|
+
provenance: {
|
|
100
|
+
botId: "bot-1",
|
|
101
|
+
sessionId: "session-1",
|
|
102
|
+
turnId: "turn-1",
|
|
103
|
+
runId: "run-1",
|
|
104
|
+
},
|
|
105
|
+
createdAt: "2026-09-03T00:00:00.000Z",
|
|
106
|
+
status: "active",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
test("round-trips the exact record", () => {
|
|
110
|
+
expect(decodeAppletGenerationV1(generation)).toEqual(generation as never);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("fails closed on the artifact shapes and the contract", () => {
|
|
114
|
+
expect(() =>
|
|
115
|
+
decodeAppletGenerationV1({ ...generation, contract: 2 }),
|
|
116
|
+
).toThrow("contract is unsupported");
|
|
117
|
+
expect(() =>
|
|
118
|
+
decodeAppletGenerationV1({
|
|
119
|
+
...generation,
|
|
120
|
+
server: { ...server, mediaType: "text/html" },
|
|
121
|
+
}),
|
|
122
|
+
).toThrow("server.mediaType is invalid");
|
|
123
|
+
expect(() =>
|
|
124
|
+
decodeAppletGenerationV1({
|
|
125
|
+
...generation,
|
|
126
|
+
ui: { ...ui, size: 4 * 1024 * 1024 + 1 },
|
|
127
|
+
}),
|
|
128
|
+
).toThrow("4 MB quota");
|
|
129
|
+
expect(() =>
|
|
130
|
+
decodeAppletGenerationV1({ ...generation, origin: "rollback" }),
|
|
131
|
+
).toThrow("origin is invalid");
|
|
132
|
+
expect(() =>
|
|
133
|
+
decodeAppletGenerationV1({ ...generation, tools: [tool, tool] }),
|
|
134
|
+
).toThrow("duplicate names");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("Applet views and viewer tokens", () => {
|
|
139
|
+
test("decodes a summary and a generation summary", () => {
|
|
140
|
+
const summary = {
|
|
141
|
+
appletId: "u1abc.todo",
|
|
142
|
+
displayName: "Todo",
|
|
143
|
+
status: "published",
|
|
144
|
+
currentGenerationId: "generation-2",
|
|
145
|
+
tools: ["add_todo"],
|
|
146
|
+
createdAt: "2026-09-03T00:00:00.000Z",
|
|
147
|
+
};
|
|
148
|
+
expect(decodeAppletSummaryV1(summary)).toEqual(summary as never);
|
|
149
|
+
const generationSummary = {
|
|
150
|
+
generationId: "generation-2",
|
|
151
|
+
parentGenerationId: "generation-1",
|
|
152
|
+
origin: "revert",
|
|
153
|
+
status: "superseded",
|
|
154
|
+
tools: ["add_todo"],
|
|
155
|
+
createdAt: "2026-09-03T00:00:00.000Z",
|
|
156
|
+
isCurrent: false,
|
|
157
|
+
};
|
|
158
|
+
expect(decodeAppletGenerationSummaryV1(generationSummary)).toEqual(
|
|
159
|
+
generationSummary as never,
|
|
160
|
+
);
|
|
161
|
+
expect(() =>
|
|
162
|
+
decodeAppletGenerationSummaryV1({
|
|
163
|
+
...generationSummary,
|
|
164
|
+
isCurrent: "no",
|
|
165
|
+
}),
|
|
166
|
+
).toThrow("isCurrent must be a boolean");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("decodes both publish outcomes and refuses a third", () => {
|
|
170
|
+
expect(
|
|
171
|
+
decodeAppletPublishResultV1({
|
|
172
|
+
status: "published",
|
|
173
|
+
appletId: "u1abc.todo",
|
|
174
|
+
generationId: "generation-3",
|
|
175
|
+
tools: ["add_todo"],
|
|
176
|
+
}),
|
|
177
|
+
).toMatchObject({ status: "published", generationId: "generation-3" });
|
|
178
|
+
expect(
|
|
179
|
+
decodeAppletPublishResultV1({
|
|
180
|
+
status: "failed",
|
|
181
|
+
appletId: "u1abc.todo",
|
|
182
|
+
generationId: "generation-3",
|
|
183
|
+
reason: "health check failed",
|
|
184
|
+
diagnostics: ["tool list did not match the declaration"],
|
|
185
|
+
}),
|
|
186
|
+
).toMatchObject({ status: "failed" });
|
|
187
|
+
expect(() =>
|
|
188
|
+
decodeAppletPublishResultV1({
|
|
189
|
+
status: "pending",
|
|
190
|
+
appletId: "u1abc.todo",
|
|
191
|
+
}),
|
|
192
|
+
).toThrow("status is invalid");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("bounds a viewer token and its socket URL", () => {
|
|
196
|
+
const token = {
|
|
197
|
+
token: "signed.token.value",
|
|
198
|
+
expiresAt: "2026-09-03T00:15:00.000Z",
|
|
199
|
+
socketUrl: "wss://bot.frockbot.com/api/applets/u1abc.todo/socket",
|
|
200
|
+
};
|
|
201
|
+
expect(decodeAppletViewerTokenV1(token)).toEqual(token);
|
|
202
|
+
expect(() =>
|
|
203
|
+
decodeAppletViewerTokenV1({ ...token, socketUrl: "ftp://example.com" }),
|
|
204
|
+
).toThrow("socketUrl is invalid");
|
|
205
|
+
expect(() => decodeAppletViewerTokenV1({ ...token, userId: "u1" })).toThrow(
|
|
206
|
+
"invalid fields",
|
|
207
|
+
);
|
|
208
|
+
expect(() =>
|
|
209
|
+
decodeAppletViewerTokenV1({ ...token, expiresAt: "soon" }),
|
|
210
|
+
).toThrow("ISO timestamp");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("Applet canvas projections", () => {
|
|
215
|
+
const summary = {
|
|
216
|
+
appletId: "u1abc.todo",
|
|
217
|
+
displayName: "Todo",
|
|
218
|
+
status: "published" as const,
|
|
219
|
+
currentGenerationId: "generation-2",
|
|
220
|
+
tools: ["add_todo"],
|
|
221
|
+
createdAt: "2026-09-03T00:00:00.000Z",
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
test("decodes the Applet list a canvas reads", () => {
|
|
225
|
+
expect(
|
|
226
|
+
decodeAppletListViewV1({ schemaVersion: 1, applets: [summary] }),
|
|
227
|
+
).toEqual({ schemaVersion: 1, applets: [summary] });
|
|
228
|
+
expect(() =>
|
|
229
|
+
decodeAppletListViewV1({ schemaVersion: 1, applets: [summary, summary] }),
|
|
230
|
+
).toThrow("duplicate Applets");
|
|
231
|
+
expect(() =>
|
|
232
|
+
decodeAppletListViewV1({ schemaVersion: 2, applets: [] }),
|
|
233
|
+
).toThrow("unsupported");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("bounds the source read the building canvas draws", () => {
|
|
237
|
+
const view = {
|
|
238
|
+
appletId: "u1abc.todo",
|
|
239
|
+
files: [
|
|
240
|
+
{
|
|
241
|
+
path: "src/server.ts",
|
|
242
|
+
text: "export class TodoApplet {}",
|
|
243
|
+
generationId: "w-1",
|
|
244
|
+
changedAt: "2026-09-03T00:01:00.000Z",
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
truncated: false,
|
|
248
|
+
};
|
|
249
|
+
expect(decodeAppletSourceViewV1(view)).toEqual(view);
|
|
250
|
+
expect(() =>
|
|
251
|
+
decodeAppletSourceViewV1({
|
|
252
|
+
...view,
|
|
253
|
+
files: [{ ...view.files[0]!, path: "../escape.ts" }],
|
|
254
|
+
}),
|
|
255
|
+
).toThrow("path is invalid");
|
|
256
|
+
expect(() =>
|
|
257
|
+
decodeAppletSourceViewV1({
|
|
258
|
+
...view,
|
|
259
|
+
files: [
|
|
260
|
+
{ path: "big.ts", text: "x".repeat(600 * 1024), generationId: "w-1" },
|
|
261
|
+
],
|
|
262
|
+
}),
|
|
263
|
+
).toThrow("source read limit");
|
|
264
|
+
expect(() =>
|
|
265
|
+
decodeAppletSourceViewV1({
|
|
266
|
+
...view,
|
|
267
|
+
files: [...view.files, ...view.files],
|
|
268
|
+
}),
|
|
269
|
+
).toThrow("duplicate paths");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("an unrecorded build outcome is unknown rather than a failure", () => {
|
|
273
|
+
expect(decodeAppletBuildViewV1({ status: "unknown" })).toEqual({
|
|
274
|
+
status: "unknown",
|
|
275
|
+
});
|
|
276
|
+
const failed = {
|
|
277
|
+
status: "failed" as const,
|
|
278
|
+
command: "check" as const,
|
|
279
|
+
at: "2026-09-03T00:02:00.000Z",
|
|
280
|
+
summary: "2 type errors",
|
|
281
|
+
diagnostics: ["src/server.ts:3:1 - error TS2339"],
|
|
282
|
+
};
|
|
283
|
+
expect(decodeAppletBuildViewV1(failed)).toEqual(failed);
|
|
284
|
+
expect(() => decodeAppletBuildViewV1({ status: "broken" })).toThrow(
|
|
285
|
+
"status is invalid",
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("the UI route names an http origin and a generation", () => {
|
|
290
|
+
const ui = {
|
|
291
|
+
uiUrl: "https://ui.bot.frockbot.com/packages/aa.html",
|
|
292
|
+
generationId: "generation-2",
|
|
293
|
+
};
|
|
294
|
+
expect(decodeAppletUiViewV1(ui)).toEqual(ui);
|
|
295
|
+
expect(() =>
|
|
296
|
+
decodeAppletUiViewV1({ ...ui, uiUrl: "ftp://example.com/x" }),
|
|
297
|
+
).toThrow("uiUrl is invalid");
|
|
298
|
+
});
|
|
299
|
+
});
|