@get-bb/plugin-sdk 0.4.56 → 0.4.87

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,17 +1,5 @@
1
- // src/testing/fake-plugin-host.ts
2
- import { createHash } from "node:crypto";
3
- import { mkdtempSync, rmSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
- import Database from "better-sqlite3";
7
- import { CronExpressionParser } from "cron-parser";
8
- import { Hono } from "hono";
9
-
10
- // ../domain/src/plugin-interaction-limits.ts
11
- var PLUGIN_INTERACTION_MAX_TITLE_LENGTH = 160;
12
- var PLUGIN_INTERACTION_MAX_PAYLOAD_BYTES = 64 * 1024;
13
-
14
1
  // src/internal/host-policy.ts
2
+ import { createHash } from "node:crypto";
15
3
  import { z as z4 } from "zod";
16
4
 
17
5
  // ../domain/src/plugin-icon.ts
@@ -57,6 +45,10 @@ var RESERVED_BB_CLI_COMMANDS = [
57
45
  "voice"
58
46
  ];
59
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
+
60
52
  // ../domain/src/provider-fork.ts
61
53
  import { z } from "zod";
62
54
  var PROVIDER_FORK_VALUES = ["none", "tip", "checkpoint"];
@@ -286,6 +278,51 @@ var providerNativeRootSetSchema = z3.object({
286
278
  resolved: providerResolvedNativeRootsSchema
287
279
  }).strict();
288
280
 
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
+ }
325
+
289
326
  // src/backend-contract.ts
290
327
  var PLUGIN_CLI_OUTPUT_MAX_BYTES = 1024 * 1024;
291
328
 
@@ -319,8 +356,7 @@ var pluginProviderEnvEntrySchema = z4.object({
319
356
  z4.string(),
320
357
  z4.object({ serverPath: z4.string().startsWith("/") }).strict()
321
358
  ]),
322
- reason: z4.string(),
323
- secret: z4.boolean()
359
+ reason: z4.string()
324
360
  }).strict();
