@get-bb/plugin-sdk 0.4.6 → 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.
@@ -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 z2 } from "zod";
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: z.string().min(1),
61
- description: z.string().min(1).optional()
71
+ label: z2.string().min(1),
72
+ description: z2.string().min(1).optional()
62
73
  };
63
- var settingDescriptorSchema = z.discriminatedUnion("type", [
64
- z.object({
65
- type: z.literal("string"),
74
+ var settingDescriptorSchema = z2.discriminatedUnion("type", [
75
+ z2.object({
76
+ type: z2.literal("string"),
66
77
  ...settingsBaseFields,
67
- secret: z.literal(true).optional(),
68
- default: z.string().optional()
78
+ secret: z2.literal(true).optional(),
79
+ default: z2.string().optional()
69
80
  }).strict(),
70
- z.object({
71
- type: z.literal("boolean"),
81
+ z2.object({
82
+ type: z2.literal("boolean"),
72
83
  ...settingsBaseFields,
73
- default: z.boolean().optional()
84
+ default: z2.boolean().optional()
74
85
  }).strict(),
75
- z.object({
76
- type: z.literal("select"),
86
+ z2.object({
87
+ type: z2.literal("select"),
77
88
  ...settingsBaseFields,
78
- options: z.array(z.string().min(1)).min(1),
79
- default: z.string().optional()
89
+ options: z2.array(z2.string().min(1)).min(1),
90
+ default: z2.string().optional()
80
91
  }).strict(),
81
- z.object({
82
- type: z.literal("project"),
92
+ z2.object({
93
+ type: z2.literal("project"),
83
94
  ...settingsBaseFields,
84
- default: z.string().optional()
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 = z2.toJSONSchema(parameters, {
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
- const response = await route.handler(context);
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(