@get-bb/plugin-sdk 0.4.4 → 0.4.8
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/README.md +8 -0
- package/bundled-types/bb-plugin-sdk-app.d.ts +213 -111
- package/bundled-types/bb-plugin-sdk-host.d.ts +124 -0
- package/bundled-types/bb-plugin-sdk-internal-composer-customization-validation.d.ts +6 -1
- package/bundled-types/bb-plugin-sdk-internal-host-policy.d.ts +154 -2
- package/bundled-types/bb-plugin-sdk-internal-plugin-app-collector.d.ts +37 -0
- package/bundled-types/bb-plugin-sdk-provider-bridge.d.ts +6479 -0
- package/bundled-types/bb-plugin-sdk-testing-app.d.ts +2 -1
- package/bundled-types/bb-plugin-sdk-testing-host.d.ts +47 -0
- package/bundled-types/bb-plugin-sdk-testing.d.ts +19 -2
- package/bundled-types/bb-plugin-sdk.d.ts +5973 -5147
- package/dist/host.js +13 -0
- package/dist/index.js +13 -1
- package/dist/internal/composer-customization-validation.js +9 -0
- package/dist/internal/host-policy.js +389 -18
- package/dist/internal/plugin-app-collector.js +519 -0
- package/dist/provider-bridge.js +7656 -0
- package/dist/testing/app.js +321 -257
- package/dist/testing/host.js +165 -0
- package/dist/testing/index.js +529 -26
- package/package.json +29 -1
package/dist/testing/index.js
CHANGED
|
@@ -5,13 +5,18 @@ import { join } from "node:path";
|
|
|
5
5
|
import Database from "better-sqlite3";
|
|
6
6
|
import { CronExpressionParser } from "cron-parser";
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
-
import { z as
|
|
8
|
+
import { z as z3 } from "zod";
|
|
9
9
|
|
|
10
10
|
// ../domain/src/plugin-interaction-limits.ts
|
|
11
11
|
var PLUGIN_INTERACTION_MAX_TITLE_LENGTH = 160;
|
|
12
12
|
|
|
13
13
|
// src/internal/host-policy.ts
|
|
14
|
-
import { z } from "zod";
|
|
14
|
+
import { z as z2 } from "zod";
|
|
15
|
+
|
|
16
|
+
// ../domain/src/plugin-icon.ts
|
|
17
|
+
function isPluginOwnedIconPath(icon) {
|
|
18
|
+
return icon.startsWith("./");
|
|
19
|
+
}
|
|
15
20
|
|
|
16
21
|
// ../domain/src/plugin-cli.ts
|
|
17
22
|
var RESERVED_BB_CLI_COMMANDS = [
|
|
@@ -28,6 +33,11 @@ var RESERVED_BB_CLI_COMMANDS = [
|
|
|
28
33
|
"thread"
|
|
29
34
|
];
|
|
30
35
|
|
|
36
|
+
// ../domain/src/provider-fork.ts
|
|
37
|
+
import { z } from "zod";
|
|
38
|
+
var PROVIDER_FORK_VALUES = ["none", "tip", "checkpoint"];
|
|
39
|
+
var providerForkSchema = z.enum(PROVIDER_FORK_VALUES);
|
|
40
|
+
|
|
31
41
|
// src/backend-contract.ts
|
|
32
42
|
var PLUGIN_CLI_OUTPUT_MAX_BYTES = 1024 * 1024;
|
|
33
43
|
|
|
@@ -55,33 +65,34 @@ var PLUGIN_AGENT_SELECTION_MAX_IDS = 256;
|
|
|
55
65
|
var PLUGIN_AGENT_DYNAMIC_INSTRUCTIONS_MAX_CHARS = 4096;
|
|
56
66
|
var PLUGIN_AGENT_TOOL_PARAMETERS_MAX_BYTES = 128 * 1024;
|
|
57
67
|
var MENTION_PROVIDER_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
68
|
+
var PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,63}$/;
|
|
58
69
|
var SETTING_KEY_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
59
70
|
var settingsBaseFields = {
|
|
60
|
-
label:
|
|
61
|
-
description:
|
|
71
|
+
label: z2.string().min(1),
|
|
72
|
+
description: z2.string().min(1).optional()
|
|
62
73
|
};
|
|
63
|
-
var settingDescriptorSchema =
|
|
64
|
-
|
|
65
|
-
type:
|
|
74
|
+
var settingDescriptorSchema = z2.discriminatedUnion("type", [
|
|
75
|
+
z2.object({
|
|
76
|
+
type: z2.literal("string"),
|
|
66
77
|
...settingsBaseFields,
|
|
67
|
-
secret:
|
|
68
|
-
default:
|
|
78
|
+
secret: z2.literal(true).optional(),
|
|
79
|
+
default: z2.string().optional()
|
|
69
80
|
}).strict(),
|
|
70
|
-
|
|
71
|
-
type:
|
|
81
|
+
z2.object({
|
|
82
|
+
type: z2.literal("boolean"),
|
|
72
83
|
...settingsBaseFields,
|
|
73
|
-
default:
|
|
84
|
+
default: z2.boolean().optional()
|
|
74
85
|
}).strict(),
|
|
75
|
-
|
|
76
|
-
type:
|
|
86
|
+
z2.object({
|
|
87
|
+
type: z2.literal("select"),
|
|
77
88
|
...settingsBaseFields,
|
|
78
|
-
options:
|
|
79
|
-
default:
|
|
89
|
+
options: z2.array(z2.string().min(1)).min(1),
|
|
90
|
+
default: z2.string().optional()
|
|
80
91
|
}).strict(),
|
|
81
|
-
|
|
82
|
-
type:
|
|
92
|
+
z2.object({
|
|
93
|
+
type: z2.literal("project"),
|
|
83
94
|
...settingsBaseFields,
|
|
84
|
-
default:
|
|
95
|
+
default: z2.string().optional()
|
|
85
96
|
}).strict()
|
|
86
97
|
]);
|
|
87
98
|
function registerSettingDescriptors(target, added) {
|
|
@@ -186,6 +197,171 @@ function normalizeMentionProviderTriggers(providerId, triggers) {
|
|
|
186
197
|
}
|
|
187
198
|
return normalized;
|
|
188
199
|
}
|
|
200
|
+
var PLUGIN_PROVIDER_DISPLAY_NAME_MAX_CHARS = 80;
|
|
201
|
+
var PLUGIN_PROVIDER_PERMISSION_MODE_VALUES = [
|
|
202
|
+
"accept-edits",
|
|
203
|
+
"auto",
|
|
204
|
+
"full"
|
|
205
|
+
];
|
|
206
|
+
var PLUGIN_PROVIDER_REASONING_LEVEL_VALUES = [
|
|
207
|
+
"none",
|
|
208
|
+
"low",
|
|
209
|
+
"medium",
|
|
210
|
+
"high",
|
|
211
|
+
"xhigh",
|
|
212
|
+
"ultracode",
|
|
213
|
+
"max",
|
|
214
|
+
"ultra"
|
|
215
|
+
];
|
|
216
|
+
var PLUGIN_PROVIDER_COMPOSER_ACTION_VALUES = [
|
|
217
|
+
"plan",
|
|
218
|
+
"goal"
|
|
219
|
+
];
|
|
220
|
+
function validateProviderRelativePath(value, label) {
|
|
221
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
222
|
+
throw new Error(`provider ${label} must be a non-blank relative path`);
|
|
223
|
+
}
|
|
224
|
+
if (value.includes("\\")) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`provider ${label} must use "/" separators, got ${JSON.stringify(value)}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (value.startsWith("/")) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`provider ${label} must be relative, got ${JSON.stringify(value)}`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (value.split("/").some((segment) => segment === "..")) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`provider ${label} must not escape the plugin directory, got ${JSON.stringify(value)}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
function validateProviderLiteralArray(args) {
|
|
242
|
+
const { providerId, field, value, allowed, requireNonEmpty } = args;
|
|
243
|
+
if (!Array.isArray(value)) {
|
|
244
|
+
throw new Error(`provider "${providerId}" ${field} must be an array`);
|
|
245
|
+
}
|
|
246
|
+
if (requireNonEmpty && value.length === 0) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`provider "${providerId}" ${field} must include at least one entry`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const seen = /* @__PURE__ */ new Set();
|
|
252
|
+
const normalized = [];
|
|
253
|
+
for (const entry of value) {
|
|
254
|
+
if (typeof entry !== "string" || !allowed.includes(entry)) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`provider "${providerId}" ${field} entry ${JSON.stringify(entry)} is invalid; use one of ${allowed.join(", ")}`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const literal = entry;
|
|
260
|
+
if (seen.has(literal)) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`provider "${providerId}" ${field} entry ${JSON.stringify(entry)} is duplicated`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
seen.add(literal);
|
|
266
|
+
normalized.push(literal);
|
|
267
|
+
}
|
|
268
|
+
return Object.freeze(normalized);
|
|
269
|
+
}
|
|
270
|
+
function validatePluginProviderDeclaration(declaration) {
|
|
271
|
+
if (typeof declaration !== "object" || declaration === null) {
|
|
272
|
+
throw new Error("provider declaration must be an object");
|
|
273
|
+
}
|
|
274
|
+
const id = declaration.id;
|
|
275
|
+
if (typeof id !== "string" || !PROVIDER_ID_PATTERN.test(id)) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`invalid provider id ${JSON.stringify(id)} \u2014 use 2-64 lowercase letters, digits, and "-", starting with a letter or digit`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const displayName = typeof declaration.displayName === "string" ? declaration.displayName.trim() : "";
|
|
281
|
+
if (displayName.length === 0 || displayName.length > PLUGIN_PROVIDER_DISPLAY_NAME_MAX_CHARS) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`provider "${id}" displayName must be 1-${PLUGIN_PROVIDER_DISPLAY_NAME_MAX_CHARS} non-blank characters`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
let icon;
|
|
287
|
+
if (declaration.icon !== void 0) {
|
|
288
|
+
if (typeof declaration.icon !== "string" || declaration.icon.trim() === "") {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`provider "${id}" icon must be a non-blank string \u2014 a named host glyph ("Zap") or a plugin-relative path ("./icons/agent.svg")`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (isPluginOwnedIconPath(declaration.icon)) {
|
|
294
|
+
icon = validateProviderRelativePath(declaration.icon, `"${id}" icon`);
|
|
295
|
+
} else if (/[/\\]/u.test(declaration.icon)) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`provider "${id}" icon looks like a path but does not start with "./" \u2014 use "./icons/agent.svg" for a plugin file, or a bare host glyph name like "Zap"`
|
|
298
|
+
);
|
|
299
|
+
} else {
|
|
300
|
+
icon = declaration.icon;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const capabilities = declaration.capabilities;
|
|
304
|
+
if (typeof capabilities !== "object" || capabilities === null) {
|
|
305
|
+
throw new Error(`provider "${id}" capabilities must be an object`);
|
|
306
|
+
}
|
|
307
|
+
const booleanCapabilityFields = [
|
|
308
|
+
"supportsServiceTier",
|
|
309
|
+
"supportsNativeUserQuestion",
|
|
310
|
+
"supportsManualCompaction",
|
|
311
|
+
"supportsThreadArchive",
|
|
312
|
+
"supportsThreadRename",
|
|
313
|
+
"supportsWorkflows"
|
|
314
|
+
];
|
|
315
|
+
for (const field of booleanCapabilityFields) {
|
|
316
|
+
if (typeof capabilities[field] !== "boolean") {
|
|
317
|
+
throw new Error(
|
|
318
|
+
`provider "${id}" capabilities.${field} must be a boolean`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (!PROVIDER_FORK_VALUES.includes(capabilities.fork)) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
`provider "${id}" capabilities.fork must be one of ${PROVIDER_FORK_VALUES.join(", ")}`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const normalizedCapabilities = Object.freeze({
|
|
328
|
+
supportsServiceTier: capabilities.supportsServiceTier,
|
|
329
|
+
supportsNativeUserQuestion: capabilities.supportsNativeUserQuestion,
|
|
330
|
+
fork: capabilities.fork,
|
|
331
|
+
supportsManualCompaction: capabilities.supportsManualCompaction,
|
|
332
|
+
supportsThreadArchive: capabilities.supportsThreadArchive,
|
|
333
|
+
supportsThreadRename: capabilities.supportsThreadRename,
|
|
334
|
+
supportsWorkflows: capabilities.supportsWorkflows,
|
|
335
|
+
permissionModes: validateProviderLiteralArray({
|
|
336
|
+
providerId: id,
|
|
337
|
+
field: "capabilities.permissionModes",
|
|
338
|
+
value: capabilities.permissionModes,
|
|
339
|
+
allowed: PLUGIN_PROVIDER_PERMISSION_MODE_VALUES,
|
|
340
|
+
requireNonEmpty: true
|
|
341
|
+
}),
|
|
342
|
+
reasoningLevels: validateProviderLiteralArray({
|
|
343
|
+
providerId: id,
|
|
344
|
+
field: "capabilities.reasoningLevels",
|
|
345
|
+
value: capabilities.reasoningLevels,
|
|
346
|
+
allowed: PLUGIN_PROVIDER_REASONING_LEVEL_VALUES,
|
|
347
|
+
requireNonEmpty: true
|
|
348
|
+
})
|
|
349
|
+
});
|
|
350
|
+
const composerActions = validateProviderLiteralArray({
|
|
351
|
+
providerId: id,
|
|
352
|
+
field: "composerActions",
|
|
353
|
+
value: declaration.composerActions,
|
|
354
|
+
allowed: PLUGIN_PROVIDER_COMPOSER_ACTION_VALUES,
|
|
355
|
+
requireNonEmpty: false
|
|
356
|
+
});
|
|
357
|
+
return Object.freeze({
|
|
358
|
+
id,
|
|
359
|
+
displayName,
|
|
360
|
+
...icon === void 0 ? {} : { icon },
|
|
361
|
+
capabilities: normalizedCapabilities,
|
|
362
|
+
composerActions
|
|
363
|
+
});
|
|
364
|
+
}
|
|
189
365
|
function isStandardSchema(value) {
|
|
190
366
|
if (typeof value !== "object" || value === null) return false;
|
|
191
367
|
const standard = Reflect.get(value, "~standard");
|
|
@@ -214,6 +390,143 @@ function readRpcMethodContract(method, value) {
|
|
|
214
390
|
function isZodSchemaLike(value) {
|
|
215
391
|
return typeof value === "object" && value !== null && typeof value.safeParse === "function";
|
|
216
392
|
}
|
|
393
|
+
var SINGLE_SCHEMA_KEYWORDS = [
|
|
394
|
+
"additionalItems",
|
|
395
|
+
"additionalProperties",
|
|
396
|
+
"contains",
|
|
397
|
+
"contentSchema",
|
|
398
|
+
"else",
|
|
399
|
+
"if",
|
|
400
|
+
"items",
|
|
401
|
+
"not",
|
|
402
|
+
"propertyNames",
|
|
403
|
+
"then",
|
|
404
|
+
"unevaluatedItems",
|
|
405
|
+
"unevaluatedProperties"
|
|
406
|
+
];
|
|
407
|
+
var SCHEMA_ARRAY_KEYWORDS = [
|
|
408
|
+
"allOf",
|
|
409
|
+
"anyOf",
|
|
410
|
+
"oneOf",
|
|
411
|
+
"prefixItems"
|
|
412
|
+
];
|
|
413
|
+
var SCHEMA_MAP_KEYWORDS = [
|
|
414
|
+
"$defs",
|
|
415
|
+
"definitions",
|
|
416
|
+
"dependentSchemas",
|
|
417
|
+
"patternProperties",
|
|
418
|
+
"properties"
|
|
419
|
+
];
|
|
420
|
+
function isJsonSchemaObject(value) {
|
|
421
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
422
|
+
}
|
|
423
|
+
function decodeJsonPointerToken(token) {
|
|
424
|
+
return token.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
425
|
+
}
|
|
426
|
+
function resolveLocalJsonSchemaReference(document, anchors, reference) {
|
|
427
|
+
if (!reference.startsWith("#")) return void 0;
|
|
428
|
+
let pointer;
|
|
429
|
+
try {
|
|
430
|
+
pointer = decodeURIComponent(reference.slice(1));
|
|
431
|
+
} catch {
|
|
432
|
+
return void 0;
|
|
433
|
+
}
|
|
434
|
+
if (pointer.length === 0) return document;
|
|
435
|
+
if (!pointer.startsWith("/")) return anchors.get(pointer);
|
|
436
|
+
let current = document;
|
|
437
|
+
for (const encodedToken of pointer.slice(1).split("/")) {
|
|
438
|
+
const token = decodeJsonPointerToken(encodedToken);
|
|
439
|
+
if (Array.isArray(current)) {
|
|
440
|
+
if (!/^(0|[1-9][0-9]*)$/.test(token)) return void 0;
|
|
441
|
+
current = current[Number(token)];
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (!isJsonSchemaObject(current) || !Object.hasOwn(current, token)) {
|
|
445
|
+
return void 0;
|
|
446
|
+
}
|
|
447
|
+
current = current[token];
|
|
448
|
+
}
|
|
449
|
+
return current;
|
|
450
|
+
}
|
|
451
|
+
function forEachJsonSchemaChild(schema, visit) {
|
|
452
|
+
for (const keyword of SINGLE_SCHEMA_KEYWORDS) {
|
|
453
|
+
const child = schema[keyword];
|
|
454
|
+
if (Array.isArray(child)) {
|
|
455
|
+
for (const entry of child) visit(entry);
|
|
456
|
+
} else {
|
|
457
|
+
visit(child);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for (const keyword of SCHEMA_ARRAY_KEYWORDS) {
|
|
461
|
+
const children = schema[keyword];
|
|
462
|
+
if (!Array.isArray(children)) continue;
|
|
463
|
+
for (const child of children) visit(child);
|
|
464
|
+
}
|
|
465
|
+
for (const keyword of SCHEMA_MAP_KEYWORDS) {
|
|
466
|
+
const children = schema[keyword];
|
|
467
|
+
if (!isJsonSchemaObject(children)) continue;
|
|
468
|
+
for (const child of Object.values(children)) visit(child);
|
|
469
|
+
}
|
|
470
|
+
const dependencies = schema.dependencies;
|
|
471
|
+
if (isJsonSchemaObject(dependencies)) {
|
|
472
|
+
for (const dependency of Object.values(dependencies)) {
|
|
473
|
+
if (!Array.isArray(dependency)) visit(dependency);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function assertNoRecursiveJsonSchemaReferences(schema, subject) {
|
|
478
|
+
if (!isJsonSchemaObject(schema)) return;
|
|
479
|
+
const document = schema;
|
|
480
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
481
|
+
const indexed = /* @__PURE__ */ new Set();
|
|
482
|
+
function indexAnchors(candidate) {
|
|
483
|
+
if (!isJsonSchemaObject(candidate) || indexed.has(candidate)) return;
|
|
484
|
+
indexed.add(candidate);
|
|
485
|
+
for (const keyword of ["$anchor", "$dynamicAnchor"]) {
|
|
486
|
+
const anchor = candidate[keyword];
|
|
487
|
+
if (typeof anchor === "string") anchors.set(anchor, candidate);
|
|
488
|
+
}
|
|
489
|
+
for (const keyword of ["$id", "id"]) {
|
|
490
|
+
const id = candidate[keyword];
|
|
491
|
+
if (typeof id === "string" && /^#[^/]+$/.test(id)) {
|
|
492
|
+
anchors.set(id.slice(1), candidate);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
forEachJsonSchemaChild(candidate, indexAnchors);
|
|
496
|
+
}
|
|
497
|
+
indexAnchors(document);
|
|
498
|
+
const visited = /* @__PURE__ */ new Set();
|
|
499
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
500
|
+
function visit(candidate, viaReference) {
|
|
501
|
+
if (typeof candidate === "boolean" || !isJsonSchemaObject(candidate)) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (visiting.has(candidate)) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`${subject} contains recursive JSON Schema ${viaReference?.keyword ?? "$ref"} ${JSON.stringify(viaReference?.value ?? "#")}`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
if (visited.has(candidate)) return;
|
|
510
|
+
visiting.add(candidate);
|
|
511
|
+
for (const keyword of ["$ref", "$recursiveRef", "$dynamicRef"]) {
|
|
512
|
+
const reference = candidate[keyword];
|
|
513
|
+
if (typeof reference === "string" && reference.startsWith("#")) {
|
|
514
|
+
const target = resolveLocalJsonSchemaReference(
|
|
515
|
+
document,
|
|
516
|
+
anchors,
|
|
517
|
+
reference
|
|
518
|
+
);
|
|
519
|
+
if (target !== void 0) {
|
|
520
|
+
visit(target, { keyword, value: reference });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
forEachJsonSchemaChild(candidate, visit);
|
|
525
|
+
visiting.delete(candidate);
|
|
526
|
+
visited.add(candidate);
|
|
527
|
+
}
|
|
528
|
+
visit(schema);
|
|
529
|
+
}
|
|
217
530
|
function summarizeParseIssues(error) {
|
|
218
531
|
const issues = error?.issues;
|
|
219
532
|
if (Array.isArray(issues) && issues.length > 0) {
|
|
@@ -244,6 +557,56 @@ function enforcePluginCliOutputLimit(result, jsonOutput) {
|
|
|
244
557
|
error
|
|
245
558
|
} : { exitCode: 1, stdout: "", stderr: error.message, error };
|
|
246
559
|
}
|
|
560
|
+
function adoptHttpRouteResponse(value) {
|
|
561
|
+
if (value instanceof Response) return value;
|
|
562
|
+
if (!isResponseLike(value)) {
|
|
563
|
+
throw new Error("http route handler must return a Response");
|
|
564
|
+
}
|
|
565
|
+
const status = value.status;
|
|
566
|
+
const isNullBodyStatus = status === 101 || status === 204 || status === 205 || status === 304;
|
|
567
|
+
const init = {
|
|
568
|
+
status,
|
|
569
|
+
statusText: typeof value.statusText === "string" ? value.statusText : "",
|
|
570
|
+
headers: new Headers(value.headers)
|
|
571
|
+
};
|
|
572
|
+
if (isNullBodyStatus || value.body === null) {
|
|
573
|
+
return new Response(null, init);
|
|
574
|
+
}
|
|
575
|
+
return new Response(adoptBodyStream(value), init);
|
|
576
|
+
}
|
|
577
|
+
function adoptBodyStream(value) {
|
|
578
|
+
const source = value.body;
|
|
579
|
+
if (!isReadableStreamLike(source)) {
|
|
580
|
+
return new ReadableStream({
|
|
581
|
+
async start(controller) {
|
|
582
|
+
controller.enqueue(new Uint8Array(await value.arrayBuffer()));
|
|
583
|
+
controller.close();
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
const reader = source.getReader();
|
|
588
|
+
return new ReadableStream({
|
|
589
|
+
async pull(controller) {
|
|
590
|
+
const { done, value: chunk } = await reader.read();
|
|
591
|
+
if (done) {
|
|
592
|
+
controller.close();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
controller.enqueue(chunk);
|
|
596
|
+
},
|
|
597
|
+
async cancel(reason) {
|
|
598
|
+
await reader.cancel(reason);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
function isReadableStreamLike(value) {
|
|
603
|
+
return value !== null && typeof value === "object" && typeof value.getReader === "function";
|
|
604
|
+
}
|
|
605
|
+
function isResponseLike(value) {
|
|
606
|
+
if (value === null || typeof value !== "object") return false;
|
|
607
|
+
const candidate = value;
|
|
608
|
+
return typeof candidate.status === "number" && typeof candidate.headers === "object" && candidate.headers !== null && typeof candidate.arrayBuffer === "function" && typeof candidate.clone === "function";
|
|
609
|
+
}
|
|
247
610
|
|
|
248
611
|
// src/testing/fake-sdk.ts
|
|
249
612
|
function withSpawnAttribution(pluginId, args) {
|
|
@@ -540,6 +903,10 @@ function normalizeAgentToolParameters(args) {
|
|
|
540
903
|
`configure() output.tools[${index}].parameters must have root type "object"`
|
|
541
904
|
);
|
|
542
905
|
}
|
|
906
|
+
assertNoRecursiveJsonSchemaReferences(
|
|
907
|
+
parameters,
|
|
908
|
+
`configure() output.tools[${index}].parameters`
|
|
909
|
+
);
|
|
543
910
|
return parameters;
|
|
544
911
|
}
|
|
545
912
|
function normalizeAgentConfigurationIds(args) {
|
|
@@ -929,6 +1296,7 @@ function createFakePluginHostInternal(options, sharedState) {
|
|
|
929
1296
|
}
|
|
930
1297
|
};
|
|
931
1298
|
const agentTools = [];
|
|
1299
|
+
const providerRegistrations = [];
|
|
932
1300
|
let agentConfigurationProvider = null;
|
|
933
1301
|
let instructionProvider = null;
|
|
934
1302
|
const agents = {
|
|
@@ -956,6 +1324,25 @@ function createFakePluginHostInternal(options, sharedState) {
|
|
|
956
1324
|
}
|
|
957
1325
|
instructionProvider = provider;
|
|
958
1326
|
},
|
|
1327
|
+
experimental_registerProvider(declaration) {
|
|
1328
|
+
assertLive();
|
|
1329
|
+
const normalized = validatePluginProviderDeclaration(declaration);
|
|
1330
|
+
if (providerRegistrations.some((existing) => existing.id === normalized.id)) {
|
|
1331
|
+
throw new Error(
|
|
1332
|
+
`Provider "${normalized.id}" is already registered; a plugin cannot shadow an existing provider.`
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
providerRegistrations.push(normalized);
|
|
1336
|
+
let disposed2 = false;
|
|
1337
|
+
const dispose = () => {
|
|
1338
|
+
if (disposed2) return;
|
|
1339
|
+
disposed2 = true;
|
|
1340
|
+
const index = providerRegistrations.indexOf(normalized);
|
|
1341
|
+
if (index !== -1) providerRegistrations.splice(index, 1);
|
|
1342
|
+
};
|
|
1343
|
+
disposeHooks.push(dispose);
|
|
1344
|
+
return { dispose };
|
|
1345
|
+
},
|
|
959
1346
|
registerTool(tool) {
|
|
960
1347
|
assertLive();
|
|
961
1348
|
const name = tool?.name;
|
|
@@ -1001,7 +1388,7 @@ function createFakePluginHostInternal(options, sharedState) {
|
|
|
1001
1388
|
let parse;
|
|
1002
1389
|
if (isZodSchemaLike(parameters)) {
|
|
1003
1390
|
try {
|
|
1004
|
-
inputSchema =
|
|
1391
|
+
inputSchema = z3.toJSONSchema(parameters, {
|
|
1005
1392
|
io: "input"
|
|
1006
1393
|
});
|
|
1007
1394
|
} catch (error) {
|
|
@@ -1028,6 +1415,10 @@ function createFakePluginHostInternal(options, sharedState) {
|
|
|
1028
1415
|
`tool "${name}" parameters must be a zod schema or a JSON-schema object`
|
|
1029
1416
|
);
|
|
1030
1417
|
}
|
|
1418
|
+
assertNoRecursiveJsonSchemaReferences(
|
|
1419
|
+
inputSchema,
|
|
1420
|
+
`tool "${name}" parameters`
|
|
1421
|
+
);
|
|
1031
1422
|
const record = {
|
|
1032
1423
|
name,
|
|
1033
1424
|
description: tool.description,
|
|
@@ -1182,7 +1573,90 @@ function createFakePluginHostInternal(options, sharedState) {
|
|
|
1182
1573
|
});
|
|
1183
1574
|
}
|
|
1184
1575
|
const sharedPortDeclarations = [];
|
|
1576
|
+
const hostRpcCalls = [];
|
|
1577
|
+
const hostWorkerExitSubscriptions = [];
|
|
1578
|
+
const hostSignalSubscriptions = [];
|
|
1185
1579
|
const hosts = {
|
|
1580
|
+
experimental_client({ contract, experimental_signals }) {
|
|
1581
|
+
return {
|
|
1582
|
+
async call(method, input, callOptions) {
|
|
1583
|
+
assertLive();
|
|
1584
|
+
const methodContract = contract[method];
|
|
1585
|
+
if (methodContract === void 0) {
|
|
1586
|
+
throw new Error(`unknown host rpc method "${String(method)}"`);
|
|
1587
|
+
}
|
|
1588
|
+
if (typeof callOptions !== "object" || callOptions === null || typeof callOptions.hostId !== "string" || callOptions.hostId.length === 0) {
|
|
1589
|
+
throw new Error(
|
|
1590
|
+
`host rpc method "${String(method)}" requires a host id`
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
if (callOptions.signal?.aborted) {
|
|
1594
|
+
throw Object.assign(new Error("Host plugin call was cancelled"), {
|
|
1595
|
+
name: "AbortError"
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
const validatedInput = normalizeRpcJsonResult(
|
|
1599
|
+
await validateRpcValue(methodContract.input, input, "input")
|
|
1600
|
+
);
|
|
1601
|
+
const call = {
|
|
1602
|
+
method: String(method),
|
|
1603
|
+
input: validatedInput,
|
|
1604
|
+
hostId: callOptions.hostId,
|
|
1605
|
+
...callOptions.signal === void 0 ? {} : { signal: callOptions.signal }
|
|
1606
|
+
};
|
|
1607
|
+
hostRpcCalls.push(call);
|
|
1608
|
+
if (options.experimental_callHostRpc === void 0) {
|
|
1609
|
+
throw new Error(
|
|
1610
|
+
`fake plugin host has no experimental_callHostRpc stub for "${String(method)}"`
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
const rawOutput = await options.experimental_callHostRpc(call);
|
|
1614
|
+
const validatedOutput = await validateRpcValue(
|
|
1615
|
+
methodContract.output,
|
|
1616
|
+
rawOutput,
|
|
1617
|
+
"output"
|
|
1618
|
+
);
|
|
1619
|
+
return normalizeRpcJsonResult(validatedOutput);
|
|
1620
|
+
},
|
|
1621
|
+
experimental_onWorkerExit(handler) {
|
|
1622
|
+
assertLive();
|
|
1623
|
+
if (typeof handler !== "function") {
|
|
1624
|
+
throw new Error("host worker exit subscription requires a handler");
|
|
1625
|
+
}
|
|
1626
|
+
hostWorkerExitSubscriptions.push(handler);
|
|
1627
|
+
let subscribed = true;
|
|
1628
|
+
return () => {
|
|
1629
|
+
if (!subscribed) return;
|
|
1630
|
+
subscribed = false;
|
|
1631
|
+
const index = hostWorkerExitSubscriptions.indexOf(handler);
|
|
1632
|
+
if (index >= 0) hostWorkerExitSubscriptions.splice(index, 1);
|
|
1633
|
+
};
|
|
1634
|
+
},
|
|
1635
|
+
experimental_onSignal(signal, handler) {
|
|
1636
|
+
assertLive();
|
|
1637
|
+
const descriptor = experimental_signals?.[signal];
|
|
1638
|
+
if (typeof signal !== "string" || signal.length === 0 || typeof descriptor !== "object" || descriptor === null || !isStandardSchema(descriptor.payload)) {
|
|
1639
|
+
throw new Error(`unknown host signal "${String(signal)}"`);
|
|
1640
|
+
}
|
|
1641
|
+
if (typeof handler !== "function") {
|
|
1642
|
+
throw new Error("host signal subscription requires a handler");
|
|
1643
|
+
}
|
|
1644
|
+
const record = {
|
|
1645
|
+
signal,
|
|
1646
|
+
payloadSchema: descriptor.payload,
|
|
1647
|
+
handler
|
|
1648
|
+
};
|
|
1649
|
+
hostSignalSubscriptions.push(record);
|
|
1650
|
+
let subscribed = true;
|
|
1651
|
+
return () => {
|
|
1652
|
+
if (!subscribed) return;
|
|
1653
|
+
subscribed = false;
|
|
1654
|
+
const index = hostSignalSubscriptions.indexOf(record);
|
|
1655
|
+
if (index >= 0) hostSignalSubscriptions.splice(index, 1);
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
},
|
|
1186
1660
|
async ensureSharedPortTunnel(hostId) {
|
|
1187
1661
|
assertLive();
|
|
1188
1662
|
if (hostId.trim().length === 0) {
|
|
@@ -1289,6 +1763,8 @@ function createFakePluginHostInternal(options, sharedState) {
|
|
|
1289
1763
|
if (cleanupStorage) {
|
|
1290
1764
|
rmSync(storageRoot, { recursive: true, force: true });
|
|
1291
1765
|
}
|
|
1766
|
+
hostWorkerExitSubscriptions.splice(0);
|
|
1767
|
+
hostSignalSubscriptions.splice(0);
|
|
1292
1768
|
invalidated = true;
|
|
1293
1769
|
}
|
|
1294
1770
|
const harness = {
|
|
@@ -1306,6 +1782,7 @@ function createFakePluginHostInternal(options, sharedState) {
|
|
|
1306
1782
|
realtimeSignals,
|
|
1307
1783
|
needsConfigurationMessages,
|
|
1308
1784
|
sharedPortDeclarations,
|
|
1785
|
+
experimental_hostRpcCalls: hostRpcCalls,
|
|
1309
1786
|
sdk: sdkHarness,
|
|
1310
1787
|
registrations: {
|
|
1311
1788
|
settingsDescriptors,
|
|
@@ -1335,7 +1812,8 @@ function createFakePluginHostInternal(options, sharedState) {
|
|
|
1335
1812
|
"thread.deleted": threadEventHandlers["thread.deleted"].length
|
|
1336
1813
|
};
|
|
1337
1814
|
},
|
|
1338
|
-
mentionProviders
|
|
1815
|
+
mentionProviders,
|
|
1816
|
+
providerRegistrations
|
|
1339
1817
|
},
|
|
1340
1818
|
get pendingInteractions() {
|
|
1341
1819
|
return [...pendingInteractions].map(([id, pending]) => ({
|
|
@@ -1343,6 +1821,35 @@ function createFakePluginHostInternal(options, sharedState) {
|
|
|
1343
1821
|
...pending.request
|
|
1344
1822
|
}));
|
|
1345
1823
|
},
|
|
1824
|
+
async experimental_emitHostWorkerExit(hostId) {
|
|
1825
|
+
assertLive();
|
|
1826
|
+
if (hostId.trim().length === 0) {
|
|
1827
|
+
throw new Error("host worker exit hostId must be non-empty");
|
|
1828
|
+
}
|
|
1829
|
+
for (const handler of [...hostWorkerExitSubscriptions]) {
|
|
1830
|
+
await handler({ hostId });
|
|
1831
|
+
}
|
|
1832
|
+
},
|
|
1833
|
+
async experimental_emitHostSignal(hostId, signal, payload) {
|
|
1834
|
+
assertLive();
|
|
1835
|
+
if (hostId.trim().length === 0) {
|
|
1836
|
+
throw new Error("host signal hostId must be non-empty");
|
|
1837
|
+
}
|
|
1838
|
+
const subscriptions = hostSignalSubscriptions.filter(
|
|
1839
|
+
(subscription) => subscription.signal === signal
|
|
1840
|
+
);
|
|
1841
|
+
for (const subscription of subscriptions) {
|
|
1842
|
+
const normalized = normalizeRpcJsonResult(
|
|
1843
|
+
await validateRpcValue(subscription.payloadSchema, payload, "input")
|
|
1844
|
+
);
|
|
1845
|
+
const parsed = await validateRpcValue(
|
|
1846
|
+
subscription.payloadSchema,
|
|
1847
|
+
normalized,
|
|
1848
|
+
"input"
|
|
1849
|
+
);
|
|
1850
|
+
await subscription.handler({ hostId, payload: parsed });
|
|
1851
|
+
}
|
|
1852
|
+
},
|
|
1346
1853
|
submitInteraction(id, value) {
|
|
1347
1854
|
const pending = pendingInteractions.get(id);
|
|
1348
1855
|
if (!pending) throw new Error(`no pending interaction "${id}"`);
|
|
@@ -1455,11 +1962,7 @@ function createFakePluginHostInternal(options, sharedState) {
|
|
|
1455
1962
|
const app = new Hono();
|
|
1456
1963
|
app.on(route.method, route.path, async (context) => {
|
|
1457
1964
|
try {
|
|
1458
|
-
|
|
1459
|
-
if (!(response instanceof Response)) {
|
|
1460
|
-
throw new Error("http route handler must return a Response");
|
|
1461
|
-
}
|
|
1462
|
-
return response;
|
|
1965
|
+
return adoptHttpRouteResponse(await route.handler(context));
|
|
1463
1966
|
} catch (error) {
|
|
1464
1967
|
const message = errorMessage(error);
|
|
1465
1968
|
emitLog(
|