325
361
  var pluginProviderEnvEntriesSchema = z4.array(pluginProviderEnvEntrySchema).max(PLUGIN_PROVIDER_ENV_MAX_ENTRIES).superRefine((entries, context) => {
326
362
  const names = /* @__PURE__ */ new Set();
@@ -528,6 +564,22 @@ function validateSettingsUpdate(descriptors, values) {
528
564
  }
529
565
  return errors;
530
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
+ }
531
583
  var PLUGIN_MENTION_TRIGGER_VALUES = [
532
584
  "@",
533
585
  "#",
@@ -1769,79 +1821,64 @@ function pluginHookAlreadyRegisteredMessage(hook) {
1769
1821
  }
1770
1822
  var ENVIRONMENT_PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,63}$/;
1771
1823
  var ENVIRONMENT_PROVIDER_DISPLAY_NAME_MAX_CHARS = 80;
1824
+ var ENVIRONMENT_PROVIDER_DESCRIPTION_MAX_CHARS = 200;
1772
1825
  var ENVIRONMENT_PROVIDER_REQUIREMENT_NAMES = [
1773
1826
  "projectCheckout",
1774
1827
  "gitCheckout",
1775
1828
  "gitRemote",
1776
1829
  "projectless"
1777
1830
  ];
1778
- function validatePluginEnvironmentProviderDeclaration(declaration) {
1831
+ var environmentCompositionSchema = z4.object({
1832
+ id: z4.string().regex(ENVIRONMENT_PROVIDER_ID_PATTERN),
1833
+ displayName: z4.string().trim().min(1).max(ENVIRONMENT_PROVIDER_DISPLAY_NAME_MAX_CHARS),
1834
+ description: z4.string().trim().min(1).max(ENVIRONMENT_PROVIDER_DESCRIPTION_MAX_CHARS).optional().transform((value) => value ?? null),
1835
+ icon: z4.string().trim().min(1).transform(
1836
+ (icon) => isPluginOwnedIconPath(icon) ? validateProviderRelativePath(icon, "Composition icon") : icon
1837
+ ).optional().transform((value) => value ?? null),
1838
+ machineProviderId: z4.string().regex(ENVIRONMENT_PROVIDER_ID_PATTERN),
1839
+ environmentProviderId: z4.string().regex(ENVIRONMENT_PROVIDER_ID_PATTERN)
1840
+ }).strict();
1841
+ function requireProviderIdAndDisplayName(noun, declaration) {
1779
1842
  if (typeof declaration !== "object" || declaration === null) {
1780
- throw new Error("environment provider declaration must be an object");
1843
+ throw new Error(`${noun} declaration must be an object`);
1781
1844
  }
1782
1845
  const id = declaration.id;
1783
1846
  if (typeof id !== "string" || !ENVIRONMENT_PROVIDER_ID_PATTERN.test(id)) {
1784
1847
  throw new Error(
1785
- `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`
1786
1849
  );
1787
1850
  }
1788
1851
  const displayName = typeof declaration.displayName === "string" ? declaration.displayName.trim() : "";
1789
1852
  if (displayName.length === 0 || displayName.length > ENVIRONMENT_PROVIDER_DISPLAY_NAME_MAX_CHARS) {
1790
1853
  throw new Error(
1791
- `environment provider "${id}" needs a displayName of 1-${ENVIRONMENT_PROVIDER_DISPLAY_NAME_MAX_CHARS} characters`
1792
- );
1793
- }
1794
- const icon = declaration.icon === void 0 ? null : z4.string().min(1).parse(declaration.icon).trim();
1795
- if (icon !== null) {
1796
- if (isPluginOwnedIconPath(icon))
1797
- validateProviderRelativePath(icon, `"${id}" icon`);
1798
- else if (!isNamespacedGlyph(icon) && /[/\\]/u.test(icon))
1799
- throw new Error(
1800
- `environment provider "${id}" icon must be a glyph, declared icon, or plugin-relative path`
1801
- );
1802
- }
1803
- if (icon !== null && icon.length === 0) {
1804
- throw new Error(`environment provider "${id}" declares an empty icon`);
1805
- }
1806
- const requires = normalizeEnvironmentProviderRequirements(id, declaration);
1807
- const inputs = normalizeEnvironmentProviderInputs(id, declaration);
1808
- if (typeof declaration.create !== "function" || typeof declaration.remove !== "function") {
1809
- throw new Error(
1810
- `environment provider "${id}" must declare create and remove functions`
1854
+ `${noun} "${id}" needs a displayName of 1-${ENVIRONMENT_PROVIDER_DISPLAY_NAME_MAX_CHARS} characters`
1811
1855
  );
1812
1856
  }
1813
- if (declaration.validate !== void 0 && typeof declaration.validate !== "function") {
1857
+ return { id, displayName };
1858
+ }
1859
+ function assertProviderIconShape(noun, id, icon) {
1860
+ if (isPluginOwnedIconPath(icon)) {
1861
+ validateProviderRelativePath(icon, `"${id}" icon`);
1862
+ } else if (!isNamespacedGlyph(icon) && /[/\\]/u.test(icon)) {
1814
1863
  throw new Error(
1815
- `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`
1816
1865
  );
1817
1866
  }
1818
- 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") {
1819
1870
  throw new Error(
1820
- `environment provider "${id}" declares availability that is not a function`
1871
+ `${noun} "${id}" declares ${phrase} that is not a function`
1821
1872
  );
1822
1873
  }
1823
- return {
1824
- id,
1825
- displayName,
1826
- icon,
1827
- requires,
1828
- inputs: inputs === null ? null : inputs.schema,
1829
- inputsJsonSchema: inputs === null ? null : inputs.jsonSchema,
1830
- availability: declaration.availability ?? null,
1831
- validate: declaration.validate ?? null,
1832
- create: declaration.create,
1833
- remove: declaration.remove,
1834
- policy: environmentProviderPolicySchema.parse(declaration.policy ?? {})
1835
- };
1836
1874
  }
1837
- function normalizeEnvironmentProviderInputs(id, declaration) {
1838
- const inputs = declaration.inputs;
1875
+ function normalizeProviderInputs(noun, id, inputs) {
1839
1876
  if (inputs === void 0) {
1840
1877
  return null;
1841
1878
  }
1842
1879
  if (!isStandardSchema(inputs)) {
1843
1880
  throw new Error(
1844
- `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`
1845
1882
  );
1846
1883
  }
1847
1884
  let converted;
@@ -1849,17 +1886,65 @@ function normalizeEnvironmentProviderInputs(id, declaration) {
1849
1886
  converted = JSON.parse(JSON.stringify(standardSchemaToJsonSchema(inputs)));
1850
1887
  } catch (error) {
1851
1888
  throw new Error(
1852
- `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()`
1853
1890
  );
1854
1891
  }
1855
1892
  const jsonSchema = jsonValueSchema.safeParse(converted);
1856
1893
  if (!jsonSchema.success) {
1857
1894
  throw new Error(
1858
- `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`
1859
1896
  );
1860
1897
  }
1861
1898
  return { schema: inputs, jsonSchema: jsonSchema.data };
1862
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
+ }
1863
1948
  function normalizeEnvironmentProviderRequirements(id, declaration) {
1864
1949
  const requires = declaration.requires ?? {};
1865
1950
  if (typeof requires !== "object" || requires === null || Array.isArray(requires)) {
@@ -1891,226 +1976,519 @@ var environmentProviderPolicySchema = z4.object({
1891
1976
  retireGraceMs: z4.number().int().nonnegative().nullable().default(3e5),
1892
1977
  pathKeys: z4.enum(["per-thread", "per-attempt"]).default("per-thread")
1893
1978
  }).strict();
1894
-
1895
- // src/testing/fake-sdk.ts
1896
- function withSpawnAttribution(pluginId, args) {
1897
- const [first, ...rest] = args;
1898
- if (typeof first !== "object" || first === null) return args;
1899
- const spawnArgs = first;
1900
- const origin = spawnArgs.origin ?? "plugin";
1901
- return [
1902
- {
1903
- ...spawnArgs,
1904
- origin,
1905
- ...origin === "plugin" ? { originPluginId: spawnArgs.originPluginId ?? pluginId } : {}
1906
- },
1907
- ...rest
1908
- ];
1909
- }
1910
- function createFakeSdk(options) {
1911
- const calls = [];
1912
- const stubs = /* @__PURE__ */ new Map();
1913
- function addOverrides(prefix, value) {
1914
- if (typeof value === "function") {
1915
- stubs.set(prefix, value);
1916
- return;
1917
- }
1918
- if (typeof value !== "object" || value === null) return;
1919
- for (const [key, child] of Object.entries(value)) {
1920
- addOverrides(prefix.length === 0 ? key : `${prefix}.${key}`, child);
1921
- }
1922
- }
1923
- addOverrides("", options.overrides ?? {});
1924
- function invoke(path, rawArgs) {
1925
- const args = path === "threads.spawn" ? withSpawnAttribution(options.pluginId, rawArgs) : rawArgs;
1926
- calls.push({ path, args });
1927
- const stub = stubs.get(path);
1928
- if (!stub) {
1929
- throw new Error(
1930
- `bb.sdk.${path} is not stubbed \u2014 pass an implementation via createFakePluginHost({ sdk: { ... } }) or harness.sdk.stub("${path}", fn)`
1931
- );
1932
- }
1933
- return stub(...args);
1979
+ var MACHINE_PROVIDER_DESCRIPTION_MAX_CHARS = 200;
1980
+ function validatePluginMachineProviderDeclaration(declaration) {
1981
+ const { id, displayName } = requireProviderIdAndDisplayName(
1982
+ "machine provider",
1983
+ declaration
1984
+ );
1985
+ const description = z4.string().trim().min(1).max(MACHINE_PROVIDER_DESCRIPTION_MAX_CHARS).parse(declaration.description);
1986
+ const icon = z4.string().trim().min(1).parse(declaration.icon);
1987
+ assertProviderIconShape("machine provider", id, icon);
1988
+ const ephemeral = z4.boolean().default(false).parse(declaration.ephemeral);
1989
+ const inputs = normalizeProviderInputs(
1990
+ "machine provider",
1991
+ id,
1992
+ declaration.inputs
1993
+ );
1994
+ if (typeof declaration.create !== "function" || typeof declaration.reconcileCleanup !== "function" || typeof declaration.remove !== "function") {
1995
+ throw new Error(
1996
+ `machine provider "${id}" must declare create, reconcileCleanup and remove functions`
1997
+ );
1934
1998
  }
1935
- const nodes = /* @__PURE__ */ new Map();
1936
- function node(path) {
1937
- const cached = nodes.get(path);
1938
- if (cached) return cached;
1939
- const created = new Proxy(function() {
1940
- }, {
1941
- get(_target, prop) {
1942
- if (typeof prop !== "string" || prop === "then") return void 0;
1943
- return node(path === "" ? prop : `${path}.${prop}`);
1944
- },
1945
- apply(_target, _thisArg, args) {
1946
- return invoke(path, args);
1947
- }
1948
- });
1949
- nodes.set(path, created);
1950
- return created;
1999
+ const hasSuspend = typeof declaration.suspend === "function";
2000
+ const hasResume = typeof declaration.resume === "function";
2001
+ if (hasSuspend !== hasResume) {
2002
+ throw new Error(
2003
+ `machine provider "${id}" must declare suspend and resume together`
2004
+ );
1951
2005
  }
1952
- const harness = {
1953
- calls,
1954
- callsTo(path) {
1955
- return calls.filter((call) => call.path === path).map((call) => call.args);
1956
- },
1957
- stub(path, implementation) {
1958
- stubs.set(path, implementation);
1959
- }
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
+ );
2018
+ return {
2019
+ id,
2020
+ displayName,
2021
+ description,
2022
+ icon,
2023
+ ephemeral,
2024
+ inputs: inputs === null ? null : inputs.schema,
2025
+ inputsJsonSchema: inputs === null ? null : inputs.jsonSchema,
2026
+ availability: declaration.availability ?? null,
2027
+ validate: declaration.validate ?? null,
2028
+ reconcileCleanup: declaration.reconcileCleanup,
2029
+ create: declaration.create,
2030
+ suspend: declaration.suspend ?? null,
2031
+ resume: declaration.resume ?? null,
2032
+ remove: declaration.remove
1960
2033
  };
1961
- return { sdk: node(""), harness };
1962
2034
  }
1963
-
1964
- // src/testing/fake-plugin-host.ts
2035
+ function validateServerAccessProviderDeclaration(declaration) {
2036
+ if (typeof declaration !== "object" || declaration === null)
2037
+ throw new Error("Invalid server access provider declaration");
2038
+ if (typeof declaration.id !== "string" || !/^[a-z][a-z0-9-]*$/u.test(declaration.id) || declaration.id === "direct")
2039
+ throw new Error("Invalid or reserved server access provider id");
2040
+ if (typeof declaration.displayName !== "string" || !declaration.displayName.trim() || typeof declaration.description !== "string" || !declaration.description.trim() || typeof declaration.availability !== "function" || typeof declaration.acquire !== "function" || typeof declaration.release !== "function")
2041
+ throw new Error("Invalid server access provider declaration");
2042
+ return declaration;
2043
+ }
1965
2044
  var LEGACY_UNKNOWN_MIGRATION_HASH = "legacy-unknown";
1966
2045
  function migrationStatementHash(statement) {
1967
2046
  return createHash("sha256").update(statement).digest("hex");
1968
2047
  }
1969
- var PluginContextStaleError = class extends Error {
1970
- constructor(pluginId) {
1971
- super(
1972
- `plugin "${pluginId}" used a stale API handle \u2014 it was reloaded or disabled; re-entry happens via a fresh factory call`
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) {
2065
+ throw new Error(
2066
+ `migration ${index} does not match the recorded statement; append a new migration instead of changing or reusing an index`
2067
+ );
2068
+ }
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
+ );
2083
+ }
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
+ })();
2090
+ }
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(", ")}`
1973
2096
  );
1974
- this.name = "PluginContextStaleError";
1975
2097
  }
1976
- };
1977
- function readSettingsValues(descriptors, stored) {
1978
- const values = {};
1979
- for (const [key, descriptor] of Object.entries(descriptors)) {
1980
- let value = stored.get(key);
1981
- if (descriptor.type === "number" && typeof value === "string") {
1982
- const legacyNumber = Number(value.trim());
1983
- value = value.trim().length > 0 && Number.isFinite(legacyNumber) ? legacyNumber : void 0;
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 };
2122
+ }
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)}`
2127
+ );
2128
+ }
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
+ );
1984
2165
  }
