@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.
package/dist/host.js ADDED
@@ -0,0 +1,13 @@
1
+ // src/host-contract.ts
2
+ function experimental_defineHostEntry(args) {
3
+ return {
4
+ experimental_apiVersion: 1,
5
+ contract: args.contract,
6
+ handlers: args.handlers,
7
+ ...args.experimental_signals === void 0 ? {} : { experimental_signals: args.experimental_signals },
8
+ ...args.dispose === void 0 ? {} : { dispose: args.dispose }
9
+ };
10
+ }
11
+ export {
12
+ experimental_defineHostEntry
13
+ };
package/dist/index.js CHANGED
@@ -1,11 +1,23 @@
1
1
  // src/backend-contract.ts
2
2
  var PLUGIN_CLI_OUTPUT_MAX_BYTES = 1024 * 1024;
3
3
 
4
+ // src/host-contract.ts
5
+ function experimental_defineHostEntry(args) {
6
+ return {
7
+ experimental_apiVersion: 1,
8
+ contract: args.contract,
9
+ handlers: args.handlers,
10
+ ...args.experimental_signals === void 0 ? {} : { experimental_signals: args.experimental_signals },
11
+ ...args.dispose === void 0 ? {} : { dispose: args.dispose }
12
+ };
13
+ }
14
+
4
15
  // src/rpc-contract.ts
5
16
  function defineRpcContract(contract) {
6
17
  return contract;
7
18
  }
8
19
  export {
9
20
  PLUGIN_CLI_OUTPUT_MAX_BYTES,
10
- defineRpcContract
21
+ defineRpcContract,
22
+ experimental_defineHostEntry
11
23
  };
@@ -40,6 +40,14 @@ function requireSlotId(kind, value) {
40
40
  }
41
41
  return value;
42
42
  }
