@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.
- package/bundled-types/bb-plugin-sdk-app.d.ts +9 -1
- package/bundled-types/bb-plugin-sdk-internal-host-policy.d.ts +229 -43
- package/bundled-types/bb-plugin-sdk-provider-bridge-testing.d.ts +52 -0
- package/bundled-types/bb-plugin-sdk-provider-bridge.d.ts +131 -2
- package/bundled-types/bb-plugin-sdk-testing.d.ts +17 -8
- package/bundled-types/bb-plugin-sdk.d.ts +172 -5
- package/dist/host.js +1 -1
- package/dist/internal/host-policy.js +790 -114
- package/dist/internal/plugin-app-collector.js +71 -90
- package/dist/provider-bridge-acp.js +1967 -2047
- package/dist/provider-bridge-testing.js +1378 -1405
- package/dist/provider-bridge-worker-entry.mjs +0 -1
- package/dist/provider-bridge.js +1458 -1500
- package/dist/testing/app.js +71 -90
- package/dist/testing/index.js +954 -791
- package/package.json +1 -2
package/dist/testing/index.js
CHANGED
|
@@ -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-
|
|
277
|
-
var
|
|
278
|
-
|
|
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
|
|
1841
|
+
function requireProviderIdAndDisplayName(noun, declaration) {
|
|
1780
1842
|
if (typeof declaration !== "object" || declaration === null) {
|
|
1781
|
-
throw new Error(
|
|
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
|
|
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
|
-
|
|
1854
|
+
`${noun} "${id}" needs a displayName of 1-${ENVIRONMENT_PROVIDER_DISPLAY_NAME_MAX_CHARS} characters`
|
|
1793
1855
|
);
|
|
1794
1856
|
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1857
|
+
return { id, displayName };
|
|
1858
|
+
}
|
|
1859
|
+
function assertProviderIconShape(noun, id, icon) {
|
|
1860
|
+
if (isPluginOwnedIconPath(icon)) {
|
|
1798
1861
|
validateProviderRelativePath(icon, `"${id}" icon`);
|
|
1799
|
-
} else if (
|
|
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
|
-
|
|
1864
|
+
`${noun} "${id}" icon must be a glyph, declared icon, or plugin-relative path`
|
|
1814
1865
|
);
|
|
1815
1866
|
}
|
|
1816
|
-
|
|
1867
|
+
}
|
|
1868
|
+
function assertOptionalFunction(noun, id, value, phrase) {
|
|
1869
|
+
if (value !== void 0 && typeof value !== "function") {
|
|
1817
1870
|
throw new Error(
|
|
1818
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
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
|
-
|
|
1995
|
-
|
|
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
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
const
|
|
2034
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
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
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
`
|
|
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
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
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
|
-
|
|
2094
|
-
|
|
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
|
-
|
|
2097
|
-
|
|
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
|
-
|
|
2174
|
+
pending.push([
|
|
2175
|
+
name,
|
|
2176
|
+
{
|
|
2177
|
+
inputSchema: methodContract.input,
|
|
2178
|
+
outputSchema: methodContract.output,
|
|
2179
|
+
handler
|
|
2180
|
+
}
|
|
2181
|
+
]);
|
|
2102
2182
|
}
|
|
2103
|
-
return
|
|
2104
|
-
}
|
|
2105
|
-
function isNeedsConfigurationError(error) {
|
|
2106
|
-
return error instanceof Error && error.name === "NeedsConfigurationError";
|
|
2183
|
+
return pending;
|
|
2107
2184
|
}
|
|
2108
|
-
function
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
if (
|
|
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(
|
|
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(
|
|
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
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
});
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
}
|
|
2138
|
-
|
|
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
|
-
|
|
2145
|
-
|
|
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
|
-
|
|
2229
|
+
parseCron(String(cron));
|
|
2148
2230
|
} catch (error) {
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
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 (
|
|
2157
|
-
|
|
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
|
|
2238
|
+
return { name, cron: String(cron), fn };
|
|
2164
2239
|
}
|
|
2165
|
-
function
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
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
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
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
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
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
|
|
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
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
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
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
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
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
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
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
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
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
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
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
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
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
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
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
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
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
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
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
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(
|
|
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 =
|
|
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 : {
|