1985
- if (descriptor.type === "number" && typeof value === "number" && !Number.isFinite(value)) {
1986
- 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`);
1987
2170
  }
1988
- const expected = descriptor.type === "boolean" ? "boolean" : descriptor.type === "number" ? "number" : "string";
1989
- if (typeof value !== expected) value = void 0;
1990
- if (descriptor.type === "select" && typeof value === "string" && !descriptor.options.includes(value)) {
1991
- value = void 0;
2171
+ if (registered.has(name)) {
2172
+ throw new Error(`rpc method "${name}" is already registered`);
1992
2173
  }
1993
- values[key] = value ?? descriptor.default;
2174
+ pending.push([
2175
+ name,
2176
+ {
2177
+ inputSchema: methodContract.input,
2178
+ outputSchema: methodContract.output,
2179
+ handler
2180
+ }
2181
+ ]);
1994
2182
  }
1995
- return values;
1996
- }
1997
- function isNeedsConfigurationError(error) {
1998
- return error instanceof Error && error.name === "NeedsConfigurationError";
2183
+ return pending;
1999
2184
  }
2000
- function errorMessage(error) {
2001
- return error instanceof Error ? error.message : String(error);
2002
- }
2003
- function jsonRoundTrip(value, what) {
2004
- 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;
2005
2190
  let json;
2006
2191
  try {
2007
- json = JSON.stringify(value);
2192
+ json = JSON.stringify(payload);
2008
2193
  } catch {
2009
2194
  json = void 0;
2010
2195
  }
2011
2196
  if (json === void 0) {
2012
- throw new Error(`${what} is not JSON-serializable`);
2197
+ throw new Error(
2198
+ `realtime payload for channel "${channel}" is not JSON-serializable`
2199
+ );
2013
2200
  }
2014
2201
  return JSON.parse(json);
2015
2202
  }
2016
- function normalizeRpcIssues(issues) {
2017
- return issues.map((issue) => {
2018
- const rawPath = issue.path;
2019
- const segments = rawPath === void 0 ? [] : Array.isArray(rawPath) ? rawPath : [rawPath];
2020
- const path = segments.map((segment) => {
2021
- const key = typeof segment === "object" && segment !== null ? Reflect.get(segment, "key") : segment;
2022
- return typeof key === "number" ? key : String(key);
2023
- });
2024
- return {
2025
- message: issue.message,
2026
- ...path.length > 0 ? { path } : {}
2027
- };
2028
- });
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) };
2029
2218
  }
2030
- function throwRpcError(error) {
2031
- const thrown = new Error(error.message);
2032
- Reflect.set(thrown, "code", error.code);
2033
- if (error.issues !== void 0) Reflect.set(thrown, "issues", error.issues);
2034
- throw thrown;
2035
- }
2036
- async function validateRpcValue(schema, value, phase) {
2037
- 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
+ }
2038
2228
  try {
2039
- result = await schema["~standard"].validate(value);
2229
+ parseCron(String(cron));
2040
2230
  } catch (error) {
2041
- const message = errorMessage(error);
2042
- return throwRpcError({
2043
- code: phase === "input" ? "invalid_input" : "invalid_output",
2044
- message: `rpc ${phase} validator failed: ${message}`,
2045
- issues: [{ message }]
2046
- });
2231
+ throw new Error(
2232
+ `invalid cron ${JSON.stringify(cron)} for schedule "${name}": ${error instanceof Error ? error.message : String(error)}`
2233
+ );
2047
2234
  }
2048
- if (result.issues !== void 0) {
2049
- return throwRpcError({
2050
- code: phase === "input" ? "invalid_input" : "invalid_output",
2051
- message: `rpc ${phase} validation failed`,
2052
- issues: normalizeRpcIssues(result.issues)
2053
- });
2235
+ if (typeof fn !== "function") {
2236
+ throw new Error(`schedule "${name}" must provide a function`);
2054
2237
  }
2055
- return result.value;
2238
+ return { name, cron: String(cron), fn };
2056
2239
  }
2057
- function normalizeRpcJsonResult(value) {
2058
- const ancestors = /* @__PURE__ */ new Set();
2059
- function visit(current, path) {
2060
- if (current === null || typeof current === "string" || typeof current === "boolean") {
2061
- return current;
2062
- }
2063
- if (typeof current === "number") {
2064
- if (!Number.isFinite(current)) {
2065
- return throwRpcError({
2066
- code: "non_json_result",
2067
- message: `rpc result at ${path} contains a non-finite number`
2068
- });
2069
- }
2070
- 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
+ );
2071
2262
  }
2072
- if (typeof current !== "object") {
2073
- return throwRpcError({
2074
- code: "non_json_result",
2075
- message: `rpc result at ${path} is not a JSON value (${typeof current})`
2076
- });
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));
2077
2318
  }
2078
- if (ancestors.has(current)) {
2079
- return throwRpcError({
2080
- code: "non_json_result",
2081
- message: `rpc result at ${path} is cyclic`
2082
- });
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
+ );
2083
2335
  }
2084
- 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)) {
2085
2342
  try {
2086
- if (Array.isArray(current)) {
2087
- return current.map((item, index) => visit(item, `${path}[${index}]`));
2088
- }
2089
- const prototype = Object.getPrototypeOf(current);
2090
- if (prototype !== Object.prototype && prototype !== null) {
2091
- return throwRpcError({
2092
- code: "non_json_result",
2093
- message: `rpc result at ${path} must be a plain JSON object`
2094
- });
2095
- }
2096
- if (Reflect.ownKeys(current).some((key) => typeof key === "symbol")) {
2097
- return throwRpcError({
2098
- code: "non_json_result",
2099
- message: `rpc result at ${path} contains a symbol key`
2100
- });
2101
- }
2102
- const normalized = {};
2103
- for (const [key, child] of Object.entries(current)) {
2104
- normalized[key] = visit(child, `${path}.${key}`);
2105
- }
2106
- return normalized;
2107
- } finally {
2108
- 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
+ );
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
+ );
2354
+ }
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");
2109
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");
2110
2430
  }
2111
- return visit(value, "$result");
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;
2112
2490
  }
2113
- function normalizeAgentToolSelections(args) {
2491
+ function normalizePluginAgentToolSelections(args) {
2114
2492
  if (!Array.isArray(args.value)) {
2115
2493
  throw new Error("configure() output.tools must be an array");
2116
2494
  }
@@ -2137,7 +2515,7 @@ function normalizeAgentToolSelections(args) {
2137
2515
  );
2138
2516
  }
2139
2517
  name = typed.name;
2140
- parameters = normalizeAgentToolParameters({
2518
+ parameters = normalizePluginAgentToolParameters({
2141
2519
  index,
2142
2520
  value: typed.parameters
2143
2521
  });
@@ -2167,50 +2545,13 @@ function normalizeAgentToolSelections(args) {
2167
2545
  }
2168
2546
  return { toolIds, parameterOverrides };
2169
2547
  }
2170
- function normalizeAgentToolParameters(args) {
2171
- const { index, value } = args;
2172
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
2173
- throw new Error(
2174
- `configure() output.tools[${index}].parameters must be a JSON-schema object`
2175
- );
2176
- }
2177
- let serialized;
2178
- try {
2179
- serialized = JSON.stringify(value);
2180
- } catch {
2181
- throw new Error(
2182
- `configure() output.tools[${index}].parameters is not JSON-serializable`
2183
- );
2184
- }
2185
- if (serialized === void 0) {
2186
- throw new Error(
2187
- `configure() output.tools[${index}].parameters is not JSON-serializable`
2188
- );
2189
- }
2190
- if (Buffer.byteLength(serialized, "utf8") > PLUGIN_AGENT_TOOL_PARAMETERS_MAX_BYTES) {
2191
- throw new Error(
2192
- `configure() output.tools[${index}].parameters exceeds the ${PLUGIN_AGENT_TOOL_PARAMETERS_MAX_BYTES}-byte limit`
2193
- );
2194
- }
2195
- const parameters = JSON.parse(serialized);
2196
- if (parameters.type !== "object") {
2197
- throw new Error(
2198
- `configure() output.tools[${index}].parameters must have root type "object"`
2199
- );
2200
- }
2201
- assertNoRecursiveJsonSchemaReferences(
2202
- parameters,
2203
- `configure() output.tools[${index}].parameters`
2204
- );
2205
- return parameters;
2206
- }
2207
- function normalizeAgentConfigurationIds(args) {
2548
+ function normalizePluginAgentSelectionIds(args) {
2208
2549
  if (!Array.isArray(args.value)) {
2209
- throw new Error(`configure() output.${args.field} must be an array`);
2550
+ throw new Error("configure() output.skills must be an array");
2210
2551
  }
2211
2552
  if (args.value.length > PLUGIN_AGENT_SELECTION_MAX_IDS) {
2212
2553
  throw new Error(
2213
- `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`
2214
2555
  );
2215
2556
  }
2216
2557
  const selected = [];
@@ -2219,12 +2560,12 @@ function normalizeAgentConfigurationIds(args) {
2219
2560
  const id = args.value[index];
2220
2561
  if (typeof id !== "string" || id.length === 0) {
2221
2562
  throw new Error(
2222
- `configure() output.${args.field}[${index}] must be a non-empty string`
2563
+ `configure() output.skills[${index}] must be a non-empty string`
2223
2564
  );
2224
2565
  }
2225
2566
  if (seen.has(id)) {
2226
2567
  throw new Error(
2227
- `configure() output.${args.field} contains duplicate id ${JSON.stringify(id)}`
2568
+ `configure() output.skills contains duplicate id ${JSON.stringify(id)}`
2228
2569
  );
2229
2570
  }
2230
2571
  if (!args.knownIds.has(id)) {
@@ -2237,7 +2578,7 @@ function normalizeAgentConfigurationIds(args) {
2237
2578
  }
2238
2579
  return selected;
2239
2580
  }
2240
- function normalizeAgentConfiguration(args) {
2581
+ function normalizePluginAgentConfiguration(args) {
2241
2582
  if (typeof args.value !== "object" || args.value === null || Array.isArray(args.value)) {
2242
2583
  throw new Error(
2243
2584
  "configure() must return { tools: string[], skills: string[], instructions?: string }"
@@ -2253,25 +2594,281 @@ function normalizeAgentConfiguration(args) {
2253
2594
  if (output.instructions !== void 0 && typeof output.instructions !== "string") {
2254
2595
  throw new Error("configure() output.instructions must be a string");
2255
2596
  }
2256
- const toolSelections = normalizeAgentToolSelections({
2257
- knownIds: args.knownToolIds,
2258
- pluginId: args.pluginId,
2259
- value: output.tools
2260
- });
2261
- return {
2262
- toolIds: toolSelections.toolIds,
2263
- toolParameterOverrides: toolSelections.parameterOverrides,
2264
- skillIds: normalizeAgentConfigurationIds({
2265
- field: "skills",
2266
- knownIds: args.knownSkillIds,
2267
- pluginId: args.pluginId,
2268
- value: output.skills
2269
- }),
2270
- instructions: typeof output.instructions === "string" && output.instructions.trim().length > 0 ? output.instructions.slice(
2271
- 0,
2272
- PLUGIN_AGENT_DYNAMIC_INSTRUCTIONS_MAX_CHARS
2273
- ) : null
2274
- };
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));
2845
+ }
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;
2275
2872
  }
2276
2873
  var fakeHostDisposers = /* @__PURE__ */ new WeakMap();
2277
2874
  function createFakePluginHost(options = {}) {
@@ -2353,49 +2950,7 @@ function createFakePluginHostInternal(options, sharedState) {
2353
2950
  },
2354
2951
  migrate(database, statements) {
2355
2952
  assertLive();
2356
- database.exec(
2357
- "CREATE TABLE IF NOT EXISTS _bb_migrations (id INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL, statement_hash TEXT)"
2358
- );
2359
- const migrationColumns = database.prepare("PRAGMA table_info(_bb_migrations)").all();
2360
- if (!migrationColumns.some((column) => column.name === "statement_hash")) {
2361
- database.exec(
2362
- "ALTER TABLE _bb_migrations ADD COLUMN statement_hash TEXT"
2363
- );
2364
- }
2365
- const rows = database.prepare(
2366
- "SELECT id, statement_hash FROM _bb_migrations ORDER BY id"
2367
- ).all();
2368
- const applied = /* @__PURE__ */ new Map();
2369
- for (const row of rows) applied.set(row.id, row.statement_hash);
2370
- const statementHashes = statements.map(migrationStatementHash);
2371
- statementHashes.forEach((statementHash, index) => {
2372
- const recordedHash = applied.get(index);
2373
- if (recordedHash !== void 0 && recordedHash !== null && recordedHash !== statementHash) {
2374
- throw new Error(
2375
- `migration ${index} does not match the recorded statement; append a new migration instead of changing or reusing an index`
2376
- );
2377
- }
2378
- });
2379
- const adopt = database.prepare(
2380
- "UPDATE _bb_migrations SET statement_hash = ? WHERE id = ? AND statement_hash IS NULL"
2381
- );
2382
- const record = database.prepare(
2383
- "INSERT INTO _bb_migrations (id, applied_at, statement_hash) VALUES (?, ?, ?)"
2384
- );
2385
- database.transaction(() => {
2386
- for (const row of rows) {
2387
- if (row.statement_hash !== null) continue;
2388
- adopt.run(
2389
- statementHashes[row.id] ?? LEGACY_UNKNOWN_MIGRATION_HASH,
2390
- row.id
2391
- );
2392
- }
2393
- statements.forEach((statement, index) => {
2394
- if (applied.has(index)) return;
2395
- database.exec(statement);
2396
- record.run(index, Date.now(), statementHashes[index]);
2397
- });
2398
- })();
2953
+ runPluginStorageMigrations(database, statements);
2399
2954
  }
2400
2955
  };
2401
2956
  const settingsDescriptors = {};
@@ -2468,107 +3023,35 @@ function createFakePluginHostInternal(options, sharedState) {
2468
3023
  const http = {
2469
3024
  route(method, path, handler, opts) {
2470
3025
  assertLive();
2471
- const normalizedMethod = String(method).toUpperCase();
2472
- if (!PLUGIN_HTTP_METHODS.has(normalizedMethod)) {
2473
- throw new Error(
2474
- `invalid http method "${String(method)}" \u2014 use one of: ${[...PLUGIN_HTTP_METHODS].join(", ")}`
2475
- );
2476
- }
2477
- if (typeof path !== "string" || !path.startsWith("/")) {
2478
- throw new Error(
2479
- `http route path must be a string starting with "/", got ${JSON.stringify(path)}`
2480
- );
2481
- }
2482
- if (typeof handler !== "function") {
2483
- throw new Error(
2484
- `http route handler for ${normalizedMethod} ${path} must be a function`
2485
- );
2486
- }
2487
- const auth = opts?.auth ?? "local";
2488
- if (auth !== "local" && auth !== "token" && auth !== "none") {
2489
- throw new Error(
2490
- `invalid auth mode "${String(auth)}" for ${normalizedMethod} ${path} \u2014 use "local", "token", or "none"`
2491
- );
2492
- }
2493
- if (httpRoutes.some(
2494
- (route) => route.method === normalizedMethod && route.path === path
2495
- )) {
2496
- throw new Error(
2497
- `http route ${normalizedMethod} ${path} is already registered`
2498
- );
2499
- }
2500
- 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 });
2501
3034
  },
2502
3035
  experimental_websocket(path, handler, opts) {
2503
3036
  assertLive();
2504
- if (typeof path !== "string" || !path.startsWith("/")) {
2505
- throw new Error(
2506
- `websocket route path must be a string starting with "/", got ${JSON.stringify(path)}`
2507
- );
2508
- }
2509
- if (typeof handler !== "function") {
2510
- throw new Error(
2511
- `websocket route handler for ${path} must be a function`
2512
- );
2513
- }
2514
- const auth = opts?.auth ?? "local";
2515
- if (auth !== "local" && auth !== "token" && auth !== "none") {
2516
- throw new Error(
2517
- `invalid auth mode "${String(auth)}" for websocket ${path} \u2014 use "local", "token", or "none"`
2518
- );
2519
- }
2520
- if (websocketRoutes.some((route) => route.path === path)) {
2521
- throw new Error(`websocket route ${path} is already registered`);
2522
- }
2523
- websocketRoutes.push({ path, auth, handler });
3037
+ const route = normalizeWebSocketRouteRegistration(
3038
+ path,
3039
+ handler,
3040
+ opts,
3041
+ websocketRoutes
3042
+ );
3043
+ websocketRoutes.push({ ...route, handler });
2524
3044
  }
2525
3045
  };
2526
3046
  const rpcHandlers = /* @__PURE__ */ new Map();
2527
3047
  const rpc = {
2528
3048
  register(contract, handlers) {
2529
3049
  assertLive();
2530
- if (typeof contract !== "object" || contract === null || Array.isArray(contract)) {
2531
- throw new Error("rpc.register contract must be an object");
2532
- }
2533
- if (typeof handlers !== "object" || handlers === null || Array.isArray(handlers)) {
2534
- throw new Error("rpc.register handlers must be an object");
2535
- }
2536
- const pending = [];
2537
- const contractEntries = Object.entries(contract);
2538
- const contractNames = new Set(contractEntries.map(([name]) => name));
2539
- for (const extraName of Object.keys(handlers)) {
2540
- if (!contractNames.has(extraName)) {
2541
- throw new Error(
2542
- `rpc handler "${extraName}" has no matching contract method`
2543
- );
2544
- }
2545
- }
2546
- for (const [name, contractValue] of contractEntries) {
2547
- if (!RPC_METHOD_PATTERN.test(name)) {
2548
- throw new Error(
2549
- `invalid rpc method name "${name}" \u2014 use dot-separated segments with letters, digits, "-" and "_"`
2550
- );
2551
- }
2552
- const methodContract = readRpcMethodContract(name, contractValue);
2553
- const handler = Reflect.get(handlers, name);
2554
- if (typeof handler !== "function") {
2555
- throw new Error(
2556
- `rpc method "${name}" must provide a handler function`
2557
- );
2558
- }
2559
- if (rpcHandlers.has(name)) {
2560
- throw new Error(`rpc method "${name}" is already registered`);
2561
- }
2562
- pending.push([
2563
- name,
2564
- {
2565
- inputSchema: methodContract.input,
2566
- outputSchema: methodContract.output,
2567
- handler
2568
- }
2569
- ]);
2570
- }
2571
- for (const [name, record] of pending) {
3050
+ for (const [name, record] of normalizeRpcRegistration(
3051
+ contract,
3052
+ handlers,
3053
+ rpcHandlers
3054
+ )) {
2572
3055
  rpcHandlers.set(name, record);
2573
3056
  }
2574
3057
  }
@@ -2577,14 +3060,10 @@ function createFakePluginHostInternal(options, sharedState) {
2577
3060
  const realtime = {
2578
3061
  publish(channel, payload) {
2579
3062
  assertLive();
2580
- if (typeof channel !== "string" || channel.length === 0) {
2581
- throw new Error("realtime channel must be a non-empty string");
2582
- }
2583
- const normalized = payload === void 0 ? null : jsonRoundTrip(
2584
- payload,
2585
- `realtime payload for channel "${channel}"`
2586
- ) ?? null;
2587
- realtimeSignals.push({ channel, payload: normalized });
3063
+ realtimeSignals.push({
3064
+ channel,
3065
+ payload: normalizeRealtimePayload(channel, payload)
3066
+ });
2588
3067
  }
2589
3068
  };
2590
3069
  const services = [];
@@ -2592,42 +3071,23 @@ function createFakePluginHostInternal(options, sharedState) {
2592
3071
  const background = {
2593
3072
  service(name, service) {
2594
3073
  assertLive();
2595
- if (typeof name !== "string" || !BACKGROUND_NAME_PATTERN.test(name)) {
2596
- throw new Error(
2597
- `invalid service name ${JSON.stringify(name)} \u2014 use letters, digits, "-" and "_"`
2598
- );
2599
- }
2600
- if (services.some((record) => record.name === name)) {
2601
- throw new Error(`background service "${name}" is already registered`);
2602
- }
2603
- if (typeof service?.start !== "function") {
2604
- throw new Error(
2605
- `background service "${name}" must provide a start(signal) function`
2606
- );
2607
- }
2608
- services.push({ name, start: service.start.bind(service) });
3074
+ services.push(
3075
+ validateBackgroundServiceRegistration(name, service, services)
3076
+ );
2609
3077
  },
2610
3078
  schedule(name, cron, fn) {
2611
3079
  assertLive();
2612
- if (typeof name !== "string" || !BACKGROUND_NAME_PATTERN.test(name)) {
2613
- throw new Error(
2614
- `invalid schedule name ${JSON.stringify(name)} \u2014 use letters, digits, "-" and "_"`
2615
- );
2616
- }
2617
- if (schedules.some((record) => record.name === name)) {
2618
- throw new Error(`schedule "${name}" is already registered`);
2619
- }
2620
- try {
2621
- CronExpressionParser.parse(String(cron));
2622
- } catch (error) {
2623
- throw new Error(
2624
- `invalid cron ${JSON.stringify(cron)} for schedule "${name}": ${errorMessage(error)}`
2625
- );
2626
- }
2627
- if (typeof fn !== "function") {
2628
- throw new Error(`schedule "${name}" must provide a function`);
2629
- }
2630
- 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
+ );
2631
3091
  }
2632
3092
  };
2633
3093
  const cliRecord = {
@@ -2636,46 +3096,12 @@ function createFakePluginHostInternal(options, sharedState) {
2636
3096
  const cli = {
2637
3097
  register(registration) {
2638
3098
  assertLive();
2639
- if (cliRecord.registration !== null) {
2640
- throw new Error("cli command is already registered");
2641
- }
2642
- const name = registration?.name;
2643
- if (typeof name !== "string" || !CLI_COMMAND_NAME_PATTERN.test(name)) {
2644
- throw new Error(
2645
- `invalid cli command name ${JSON.stringify(name)} \u2014 use lowercase letters, digits, and "-"`
2646
- );
2647
- }
2648
- if (typeof registration.summary !== "string" || registration.summary.trim().length === 0) {
2649
- throw new Error(`cli command "${name}" must provide a summary`);
2650
- }
2651
- const commands = registration.commands ?? [];
2652
- if (!Array.isArray(commands)) {
2653
- throw new Error(`cli command "${name}" commands must be an array`);
2654
- }
2655
- const validatedCommands = commands.map((command, index) => {
2656
- if (typeof command?.name !== "string" || !CLI_COMMAND_NAME_PATTERN.test(command.name) || typeof command.summary !== "string" || typeof command.usage !== "string") {
2657
- throw new Error(
2658
- `cli command "${name}" commands[${index}] must be { name: [a-z0-9-]+, summary, usage }`
2659
- );
2660
- }
2661
- return {
2662
- name: command.name,
2663
- summary: command.summary,
2664
- usage: command.usage
2665
- };
2666
- });
2667
- if (typeof registration.run !== "function") {
2668
- throw new Error(
2669
- `cli command "${name}" must provide a run(argv, ctx) function`
2670
- );
2671
- }
2672
- cliRecord.registration = {
2673
- name,
2674
- summary: registration.summary,
2675
- commands: validatedCommands,
2676
- run: registration.run.bind(registration)
2677
- };
2678
- 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);
2679
3105
  if (warning) emitLog("warn", warning);
2680
3106
  }
2681
3107
  };
@@ -2761,93 +3187,13 @@ function createFakePluginHostInternal(options, sharedState) {
2761
3187
  },
2762
3188
  registerTool(tool) {
2763
3189
  assertLive();
2764
- const name = tool?.name;
2765
- if (typeof name !== "string" || !AGENT_TOOL_NAME_PATTERN.test(name)) {
2766
- throw new Error(
2767
- `invalid tool name ${JSON.stringify(name)} \u2014 use letters, digits, "-" and "_"`
2768
- );
2769
- }
2770
- if (RESERVED_AGENT_TOOL_NAMES.includes(name)) {
2771
- throw new Error(
2772
- `tool name "${name}" is a built-in bb tool \u2014 pick another name`
2773
- );
2774
- }
2775
- rejectStaleAgentToolFields(name, tool);
2776
- if (typeof tool.description !== "string" || tool.description.trim().length === 0) {
2777
- throw new Error(`tool "${name}" must provide a description`);
2778
- }
2779
- if (tool.instructions !== void 0 && typeof tool.instructions !== "string") {
2780
- throw new Error(`tool "${name}" instructions must be a string`);
2781
- }
2782
- if (typeof tool.instructions === "string" && tool.instructions.length > PLUGIN_AGENT_STATIC_INSTRUCTIONS_MAX_CHARS) {
2783
- throw new Error(
2784
- `tool "${name}" instructions exceed the ${PLUGIN_AGENT_STATIC_INSTRUCTIONS_MAX_CHARS}-character limit`
2785
- );
2786
- }
2787
- const presentation = parsePluginAgentToolPresentation(
2788
- name,
2789
- tool.presentation
2790
- );
2791
- if (presentation?.icon !== void 0) {
2792
- const problem = undeclaredIconProblem(
2793
- pluginId,
2794
- declaredIconNames,
2795
- presentation.icon.glyph
2796
- );
2797
- if (problem !== null) {
2798
- throw new Error(agentToolIconRefusalMessage(name, problem));
2799
- }
2800
- }
2801
- if (typeof tool.execute !== "function") {
2802
- throw new Error(
2803
- `tool "${name}" must provide an execute(params, ctx) function`
2804
- );
2805
- }
2806
- const parameters = tool.parameters;
2807
- let inputSchema;
2808
- let parse;
2809
- if (isZodSchemaLike(parameters)) {
2810
- try {
2811
- inputSchema = zodSchemaToJsonSchema(parameters);
2812
- } catch (error) {
2813
- throw new Error(
2814
- `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`
2815
- );
2816
- }
2817
- parse = (input) => {
2818
- const result = parameters.safeParse(input);
2819
- if (result.success) return { ok: true, value: result.data };
2820
- return { ok: false, error: summarizeParseIssues(result.error) };
2821
- };
2822
- } else if (typeof parameters === "object" && parameters !== null && !Array.isArray(parameters)) {
2823
- try {
2824
- inputSchema = JSON.parse(JSON.stringify(parameters));
2825
- } catch {
2826
- throw new Error(
2827
- `tool "${name}" parameters JSON schema is not JSON-serializable`
2828
- );
2829
- }
2830
- parse = (input) => ({ ok: true, value: input });
2831
- } else {
2832
- throw new Error(
2833
- `tool "${name}" parameters must be a zod schema or a JSON-schema object`
2834
- );
2835
- }
2836
- assertNoRecursiveJsonSchemaReferences(
2837
- inputSchema,
2838
- `tool "${name}" parameters`
2839
- );
2840
- const record = {
2841
- name,
2842
- description: tool.description,
2843
- presentation,
2844
- instructions: tool.instructions !== void 0 && tool.instructions.trim().length > 0 ? tool.instructions : null,
2845
- inputSchema,
2846
- parse,
2847
- execute: tool.execute.bind(tool)
2848
- };
2849
- if (agentTools.some((existing) => existing.name === name)) {
2850
- 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`);
2851
3197
  }