43
+ function requireProviderId(kind, value) {
44
+ if (typeof value !== "string" || !PLUGIN_SLOT_ID_PATTERN.test(value)) {
45
+ throw new Error(
46
+ `${kind}: "providerId" must match ${String(PLUGIN_SLOT_ID_PATTERN)}, got ${JSON.stringify(value)}`
47
+ );
48
+ }
49
+ return value;
50
+ }
43
51
  function requireMessageDirectiveId(kind, value) {
44
52
  if (typeof value !== "string" || !PLUGIN_MESSAGE_DIRECTIVE_ID_PATTERN.test(value)) {
45
53
  throw new Error(
@@ -233,6 +241,7 @@ export {
233
241
  requireMessageDirectiveId,
234
242
  requireNonEmptyString,
235
243
  requireOptionalString,
244
+ requireProviderId,
236
245
  requireSlotId,
237
246
  requireUniqueId
238
247
  };
@@ -1,5 +1,10 @@
1
1
  // src/internal/host-policy.ts
2
- import { z } from "zod";
2
+ import { z as z2 } from "zod";
3
+
4
+ // ../domain/src/plugin-icon.ts
5
+ function isPluginOwnedIconPath(icon) {
6
+ return icon.startsWith("./");
7
+ }
3
8
 
4
9
  // ../domain/src/plugin-cli.ts
5
10
  var RESERVED_BB_CLI_COMMANDS = [
@@ -16,6 +21,11 @@ var RESERVED_BB_CLI_COMMANDS = [
16
21
  "thread"
17
22
  ];
18
23
 
24
+ // ../domain/src/provider-fork.ts
25
+ import { z } from "zod";
26
+ var PROVIDER_FORK_VALUES = ["none", "tip", "checkpoint"];
27
+ var providerForkSchema = z.enum(PROVIDER_FORK_VALUES);
28
+
19
29
  // src/backend-contract.ts
20
30
  var PLUGIN_CLI_OUTPUT_MAX_BYTES = 1024 * 1024;
21
31
 
@@ -43,33 +53,34 @@ var PLUGIN_AGENT_SELECTION_MAX_IDS = 256;
43
53
  var PLUGIN_AGENT_DYNAMIC_INSTRUCTIONS_MAX_CHARS = 4096;
44
54
  var PLUGIN_AGENT_TOOL_PARAMETERS_MAX_BYTES = 128 * 1024;
45
55
  var MENTION_PROVIDER_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
56
+ var PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,63}$/;
46
57
  var SETTING_KEY_PATTERN = /^[a-zA-Z0-9_-]+$/;
47
58
  var settingsBaseFields = {
48
- label: z.string().min(1),
49
- description: z.string().min(1).optional()
59
+ label: z2.string().min(1),
60
+ description: z2.string().min(1).optional()
50
61
  };
51
- var settingDescriptorSchema = z.discriminatedUnion("type", [
52
- z.object({
53
- type: z.literal("string"),
62
+ var settingDescriptorSchema = z2.discriminatedUnion("type", [
63
+ z2.object({
64
+ type: z2.literal("string"),
54
65
  ...settingsBaseFields,
55
- secret: z.literal(true).optional(),
56
- default: z.string().optional()
66
+ secret: z2.literal(true).optional(),
67
+ default: z2.string().optional()
57
68
  }).strict(),
58
- z.object({
59
- type: z.literal("boolean"),
69
+ z2.object({
70
+ type: z2.literal("boolean"),
60
71
  ...settingsBaseFields,
61
- default: z.boolean().optional()
72
+ default: z2.boolean().optional()
62
73
  }).strict(),
63
- z.object({
64
- type: z.literal("select"),
74
+ z2.object({
75
+ type: z2.literal("select"),
65
76
  ...settingsBaseFields,
66
- options: z.array(z.string().min(1)).min(1),
67
- default: z.string().optional()
77
+ options: z2.array(z2.string().min(1)).min(1),
78
+ default: z2.string().optional()
68
79
  }).strict(),
69
- z.object({
70
- type: z.literal("project"),
80
+ z2.object({
81
+ type: z2.literal("project"),
71
82
  ...settingsBaseFields,
72
- default: z.string().optional()
83
+ default: z2.string().optional()
73
84
  }).strict()
74
85
  ]);
75
86
  function registerSettingDescriptors(target, added) {
@@ -174,6 +185,171 @@ function normalizeMentionProviderTriggers(providerId, triggers) {
174
185
  }
175
186
  return normalized;
176
187
  }
188
+ var PLUGIN_PROVIDER_DISPLAY_NAME_MAX_CHARS = 80;
189
+ var PLUGIN_PROVIDER_PERMISSION_MODE_VALUES = [
190
+ "accept-edits",
191
+ "auto",
192
+ "full"
193
+ ];
194
+ var PLUGIN_PROVIDER_REASONING_LEVEL_VALUES = [
195
+ "none",
196
+ "low",
197
+ "medium",
198
+ "high",
199
+ "xhigh",
200
+ "ultracode",
201
+ "max",
202
+ "ultra"
203
+ ];
204
+ var PLUGIN_PROVIDER_COMPOSER_ACTION_VALUES = [
205
+ "plan",
206
+ "goal"
207
+ ];
208
+ function validateProviderRelativePath(value, label) {
209
+ if (typeof value !== "string" || value.trim().length === 0) {
210
+ throw new Error(`provider ${label} must be a non-blank relative path`);
211
+ }
212
+ if (value.includes("\\")) {
213
+ throw new Error(
214
+ `provider ${label} must use "/" separators, got ${JSON.stringify(value)}`
215
+ );
216
+ }
217
+ if (value.startsWith("/")) {
218
+ throw new Error(
219
+ `provider ${label} must be relative, got ${JSON.stringify(value)}`
220
+ );
221
+ }
222
+ if (value.split("/").some((segment) => segment === "..")) {
223
+ throw new Error(
224
+ `provider ${label} must not escape the plugin directory, got ${JSON.stringify(value)}`
225
+ );
226
+ }
227
+ return value;
228
+ }
229
+ function validateProviderLiteralArray(args) {
230
+ const { providerId, field, value, allowed, requireNonEmpty } = args;
231
+ if (!Array.isArray(value)) {
232
+ throw new Error(`provider "${providerId}" ${field} must be an array`);
233
+ }
234
+ if (requireNonEmpty && value.length === 0) {
235
+ throw new Error(
236
+ `provider "${providerId}" ${field} must include at least one entry`
237
+ );
238
+ }
239
+ const seen = /* @__PURE__ */ new Set();
240
+ const normalized = [];
241
+ for (const entry of value) {
242
+ if (typeof entry !== "string" || !allowed.includes(entry)) {
243
+ throw new Error(
244
+ `provider "${providerId}" ${field} entry ${JSON.stringify(entry)} is invalid; use one of ${allowed.join(", ")}`
245
+ );
246
+ }
247
+ const literal = entry;
248
+ if (seen.has(literal)) {
249
+ throw new Error(
250
+ `provider "${providerId}" ${field} entry ${JSON.stringify(entry)} is duplicated`
251
+ );
252
+ }
253
+ seen.add(literal);
254
+ normalized.push(literal);
255
+ }
256
+ return Object.freeze(normalized);
257
+ }
258
+ function validatePluginProviderDeclaration(declaration) {
259
+ if (typeof declaration !== "object" || declaration === null) {
260
+ throw new Error("provider declaration must be an object");
261
+ }
262
+ const id = declaration.id;
263
+ if (typeof id !== "string" || !PROVIDER_ID_PATTERN.test(id)) {
264
+ throw new Error(
265
+ `invalid provider id ${JSON.stringify(id)} \u2014 use 2-64 lowercase letters, digits, and "-", starting with a letter or digit`
266
+ );
267
+ }
268
+ const displayName = typeof declaration.displayName === "string" ? declaration.displayName.trim() : "";
269
+ if (displayName.length === 0 || displayName.length > PLUGIN_PROVIDER_DISPLAY_NAME_MAX_CHARS) {
270
+ throw new Error(
271
+ `provider "${id}" displayName must be 1-${PLUGIN_PROVIDER_DISPLAY_NAME_MAX_CHARS} non-blank characters`
272
+ );
273
+ }
274
+ let icon;
275
+ if (declaration.icon !== void 0) {
276
+ if (typeof declaration.icon !== "string" || declaration.icon.trim() === "") {
277
+ throw new Error(
278
+ `provider "${id}" icon must be a non-blank string \u2014 a named host glyph ("Zap") or a plugin-relative path ("./icons/agent.svg")`
279
+ );
280
+ }
281
+ if (isPluginOwnedIconPath(declaration.icon)) {
282
+ icon = validateProviderRelativePath(declaration.icon, `"${id}" icon`);
283
+ } else if (/[/\\]/u.test(declaration.icon)) {
284
+ throw new Error(
285
+ `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"`
286
+ );
287
+ } else {
288
+ icon = declaration.icon;
289
+ }
290
+ }
291
+ const capabilities = declaration.capabilities;
292
+ if (typeof capabilities !== "object" || capabilities === null) {
293
+ throw new Error(`provider "${id}" capabilities must be an object`);
294
+ }
295
+ const booleanCapabilityFields = [
296
+ "supportsServiceTier",
297
+ "supportsNativeUserQuestion",
298
+ "supportsManualCompaction",
299
+ "supportsThreadArchive",
300
+ "supportsThreadRename",
301
+ "supportsWorkflows"
302
+ ];
303
+ for (const field of booleanCapabilityFields) {
304
+ if (typeof capabilities[field] !== "boolean") {
305
+ throw new Error(
306
+ `provider "${id}" capabilities.${field} must be a boolean`
307
+ );
308
+ }
309
+ }
310
+ if (!PROVIDER_FORK_VALUES.includes(capabilities.fork)) {
311
+ throw new Error(
312
+ `provider "${id}" capabilities.fork must be one of ${PROVIDER_FORK_VALUES.join(", ")}`
313
+ );
314
+ }
315
+ const normalizedCapabilities = Object.freeze({
316
+ supportsServiceTier: capabilities.supportsServiceTier,
317
+ supportsNativeUserQuestion: capabilities.supportsNativeUserQuestion,
318
+ fork: capabilities.fork,
319
+ supportsManualCompaction: capabilities.supportsManualCompaction,
320
+ supportsThreadArchive: capabilities.supportsThreadArchive,
321
+ supportsThreadRename: capabilities.supportsThreadRename,
322
+ supportsWorkflows: capabilities.supportsWorkflows,
323
+ permissionModes: validateProviderLiteralArray({
324
+ providerId: id,
325
+ field: "capabilities.permissionModes",
326
+ value: capabilities.permissionModes,
327
+ allowed: PLUGIN_PROVIDER_PERMISSION_MODE_VALUES,
328
+ requireNonEmpty: true
329
+ }),
330
+ reasoningLevels: validateProviderLiteralArray({
331
+ providerId: id,
332
+ field: "capabilities.reasoningLevels",
333
+ value: capabilities.reasoningLevels,
334
+ allowed: PLUGIN_PROVIDER_REASONING_LEVEL_VALUES,
335
+ requireNonEmpty: true
336
+ })
337
+ });
338
+ const composerActions = validateProviderLiteralArray({
339
+ providerId: id,
340
+ field: "composerActions",
341
+ value: declaration.composerActions,
342
+ allowed: PLUGIN_PROVIDER_COMPOSER_ACTION_VALUES,
343
+ requireNonEmpty: false
344
+ });
345
+ return Object.freeze({
346
+ id,
347
+ displayName,
348
+ ...icon === void 0 ? {} : { icon },
349
+ capabilities: normalizedCapabilities,
350
+ composerActions
351
+ });
352
+ }
177
353
  function isStandardSchema(value) {
178
354
  if (typeof value !== "object" || value === null) return false;
179
355
  const standard = Reflect.get(value, "~standard");
@@ -202,6 +378,143 @@ function readRpcMethodContract(method, value) {
202
378
  function isZodSchemaLike(value) {
203
379
  return typeof value === "object" && value !== null && typeof value.safeParse === "function";
204
380
  }
381
+ var SINGLE_SCHEMA_KEYWORDS = [
382
+ "additionalItems",
383
+ "additionalProperties",
384
+ "contains",
385
+ "contentSchema",
386
+ "else",
387
+ "if",
388
+ "items",
389
+ "not",
390
+ "propertyNames",
391
+ "then",
392
+ "unevaluatedItems",
393
+ "unevaluatedProperties"
394
+ ];
395
+ var SCHEMA_ARRAY_KEYWORDS = [
396
+ "allOf",
397
+ "anyOf",
398
+ "oneOf",
399
+ "prefixItems"
400
+ ];
401
+ var SCHEMA_MAP_KEYWORDS = [
402
+ "$defs",
403
+ "definitions",
404
+ "dependentSchemas",
405
+ "patternProperties",
406
+ "properties"
407
+ ];
408
+ function isJsonSchemaObject(value) {
409
+ return typeof value === "object" && value !== null && !Array.isArray(value);
410
+ }
411
+ function decodeJsonPointerToken(token) {
412
+ return token.replaceAll("~1", "/").replaceAll("~0", "~");
413
+ }
414
+ function resolveLocalJsonSchemaReference(document, anchors, reference) {
415
+ if (!reference.startsWith("#")) return void 0;
416
+ let pointer;
417
+ try {
418
+ pointer = decodeURIComponent(reference.slice(1));
419
+ } catch {
420
+ return void 0;
421
+ }
422
+ if (pointer.length === 0) return document;
423
+ if (!pointer.startsWith("/")) return anchors.get(pointer);
424
+ let current = document;
425
+ for (const encodedToken of pointer.slice(1).split("/")) {
426
+ const token = decodeJsonPointerToken(encodedToken);
427
+ if (Array.isArray(current)) {
428
+ if (!/^(0|[1-9][0-9]*)$/.test(token)) return void 0;
429
+ current = current[Number(token)];
430
+ continue;
431
+ }
432
+ if (!isJsonSchemaObject(current) || !Object.hasOwn(current, token)) {
433
+ return void 0;
434
+ }
435
+ current = current[token];
436
+ }
437
+ return current;
438
+ }
439
+ function forEachJsonSchemaChild(schema, visit) {
440
+ for (const keyword of SINGLE_SCHEMA_KEYWORDS) {
441
+ const child = schema[keyword];
442
+ if (Array.isArray(child)) {
443
+ for (const entry of child) visit(entry);
444
+ } else {
445
+ visit(child);
446
+ }
447
+ }
448
+ for (const keyword of SCHEMA_ARRAY_KEYWORDS) {
449
+ const children = schema[keyword];
450
+ if (!Array.isArray(children)) continue;
451
+ for (const child of children) visit(child);
452
+ }
453
+ for (const keyword of SCHEMA_MAP_KEYWORDS) {
454
+ const children = schema[keyword];
455
+ if (!isJsonSchemaObject(children)) continue;
456
+ for (const child of Object.values(children)) visit(child);
457
+ }
458
+ const dependencies = schema.dependencies;
459
+ if (isJsonSchemaObject(dependencies)) {
460
+ for (const dependency of Object.values(dependencies)) {
461
+ if (!Array.isArray(dependency)) visit(dependency);
462
+ }
463
+ }
464
+ }
465
+ function assertNoRecursiveJsonSchemaReferences(schema, subject) {
466
+ if (!isJsonSchemaObject(schema)) return;
467
+ const document = schema;
468
+ const anchors = /* @__PURE__ */ new Map();
469
+ const indexed = /* @__PURE__ */ new Set();
470
+ function indexAnchors(candidate) {
471
+ if (!isJsonSchemaObject(candidate) || indexed.has(candidate)) return;
472
+ indexed.add(candidate);
473
+ for (const keyword of ["$anchor", "$dynamicAnchor"]) {
474
+ const anchor = candidate[keyword];
475
+ if (typeof anchor === "string") anchors.set(anchor, candidate);
476
+ }
477
+ for (const keyword of ["$id", "id"]) {
478
+ const id = candidate[keyword];
479
+ if (typeof id === "string" && /^#[^/]+$/.test(id)) {
480
+ anchors.set(id.slice(1), candidate);
481
+ }
482
+ }
483
+ forEachJsonSchemaChild(candidate, indexAnchors);
484
+ }
485
+ indexAnchors(document);
486
+ const visited = /* @__PURE__ */ new Set();
487
+ const visiting = /* @__PURE__ */ new Set();
488
+ function visit(candidate, viaReference) {
489
+ if (typeof candidate === "boolean" || !isJsonSchemaObject(candidate)) {
490
+ return;
491
+ }
492
+ if (visiting.has(candidate)) {
493
+ throw new Error(
494
+ `${subject} contains recursive JSON Schema ${viaReference?.keyword ?? "$ref"} ${JSON.stringify(viaReference?.value ?? "#")}`
495
+ );
496
+ }
497
+ if (visited.has(candidate)) return;
498
+ visiting.add(candidate);
499
+ for (const keyword of ["$ref", "$recursiveRef", "$dynamicRef"]) {
500
+ const reference = candidate[keyword];
501
+ if (typeof reference === "string" && reference.startsWith("#")) {
502
+ const target = resolveLocalJsonSchemaReference(
503
+ document,
504
+ anchors,
505
+ reference
506
+ );
507
+ if (target !== void 0) {
508
+ visit(target, { keyword, value: reference });
509
+ }
510
+ }
511
+ }
512
+ forEachJsonSchemaChild(candidate, visit);
513
+ visiting.delete(candidate);
514
+ visited.add(candidate);
515
+ }
516
+ visit(schema);
517
+ }
205
518
  function summarizeParseIssues(error) {
206
519
  const issues = error?.issues;
207
520
  if (Array.isArray(issues) && issues.length > 0) {
@@ -232,6 +545,56 @@ function enforcePluginCliOutputLimit(result, jsonOutput) {
232
545
  error
233
546
  } : { exitCode: 1, stdout: "", stderr: error.message, error };
234
547
  }
548
+ function adoptHttpRouteResponse(value) {
549
+ if (value instanceof Response) return value;
550
+ if (!isResponseLike(value)) {
551
+ throw new Error("http route handler must return a Response");
552
+ }
553
+ const status = value.status;
554
+ const isNullBodyStatus = status === 101 || status === 204 || status === 205 || status === 304;
555
+ const init = {
556
+ status,
557
+ statusText: typeof value.statusText === "string" ? value.statusText : "",
558
+ headers: new Headers(value.headers)
559
+ };
560
+ if (isNullBodyStatus || value.body === null) {
561
+ return new Response(null, init);
562
+ }
563
+ return new Response(adoptBodyStream(value), init);
564
+ }
565
+ function adoptBodyStream(value) {
566
+ const source = value.body;
567
+ if (!isReadableStreamLike(source)) {
568
+ return new ReadableStream({
569
+ async start(controller) {
570
+ controller.enqueue(new Uint8Array(await value.arrayBuffer()));
571
+ controller.close();
572
+ }
573
+ });
574
+ }
575
+ const reader = source.getReader();
576
+ return new ReadableStream({
577
+ async pull(controller) {
578
+ const { done, value: chunk } = await reader.read();
579
+ if (done) {
580
+ controller.close();
581
+ return;
582
+ }
583
+ controller.enqueue(chunk);
584
+ },
585
+ async cancel(reason) {
586
+ await reader.cancel(reason);
587
+ }
588
+ });
589
+ }
590
+ function isReadableStreamLike(value) {
591
+ return value !== null && typeof value === "object" && typeof value.getReader === "function";
592
+ }
593
+ function isResponseLike(value) {
594
+ if (value === null || typeof value !== "object") return false;
595
+ const candidate = value;
596
+ return typeof candidate.status === "number" && typeof candidate.headers === "object" && candidate.headers !== null && typeof candidate.arrayBuffer === "function" && typeof candidate.clone === "function";
597
+ }
235
598
  export {
236
599
  AGENT_TOOL_NAME_PATTERN,
237
600
  BACKGROUND_NAME_PATTERN,
@@ -245,10 +608,17 @@ export {
245
608
  PLUGIN_AGENT_TOOL_PARAMETERS_MAX_BYTES,
246
609
  PLUGIN_HTTP_METHODS,
247
610
  PLUGIN_MENTION_TRIGGER_VALUES,
611
+ PLUGIN_PROVIDER_COMPOSER_ACTION_VALUES,
612
+ PLUGIN_PROVIDER_DISPLAY_NAME_MAX_CHARS,
613
+ PLUGIN_PROVIDER_PERMISSION_MODE_VALUES,
614
+ PLUGIN_PROVIDER_REASONING_LEVEL_VALUES,
615
+ PROVIDER_ID_PATTERN,
248
616
  RESERVED_AGENT_TOOL_NAMES,
249
617
  RESERVED_BB_CLI_COMMANDS,
250
618
  RPC_METHOD_PATTERN,
251
619
  SETTING_KEY_PATTERN,
620
+ adoptHttpRouteResponse,
621
+ assertNoRecursiveJsonSchemaReferences,
252
622
  enforcePluginCliOutputLimit,
253
623
  isPluginMentionTrigger,
254
624
  isStandardSchema,
@@ -257,5 +627,6 @@ export {
257
627
  readRpcMethodContract,
258
628
  registerSettingDescriptors,
259
629
  summarizeParseIssues,
630
+ validatePluginProviderDeclaration,
260
631
  validateSettingsUpdate
261
632
  };