@get-bb/plugin-sdk 0.4.84 → 0.4.88

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.
@@ -1,4 +1,5 @@
1
1
  // src/internal/host-policy.ts
2
+ import { createHash } from "node:crypto";
2
3
  import { z as z4 } from "zod";
3
4
 
4
5
  // ../domain/src/plugin-icon.ts
@@ -44,6 +45,10 @@ var RESERVED_BB_CLI_COMMANDS = [
44
45
  "voice"
45
46
  ];
46
47
 
48
+ // ../domain/src/plugin-interaction-limits.ts
49
+ var PLUGIN_INTERACTION_MAX_TITLE_LENGTH = 160;
50
+ var PLUGIN_INTERACTION_MAX_PAYLOAD_BYTES = 64 * 1024;
51
+
47
52
  // ../domain/src/provider-fork.ts
48
53
  import { z } from "zod";
49
54
  var PROVIDER_FORK_VALUES = ["none", "tip", "checkpoint"];
@@ -273,9 +278,50 @@ var providerNativeRootSetSchema = z3.object({
273
278
  resolved: providerResolvedNativeRootsSchema
274
279
  }).strict();
275
280
 
276
- // ../domain/src/plugin-interaction-limits.ts
277
- var PLUGIN_INTERACTION_MAX_TITLE_LENGTH = 160;
278
- var PLUGIN_INTERACTION_MAX_PAYLOAD_BYTES = 64 * 1024;
281
+ // ../domain/src/plugin-metadata.ts
282
+ var PLUGIN_METADATA_MAX_BYTES = 256 * 1024;
283
+ function exceedsPluginMetadataLimit(metadataJson) {
284
+ return new TextEncoder().encode(metadataJson).byteLength > PLUGIN_METADATA_MAX_BYTES;
285
+ }
286
+ var pluginMetadataSchema = jsonObjectSchema.superRefine(
287
+ (value, ctx) => {
288
+ if (exceedsPluginMetadataLimit(JSON.stringify(value))) {
289
+ ctx.addIssue({
290
+ code: "custom",
291
+ message: "pluginMetadata exceeds 256 KiB"
292
+ });
293
+ }
294
+ }
295
+ );
296
+ function assertPlainJsonData(value, seen) {
297
+ if (value === null || typeof value !== "object") return;
298
+ if (seen.has(value)) throw new Error("pluginMetadata contains a cycle");
299
+ const array = Array.isArray(value);
300
+ if (Object.getPrototypeOf(value) !== (array ? Array.prototype : Object.prototype)) {
301
+ throw new Error("pluginMetadata must contain plain JSON data");
302
+ }
303
+ seen.add(value);
304
+ for (const child of array ? value : Object.values(value)) {
305
+ assertPlainJsonData(child, seen);
306
+ }
307
+ seen.delete(value);
308
+ }
309
+ function validatePluginMetadata(value) {
310
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
311
+ throw new Error("pluginMetadata must be a plain JSON object");
312
+ }
313
+ assertPlainJsonData(value, /* @__PURE__ */ new Set());
314
+ return pluginMetadataSchema.parse(value);
315
+ }
316
+ function deepFreezeJsonValue(value) {
317
+ if (value === null || typeof value !== "object") return;
318
+ for (const child of Object.values(value)) deepFreezeJsonValue(child);
319
+ Object.freeze(value);
320
+ }
321
+ function deepFreezePluginMetadata(metadata) {
322
+ deepFreezeJsonValue(metadata);
323
+ return metadata;
324
+ }
279
325
 
280
326
  // src/backend-contract.ts
281
327
  var PLUGIN_CLI_OUTPUT_MAX_BYTES = 1024 * 1024;
@@ -518,6 +564,22 @@ function validateSettingsUpdate(descriptors, values) {
518
564
  }
519
565
  return errors;
520
566
  }