2852
3198
  agentTools.push(record);
2853
3199
  }
@@ -2857,35 +3203,9 @@ function createFakePluginHostInternal(options, sharedState) {
2857
3203
  requestInput,
2858
3204
  registerMentionProvider(provider) {
2859
3205
  assertLive();
2860
- const id = provider?.id;
2861
- if (typeof id !== "string" || !MENTION_PROVIDER_ID_PATTERN.test(id)) {
2862
- throw new Error(
2863
- `invalid mention provider id ${JSON.stringify(id)} \u2014 use letters, digits, "-" and "_"`
2864
- );
2865
- }
2866
- if (mentionProviders.some((record) => record.id === id)) {
2867
- throw new Error(`mention provider "${id}" is already registered`);
2868
- }
2869
- if (typeof provider.label !== "string" || provider.label.trim().length === 0) {
2870
- throw new Error(`mention provider "${id}" must provide a label`);
2871
- }
2872
- if (typeof provider.search !== "function") {
2873
- throw new Error(
2874
- `mention provider "${id}" must provide a search({ query, projectId, threadId }) function`
2875
- );
2876
- }
2877
- if (typeof provider.resolve !== "function") {
2878
- throw new Error(
2879
- `mention provider "${id}" must provide a resolve(itemId) function`
2880
- );
2881
- }
2882
- mentionProviders.push({
2883
- id,
2884
- label: provider.label.trim(),
2885
- triggers: normalizeMentionProviderTriggers(id, provider.triggers),
2886
- search: provider.search.bind(provider),
2887
- resolve: provider.resolve.bind(provider)
2888
- });
3206
+ mentionProviders.push(
3207
+ normalizeMentionProviderRegistration(provider, mentionProviders)
3208
+ );
2889
3209
  }
2890
3210
  };
