@frockbot/kernel-composition 0.0.0 → 0.1.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 +31 -6
- package/src/activation.ts +417 -0
- package/src/compiler.test.ts +230 -0
- package/src/compiler.ts +288 -0
- package/src/generation.test.ts +203 -0
- package/src/generation.ts +423 -0
- package/src/index.test.ts +1072 -0
- package/src/index.ts +328 -0
- package/src/isolate-host.test.ts +423 -0
- package/src/isolate-host.ts +467 -0
- package/src/isolate-wrapper.test.ts +125 -0
- package/src/isolate-wrapper.ts +247 -0
- package/src/manifest.ts +1233 -0
- package/src/runtime.ts +18 -0
- package/tsconfig.json +15 -0
- package/README.md +0 -3
|
@@ -0,0 +1,1072 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { Context } from "cordis";
|
|
3
|
+
import {
|
|
4
|
+
type ActiveContribution,
|
|
5
|
+
type ContributionHost,
|
|
6
|
+
type ContributionKind,
|
|
7
|
+
decodeFrockBotManifest,
|
|
8
|
+
declaredContributionKinds,
|
|
9
|
+
LocalCordisContributionHost,
|
|
10
|
+
PackageCatalog,
|
|
11
|
+
type PackageDescriptor,
|
|
12
|
+
type PackageSettingSchema,
|
|
13
|
+
type PreparedContribution,
|
|
14
|
+
} from "./index.js";
|
|
15
|
+
|
|
16
|
+
const roots: Context[] = [];
|
|
17
|
+
|
|
18
|
+
async function createCatalog(): Promise<Context> {
|
|
19
|
+
const root = new Context();
|
|
20
|
+
roots.push(root);
|
|
21
|
+
await root.plugin(PackageCatalog);
|
|
22
|
+
return root;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function manifest(id = "fixture") {
|
|
26
|
+
return {
|
|
27
|
+
schemaVersion: 3,
|
|
28
|
+
id,
|
|
29
|
+
displayName: id,
|
|
30
|
+
version: "1.0.0",
|
|
31
|
+
compatibility: { frockbot: "*" },
|
|
32
|
+
contributions: {
|
|
33
|
+
runtime: { entry: "./agent" },
|
|
34
|
+
desktop: {
|
|
35
|
+
entry: "./host",
|
|
36
|
+
execution: "trusted-main",
|
|
37
|
+
commands: [],
|
|
38
|
+
},
|
|
39
|
+
client: { entry: "./client.ts", mounts: [], outlets: [] },
|
|
40
|
+
},
|
|
41
|
+
permissions: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function v3ManifestWithSchema(schema: unknown) {
|
|
46
|
+
return {
|
|
47
|
+
schemaVersion: 3,
|
|
48
|
+
id: "schema-fixture",
|
|
49
|
+
displayName: "Schema Fixture",
|
|
50
|
+
version: "1.0.0",
|
|
51
|
+
compatibility: { frockbot: "*" },
|
|
52
|
+
contributions: { runtime: { entry: "./runtime" } },
|
|
53
|
+
configuration: {
|
|
54
|
+
settings: [
|
|
55
|
+
{
|
|
56
|
+
id: "preferences",
|
|
57
|
+
schemaVersion: 1,
|
|
58
|
+
scopes: ["user"],
|
|
59
|
+
schema,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class FakeHost implements ContributionHost {
|
|
67
|
+
readonly kind: ContributionKind;
|
|
68
|
+
private log: string[];
|
|
69
|
+
private failCommit: boolean;
|
|
70
|
+
|
|
71
|
+
constructor(kind: ContributionKind, log: string[], failCommit = false) {
|
|
72
|
+
this.kind = kind;
|
|
73
|
+
this.log = log;
|
|
74
|
+
this.failCommit = failCommit;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
prepare(pkg: PackageDescriptor): Promise<PreparedContribution> {
|
|
78
|
+
this.log.push(`prepare:${this.kind}:${pkg.manifest.id}`);
|
|
79
|
+
return Promise.resolve({
|
|
80
|
+
kind: this.kind,
|
|
81
|
+
commit: async (): Promise<ActiveContribution> => {
|
|
82
|
+
this.log.push(`commit:${this.kind}`);
|
|
83
|
+
if (this.failCommit) throw new Error(`${this.kind} commit failed`);
|
|
84
|
+
return {
|
|
85
|
+
dispose: async () => {
|
|
86
|
+
this.log.push(`dispose:${this.kind}`);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
rollback: async () => {
|
|
91
|
+
this.log.push(`rollback:${this.kind}`);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
afterEach(async () => {
|
|
98
|
+
await Promise.all(roots.splice(0).map((root) => root.fiber.dispose()));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("PackageCatalog", () => {
|
|
102
|
+
test("keeps trusted main authority exclusive to manifest v3", () => {
|
|
103
|
+
expect(() =>
|
|
104
|
+
decodeFrockBotManifest({
|
|
105
|
+
schemaVersion: 1,
|
|
106
|
+
id: "legacy-desktop",
|
|
107
|
+
displayName: "Legacy Desktop",
|
|
108
|
+
version: "1.0.0",
|
|
109
|
+
contributions: { desktop: "./desktop" },
|
|
110
|
+
permissions: [],
|
|
111
|
+
}),
|
|
112
|
+
).toThrow("manifest v1 desktop Contributions are unsupported");
|
|
113
|
+
expect(() =>
|
|
114
|
+
decodeFrockBotManifest({
|
|
115
|
+
schemaVersion: 2,
|
|
116
|
+
id: "v2-desktop",
|
|
117
|
+
displayName: "V2 Desktop",
|
|
118
|
+
version: "1.0.0",
|
|
119
|
+
compatibility: { frockbot: "*" },
|
|
120
|
+
contributions: {
|
|
121
|
+
desktop: { entry: "./desktop", execution: "trusted-main" },
|
|
122
|
+
},
|
|
123
|
+
permissions: [],
|
|
124
|
+
}),
|
|
125
|
+
).toThrow('manifest v2 desktop execution must be "sandboxed-renderer"');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("keeps backend Contributions unavailable to v2 manifests", () => {
|
|
129
|
+
expect(() =>
|
|
130
|
+
decodeFrockBotManifest({
|
|
131
|
+
schemaVersion: 2,
|
|
132
|
+
id: "legacy",
|
|
133
|
+
displayName: "Legacy",
|
|
134
|
+
version: "1.0.0",
|
|
135
|
+
compatibility: { frockbot: "*" },
|
|
136
|
+
contributions: {
|
|
137
|
+
backend: { entry: "./backend", host: "gateway" },
|
|
138
|
+
},
|
|
139
|
+
permissions: [],
|
|
140
|
+
}),
|
|
141
|
+
).toThrow('manifest contributions has unknown field "backend"');
|
|
142
|
+
expect(
|
|
143
|
+
decodeFrockBotManifest({
|
|
144
|
+
schemaVersion: 3,
|
|
145
|
+
id: "current",
|
|
146
|
+
displayName: "Current",
|
|
147
|
+
version: "1.0.0",
|
|
148
|
+
compatibility: { frockbot: "*" },
|
|
149
|
+
contributions: {
|
|
150
|
+
backend: { entry: "./backend", host: "bot" },
|
|
151
|
+
},
|
|
152
|
+
permissions: [],
|
|
153
|
+
}).contributions.backend,
|
|
154
|
+
).toEqual([{ entry: "./backend", host: "bot" }]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("decodes the reference package manifest", async () => {
|
|
158
|
+
const root = await createCatalog();
|
|
159
|
+
const value = await Bun.file(
|
|
160
|
+
new URL("../../plugin-clock/frockbot.json", import.meta.url),
|
|
161
|
+
).json();
|
|
162
|
+
const installed = root.packages.install({
|
|
163
|
+
specifier: "@frockbot/plugin-clock",
|
|
164
|
+
manifest: value,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(installed.manifest).toMatchObject({
|
|
168
|
+
schemaVersion: 3,
|
|
169
|
+
id: "clock",
|
|
170
|
+
contributions: {
|
|
171
|
+
runtime: { entry: "./agent" },
|
|
172
|
+
},
|
|
173
|
+
permissions: ["time:read"],
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("commits and disables contributions in dependency-safe order", async () => {
|
|
178
|
+
const root = await createCatalog();
|
|
179
|
+
const log: string[] = [];
|
|
180
|
+
root.packages.registerHost(new FakeHost("runtime", log));
|
|
181
|
+
root.packages.registerHost(new FakeHost("client", log));
|
|
182
|
+
root.packages.registerHost(new FakeHost("desktop", log));
|
|
183
|
+
root.packages.install({ specifier: "fixture", manifest: manifest() });
|
|
184
|
+
|
|
185
|
+
await root.packages.enable("fixture");
|
|
186
|
+
expect(root.packages.get("fixture")?.status).toBe("active");
|
|
187
|
+
await root.packages.disable("fixture");
|
|
188
|
+
|
|
189
|
+
expect(log).toEqual([
|
|
190
|
+
"prepare:runtime:fixture",
|
|
191
|
+
"prepare:client:fixture",
|
|
192
|
+
"prepare:desktop:fixture",
|
|
193
|
+
"commit:runtime",
|
|
194
|
+
"commit:client",
|
|
195
|
+
"commit:desktop",
|
|
196
|
+
"dispose:desktop",
|
|
197
|
+
"dispose:client",
|
|
198
|
+
"dispose:runtime",
|
|
199
|
+
]);
|
|
200
|
+
expect(root.packages.get("fixture")?.status).toBe("installed");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("rolls back prepared and committed contributions after failure", async () => {
|
|
204
|
+
const root = await createCatalog();
|
|
205
|
+
const log: string[] = [];
|
|
206
|
+
root.packages.registerHost(new FakeHost("runtime", log));
|
|
207
|
+
root.packages.registerHost(new FakeHost("client", log));
|
|
208
|
+
root.packages.registerHost(new FakeHost("desktop", log, true));
|
|
209
|
+
root.packages.install({ specifier: "fixture", manifest: manifest() });
|
|
210
|
+
|
|
211
|
+
let failure: unknown;
|
|
212
|
+
try {
|
|
213
|
+
await root.packages.enable("fixture");
|
|
214
|
+
} catch (error) {
|
|
215
|
+
failure = error;
|
|
216
|
+
}
|
|
217
|
+
expect(failure).toBeInstanceOf(Error);
|
|
218
|
+
expect(failure instanceof Error ? failure.message : "").toContain(
|
|
219
|
+
"desktop commit failed",
|
|
220
|
+
);
|
|
221
|
+
expect(log.slice(-3)).toEqual([
|
|
222
|
+
"dispose:client",
|
|
223
|
+
"dispose:runtime",
|
|
224
|
+
"rollback:desktop",
|
|
225
|
+
]);
|
|
226
|
+
expect(root.packages.get("fixture")).toMatchObject({
|
|
227
|
+
status: "failed",
|
|
228
|
+
error: "desktop commit failed",
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("mounts a local mobile contribution behind the host interface", async () => {
|
|
233
|
+
const root = await createCatalog();
|
|
234
|
+
let setups = 0;
|
|
235
|
+
let cleanups = 0;
|
|
236
|
+
const plugin = () => {
|
|
237
|
+
setups += 1;
|
|
238
|
+
return () => {
|
|
239
|
+
cleanups += 1;
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
root.packages.registerHost(
|
|
243
|
+
new LocalCordisContributionHost("mobile", root, () =>
|
|
244
|
+
Promise.resolve({ default: plugin }),
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
root.packages.install({
|
|
248
|
+
specifier: "fixture",
|
|
249
|
+
manifest: {
|
|
250
|
+
schemaVersion: 1,
|
|
251
|
+
id: "local-mobile",
|
|
252
|
+
displayName: "Local Mobile",
|
|
253
|
+
version: "1.0.0",
|
|
254
|
+
contributions: { mobile: "./mobile" },
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await root.packages.enable("local-mobile");
|
|
259
|
+
expect(setups).toBe(1);
|
|
260
|
+
await root.packages.disable("local-mobile");
|
|
261
|
+
expect(cleanups).toBe(1);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("mounts a local Cordis contribution behind the host interface", async () => {
|
|
265
|
+
const root = await createCatalog();
|
|
266
|
+
let setups = 0;
|
|
267
|
+
let cleanups = 0;
|
|
268
|
+
const plugin = () => {
|
|
269
|
+
setups += 1;
|
|
270
|
+
return () => {
|
|
271
|
+
cleanups += 1;
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
root.packages.registerHost(
|
|
275
|
+
new LocalCordisContributionHost("runtime", root, () =>
|
|
276
|
+
Promise.resolve({ default: plugin }),
|
|
277
|
+
),
|
|
278
|
+
);
|
|
279
|
+
root.packages.install({
|
|
280
|
+
specifier: "fixture",
|
|
281
|
+
manifest: {
|
|
282
|
+
schemaVersion: 1,
|
|
283
|
+
id: "local",
|
|
284
|
+
displayName: "Local",
|
|
285
|
+
version: "1.0.0",
|
|
286
|
+
contributions: { agent: "./agent" },
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
await root.packages.enable("local");
|
|
291
|
+
expect(setups).toBe(1);
|
|
292
|
+
await root.packages.disable("local");
|
|
293
|
+
expect(cleanups).toBe(1);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe("decodeFrockBotManifest", () => {
|
|
298
|
+
test("keeps trusted Electron main execution exclusive to manifest v3", () => {
|
|
299
|
+
const contribution = {
|
|
300
|
+
desktop: {
|
|
301
|
+
entry: "./desktop",
|
|
302
|
+
execution: "trusted-main",
|
|
303
|
+
commands: [],
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
expect(() =>
|
|
307
|
+
decodeFrockBotManifest({
|
|
308
|
+
schemaVersion: 2,
|
|
309
|
+
id: "desktop-v2",
|
|
310
|
+
displayName: "Desktop v2",
|
|
311
|
+
version: "1.0.0",
|
|
312
|
+
compatibility: { frockbot: "*" },
|
|
313
|
+
contributions: contribution,
|
|
314
|
+
permissions: [],
|
|
315
|
+
}),
|
|
316
|
+
).toThrow('manifest v2 desktop execution must be "sandboxed-renderer"');
|
|
317
|
+
expect(
|
|
318
|
+
decodeFrockBotManifest({
|
|
319
|
+
schemaVersion: 3,
|
|
320
|
+
id: "desktop-v3",
|
|
321
|
+
displayName: "Desktop v3",
|
|
322
|
+
version: "1.0.0",
|
|
323
|
+
compatibility: { frockbot: "*" },
|
|
324
|
+
contributions: contribution,
|
|
325
|
+
permissions: [],
|
|
326
|
+
}).contributions.desktop,
|
|
327
|
+
).toEqual({
|
|
328
|
+
entry: "./desktop",
|
|
329
|
+
execution: "trusted-main",
|
|
330
|
+
commands: [],
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("decodes an explicitly hosted backend Contribution", () => {
|
|
335
|
+
const decoded = decodeFrockBotManifest({
|
|
336
|
+
schemaVersion: 3,
|
|
337
|
+
id: "connection-driver",
|
|
338
|
+
displayName: "Connection driver",
|
|
339
|
+
version: "1.0.0",
|
|
340
|
+
compatibility: { frockbot: ">=0.0.1" },
|
|
341
|
+
contributions: {
|
|
342
|
+
backend: { entry: "./backend", host: "gateway" },
|
|
343
|
+
},
|
|
344
|
+
permissions: [],
|
|
345
|
+
configuration: {},
|
|
346
|
+
});
|
|
347
|
+
expect(decoded.contributions.backend).toEqual([
|
|
348
|
+
{ entry: "./backend", host: "gateway" },
|
|
349
|
+
]);
|
|
350
|
+
expect(declaredContributionKinds(decoded)).toEqual(["backend"]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("accepts a manifest that only contributes to mobile", () => {
|
|
354
|
+
const decoded = decodeFrockBotManifest({
|
|
355
|
+
schemaVersion: 1,
|
|
356
|
+
id: "mobile-only",
|
|
357
|
+
displayName: "Mobile Only",
|
|
358
|
+
version: "1.0.0",
|
|
359
|
+
contributions: { mobile: "./mobile" },
|
|
360
|
+
permissions: ["mobile:notifications"],
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
expect(decoded.contributions).toEqual({
|
|
364
|
+
runtime: undefined,
|
|
365
|
+
client: undefined,
|
|
366
|
+
desktop: undefined,
|
|
367
|
+
mobile: { entry: "./mobile" },
|
|
368
|
+
});
|
|
369
|
+
expect(declaredContributionKinds(decoded)).toEqual(["mobile"]);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("rejects a mobile contribution that is not a relative export path", () => {
|
|
373
|
+
expect(() =>
|
|
374
|
+
decodeFrockBotManifest({
|
|
375
|
+
schemaVersion: 1,
|
|
376
|
+
id: "mobile-only",
|
|
377
|
+
displayName: "Mobile Only",
|
|
378
|
+
version: "1.0.0",
|
|
379
|
+
contributions: { mobile: "mobile" },
|
|
380
|
+
}),
|
|
381
|
+
).toThrow('manifest contribution "mobile" must be a relative export path');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("decodes manifest v3 settings, Connection Types, and capabilities", () => {
|
|
385
|
+
const decoded = decodeFrockBotManifest({
|
|
386
|
+
schemaVersion: 3,
|
|
387
|
+
id: "composio",
|
|
388
|
+
displayName: "Composio",
|
|
389
|
+
version: "1.0.0",
|
|
390
|
+
compatibility: { frockbot: ">=0.0.1" },
|
|
391
|
+
contributions: { runtime: { entry: "./runtime" } },
|
|
392
|
+
permissions: ["connections:manage"],
|
|
393
|
+
configuration: {
|
|
394
|
+
settings: [
|
|
395
|
+
{
|
|
396
|
+
id: "preferences",
|
|
397
|
+
schemaVersion: 1,
|
|
398
|
+
scopes: ["user"],
|
|
399
|
+
schema: { type: "object", properties: {} },
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
connectionTypes: [
|
|
403
|
+
{
|
|
404
|
+
id: "gmail",
|
|
405
|
+
displayName: "Gmail",
|
|
406
|
+
allowMultiple: true,
|
|
407
|
+
authorization: { kind: "grant", driverId: "composio" },
|
|
408
|
+
capabilities: ["gmail-tools"],
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
capabilities: [
|
|
412
|
+
{
|
|
413
|
+
id: "gmail-tools",
|
|
414
|
+
kind: "tool",
|
|
415
|
+
connectionTypes: ["gmail"],
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
expect(decoded).toMatchObject({
|
|
422
|
+
schemaVersion: 3,
|
|
423
|
+
configuration: {
|
|
424
|
+
connectionTypes: [{ id: "gmail", authorization: { kind: "grant" } }],
|
|
425
|
+
capabilities: [{ id: "gmail-tools", kind: "tool" }],
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("decodes an ambient-native Connection without an authorization driver", () => {
|
|
431
|
+
const decoded = decodeFrockBotManifest({
|
|
432
|
+
schemaVersion: 4,
|
|
433
|
+
id: "workers-ai",
|
|
434
|
+
displayName: "Workers AI",
|
|
435
|
+
version: "1.0.0",
|
|
436
|
+
compatibility: { frockbot: ">=0.0.1" },
|
|
437
|
+
contributions: { runtime: { entry: "./runtime" } },
|
|
438
|
+
permissions: ["models:invoke"],
|
|
439
|
+
configuration: {
|
|
440
|
+
connectionTypes: [
|
|
441
|
+
{
|
|
442
|
+
id: "workers-ai-account",
|
|
443
|
+
displayName: "Workers AI",
|
|
444
|
+
allowMultiple: false,
|
|
445
|
+
authorization: { kind: "ambient-native" },
|
|
446
|
+
capabilities: ["workers-ai-models"],
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
capabilities: [
|
|
450
|
+
{
|
|
451
|
+
id: "workers-ai-models",
|
|
452
|
+
kind: "model",
|
|
453
|
+
connectionTypes: ["workers-ai-account"],
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
expect(decoded.configuration?.connectionTypes[0]?.authorization).toEqual({
|
|
460
|
+
kind: "ambient-native",
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("decodes a manifest v4 Capability admission ceiling", () => {
|
|
465
|
+
const decoded = decodeFrockBotManifest({
|
|
466
|
+
schemaVersion: 4,
|
|
467
|
+
id: "shell",
|
|
468
|
+
displayName: "Shell",
|
|
469
|
+
version: "1.0.0",
|
|
470
|
+
compatibility: { frockbot: ">=0.0.1" },
|
|
471
|
+
contributions: { runtime: { entry: "./runtime" } },
|
|
472
|
+
permissions: [],
|
|
473
|
+
configuration: {
|
|
474
|
+
capabilities: [
|
|
475
|
+
{
|
|
476
|
+
id: "user-voice",
|
|
477
|
+
kind: "tool",
|
|
478
|
+
connectionTypes: [],
|
|
479
|
+
admission: { turnTypes: ["chat"] },
|
|
480
|
+
},
|
|
481
|
+
{ id: "work", kind: "tool", connectionTypes: [] },
|
|
482
|
+
],
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
expect(decoded).toMatchObject({
|
|
487
|
+
schemaVersion: 4,
|
|
488
|
+
configuration: {
|
|
489
|
+
capabilities: [
|
|
490
|
+
{ id: "user-voice", admission: { turnTypes: ["chat"] } },
|
|
491
|
+
{ id: "work" },
|
|
492
|
+
],
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
expect(decoded.configuration?.capabilities[1]?.admission).toBeUndefined();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("rejects an unknown turn type in a v4 admission ceiling", () => {
|
|
499
|
+
const manifestWith = (admission: unknown) => ({
|
|
500
|
+
schemaVersion: 4,
|
|
501
|
+
id: "shell",
|
|
502
|
+
displayName: "Shell",
|
|
503
|
+
version: "1.0.0",
|
|
504
|
+
compatibility: { frockbot: ">=0.0.1" },
|
|
505
|
+
contributions: { runtime: { entry: "./runtime" } },
|
|
506
|
+
permissions: [],
|
|
507
|
+
configuration: {
|
|
508
|
+
capabilities: [
|
|
509
|
+
{ id: "user-voice", kind: "tool", connectionTypes: [], admission },
|
|
510
|
+
],
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
expect(() =>
|
|
514
|
+
decodeFrockBotManifest(manifestWith({ turnTypes: ["routine"] })),
|
|
515
|
+
).toThrow(/turn type is invalid/);
|
|
516
|
+
expect(() =>
|
|
517
|
+
decodeFrockBotManifest(manifestWith({ turnTypes: [] })),
|
|
518
|
+
).toThrow(/admission turnTypes must not be empty/);
|
|
519
|
+
expect(() =>
|
|
520
|
+
decodeFrockBotManifest(manifestWith({ turnTypes: ["chat"], extra: 1 })),
|
|
521
|
+
).toThrow(/unknown field/);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("keeps the admission ceiling exclusive to manifest v4", () => {
|
|
525
|
+
expect(() =>
|
|
526
|
+
decodeFrockBotManifest({
|
|
527
|
+
schemaVersion: 3,
|
|
528
|
+
id: "shell",
|
|
529
|
+
displayName: "Shell",
|
|
530
|
+
version: "1.0.0",
|
|
531
|
+
compatibility: { frockbot: ">=0.0.1" },
|
|
532
|
+
contributions: { runtime: { entry: "./runtime" } },
|
|
533
|
+
permissions: [],
|
|
534
|
+
configuration: {
|
|
535
|
+
capabilities: [
|
|
536
|
+
{
|
|
537
|
+
id: "user-voice",
|
|
538
|
+
kind: "tool",
|
|
539
|
+
connectionTypes: [],
|
|
540
|
+
admission: { turnTypes: ["chat"] },
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
},
|
|
544
|
+
}),
|
|
545
|
+
).toThrow(/unknown field "admission"/);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("decodes a v4 manifest exactly as v3 apart from the admission field", () => {
|
|
549
|
+
const body = {
|
|
550
|
+
id: "composio",
|
|
551
|
+
displayName: "Composio",
|
|
552
|
+
version: "1.0.0",
|
|
553
|
+
compatibility: { frockbot: ">=0.0.1" },
|
|
554
|
+
contributions: {
|
|
555
|
+
backend: { entry: "./backend", host: "gateway" },
|
|
556
|
+
desktop: {
|
|
557
|
+
entry: "./desktop",
|
|
558
|
+
execution: "trusted-main",
|
|
559
|
+
commands: [],
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
permissions: ["connections:manage"],
|
|
563
|
+
configuration: {
|
|
564
|
+
capabilities: [{ id: "gmail-tools", kind: "tool" }],
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
const v3 = decodeFrockBotManifest({ ...body, schemaVersion: 3 });
|
|
568
|
+
const v4 = decodeFrockBotManifest({ ...body, schemaVersion: 4 });
|
|
569
|
+
expect({ ...v4, schemaVersion: 3 }).toEqual(v3);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("v4 still decodes exactly as it did", () => {
|
|
573
|
+
expect(
|
|
574
|
+
decodeFrockBotManifest({
|
|
575
|
+
schemaVersion: 4,
|
|
576
|
+
id: "routines",
|
|
577
|
+
displayName: "Routines",
|
|
578
|
+
version: "0.0.1",
|
|
579
|
+
compatibility: { frockbot: "*" },
|
|
580
|
+
dependencies: {},
|
|
581
|
+
contributions: { runtime: { entry: "./agent" } },
|
|
582
|
+
permissions: [],
|
|
583
|
+
configuration: {
|
|
584
|
+
settings: [],
|
|
585
|
+
connectionTypes: [],
|
|
586
|
+
capabilities: [
|
|
587
|
+
{
|
|
588
|
+
id: "routine-tools",
|
|
589
|
+
kind: "tool",
|
|
590
|
+
connectionTypes: [],
|
|
591
|
+
admission: { turnTypes: ["chat", "subagent"] },
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
},
|
|
595
|
+
}),
|
|
596
|
+
).toMatchObject({ schemaVersion: 4 });
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("rejects an unsupported manifest version", () => {
|
|
600
|
+
expect(() =>
|
|
601
|
+
decodeFrockBotManifest({
|
|
602
|
+
schemaVersion: 5,
|
|
603
|
+
id: "future",
|
|
604
|
+
displayName: "Future",
|
|
605
|
+
version: "1.0.0",
|
|
606
|
+
compatibility: { frockbot: "*" },
|
|
607
|
+
contributions: { runtime: { entry: "./runtime" } },
|
|
608
|
+
permissions: [],
|
|
609
|
+
}),
|
|
610
|
+
).toThrow(/unsupported FrockBot manifest version/);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test("rejects unknown fields at every manifest object boundary", () => {
|
|
614
|
+
const cases: Array<[string, unknown]> = [
|
|
615
|
+
["", { ...manifest(), unexpected: true }],
|
|
616
|
+
[
|
|
617
|
+
"compatibility",
|
|
618
|
+
{ ...manifest(), compatibility: { frockbot: "*", unexpected: true } },
|
|
619
|
+
],
|
|
620
|
+
[
|
|
621
|
+
"contributions",
|
|
622
|
+
{
|
|
623
|
+
...manifest(),
|
|
624
|
+
contributions: {
|
|
625
|
+
...manifest().contributions,
|
|
626
|
+
unexpected: { entry: "./unexpected" },
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
[
|
|
631
|
+
"runtime contribution",
|
|
632
|
+
{
|
|
633
|
+
...manifest(),
|
|
634
|
+
contributions: {
|
|
635
|
+
runtime: { entry: "./agent", unexpected: true },
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
],
|
|
639
|
+
[
|
|
640
|
+
"backend contribution",
|
|
641
|
+
{
|
|
642
|
+
...manifest(),
|
|
643
|
+
contributions: {
|
|
644
|
+
backend: {
|
|
645
|
+
entry: "./backend",
|
|
646
|
+
host: "bot",
|
|
647
|
+
unexpected: true,
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
[
|
|
653
|
+
"client contribution",
|
|
654
|
+
{
|
|
655
|
+
...manifest(),
|
|
656
|
+
contributions: {
|
|
657
|
+
client: { entry: "./client", mounts: [], unexpected: true },
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
],
|
|
661
|
+
[
|
|
662
|
+
"client mount",
|
|
663
|
+
{
|
|
664
|
+
...manifest(),
|
|
665
|
+
contributions: {
|
|
666
|
+
client: {
|
|
667
|
+
entry: "./client",
|
|
668
|
+
mounts: [{ slot: "root", unexpected: true }],
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
[
|
|
674
|
+
"desktop contribution",
|
|
675
|
+
{
|
|
676
|
+
...manifest(),
|
|
677
|
+
contributions: {
|
|
678
|
+
desktop: {
|
|
679
|
+
entry: "./desktop",
|
|
680
|
+
execution: "trusted-main",
|
|
681
|
+
unexpected: true,
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
],
|
|
686
|
+
[
|
|
687
|
+
"mobile contribution",
|
|
688
|
+
{
|
|
689
|
+
...manifest(),
|
|
690
|
+
contributions: {
|
|
691
|
+
mobile: { entry: "./mobile", unexpected: true },
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
],
|
|
695
|
+
[
|
|
696
|
+
"configuration",
|
|
697
|
+
{
|
|
698
|
+
...manifest(),
|
|
699
|
+
configuration: { unexpected: true },
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
[
|
|
703
|
+
"setting definition",
|
|
704
|
+
{
|
|
705
|
+
...v3ManifestWithSchema({ type: "string" }),
|
|
706
|
+
configuration: {
|
|
707
|
+
settings: [
|
|
708
|
+
{
|
|
709
|
+
id: "preferences",
|
|
710
|
+
schemaVersion: 1,
|
|
711
|
+
scopes: ["user"],
|
|
712
|
+
schema: { type: "string" },
|
|
713
|
+
unexpected: true,
|
|
714
|
+
},
|
|
715
|
+
],
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
],
|
|
719
|
+
[
|
|
720
|
+
"connection definition",
|
|
721
|
+
{
|
|
722
|
+
...manifest(),
|
|
723
|
+
configuration: {
|
|
724
|
+
connectionTypes: [
|
|
725
|
+
{
|
|
726
|
+
id: "mail",
|
|
727
|
+
displayName: "Mail",
|
|
728
|
+
allowMultiple: false,
|
|
729
|
+
authorization: { kind: "grant", driverId: "driver" },
|
|
730
|
+
unexpected: true,
|
|
731
|
+
},
|
|
732
|
+
],
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
],
|
|
736
|
+
[
|
|
737
|
+
"connection authorization",
|
|
738
|
+
{
|
|
739
|
+
...manifest(),
|
|
740
|
+
configuration: {
|
|
741
|
+
connectionTypes: [
|
|
742
|
+
{
|
|
743
|
+
id: "mail",
|
|
744
|
+
displayName: "Mail",
|
|
745
|
+
allowMultiple: false,
|
|
746
|
+
authorization: {
|
|
747
|
+
kind: "grant",
|
|
748
|
+
driverId: "driver",
|
|
749
|
+
unexpected: true,
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
],
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
[
|
|
757
|
+
"capability definition",
|
|
758
|
+
{
|
|
759
|
+
...manifest(),
|
|
760
|
+
configuration: {
|
|
761
|
+
capabilities: [
|
|
762
|
+
{ id: "mail-tools", kind: "tool", unexpected: true },
|
|
763
|
+
],
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
],
|
|
767
|
+
[
|
|
768
|
+
"legacy web contribution",
|
|
769
|
+
{
|
|
770
|
+
schemaVersion: 1,
|
|
771
|
+
id: "legacy",
|
|
772
|
+
displayName: "Legacy",
|
|
773
|
+
version: "1.0.0",
|
|
774
|
+
contributions: {
|
|
775
|
+
web: { entry: "./web", slots: [], unexpected: true },
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
],
|
|
779
|
+
];
|
|
780
|
+
|
|
781
|
+
for (const [boundary, candidate] of cases) {
|
|
782
|
+
expect(() => decodeFrockBotManifest(candidate)).toThrow(
|
|
783
|
+
`${boundary ? `manifest ${boundary}` : "manifest"} has unknown field "unexpected"`,
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
test("rejects non-enumerable and symbol fields", () => {
|
|
789
|
+
const nonEnumerableCompatibility = { frockbot: "*" };
|
|
790
|
+
Object.defineProperty(nonEnumerableCompatibility, "hidden", {
|
|
791
|
+
value: true,
|
|
792
|
+
});
|
|
793
|
+
const symbolCompatibility = { frockbot: "*" };
|
|
794
|
+
Object.defineProperty(symbolCompatibility, Symbol("unexpected"), {
|
|
795
|
+
value: true,
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
for (const [field, compatibility] of [
|
|
799
|
+
["hidden", nonEnumerableCompatibility],
|
|
800
|
+
["Symbol(unexpected)", symbolCompatibility],
|
|
801
|
+
] as const) {
|
|
802
|
+
expect(() =>
|
|
803
|
+
decodeFrockBotManifest({ ...manifest(), compatibility }),
|
|
804
|
+
).toThrow(`manifest compatibility has unknown field "${field}"`);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
test("recursively decodes the supported manifest v3 schema subset", () => {
|
|
809
|
+
const schema = {
|
|
810
|
+
type: "object",
|
|
811
|
+
title: "Preferences",
|
|
812
|
+
description: "Bounded provider preferences",
|
|
813
|
+
properties: {
|
|
814
|
+
endpoint: {
|
|
815
|
+
type: "string",
|
|
816
|
+
minLength: 1,
|
|
817
|
+
maxLength: 200,
|
|
818
|
+
enum: ["primary", "secondary"],
|
|
819
|
+
},
|
|
820
|
+
retries: {
|
|
821
|
+
type: "integer",
|
|
822
|
+
minimum: 0,
|
|
823
|
+
maximum: 5,
|
|
824
|
+
multipleOf: 1,
|
|
825
|
+
},
|
|
826
|
+
flags: {
|
|
827
|
+
type: "array",
|
|
828
|
+
items: { type: "boolean", const: true },
|
|
829
|
+
minItems: 0,
|
|
830
|
+
maxItems: 3,
|
|
831
|
+
uniqueItems: true,
|
|
832
|
+
},
|
|
833
|
+
},
|
|
834
|
+
required: ["endpoint"],
|
|
835
|
+
additionalProperties: false,
|
|
836
|
+
minProperties: 1,
|
|
837
|
+
maxProperties: 3,
|
|
838
|
+
} satisfies PackageSettingSchema;
|
|
839
|
+
|
|
840
|
+
const decoded = decodeFrockBotManifest(v3ManifestWithSchema(schema));
|
|
841
|
+
|
|
842
|
+
expect(decoded.configuration?.settings[0]?.schema).toEqual(schema);
|
|
843
|
+
expect(decoded.configuration?.settings[0]?.schema).not.toBe(schema);
|
|
844
|
+
expect(
|
|
845
|
+
decoded.configuration?.settings[0]?.schema.properties?.flags,
|
|
846
|
+
).not.toBe(schema.properties.flags);
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
test("rejects references, defaults, formats, and unknown schema keywords", () => {
|
|
850
|
+
const forbidden = [
|
|
851
|
+
"$schema",
|
|
852
|
+
"$id",
|
|
853
|
+
"$anchor",
|
|
854
|
+
"$dynamicAnchor",
|
|
855
|
+
"$ref",
|
|
856
|
+
"$dynamicRef",
|
|
857
|
+
"$defs",
|
|
858
|
+
"definitions",
|
|
859
|
+
"default",
|
|
860
|
+
"format",
|
|
861
|
+
"pattern",
|
|
862
|
+
"contentEncoding",
|
|
863
|
+
"contentMediaType",
|
|
864
|
+
"contentSchema",
|
|
865
|
+
"examples",
|
|
866
|
+
"deprecated",
|
|
867
|
+
"readOnly",
|
|
868
|
+
"writeOnly",
|
|
869
|
+
"allOf",
|
|
870
|
+
"anyOf",
|
|
871
|
+
"oneOf",
|
|
872
|
+
"not",
|
|
873
|
+
"if",
|
|
874
|
+
"then",
|
|
875
|
+
"else",
|
|
876
|
+
"prefixItems",
|
|
877
|
+
"contains",
|
|
878
|
+
"patternProperties",
|
|
879
|
+
"propertyNames",
|
|
880
|
+
"dependentRequired",
|
|
881
|
+
"dependentSchemas",
|
|
882
|
+
"unevaluatedProperties",
|
|
883
|
+
"minContains",
|
|
884
|
+
"maxContains",
|
|
885
|
+
"unevaluatedItems",
|
|
886
|
+
"unknownKeyword",
|
|
887
|
+
];
|
|
888
|
+
|
|
889
|
+
for (const keyword of forbidden) {
|
|
890
|
+
expect(() =>
|
|
891
|
+
decodeFrockBotManifest(
|
|
892
|
+
v3ManifestWithSchema({ type: "string", [keyword]: "forbidden" }),
|
|
893
|
+
),
|
|
894
|
+
).toThrow(`manifest setting schema "${keyword}" is not supported`);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
expect(() =>
|
|
898
|
+
decodeFrockBotManifest(
|
|
899
|
+
v3ManifestWithSchema({
|
|
900
|
+
type: "object",
|
|
901
|
+
properties: {
|
|
902
|
+
nested: { type: "string", default: "secret" },
|
|
903
|
+
},
|
|
904
|
+
}),
|
|
905
|
+
),
|
|
906
|
+
).toThrow('manifest setting schema "default" is not supported');
|
|
907
|
+
expect(() =>
|
|
908
|
+
decodeFrockBotManifest(
|
|
909
|
+
v3ManifestWithSchema({
|
|
910
|
+
type: "array",
|
|
911
|
+
items: { type: "string", format: "password" },
|
|
912
|
+
}),
|
|
913
|
+
),
|
|
914
|
+
).toThrow('manifest setting schema "format" is not supported');
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
test("rejects malformed supported schema keyword values", () => {
|
|
918
|
+
const malformed = [
|
|
919
|
+
null,
|
|
920
|
+
[],
|
|
921
|
+
"schema",
|
|
922
|
+
{ type: ["string"] },
|
|
923
|
+
{ type: "string", title: 1 },
|
|
924
|
+
{ type: "string", description: false },
|
|
925
|
+
{ type: "string", enum: [] },
|
|
926
|
+
{ type: "string", enum: ["duplicate", "duplicate"] },
|
|
927
|
+
{ type: "string", enum: [1] },
|
|
928
|
+
{ type: "string", const: {} },
|
|
929
|
+
{ type: "string", const: 1 },
|
|
930
|
+
{ type: "object", properties: [] },
|
|
931
|
+
{ type: "object", properties: { nested: "invalid" } },
|
|
932
|
+
{ type: "object", properties: { "": { type: "string" } } },
|
|
933
|
+
{ type: "object", properties: {}, required: "name" },
|
|
934
|
+
{
|
|
935
|
+
type: "object",
|
|
936
|
+
properties: { name: { type: "string" } },
|
|
937
|
+
required: ["name", "name"],
|
|
938
|
+
},
|
|
939
|
+
{ type: "object", properties: {}, required: ["missing"] },
|
|
940
|
+
{ type: "object", additionalProperties: {} },
|
|
941
|
+
{ type: "array", items: [] },
|
|
942
|
+
{ type: "string", minLength: -1 },
|
|
943
|
+
{ type: "string", maxLength: 1.5 },
|
|
944
|
+
{ type: "number", minimum: Number.NaN },
|
|
945
|
+
{ type: "number", maximum: "five" },
|
|
946
|
+
{ type: "number", multipleOf: 0 },
|
|
947
|
+
{ type: "array", minItems: -1 },
|
|
948
|
+
{ type: "array", maxItems: 1.5 },
|
|
949
|
+
{ type: "array", uniqueItems: "yes" },
|
|
950
|
+
{ type: "object", minProperties: -1 },
|
|
951
|
+
{ type: "object", maxProperties: 1.5 },
|
|
952
|
+
{ type: "string", minLength: 2, maxLength: 1 },
|
|
953
|
+
{ type: "array", minItems: 2, maxItems: 1 },
|
|
954
|
+
{ type: "object", minProperties: 2, maxProperties: 1 },
|
|
955
|
+
{ type: "number", minimum: 2, maximum: 1 },
|
|
956
|
+
{ type: "string", minimum: 0 },
|
|
957
|
+
{ type: "array", minLength: 0 },
|
|
958
|
+
{ type: "object", minItems: 0 },
|
|
959
|
+
{ type: "boolean", properties: {} },
|
|
960
|
+
];
|
|
961
|
+
|
|
962
|
+
for (const schema of malformed) {
|
|
963
|
+
expect(() =>
|
|
964
|
+
decodeFrockBotManifest(v3ManifestWithSchema(schema)),
|
|
965
|
+
).toThrow();
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
test("rejects non-JSON and structurally ambiguous schema values", () => {
|
|
970
|
+
const sparseEnum = new Array(1);
|
|
971
|
+
const sparseRequired = new Array(1);
|
|
972
|
+
const arrayWithExtraEntry = ["value"] as unknown[] & { extra?: string };
|
|
973
|
+
arrayWithExtraEntry.extra = "hidden";
|
|
974
|
+
const arrayWithOutOfRangeEntry = ["value"] as unknown[] & {
|
|
975
|
+
[key: string]: unknown;
|
|
976
|
+
};
|
|
977
|
+
Object.defineProperty(arrayWithOutOfRangeEntry, "4294967295", {
|
|
978
|
+
enumerable: true,
|
|
979
|
+
value: undefined,
|
|
980
|
+
});
|
|
981
|
+
const inherited = Object.assign(Object.create({ default: "secret" }), {
|
|
982
|
+
type: "string",
|
|
983
|
+
});
|
|
984
|
+
const inheritedProperties = Object.assign(
|
|
985
|
+
Object.create({ hidden: { type: "string" } }),
|
|
986
|
+
{ visible: { type: "string" } },
|
|
987
|
+
);
|
|
988
|
+
const symbolKey = { type: "string" } as Record<PropertyKey, unknown>;
|
|
989
|
+
symbolKey[Symbol("hidden")] = "value";
|
|
990
|
+
const accessor: Record<string, unknown> = {};
|
|
991
|
+
Object.defineProperty(accessor, "type", {
|
|
992
|
+
enumerable: true,
|
|
993
|
+
get: () => "string",
|
|
994
|
+
});
|
|
995
|
+
const cyclic: Record<string, unknown> = { type: "array" };
|
|
996
|
+
cyclic.items = cyclic;
|
|
997
|
+
|
|
998
|
+
const invalidSchemas = [
|
|
999
|
+
{ type: undefined },
|
|
1000
|
+
{ type: "string", title: undefined },
|
|
1001
|
+
{ type: "string", description: Symbol("description") },
|
|
1002
|
+
{ type: () => "string" },
|
|
1003
|
+
{ type: "string", enum: sparseEnum },
|
|
1004
|
+
{ type: "string", enum: [undefined] },
|
|
1005
|
+
{ type: "number", enum: [Number.POSITIVE_INFINITY] },
|
|
1006
|
+
{ type: "integer", enum: [1n] },
|
|
1007
|
+
{ type: "object", properties: { value: undefined } },
|
|
1008
|
+
{ type: "object", properties: inheritedProperties },
|
|
1009
|
+
{ type: "object", properties: {}, required: sparseRequired },
|
|
1010
|
+
{ type: "object", properties: {}, required: [undefined] },
|
|
1011
|
+
{ type: "array", items: undefined },
|
|
1012
|
+
{ type: "string", enum: arrayWithExtraEntry },
|
|
1013
|
+
{ type: "string", enum: arrayWithOutOfRangeEntry },
|
|
1014
|
+
inherited,
|
|
1015
|
+
symbolKey,
|
|
1016
|
+
accessor,
|
|
1017
|
+
cyclic,
|
|
1018
|
+
new Date(),
|
|
1019
|
+
];
|
|
1020
|
+
|
|
1021
|
+
for (const schema of invalidSchemas) {
|
|
1022
|
+
expect(() =>
|
|
1023
|
+
decodeFrockBotManifest(v3ManifestWithSchema(schema)),
|
|
1024
|
+
).toThrow();
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
test("rejects excessively deep and large manifest v3 schemas", () => {
|
|
1029
|
+
let deeplyNested: Record<string, unknown> = { type: "string" };
|
|
1030
|
+
for (let depth = 0; depth < 13; depth += 1) {
|
|
1031
|
+
deeplyNested = { type: "array", items: deeplyNested };
|
|
1032
|
+
}
|
|
1033
|
+
expect(() =>
|
|
1034
|
+
decodeFrockBotManifest(v3ManifestWithSchema(deeplyNested)),
|
|
1035
|
+
).toThrow("manifest setting schema is too deeply nested");
|
|
1036
|
+
expect(() =>
|
|
1037
|
+
decodeFrockBotManifest(
|
|
1038
|
+
v3ManifestWithSchema({
|
|
1039
|
+
type: "string",
|
|
1040
|
+
description: "x".repeat(50_000),
|
|
1041
|
+
}),
|
|
1042
|
+
),
|
|
1043
|
+
).toThrow("manifest setting schema is too large");
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
test("orders normalized contribution kinds", () => {
|
|
1047
|
+
const decoded = decodeFrockBotManifest({
|
|
1048
|
+
schemaVersion: 3,
|
|
1049
|
+
id: "every-kind",
|
|
1050
|
+
displayName: "Every Kind",
|
|
1051
|
+
version: "1.0.0",
|
|
1052
|
+
compatibility: { frockbot: "*" },
|
|
1053
|
+
contributions: {
|
|
1054
|
+
client: { entry: "./client.ts", mounts: [], outlets: [] },
|
|
1055
|
+
mobile: { entry: "./mobile" },
|
|
1056
|
+
desktop: {
|
|
1057
|
+
entry: "./host",
|
|
1058
|
+
execution: "trusted-main",
|
|
1059
|
+
commands: [],
|
|
1060
|
+
},
|
|
1061
|
+
runtime: { entry: "./agent" },
|
|
1062
|
+
},
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
expect(declaredContributionKinds(decoded)).toEqual([
|
|
1066
|
+
"runtime",
|
|
1067
|
+
"client",
|
|
1068
|
+
"desktop",
|
|
1069
|
+
"mobile",
|
|
1070
|
+
]);
|
|
1071
|
+
});
|
|
1072
|
+
});
|