@get-bb/plugin-sdk 0.4.15 → 0.4.17

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,18 +5,34 @@ 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 z3 } from "zod";
8
+ import { z as z4 } from "zod";
9
9
 
10
10
  // ../domain/src/plugin-interaction-limits.ts
11
11
  var PLUGIN_INTERACTION_MAX_TITLE_LENGTH = 160;
12
+ var PLUGIN_INTERACTION_MAX_PAYLOAD_BYTES = 64 * 1024;
12
13
 
13
14
  // src/internal/host-policy.ts
14
- import { z as z2 } from "zod";
15
+ import { z as z3 } from "zod";
15
16
 
16
17
  // ../domain/src/plugin-icon.ts
17
18
  function isPluginOwnedIconPath(icon) {
18
19
  return icon.startsWith("./");
19
20
  }
21
+ var PLUGIN_ICON_MAX_BYTES = 32 * 1024;
22
+ var NAMESPACED_GLYPH_PATTERN = /^[a-z0-9-]+\/[a-z0-9][a-z0-9-]*$/u;
23
+ function isNamespacedGlyph(glyph) {
24
+ return NAMESPACED_GLYPH_PATTERN.test(glyph);
25
+ }
26
+ function parseNamespacedGlyph(glyph) {
27
+ if (!isNamespacedGlyph(glyph)) {
28
+ return null;
29
+ }
30
+ const separator = glyph.indexOf("/");
31
+ return {
32
+ pluginId: glyph.slice(0, separator),
33
+ name: glyph.slice(separator + 1)
34
+ };
35
+ }
20
36
 
21
37
  // ../domain/src/plugin-cli.ts
22
38
  var RESERVED_BB_CLI_COMMANDS = [
@@ -38,6 +54,213 @@ import { z } from "zod";
38
54
  var PROVIDER_FORK_VALUES = ["none", "tip", "checkpoint"];
39
55
  var providerForkSchema = z.enum(PROVIDER_FORK_VALUES);
40
56
 
57
+ // ../domain/src/provider-skill-roots.ts
58
+ function isAbsoluteProviderSkillRootPath(value) {
59
+ if (value.length === 0) {
60
+ return false;
61
+ }
62
+ const normalized = value.replaceAll("\\", "/");
63
+ const drive = /^[a-zA-Z]:\//u.exec(normalized);
64
+ const rest = drive ? normalized.slice(drive[0].length) : normalized.slice(1);
65
+ if (!drive && !normalized.startsWith("/")) {
66
+ return false;
67
+ }
68
+ return rest.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..");
69
+ }
70
+ function isRelativeProviderSkillRootPath(value) {
71
+ if (value.length === 0) {
72
+ return false;
73
+ }
74
+ const normalized = value.replaceAll("\\", "/");
75
+ return !normalized.startsWith("/") && !/^[a-zA-Z]:\//u.test(normalized) && normalized.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..");
76
+ }
77
+
78
+ // ../domain/src/native-roots.ts
79
+ import { z as z2 } from "zod";
80
+ var PROVIDER_NATIVE_ROOTS_MAX = 32;
81
+ var PROVIDER_NATIVE_ROOT_NAME_PREFIX_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,62}:$/u;
82
+ var nativeRootNamePrefixSchema = z2.string().refine(
83
+ (value) => value === "" || PROVIDER_NATIVE_ROOT_NAME_PREFIX_PATTERN.test(value),
84
+ "A root name prefix is a plugin-name-like token ending in ':'"
85
+ );
86
+ var nativeRootManifestPathSchema = z2.string().min(1).refine(
87
+ isRelativeProviderSkillRootPath,
88
+ "A manifest marker is a relative path without dot segments"
89
+ );
90
+ var relativeNativeRootPathSchema = z2.string().min(1).refine(
91
+ isRelativeProviderSkillRootPath,
92
+ "Roots must be relative paths without dot segments"
93
+ );
94
+ var absoluteNativeRootPathSchema = z2.string().min(1).refine(
95
+ isAbsoluteProviderSkillRootPath,
96
+ "Absolute roots must be absolute paths without dot segments"
97
+ );
98
+ var providerNativeRootInputSchema = z2.union([
99
+ z2.string().min(1),
100
+ z2.object({
101
+ path: z2.string().min(1),
102
+ /** Skills nest in subdirectories (the agent scans recursively). */
103
+ recursive: z2.boolean().optional(),
104
+ /**
105
+ * Scan the same relative directory in every ancestor of the workspace
106
+ * up to the repository root (`project` roots only).
107
+ */
108
+ ancestors: z2.boolean().optional(),
109
+ /**
110
+ * Prepended to every name under the root, a vendor plugin's
111
+ * `plugin-name:`; a prefixed root is listed as a plugin root.
112
+ */
113
+ namePrefix: nativeRootNamePrefixSchema.optional(),
114
+ /**
115
+ * A file, relative to a skill directory under this root, that marks the
116
+ * directory as a vendor plugin rather than a skill (Claude's
117
+ * `.claude-plugin/plugin.json`): bb skips such a directory. The plugin
118
+ * that knows the vendor layout declares it; core names no vendor path.
119
+ */
120
+ skipIfManifest: nativeRootManifestPathSchema.optional()
121
+ }).strict()
122
+ ]);
123
+ var providerNativeRootsInputSchema = z2.object({
124
+ user: z2.array(providerNativeRootInputSchema).optional(),
125
+ project: z2.array(providerNativeRootInputSchema).optional()
126
+ }).strict();
127
+ var providerNativeRootSchema = z2.object({
128
+ path: z2.string().min(1),
129
+ recursive: z2.boolean(),
130
+ ancestors: z2.boolean(),
131
+ namePrefix: nativeRootNamePrefixSchema,
132
+ /** Absent: every skill-shaped directory under the root is a skill. */
133
+ skipIfManifest: nativeRootManifestPathSchema.optional()
134
+ }).strict();
135
+ function uniqueByPath(roots) {
136
+ return new Set(roots.map((root) => root.path)).size === roots.length;
137
+ }
138
+ function nativeRootSideSchema(side) {
139
+ return z2.array(
140
+ providerNativeRootSchema.extend({ path: relativeNativeRootPathSchema }).superRefine((root, context) => {
141
+ if (root.ancestors && side !== "project") {
142
+ context.addIssue({
143
+ code: "custom",
144
+ message: "Only project roots may walk ancestors"
145
+ });
146
+ }
147
+ })
148
+ ).max(PROVIDER_NATIVE_ROOTS_MAX).refine(uniqueByPath, "Roots must not repeat a path");
149
+ }
150
+ var providerNativeRootsSchema = z2.object({
151
+ user: nativeRootSideSchema("user"),
152
+ project: nativeRootSideSchema("project")
153
+ }).strict();
154
+ var EMPTY_PROVIDER_NATIVE_ROOTS = Object.freeze({
155
+ user: Object.freeze([]),
156
+ project: Object.freeze([])
157
+ });
158
+ function normalizeProviderNativeRoot(entry) {
159
+ if (typeof entry === "string") {
160
+ return { path: entry, recursive: false, ancestors: false, namePrefix: "" };
161
+ }
162
+ return {
163
+ path: entry.path,
164
+ recursive: entry.recursive ?? false,
165
+ ancestors: entry.ancestors ?? false,
166
+ namePrefix: entry.namePrefix ?? "",
167
+ ...entry.skipIfManifest === void 0 ? {} : { skipIfManifest: entry.skipIfManifest }
168
+ };
169
+ }
170
+ function normalizeProviderNativeRoots(roots) {
171
+ return {
172
+ user: (roots?.user ?? []).map(normalizeProviderNativeRoot),
173
+ project: (roots?.project ?? []).map(normalizeProviderNativeRoot)
174
+ };
175
+ }
176
+ var providerResolvedNativeRootShapeSchema = z2.enum([
177
+ "skills",
178
+ "skill",
179
+ "skill-file",
180
+ "commands",
181
+ "command-file"
182
+ ]);
183
+ var resolvedNativeRootFieldsSchema = z2.object({
184
+ path: absoluteNativeRootPathSchema,
185
+ origin: z2.enum(["user", "project"]),
186
+ recursive: z2.boolean(),
187
+ /** Only with origin `project`, for a path inside the workspace. */
188
+ ancestors: z2.boolean(),
189
+ namePrefix: nativeRootNamePrefixSchema,
190
+ shape: providerResolvedNativeRootShapeSchema,
191
+ /**
192
+ * `skill-file` only: the skill name when the file's frontmatter names
193
+ * none. A vendor plugin's root SKILL.md takes the plugin's name; absent
194
+ * means the parent directory's name.
195
+ */
196
+ fallbackName: z2.string().min(1).optional(),
197
+ /**
198
+ * `skills` only: a file, relative to a skill directory under this root,
199
+ * that marks the directory as a vendor plugin rather than a skill; the
200
+ * daemon skips such a directory (see the declared root's `skipIfManifest`).
201
+ */
202
+ skipIfManifest: nativeRootManifestPathSchema.optional()
203
+ }).strict();
204
+ var providerResolvedNativeRootSchema = resolvedNativeRootFieldsSchema.superRefine((root, context) => {
205
+ if (root.skipIfManifest !== void 0 && root.shape !== "skills") {
206
+ context.addIssue({
207
+ code: "custom",
208
+ message: "Only a skills root carries a manifest marker"
209
+ });
210
+ }
211
+ if (root.ancestors && root.origin !== "project") {
212
+ context.addIssue({
213
+ code: "custom",
214
+ message: "Only project roots may walk ancestors"
215
+ });
216
+ }
217
+ if (root.fallbackName !== void 0 && root.shape !== "skill-file") {
218
+ context.addIssue({
219
+ code: "custom",
220
+ message: "Only a skill-file root carries a fallback name"
221
+ });
222
+ }
223
+ });
224
+ var providerResolvedNativeRootInputSchema = resolvedNativeRootFieldsSchema.partial({
225
+ recursive: true,
226
+ ancestors: true,
227
+ namePrefix: true,
228
+ shape: true
229
+ });
230
+ var resolvedSkillShapes = /* @__PURE__ */ new Set([
231
+ "skills",
232
+ "skill",
233
+ "skill-file"
234
+ ]);
235
+ var resolvedCommandShapes = /* @__PURE__ */ new Set([
236
+ "commands",
237
+ "command-file"
238
+ ]);
239
+ var PROVIDER_RESOLVED_NATIVE_ROOTS_MAX = 256;
240
+ var providerResolvedNativeRootsSchema = z2.object({
241
+ skills: z2.array(
242
+ providerResolvedNativeRootSchema.refine(
243
+ (root) => resolvedSkillShapes.has(root.shape),
244
+ "A skills root needs a skill shape"
245
+ )
246
+ ).max(PROVIDER_RESOLVED_NATIVE_ROOTS_MAX),
247
+ commands: z2.array(
248
+ providerResolvedNativeRootSchema.refine(
249
+ (root) => resolvedCommandShapes.has(root.shape),
250
+ "A commands root needs a command shape"
251
+ )
252
+ ).max(PROVIDER_RESOLVED_NATIVE_ROOTS_MAX)
253
+ }).strict();
254
+ var EMPTY_PROVIDER_RESOLVED_NATIVE_ROOTS = Object.freeze({
255
+ skills: Object.freeze([]),
256
+ commands: Object.freeze([])
257
+ });
258
+ var providerNativeRootSetSchema = z2.object({
259
+ skills: providerNativeRootsSchema,
260
+ commands: providerNativeRootsSchema,
261
+ resolved: providerResolvedNativeRootsSchema
262
+ }).strict();
263
+
41
264
  // src/backend-contract.ts