2891
3211
  let requestedDrains = 0;
@@ -2920,6 +3240,8 @@ function createFakePluginHostInternal(options, sharedState) {
2920
3240
  overrides: options.sdk
2921
3241
  });
2922
3242
  const threadEventHandlers = {
3243
+ "experimental_thread.events": [],
3244
+ "experimental_terminal.input": [],
2923
3245
  "thread.created": [],
2924
3246
  "thread.active": [],
2925
3247
  "thread.idle": [],
@@ -2936,54 +3258,20 @@ function createFakePluginHostInternal(options, sharedState) {
2936
3258
  const hooks = {
2937
3259
  "message.dispatch": null
2938
3260
  };
3261
+ const environmentCompositions = /* @__PURE__ */ new Map();
2939
3262
  const environmentProviders = /* @__PURE__ */ new Map();
3263
+ const machineProviders = /* @__PURE__ */ new Map();
3264
+ const serverAccessProviders = /* @__PURE__ */ new Map();
2940
3265
  const disposeHooks = [];
2941
3266
  const serviceControllers = [];
2942
3267
  let nextInteractionId = 1;
2943
3268
  const pendingInteractions = /* @__PURE__ */ new Map();
2944
3269
  function requestInput(request, requestOptions) {
2945
3270
  assertLive();
2946
- if (!request || typeof request !== "object") {
2947
- throw new Error("ui.requestInput requires an options object");
2948
- }
2949
- if (typeof request.threadId !== "string" || request.threadId.length === 0) {
2950
- throw new Error("ui.requestInput threadId must be a non-empty string");
2951
- }
2952
- if (typeof request.rendererId !== "string" || !/^[a-zA-Z0-9_-]+$/.test(request.rendererId)) {
2953
- throw new Error(
2954
- "ui.requestInput rendererId must use letters, digits, '-' or '_'"
2955
- );
2956
- }
2957
- if (typeof request.title !== "string" || request.title.trim().length === 0 || request.title.trim().length > PLUGIN_INTERACTION_MAX_TITLE_LENGTH) {
2958
- throw new Error(
2959
- `ui.requestInput title must be 1-${PLUGIN_INTERACTION_MAX_TITLE_LENGTH} characters`
2960
- );
2961
- }
2962
- let payload;
2963
- try {
2964
- const json = JSON.stringify(request.payload);
2965
- if (json === void 0) throw new Error();
2966
- if (Buffer.byteLength(json, "utf8") > 64 * 1024) {
2967
- throw new Error("ui.requestInput payload exceeds 64 KiB");
2968
- }
2969
- payload = JSON.parse(json);
2970
- } catch (error) {
2971
- if (error instanceof Error && error.message.includes("64 KiB")) {
2972
- throw error;
2973
- }
2974
- throw new Error("ui.requestInput payload must be JSON-serializable");
2975
- }
2976
- const timeoutMs = request.timeoutMs ?? 10 * 60 * 1e3;
2977
- if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60 * 60 * 1e3) {
2978
- throw new Error(
2979
- "ui.requestInput timeoutMs must be between 1 and 3600000"
2980
- );
2981
- }
3271
+ const normalized = normalizeInteractionRequest(request);
2982
3272
  const normalizedRequest = {
2983
3273
  ...request,
2984
- title: request.title.trim(),
2985
- payload,
2986
- timeoutMs
3274
+ ...normalized
2987
3275
  };
2988
3276
  const id = `fake-interaction-${nextInteractionId++}`;
2989
3277
  return new Promise((resolve2) => {
@@ -3000,7 +3288,7 @@ function createFakePluginHostInternal(options, sharedState) {
3000
3288
  const timer = setTimeout(() => {
3001
3289
  pendingInteractions.delete(id);
3002
3290
  resolve2({ outcome: "cancelled", reason: "timeout" });
3003
- }, timeoutMs);
3291
+ }, normalized.timeoutMs);
3004
3292
  pendingInteractions.set(id, {
3005
3293
  request: normalizedRequest,
3006
3294
  resolve: resolve2,
@@ -3032,7 +3320,13 @@ function createFakePluginHostInternal(options, sharedState) {
3032
3320
  });
3033
3321
  }
3034
3322
  const validatedInput = normalizeRpcJsonResult(
3035
- await validateRpcValue(methodContract.input, input, "input")
3323
+ await validateRpcValue(
3324
+ methodContract.input,
3325
+ input,
3326
+ "input",
3327
+ throwRpcError
3328
+ ),
3329
+ throwRpcError
3036
3330
  );
3037
3331
  const call = {
3038
3332
  method: String(method),
@@ -3050,9 +3344,13 @@ function createFakePluginHostInternal(options, sharedState) {
3050
3344
  const validatedOutput = await validateRpcValue(
3051
3345
  methodContract.output,
3052
3346
  rawOutput,
3053
- "output"
3347
+ "output",
3348
+ throwRpcError
3349
+ );
3350
+ return normalizeRpcJsonResult(
3351
+ validatedOutput,
3352
+ throwRpcError
3054
3353
  );
3055
- return normalizeRpcJsonResult(validatedOutput);
3056
3354
  },
3057
3355
  experimental_onWorkerExit(handler) {
3058
3356
  assertLive();
@@ -3154,40 +3452,22 @@ function createFakePluginHostInternal(options, sharedState) {
3154
3452
  },
3155
3453
  experimental_contributeEnv(providerId, resolve2) {
3156
3454
  assertLive();
3157
- if (typeof providerId !== "string" || providerId.trim().length === 0) {
3158
- throw new Error(
3159
- "provider environment contribution requires a provider id"
3160
- );
3161
- }
3162
- if (providerEnvResolvers.has(providerId)) {
3163
- throw new Error(
3164
- `provider environment contribution for "${providerId}" is already registered`
3165
- );
3166
- }
3167
- if (typeof resolve2 !== "function") {
3168
- throw new Error(
3169
- "provider environment contribution requires a resolver function"
3170
- );
3171
- }
3455
+ validateProviderEnvContribution(
3456
+ "provider environment contribution",
3457
+ providerId,
3458
+ resolve2,
3459
+ providerEnvResolvers
3460
+ );
3172
3461
  providerEnvResolvers.set(providerId, resolve2);
3173
3462
  },
3174
3463
  experimental_contributeEnvHealth(providerId, resolve2) {
3175
3464
  assertLive();
3176
- if (typeof providerId !== "string" || providerId.trim().length === 0) {
3177
- throw new Error(
3178
- "provider environment health contribution requires a provider id"
3179
- );
3180
- }
3181
- if (providerEnvHealthResolvers.has(providerId)) {
3182
- throw new Error(
3183
- `provider environment health contribution for "${providerId}" is already registered`
3184
- );
3185
- }
3186
- if (typeof resolve2 !== "function") {
3187
- throw new Error(
3188
- "provider environment health contribution requires a resolver function"
3189
- );
3190
- }
3465
+ validateProviderEnvContribution(
3466
+ "provider environment health contribution",
3467
+ providerId,
3468
+ resolve2,
3469
+ providerEnvHealthResolvers
3470
+ );
3191
3471
  providerEnvHealthResolvers.set(providerId, resolve2);
3192
3472
  }
3193
3473
  };
@@ -3206,6 +3486,26 @@ function createFakePluginHostInternal(options, sharedState) {
3206
3486
  const experimental_environments = {
3207
3487
  register(declaration) {
3208
3488
  assertLive();
3489
+ if ("machineProviderId" in declaration) {
3490
+ const composition = environmentCompositionSchema.parse(declaration);
3491
+ const problem2 = composition.icon === null ? null : undeclaredIconProblem(
3492
+ pluginId,
3493
+ declaredIconNames,
3494
+ composition.icon
3495
+ );
3496
+ if (problem2 !== null)
3497
+ throw new Error(providerIconRefusalMessage(composition.id, problem2));
3498
+ if (environmentProviders.has(composition.id))
3499
+ throw new Error(
3500
+ "Environment ID is already registered as a concrete provider"
3501
+ );
3502
+ environmentCompositions.set(composition.id, composition);
3503
+ return;
3504
+ }
3505
+ if (environmentCompositions.has(declaration.id))
3506
+ throw new Error(
3507
+ "Environment ID is already registered as a composition"
3508
+ );
3209
3509
  const target = validatePluginEnvironmentProviderDeclaration(declaration);
3210
3510
  const problem = target.icon === null ? null : undeclaredIconProblem(pluginId, declaredIconNames, target.icon);
3211
3511
  if (problem !== null)
@@ -3217,6 +3517,29 @@ function createFakePluginHostInternal(options, sharedState) {
3217
3517
  requestedDrains += 1;
3218
3518
  }
3219
3519
  };
3520
+ const unavailableMachineBootstrap = () => {
3521
+ throw new Error(
3522
+ "Configure machineBootstrap in createFakePluginHost to exercise machine bootstrap"
3523
+ );
3524
+ };
3525
+ const experimental_machines = {
3526
+ async getResource(hostId) {
3527
+ assertLive();
3528
+ return options.machineResource ? options.machineResource(hostId) : null;
3529
+ },
3530
+ ...options.machineBootstrap ?? {
3531
+ bootstrap: unavailableMachineBootstrap
3532
+ },
3533
+ register(declaration) {
3534
+ assertLive();
3535
+ const target = validatePluginMachineProviderDeclaration(declaration);
3536
+ const problem = target.icon === null ? null : undeclaredIconProblem(pluginId, declaredIconNames, target.icon);
3537
+ if (problem !== null) {
3538
+ throw new Error(providerIconRefusalMessage(target.id, problem));
3539
+ }
3540
+ machineProviders.set(target.id, target);
3541
+ }
3542
+ };
3220
3543
  const bb = {
3221
3544
  pluginId,
3222
3545
  log,
@@ -3233,6 +3556,22 @@ function createFakePluginHostInternal(options, sharedState) {
3233
3556
  events,
3234
3557
  experimental_hooks,
3235
3558
  experimental_environments,
3559
+ experimental_machines,
3560
+ experimental_serverAccess: {
3561
+ register(declaration) {
3562
+ assertLive();
3563
+ validateServerAccessProviderDeclaration(declaration);
3564
+ if (serverAccessProviders.has(declaration.id))
3565
+ throw new Error(
3566
+ `Server access provider "${declaration.id}" is already registered`
3567
+ );
3568
+ serverAccessProviders.set(declaration.id, declaration);
3569
+ },
3570
+ recheck() {
3571
+ assertLive();
3572
+ requestedDrains += 1;
3573
+ }
3574
+ },
3236
3575
  status,
3237
3576
  server,
3238
3577
  hosts,
@@ -3320,6 +3659,8 @@ function createFakePluginHostInternal(options, sharedState) {
3320
3659
  },
3321
3660
  get threadEventHandlers() {
3322
3661
  return {
3662
+ "experimental_thread.events": threadEventHandlers["experimental_thread.events"].length,
3663
+ "experimental_terminal.input": threadEventHandlers["experimental_terminal.input"].length,
3323
3664
  "thread.created": threadEventHandlers["thread.created"].length,
3324
3665
  "thread.active": threadEventHandlers["thread.active"].length,
3325
3666
  "thread.idle": threadEventHandlers["thread.idle"].length,
@@ -3337,9 +3678,18 @@ function createFakePluginHostInternal(options, sharedState) {
3337
3678
  get hooks() {
3338
3679
  return { ...hooks };
3339
3680
  },
3681
+ get environmentCompositions() {
3682
+ return new Map(environmentCompositions);
3683
+ },
3340
3684
  get environmentProviders() {
3341
3685
  return new Map(environmentProviders);
3342
3686
  },
3687
+ get serverAccessProviders() {
3688
+ return new Map(serverAccessProviders);
3689
+ },
3690
+ get machineProviders() {
3691
+ return new Map(machineProviders);
3692
+ },
3343
3693
  mentionProviders,
3344
3694
  providerRegistrations,
3345
3695
  providerEnvResolvers,
@@ -3408,12 +3758,19 @@ function createFakePluginHostInternal(options, sharedState) {
3408
3758
  );
3409
3759
  for (const subscription of subscriptions) {
3410
3760
  const normalized = normalizeRpcJsonResult(
3411
- await validateRpcValue(subscription.payloadSchema, payload, "input")
3761
+ await validateRpcValue(
3762
+ subscription.payloadSchema,
3763
+ payload,
3764
+ "input",
3765
+ throwRpcError
3766
+ ),
3767
+ throwRpcError
3412
3768
  );
3413
3769
  const parsed = await validateRpcValue(
3414
3770
  subscription.payloadSchema,
3415
3771
  normalized,
3416
- "input"
3772
+ "input",
3773
+ throwRpcError
3417
3774
  );
3418
3775
  await subscription.handler({ hostId, payload: parsed });
3419
3776
  }
@@ -3447,7 +3804,8 @@ function createFakePluginHostInternal(options, sharedState) {
3447
3804
  const validatedInput = await validateRpcValue(
3448
3805
  record.inputSchema,
3449
3806
  parsedInput,
3450
- "input"
3807
+ "input",
3808
+ throwRpcError
3451
3809
  );
3452
3810
  let result;
3453
3811
  try {
@@ -3461,9 +3819,10 @@ function createFakePluginHostInternal(options, sharedState) {
3461
3819
  const validatedOutput = await validateRpcValue(
3462
3820
  record.outputSchema,
3463
3821
  result,
3464
- "output"
3822
+ "output",
3823
+ throwRpcError
3465
3824
  );
3466
- return normalizeRpcJsonResult(validatedOutput);
3825
+ return normalizeRpcJsonResult(validatedOutput, throwRpcError);
3467
3826
  },
3468
3827
  async runCli(argv, ctx = {}) {
3469
3828
  const registration = cliRecord.registration;
@@ -3714,12 +4073,15 @@ function createFakePluginHostInternal(options, sharedState) {
3714
4073
  instructions: null
3715
4074
  };
3716
4075
  }
4076
+ const pluginMetadata = deepFreezePluginMetadata(
4077
+ validatePluginMetadata(context.pluginMetadata ?? {})
4078
+ );
3717
4079
  try {
3718
- const normalized = normalizeAgentConfiguration({
4080
+ const normalized = normalizePluginAgentConfiguration({
3719
4081
  knownSkillIds: new Set(agentSkillIds),
3720
4082
  knownToolIds: new Set(agentTools.map((tool) => tool.name)),
3721
4083
  pluginId,
3722
- value: agentConfigurationProvider(context)
4084
+ value: agentConfigurationProvider({ ...context, pluginMetadata })
3723
4085
  });
3724
4086
  const selectedTools = new Set(normalized.toolIds);
3725
4087
  return {
@@ -3765,8 +4127,16 @@ function makeHostResponse(overrides = {}) {
3765
4127
  return {
3766
4128
  id: "host-1",
3767
4129
  name: "Test host",
3768
- status: "connected",
3769
4130
  type: "persistent",
4131
+ status: "connected",
4132
+ machineProviderId: null,
4133
+ lifecycle: {
4134
+ phase: "active",
4135
+ suspendedAt: null,
4136
+ message: null,
4137
+ pendingLog: "",
4138
+ teardown: null
4139
+ },
3770
4140
  maxPermissionMode: "full",
3771
4141
  lastSeenAt: null,
3772
4142
  lastRejectedProtocolVersion: null,
@@ -3806,6 +4176,7 @@ function makeThreadResponse(overrides = {}) {
3806
4176
  }
3807
4177
  function makePluginAgentConfigurationContext(overrides = {}) {
3808
4178
  const context = {
4179
+ pluginMetadata: {},
3809
4180
  thread: {
3810
4181
  id: "thread-test",
3811
4182
  title: null,
@@ -3834,6 +4205,7 @@ function makePluginAgentConfigurationContext(overrides = {}) {
3834
4205
  origin: { kind: null, pluginId: null }
3835
4206
  };
3836
4207
  return {
4208
+ pluginMetadata: overrides.pluginMetadata ?? context.pluginMetadata,
3837
4209
  thread: { ...context.thread, ...overrides.thread },
3838
4210
  project: { ...context.project, ...overrides.project },
3839
4211
  environment: { ...context.environment, ...overrides.environment },
@@ -3907,17 +4279,7 @@ function makeMessageDispatchHookContext(overrides = {}) {
3907
4279
  createdAt: 0,
3908
4280
  updatedAt: 0
3909
4281
  };
3910
- const hostDefaults = {
3911
- id: "host-1",
3912
- name: "Test host",
3913
- status: "connected",
3914
- type: "persistent",
3915
- maxPermissionMode: "full",
3916
- lastSeenAt: null,
3917
- lastRejectedProtocolVersion: null,
3918
- createdAt: 0,
3919
- updatedAt: 0
3920
- };
4282
+ const hostDefaults = makeHostResponse();
3921
4283
  const project = { ...context.project, ...overrides.project };
3922
4284
  const host = overrides.host === void 0 ? context.host : overrides.host === null ? null : { ...hostDefaults, ...overrides.host };
3923
4285
  const environment = overrides.environment === void 0 ? context.environment : overrides.environment === null ? null : {