567
+ function coerceStoredPluginSettingValue(descriptor, value) {
568
+ let coerced = value;
569
+ if (descriptor.type === "number" && typeof coerced === "string") {
570
+ const legacyNumber = Number(coerced.trim());
571
+ coerced = coerced.trim().length > 0 && Number.isFinite(legacyNumber) ? legacyNumber : void 0;
572
+ }
573
+ if (descriptor.type === "number" && typeof coerced === "number" && !Number.isFinite(coerced)) {
574
+ coerced = void 0;
575
+ }
576
+ const expected = descriptor.type === "boolean" ? "boolean" : descriptor.type === "number" ? "number" : "string";
577
+ if (typeof coerced !== expected) coerced = void 0;
578
+ if (descriptor.type === "select" && typeof coerced === "string" && !descriptor.options.includes(coerced)) {
579
+ coerced = void 0;
580
+ }
581
+ return coerced ?? descriptor.default;
582
+ }
521
583
  var PLUGIN_MENTION_TRIGGER_VALUES = [
522
584
  "@",
523
585
  "#",
@@ -1776,71 +1838,47 @@ var environmentCompositionSchema = z4.object({
1776
1838
  machineProviderId: z4.string().regex(ENVIRONMENT_PROVIDER_ID_PATTERN),
1777
1839
  environmentProviderId: z4.string().regex(ENVIRONMENT_PROVIDER_ID_PATTERN)
1778
1840
  }).strict();
1779
- function validatePluginEnvironmentProviderDeclaration(declaration) {
1841
+ function requireProviderIdAndDisplayName(noun, declaration) {
1780
1842
  if (typeof declaration !== "object" || declaration === null) {
1781
- throw new Error("environment provider declaration must be an object");
1843
+ throw new Error(`${noun} declaration must be an object`);
1782
1844
  }
1783
1845
  const id = declaration.id;
1784
1846
  if (typeof id !== "string" || !ENVIRONMENT_PROVIDER_ID_PATTERN.test(id)) {
1785
1847
  throw new Error(
1786
- `invalid environment provider id ${JSON.stringify(id)} \u2014 use 2-64 lowercase letters, digits, or "-", starting with a letter or digit`
1848
+ `invalid ${noun} id ${JSON.stringify(id)} \u2014 use 2-64 lowercase letters, digits, or "-", starting with a letter or digit`
1787
1849
  );
1788
1850
  }
1789
1851
  const displayName = typeof declaration.displayName === "string" ? declaration.displayName.trim() : "";
1790
1852
  if (displayName.length === 0 || displayName.length > ENVIRONMENT_PROVIDER_DISPLAY_NAME_MAX_CHARS) {
1791
1853
  throw new Error(
1792
- `environment provider "${id}" needs a displayName of 1-${ENVIRONMENT_PROVIDER_DISPLAY_NAME_MAX_CHARS} characters`
1854
+ `${noun} "${id}" needs a displayName of 1-${ENVIRONMENT_PROVIDER_DISPLAY_NAME_MAX_CHARS} characters`
1793
1855
  );
1794
1856
  }
1795
- const description = z4.string().trim().min(1).max(ENVIRONMENT_PROVIDER_DESCRIPTION_MAX_CHARS).optional().transform((value) => value ?? null).parse(declaration.description);
1796
- const icon = z4.string().trim().min(1).optional().transform((value) => value ?? null).parse(declaration.icon);
1797
- if (icon !== null && isPluginOwnedIconPath(icon)) {
1857
+ return { id, displayName };
1858
+ }
1859
+ function assertProviderIconShape(noun, id, icon) {
1860
+ if (isPluginOwnedIconPath(icon)) {
1798
1861
  validateProviderRelativePath(icon, `"${id}" icon`);
1799
- } else if (icon !== null && !isNamespacedGlyph(icon) && /[/\\]/u.test(icon)) {
1800
- throw new Error(
1801
- `environment provider "${id}" icon must be a glyph, declared icon, or plugin-relative path`
1802
- );
1803
- }
1804
- const requires = normalizeEnvironmentProviderRequirements(id, declaration);
1805
- const inputs = normalizeEnvironmentProviderInputs(id, declaration);
1806
- if (typeof declaration.create !== "function" || typeof declaration.remove !== "function") {
1807
- throw new Error(
1808
- `environment provider "${id}" must declare create and remove functions`
1809
- );
1810
- }
1811
- if (declaration.validate !== void 0 && typeof declaration.validate !== "function") {
1862
+ } else if (!isNamespacedGlyph(icon) && /[/\\]/u.test(icon)) {
1812
1863
  throw new Error(
1813
- `environment provider "${id}" declares a validate that is not a function`
1864
+ `${noun} "${id}" icon must be a glyph, declared icon, or plugin-relative path`
1814
1865
  );
1815
1866
  }
1816
- if (declaration.availability !== void 0 && typeof declaration.availability !== "function") {
1867
+ }
1868
+ function assertOptionalFunction(noun, id, value, phrase) {
1869
+ if (value !== void 0 && typeof value !== "function") {
1817
1870
  throw new Error(
1818
- `environment provider "${id}" declares availability that is not a function`
1871
+ `${noun} "${id}" declares ${phrase} that is not a function`
1819
1872
  );
1820
1873
  }
1821
- return {
1822
- id,
1823
- displayName,
1824
- description,
1825
- icon,
1826
- requires,
1827
- inputs: inputs === null ? null : inputs.schema,
1828
- inputsJsonSchema: inputs === null ? null : inputs.jsonSchema,
1829
- availability: declaration.availability ?? null,
1830
- validate: declaration.validate ?? null,
1831
- create: declaration.create,
1832
- remove: declaration.remove,
1833
- policy: environmentProviderPolicySchema.parse(declaration.policy ?? {})
1834
- };
1835
1874
  }
1836
- function normalizeEnvironmentProviderInputs(id, declaration) {
1837
- const inputs = declaration.inputs;
1875
+ function normalizeProviderInputs(noun, id, inputs) {
1838
1876
  if (inputs === void 0) {
1839
1877
  return null;
1840
1878
  }
1841
1879
  if (!isStandardSchema(inputs)) {
1842
1880
  throw new Error(
1843
- `environment provider "${id}" declares an inputs that is not a Standard Schema v1 validator`
1881
+ `${noun} "${id}" declares an inputs that is not a Standard Schema v1 validator`
1844
1882
  );
1845
1883
  }
1846
1884
  let converted;
@@ -1848,17 +1886,65 @@ function normalizeEnvironmentProviderInputs(id, declaration) {
1848
1886
  converted = JSON.parse(JSON.stringify(standardSchemaToJsonSchema(inputs)));
1849
1887
  } catch (error) {
1850
1888
  throw new Error(
1851
- `environment provider "${id}" declares an inputs validator that cannot be published as JSON Schema (${error instanceof Error ? error.message : String(error)}) \u2014 declare it with zod 4 or a validator exposing toJSONSchema()`
1889
+ `${noun} "${id}" declares an inputs validator that cannot be published as JSON Schema (${error instanceof Error ? error.message : String(error)}) \u2014 declare it with zod 4 or a validator exposing toJSONSchema()`
1852
1890
  );
1853
1891
  }
1854
1892
  const jsonSchema = jsonValueSchema.safeParse(converted);
1855
1893
  if (!jsonSchema.success) {
1856
1894
  throw new Error(
1857
- `environment provider "${id}" declares an inputs schema whose JSON Schema is not JSON-serializable`
1895
+ `${noun} "${id}" declares an inputs schema whose JSON Schema is not JSON-serializable`
1858
1896
  );
1859
1897
  }
1860
1898
  return { schema: inputs, jsonSchema: jsonSchema.data };
1861
1899
  }
1900
+ function validatePluginEnvironmentProviderDeclaration(declaration) {
1901
+ const { id, displayName } = requireProviderIdAndDisplayName(
1902
+ "environment provider",
1903
+ declaration
1904
+ );
1905
+ const description = z4.string().trim().min(1).max(ENVIRONMENT_PROVIDER_DESCRIPTION_MAX_CHARS).optional().transform((value) => value ?? null).parse(declaration.description);
1906
+ const icon = z4.string().trim().min(1).optional().transform((value) => value ?? null).parse(declaration.icon);
1907
+ if (icon !== null) {
1908
+ assertProviderIconShape("environment provider", id, icon);
1909
+ }
1910
+ const requires = normalizeEnvironmentProviderRequirements(id, declaration);
1911
+ const inputs = normalizeProviderInputs(
1912
+ "environment provider",
1913
+ id,
1914
+ declaration.inputs
1915
+ );
1916
+ if (typeof declaration.create !== "function" || typeof declaration.remove !== "function") {
1917
+ throw new Error(
1918
+ `environment provider "${id}" must declare create and remove functions`
1919
+ );
1920
+ }
1921
+ assertOptionalFunction(
1922
+ "environment provider",
1923
+ id,
1924
+ declaration.validate,
1925
+ "a validate"
1926
+ );
1927
+ assertOptionalFunction(
1928
+ "environment provider",
1929
+ id,
1930
+ declaration.availability,
1931
+ "availability"
1932
+ );
1933
+ return {
1934
+ id,
1935
+ displayName,
1936
+ description,
1937
+ icon,
1938
+ requires,
1939
+ inputs: inputs === null ? null : inputs.schema,
1940
+ inputsJsonSchema: inputs === null ? null : inputs.jsonSchema,
1941
+ availability: declaration.availability ?? null,
1942
+ validate: declaration.validate ?? null,
1943
+ create: declaration.create,
1944
+ remove: declaration.remove,
1945
+ policy: environmentProviderPolicySchema.parse(declaration.policy ?? {})
1946
+ };
1947
+ }
1862
1948
  function normalizeEnvironmentProviderRequirements(id, declaration) {
1863
1949
  const requires = declaration.requires ?? {};
1864
1950
  if (typeof requires !== "object" || requires === null || Array.isArray(requires)) {
@@ -1892,32 +1978,19 @@ var environmentProviderPolicySchema = z4.object({
1892
1978
  }).strict();
1893
1979
  var MACHINE_PROVIDER_DESCRIPTION_MAX_CHARS = 200;
1894
1980
  function validatePluginMachineProviderDeclaration(declaration) {
1895
- if (typeof declaration !== "object" || declaration === null) {
1896
- throw new Error("machine provider declaration must be an object");
1897
- }
1898
- const id = declaration.id;
1899
- if (typeof id !== "string" || !ENVIRONMENT_PROVIDER_ID_PATTERN.test(id)) {
1900
- throw new Error(
1901
- `invalid machine provider id ${JSON.stringify(id)} \u2014 use 2-64 lowercase letters, digits, or "-", starting with a letter or digit`
1902
- );
1903
- }
1904
- const displayName = typeof declaration.displayName === "string" ? declaration.displayName.trim() : "";
1905
- if (displayName.length === 0 || displayName.length > ENVIRONMENT_PROVIDER_DISPLAY_NAME_MAX_CHARS) {
1906
- throw new Error(
1907
- `machine provider "${id}" needs a displayName of 1-${ENVIRONMENT_PROVIDER_DISPLAY_NAME_MAX_CHARS} characters`
1908
- );
1909
- }
1981
+ const { id, displayName } = requireProviderIdAndDisplayName(
1982
+ "machine provider",
1983
+ declaration
1984
+ );
1910
1985
  const description = z4.string().trim().min(1).max(MACHINE_PROVIDER_DESCRIPTION_MAX_CHARS).parse(declaration.description);
1911
1986
  const icon = z4.string().trim().min(1).parse(declaration.icon);
1912
- if (isPluginOwnedIconPath(icon)) {
1913
- validateProviderRelativePath(icon, `"${id}" icon`);
1914
- } else if (!isNamespacedGlyph(icon) && /[/\\]/u.test(icon)) {
1915
- throw new Error(
1916
- `machine provider "${id}" icon must be a glyph, declared icon, or plugin-relative path`
1917
- );
1918
- }
1987
+ assertProviderIconShape("machine provider", id, icon);
1919
1988
  const ephemeral = z4.boolean().default(false).parse(declaration.ephemeral);
1920
- const inputs = normalizeMachineProviderInputs(id, declaration);
1989
+ const inputs = normalizeProviderInputs(
1990
+ "machine provider",
1991
+ id,
1992
+ declaration.inputs
1993
+ );
1921
1994
  if (typeof declaration.create !== "function" || typeof declaration.reconcileCleanup !== "function" || typeof declaration.remove !== "function") {
1922
1995
  throw new Error(
1923
1996
  `machine provider "${id}" must declare create, reconcileCleanup and remove functions`
@@ -1930,16 +2003,18 @@ function validatePluginMachineProviderDeclaration(declaration) {
1930
2003
  `machine provider "${id}" must declare suspend and resume together`
1931
2004
  );
1932
2005
  }
1933
- if (declaration.validate !== void 0 && typeof declaration.validate !== "function") {
1934
- throw new Error(
1935
- `machine provider "${id}" declares a validate that is not a function`
1936
- );
1937
- }
1938
- if (declaration.availability !== void 0 && typeof declaration.availability !== "function") {
1939
- throw new Error(
1940
- `machine provider "${id}" declares availability that is not a function`
1941
- );
1942
- }
2006
+ assertOptionalFunction(
2007
+ "machine provider",
2008
+ id,
2009
+ declaration.validate,
2010
+ "a validate"
2011
+ );
2012
+ assertOptionalFunction(
2013
+ "machine provider",
2014
+ id,
2015
+ declaration.availability,
2016
+ "availability"
2017
+ );
1943
2018
  return {
1944
2019
  id,
1945
2020
  displayName,
@@ -1957,30 +2032,6 @@ function validatePluginMachineProviderDeclaration(declaration) {
1957
2032
  remove: declaration.remove
1958
2033
  };
1959
2034
  }
1960
- function normalizeMachineProviderInputs(id, declaration) {
1961
- const inputs = declaration.inputs;
1962
- if (inputs === void 0) return null;
1963
- if (!isStandardSchema(inputs)) {
1964
- throw new Error(
1965
- `machine provider "${id}" declares an inputs that is not a Standard Schema v1 validator`
1966
- );
1967
- }
1968
- let converted;
1969
- try {
1970
- converted = JSON.parse(JSON.stringify(standardSchemaToJsonSchema(inputs)));
1971
- } catch (error) {
1972
- throw new Error(
1973
- `machine provider "${id}" declares an inputs validator that cannot be published as JSON Schema (${error instanceof Error ? error.message : String(error)}) \u2014 declare it with zod 4 or a validator exposing toJSONSchema()`
1974
- );
1975
- }
1976
- const jsonSchema = jsonValueSchema.safeParse(converted);
1977
- if (!jsonSchema.success) {
1978
- throw new Error(
1979
- `machine provider "${id}" declares an inputs schema whose JSON Schema is not JSON-serializable`
1980
- );
1981
- }
1982
- return { schema: inputs, jsonSchema: jsonSchema.data };
1983
- }
1984
2035
  function validateServerAccessProviderDeclaration(declaration) {
1985
2036
  if (typeof declaration !== "object" || declaration === null)
1986
2037
  throw new Error("Invalid server access provider declaration");
@@ -1990,235 +2041,454 @@ function validateServerAccessProviderDeclaration(declaration) {
1990
2041
  throw new Error("Invalid server access provider declaration");
1991
2042
  return declaration;
1992
2043
  }
1993
-
1994
- // src/testing/fake-plugin-host.ts
1995
- import { createHash } from "node:crypto";
1996
- import { mkdtempSync, rmSync } from "node:fs";
1997
- import { tmpdir } from "node:os";
1998
- import { join } from "node:path";
1999
- import Database from "better-sqlite3";
2000
- import { CronExpressionParser } from "cron-parser";
2001
- import { Hono } from "hono";
2002
-
2003
- // src/testing/fake-sdk.ts
2004
- function withSpawnAttribution(pluginId, args) {
2005
- const [first, ...rest] = args;
2006
- if (typeof first !== "object" || first === null) return args;
2007
- const spawnArgs = first;
2008
- const origin = spawnArgs.origin ?? "plugin";
2009
- return [
2010
- {
2011
- ...spawnArgs,
2012
- origin,
2013
- ...origin === "plugin" ? { originPluginId: spawnArgs.originPluginId ?? pluginId } : {}
2014
- },
2015
- ...rest
2016
- ];
2044
+ var LEGACY_UNKNOWN_MIGRATION_HASH = "legacy-unknown";
2045
+ function migrationStatementHash(statement) {
2046
+ return createHash("sha256").update(statement).digest("hex");
2017
2047
  }
2018
- function createFakeSdk(options) {
2019
- const calls = [];
2020
- const stubs = /* @__PURE__ */ new Map();
2021
- function addOverrides(prefix, value) {
2022
- if (typeof value === "function") {
2023
- stubs.set(prefix, value);
2024
- return;
2025
- }
2026
- if (typeof value !== "object" || value === null) return;
2027
- for (const [key, child] of Object.entries(value)) {
2028
- addOverrides(prefix.length === 0 ? key : `${prefix}.${key}`, child);
2029
- }
2030
- }
2031
- addOverrides("", options.overrides ?? {});
2032
- function invoke(path, rawArgs) {
2033
- const args = path === "threads.spawn" ? withSpawnAttribution(options.pluginId, rawArgs) : rawArgs;
2034
- calls.push({ path, args });
2035
- const stub = stubs.get(path);
2036
- if (!stub) {
2048
+ function runPluginStorageMigrations(database, statements) {
2049
+ database.exec(
2050
+ "CREATE TABLE IF NOT EXISTS _bb_migrations (id INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL, statement_hash TEXT)"
2051
+ );
2052
+ const migrationColumns = database.prepare("PRAGMA table_info(_bb_migrations)").all();
2053
+ if (!migrationColumns.some((column) => column.name === "statement_hash")) {
2054
+ database.exec("ALTER TABLE _bb_migrations ADD COLUMN statement_hash TEXT");
2055
+ }
2056
+ const rows = database.prepare(
2057
+ "SELECT id, statement_hash FROM _bb_migrations ORDER BY id"
2058
+ ).all();
2059
+ const applied = /* @__PURE__ */ new Map();
2060
+ for (const row of rows) applied.set(row.id, row.statement_hash);
2061
+ const statementHashes = statements.map(migrationStatementHash);
2062
+ statementHashes.forEach((statementHash, index) => {
2063
+ const recordedHash = applied.get(index);
2064
+ if (recordedHash !== void 0 && recordedHash !== null && recordedHash !== statementHash) {
2037
2065
  throw new Error(
2038
- `bb.sdk.${path} is not stubbed \u2014 pass an implementation via createFakePluginHost({ sdk: { ... } }) or harness.sdk.stub("${path}", fn)`
2066
+ `migration ${index} does not match the recorded statement; append a new migration instead of changing or reusing an index`
2039
2067
  );
2040
2068
  }
2041
- return stub(...args);
2042
- }
2043
- const nodes = /* @__PURE__ */ new Map();
2044
- function node(path) {
2045
- const cached = nodes.get(path);
2046
- if (cached) return cached;
2047
- const created = new Proxy(function() {
2048
- }, {
2049
- get(_target, prop) {
2050
- if (typeof prop !== "string" || prop === "then") return void 0;
2051
- return node(path === "" ? prop : `${path}.${prop}`);
2052
- },
2053
- apply(_target, _thisArg, args) {
2054
- return invoke(path, args);
2055
- }
2056
- });
2057
- nodes.set(path, created);
2058
- return created;
2059
- }
2060
- const harness = {
2061
- calls,
2062
- callsTo(path) {
2063
- return calls.filter((call) => call.path === path).map((call) => call.args);
2064
- },
2065
- stub(path, implementation) {
2066
- stubs.set(path, implementation);
2069
+ });
2070
+ const adopt = database.prepare(
2071
+ "UPDATE _bb_migrations SET statement_hash = ? WHERE id = ? AND statement_hash IS NULL"
2072
+ );
2073
+ const record = database.prepare(
2074
+ "INSERT INTO _bb_migrations (id, applied_at, statement_hash) VALUES (?, ?, ?)"
2075
+ );
2076
+ database.transaction(() => {
2077
+ for (const row of rows) {
2078
+ if (row.statement_hash !== null) continue;
2079
+ adopt.run(
2080
+ statementHashes[row.id] ?? LEGACY_UNKNOWN_MIGRATION_HASH,
2081
+ row.id
2082
+ );
2067
2083
  }
2068
- };
2069
- return { sdk: node(""), harness };
2084
+ statements.forEach((statement, index) => {
2085
+ if (applied.has(index)) return;
2086
+ database.exec(statement);
2087
+ record.run(index, Date.now(), statementHashes[index]);
2088
+ });
2089
+ })();
2070
2090
  }
2071
-
2072
- // src/testing/fake-plugin-host.ts
2073
- var LEGACY_UNKNOWN_MIGRATION_HASH = "legacy-unknown";
2074
- function migrationStatementHash(statement) {
2075
- return createHash("sha256").update(statement).digest("hex");
2091
+ function normalizeHttpRouteRegistration(method, path, handler, opts, existingRoutes) {
2092
+ const normalizedMethod = String(method).toUpperCase();
2093
+ if (!PLUGIN_HTTP_METHODS.has(normalizedMethod)) {
2094
+ throw new Error(
2095
+ `invalid http method "${String(method)}" \u2014 use one of: ${[...PLUGIN_HTTP_METHODS].join(", ")}`
2096
+ );
2097
+ }
2098
+ if (typeof path !== "string" || !path.startsWith("/")) {
2099
+ throw new Error(
2100
+ `http route path must be a string starting with "/", got ${JSON.stringify(path)}`
2101
+ );
2102
+ }
2103
+ if (typeof handler !== "function") {
2104
+ throw new Error(
2105
+ `http route handler for ${normalizedMethod} ${path} must be a function`
2106
+ );
2107
+ }
2108
+ const auth = opts?.auth ?? "local";
2109
+ if (auth !== "local" && auth !== "token" && auth !== "none") {
2110
+ throw new Error(
2111
+ `invalid auth mode "${String(auth)}" for ${normalizedMethod} ${path} \u2014 use "local", "token", or "none"`
2112
+ );
2113
+ }
2114
+ if (existingRoutes.some(
2115
+ (route) => route.method === normalizedMethod && route.path === path
2116
+ )) {
2117
+ throw new Error(
2118
+ `http route ${normalizedMethod} ${path} is already registered`
2119
+ );
2120
+ }
2121
+ return { method: normalizedMethod, path, auth };
2076
2122
  }
2077
- var PluginContextStaleError = class extends Error {
2078
- constructor(pluginId) {
2079
- super(
2080
- `plugin "${pluginId}" used a stale API handle \u2014 it was reloaded or disabled; re-entry happens via a fresh factory call`
2123
+ function normalizeWebSocketRouteRegistration(path, handler, opts, existingRoutes) {
2124
+ if (typeof path !== "string" || !path.startsWith("/")) {
2125
+ throw new Error(
2126
+ `websocket route path must be a string starting with "/", got ${JSON.stringify(path)}`
2081
2127
  );
2082
- this.name = "PluginContextStaleError";
2083
2128
  }
2084
- };
2085
- function readSettingsValues(descriptors, stored) {
2086
- const values = {};
2087
- for (const [key, descriptor] of Object.entries(descriptors)) {
2088
- let value = stored.get(key);
2089
- if (descriptor.type === "number" && typeof value === "string") {
2090
- const legacyNumber = Number(value.trim());
2091
- value = value.trim().length > 0 && Number.isFinite(legacyNumber) ? legacyNumber : void 0;
2129
+ if (typeof handler !== "function") {
2130
+ throw new Error(`websocket route handler for ${path} must be a function`);
2131
+ }
2132
+ const auth = opts?.auth ?? "local";
2133
+ if (auth !== "local" && auth !== "token" && auth !== "none") {
2134
+ throw new Error(
2135
+ `invalid auth mode "${String(auth)}" for websocket ${path} \u2014 use "local", "token", or "none"`
2136
+ );
2137
+ }
2138
+ if (existingRoutes.some((route) => route.path === path)) {
2139
+ throw new Error(`websocket route ${path} is already registered`);
2140
+ }
2141
+ return { path, auth };
2142
+ }
2143
+ function normalizeRpcRegistration(contract, handlers, registered) {
2144
+ if (typeof contract !== "object" || contract === null || Array.isArray(contract)) {
2145
+ throw new Error("rpc.register contract must be an object");
2146
+ }
2147
+ if (typeof handlers !== "object" || handlers === null || Array.isArray(handlers)) {
2148
+ throw new Error("rpc.register handlers must be an object");
2149
+ }
2150
+ const pending = [];
2151
+ const contractEntries = Object.entries(contract);
2152
+ const contractNames = new Set(contractEntries.map(([name]) => name));
2153
+ for (const extraName of Object.keys(handlers)) {
2154
+ if (!contractNames.has(extraName)) {
2155
+ throw new Error(
2156
+ `rpc handler "${extraName}" has no matching contract method`
2157
+ );
2158
+ }
2159
+ }
2160
+ for (const [name, methodContractValue] of contractEntries) {
2161
+ if (!RPC_METHOD_PATTERN.test(name)) {
2162
+ throw new Error(
2163
+ `invalid rpc method name "${name}" \u2014 use dot-separated segments with letters, digits, "-" and "_"`
2164
+ );
2092
2165
  }
2093
- if (descriptor.type === "number" && typeof value === "number" && !Number.isFinite(value)) {
2094
- value = void 0;
2166
+ const methodContract = readRpcMethodContract(name, methodContractValue);
2167
+ const handler = Reflect.get(handlers, name);
2168
+ if (typeof handler !== "function") {
2169
+ throw new Error(`rpc method "${name}" must provide a handler function`);
2095
2170
  }
2096
- const expected = descriptor.type === "boolean" ? "boolean" : descriptor.type === "number" ? "number" : "string";
2097
- if (typeof value !== expected) value = void 0;
2098
- if (descriptor.type === "select" && typeof value === "string" && !descriptor.options.includes(value)) {
2099
- value = void 0;
2171
+ if (registered.has(name)) {
2172
+ throw new Error(`rpc method "${name}" is already registered`);
2100
2173
  }
2101
- values[key] = value ?? descriptor.default;
2174
+ pending.push([
2175
+ name,
2176
+ {
2177
+ inputSchema: methodContract.input,
2178
+ outputSchema: methodContract.output,
2179
+ handler
2180
+ }
2181
+ ]);
2102
2182
  }
2103
- return values;
2104
- }
2105
- function isNeedsConfigurationError(error) {
2106
- return error instanceof Error && error.name === "NeedsConfigurationError";
2183
+ return pending;
2107
2184
  }
2108
- function errorMessage(error) {
2109
- return error instanceof Error ? error.message : String(error);
2110
- }
2111
- function jsonRoundTrip(value, what) {
2112
- if (value === void 0) return void 0;
2185
+ function normalizeRealtimePayload(channel, payload) {
2186
+ if (typeof channel !== "string" || channel.length === 0) {
2187
+ throw new Error("realtime channel must be a non-empty string");
2188
+ }
2189
+ if (payload === void 0) return null;
2113
2190
  let json;
2114
2191
  try {
2115
- json = JSON.stringify(value);
2192
+ json = JSON.stringify(payload);
2116
2193
  } catch {
2117
2194
  json = void 0;
2118
2195
  }
2119
2196
  if (json === void 0) {
2120
- throw new Error(`${what} is not JSON-serializable`);
2197
+ throw new Error(
2198
+ `realtime payload for channel "${channel}" is not JSON-serializable`
2199
+ );
2121
2200
  }
2122
2201
  return JSON.parse(json);
2123
2202
  }
2124
- function normalizeRpcIssues(issues) {
2125
- return issues.map((issue) => {
2126
- const rawPath = issue.path;
2127
- const segments = rawPath === void 0 ? [] : Array.isArray(rawPath) ? rawPath : [rawPath];
2128
- const path = segments.map((segment) => {
2129
- const key = typeof segment === "object" && segment !== null ? Reflect.get(segment, "key") : segment;
2130
- return typeof key === "number" ? key : String(key);
2131
- });
2132
- return {
2133
- message: issue.message,
2134
- ...path.length > 0 ? { path } : {}
2135
- };
2136
- });
2137
- }
2138
- function throwRpcError(error) {
2139
- const thrown = new Error(error.message);
2140
- Reflect.set(thrown, "code", error.code);
2141
- if (error.issues !== void 0) Reflect.set(thrown, "issues", error.issues);
2142
- throw thrown;
2203
+ function validateBackgroundServiceRegistration(name, service, existing) {
2204
+ if (typeof name !== "string" || !BACKGROUND_NAME_PATTERN.test(name)) {
2205
+ throw new Error(
2206
+ `invalid service name ${JSON.stringify(name)} \u2014 use letters, digits, "-" and "_"`
2207
+ );
2208
+ }
2209
+ if (existing.some((record) => record.name === name)) {
2210
+ throw new Error(`background service "${name}" is already registered`);
2211
+ }
2212
+ if (typeof service?.start !== "function") {
2213
+ throw new Error(
2214
+ `background service "${name}" must provide a start(signal) function`
2215
+ );
2216
+ }
2217
+ return { name, start: service.start.bind(service) };
2143
2218
  }
2144
- async function validateRpcValue(schema, value, phase) {
2145
- let result;
2219
+ function validateScheduleRegistration(name, cron, fn, existing, parseCron) {
2220
+ if (typeof name !== "string" || !BACKGROUND_NAME_PATTERN.test(name)) {
2221
+ throw new Error(
2222
+ `invalid schedule name ${JSON.stringify(name)} \u2014 use letters, digits, "-" and "_"`
2223
+ );
2224
+ }
2225
+ if (existing.some((record) => record.name === name)) {
2226
+ throw new Error(`schedule "${name}" is already registered`);
2227
+ }
2146
2228
  try {
2147
- result = await schema["~standard"].validate(value);
2229
+ parseCron(String(cron));
2148
2230
  } catch (error) {
2149
- const message = errorMessage(error);
2150
- return throwRpcError({
2151
- code: phase === "input" ? "invalid_input" : "invalid_output",
2152
- message: `rpc ${phase} validator failed: ${message}`,
2153
- issues: [{ message }]
2154
- });
2231
+ throw new Error(
2232
+ `invalid cron ${JSON.stringify(cron)} for schedule "${name}": ${error instanceof Error ? error.message : String(error)}`
2233
+ );
2155
2234
  }
2156
- if (result.issues !== void 0) {
2157
- return throwRpcError({
2158
- code: phase === "input" ? "invalid_input" : "invalid_output",
2159
- message: `rpc ${phase} validation failed`,
2160
- issues: normalizeRpcIssues(result.issues)
2161
- });
2235
+ if (typeof fn !== "function") {
2236
+ throw new Error(`schedule "${name}" must provide a function`);
2162
2237
  }
2163
- return result.value;
2238
+ return { name, cron: String(cron), fn };
2164
2239
  }
2165
- function normalizeRpcJsonResult(value) {
2166
- const ancestors = /* @__PURE__ */ new Set();
2167
- function visit(current, path) {
2168
- if (current === null || typeof current === "string" || typeof current === "boolean") {
2169
- return current;
2170
- }
2171
- if (typeof current === "number") {
2172
- if (!Number.isFinite(current)) {
2173
- return throwRpcError({
2174
- code: "non_json_result",
2175
- message: `rpc result at ${path} contains a non-finite number`
2176
- });
2177
- }
2178
- return current;
2240
+ function normalizeCliRegistration(registration, alreadyRegistered) {
2241
+ if (alreadyRegistered) {
2242
+ throw new Error("cli command is already registered");
2243
+ }
2244
+ const name = registration?.name;
2245
+ if (typeof name !== "string" || !CLI_COMMAND_NAME_PATTERN.test(name)) {
2246
+ throw new Error(
2247
+ `invalid cli command name ${JSON.stringify(name)} \u2014 use lowercase letters, digits, and "-"`
2248
+ );
2249
+ }
2250
+ if (typeof registration.summary !== "string" || registration.summary.trim().length === 0) {
2251
+ throw new Error(`cli command "${name}" must provide a summary`);
2252
+ }
2253
+ const commands = registration.commands ?? [];
2254
+ if (!Array.isArray(commands)) {
2255
+ throw new Error(`cli command "${name}" commands must be an array`);
2256
+ }
2257
+ const validatedCommands = commands.map((command, index) => {
2258
+ if (typeof command?.name !== "string" || !CLI_COMMAND_NAME_PATTERN.test(command.name) || typeof command.summary !== "string" || typeof command.usage !== "string") {
2259
+ throw new Error(
2260
+ `cli command "${name}" commands[${index}] must be { name: [a-z0-9-]+, summary, usage }`
2261
+ );
2179
2262
  }
2180
- if (typeof current !== "object") {
2181
- return throwRpcError({
2182
- code: "non_json_result",
2183
- message: `rpc result at ${path} is not a JSON value (${typeof current})`
2184
- });
2263
+ return {
2264
+ name: command.name,
2265
+ summary: command.summary,
2266
+ usage: command.usage
2267
+ };
2268
+ });
2269
+ if (typeof registration.run !== "function") {
2270
+ throw new Error(
2271
+ `cli command "${name}" must provide a run(argv, ctx) function`
2272
+ );
2273
+ }
2274
+ return {
2275
+ name,
2276
+ summary: registration.summary,
2277
+ commands: validatedCommands,
2278
+ run: registration.run.bind(registration)
2279
+ };
2280
+ }
2281
+ function normalizeAgentToolRegistration(args) {
2282
+ const { pluginId, declaredIconNames, tool } = args;
2283
+ const name = tool?.name;
2284
+ if (typeof name !== "string" || !AGENT_TOOL_NAME_PATTERN.test(name)) {
2285
+ throw new Error(
2286
+ `invalid tool name ${JSON.stringify(name)} \u2014 use letters, digits, "-" and "_"`
2287
+ );
2288
+ }
2289
+ if (RESERVED_AGENT_TOOL_NAMES.includes(name)) {
2290
+ throw new Error(
2291
+ `tool name "${name}" is a built-in bb tool \u2014 pick another name`
2292
+ );
2293
+ }
2294
+ rejectStaleAgentToolFields(name, tool);
2295
+ if (typeof tool.description !== "string" || tool.description.trim().length === 0) {
2296
+ throw new Error(`tool "${name}" must provide a description`);
2297
+ }
2298
+ if (tool.instructions !== void 0 && typeof tool.instructions !== "string") {
2299
+ throw new Error(`tool "${name}" instructions must be a string`);
2300
+ }
2301
+ if (typeof tool.instructions === "string" && tool.instructions.length > PLUGIN_AGENT_STATIC_INSTRUCTIONS_MAX_CHARS) {
2302
+ throw new Error(
2303
+ `tool "${name}" instructions exceed the ${PLUGIN_AGENT_STATIC_INSTRUCTIONS_MAX_CHARS}-character limit`
2304
+ );
2305
+ }
2306
+ const presentation = parsePluginAgentToolPresentation(
2307
+ name,
2308
+ tool.presentation
2309
+ );
2310
+ if (presentation?.icon !== void 0) {
2311
+ const problem = undeclaredIconProblem(
2312
+ pluginId,
2313
+ declaredIconNames,
2314
+ presentation.icon.glyph
2315
+ );
2316
+ if (problem !== null) {
2317
+ throw new Error(agentToolIconRefusalMessage(name, problem));
2185
2318
  }
2186
- if (ancestors.has(current)) {
2187
- return throwRpcError({
2188
- code: "non_json_result",
2189
- message: `rpc result at ${path} is cyclic`
2190
- });
2319
+ }
2320
+ if (typeof tool.execute !== "function") {
2321
+ throw new Error(
2322
+ `tool "${name}" must provide an execute(params, ctx) function`
2323
+ );
2324
+ }
2325
+ const parameters = tool.parameters;
2326
+ let inputSchema;
2327
+ let parse;
2328
+ if (isZodSchemaLike(parameters)) {
2329
+ try {
2330
+ inputSchema = zodSchemaToJsonSchema(parameters);
2331
+ } catch (error) {
2332
+ throw new Error(
2333
+ `tool "${name}" parameters look like a zod schema but could not be converted to JSON Schema (${error instanceof Error ? error.message : String(error)}) \u2014 use zod 4, or pass a plain JSON-schema object`
2334
+ );
2191
2335
  }
2192
- ancestors.add(current);
2336
+ parse = (input) => {
2337
+ const result = parameters.safeParse(input);
2338
+ if (result.success) return { ok: true, value: result.data };
2339
+ return { ok: false, error: summarizeParseIssues(result.error) };
2340
+ };
2341
+ } else if (typeof parameters === "object" && parameters !== null && !Array.isArray(parameters)) {
2193
2342
  try {
2194
- if (Array.isArray(current)) {
2195
- return current.map((item, index) => visit(item, `${path}[${index}]`));
2196
- }
2197
- const prototype = Object.getPrototypeOf(current);
2198
- if (prototype !== Object.prototype && prototype !== null) {
2199
- return throwRpcError({
2200
- code: "non_json_result",
2201
- message: `rpc result at ${path} must be a plain JSON object`
2202
- });
2203
- }
2204
- if (Reflect.ownKeys(current).some((key) => typeof key === "symbol")) {
2205
- return throwRpcError({
2206
- code: "non_json_result",
2207
- message: `rpc result at ${path} contains a symbol key`
2208
- });
2209
- }
2210
- const normalized = {};
2211
- for (const [key, child] of Object.entries(current)) {
2212
- normalized[key] = visit(child, `${path}.${key}`);
2213
- }
2214
- return normalized;
2215
- } finally {
2216
- ancestors.delete(current);
2343
+ inputSchema = JSON.parse(JSON.stringify(parameters));
2344
+ } catch {
2345
+ throw new Error(
2346
+ `tool "${name}" parameters JSON schema is not JSON-serializable`
2347
+ );
2217
2348
  }
2349
+ parse = (input) => ({ ok: true, value: input });
2350
+ } else {
2351
+ throw new Error(
2352
+ `tool "${name}" parameters must be a zod schema or a JSON-schema object`
2353
+ );
2218
2354
  }
2219
- return visit(value, "$result");
2355
+ assertNoRecursiveJsonSchemaReferences(
2356
+ inputSchema,
2357
+ `tool "${name}" parameters`
2358
+ );
2359
+ return {
2360
+ name,
2361
+ description: tool.description,
2362
+ presentation,
2363
+ instructions: tool.instructions !== void 0 && tool.instructions.trim().length > 0 ? tool.instructions : null,
2364
+ inputSchema,
2365
+ parse,
2366
+ execute: tool.execute.bind(tool)
2367
+ };
2368
+ }
2369
+ function normalizeMentionProviderRegistration(provider, existing) {
2370
+ const id = provider?.id;
2371
+ if (typeof id !== "string" || !MENTION_PROVIDER_ID_PATTERN.test(id)) {
2372
+ throw new Error(
2373
+ `invalid mention provider id ${JSON.stringify(id)} \u2014 use letters, digits, "-" and "_"`
2374
+ );
2375
+ }
2376
+ if (existing.some((record) => record.id === id)) {
2377
+ throw new Error(`mention provider "${id}" is already registered`);
2378
+ }
2379
+ if (typeof provider.label !== "string" || provider.label.trim().length === 0) {
2380
+ throw new Error(`mention provider "${id}" must provide a label`);
2381
+ }
2382
+ if (typeof provider.search !== "function") {
2383
+ throw new Error(
2384
+ `mention provider "${id}" must provide a search({ query, projectId, threadId }) function`
2385
+ );
2386
+ }
2387
+ if (typeof provider.resolve !== "function") {
2388
+ throw new Error(
2389
+ `mention provider "${id}" must provide a resolve(itemId) function`
2390
+ );
2391
+ }
2392
+ return {
2393
+ id,
2394
+ label: provider.label.trim(),
2395
+ triggers: normalizeMentionProviderTriggers(id, provider.triggers),
2396
+ search: provider.search.bind(provider),
2397
+ resolve: provider.resolve.bind(provider)
2398
+ };
2399
+ }
2400
+ function normalizeInteractionRequest(request) {
2401
+ if (!request || typeof request !== "object") {
2402
+ throw new Error("ui.requestInput requires an options object");
2403
+ }
2404
+ if (typeof request.threadId !== "string" || request.threadId.length === 0) {
2405
+ throw new Error("ui.requestInput threadId must be a non-empty string");
2406
+ }
2407
+ if (typeof request.rendererId !== "string" || !/^[a-zA-Z0-9_-]+$/.test(request.rendererId)) {
2408
+ throw new Error(
2409
+ "ui.requestInput rendererId must use letters, digits, '-' or '_'"
2410
+ );
2411
+ }
2412
+ if (typeof request.title !== "string" || request.title.trim().length === 0 || request.title.trim().length > PLUGIN_INTERACTION_MAX_TITLE_LENGTH) {
2413
+ throw new Error(
2414
+ `ui.requestInput title must be 1-${PLUGIN_INTERACTION_MAX_TITLE_LENGTH} characters`
2415
+ );
2416
+ }
2417
+ let payload;
2418
+ try {
2419
+ const json = JSON.stringify(request.payload);
2420
+ if (json === void 0) throw new Error();
2421
+ if (Buffer.byteLength(json, "utf8") > PLUGIN_INTERACTION_MAX_PAYLOAD_BYTES) {
2422
+ throw new Error("ui.requestInput payload exceeds 64 KiB");
2423
+ }
2424
+ payload = JSON.parse(json);
2425
+ } catch (error) {
2426
+ if (error instanceof Error && error.message.includes("64 KiB")) {
2427
+ throw error;
2428
+ }
2429
+ throw new Error("ui.requestInput payload must be JSON-serializable");
2430
+ }
2431
+ const timeoutMs = request.timeoutMs ?? 10 * 60 * 1e3;
2432
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60 * 60 * 1e3) {
2433
+ throw new Error("ui.requestInput timeoutMs must be between 1 and 3600000");
2434
+ }
2435
+ return {
2436
+ threadId: request.threadId,
2437
+ rendererId: request.rendererId,
2438
+ title: request.title.trim(),
2439
+ payload,
2440
+ timeoutMs
2441
+ };
2442
+ }
2443
+ function validateProviderEnvContribution(label, providerId, resolve2, registered) {
2444
+ if (typeof providerId !== "string" || providerId.trim().length === 0) {
2445
+ throw new Error(`${label} requires a provider id`);
2446
+ }
2447
+ if (registered.has(providerId)) {
2448
+ throw new Error(`${label} for "${providerId}" is already registered`);
2449
+ }
2450
+ if (typeof resolve2 !== "function") {
2451
+ throw new Error(`${label} requires a resolver function`);
2452
+ }
2453
+ }
2454
+ function normalizePluginAgentToolParameters(args) {
2455
+ const { index, value } = args;
2456
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2457
+ throw new Error(
2458
+ `configure() output.tools[${index}].parameters must be a JSON-schema object`
2459
+ );
2460
+ }
2461
+ let serialized;
2462
+ try {
2463
+ serialized = JSON.stringify(value);
2464
+ } catch {
2465
+ throw new Error(
2466
+ `configure() output.tools[${index}].parameters is not JSON-serializable`
2467
+ );
2468
+ }
2469
+ if (serialized === void 0) {
2470
+ throw new Error(
2471
+ `configure() output.tools[${index}].parameters is not JSON-serializable`
2472
+ );
2473
+ }
2474
+ if (Buffer.byteLength(serialized, "utf8") > PLUGIN_AGENT_TOOL_PARAMETERS_MAX_BYTES) {
2475
+ throw new Error(
2476
+ `configure() output.tools[${index}].parameters exceeds the ${PLUGIN_AGENT_TOOL_PARAMETERS_MAX_BYTES}-byte limit`
2477
+ );
2478
+ }
2479
+ const parameters = JSON.parse(serialized);
2480
+ if (parameters.type !== "object") {
2481
+ throw new Error(
2482
+ `configure() output.tools[${index}].parameters must have root type "object"`
2483
+ );
2484
+ }
2485
+ assertNoRecursiveJsonSchemaReferences(
2486
+ parameters,
2487
+ `configure() output.tools[${index}].parameters`
2488
+ );
2489
+ return parameters;
2220
2490
  }
2221
- function normalizeAgentToolSelections(args) {
2491
+ function normalizePluginAgentToolSelections(args) {
2222
2492
  if (!Array.isArray(args.value)) {
2223
2493
  throw new Error("configure() output.tools must be an array");
2224
2494
  }
@@ -2245,7 +2515,7 @@ function normalizeAgentToolSelections(args) {
2245
2515
  );
2246
2516
  }
2247
2517
  name = typed.name;
2248
- parameters = normalizeAgentToolParameters({
2518
+ parameters = normalizePluginAgentToolParameters({
2249
2519
  index,
2250
2520
  value: typed.parameters
2251
2521
  });
@@ -2275,50 +2545,13 @@ function normalizeAgentToolSelections(args) {
2275
2545
  }
2276
2546
  return { toolIds, parameterOverrides };
2277
2547
  }
2278
- function normalizeAgentToolParameters(args) {
2279
- const { index, value } = args;
2280
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
2281
- throw new Error(
2282
- `configure() output.tools[${index}].parameters must be a JSON-schema object`
2283
- );
2284
- }
2285
- let serialized;
2286
- try {
2287
- serialized = JSON.stringify(value);
2288
- } catch {
2289
- throw new Error(
2290
- `configure() output.tools[${index}].parameters is not JSON-serializable`
2291
- );
2292
- }
2293
- if (serialized === void 0) {
2294
- throw new Error(
2295
- `configure() output.tools[${index}].parameters is not JSON-serializable`
2296
- );
2297
- }
2298
- if (Buffer.byteLength(serialized, "utf8") > PLUGIN_AGENT_TOOL_PARAMETERS_MAX_BYTES) {
2299
- throw new Error(
2300
- `configure() output.tools[${index}].parameters exceeds the ${PLUGIN_AGENT_TOOL_PARAMETERS_MAX_BYTES}-byte limit`
2301
- );
2302
- }
2303
- const parameters = JSON.parse(serialized);
2304
- if (parameters.type !== "object") {
2305
- throw new Error(
2306
- `configure() output.tools[${index}].parameters must have root type "object"`
2307
- );
2308
- }
2309
- assertNoRecursiveJsonSchemaReferences(
2310
- parameters,
2311
- `configure() output.tools[${index}].parameters`
2312
- );
2313
- return parameters;
2314
- }
2315
- function normalizeAgentConfigurationIds(args) {
2548
+ function normalizePluginAgentSelectionIds(args) {
2316
2549
  if (!Array.isArray(args.value)) {
2317
- throw new Error(`configure() output.${args.field} must be an array`);
2550
+ throw new Error("configure() output.skills must be an array");
2318
2551
  }
2319
2552
  if (args.value.length > PLUGIN_AGENT_SELECTION_MAX_IDS) {
2320
2553
  throw new Error(
2321
- `configure() output.${args.field} exceeds the ${PLUGIN_AGENT_SELECTION_MAX_IDS}-id limit`
2554
+ `configure() output.skills exceeds the ${PLUGIN_AGENT_SELECTION_MAX_IDS}-id limit`
2322
2555
  );
2323
2556
  }
2324
2557
  const selected = [];
@@ -2327,12 +2560,12 @@ function normalizeAgentConfigurationIds(args) {
2327
2560
  const id = args.value[index];
2328
2561
  if (typeof id !== "string" || id.length === 0) {
2329
2562
  throw new Error(
2330
- `configure() output.${args.field}[${index}] must be a non-empty string`
2563
+ `configure() output.skills[${index}] must be a non-empty string`
2331
2564
  );
2332
2565
  }
2333
2566
  if (seen.has(id)) {
2334
2567
  throw new Error(
2335
- `configure() output.${args.field} contains duplicate id ${JSON.stringify(id)}`
2568
+ `configure() output.skills contains duplicate id ${JSON.stringify(id)}`
2336
2569
  );
2337
2570
  }
2338
2571
  if (!args.knownIds.has(id)) {
@@ -2345,7 +2578,7 @@ function normalizeAgentConfigurationIds(args) {
2345
2578
  }
2346
2579
  return selected;
2347
2580
  }
2348
- function normalizeAgentConfiguration(args) {
2581
+ function normalizePluginAgentConfiguration(args) {
2349
2582
  if (typeof args.value !== "object" || args.value === null || Array.isArray(args.value)) {
2350
2583
  throw new Error(
2351
2584
  "configure() must return { tools: string[], skills: string[], instructions?: string }"
@@ -2358,28 +2591,284 @@ function normalizeAgentConfiguration(args) {
2358
2591
  `configure() output contains unknown field${unknownKeys.length === 1 ? "" : "s"}: ${unknownKeys.join(", ")}`
2359
2592
  );
2360
2593
  }
2361
- if (output.instructions !== void 0 && typeof output.instructions !== "string") {
2362
- throw new Error("configure() output.instructions must be a string");
2594
+ if (output.instructions !== void 0 && typeof output.instructions !== "string") {
2595
+ throw new Error("configure() output.instructions must be a string");
2596
+ }
2597
+ const instructions = typeof output.instructions === "string" && output.instructions.trim().length > 0 ? output.instructions.slice(
2598
+ 0,
2599
+ PLUGIN_AGENT_DYNAMIC_INSTRUCTIONS_MAX_CHARS
2600
+ ) : null;
2601
+ const toolSelections = normalizePluginAgentToolSelections({
2602
+ knownIds: args.knownToolIds,
2603
+ pluginId: args.pluginId,
2604
+ value: output.tools
2605
+ });
2606
+ return {
2607
+ toolIds: toolSelections.toolIds,
2608
+ toolParameterOverrides: toolSelections.parameterOverrides,
2609
+ skillIds: normalizePluginAgentSelectionIds({
2610
+ knownIds: args.knownSkillIds,
2611
+ pluginId: args.pluginId,
2612
+ value: output.skills
2613
+ }),
2614
+ instructions
2615
+ };
2616
+ }
2617
+ function normalizeRpcIssuePath(path) {
2618
+ if (path === void 0) return void 0;
2619
+ const segments = Array.isArray(path) ? path : [path];
2620
+ const normalized = segments.map((segment) => {
2621
+ const key = typeof segment === "object" && segment !== null ? Reflect.get(segment, "key") : segment;
2622
+ return typeof key === "number" ? key : String(key);
2623
+ });
2624
+ return normalized.length > 0 ? normalized : void 0;
2625
+ }
2626
+ function normalizeRpcIssues(issues) {
2627
+ return issues.map((issue) => {
2628
+ const path = normalizeRpcIssuePath(issue.path);
2629
+ return {
2630
+ message: issue.message,
2631
+ ...path !== void 0 ? { path } : {}
2632
+ };
2633
+ });
2634
+ }
2635
+ async function validateRpcValue(schema, value, phase, fail) {
2636
+ let result;
2637
+ try {
2638
+ result = await schema["~standard"].validate(value);
2639
+ } catch (error) {
2640
+ const detail = error instanceof Error ? error.message : String(error);
2641
+ return fail({
2642
+ code: phase === "input" ? "invalid_input" : "invalid_output",
2643
+ message: `rpc ${phase} validator failed: ${detail}`,
2644
+ issues: [{ message: detail }]
2645
+ });
2646
+ }
2647
+ if (result.issues !== void 0) {
2648
+ return fail({
2649
+ code: phase === "input" ? "invalid_input" : "invalid_output",
2650
+ message: `rpc ${phase} validation failed`,
2651
+ issues: normalizeRpcIssues(result.issues)
2652
+ });
2653
+ }
2654
+ return result.value;
2655
+ }
2656
+ function normalizeRpcJsonResult(value, fail) {
2657
+ const ancestors = /* @__PURE__ */ new Set();
2658
+ function visit(current, path) {
2659
+ if (current === null || typeof current === "string" || typeof current === "boolean") {
2660
+ return current;
2661
+ }
2662
+ if (typeof current === "number") {
2663
+ if (!Number.isFinite(current)) {
2664
+ return fail({
2665
+ code: "non_json_result",
2666
+ message: `rpc result at ${path} contains a non-finite number`
2667
+ });
2668
+ }
2669
+ return current;
2670
+ }
2671
+ if (typeof current !== "object") {
2672
+ return fail({
2673
+ code: "non_json_result",
2674
+ message: `rpc result at ${path} is not a JSON value (${typeof current})`
2675
+ });
2676
+ }
2677
+ if (ancestors.has(current)) {
2678
+ return fail({
2679
+ code: "non_json_result",
2680
+ message: `rpc result at ${path} is cyclic`
2681
+ });
2682
+ }
2683
+ ancestors.add(current);
2684
+ try {
2685
+ if (Array.isArray(current)) {
2686
+ return current.map((item, index) => visit(item, `${path}[${index}]`));
2687
+ }
2688
+ const prototype = Object.getPrototypeOf(current);
2689
+ if (prototype !== Object.prototype && prototype !== null) {
2690
+ return fail({
2691
+ code: "non_json_result",
2692
+ message: `rpc result at ${path} must be a plain JSON object`
2693
+ });
2694
+ }
2695
+ if (Reflect.ownKeys(current).some((key) => typeof key === "symbol")) {
2696
+ return fail({
2697
+ code: "non_json_result",
2698
+ message: `rpc result at ${path} contains a symbol key`
2699
+ });
2700
+ }
2701
+ const normalized = {};
2702
+ for (const [key, child] of Object.entries(current)) {
2703
+ normalized[key] = visit(child, `${path}.${key}`);
2704
+ }
2705
+ return normalized;
2706
+ } finally {
2707
+ ancestors.delete(current);
2708
+ }
2709
+ }
2710
+ return visit(value, "$result");
2711
+ }
2712
+
2713
+ // src/testing/fake-plugin-host.ts
2714
+ import { mkdtempSync, rmSync } from "node:fs";
2715
+ import { tmpdir } from "node:os";
2716
+ import { join } from "node:path";
2717
+ import Database from "better-sqlite3";
2718
+ import { CronExpressionParser } from "cron-parser";
2719
+ import { Hono } from "hono";
2720
+
2721
+ // src/testing/fake-sdk.ts
2722
+ function withThreadAttribution(pluginId, args) {
2723
+ const [first, ...rest] = args;
2724
+ if (typeof first !== "object" || first === null) return args;
2725
+ const threadArgs = first;
2726
+ if (threadArgs.pluginMetadata !== void 0) {
2727
+ return [
2728
+ {
2729
+ ...threadArgs,
2730
+ pluginMetadata: validatePluginMetadata(threadArgs.pluginMetadata),
2731
+ origin: "plugin",
2732
+ originPluginId: pluginId
2733
+ },
2734
+ ...rest
2735
+ ];
2736
+ }
2737
+ const origin = threadArgs.origin ?? "plugin";
2738
+ return [
2739
+ {
2740
+ ...threadArgs,
2741
+ origin,
2742
+ ...origin === "plugin" ? { originPluginId: threadArgs.originPluginId ?? pluginId } : {}
2743
+ },
2744
+ ...rest
2745
+ ];
2746
+ }
2747
+ function withPluginMetadataTarget(pluginId, args, validateSet) {
2748
+ const [first, ...rest] = args;
2749
+ if (typeof first !== "object" || first === null) return args;
2750
+ const input = first;
2751
+ return [
2752
+ {
2753
+ ...input,
2754
+ pluginId: input.pluginId ?? pluginId,
2755
+ ...validateSet && input.set !== void 0 ? { set: validatePluginMetadata(input.set) } : {}
2756
+ },
2757
+ ...rest
2758
+ ];
2759
+ }
2760
+ function normalizeCallArgs(pluginId, path, args) {
2761
+ switch (path) {
2762
+ case "threads.spawn":
2763
+ case "threads.fork":
2764
+ return withThreadAttribution(pluginId, args);
2765
+ case "threads.getPluginMetadata":
2766
+ return withPluginMetadataTarget(pluginId, args, false);
2767
+ case "threads.updatePluginMetadata":
2768
+ return withPluginMetadataTarget(pluginId, args, true);
2769
+ default:
2770
+ return args;
2771
+ }
2772
+ }
2773
+ function createFakeSdk(options) {
2774
+ const calls = [];
2775
+ const stubs = /* @__PURE__ */ new Map();
2776
+ function addOverrides(prefix, value) {
2777
+ if (typeof value === "function") {
2778
+ stubs.set(prefix, value);
2779
+ return;
2780
+ }
2781
+ if (typeof value !== "object" || value === null) return;
2782
+ for (const [key, child] of Object.entries(value)) {
2783
+ addOverrides(prefix.length === 0 ? key : `${prefix}.${key}`, child);
2784
+ }
2785
+ }
2786
+ addOverrides("", options.overrides ?? {});
2787
+ function invoke(path, rawArgs) {
2788
+ let args;
2789
+ try {
2790
+ args = normalizeCallArgs(options.pluginId, path, rawArgs);
2791
+ } catch (error) {
2792
+ return Promise.reject(error);
2793
+ }
2794
+ calls.push({ path, args });
2795
+ const stub = stubs.get(path);
2796
+ if (!stub) {
2797
+ throw new Error(
2798
+ `bb.sdk.${path} is not stubbed \u2014 pass an implementation via createFakePluginHost({ sdk: { ... } }) or harness.sdk.stub("${path}", fn)`
2799
+ );
2800
+ }
2801
+ return stub(...args);
2802
+ }
2803
+ const nodes = /* @__PURE__ */ new Map();
2804
+ function node(path) {
2805
+ const cached = nodes.get(path);
2806
+ if (cached) return cached;
2807
+ const created = new Proxy(function() {
2808
+ }, {
2809
+ get(_target, prop) {
2810
+ if (typeof prop !== "string" || prop === "then") return void 0;
2811
+ return node(path === "" ? prop : `${path}.${prop}`);
2812
+ },
2813
+ apply(_target, _thisArg, args) {
2814
+ return invoke(path, args);
2815
+ }
2816
+ });
2817
+ nodes.set(path, created);
2818
+ return created;
2819
+ }
2820
+ const harness = {
2821
+ calls,
2822
+ callsTo(path) {
2823
+ return calls.filter((call) => call.path === path).map((call) => call.args);
2824
+ },
2825
+ stub(path, implementation) {
2826
+ stubs.set(path, implementation);
2827
+ }
2828
+ };
2829
+ return { sdk: node(""), harness };
2830
+ }
2831
+
2832
+ // src/testing/fake-plugin-host.ts
2833
+ var PluginContextStaleError = class extends Error {
2834
+ constructor(pluginId) {
2835
+ super(
2836
+ `plugin "${pluginId}" used a stale API handle \u2014 it was reloaded or disabled; re-entry happens via a fresh factory call`
2837
+ );
2838
+ this.name = "PluginContextStaleError";
2839
+ }
2840
+ };
2841
+ function readSettingsValues(descriptors, stored) {
2842
+ const values = {};
2843
+ for (const [key, descriptor] of Object.entries(descriptors)) {
2844
+ values[key] = coerceStoredPluginSettingValue(descriptor, stored.get(key));
2363
2845
  }
2364
- const toolSelections = normalizeAgentToolSelections({
2365
- knownIds: args.knownToolIds,
2366
- pluginId: args.pluginId,
2367
- value: output.tools
2368
- });
2369
- return {
2370
- toolIds: toolSelections.toolIds,
2371
- toolParameterOverrides: toolSelections.parameterOverrides,
2372
- skillIds: normalizeAgentConfigurationIds({
2373
- field: "skills",
2374
- knownIds: args.knownSkillIds,
2375
- pluginId: args.pluginId,
2376
- value: output.skills
2377
- }),
2378
- instructions: typeof output.instructions === "string" && output.instructions.trim().length > 0 ? output.instructions.slice(
2379
- 0,
2380
- PLUGIN_AGENT_DYNAMIC_INSTRUCTIONS_MAX_CHARS
2381
- ) : null
2382
- };
2846
+ return values;
2847
+ }
2848
+ function isNeedsConfigurationError(error) {
2849
+ return error instanceof Error && error.name === "NeedsConfigurationError";
2850
+ }
2851
+ function errorMessage(error) {
2852
+ return error instanceof Error ? error.message : String(error);
2853
+ }
2854
+ function jsonRoundTrip(value, what) {
2855
+ if (value === void 0) return void 0;
2856
+ let json;
2857
+ try {
2858
+ json = JSON.stringify(value);
2859
+ } catch {
2860
+ json = void 0;
2861
+ }
2862
+ if (json === void 0) {
2863
+ throw new Error(`${what} is not JSON-serializable`);
2864
+ }
2865
+ return JSON.parse(json);
2866
+ }
2867
+ function throwRpcError(error) {
2868
+ const thrown = new Error(error.message);
2869
+ Reflect.set(thrown, "code", error.code);
2870
+ if (error.issues !== void 0) Reflect.set(thrown, "issues", error.issues);
2871
+ throw thrown;
2383
2872
  }
2384
2873
  var fakeHostDisposers = /* @__PURE__ */ new WeakMap();
2385
2874
  function createFakePluginHost(options = {}) {
@@ -2461,49 +2950,7 @@ function createFakePluginHostInternal(options, sharedState) {
2461
2950
  },
2462
2951
  migrate(database, statements) {
2463
2952
  assertLive();
2464
- database.exec(
2465
- "CREATE TABLE IF NOT EXISTS _bb_migrations (id INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL, statement_hash TEXT)"
2466
- );
2467
- const migrationColumns = database.prepare("PRAGMA table_info(_bb_migrations)").all();
2468
- if (!migrationColumns.some((column) => column.name === "statement_hash")) {
2469
- database.exec(
2470
- "ALTER TABLE _bb_migrations ADD COLUMN statement_hash TEXT"
2471
- );
2472
- }
2473
- const rows = database.prepare(
2474
- "SELECT id, statement_hash FROM _bb_migrations ORDER BY id"
2475
- ).all();
2476
- const applied = /* @__PURE__ */ new Map();
2477
- for (const row of rows) applied.set(row.id, row.statement_hash);
2478
- const statementHashes = statements.map(migrationStatementHash);
2479
- statementHashes.forEach((statementHash, index) => {
2480
- const recordedHash = applied.get(index);
2481
- if (recordedHash !== void 0 && recordedHash !== null && recordedHash !== statementHash) {
2482
- throw new Error(
2483
- `migration ${index} does not match the recorded statement; append a new migration instead of changing or reusing an index`
2484
- );
2485
- }
2486
- });
2487
- const adopt = database.prepare(
2488
- "UPDATE _bb_migrations SET statement_hash = ? WHERE id = ? AND statement_hash IS NULL"
2489
- );
2490
- const record = database.prepare(
2491
- "INSERT INTO _bb_migrations (id, applied_at, statement_hash) VALUES (?, ?, ?)"
2492
- );
2493
- database.transaction(() => {
2494
- for (const row of rows) {
2495
- if (row.statement_hash !== null) continue;
2496
- adopt.run(
2497
- statementHashes[row.id] ?? LEGACY_UNKNOWN_MIGRATION_HASH,
2498
- row.id
2499
- );
2500
- }
2501
- statements.forEach((statement, index) => {
2502
- if (applied.has(index)) return;
2503
- database.exec(statement);
2504
- record.run(index, Date.now(), statementHashes[index]);
2505
- });
2506
- })();
2953
+ runPluginStorageMigrations(database, statements);
2507
2954
  }
2508
2955
  };
2509
2956
  const settingsDescriptors = {};
@@ -2576,107 +3023,35 @@ function createFakePluginHostInternal(options, sharedState) {
2576
3023
  const http = {
2577
3024
  route(method, path, handler, opts) {
2578
3025
  assertLive();
2579
- const normalizedMethod = String(method).toUpperCase();
2580
- if (!PLUGIN_HTTP_METHODS.has(normalizedMethod)) {
2581
- throw new Error(
2582
- `invalid http method "${String(method)}" \u2014 use one of: ${[...PLUGIN_HTTP_METHODS].join(", ")}`
2583
- );
2584
- }
2585
- if (typeof path !== "string" || !path.startsWith("/")) {
2586
- throw new Error(
2587
- `http route path must be a string starting with "/", got ${JSON.stringify(path)}`
2588
- );
2589
- }
2590
- if (typeof handler !== "function") {
2591
- throw new Error(
2592
- `http route handler for ${normalizedMethod} ${path} must be a function`
2593
- );
2594
- }
2595
- const auth = opts?.auth ?? "local";
2596
- if (auth !== "local" && auth !== "token" && auth !== "none") {
2597
- throw new Error(
2598
- `invalid auth mode "${String(auth)}" for ${normalizedMethod} ${path} \u2014 use "local", "token", or "none"`
2599
- );
2600
- }
2601
- if (httpRoutes.some(
2602
- (route) => route.method === normalizedMethod && route.path === path
2603
- )) {
2604
- throw new Error(
2605
- `http route ${normalizedMethod} ${path} is already registered`
2606
- );
2607
- }
2608
- httpRoutes.push({ method: normalizedMethod, path, auth, handler });
3026
+ const route = normalizeHttpRouteRegistration(
3027
+ method,
3028
+ path,
3029
+ handler,
3030
+ opts,
3031
+ httpRoutes
3032
+ );
3033
+ httpRoutes.push({ ...route, handler });
2609
3034
  },
2610
3035
  experimental_websocket(path, handler, opts) {
2611
3036
  assertLive();
2612
- if (typeof path !== "string" || !path.startsWith("/")) {
2613
- throw new Error(
2614
- `websocket route path must be a string starting with "/", got ${JSON.stringify(path)}`
2615
- );
2616
- }
2617
- if (typeof handler !== "function") {
2618
- throw new Error(
2619
- `websocket route handler for ${path} must be a function`
2620
- );
2621
- }
2622
- const auth = opts?.auth ?? "local";
2623
- if (auth !== "local" && auth !== "token" && auth !== "none") {
2624
- throw new Error(
2625
- `invalid auth mode "${String(auth)}" for websocket ${path} \u2014 use "local", "token", or "none"`
2626
- );
2627
- }
2628
- if (websocketRoutes.some((route) => route.path === path)) {
2629
- throw new Error(`websocket route ${path} is already registered`);
2630
- }
2631
- websocketRoutes.push({ path, auth, handler });
3037
+ const route = normalizeWebSocketRouteRegistration(
3038
+ path,
3039
+ handler,
3040
+ opts,
3041
+ websocketRoutes
3042
+ );
3043
+ websocketRoutes.push({ ...route, handler });
2632
3044
  }
2633
3045
  };
2634
3046
  const rpcHandlers = /* @__PURE__ */ new Map();
2635
3047
  const rpc = {
2636
3048
  register(contract, handlers) {
2637
3049
  assertLive();
2638
- if (typeof contract !== "object" || contract === null || Array.isArray(contract)) {
2639
- throw new Error("rpc.register contract must be an object");
2640
- }
2641
- if (typeof handlers !== "object" || handlers === null || Array.isArray(handlers)) {
2642
- throw new Error("rpc.register handlers must be an object");
2643
- }
2644
- const pending = [];
2645
- const contractEntries = Object.entries(contract);
2646
- const contractNames = new Set(contractEntries.map(([name]) => name));
2647
- for (const extraName of Object.keys(handlers)) {
2648
- if (!contractNames.has(extraName)) {
2649
- throw new Error(
2650
- `rpc handler "${extraName}" has no matching contract method`
2651
- );
2652
- }
2653
- }
2654
- for (const [name, contractValue] of contractEntries) {
2655
- if (!RPC_METHOD_PATTERN.test(name)) {
2656
- throw new Error(
2657
- `invalid rpc method name "${name}" \u2014 use dot-separated segments with letters, digits, "-" and "_"`
2658
- );
2659
- }
2660
- const methodContract = readRpcMethodContract(name, contractValue);
2661
- const handler = Reflect.get(handlers, name);
2662
- if (typeof handler !== "function") {
2663
- throw new Error(
2664
- `rpc method "${name}" must provide a handler function`
2665
- );
2666
- }
2667
- if (rpcHandlers.has(name)) {
2668
- throw new Error(`rpc method "${name}" is already registered`);
2669
- }
2670
- pending.push([
2671
- name,
2672
- {
2673
- inputSchema: methodContract.input,
2674
- outputSchema: methodContract.output,
2675
- handler
2676
- }
2677
- ]);
2678
- }
2679
- for (const [name, record] of pending) {
3050
+ for (const [name, record] of normalizeRpcRegistration(
3051
+ contract,
3052
+ handlers,
3053
+ rpcHandlers
3054
+ )) {
2680
3055
  rpcHandlers.set(name, record);
2681
3056
  }
2682
3057
  }
@@ -2685,14 +3060,10 @@ function createFakePluginHostInternal(options, sharedState) {
2685
3060
  const realtime = {
2686
3061
  publish(channel, payload) {
2687
3062
  assertLive();
2688
- if (typeof channel !== "string" || channel.length === 0) {
2689
- throw new Error("realtime channel must be a non-empty string");
2690
- }
2691
- const normalized = payload === void 0 ? null : jsonRoundTrip(
2692
- payload,
2693
- `realtime payload for channel "${channel}"`
2694
- ) ?? null;
2695
- realtimeSignals.push({ channel, payload: normalized });
3063
+ realtimeSignals.push({
3064
+ channel,
3065
+ payload: normalizeRealtimePayload(channel, payload)
3066
+ });
2696
3067
  }
2697
3068
  };
2698
3069
  const services = [];
@@ -2700,42 +3071,23 @@ function createFakePluginHostInternal(options, sharedState) {
2700
3071
  const background = {
2701
3072
  service(name, service) {
2702
3073
  assertLive();
2703
- if (typeof name !== "string" || !BACKGROUND_NAME_PATTERN.test(name)) {
2704
- throw new Error(
2705
- `invalid service name ${JSON.stringify(name)} \u2014 use letters, digits, "-" and "_"`
2706
- );
2707
- }
2708
- if (services.some((record) => record.name === name)) {
2709
- throw new Error(`background service "${name}" is already registered`);
2710
- }
2711
- if (typeof service?.start !== "function") {
2712
- throw new Error(
2713
- `background service "${name}" must provide a start(signal) function`
2714
- );
2715
- }
2716
- services.push({ name, start: service.start.bind(service) });
3074
+ services.push(
3075
+ validateBackgroundServiceRegistration(name, service, services)
3076
+ );
2717
3077
  },
2718
3078
  schedule(name, cron, fn) {
2719
3079
  assertLive();
2720
- if (typeof name !== "string" || !BACKGROUND_NAME_PATTERN.test(name)) {
2721
- throw new Error(
2722
- `invalid schedule name ${JSON.stringify(name)} \u2014 use letters, digits, "-" and "_"`
2723
- );
2724
- }
2725
- if (schedules.some((record) => record.name === name)) {
2726
- throw new Error(`schedule "${name}" is already registered`);
2727
- }
2728
- try {
2729
- CronExpressionParser.parse(String(cron));
2730
- } catch (error) {
2731
- throw new Error(
2732
- `invalid cron ${JSON.stringify(cron)} for schedule "${name}": ${errorMessage(error)}`
2733
- );
2734
- }
2735
- if (typeof fn !== "function") {
2736
- throw new Error(`schedule "${name}" must provide a function`);
2737
- }
2738
- schedules.push({ name, cron: String(cron), fn });
3080
+ schedules.push(
3081
+ validateScheduleRegistration(
3082
+ name,
3083
+ cron,
3084
+ fn,
3085
+ schedules,
3086
+ (expression) => {
3087
+ CronExpressionParser.parse(expression);
3088
+ }
3089
+ )
3090
+ );
2739
3091
  }
2740
3092
  };
2741
3093
  const cliRecord = {
@@ -2744,46 +3096,12 @@ function createFakePluginHostInternal(options, sharedState) {
2744
3096
  const cli = {
2745
3097
  register(registration) {
2746
3098
  assertLive();
2747
- if (cliRecord.registration !== null) {
2748
- throw new Error("cli command is already registered");
2749
- }
2750
- const name = registration?.name;
2751
- if (typeof name !== "string" || !CLI_COMMAND_NAME_PATTERN.test(name)) {
2752
- throw new Error(
2753
- `invalid cli command name ${JSON.stringify(name)} \u2014 use lowercase letters, digits, and "-"`
2754
- );
2755
- }
2756
- if (typeof registration.summary !== "string" || registration.summary.trim().length === 0) {
2757
- throw new Error(`cli command "${name}" must provide a summary`);
2758
- }
2759
- const commands = registration.commands ?? [];
2760
- if (!Array.isArray(commands)) {
2761
- throw new Error(`cli command "${name}" commands must be an array`);
2762
- }
2763
- const validatedCommands = commands.map((command, index) => {
2764
- if (typeof command?.name !== "string" || !CLI_COMMAND_NAME_PATTERN.test(command.name) || typeof command.summary !== "string" || typeof command.usage !== "string") {
2765
- throw new Error(
2766
- `cli command "${name}" commands[${index}] must be { name: [a-z0-9-]+, summary, usage }`
2767
- );
2768
- }
2769
- return {
2770
- name: command.name,
2771
- summary: command.summary,
2772
- usage: command.usage
2773
- };
2774
- });
2775
- if (typeof registration.run !== "function") {
2776
- throw new Error(
2777
- `cli command "${name}" must provide a run(argv, ctx) function`
2778
- );
2779
- }
2780
- cliRecord.registration = {
2781
- name,
2782
- summary: registration.summary,
2783
- commands: validatedCommands,
2784
- run: registration.run.bind(registration)
2785
- };
2786
- const warning = pluginCliCollisionWarning(pluginId, name);
3099
+ const record = normalizeCliRegistration(
3100
+ registration,
3101
+ cliRecord.registration !== null
3102
+ );
3103
+ cliRecord.registration = record;
3104
+ const warning = pluginCliCollisionWarning(pluginId, record.name);
2787
3105
  if (warning) emitLog("warn", warning);
2788
3106
  }
2789
3107
  };
@@ -2869,93 +3187,13 @@ function createFakePluginHostInternal(options, sharedState) {
2869
3187
  },
2870
3188
  registerTool(tool) {
2871
3189
  assertLive();
2872
- const name = tool?.name;
2873
- if (typeof name !== "string" || !AGENT_TOOL_NAME_PATTERN.test(name)) {
2874
- throw new Error(
2875
- `invalid tool name ${JSON.stringify(name)} \u2014 use letters, digits, "-" and "_"`
2876
- );
2877
- }
2878
- if (RESERVED_AGENT_TOOL_NAMES.includes(name)) {
2879
- throw new Error(
2880
- `tool name "${name}" is a built-in bb tool \u2014 pick another name`
2881
- );
2882
- }
2883
- rejectStaleAgentToolFields(name, tool);
2884
- if (typeof tool.description !== "string" || tool.description.trim().length === 0) {
2885
- throw new Error(`tool "${name}" must provide a description`);
2886
- }
2887
- if (tool.instructions !== void 0 && typeof tool.instructions !== "string") {
2888
- throw new Error(`tool "${name}" instructions must be a string`);
2889
- }
2890
- if (typeof tool.instructions === "string" && tool.instructions.length > PLUGIN_AGENT_STATIC_INSTRUCTIONS_MAX_CHARS) {
2891
- throw new Error(
2892
- `tool "${name}" instructions exceed the ${PLUGIN_AGENT_STATIC_INSTRUCTIONS_MAX_CHARS}-character limit`
2893
- );
2894
- }
2895
- const presentation = parsePluginAgentToolPresentation(
2896
- name,
2897
- tool.presentation
2898
- );
2899
- if (presentation?.icon !== void 0) {
2900
- const problem = undeclaredIconProblem(
2901
- pluginId,
2902
- declaredIconNames,
2903
- presentation.icon.glyph
2904
- );
2905
- if (problem !== null) {
2906
- throw new Error(agentToolIconRefusalMessage(name, problem));
2907
- }
2908
- }
2909
- if (typeof tool.execute !== "function") {
2910
- throw new Error(
2911
- `tool "${name}" must provide an execute(params, ctx) function`
2912
- );
2913
- }
2914
- const parameters = tool.parameters;
2915
- let inputSchema;
2916
- let parse;
2917
- if (isZodSchemaLike(parameters)) {
2918
- try {
2919
- inputSchema = zodSchemaToJsonSchema(parameters);
2920
- } catch (error) {
2921
- throw new Error(
2922
- `tool "${name}" parameters look like a zod schema but could not be converted to JSON Schema (${errorMessage(error)}) \u2014 use zod 4, or pass a plain JSON-schema object`
2923
- );
2924
- }
2925
- parse = (input) => {
2926
- const result = parameters.safeParse(input);
2927
- if (result.success) return { ok: true, value: result.data };
2928
- return { ok: false, error: summarizeParseIssues(result.error) };
2929
- };
2930
- } else if (typeof parameters === "object" && parameters !== null && !Array.isArray(parameters)) {
2931
- try {
2932
- inputSchema = JSON.parse(JSON.stringify(parameters));
2933
- } catch {
2934
- throw new Error(
2935
- `tool "${name}" parameters JSON schema is not JSON-serializable`
2936
- );
2937
- }
2938
- parse = (input) => ({ ok: true, value: input });
2939
- } else {
2940
- throw new Error(
2941
- `tool "${name}" parameters must be a zod schema or a JSON-schema object`
2942
- );
2943
- }
2944
- assertNoRecursiveJsonSchemaReferences(
2945
- inputSchema,
2946
- `tool "${name}" parameters`
2947
- );
2948
- const record = {
2949
- name,
2950
- description: tool.description,
2951
- presentation,
2952
- instructions: tool.instructions !== void 0 && tool.instructions.trim().length > 0 ? tool.instructions : null,
2953
- inputSchema,
2954
- parse,
2955
- execute: tool.execute.bind(tool)
2956
- };
2957
- if (agentTools.some((existing) => existing.name === name)) {
2958
- throw new Error(`tool "${name}" is already registered`);
3190
+ const record = normalizeAgentToolRegistration({
3191
+ pluginId,
3192
+ declaredIconNames,
3193
+ tool
3194
+ });
3195
+ if (agentTools.some((existing) => existing.name === record.name)) {
3196
+ throw new Error(`tool "${record.name}" is already registered`);
2959
3197
  }
2960
3198
  agentTools.push(record);
2961
3199
  }
@@ -2965,35 +3203,9 @@ function createFakePluginHostInternal(options, sharedState) {
2965
3203
  requestInput,
2966
3204
  registerMentionProvider(provider) {
2967
3205
  assertLive();
2968
- const id = provider?.id;
2969
- if (typeof id !== "string" || !MENTION_PROVIDER_ID_PATTERN.test(id)) {
2970
- throw new Error(
2971
- `invalid mention provider id ${JSON.stringify(id)} \u2014 use letters, digits, "-" and "_"`
2972
- );
2973
- }
2974
- if (mentionProviders.some((record) => record.id === id)) {
2975
- throw new Error(`mention provider "${id}" is already registered`);
2976
- }
2977
- if (typeof provider.label !== "string" || provider.label.trim().length === 0) {
2978
- throw new Error(`mention provider "${id}" must provide a label`);
2979
- }
2980
- if (typeof provider.search !== "function") {
2981
- throw new Error(
2982
- `mention provider "${id}" must provide a search({ query, projectId, threadId }) function`
2983
- );
2984
- }
2985
- if (typeof provider.resolve !== "function") {
2986
- throw new Error(
2987
- `mention provider "${id}" must provide a resolve(itemId) function`
2988
- );
2989
- }
2990
- mentionProviders.push({
2991
- id,
2992
- label: provider.label.trim(),
2993
- triggers: normalizeMentionProviderTriggers(id, provider.triggers),
2994
- search: provider.search.bind(provider),
2995
- resolve: provider.resolve.bind(provider)
2996
- });
3206
+ mentionProviders.push(
3207
+ normalizeMentionProviderRegistration(provider, mentionProviders)
3208
+ );
2997
3209
  }
2998
3210
  };
2999
3211
  let requestedDrains = 0;
@@ -3056,47 +3268,10 @@ function createFakePluginHostInternal(options, sharedState) {
3056
3268
  const pendingInteractions = /* @__PURE__ */ new Map();
3057
3269
  function requestInput(request, requestOptions) {
3058
3270
  assertLive();
3059
- if (!request || typeof request !== "object") {
3060
- throw new Error("ui.requestInput requires an options object");
3061
- }
3062
- if (typeof request.threadId !== "string" || request.threadId.length === 0) {
3063
- throw new Error("ui.requestInput threadId must be a non-empty string");
3064
- }
3065
- if (typeof request.rendererId !== "string" || !/^[a-zA-Z0-9_-]+$/.test(request.rendererId)) {
3066
- throw new Error(
3067
- "ui.requestInput rendererId must use letters, digits, '-' or '_'"
3068
- );
3069
- }
3070
- if (typeof request.title !== "string" || request.title.trim().length === 0 || request.title.trim().length > PLUGIN_INTERACTION_MAX_TITLE_LENGTH) {
3071
- throw new Error(
3072
- `ui.requestInput title must be 1-${PLUGIN_INTERACTION_MAX_TITLE_LENGTH} characters`
3073
- );
3074
- }
3075
- let payload;
3076
- try {
3077
- const json = JSON.stringify(request.payload);
3078
- if (json === void 0) throw new Error();
3079
- if (Buffer.byteLength(json, "utf8") > 64 * 1024) {
3080
- throw new Error("ui.requestInput payload exceeds 64 KiB");
3081
- }
3082
- payload = JSON.parse(json);
3083
- } catch (error) {
3084
- if (error instanceof Error && error.message.includes("64 KiB")) {
3085
- throw error;
3086
- }
3087
- throw new Error("ui.requestInput payload must be JSON-serializable");
3088
- }
3089
- const timeoutMs = request.timeoutMs ?? 10 * 60 * 1e3;
3090
- if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60 * 60 * 1e3) {
3091
- throw new Error(
3092
- "ui.requestInput timeoutMs must be between 1 and 3600000"
3093
- );
3094
- }
3271
+ const normalized = normalizeInteractionRequest(request);
3095
3272
  const normalizedRequest = {
3096
3273
  ...request,
3097
- title: request.title.trim(),
3098
- payload,
3099
- timeoutMs
3274
+ ...normalized
3100
3275
  };
3101
3276
  const id = `fake-interaction-${nextInteractionId++}`;
3102
3277
  return new Promise((resolve2) => {
@@ -3113,7 +3288,7 @@ function createFakePluginHostInternal(options, sharedState) {
3113
3288
  const timer = setTimeout(() => {
3114
3289
  pendingInteractions.delete(id);
3115
3290
  resolve2({ outcome: "cancelled", reason: "timeout" });
3116
- }, timeoutMs);
3291
+ }, normalized.timeoutMs);
3117
3292
  pendingInteractions.set(id, {
3118
3293
  request: normalizedRequest,
3119
3294
  resolve: resolve2,
@@ -3145,7 +3320,13 @@ function createFakePluginHostInternal(options, sharedState) {
3145
3320
  });
3146
3321
  }
3147
3322
  const validatedInput = normalizeRpcJsonResult(
3148
- await validateRpcValue(methodContract.input, input, "input")
3323
+ await validateRpcValue(
3324
+ methodContract.input,
3325
+ input,
3326
+ "input",
3327
+ throwRpcError
3328
+ ),
3329
+ throwRpcError
3149
3330
  );
3150
3331
  const call = {
3151
3332
  method: String(method),
@@ -3163,9 +3344,13 @@ function createFakePluginHostInternal(options, sharedState) {
3163
3344
  const validatedOutput = await validateRpcValue(
3164
3345
  methodContract.output,
3165
3346
  rawOutput,
3166
- "output"
3347
+ "output",
3348
+ throwRpcError
3349
+ );
3350
+ return normalizeRpcJsonResult(
3351
+ validatedOutput,
3352
+ throwRpcError
3167
3353
  );
3168
- return normalizeRpcJsonResult(validatedOutput);
3169
3354
  },
3170
3355
  experimental_onWorkerExit(handler) {
3171
3356
  assertLive();
@@ -3267,40 +3452,22 @@ function createFakePluginHostInternal(options, sharedState) {
3267
3452
  },
3268
3453
  experimental_contributeEnv(providerId, resolve2) {
3269
3454
  assertLive();
3270
- if (typeof providerId !== "string" || providerId.trim().length === 0) {
3271
- throw new Error(
3272
- "provider environment contribution requires a provider id"
3273
- );
3274
- }
3275
- if (providerEnvResolvers.has(providerId)) {
3276
- throw new Error(
3277
- `provider environment contribution for "${providerId}" is already registered`
3278
- );
3279
- }
3280
- if (typeof resolve2 !== "function") {
3281
- throw new Error(
3282
- "provider environment contribution requires a resolver function"
3283
- );
3284
- }
3455
+ validateProviderEnvContribution(
3456
+ "provider environment contribution",
3457
+ providerId,
3458
+ resolve2,
3459
+ providerEnvResolvers
3460
+ );
3285
3461
  providerEnvResolvers.set(providerId, resolve2);
3286
3462
  },
3287
3463
  experimental_contributeEnvHealth(providerId, resolve2) {
3288
3464
  assertLive();
3289
- if (typeof providerId !== "string" || providerId.trim().length === 0) {
3290
- throw new Error(
3291
- "provider environment health contribution requires a provider id"
3292
- );
3293
- }
3294
- if (providerEnvHealthResolvers.has(providerId)) {
3295
- throw new Error(
3296
- `provider environment health contribution for "${providerId}" is already registered`
3297
- );
3298
- }
3299
- if (typeof resolve2 !== "function") {
3300
- throw new Error(
3301
- "provider environment health contribution requires a resolver function"
3302
- );
3303
- }
3465
+ validateProviderEnvContribution(
3466
+ "provider environment health contribution",
3467
+ providerId,
3468
+ resolve2,
3469
+ providerEnvHealthResolvers
3470
+ );
3304
3471
  providerEnvHealthResolvers.set(providerId, resolve2);
3305
3472
  }
3306
3473
  };
@@ -3591,12 +3758,19 @@ function createFakePluginHostInternal(options, sharedState) {
3591
3758
  );
3592
3759
  for (const subscription of subscriptions) {
3593
3760
  const normalized = normalizeRpcJsonResult(
3594
- await validateRpcValue(subscription.payloadSchema, payload, "input")
3761
+ await validateRpcValue(
3762
+ subscription.payloadSchema,
3763
+ payload,
3764
+ "input",
3765
+ throwRpcError
3766
+ ),
3767
+ throwRpcError
3595
3768
  );
3596
3769
  const parsed = await validateRpcValue(
3597
3770
  subscription.payloadSchema,
3598
3771
  normalized,
3599
- "input"
3772
+ "input",
3773
+ throwRpcError
3600
3774
  );
3601
3775
  await subscription.handler({ hostId, payload: parsed });
3602
3776
  }
@@ -3630,7 +3804,8 @@ function createFakePluginHostInternal(options, sharedState) {
3630
3804
  const validatedInput = await validateRpcValue(
3631
3805
  record.inputSchema,
3632
3806
  parsedInput,
3633
- "input"
3807
+ "input",
3808
+ throwRpcError
3634
3809
  );
3635
3810
  let result;
3636
3811
  try {
@@ -3644,9 +3819,10 @@ function createFakePluginHostInternal(options, sharedState) {
3644
3819
  const validatedOutput = await validateRpcValue(
3645
3820
  record.outputSchema,
3646
3821
  result,
3647
- "output"
3822
+ "output",
3823
+ throwRpcError
3648
3824
  );
3649
- return normalizeRpcJsonResult(validatedOutput);
3825
+ return normalizeRpcJsonResult(validatedOutput, throwRpcError);
3650
3826
  },
3651
3827
  async runCli(argv, ctx = {}) {
3652
3828
  const registration = cliRecord.registration;
@@ -3897,12 +4073,15 @@ function createFakePluginHostInternal(options, sharedState) {
3897
4073
  instructions: null
3898
4074
  };
3899
4075
  }
4076
+ const pluginMetadata = deepFreezePluginMetadata(
4077
+ validatePluginMetadata(context.pluginMetadata ?? {})
4078
+ );
3900
4079
  try {
3901
- const normalized = normalizeAgentConfiguration({
4080
+ const normalized = normalizePluginAgentConfiguration({
3902
4081
  knownSkillIds: new Set(agentSkillIds),
3903
4082
  knownToolIds: new Set(agentTools.map((tool) => tool.name)),
3904
4083
  pluginId,
3905
- value: agentConfigurationProvider(context)
4084
+ value: agentConfigurationProvider({ ...context, pluginMetadata })
3906
4085
  });
3907
4086
  const selectedTools = new Set(normalized.toolIds);
3908
4087
  return {
@@ -3997,6 +4176,7 @@ function makeThreadResponse(overrides = {}) {
3997
4176
  }
3998
4177
  function makePluginAgentConfigurationContext(overrides = {}) {
3999
4178
  const context = {
4179
+ pluginMetadata: {},
4000
4180
  thread: {
4001
4181
  id: "thread-test",
4002
4182
  title: null,
@@ -4025,6 +4205,7 @@ function makePluginAgentConfigurationContext(overrides = {}) {
4025
4205
  origin: { kind: null, pluginId: null }
4026
4206
  };
4027
4207
  return {
4208
+ pluginMetadata: overrides.pluginMetadata ?? context.pluginMetadata,
4028
4209
  thread: { ...context.thread, ...overrides.thread },
4029
4210
  project: { ...context.project, ...overrides.project },
4030
4211
  environment: { ...context.environment, ...overrides.environment },
@@ -4098,25 +4279,7 @@ function makeMessageDispatchHookContext(overrides = {}) {
4098
4279
  createdAt: 0,
4099
4280
  updatedAt: 0
4100
4281
  };
4101
- const hostDefaults = {
4102
- id: "host-1",
4103
- name: "Test host",
4104
- type: "persistent",
4105
- status: "connected",
4106
- machineProviderId: null,
4107
- lifecycle: {
4108
- phase: "active",
4109
- suspendedAt: null,
4110
- message: null,
4111
- pendingLog: "",
4112
- teardown: null
4113
- },
4114
- maxPermissionMode: "full",
4115
- lastSeenAt: null,
4116
- lastRejectedProtocolVersion: null,
4117
- createdAt: 0,
4118
- updatedAt: 0
4119
- };
4282
+ const hostDefaults = makeHostResponse();
4120
4283
  const project = { ...context.project, ...overrides.project };
4121
4284
  const host = overrides.host === void 0 ? context.host : overrides.host === null ? null : { ...hostDefaults, ...overrides.host };
4122
4285
  const environment = overrides.environment === void 0 ? context.environment : overrides.environment === null ? null : {