42
265
  var PLUGIN_CLI_OUTPUT_MAX_BYTES = 1024 * 1024;
43
266
 
@@ -69,31 +292,38 @@ var PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,63}$/;
69
292
  var PLUGIN_PROVIDER_BRIDGE_OPTIONS_MAX_BYTES = 64 * 1024;
70
293
  var SETTING_KEY_PATTERN = /^[a-zA-Z0-9_-]+$/;
71
294
  var settingsBaseFields = {
72
- label: z2.string().min(1),
73
- description: z2.string().min(1).optional()
295
+ label: z3.string().min(1),
296
+ description: z3.string().min(1).optional()
74
297
  };
75
- var settingDescriptorSchema = z2.discriminatedUnion("type", [
76
- z2.object({
77
- type: z2.literal("string"),
298
+ var settingDescriptorSchema = z3.discriminatedUnion("type", [
299
+ z3.object({
300
+ type: z3.literal("string"),
78
301
  ...settingsBaseFields,
79
- secret: z2.literal(true).optional(),
80
- default: z2.string().optional()
81
- }).strict(),
82
- z2.object({
83
- type: z2.literal("boolean"),
302
+ secret: z3.literal(true).optional(),
303
+ experimental_multiline: z3.boolean().optional(),
304
+ default: z3.string().optional()
305
+ }).strict().refine(
306
+ (descriptor) => !(descriptor.secret === true && descriptor.experimental_multiline === true),
307
+ {
308
+ message: "a secret setting cannot be experimental_multiline",
309
+ path: ["experimental_multiline"]
310
+ }
311
+ ),
312
+ z3.object({
313
+ type: z3.literal("boolean"),
84
314
  ...settingsBaseFields,
85
- default: z2.boolean().optional()
315
+ default: z3.boolean().optional()
86
316
  }).strict(),
87
- z2.object({
88
- type: z2.literal("select"),
317
+ z3.object({
318
+ type: z3.literal("select"),
89
319
  ...settingsBaseFields,
90
- options: z2.array(z2.string().min(1)).min(1),
91
- default: z2.string().optional()
320
+ options: z3.array(z3.string().min(1)).min(1),
321
+ default: z3.string().optional()
92
322
  }).strict(),
93
- z2.object({
94
- type: z2.literal("project"),
323
+ z3.object({
324
+ type: z3.literal("project"),
95
325
  ...settingsBaseFields,
96
- default: z2.string().optional()
326
+ default: z3.string().optional()
97
327
  }).strict()
98
328
  ]);
99
329
  function registerSettingDescriptors(target, added) {
@@ -347,7 +577,7 @@ function requireNonBlankString(args) {
347
577
  function validateProviderStrings(providerId, value) {
348
578
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
349
579
  throw new Error(
350
- `provider "${providerId}" experimental_strings must be an object`
580
+ `provider "${providerId}" strings must be an object`
351
581
  );
352
582
  }
353
583
  const record = Object.fromEntries(
@@ -355,12 +585,12 @@ function validateProviderStrings(providerId, value) {
355
585
  );
356
586
  const required = (field) => requireNonBlankString({
357
587
  providerId,
358
- field: `experimental_strings.${field}`,
588
+ field: `strings.${field}`,
359
589
  value: record[field]
360
590
  });
361
591
  const optional = (field) => record[field] === void 0 ? void 0 : requireNonBlankString({
362
592
  providerId,
363
- field: `experimental_strings.${field}`,
593
+ field: `strings.${field}`,
364
594
  value: record[field]
365
595
  });
366
596
  let iconTint;
@@ -368,7 +598,7 @@ function validateProviderStrings(providerId, value) {
368
598
  const tint = record.iconTint;
369
599
  if (typeof tint !== "object" || tint === null || Array.isArray(tint)) {
370
600
  throw new Error(
371
- `provider "${providerId}" experimental_strings.iconTint must be { light, dark }`
601
+ `provider "${providerId}" strings.iconTint must be { light, dark }`
372
602
  );
373
603
  }
374
604
  const tintRecord = Object.fromEntries(
@@ -377,12 +607,12 @@ function validateProviderStrings(providerId, value) {
377
607
  iconTint = Object.freeze({
378
608
  light: requireNonBlankString({
379
609
  providerId,
380
- field: "experimental_strings.iconTint.light",
610
+ field: "strings.iconTint.light",
381
611
  value: tintRecord.light
382
612
  }),
383
613
  dark: requireNonBlankString({
384
614
  providerId,
385
- field: "experimental_strings.iconTint.dark",
615
+ field: "strings.iconTint.dark",
386
616
  value: tintRecord.dark
387
617
  })
388
618
  });
@@ -449,42 +679,42 @@ function validateProviderOptionDescriptors(args) {
449
679
  function validateProviderExtensionKinds(providerId, value) {
450
680
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
451
681
  throw new Error(
452
- `provider "${providerId}" experimental_extensionKinds must be an object keyed by kind name`
682
+ `provider "${providerId}" extensionKinds must be an object keyed by kind name`
453
683
  );
454
684
  }
455
685
  const entries = Object.entries(value);
456
686
  if (entries.length > PROVIDER_EXTENSION_KINDS_MAX) {
457
687
  throw new Error(
458
- `provider "${providerId}" experimental_extensionKinds declares more than ${PROVIDER_EXTENSION_KINDS_MAX} kinds`
688
+ `provider "${providerId}" extensionKinds declares more than ${PROVIDER_EXTENSION_KINDS_MAX} kinds`
459
689
  );
460
690
  }
461
691
  const normalized = {};
462
692
  for (const [name, declaration] of entries) {
463
693
  if (!PROVIDER_EXTENSION_KIND_NAME_PATTERN.test(name)) {
464
694
  throw new Error(
465
- `provider "${providerId}" experimental_extensionKinds name ${JSON.stringify(name)} must match ${PROVIDER_EXTENSION_KIND_NAME_PATTERN}`
695
+ `provider "${providerId}" extensionKinds name ${JSON.stringify(name)} must match ${PROVIDER_EXTENSION_KIND_NAME_PATTERN}`
466
696
  );
467
697
  }
468
698
  if (typeof declaration !== "object" || declaration === null || Array.isArray(declaration)) {
469
699
  throw new Error(
470
- `provider "${providerId}" experimental_extensionKinds.${name} must be { item?, state? }`
700
+ `provider "${providerId}" extensionKinds.${name} must be { item?, state? }`
471
701
  );
472
702
  }
473
703
  const item = Reflect.get(declaration, "item");
474
704
  const state = Reflect.get(declaration, "state");
475
705
  if (item === void 0 && state === void 0) {
476
706
  throw new Error(
477
- `provider "${providerId}" experimental_extensionKinds.${name} must declare an item schema, a state schema, or both`
707
+ `provider "${providerId}" extensionKinds.${name} must declare an item schema, a state schema, or both`
478
708
  );
479
709
  }
480
710
  if (item !== void 0 && !isStandardSchema(item)) {
481
711
  throw new Error(
482
- `provider "${providerId}" experimental_extensionKinds.${name}.item must be a Standard Schema v1 validator`
712
+ `provider "${providerId}" extensionKinds.${name}.item must be a Standard Schema v1 validator`
483
713
  );
484
714
  }
485
715
  if (state !== void 0 && !isStandardSchema(state)) {
486
716
  throw new Error(
487
- `provider "${providerId}" experimental_extensionKinds.${name}.state must be a Standard Schema v1 validator`
717
+ `provider "${providerId}" extensionKinds.${name}.state must be a Standard Schema v1 validator`
488
718
  );
489
719
  }
490
720
  normalized[name] = Object.freeze({
@@ -500,57 +730,98 @@ var PROVIDER_ENV_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/u;
500
730
  function validateProviderEnvPassthrough(providerId, value) {
501
731
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
502
732
  throw new Error(
503
- `provider "${providerId}" experimental_env must be { passthrough: [...] }`
733
+ `provider "${providerId}" env must be { passthrough: [...] }`
504
734
  );
505
735
  }
506
736
  const passthrough = Reflect.get(value, "passthrough");
507
737
  if (!Array.isArray(passthrough)) {
508
738
  throw new Error(
509
- `provider "${providerId}" experimental_env.passthrough must be an array of variable names`
739
+ `provider "${providerId}" env.passthrough must be an array of variable names`
510
740
  );
511
741
  }
512
742
  if (passthrough.length > PROVIDER_ENV_PASSTHROUGH_MAX) {
513
743
  throw new Error(
514
- `provider "${providerId}" experimental_env.passthrough names more than ${PROVIDER_ENV_PASSTHROUGH_MAX} variables`
744
+ `provider "${providerId}" env.passthrough names more than ${PROVIDER_ENV_PASSTHROUGH_MAX} variables`
515
745
  );
516
746
  }
517
747
  const seen = /* @__PURE__ */ new Set();
518
748
  for (const name of passthrough) {
519
749
  if (typeof name !== "string" || !PROVIDER_ENV_NAME_PATTERN.test(name)) {
520
750
  throw new Error(
521
- `provider "${providerId}" experimental_env.passthrough entries must match ${PROVIDER_ENV_NAME_PATTERN}`
751
+ `provider "${providerId}" env.passthrough entries must match ${PROVIDER_ENV_NAME_PATTERN}`
522
752
  );
523
753
  }
524
754
  if (seen.has(name)) {
525
755
  throw new Error(
526
- `provider "${providerId}" experimental_env.passthrough repeats ${JSON.stringify(name)}`
756
+ `provider "${providerId}" env.passthrough repeats ${JSON.stringify(name)}`
527
757
  );
528
758
  }
529
759
  seen.add(name);
530
760
  }
531
761
  return Object.freeze([...seen]);
532
762
  }
763
+ function validateProviderNativeRoots(providerId, field, value) {
764
+ const input = providerNativeRootsInputSchema.safeParse(value);
765
+ if (!input.success) {
766
+ const issue = input.error.issues[0];
767
+ const where = issue?.path.length ? `.${issue.path.join(".")}` : "";
768
+ throw new Error(
769
+ `provider "${providerId}" ${field}${where} ${issue?.message ?? "is invalid"}`
770
+ );
771
+ }
772
+ const normalized = normalizeProviderNativeRoots(input.data);
773
+ const wire = providerNativeRootsSchema.safeParse(normalized);
774
+ if (!wire.success) {
775
+ const issue = wire.error.issues[0];
776
+ const where = issue?.path.length ? `.${issue.path.join(".")}` : "";
777
+ throw new Error(
778
+ `provider "${providerId}" ${field}${where} ${issue?.message ?? "is invalid"}`
779
+ );
780
+ }
781
+ return Object.freeze({
782
+ user: Object.freeze(wire.data.user.map((root) => Object.freeze(root))),
783
+ project: Object.freeze(wire.data.project.map((root) => Object.freeze(root)))
784
+ });
785
+ }
786
+ var PROVIDER_MODEL_CATALOG_SCOPES = [
787
+ "host",
788
+ "workspace"
789
+ ];
790
+ function validateProviderModelCatalogScope(providerId, value) {
791
+ if (value === void 0) {
792
+ return "workspace";
793
+ }
794
+ if (typeof value !== "string" || !PROVIDER_MODEL_CATALOG_SCOPES.includes(value)) {
795
+ throw new Error(
796
+ `provider "${providerId}" models.scope must be one of ${PROVIDER_MODEL_CATALOG_SCOPES.join(", ")}`
797
+ );
798
+ }
799
+ return value;
800
+ }
533
801
  function validateProviderFallbackModels(providerId, value) {
534
802
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
535
803
  throw new Error(
536
- `provider "${providerId}" experimental_models must be { fallback: [...] }`
804
+ `provider "${providerId}" models must be an object`
537
805
  );
538
806
  }
539
807
  const fallback = Reflect.get(value, "fallback");
808
+ if (fallback === void 0) {
809
+ return void 0;
810
+ }
540
811
  if (!Array.isArray(fallback)) {
541
812
  throw new Error(
542
- `provider "${providerId}" experimental_models.fallback must be an array`
813
+ `provider "${providerId}" models.fallback must be an array`
543
814
  );
544
815
  }
545
816
  if (fallback.length > PROVIDER_FALLBACK_MODELS_MAX) {
546
817
  throw new Error(
547
- `provider "${providerId}" experimental_models.fallback lists more than ${PROVIDER_FALLBACK_MODELS_MAX} models`
818
+ `provider "${providerId}" models.fallback lists more than ${PROVIDER_FALLBACK_MODELS_MAX} models`
548
819
  );
549
820
  }
550
821
  const seen = /* @__PURE__ */ new Set();
551
822
  let defaults = 0;
552
823
  const normalized = fallback.map((entry, index) => {
553
- const field = `experimental_models.fallback[${index}]`;
824
+ const field = `models.fallback[${index}]`;
554
825
  if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
555
826
  throw new Error(`provider "${providerId}" ${field} must be an object`);
556
827
  }
@@ -564,7 +835,7 @@ function validateProviderFallbackModels(providerId, value) {
564
835
  });
565
836
  if (seen.has(id)) {
566
837
  throw new Error(
567
- `provider "${providerId}" experimental_models.fallback id ${JSON.stringify(id)} is duplicated`
838
+ `provider "${providerId}" models.fallback id ${JSON.stringify(id)} is duplicated`
568
839
  );
569
840
  }
570
841
  seen.add(id);
@@ -640,11 +911,153 @@ function validateProviderFallbackModels(providerId, value) {
640
911
  });
641
912
  if (normalized.length > 0 && defaults !== 1) {
642
913
  throw new Error(
643
- `provider "${providerId}" experimental_models.fallback must mark exactly one model isDefault (found ${defaults})`
914
+ `provider "${providerId}" models.fallback must mark exactly one model isDefault (found ${defaults})`
644
915
  );
645
916
  }
646
917
  return Object.freeze(normalized);
647
918
  }
919
+ var AI_SERVICE_KINDS = /* @__PURE__ */ new Set(["inference", "voice"]);
920
+ var SERVER_DIRECT_AI_SERVICE_IDS = Object.freeze([
921
+ "openai",
922
+ "amazon-bedrock",
923
+ "ant-ling",
924
+ "anthropic",
925
+ "azure-openai-responses",
926
+ "baseten",
927
+ "cerebras",
928
+ "cloudflare-ai-gateway",
929
+ "cloudflare-workers-ai",
930
+ "deepseek",
931
+ "fireworks",
932
+ "github-copilot",
933
+ "google",
934
+ "google-vertex",
935
+ "groq",
936
+ "huggingface",
937
+ "kimi-coding",
938
+ "minimax",
939
+ "minimax-cn",
940
+ "mistral",
941
+ "moonshotai",
942
+ "moonshotai-cn",
943
+ "nvidia",
944
+ "openai-codex",
945
+ "opencode",
946
+ "opencode-go",
947
+ "openrouter",
948
+ "qwen-token-plan",
949
+ "qwen-token-plan-cn",
950
+ "radius",
951
+ "together",
952
+ "vercel-ai-gateway",
953
+ "xai",
954
+ "xiaomi",
955
+ "xiaomi-token-plan-ams",
956
+ "xiaomi-token-plan-cn",
957
+ "xiaomi-token-plan-sgp",
958
+ "zai",
959
+ "zai-coding-cn"
960
+ ]);
961
+ function validatePluginAiServiceDeclaration(declaration) {
962
+ if (typeof declaration !== "object" || declaration === null) {
963
+ throw new Error("AI service declaration must be an object");
964
+ }
965
+ const id = declaration.id;
966
+ if (typeof id !== "string" || !PROVIDER_ID_PATTERN.test(id)) {
967
+ throw new Error(
968
+ `invalid AI service id ${JSON.stringify(id)} \u2014 use 2-64 lowercase letters, digits, and "-", starting with a letter or digit`
969
+ );
970
+ }
971
+ const displayName = typeof declaration.displayName === "string" ? declaration.displayName.trim() : "";
972
+ if (displayName.length === 0 || displayName.length > 64) {
973
+ throw new Error(
974
+ `AI service "${id}" displayName must be 1-64 characters`
975
+ );
976
+ }
977
+ const kinds = declaration.kinds;
978
+ if (!Array.isArray(kinds) || kinds.length === 0) {
979
+ throw new Error(`AI service "${id}" must declare at least one kind`);
980
+ }
981
+ const seen = /* @__PURE__ */ new Set();
982
+ for (const kind of kinds) {
983
+ if (typeof kind !== "string" || !AI_SERVICE_KINDS.has(kind)) {
984
+ throw new Error(
985
+ `AI service "${id}" kind ${JSON.stringify(kind)} is not one of: ${[...AI_SERVICE_KINDS].join(", ")}`
986
+ );
987
+ }
988
+ if (seen.has(kind)) {
989
+ throw new Error(`AI service "${id}" declares kind "${kind}" twice`);
990
+ }
991
+ seen.add(kind);
992
+ }
993
+ return Object.freeze({
994
+ id,
995
+ displayName,
996
+ kinds: Object.freeze([...seen])
997
+ });
998
+ }
999
+ function assertAiServiceRegistrable(args) {
1000
+ if (SERVER_DIRECT_AI_SERVICE_IDS.includes(args.id)) {
1001
+ throw new Error(
1002
+ `AI service id "${args.id}" is reserved: the server serves it directly, so a plugin cannot register it`
1003
+ );
1004
+ }
1005
+ if (args.hostArtifact !== null) {
1006
+ return { artifact: args.hostArtifact, problem: null };
1007
+ }
1008
+ if (args.hostArtifactProblem !== null) {
1009
+ return { artifact: null, problem: args.hostArtifactProblem };
1010
+ }
1011
+ throw new Error(
1012
+ `AI service "${args.id}" needs a bb.host entry to run on: this plugin declares none`
1013
+ );
1014
+ }
1015
+ function aiServiceAlreadyRegisteredMessage(id) {
1016
+ return `AI service "${id}" is already registered; a plugin cannot shadow an existing service.`;
1017
+ }
1018
+ function providerAlreadyRegisteredMessage(id) {
1019
+ return `Provider "${id}" is already registered; a plugin cannot shadow an existing provider.`;
1020
+ }
1021
+ var RENAMED_PROVIDER_DECLARATION_FIELDS = Object.freeze({
1022
+ experimental_family: "family",
1023
+ experimental_strings: "strings",
1024
+ experimental_serviceTiers: "serviceTiers",
1025
+ experimental_reasoningLevels: "reasoningLevels",
1026
+ experimental_extensionKinds: "extensionKinds",
1027
+ experimental_models: "models",
1028
+ experimental_env: "env",
1029
+ experimental_deriveProviderOptions: "deriveProviderOptions"
1030
+ });
1031
+ var MOVED_PROVIDER_CAPABILITY_FIELDS = Object.freeze({
1032
+ experimental_providerHealth: "maintenance.health",
1033
+ experimental_providerUsage: "maintenance.usage",
1034
+ experimental_providerInstallation: "maintenance.installation"
1035
+ });
1036
+ var READ_EXPERIMENTAL_PROVIDER_DECLARATION_FIELDS = /* @__PURE__ */ new Set([
1037
+ "experimental_bridgeOptions",
1038
+ "experimental_visibility",
1039
+ "experimental_nativeSkillRoots",
1040
+ "experimental_nativeCommandRoots",
1041
+ "experimental_resolvesNativeRoots"
1042
+ ]);
1043
+ var RENAMED_PROVIDER_FIELDS_SDK_VERSION = "0.4.16";
1044
+ function rejectStaleExperimentalFields(args) {
1045
+ for (const key of Object.keys(args.value)) {
1046
+ if (!key.startsWith("experimental_") || args.read.has(key)) {
1047
+ continue;
1048
+ }
1049
+ const replacement = Object.hasOwn(args.renamed, key) ? args.renamed[key] : void 0;
1050
+ const field = `${args.scope}${key}`;
1051
+ if (replacement === void 0) {
1052
+ throw new Error(
1053
+ `provider "${args.providerId}": unknown declaration field "${field}"`
1054
+ );
1055
+ }
1056
+ throw new Error(
1057
+ `provider "${args.providerId}": "${field}" was ${args.verb} to "${replacement}" in SDK ${RENAMED_PROVIDER_FIELDS_SDK_VERSION}`
1058
+ );
1059
+ }
1060
+ }
648
1061
  function validatePluginProviderDeclaration(declaration) {
649
1062
  if (typeof declaration !== "object" || declaration === null) {
650
1063
  throw new Error("provider declaration must be an object");
@@ -655,10 +1068,18 @@ function validatePluginProviderDeclaration(declaration) {
655
1068
  `invalid provider id ${JSON.stringify(id)} \u2014 use 2-64 lowercase letters, digits, and "-", starting with a letter or digit`
656
1069
  );
657
1070
  }
658
- const family = declaration.experimental_family;
1071
+ rejectStaleExperimentalFields({
1072
+ providerId: id,
1073
+ value: declaration,
1074
+ scope: "",
1075
+ read: READ_EXPERIMENTAL_PROVIDER_DECLARATION_FIELDS,
1076
+ renamed: RENAMED_PROVIDER_DECLARATION_FIELDS,
1077
+ verb: "renamed"
1078
+ });
1079
+ const family = declaration.family;
659
1080
  if (family !== void 0 && (typeof family !== "string" || !PROVIDER_ID_PATTERN.test(family))) {
660
1081
  throw new Error(
661
- `provider "${id}" experimental_family must use the provider id grammar (2-64 lowercase letters, digits, and "-")`
1082
+ `provider "${id}" family must use the provider id grammar (2-64 lowercase letters, digits, and "-")`
662
1083
  );
663
1084
  }
664
1085
  const displayName = typeof declaration.displayName === "string" ? declaration.displayName.trim() : "";
@@ -671,14 +1092,16 @@ function validatePluginProviderDeclaration(declaration) {
671
1092
  if (declaration.icon !== void 0) {
672
1093
  if (typeof declaration.icon !== "string" || declaration.icon.trim() === "") {
673
1094
  throw new Error(
674
- `provider "${id}" icon must be a non-blank string \u2014 a named host glyph ("Zap") or a plugin-relative path ("./icons/agent.svg")`
1095
+ `provider "${id}" icon must be a non-blank string \u2014 a named host glyph ("Zap"), a plugin-relative path ("./icons/agent.svg"), or a declared icon ("<pluginId>/<name>")`
675
1096
  );
676
1097
  }
677
1098
  if (isPluginOwnedIconPath(declaration.icon)) {
678
1099
  icon = validateProviderRelativePath(declaration.icon, `"${id}" icon`);
1100
+ } else if (isNamespacedGlyph(declaration.icon)) {
1101
+ icon = declaration.icon;
679
1102
  } else if (/[/\\]/u.test(declaration.icon)) {
680
1103
  throw new Error(
681
- `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"`
1104
+ `provider "${id}" icon looks like a path but does not start with "./" \u2014 use "./icons/agent.svg" for a plugin file, "<pluginId>/<name>" for a declared icon, or a bare host glyph name like "Zap"`
682
1105
  );
683
1106
  } else {
684
1107
  icon = declaration.icon;
@@ -688,24 +1111,29 @@ function validatePluginProviderDeclaration(declaration) {
688
1111
  if (typeof capabilities !== "object" || capabilities === null) {
689
1112
  throw new Error(`provider "${id}" capabilities must be an object`);
690
1113
  }
691
- const experimentalProviderHealth = capabilities.experimental_providerHealth ?? false;
692
- const experimentalProviderUsage = capabilities.experimental_providerUsage ?? false;
693
- const experimentalProviderInstallation = capabilities.experimental_providerInstallation ?? false;
694
- if (typeof experimentalProviderHealth !== "boolean") {
695
- throw new Error(
696
- `provider "${id}" capabilities.experimental_providerHealth must be a boolean`
697
- );
698
- }
699
- if (typeof experimentalProviderUsage !== "boolean") {
700
- throw new Error(
701
- `provider "${id}" capabilities.experimental_providerUsage must be a boolean`
702
- );
1114
+ rejectStaleExperimentalFields({
1115
+ providerId: id,
1116
+ value: capabilities,
1117
+ scope: "capabilities.",
1118
+ read: /* @__PURE__ */ new Set(),
1119
+ renamed: MOVED_PROVIDER_CAPABILITY_FIELDS,
1120
+ verb: "moved"
1121
+ });
1122
+ const maintenance = declaration.maintenance ?? {};
1123
+ if (typeof maintenance !== "object" || maintenance === null) {
1124
+ throw new Error(`provider "${id}" maintenance must be an object`);
703
1125
  }
704
- if (typeof experimentalProviderInstallation !== "boolean") {
705
- throw new Error(
706
- `provider "${id}" capabilities.experimental_providerInstallation must be a boolean`
707
- );
1126
+ for (const key of ["health", "usage", "installation"]) {
1127
+ const value = maintenance[key];
1128
+ if (value !== void 0 && typeof value !== "boolean") {
1129
+ throw new Error(`provider "${id}" maintenance.${key} must be a boolean`);
1130
+ }
708
1131
  }
1132
+ const normalizedMaintenance = Object.freeze({
1133
+ health: maintenance.health ?? false,
1134
+ usage: maintenance.usage ?? false,
1135
+ installation: maintenance.installation ?? false
1136
+ });
709
1137
  const booleanCapabilityFields = [
710
1138
  "supportsServiceTier",
711
1139
  "supportsNativeUserQuestion",
@@ -726,9 +1154,6 @@ function validatePluginProviderDeclaration(declaration) {
726
1154
  );
727
1155
  }
728
1156
  const normalizedCapabilities = Object.freeze({
729
- experimental_providerHealth: experimentalProviderHealth,
730
- experimental_providerUsage: experimentalProviderUsage,
731
- experimental_providerInstallation: experimentalProviderInstallation,
732
1157
  supportsServiceTier: capabilities.supportsServiceTier,
733
1158
  supportsNativeUserQuestion: capabilities.supportsNativeUserQuestion,
734
1159
  fork: capabilities.fork,
@@ -767,50 +1192,77 @@ function validatePluginProviderDeclaration(declaration) {
767
1192
  `provider "${id}" experimental_visibility must be "always" or "installed"`
768
1193
  );
769
1194
  }
770
- if (visibility === "installed" && !normalizedCapabilities.experimental_providerHealth) {
1195
+ if (visibility === "installed" && !normalizedMaintenance.health) {
771
1196
  throw new Error(
772
- `provider "${id}" experimental_visibility "installed" requires experimental_providerHealth`
1197
+ `provider "${id}" experimental_visibility "installed" requires maintenance.health`
773
1198
  );
774
1199
  }
775
- const strings = declaration.experimental_strings === void 0 ? void 0 : validateProviderStrings(id, declaration.experimental_strings);
776
- const serviceTiers = declaration.experimental_serviceTiers === void 0 ? void 0 : validateProviderOptionDescriptors({
1200
+ const strings = declaration.strings === void 0 ? void 0 : validateProviderStrings(id, declaration.strings);
1201
+ const serviceTiers = declaration.serviceTiers === void 0 ? void 0 : validateProviderOptionDescriptors({
777
1202
  providerId: id,
778
- field: "experimental_serviceTiers",
779
- value: declaration.experimental_serviceTiers
1203
+ field: "serviceTiers",
1204
+ value: declaration.serviceTiers
780
1205
  });
781
- const reasoningLevels = declaration.experimental_reasoningLevels === void 0 ? void 0 : validateProviderOptionDescriptors({
1206
+ const reasoningLevels = declaration.reasoningLevels === void 0 ? void 0 : validateProviderOptionDescriptors({
782
1207
  providerId: id,
783
- field: "experimental_reasoningLevels",
784
- value: declaration.experimental_reasoningLevels
1208
+ field: "reasoningLevels",
1209
+ value: declaration.reasoningLevels
785
1210
  });
786
- const extensionKinds = declaration.experimental_extensionKinds === void 0 ? void 0 : validateProviderExtensionKinds(
1211
+ const extensionKinds = declaration.extensionKinds === void 0 ? void 0 : validateProviderExtensionKinds(
1212
+ id,
1213
+ declaration.extensionKinds
1214
+ );
1215
+ const fallbackModels = declaration.models === void 0 ? void 0 : validateProviderFallbackModels(id, declaration.models);
1216
+ const modelCatalogScope = validateProviderModelCatalogScope(
1217
+ id,
1218
+ declaration.models?.scope
1219
+ );
1220
+ const envPassthrough = declaration.env === void 0 ? void 0 : validateProviderEnvPassthrough(id, declaration.env);
1221
+ const nativeSkillRoots = declaration.experimental_nativeSkillRoots === void 0 ? void 0 : validateProviderNativeRoots(
787
1222
  id,
788
- declaration.experimental_extensionKinds
1223
+ "experimental_nativeSkillRoots",
1224
+ declaration.experimental_nativeSkillRoots
789
1225
  );
790
- const fallbackModels = declaration.experimental_models === void 0 ? void 0 : validateProviderFallbackModels(id, declaration.experimental_models);
791
- const envPassthrough = declaration.experimental_env === void 0 ? void 0 : validateProviderEnvPassthrough(id, declaration.experimental_env);
792
- const deriveProviderOptions = declaration.experimental_deriveProviderOptions;
1226
+ const nativeCommandRoots = declaration.experimental_nativeCommandRoots === void 0 ? void 0 : validateProviderNativeRoots(
1227
+ id,
1228
+ "experimental_nativeCommandRoots",
1229
+ declaration.experimental_nativeCommandRoots
1230
+ );
1231
+ const resolvesNativeRoots = declaration.experimental_resolvesNativeRoots;
1232
+ if (resolvesNativeRoots !== void 0 && typeof resolvesNativeRoots !== "boolean") {
1233
+ throw new Error(
1234
+ `provider "${id}" experimental_resolvesNativeRoots must be a boolean`
1235
+ );
1236
+ }
1237
+ const deriveProviderOptions = declaration.deriveProviderOptions;
793
1238
  if (deriveProviderOptions !== void 0 && typeof deriveProviderOptions !== "function") {
794
1239
  throw new Error(
795
- `provider "${id}" experimental_deriveProviderOptions must be a function (context) => providerOptions`
1240
+ `provider "${id}" deriveProviderOptions must be a function (context) => providerOptions`
796
1241
  );
797
1242
  }
798
1243
  return Object.freeze({
799
1244
  id,
800
1245
  displayName,
801
- ...family === void 0 ? {} : { experimental_family: family },
1246
+ ...family === void 0 ? {} : { family },
802
1247
  ...icon === void 0 ? {} : { icon },
803
1248
  ...bridgeOptions === void 0 ? {} : { experimental_bridgeOptions: bridgeOptions },
804
1249
  experimental_visibility: visibility,
1250
+ maintenance: normalizedMaintenance,
805
1251
  capabilities: normalizedCapabilities,
806
1252
  composerActions,
807
- ...strings === void 0 ? {} : { experimental_strings: strings },
808
- ...serviceTiers === void 0 ? {} : { experimental_serviceTiers: serviceTiers },
809
- ...reasoningLevels === void 0 ? {} : { experimental_reasoningLevels: reasoningLevels },
810
- ...extensionKinds === void 0 ? {} : { experimental_extensionKinds: extensionKinds },
811
- ...fallbackModels === void 0 ? {} : { experimental_models: Object.freeze({ fallback: fallbackModels }) },
812
- ...envPassthrough === void 0 ? {} : { experimental_env: Object.freeze({ passthrough: envPassthrough }) },
813
- ...deriveProviderOptions === void 0 ? {} : { experimental_deriveProviderOptions: deriveProviderOptions }
1253
+ ...strings === void 0 ? {} : { strings },
1254
+ ...serviceTiers === void 0 ? {} : { serviceTiers },
1255
+ ...reasoningLevels === void 0 ? {} : { reasoningLevels },
1256
+ ...extensionKinds === void 0 ? {} : { extensionKinds },
1257
+ models: Object.freeze({
1258
+ ...fallbackModels === void 0 ? {} : { fallback: fallbackModels },
1259
+ scope: modelCatalogScope
1260
+ }),
1261
+ ...envPassthrough === void 0 ? {} : { env: Object.freeze({ passthrough: envPassthrough }) },
1262
+ ...nativeSkillRoots === void 0 ? {} : { experimental_nativeSkillRoots: nativeSkillRoots },
1263
+ ...nativeCommandRoots === void 0 ? {} : { experimental_nativeCommandRoots: nativeCommandRoots },
1264
+ experimental_resolvesNativeRoots: resolvesNativeRoots ?? false,
1265
+ ...deriveProviderOptions === void 0 ? {} : { deriveProviderOptions }
814
1266
  });
815
1267
  }
816
1268
  function isStandardSchema(value) {
@@ -978,6 +1430,90 @@ function assertNoRecursiveJsonSchemaReferences(schema, subject) {
978
1430
  }
979
1431
  visit(schema);
980
1432
  }
1433
+ var RENAMED_AGENT_TOOL_FIELDS = /* @__PURE__ */ new Map([
1434
+ [
1435
+ "experimental_presentation",
1436
+ '"experimental_presentation" was renamed to "presentation" in SDK 0.4.16'
1437
+ ],
1438
+ [
1439
+ "experimental_statusLabels",
1440
+ '"experimental_statusLabels" was folded into "presentation" (labels) in SDK 0.4.16'
1441
+ ]
1442
+ ]);
1443
+ function rejectStaleAgentToolFields(toolName, tool) {
1444
+ const unknownKeys = [];
1445
+ for (const key of Object.keys(tool).sort()) {
1446
+ const renamed = RENAMED_AGENT_TOOL_FIELDS.get(key);
1447
+ if (renamed !== void 0) {
1448
+ throw new Error(`registerTool: ${renamed} (tool "${toolName}")`);
1449
+ }
1450
+ if (key.startsWith("experimental_")) {
1451
+ unknownKeys.push(key);
1452
+ }
1453
+ }
1454
+ if (unknownKeys.length > 0) {
1455
+ throw new Error(
1456
+ `registerTool: tool "${toolName}" contains unknown field${unknownKeys.length === 1 ? "" : "s"}: ${unknownKeys.join(", ")}`
1457
+ );
1458
+ }
1459
+ }
1460
+ function parsePluginAgentToolPresentation(toolName, value) {
1461
+ if (value === void 0) {
1462
+ return null;
1463
+ }
1464
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1465
+ throw new Error(
1466
+ `tool "${toolName}" presentation must be an object`
1467
+ );
1468
+ }
1469
+ const declared = value;
1470
+ const presentation = {};
1471
+ if (declared.label !== void 0) {
1472
+ const label = declared.label;
1473
+ if (typeof label !== "object" || label === null || typeof label.pending !== "string" || typeof label.completed !== "string") {
1474
+ throw new Error(
1475
+ `tool "${toolName}" presentation.label must provide pending and completed strings`
1476
+ );
1477
+ }
1478
+ const { pending, completed } = label;
1479
+ if (pending.trim().length === 0 || completed.trim().length === 0 || pending.length > PLUGIN_AGENT_STATUS_LABEL_MAX_CHARS || completed.length > PLUGIN_AGENT_STATUS_LABEL_MAX_CHARS) {
1480
+ throw new Error(
1481
+ `tool "${toolName}" presentation.label strings must be non-empty and at most ${PLUGIN_AGENT_STATUS_LABEL_MAX_CHARS} characters`
1482
+ );
1483
+ }
1484
+ presentation.label = { pending, completed };
1485
+ }
1486
+ if (declared.icon !== void 0) {
1487
+ const icon = declared.icon;
1488
+ if (typeof icon !== "object" || icon === null || typeof icon.glyph !== "string" || icon.glyph.trim().length === 0) {
1489
+ throw new Error(
1490
+ `tool "${toolName}" presentation.icon must be { glyph: string }`
1491
+ );
1492
+ }
1493
+ presentation.icon = { glyph: icon.glyph };
1494
+ }
1495
+ if (declared.suppress !== void 0) {
1496
+ if (typeof declared.suppress !== "boolean") {
1497
+ throw new Error(
1498
+ `tool "${toolName}" presentation.suppress must be a boolean`
1499
+ );
1500
+ }
1501
+ presentation.suppress = declared.suppress;
1502
+ }
1503
+ if (declared.tint !== void 0) {
1504
+ const tint = declared.tint;
1505
+ if (typeof tint !== "object" || tint === null || typeof tint.light !== "string" || typeof tint.dark !== "string") {
1506
+ throw new Error(
1507
+ `tool "${toolName}" presentation.tint must provide light and dark strings`
1508
+ );
1509
+ }
1510
+ presentation.tint = {
1511
+ light: tint.light,
1512
+ dark: tint.dark
1513
+ };
1514
+ }
1515
+ return presentation;
1516
+ }
981
1517
  function summarizeParseIssues(error) {
982
1518
  const issues = error?.issues;
983
1519
  if (Array.isArray(issues) && issues.length > 0) {
@@ -1058,6 +1594,25 @@ function isResponseLike(value) {
1058
1594
  const candidate = value;
1059
1595
  return typeof candidate.status === "number" && typeof candidate.headers === "object" && candidate.headers !== null && typeof candidate.arrayBuffer === "function" && typeof candidate.clone === "function";
1060
1596
  }
1597
+ function undeclaredIconProblem(pluginId, declaredIconNames, glyph) {
1598
+ const parsed = parseNamespacedGlyph(glyph);
1599
+ if (parsed === null) {
1600
+ return null;
1601
+ }
1602
+ if (parsed.pluginId !== pluginId || !declaredIconNames.has(parsed.name)) {
1603
+ return `"${glyph}" is not an icon declared by plugin "${pluginId}"`;
1604
+ }
1605
+ return null;
1606
+ }
1607
+ function providerIconRefusalMessage(providerId, problem) {
1608
+ return `provider "${providerId}" icon ${problem}`;
1609
+ }
1610
+ function agentToolIconRefusalMessage(toolName, problem) {
1611
+ return `tool "${toolName}" presentation.icon ${problem}`;
1612
+ }
1613
+ function providerWithoutBridgeMessage(providerId) {
1614
+ return `provider "${providerId}" has no bridge to run on: this plugin declares no "bb.host" entry in its manifest`;
1615
+ }
1061
1616
 
1062
1617
  // src/testing/fake-sdk.ts
1063
1618
  function withSpawnAttribution(pluginId, args) {
@@ -1442,6 +1997,7 @@ function createFakePluginHostInternal(options, sharedState) {
1442
1997
  )
1443
1998
  };
1444
1999
  const pluginId = options.pluginId ?? "test-plugin";
2000
+ const declaredIconNames = new Set(options.experimental_declaredIconNames ?? []);
1445
2001
  const agentSkillIds = [...options.agentSkillIds ?? []];
1446
2002
  if (new Set(agentSkillIds).size !== agentSkillIds.length) {
1447
2003
  throw new Error("agentSkillIds must not contain duplicates");
@@ -1753,10 +2309,15 @@ function createFakePluginHostInternal(options, sharedState) {
1753
2309
  function registerProviderDeclaration(declaration) {
1754
2310
  assertLive();
1755
2311
  const normalized = validatePluginProviderDeclaration(declaration);
2312
+ const iconProblem = normalized.icon === void 0 ? null : undeclaredIconProblem(pluginId, declaredIconNames, normalized.icon);
2313
+ if (iconProblem !== null) {
2314
+ throw new Error(providerIconRefusalMessage(normalized.id, iconProblem));
2315
+ }
2316
+ if (options.experimental_hostEntry === false) {
2317
+ throw new Error(providerWithoutBridgeMessage(normalized.id));
2318
+ }
1756
2319
  if (providerRegistrations.some((existing) => existing.id === normalized.id)) {
1757
- throw new Error(
1758
- `Provider "${normalized.id}" is already registered; a plugin cannot shadow an existing provider.`
1759
- );
2320
+ throw new Error(providerAlreadyRegisteredMessage(normalized.id));
1760
2321
  }
1761
2322
  providerRegistrations.push(normalized);
1762
2323
  let disposed2 = false;
@@ -1769,6 +2330,31 @@ function createFakePluginHostInternal(options, sharedState) {
1769
2330
  disposeHooks.push(dispose);
1770
2331
  return { dispose };
1771
2332
  }
2333
+ const aiServiceRegistrations = [];
2334
+ const experimental_aiServices = {
2335
+ register(declaration) {
2336
+ assertLive();
2337
+ const normalized = validatePluginAiServiceDeclaration(declaration);
2338
+ assertAiServiceRegistrable({
2339
+ id: normalized.id,
2340
+ hostArtifact: options.experimental_hostEntry === false ? null : "declared",
2341
+ hostArtifactProblem: null
2342
+ });
2343
+ if (aiServiceRegistrations.some((existing) => existing.id === normalized.id)) {
2344
+ throw new Error(aiServiceAlreadyRegisteredMessage(normalized.id));
2345
+ }
2346
+ aiServiceRegistrations.push(normalized);
2347
+ let disposed2 = false;
2348
+ const dispose = () => {
2349
+ if (disposed2) return;
2350
+ disposed2 = true;
2351
+ const index = aiServiceRegistrations.indexOf(normalized);
2352
+ if (index !== -1) aiServiceRegistrations.splice(index, 1);
2353
+ };
2354
+ disposeHooks.push(dispose);
2355
+ return { dispose };
2356
+ }
2357
+ };
1772
2358
  const agents = {
1773
2359
  configure(provider) {
1774
2360
  assertLive();
@@ -1794,9 +2380,6 @@ function createFakePluginHostInternal(options, sharedState) {
1794
2380
  }
1795
2381
  instructionProvider = provider;
1796
2382
  },
1797
- experimental_registerProvider(declaration) {
1798
- return registerProviderDeclaration(declaration);
1799
- },
1800
2383
  registerTool(tool) {
1801
2384
  assertLive();
1802
2385
  const name = tool?.name;
@@ -1810,6 +2393,7 @@ function createFakePluginHostInternal(options, sharedState) {
1810
2393
  `tool name "${name}" is a built-in bb tool \u2014 pick another name`
1811
2394
  );
1812
2395
  }
2396
+ rejectStaleAgentToolFields(name, tool);
1813
2397
  if (typeof tool.description !== "string" || tool.description.trim().length === 0) {
1814
2398
  throw new Error(`tool "${name}" must provide a description`);
1815
2399
  }
@@ -1821,16 +2405,19 @@ function createFakePluginHostInternal(options, sharedState) {
1821
2405
  `tool "${name}" instructions exceed the ${PLUGIN_AGENT_STATIC_INSTRUCTIONS_MAX_CHARS}-character limit`
1822
2406
  );
1823
2407
  }
1824
- const experimentalStatusLabels = tool.experimental_statusLabels;
1825
- if (experimentalStatusLabels !== void 0 && (typeof experimentalStatusLabels !== "object" || experimentalStatusLabels === null || typeof experimentalStatusLabels.pending !== "string" || typeof experimentalStatusLabels.completed !== "string" || experimentalStatusLabels.pending.trim().length === 0 || experimentalStatusLabels.completed.trim().length === 0)) {
1826
- throw new Error(
1827
- `tool "${name}" experimental_statusLabels must provide non-empty pending and completed strings`
1828
- );
1829
- }
1830
- if (experimentalStatusLabels !== void 0 && (experimentalStatusLabels.pending.length > PLUGIN_AGENT_STATUS_LABEL_MAX_CHARS || experimentalStatusLabels.completed.length > PLUGIN_AGENT_STATUS_LABEL_MAX_CHARS)) {
1831
- throw new Error(
1832
- `tool "${name}" experimental_statusLabels exceed the ${PLUGIN_AGENT_STATUS_LABEL_MAX_CHARS}-character limit`
2408
+ const presentation = parsePluginAgentToolPresentation(
2409
+ name,
2410
+ tool.presentation
2411
+ );
2412
+ if (presentation?.icon !== void 0) {
2413
+ const problem = undeclaredIconProblem(
2414
+ pluginId,
2415
+ declaredIconNames,
2416
+ presentation.icon.glyph
1833
2417
  );
2418
+ if (problem !== null) {
2419
+ throw new Error(agentToolIconRefusalMessage(name, problem));
2420
+ }
1834
2421
  }
1835
2422
  if (typeof tool.execute !== "function") {
1836
2423
  throw new Error(
@@ -1842,7 +2429,7 @@ function createFakePluginHostInternal(options, sharedState) {
1842
2429
  let parse;
1843
2430
  if (isZodSchemaLike(parameters)) {
1844
2431
  try {
1845
- inputSchema = z3.toJSONSchema(parameters, {
2432
+ inputSchema = z4.toJSONSchema(parameters, {
1846
2433
  io: "input"
1847
2434
  });
1848
2435
  } catch (error) {
@@ -1876,10 +2463,7 @@ function createFakePluginHostInternal(options, sharedState) {
1876
2463
  const record = {
1877
2464
  name,
1878
2465
  description: tool.description,
1879
- experimentalStatusLabels: experimentalStatusLabels === void 0 ? null : {
1880
- pending: experimentalStatusLabels.pending,
1881
- completed: experimentalStatusLabels.completed
1882
- },
2466
+ presentation,
1883
2467
  instructions: tool.instructions !== void 0 && tool.instructions.trim().length > 0 ? tool.instructions : null,
1884
2468
  inputSchema,
1885
2469
  parse,
@@ -1937,10 +2521,15 @@ function createFakePluginHostInternal(options, sharedState) {
1937
2521
  }
1938
2522
  };
1939
2523
  const loopbackBaseUrl = options.loopbackBaseUrl ?? "http://127.0.0.1:38886";
2524
+ const dataDir = options.dataDir ?? "/tmp/bb-fake-data-dir";
1940
2525
  const server = {
1941
2526
  get loopbackBaseUrl() {
1942
2527
  assertLive();
1943
2528
  return loopbackBaseUrl;
2529
+ },
2530
+ get experimental_dataDir() {
2531
+ assertLive();
2532
+ return dataDir;
1944
2533
  }
1945
2534
  };
1946
2535
  const { sdk, harness: sdkHarness } = createFakeSdk({
@@ -2004,24 +2593,24 @@ function createFakePluginHostInternal(options, sharedState) {
2004
2593
  timeoutMs
2005
2594
  };
2006
2595
  const id = `fake-interaction-${nextInteractionId++}`;
2007
- return new Promise((resolve) => {
2596
+ return new Promise((resolve2) => {
2008
2597
  const settleAborted = () => {
2009
2598
  const pending = pendingInteractions.get(id);
2010
2599
  if (!pending) return;
2011
2600
  clearTimeout(pending.timer);
2012
2601
  pendingInteractions.delete(id);
2013
- resolve({ outcome: "cancelled", reason: "request-aborted" });
2602
+ resolve2({ outcome: "cancelled", reason: "request-aborted" });
2014
2603
  };
2015
2604
  requestOptions?.signal?.addEventListener("abort", settleAborted, {
2016
2605
  once: true
2017
2606
  });
2018
2607
  const timer = setTimeout(() => {
2019
2608
  pendingInteractions.delete(id);
2020
- resolve({ outcome: "cancelled", reason: "timeout" });
2609
+ resolve2({ outcome: "cancelled", reason: "timeout" });
2021
2610
  }, timeoutMs);
2022
2611
  pendingInteractions.set(id, {
2023
2612
  request: normalizedRequest,
2024
- resolve,
2613
+ resolve: resolve2,
2025
2614
  timer
2026
2615
  });
2027
2616
  });
@@ -2188,6 +2777,7 @@ function createFakePluginHostInternal(options, sharedState) {
2188
2777
  status,
2189
2778
  server,
2190
2779
  hosts,
2780
+ experimental_aiServices,
2191
2781
  get sdk() {
2192
2782
  assertLive();
2193
2783
  return sdk;
@@ -2273,7 +2863,8 @@ function createFakePluginHostInternal(options, sharedState) {
2273
2863
  };
2274
2864
  },
2275
2865
  mentionProviders,
2276
- providerRegistrations
2866
+ providerRegistrations,
2867
+ aiServiceRegistrations
2277
2868
  },
2278
2869
  get pendingInteractions() {
2279
2870
  return [...pendingInteractions].map(([id, pending]) => ({
@@ -2580,9 +3171,106 @@ function makeThreadResponse(overrides = {}) {
2580
3171
  ...overrides
2581
3172
  };
2582
3173
  }
3174
+
3175
+ // src/testing/public-sdk-only.ts
3176
+ import { readdirSync, readFileSync } from "node:fs";
3177
+ import { dirname, isAbsolute, join as join2, relative, resolve, sep } from "node:path";
3178
+ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist"]);
3179
+ var SOURCE_EXTENSIONS = /\.(?:[cm]?[jt]s|tsx)$/u;
3180
+ var TEST_FILE_PATTERN = /\.test\.[cm]?[jt]sx?$/u;
3181
+ var PRIVATE_PACKAGE_PREFIX = "@bb/";
3182
+ var PLUGIN_IMPORT_ALLOWLIST = [
3183
+ /^@get-bb\/plugin-sdk$/u,
3184
+ /^@get-bb\/plugin-sdk\/(?:host|app|ai-services|provider-bridge|provider-bridge\/acp)$/u,
3185
+ /^zod$/u,
3186
+ /^node:/u,
3187
+ /^\.\.?\//u
3188
+ ];
3189
+ var TEST_IMPORT_ALLOWLIST = [
3190
+ /^@get-bb\/plugin-sdk\/(?:testing|testing\/app|testing\/host|provider-bridge\/testing)$/u,
3191
+ /^vitest$/u
3192
+ ];
3193
+ var IMPORT_SPECIFIER_PATTERN = /(?:\bfrom\s*|\bimport\s*\(?\s*|\brequire\s*\(\s*)["']([^"']+)["']/gu;
3194
+ var DYNAMIC_SPECIFIER_PATTERN = /\b(?:import|require)\s*\(\s*(?!["'])([^)]+)\)/gu;
3195
+ var RELATIVE_SPECIFIER_PATTERN = /^\.\.?\//u;
3196
+ function listSourceFiles(directory) {
3197
+ const files = [];
3198
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
3199
+ if (entry.isDirectory()) {
3200
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) {
3201
+ files.push(...listSourceFiles(join2(directory, entry.name)));
3202
+ }
3203
+ continue;
3204
+ }
3205
+ if (SOURCE_EXTENSIONS.test(entry.name)) {
3206
+ files.push(join2(directory, entry.name));
3207
+ }
3208
+ }
3209
+ return files;
3210
+ }
3211
+ function importSpecifiers(source) {
3212
+ return [...source.matchAll(IMPORT_SPECIFIER_PATTERN)].map(
3213
+ (match) => match[1] ?? ""
3214
+ );
3215
+ }
3216
+ function dynamicSpecifiers(source) {
3217
+ return [...source.matchAll(DYNAMIC_SPECIFIER_PATTERN)].map(
3218
+ (match) => (match[1] ?? "").trim()
3219
+ );
3220
+ }
3221
+ function escapesPackage(packageRoot, file, specifier) {
3222
+ const target = relative(packageRoot, resolve(dirname(file), specifier));
3223
+ return isAbsolute(target) || target === ".." || target.startsWith(`..${sep}`);
3224
+ }
3225
+ function declaredDependencyNames(packageRoot) {
3226
+ const manifest = JSON.parse(
3227
+ readFileSync(join2(packageRoot, "package.json"), "utf8")
3228
+ );
3229
+ const names = [];
3230
+ for (const field of ["dependencies", "devDependencies"]) {
3231
+ const block = typeof manifest === "object" && manifest !== null && field in manifest ? manifest[field] : void 0;
3232
+ if (typeof block === "object" && block !== null) {
3233
+ names.push(...Object.keys(block));
3234
+ }
3235
+ }
3236
+ return names;
3237
+ }
3238
+ function scanPublicSdkOnly(packageRoot, options = {}) {
3239
+ const extra = options.allow ?? [];
3240
+ const pluginAllowlist = [...PLUGIN_IMPORT_ALLOWLIST, ...extra];
3241
+ const testAllowlist = [...pluginAllowlist, ...TEST_IMPORT_ALLOWLIST];
3242
+ const files = [];
3243
+ const violations = [];
3244
+ for (const path of listSourceFiles(packageRoot)) {
3245
+ const file = relative(packageRoot, path);
3246
+ files.push(file);
3247
+ const allowlist = TEST_FILE_PATTERN.test(path) ? testAllowlist : pluginAllowlist;
3248
+ const source = readFileSync(path, "utf8");
3249
+ for (const specifier of importSpecifiers(source)) {
3250
+ if (specifier.startsWith(PRIVATE_PACKAGE_PREFIX)) {
3251
+ violations.push({ file, specifier, reason: "private-package" });
3252
+ } else if (RELATIVE_SPECIFIER_PATTERN.test(specifier) && escapesPackage(packageRoot, path, specifier) && !extra.some((pattern) => pattern.test(specifier))) {
3253
+ violations.push({ file, specifier, reason: "outside-package" });
3254
+ } else if (!allowlist.some((pattern) => pattern.test(specifier))) {
3255
+ violations.push({ file, specifier, reason: "outside-allowlist" });
3256
+ }
3257
+ }
3258
+ for (const specifier of dynamicSpecifiers(source)) {
3259
+ violations.push({ file, specifier, reason: "dynamic-specifier" });
3260
+ }
3261
+ }
3262
+ return {
3263
+ files,
3264
+ violations,
3265
+ privateDependencies: declaredDependencyNames(packageRoot).filter(
3266
+ (name) => name.startsWith(PRIVATE_PACKAGE_PREFIX)
3267
+ )
3268
+ };
3269
+ }
2583
3270
  export {
2584
3271
  PluginContextStaleError,
2585
3272
  createFakePluginHost,
2586
3273
  createFakeSdk,
3274
+ scanPublicSdkOnly as experimental_scanPublicSdkOnly,
2587
3275
  makeThreadResponse
2588
3276
  };