@hachej/boring-workspace 0.1.32 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1019,7 +1019,7 @@ function buildBoringSystemPrompt(opts) {
1019
1019
  "- Imperative method names: `registerComponent`, `addPanel`, `registerCommand` (no `Panel`), `registerTab` \u2014 the actual names are `registerPanel`, `registerPanelCommand`, `registerLeftTab`, `registerSurfaceResolver` (and you usually express these declaratively, not as method calls).",
1020
1020
  "- Import paths: `@hachej/boring-pi` (it's a skills package, not for code), `@boring-ui/*`, `@hachej/pi-sdk` \u2014 use `@hachej/boring-workspace/plugin` for front and `@hachej/boring-workspace/server` for server.",
1021
1021
  '- File visualizers: import `WORKSPACE_OPEN_PATH_SURFACE_KIND`/`PaneProps` from `@hachej/boring-workspace/plugin`; import `useApiBaseUrl`/`useWorkspaceRequestId` from `@hachej/boring-workspace`; read `request.target`; fetch `${apiBaseUrl}/api/v1/files/raw?...` with `credentials: "include"` and `x-boring-workspace-id` when present. Never use `/workspace/read` or string kind `"WORKSPACE_OPEN_PATH_SURFACE_KIND"`.',
1022
- "- Pi extension tools: `defineTool` and `export const tools` do NOT exist. Export `default function (pi) { pi.registerTool({ name, description, execute }) }`.",
1022
+ '- Pi extension tools: `defineTool` and `export const tools` do NOT exist. Export `default function (pi) { pi.registerTool({ name, description, parameters: { type: "object", properties: {} }, execute }) }`. `parameters` is mandatory even for no-arg tools; omitting it breaks tool execution.',
1023
1023
  '- Server/Pi tool method: `handler` \u2014 use `execute`. Return shape: `{ content: [{ type: "text", text }] }` (NEVER a bare string).',
1024
1024
  "- Manifest values: `boring.server: true` \u2014 use `false`/omit for hot-reload user plugins, or a relative path string only for advanced boot-time/static server integration.",
1025
1025
  "- File layout: files at the package root, or `src/` / `dist/` / `lib/` subdirectories \u2014 the scaffold's hot-reload layout (`front/index.tsx`, optional `agent/index.ts` declared in `pi.extensions`) is the one the workspace refreshes on `/reload`.",
@@ -1227,6 +1227,14 @@ function resolveContainedPluginPath(rootDir, value, options = {}) {
1227
1227
  }
1228
1228
 
1229
1229
  // src/server/agentPlugins/scan.ts
1230
+ function normalizeBoringPluginSource(input) {
1231
+ if (typeof input === "string") return { rootDir: resolve3(input), kind: "internal" };
1232
+ return {
1233
+ rootDir: resolve3(input.rootDir),
1234
+ kind: input.kind,
1235
+ ...input.workspaceId ? { workspaceId: input.workspaceId } : {}
1236
+ };
1237
+ }
1230
1238
  function pluginIdFromPackageJson(pkg, rootDir) {
1231
1239
  const explicitId = typeof pkg.boring?.id === "string" && pkg.boring.id.trim() ? pkg.boring.id.trim() : void 0;
1232
1240
  if (explicitId) return explicitId;
@@ -1285,10 +1293,11 @@ function packagePathContainmentIssues(rootDir, pkg) {
1285
1293
  return issues;
1286
1294
  }
1287
1295
  function discoverBoringPluginDirs(pluginDirs) {
1288
- const out = /* @__PURE__ */ new Set();
1296
+ const out = /* @__PURE__ */ new Map();
1289
1297
  const missingPackageJson = [];
1290
1298
  for (const raw of pluginDirs) {
1291
- const dir = resolve3(raw);
1299
+ const source = normalizeBoringPluginSource(raw);
1300
+ const dir = source.rootDir;
1292
1301
  if (!existsSync2(dir)) continue;
1293
1302
  const info = statSync(dir);
1294
1303
  if (!info.isDirectory()) continue;
@@ -1299,13 +1308,19 @@ function discoverBoringPluginDirs(pluginDirs) {
1299
1308
  const child = join3(dir, entry.name);
1300
1309
  if (existsSync2(join3(child, "package.json"))) childPackageDirs.push(child);
1301
1310
  }
1302
- if (hasPackageJson) out.add(dir);
1303
- for (const child of childPackageDirs) out.add(child);
1304
- if (!hasPackageJson && childPackageDirs.length === 0 && basename(dir) !== "extensions") {
1311
+ if (hasPackageJson && !out.has(dir)) out.set(dir, source);
1312
+ for (const child of childPackageDirs) {
1313
+ if (!out.has(child)) out.set(child, { ...source, rootDir: child });
1314
+ }
1315
+ const collectionDirNames = /* @__PURE__ */ new Set(["extensions", "npm", "git"]);
1316
+ if (!hasPackageJson && childPackageDirs.length === 0 && !collectionDirNames.has(basename(dir))) {
1305
1317
  missingPackageJson.push(dir);
1306
1318
  }
1307
1319
  }
1308
- return { dirs: [...out].sort(), missingPackageJson: [...new Set(missingPackageJson)].sort() };
1320
+ return {
1321
+ sources: [...out.values()].sort((a, b) => a.rootDir.localeCompare(b.rootDir)),
1322
+ missingPackageJson: [...new Set(missingPackageJson)].sort()
1323
+ };
1309
1324
  }
1310
1325
  function scanBoringPlugins(pluginDirs) {
1311
1326
  const errors = [];
@@ -1315,7 +1330,8 @@ function scanBoringPlugins(pluginDirs) {
1315
1330
  for (const pluginDir of discovered.missingPackageJson) {
1316
1331
  errors.push({ pluginDir, code: "MISSING_PACKAGE_JSON", message: "package.json is missing" });
1317
1332
  }
1318
- for (const rootDir of discovered.dirs) {
1333
+ for (const source of discovered.sources) {
1334
+ const rootDir = source.rootDir;
1319
1335
  let raw;
1320
1336
  try {
1321
1337
  raw = parsePackageJson(rootDir);
@@ -1353,15 +1369,26 @@ function scanBoringPlugins(pluginDirs) {
1353
1369
  } else {
1354
1370
  const previous = seenIds.get(id);
1355
1371
  if (previous) {
1356
- errors.push({
1357
- pluginDir: rootDir,
1358
- pluginId: id,
1359
- code: "INVALID_PLUGIN_METADATA",
1360
- message: `duplicate plugin id "${id}" also declared by ${previous}`
1361
- });
1362
1372
  const previousPluginIndex = plugins.findIndex((plugin) => plugin.id === id);
1363
- if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
1364
- canAddPlugin = false;
1373
+ const previousPlugin = previousPluginIndex >= 0 ? plugins[previousPluginIndex] : void 0;
1374
+ const currentIsWorkspaceLocal = Boolean(source.workspaceId);
1375
+ const previousIsWorkspaceLocal = Boolean(previousPlugin?.source.workspaceId);
1376
+ const currentMayShadowPrevious = source.kind === "external" && currentIsWorkspaceLocal && previousPlugin?.source.kind === "external" && !previousIsWorkspaceLocal;
1377
+ if (currentMayShadowPrevious) {
1378
+ if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
1379
+ seenIds.set(id, rootDir);
1380
+ } else if (!currentIsWorkspaceLocal && previousIsWorkspaceLocal) {
1381
+ canAddPlugin = false;
1382
+ } else {
1383
+ errors.push({
1384
+ pluginDir: rootDir,
1385
+ pluginId: id,
1386
+ code: "INVALID_PLUGIN_METADATA",
1387
+ message: `duplicate plugin id "${id}" also declared by ${previous}`
1388
+ });
1389
+ if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
1390
+ canAddPlugin = false;
1391
+ }
1365
1392
  } else {
1366
1393
  seenIds.set(id, rootDir);
1367
1394
  }
@@ -1373,6 +1400,7 @@ function scanBoringPlugins(pluginDirs) {
1373
1400
  }
1374
1401
  if (!canAddPlugin) continue;
1375
1402
  const pkg = result.packageJson;
1403
+ const hasBoring = pkg.boring !== void 0;
1376
1404
  const boring = pkg.boring ?? {};
1377
1405
  const pi = pkg.pi;
1378
1406
  const frontPath = resolvePluginPath(rootDir, boring.front, { mustExist: true });
@@ -1385,11 +1413,13 @@ function scanBoringPlugins(pluginDirs) {
1385
1413
  rootDir,
1386
1414
  version,
1387
1415
  boring,
1416
+ hasBoring,
1388
1417
  ...pi ? { pi } : {},
1389
1418
  ...frontPath ? { frontPath, frontUrl: `/@fs/${frontPath}` } : {},
1390
1419
  ...serverPath ? { serverPath } : {},
1391
1420
  ...extensionPaths.length > 0 ? { extensionPaths } : {},
1392
- ...skillPaths.length > 0 ? { skillPaths } : {}
1421
+ ...skillPaths.length > 0 ? { skillPaths } : {},
1422
+ source
1393
1423
  });
1394
1424
  }
1395
1425
  const preflight = { ok: errors.length === 0, errors };
@@ -1400,7 +1430,7 @@ function preflightBoringPlugins(pluginDirs) {
1400
1430
  }
1401
1431
  function readBoringPlugins(pluginDirs) {
1402
1432
  const scan = scanBoringPlugins(pluginDirs);
1403
- return scan.preflight.ok ? scan.plugins : [];
1433
+ return scan.preflight.ok ? scan.plugins.filter((plugin) => plugin.hasBoring) : [];
1404
1434
  }
1405
1435
 
1406
1436
  // src/server/agentPlugins/signatureCache.ts
@@ -1564,10 +1594,11 @@ function frontSignatureRoot(plugin) {
1564
1594
  return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel) ? frontRoot : dirname6(plugin.frontPath);
1565
1595
  }
1566
1596
  function pluginSignature(plugin) {
1567
- return createHash("sha256").update(JSON.stringify(plugin.boring)).update(JSON.stringify(plugin.pi ?? {})).update(plugin.version).update(plugin.frontPath ?? "").update(pluginFileSignature(plugin.frontPath)).update(directorySignature(frontSignatureRoot(plugin))).update(directorySignature(join5(plugin.rootDir, "shared"))).update(plugin.serverPath ?? "").update(pluginFileSignature(plugin.serverPath)).update(directorySignature(plugin.serverPath ? dirname6(plugin.serverPath) : void 0)).update((plugin.extensionPaths ?? []).join("\0")).update((plugin.skillPaths ?? []).join("\0")).digest("hex");
1597
+ return createHash("sha256").update(JSON.stringify(plugin.boring)).update(JSON.stringify(plugin.pi ?? {})).update(plugin.version).update(JSON.stringify(plugin.source)).update(plugin.frontPath ?? "").update(pluginFileSignature(plugin.frontPath)).update(directorySignature(frontSignatureRoot(plugin))).update(directorySignature(join5(plugin.rootDir, "shared"))).update(plugin.serverPath ?? "").update(pluginFileSignature(plugin.serverPath)).update(directorySignature(plugin.serverPath ? dirname6(plugin.serverPath) : void 0)).update((plugin.extensionPaths ?? []).join("\0")).update((plugin.skillPaths ?? []).join("\0")).digest("hex");
1568
1598
  }
1569
1599
  function computeRequiresRestart(previous, next) {
1570
1600
  if (!previous) return [];
1601
+ if (previous.source.kind === "external" && next.source.kind === "external") return [];
1571
1602
  const prevHasServer = !!previous.serverPath;
1572
1603
  const nextHasServer = !!next.serverPath;
1573
1604
  if (!prevHasServer && !nextHasServer) return [];
@@ -1613,8 +1644,10 @@ var BoringPluginAssetManager = class {
1613
1644
  version: plugin.version,
1614
1645
  revision: plugin.revision,
1615
1646
  rootDir: plugin.rootDir,
1647
+ source: plugin.source,
1616
1648
  ...plugin.frontPath ? { frontPath: plugin.frontPath } : {},
1617
- ...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {}
1649
+ ...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {},
1650
+ ...plugin.serverPath ? { serverPath: plugin.serverPath } : {}
1618
1651
  }));
1619
1652
  }
1620
1653
  inspectLoadedPiSnapshot() {
@@ -1654,7 +1687,7 @@ ${prompts.join("\n\n")}` } : {}
1654
1687
  async doLoadOnce() {
1655
1688
  this.lastErrors.clear();
1656
1689
  const scan = scanBoringPlugins(this.pluginDirs);
1657
- const nextPlugins = scan.plugins;
1690
+ const nextPlugins = scan.plugins.filter((plugin) => plugin.hasBoring);
1658
1691
  const nextIds = new Set(nextPlugins.map((plugin) => plugin.id));
1659
1692
  const invalidPluginDirs = new Set(scan.preflight.errors.map((error) => resolve5(error.pluginDir)));
1660
1693
  const events = [];
@@ -1818,31 +1851,7 @@ function collectRestartWarnings(events) {
1818
1851
  return warnings;
1819
1852
  }
1820
1853
  async function boringPluginRoutes(app, opts) {
1821
- const { manager, rebuildPlugins, enableReloadRoute = true } = opts;
1822
- if (enableReloadRoute) {
1823
- app.post("/api/boring.reload", async (_request, reply) => {
1824
- const scan = await manager.load();
1825
- const rebuild = rebuildPlugins ? await rebuildPlugins() : { ok: true, diagnostics: [] };
1826
- const restart_warnings = collectRestartWarnings(scan.events);
1827
- const hasFailures = scan.errors.length > 0 || rebuild.diagnostics.length > 0;
1828
- if (hasFailures) {
1829
- return reply.status(422).send({
1830
- ok: false,
1831
- errors: scan.errors,
1832
- diagnostics: rebuild.diagnostics,
1833
- plugins: scan.loaded,
1834
- // Even on failure, emit warnings for plugins that DID reload
1835
- // — partial-failure tolerance means some loaded successfully.
1836
- ...restart_warnings.length > 0 ? { restart_warnings } : {}
1837
- });
1838
- }
1839
- return reply.send({
1840
- ok: true,
1841
- plugins: scan.loaded,
1842
- ...restart_warnings.length > 0 ? { restart_warnings } : {}
1843
- });
1844
- });
1845
- }
1854
+ const { manager } = opts;
1846
1855
  const listPlugins = async () => manager.list();
1847
1856
  app.get("/api/v1/agent-plugins", listPlugins);
1848
1857
  const getPluginError = async (request, reply) => {
@@ -1919,8 +1928,499 @@ function aggregatePluginPrompts(manager) {
1919
1928
 
1920
1929
  ${prompts.join("\n\n")}`;
1921
1930
  }
1931
+
1932
+ // src/server/runtimeBackend/defineRuntimeServerPlugin.ts
1933
+ function defineRuntimeServerPlugin(plugin) {
1934
+ return plugin;
1935
+ }
1936
+ function isPlainObject(value) {
1937
+ if (typeof value !== "object" || value === null) return false;
1938
+ const proto = Object.getPrototypeOf(value);
1939
+ return proto === Object.prototype || proto === null;
1940
+ }
1941
+ function validateRuntimeServerPlugin(value) {
1942
+ if (!isPlainObject(value)) {
1943
+ throw new Error("runtime server plugin default export must be a plain object");
1944
+ }
1945
+ if ("id" in value) {
1946
+ throw new Error("runtime server plugin must not declare id; the host supplies plugin id from package metadata");
1947
+ }
1948
+ if (typeof value.routes !== "function") {
1949
+ throw new Error("runtime server plugin default export must define routes(router)");
1950
+ }
1951
+ if (value.dispose !== void 0 && typeof value.dispose !== "function") {
1952
+ throw new Error("runtime server plugin dispose must be a function when provided");
1953
+ }
1954
+ return value;
1955
+ }
1956
+ function isRuntimePluginResponse(value) {
1957
+ return isPlainObject(value) && value.kind === "response";
1958
+ }
1959
+
1960
+ // src/server/runtimeBackend/runtimePathSegments.ts
1961
+ function findUnsafeRuntimePathSegment(path) {
1962
+ if (path.includes("\\")) return "\\";
1963
+ for (const segment of path.split("/")) {
1964
+ if (segment === "." || segment === "..") return segment;
1965
+ }
1966
+ return null;
1967
+ }
1968
+ function describeUnsafeRuntimePathSegment(segment) {
1969
+ if (segment === "\\") return "backslashes";
1970
+ return `${segment} segments`;
1971
+ }
1972
+
1973
+ // src/server/runtimeBackend/routerCapture.ts
1974
+ var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "ALL"];
1975
+ function routeKey(method, path) {
1976
+ return `${method} ${path}`;
1977
+ }
1978
+ function runtimeRouteKey(method, path) {
1979
+ return routeKey(method.toUpperCase(), path);
1980
+ }
1981
+ function validateRuntimeRoutePath(path) {
1982
+ if (typeof path !== "string" || path.length === 0) {
1983
+ throw new Error("runtime route path must be a non-empty string");
1984
+ }
1985
+ if (!path.startsWith("/")) {
1986
+ throw new Error(`runtime route path must start with /: ${path}`);
1987
+ }
1988
+ if (path.includes("?") || path.includes("#")) {
1989
+ throw new Error(`runtime route path must not include query strings or fragments: ${path}`);
1990
+ }
1991
+ let decodedPath;
1992
+ try {
1993
+ decodedPath = decodeURIComponent(path);
1994
+ } catch {
1995
+ throw new Error(`runtime route path must be valid percent-encoding: ${path}`);
1996
+ }
1997
+ const unsafeSegment = findUnsafeRuntimePathSegment(decodedPath);
1998
+ if (unsafeSegment) {
1999
+ throw new Error(`runtime route path must not contain ${describeUnsafeRuntimePathSegment(unsafeSegment)}: ${path}`);
2000
+ }
2001
+ if (path.includes(":")) {
2002
+ throw new Error(`runtime route path must be exact and must not contain params: ${path}`);
2003
+ }
2004
+ if (path.includes("*")) {
2005
+ throw new Error(`runtime route path must be exact and must not contain wildcards: ${path}`);
2006
+ }
2007
+ return path;
2008
+ }
2009
+ async function captureRuntimeRoutes(register) {
2010
+ const routes = [];
2011
+ const seen = /* @__PURE__ */ new Set();
2012
+ const add = (method, path, handler) => {
2013
+ if (typeof handler !== "function") {
2014
+ throw new Error(`runtime route ${method} ${path} handler must be a function`);
2015
+ }
2016
+ const normalizedPath = validateRuntimeRoutePath(path);
2017
+ const key = routeKey(method, normalizedPath);
2018
+ if (seen.has(key)) throw new Error(`duplicate runtime route: ${key}`);
2019
+ seen.add(key);
2020
+ routes.push({ method, path: normalizedPath, handler });
2021
+ };
2022
+ const router = Object.fromEntries(
2023
+ METHODS.map((method) => [method.toLowerCase(), (path, handler) => add(method, path, handler)])
2024
+ );
2025
+ await register(router);
2026
+ return routes;
2027
+ }
2028
+
2029
+ // src/server/runtimeBackend/runtimeBackendRegistry.ts
2030
+ import { ErrorCode } from "@hachej/boring-agent/shared";
2031
+
2032
+ // src/server/pluginImports/importServerModule.ts
2033
+ import { createRequire as createRequire2 } from "module";
2034
+ import { pathToFileURL } from "url";
2035
+ var require3 = createRequire2(import.meta.url);
2036
+ var warnedJitiMissing = false;
2037
+ function warnJitiUnavailable(serverPath, reason) {
2038
+ if (warnedJitiMissing) return;
2039
+ warnedJitiMissing = true;
2040
+ console.warn(
2041
+ `[boring-workspace] hotReload requested but jiti is unavailable (${reason}). Falling back to native import() for ${serverPath}; subsequent reloads will NOT pick up source changes because Node's module cache will return the same module. Install jiti or set hotReload: false.`
2042
+ );
2043
+ }
2044
+ function jitiImport(serverPath) {
2045
+ try {
2046
+ const jitiModule = require3("jiti");
2047
+ const create = jitiModule.createJiti;
2048
+ if (!create) {
2049
+ warnJitiUnavailable(serverPath, "createJiti not exported");
2050
+ return null;
2051
+ }
2052
+ return create(import.meta.url, { moduleCache: false }).import(serverPath);
2053
+ } catch (err) {
2054
+ warnJitiUnavailable(serverPath, err instanceof Error ? err.message : String(err));
2055
+ return null;
2056
+ }
2057
+ }
2058
+ async function importServerModule(serverPath, hotReload) {
2059
+ if (hotReload) {
2060
+ const jiti = jitiImport(serverPath);
2061
+ if (jiti) return await jiti;
2062
+ }
2063
+ const href = pathToFileURL(serverPath).href;
2064
+ return await import(
2065
+ /* @vite-ignore */
2066
+ href
2067
+ );
2068
+ }
2069
+
2070
+ // src/server/runtimeBackend/runtimeBackendRegistry.ts
2071
+ var RuntimeBackendError = class extends Error {
2072
+ constructor(code, statusCode, message, details) {
2073
+ super(message);
2074
+ this.code = code;
2075
+ this.statusCode = statusCode;
2076
+ this.details = details;
2077
+ this.name = "RuntimeBackendError";
2078
+ }
2079
+ code;
2080
+ statusCode;
2081
+ details;
2082
+ };
2083
+ function errorMessage(error) {
2084
+ return error instanceof Error ? error.stack ?? error.message : String(error);
2085
+ }
2086
+ function moduleValue(mod) {
2087
+ return typeof mod === "object" && mod !== null && "default" in mod ? mod.default : mod;
2088
+ }
2089
+ function toRouteMap(routes) {
2090
+ const map = /* @__PURE__ */ new Map();
2091
+ for (const route of routes) map.set(runtimeRouteKey(route.method, route.path), route);
2092
+ return map;
2093
+ }
2094
+ function assertJsonSerializable(value) {
2095
+ if (value === void 0 || value === null) return;
2096
+ if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
2097
+ throw new RuntimeBackendError(
2098
+ ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
2099
+ 500,
2100
+ "runtime plugin response is not JSON-serializable"
2101
+ );
2102
+ }
2103
+ try {
2104
+ JSON.stringify(value);
2105
+ } catch (error) {
2106
+ throw new RuntimeBackendError(
2107
+ ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
2108
+ 500,
2109
+ `runtime plugin response is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`
2110
+ );
2111
+ }
2112
+ }
2113
+ function normalizeResponse(value) {
2114
+ if (value === void 0 || value === null) return { status: 204, headers: {} };
2115
+ if (isRuntimePluginResponse(value)) return normalizeExplicitResponse(value);
2116
+ assertJsonSerializable(value);
2117
+ return { status: 200, headers: { "content-type": "application/json; charset=utf-8" }, body: value };
2118
+ }
2119
+ function normalizeExplicitResponse(value) {
2120
+ const status = value.status ?? (value.body === void 0 || value.body === null ? 204 : 200);
2121
+ if (!Number.isInteger(status) || status < 100 || status > 599) {
2122
+ throw new RuntimeBackendError(
2123
+ ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
2124
+ 500,
2125
+ "runtime plugin response status must be an integer HTTP status code"
2126
+ );
2127
+ }
2128
+ const headers = {};
2129
+ if (value.headers !== void 0) {
2130
+ for (const [name, headerValue] of Object.entries(value.headers)) {
2131
+ if (typeof headerValue !== "string") {
2132
+ throw new RuntimeBackendError(
2133
+ ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
2134
+ 500,
2135
+ "runtime plugin response headers must be strings"
2136
+ );
2137
+ }
2138
+ headers[name] = headerValue;
2139
+ }
2140
+ }
2141
+ if (value.body === void 0 || value.body === null) return { status, headers };
2142
+ assertJsonSerializable(value.body);
2143
+ if (!Object.keys(headers).some((name) => name.toLowerCase() === "content-type")) {
2144
+ headers["content-type"] = "application/json; charset=utf-8";
2145
+ }
2146
+ return { status, headers, body: value.body };
2147
+ }
2148
+ async function disposeSnapshot(snapshot) {
2149
+ if (!snapshot.module.dispose) return [];
2150
+ try {
2151
+ await snapshot.module.dispose();
2152
+ return [];
2153
+ } catch (error) {
2154
+ return [{
2155
+ pluginId: snapshot.pluginId,
2156
+ source: `runtime backend dispose (${snapshot.pluginId})`,
2157
+ code: ErrorCode.enum.RUNTIME_PLUGIN_LOAD_FAILED,
2158
+ message: errorMessage(error)
2159
+ }];
2160
+ }
2161
+ }
2162
+ var RuntimeBackendRegistry = class {
2163
+ snapshots = /* @__PURE__ */ new Map();
2164
+ lastDiagnostics = [];
2165
+ reloadQueue = Promise.resolve({ ok: true, diagnostics: [] });
2166
+ getDiagnostics() {
2167
+ return [...this.lastDiagnostics];
2168
+ }
2169
+ listPluginIds() {
2170
+ return [...this.snapshots.keys()].sort();
2171
+ }
2172
+ async reloadFromLoadedPlugins(plugins) {
2173
+ const run = this.reloadQueue.then(() => this.reloadOnce(plugins), () => this.reloadOnce(plugins));
2174
+ this.reloadQueue = run.then(() => ({ ok: true, diagnostics: [] }), () => ({ ok: false, diagnostics: [] }));
2175
+ return run;
2176
+ }
2177
+ async close() {
2178
+ const run = this.reloadQueue.then(() => this.closeOnce(), () => this.closeOnce());
2179
+ this.reloadQueue = run.then(() => ({ ok: true, diagnostics: [] }), () => ({ ok: false, diagnostics: [] }));
2180
+ return run;
2181
+ }
2182
+ async dispatch(request) {
2183
+ const snapshot = this.snapshots.get(request.pluginId);
2184
+ if (!snapshot) {
2185
+ throw new RuntimeBackendError(
2186
+ ErrorCode.enum.RUNTIME_PLUGIN_NOT_FOUND,
2187
+ 404,
2188
+ `runtime backend plugin not found: ${request.pluginId}`
2189
+ );
2190
+ }
2191
+ if (snapshot.source.workspaceId && snapshot.source.workspaceId !== request.workspaceId) {
2192
+ throw new RuntimeBackendError(
2193
+ ErrorCode.enum.RUNTIME_PLUGIN_NOT_FOUND,
2194
+ 404,
2195
+ `runtime backend plugin not found in workspace: ${request.pluginId}`
2196
+ );
2197
+ }
2198
+ const route = snapshot.routes.get(runtimeRouteKey(request.method, request.path)) ?? snapshot.routes.get(runtimeRouteKey("ALL", request.path));
2199
+ if (!route) {
2200
+ throw new RuntimeBackendError(
2201
+ ErrorCode.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
2202
+ 404,
2203
+ `runtime backend route not found: ${request.method.toUpperCase()} ${request.path}`
2204
+ );
2205
+ }
2206
+ const ctx = {
2207
+ pluginId: request.pluginId,
2208
+ method: request.method.toUpperCase(),
2209
+ path: request.path,
2210
+ query: request.query,
2211
+ headers: request.headers,
2212
+ signal: request.signal,
2213
+ body: request.body,
2214
+ logger: request.logger
2215
+ };
2216
+ try {
2217
+ return normalizeResponse(await route.handler(ctx));
2218
+ } catch (error) {
2219
+ if (error instanceof RuntimeBackendError) throw error;
2220
+ throw new RuntimeBackendError(
2221
+ ErrorCode.enum.RUNTIME_PLUGIN_HANDLER_FAILED,
2222
+ 500,
2223
+ error instanceof Error ? error.message : String(error)
2224
+ );
2225
+ }
2226
+ }
2227
+ async reloadOnce(plugins) {
2228
+ const diagnostics = [];
2229
+ const externalRuntimePlugins = plugins.filter((plugin) => plugin.source.kind === "external" && plugin.serverPath);
2230
+ const nextIds = new Set(externalRuntimePlugins.map((plugin) => plugin.id));
2231
+ for (const id of [...this.snapshots.keys()]) {
2232
+ if (nextIds.has(id)) continue;
2233
+ const previous = this.snapshots.get(id);
2234
+ if (!previous) continue;
2235
+ this.snapshots.delete(id);
2236
+ diagnostics.push(...await disposeSnapshot(previous));
2237
+ }
2238
+ for (const plugin of externalRuntimePlugins) {
2239
+ const serverPath = plugin.serverPath;
2240
+ if (!serverPath) continue;
2241
+ try {
2242
+ const mod = await importServerModule(serverPath, true);
2243
+ const runtimePlugin = validateRuntimeServerPlugin(moduleValue(mod));
2244
+ const routes = await captureRuntimeRoutes((router) => runtimePlugin.routes(router));
2245
+ const nextSnapshot = {
2246
+ pluginId: plugin.id,
2247
+ source: plugin.source,
2248
+ module: runtimePlugin,
2249
+ routes: toRouteMap(routes)
2250
+ };
2251
+ const previous = this.snapshots.get(plugin.id);
2252
+ this.snapshots.set(plugin.id, nextSnapshot);
2253
+ if (previous) diagnostics.push(...await disposeSnapshot(previous));
2254
+ } catch (error) {
2255
+ diagnostics.push({
2256
+ pluginId: plugin.id,
2257
+ source: `runtime backend (${plugin.id})`,
2258
+ code: ErrorCode.enum.RUNTIME_PLUGIN_LOAD_FAILED,
2259
+ message: errorMessage(error)
2260
+ });
2261
+ }
2262
+ }
2263
+ this.lastDiagnostics = diagnostics;
2264
+ return { ok: diagnostics.length === 0, diagnostics };
2265
+ }
2266
+ async closeOnce() {
2267
+ const diagnostics = [];
2268
+ for (const snapshot of this.snapshots.values()) {
2269
+ diagnostics.push(...await disposeSnapshot(snapshot));
2270
+ }
2271
+ this.snapshots.clear();
2272
+ this.lastDiagnostics = diagnostics;
2273
+ return { ok: diagnostics.length === 0, diagnostics };
2274
+ }
2275
+ };
2276
+
2277
+ // src/server/runtimeBackend/runtimeBackendGateway.ts
2278
+ import { ErrorCode as ErrorCode2 } from "@hachej/boring-agent/shared";
2279
+ var GATEWAY_PREFIX = "/api/v1/plugins/";
2280
+ function rawPathFromRequest(request) {
2281
+ const url = request.raw.url ?? request.url;
2282
+ const queryIndex = url.indexOf("?");
2283
+ return queryIndex === -1 ? url : url.slice(0, queryIndex);
2284
+ }
2285
+ function rawGatewayTail(request, pluginId) {
2286
+ const rawPath = rawPathFromRequest(request);
2287
+ const prefix = `${GATEWAY_PREFIX}${pluginId}`;
2288
+ if (rawPath === prefix || rawPath === `${prefix}/`) return "/";
2289
+ if (!rawPath.startsWith(`${prefix}/`)) {
2290
+ throw new RuntimeBackendError(
2291
+ ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
2292
+ 404,
2293
+ "runtime backend route not found"
2294
+ );
2295
+ }
2296
+ return rawPath.slice(prefix.length);
2297
+ }
2298
+ function normalizeGatewayPath(rawTail) {
2299
+ let path;
2300
+ try {
2301
+ path = decodeURIComponent(rawTail);
2302
+ } catch {
2303
+ throw new RuntimeBackendError(
2304
+ ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
2305
+ 404,
2306
+ "runtime backend route path is not valid percent-encoding"
2307
+ );
2308
+ }
2309
+ if (path.length === 0) path = "/";
2310
+ const unsafeSegment = findUnsafeRuntimePathSegment(path);
2311
+ if (unsafeSegment) {
2312
+ throw new RuntimeBackendError(
2313
+ ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
2314
+ 404,
2315
+ `runtime backend route path must not contain ${describeUnsafeRuntimePathSegment(unsafeSegment)}`
2316
+ );
2317
+ }
2318
+ return path;
2319
+ }
2320
+ function firstString(value) {
2321
+ if (Array.isArray(value)) return value[0];
2322
+ return value;
2323
+ }
2324
+ function headersFromRequest(request) {
2325
+ const headers = new Headers();
2326
+ for (const [name, value] of Object.entries(request.headers)) {
2327
+ if (value === void 0) continue;
2328
+ if (Array.isArray(value)) {
2329
+ for (const item of value) headers.append(name, item);
2330
+ } else {
2331
+ headers.set(name, String(value));
2332
+ }
2333
+ }
2334
+ return headers;
2335
+ }
2336
+ function loggerFromRequest(request) {
2337
+ return {
2338
+ debug: (arg, message) => {
2339
+ if (message === void 0) request.log.debug(arg);
2340
+ else request.log.debug(arg, message);
2341
+ },
2342
+ info: (arg, message) => {
2343
+ if (message === void 0) request.log.info(arg);
2344
+ else request.log.info(arg, message);
2345
+ },
2346
+ warn: (arg, message) => {
2347
+ if (message === void 0) request.log.warn(arg);
2348
+ else request.log.warn(arg, message);
2349
+ },
2350
+ error: (arg, message) => {
2351
+ if (message === void 0) request.log.error(arg);
2352
+ else request.log.error(arg, message);
2353
+ }
2354
+ };
2355
+ }
2356
+ function sendDispatchResponse(reply, response) {
2357
+ reply.status(response.status);
2358
+ for (const [name, value] of Object.entries(response.headers)) reply.header(name, value);
2359
+ if (response.body === void 0 || response.body === null) return reply.send();
2360
+ return reply.send(response.body);
2361
+ }
2362
+ function sendError(reply, error) {
2363
+ if (error instanceof RuntimeBackendError) {
2364
+ return reply.status(error.statusCode).send({
2365
+ error: {
2366
+ code: error.code,
2367
+ message: error.message,
2368
+ ...error.details ? { details: error.details } : {}
2369
+ }
2370
+ });
2371
+ }
2372
+ return reply.status(500).send({
2373
+ error: {
2374
+ code: ErrorCode2.enum.INTERNAL_ERROR,
2375
+ message: error instanceof Error ? error.message : String(error)
2376
+ }
2377
+ });
2378
+ }
2379
+ async function runtimeBackendGateway(app, opts) {
2380
+ const handle = async (request, reply) => {
2381
+ const { pluginId } = request.params;
2382
+ if (!isValidBoringPluginId(pluginId)) {
2383
+ return sendError(reply, new RuntimeBackendError(
2384
+ ErrorCode2.enum.RUNTIME_PLUGIN_NOT_FOUND,
2385
+ 404,
2386
+ "runtime backend plugin not found"
2387
+ ));
2388
+ }
2389
+ let path;
2390
+ try {
2391
+ path = normalizeGatewayPath(rawGatewayTail(request, pluginId));
2392
+ } catch (error) {
2393
+ return sendError(reply, error);
2394
+ }
2395
+ const abort = new AbortController();
2396
+ const close = () => abort.abort();
2397
+ request.raw.on("close", close);
2398
+ try {
2399
+ const response = await opts.registry.dispatch({
2400
+ pluginId,
2401
+ method: request.method,
2402
+ path,
2403
+ query: new URLSearchParams(request.query),
2404
+ headers: headersFromRequest(request),
2405
+ signal: abort.signal,
2406
+ body: request.body,
2407
+ logger: loggerFromRequest(request),
2408
+ ...firstString(request.headers["x-boring-workspace-id"]) ?? opts.defaultWorkspaceId ? { workspaceId: firstString(request.headers["x-boring-workspace-id"]) ?? opts.defaultWorkspaceId } : {}
2409
+ });
2410
+ return sendDispatchResponse(reply, response);
2411
+ } catch (error) {
2412
+ return sendError(reply, error);
2413
+ } finally {
2414
+ request.raw.off("close", close);
2415
+ }
2416
+ };
2417
+ app.all("/api/v1/plugins/:pluginId", handle);
2418
+ app.all("/api/v1/plugins/:pluginId/*", handle);
2419
+ }
1922
2420
  export {
1923
2421
  BoringPluginAssetManager,
2422
+ RuntimeBackendError,
2423
+ RuntimeBackendRegistry,
1924
2424
  aggregatePluginPrompts,
1925
2425
  bootstrapServer,
1926
2426
  boringPluginRoutes,
@@ -1931,14 +2431,17 @@ export {
1931
2431
  createInMemoryBridge,
1932
2432
  createWorkspaceUiTools,
1933
2433
  definePluginAsset,
2434
+ defineRuntimeServerPlugin,
1934
2435
  defineServerPlugin,
1935
2436
  pluginFileSignature,
1936
2437
  preflightBoringPlugins,
1937
2438
  readBoringPlugins,
1938
2439
  readPluginSignatureCache,
1939
2440
  resolvePluginAssetPath,
2441
+ runtimeBackendGateway,
1940
2442
  scanBoringPlugins,
1941
2443
  uiRoutes,
2444
+ validateRuntimeServerPlugin,
1942
2445
  validateServerPlugin,
1943
2446
  writePluginSignatureCache
1944
2447
  };