@hachej/boring-workspace 0.1.33 → 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.
@@ -9,7 +9,8 @@ import {
9
9
  VERCEL_SANDBOX_WORKSPACE_ROOT
10
10
  } from "@hachej/boring-agent/server";
11
11
  import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
12
- import { dirname as dirname7, join as join7 } from "path";
12
+ import { dirname as dirname7, isAbsolute as isAbsolute5, join as join7, resolve as resolve7 } from "path";
13
+ import { homedir } from "os";
13
14
  import { createRequire as createRequire4 } from "module";
14
15
  import { fileURLToPath } from "url";
15
16
 
@@ -103,7 +104,7 @@ function buildBoringSystemPrompt(opts) {
103
104
  "- 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).",
104
105
  "- 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.",
105
106
  '- 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"`.',
106
- "- Pi extension tools: `defineTool` and `export const tools` do NOT exist. Export `default function (pi) { pi.registerTool({ name, description, execute }) }`.",
107
+ '- 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.',
107
108
  '- Server/Pi tool method: `handler` \u2014 use `execute`. Return shape: `{ content: [{ type: "text", text }] }` (NEVER a bare string).',
108
109
  "- 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.",
109
110
  "- 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`.",
@@ -348,6 +349,14 @@ function resolveSafePluginEntryPath({
348
349
  }
349
350
 
350
351
  // src/server/agentPlugins/scan.ts
352
+ function normalizeBoringPluginSource(input) {
353
+ if (typeof input === "string") return { rootDir: resolve2(input), kind: "internal" };
354
+ return {
355
+ rootDir: resolve2(input.rootDir),
356
+ kind: input.kind,
357
+ ...input.workspaceId ? { workspaceId: input.workspaceId } : {}
358
+ };
359
+ }
351
360
  function pluginIdFromPackageJson(pkg, rootDir) {
352
361
  const explicitId = typeof pkg.boring?.id === "string" && pkg.boring.id.trim() ? pkg.boring.id.trim() : void 0;
353
362
  if (explicitId) return explicitId;
@@ -406,10 +415,11 @@ function packagePathContainmentIssues(rootDir, pkg) {
406
415
  return issues;
407
416
  }
408
417
  function discoverBoringPluginDirs(pluginDirs) {
409
- const out = /* @__PURE__ */ new Set();
418
+ const out = /* @__PURE__ */ new Map();
410
419
  const missingPackageJson = [];
411
420
  for (const raw of pluginDirs) {
412
- const dir = resolve2(raw);
421
+ const source = normalizeBoringPluginSource(raw);
422
+ const dir = source.rootDir;
413
423
  if (!existsSync2(dir)) continue;
414
424
  const info = statSync(dir);
415
425
  if (!info.isDirectory()) continue;
@@ -420,13 +430,19 @@ function discoverBoringPluginDirs(pluginDirs) {
420
430
  const child = join2(dir, entry.name);
421
431
  if (existsSync2(join2(child, "package.json"))) childPackageDirs.push(child);
422
432
  }
423
- if (hasPackageJson) out.add(dir);
424
- for (const child of childPackageDirs) out.add(child);
425
- if (!hasPackageJson && childPackageDirs.length === 0 && basename(dir) !== "extensions") {
433
+ if (hasPackageJson && !out.has(dir)) out.set(dir, source);
434
+ for (const child of childPackageDirs) {
435
+ if (!out.has(child)) out.set(child, { ...source, rootDir: child });
436
+ }
437
+ const collectionDirNames = /* @__PURE__ */ new Set(["extensions", "npm", "git"]);
438
+ if (!hasPackageJson && childPackageDirs.length === 0 && !collectionDirNames.has(basename(dir))) {
426
439
  missingPackageJson.push(dir);
427
440
  }
428
441
  }
429
- return { dirs: [...out].sort(), missingPackageJson: [...new Set(missingPackageJson)].sort() };
442
+ return {
443
+ sources: [...out.values()].sort((a, b) => a.rootDir.localeCompare(b.rootDir)),
444
+ missingPackageJson: [...new Set(missingPackageJson)].sort()
445
+ };
430
446
  }
431
447
  function scanBoringPlugins(pluginDirs) {
432
448
  const errors = [];
@@ -436,7 +452,8 @@ function scanBoringPlugins(pluginDirs) {
436
452
  for (const pluginDir of discovered.missingPackageJson) {
437
453
  errors.push({ pluginDir, code: "MISSING_PACKAGE_JSON", message: "package.json is missing" });
438
454
  }
439
- for (const rootDir of discovered.dirs) {
455
+ for (const source of discovered.sources) {
456
+ const rootDir = source.rootDir;
440
457
  let raw;
441
458
  try {
442
459
  raw = parsePackageJson(rootDir);
@@ -474,15 +491,26 @@ function scanBoringPlugins(pluginDirs) {
474
491
  } else {
475
492
  const previous = seenIds.get(id);
476
493
  if (previous) {
477
- errors.push({
478
- pluginDir: rootDir,
479
- pluginId: id,
480
- code: "INVALID_PLUGIN_METADATA",
481
- message: `duplicate plugin id "${id}" also declared by ${previous}`
482
- });
483
494
  const previousPluginIndex = plugins.findIndex((plugin) => plugin.id === id);
484
- if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
485
- canAddPlugin = false;
495
+ const previousPlugin = previousPluginIndex >= 0 ? plugins[previousPluginIndex] : void 0;
496
+ const currentIsWorkspaceLocal = Boolean(source.workspaceId);
497
+ const previousIsWorkspaceLocal = Boolean(previousPlugin?.source.workspaceId);
498
+ const currentMayShadowPrevious = source.kind === "external" && currentIsWorkspaceLocal && previousPlugin?.source.kind === "external" && !previousIsWorkspaceLocal;
499
+ if (currentMayShadowPrevious) {
500
+ if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
501
+ seenIds.set(id, rootDir);
502
+ } else if (!currentIsWorkspaceLocal && previousIsWorkspaceLocal) {
503
+ canAddPlugin = false;
504
+ } else {
505
+ errors.push({
506
+ pluginDir: rootDir,
507
+ pluginId: id,
508
+ code: "INVALID_PLUGIN_METADATA",
509
+ message: `duplicate plugin id "${id}" also declared by ${previous}`
510
+ });
511
+ if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
512
+ canAddPlugin = false;
513
+ }
486
514
  } else {
487
515
  seenIds.set(id, rootDir);
488
516
  }
@@ -494,6 +522,7 @@ function scanBoringPlugins(pluginDirs) {
494
522
  }
495
523
  if (!canAddPlugin) continue;
496
524
  const pkg = result.packageJson;
525
+ const hasBoring = pkg.boring !== void 0;
497
526
  const boring = pkg.boring ?? {};
498
527
  const pi = pkg.pi;
499
528
  const frontPath = resolvePluginPath(rootDir, boring.front, { mustExist: true });
@@ -506,11 +535,13 @@ function scanBoringPlugins(pluginDirs) {
506
535
  rootDir,
507
536
  version,
508
537
  boring,
538
+ hasBoring,
509
539
  ...pi ? { pi } : {},
510
540
  ...frontPath ? { frontPath, frontUrl: `/@fs/${frontPath}` } : {},
511
541
  ...serverPath ? { serverPath } : {},
512
542
  ...extensionPaths.length > 0 ? { extensionPaths } : {},
513
- ...skillPaths.length > 0 ? { skillPaths } : {}
543
+ ...skillPaths.length > 0 ? { skillPaths } : {},
544
+ source
514
545
  });
515
546
  }
516
547
  const preflight = { ok: errors.length === 0, errors };
@@ -933,10 +964,11 @@ function frontSignatureRoot(plugin) {
933
964
  return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel) ? frontRoot : dirname5(plugin.frontPath);
934
965
  }
935
966
  function pluginSignature(plugin) {
936
- 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(join4(plugin.rootDir, "shared"))).update(plugin.serverPath ?? "").update(pluginFileSignature(plugin.serverPath)).update(directorySignature(plugin.serverPath ? dirname5(plugin.serverPath) : void 0)).update((plugin.extensionPaths ?? []).join("\0")).update((plugin.skillPaths ?? []).join("\0")).digest("hex");
967
+ 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(join4(plugin.rootDir, "shared"))).update(plugin.serverPath ?? "").update(pluginFileSignature(plugin.serverPath)).update(directorySignature(plugin.serverPath ? dirname5(plugin.serverPath) : void 0)).update((plugin.extensionPaths ?? []).join("\0")).update((plugin.skillPaths ?? []).join("\0")).digest("hex");
937
968
  }
938
969
  function computeRequiresRestart(previous, next) {
939
970
  if (!previous) return [];
971
+ if (previous.source.kind === "external" && next.source.kind === "external") return [];
940
972
  const prevHasServer = !!previous.serverPath;
941
973
  const nextHasServer = !!next.serverPath;
942
974
  if (!prevHasServer && !nextHasServer) return [];
@@ -982,8 +1014,10 @@ var BoringPluginAssetManager = class {
982
1014
  version: plugin.version,
983
1015
  revision: plugin.revision,
984
1016
  rootDir: plugin.rootDir,
1017
+ source: plugin.source,
985
1018
  ...plugin.frontPath ? { frontPath: plugin.frontPath } : {},
986
- ...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {}
1019
+ ...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {},
1020
+ ...plugin.serverPath ? { serverPath: plugin.serverPath } : {}
987
1021
  }));
988
1022
  }
989
1023
  inspectLoadedPiSnapshot() {
@@ -1023,7 +1057,7 @@ ${prompts.join("\n\n")}` } : {}
1023
1057
  async doLoadOnce() {
1024
1058
  this.lastErrors.clear();
1025
1059
  const scan = scanBoringPlugins(this.pluginDirs);
1026
- const nextPlugins = scan.plugins;
1060
+ const nextPlugins = scan.plugins.filter((plugin) => plugin.hasBoring);
1027
1061
  const nextIds = new Set(nextPlugins.map((plugin) => plugin.id));
1028
1062
  const invalidPluginDirs = new Set(scan.preflight.errors.map((error) => resolve4(error.pluginDir)));
1029
1063
  const events = [];
@@ -1187,31 +1221,7 @@ function collectRestartWarnings(events) {
1187
1221
  return warnings;
1188
1222
  }
1189
1223
  async function boringPluginRoutes(app, opts) {
1190
- const { manager, rebuildPlugins, enableReloadRoute = true } = opts;
1191
- if (enableReloadRoute) {
1192
- app.post("/api/boring.reload", async (_request, reply) => {
1193
- const scan = await manager.load();
1194
- const rebuild = rebuildPlugins ? await rebuildPlugins() : { ok: true, diagnostics: [] };
1195
- const restart_warnings = collectRestartWarnings(scan.events);
1196
- const hasFailures = scan.errors.length > 0 || rebuild.diagnostics.length > 0;
1197
- if (hasFailures) {
1198
- return reply.status(422).send({
1199
- ok: false,
1200
- errors: scan.errors,
1201
- diagnostics: rebuild.diagnostics,
1202
- plugins: scan.loaded,
1203
- // Even on failure, emit warnings for plugins that DID reload
1204
- // — partial-failure tolerance means some loaded successfully.
1205
- ...restart_warnings.length > 0 ? { restart_warnings } : {}
1206
- });
1207
- }
1208
- return reply.send({
1209
- ok: true,
1210
- plugins: scan.loaded,
1211
- ...restart_warnings.length > 0 ? { restart_warnings } : {}
1212
- });
1213
- });
1214
- }
1224
+ const { manager } = opts;
1215
1225
  const listPlugins = async () => manager.list();
1216
1226
  app.get("/api/v1/agent-plugins", listPlugins);
1217
1227
  const getPluginError = async (request, reply) => {
@@ -1280,29 +1290,106 @@ async function boringPluginRoutes(app, opts) {
1280
1290
  });
1281
1291
  }
1282
1292
 
1283
- // src/server/agentPlugins/aggregatePluginPrompts.ts
1284
- function aggregatePluginPrompts(manager) {
1285
- const prompts = manager.list().map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
1286
- if (prompts.length === 0) return void 0;
1287
- return `# Loaded boring-ui plugin context
1293
+ // src/server/runtimeBackend/defineRuntimeServerPlugin.ts
1294
+ function isPlainObject(value) {
1295
+ if (typeof value !== "object" || value === null) return false;
1296
+ const proto = Object.getPrototypeOf(value);
1297
+ return proto === Object.prototype || proto === null;
1298
+ }
1299
+ function validateRuntimeServerPlugin(value) {
1300
+ if (!isPlainObject(value)) {
1301
+ throw new Error("runtime server plugin default export must be a plain object");
1302
+ }
1303
+ if ("id" in value) {
1304
+ throw new Error("runtime server plugin must not declare id; the host supplies plugin id from package metadata");
1305
+ }
1306
+ if (typeof value.routes !== "function") {
1307
+ throw new Error("runtime server plugin default export must define routes(router)");
1308
+ }
1309
+ if (value.dispose !== void 0 && typeof value.dispose !== "function") {
1310
+ throw new Error("runtime server plugin dispose must be a function when provided");
1311
+ }
1312
+ return value;
1313
+ }
1314
+ function isRuntimePluginResponse(value) {
1315
+ return isPlainObject(value) && value.kind === "response";
1316
+ }
1288
1317
 
1289
- ${prompts.join("\n\n")}`;
1318
+ // src/server/runtimeBackend/runtimePathSegments.ts
1319
+ function findUnsafeRuntimePathSegment(path) {
1320
+ if (path.includes("\\")) return "\\";
1321
+ for (const segment of path.split("/")) {
1322
+ if (segment === "." || segment === "..") return segment;
1323
+ }
1324
+ return null;
1325
+ }
1326
+ function describeUnsafeRuntimePathSegment(segment) {
1327
+ if (segment === "\\") return "backslashes";
1328
+ return `${segment} segments`;
1290
1329
  }
1291
1330
 
1292
- // src/app/server/pluginEntryResolver.ts
1293
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1294
- import { join as join5, resolve as resolve5 } from "path";
1295
- import { createRequire as createRequire2 } from "module";
1296
- import { pathToFileURL } from "url";
1297
- function readPluginPackageJson(dir) {
1298
- const pkgPath = resolve5(dir, "package.json");
1299
- if (!existsSync5(pkgPath)) return null;
1331
+ // src/server/runtimeBackend/routerCapture.ts
1332
+ var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "ALL"];
1333
+ function routeKey(method, path) {
1334
+ return `${method} ${path}`;
1335
+ }
1336
+ function runtimeRouteKey(method, path) {
1337
+ return routeKey(method.toUpperCase(), path);
1338
+ }
1339
+ function validateRuntimeRoutePath(path) {
1340
+ if (typeof path !== "string" || path.length === 0) {
1341
+ throw new Error("runtime route path must be a non-empty string");
1342
+ }
1343
+ if (!path.startsWith("/")) {
1344
+ throw new Error(`runtime route path must start with /: ${path}`);
1345
+ }
1346
+ if (path.includes("?") || path.includes("#")) {
1347
+ throw new Error(`runtime route path must not include query strings or fragments: ${path}`);
1348
+ }
1349
+ let decodedPath;
1300
1350
  try {
1301
- return JSON.parse(readFileSync4(pkgPath, "utf8"));
1351
+ decodedPath = decodeURIComponent(path);
1302
1352
  } catch {
1303
- return null;
1353
+ throw new Error(`runtime route path must be valid percent-encoding: ${path}`);
1354
+ }
1355
+ const unsafeSegment = findUnsafeRuntimePathSegment(decodedPath);
1356
+ if (unsafeSegment) {
1357
+ throw new Error(`runtime route path must not contain ${describeUnsafeRuntimePathSegment(unsafeSegment)}: ${path}`);
1304
1358
  }
1359
+ if (path.includes(":")) {
1360
+ throw new Error(`runtime route path must be exact and must not contain params: ${path}`);
1361
+ }
1362
+ if (path.includes("*")) {
1363
+ throw new Error(`runtime route path must be exact and must not contain wildcards: ${path}`);
1364
+ }
1365
+ return path;
1366
+ }
1367
+ async function captureRuntimeRoutes(register) {
1368
+ const routes = [];
1369
+ const seen = /* @__PURE__ */ new Set();
1370
+ const add = (method, path, handler) => {
1371
+ if (typeof handler !== "function") {
1372
+ throw new Error(`runtime route ${method} ${path} handler must be a function`);
1373
+ }
1374
+ const normalizedPath = validateRuntimeRoutePath(path);
1375
+ const key = routeKey(method, normalizedPath);
1376
+ if (seen.has(key)) throw new Error(`duplicate runtime route: ${key}`);
1377
+ seen.add(key);
1378
+ routes.push({ method, path: normalizedPath, handler });
1379
+ };
1380
+ const router = Object.fromEntries(
1381
+ METHODS.map((method) => [method.toLowerCase(), (path, handler) => add(method, path, handler)])
1382
+ );
1383
+ await register(router);
1384
+ return routes;
1305
1385
  }
1386
+
1387
+ // src/server/runtimeBackend/runtimeBackendRegistry.ts
1388
+ import { ErrorCode } from "@hachej/boring-agent/shared";
1389
+
1390
+ // src/server/pluginImports/importServerModule.ts
1391
+ import { createRequire as createRequire2 } from "module";
1392
+ import { pathToFileURL } from "url";
1306
1393
  var require3 = createRequire2(import.meta.url);
1307
1394
  var warnedJitiMissing = false;
1308
1395
  function warnJitiUnavailable(serverPath, reason) {
@@ -1337,6 +1424,379 @@ async function importServerModule(serverPath, hotReload) {
1337
1424
  href
1338
1425
  );
1339
1426
  }
1427
+
1428
+ // src/server/runtimeBackend/runtimeBackendRegistry.ts
1429
+ var RuntimeBackendError = class extends Error {
1430
+ constructor(code, statusCode, message, details) {
1431
+ super(message);
1432
+ this.code = code;
1433
+ this.statusCode = statusCode;
1434
+ this.details = details;
1435
+ this.name = "RuntimeBackendError";
1436
+ }
1437
+ code;
1438
+ statusCode;
1439
+ details;
1440
+ };
1441
+ function errorMessage(error) {
1442
+ return error instanceof Error ? error.stack ?? error.message : String(error);
1443
+ }
1444
+ function moduleValue(mod) {
1445
+ return typeof mod === "object" && mod !== null && "default" in mod ? mod.default : mod;
1446
+ }
1447
+ function toRouteMap(routes) {
1448
+ const map = /* @__PURE__ */ new Map();
1449
+ for (const route of routes) map.set(runtimeRouteKey(route.method, route.path), route);
1450
+ return map;
1451
+ }
1452
+ function assertJsonSerializable(value) {
1453
+ if (value === void 0 || value === null) return;
1454
+ if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
1455
+ throw new RuntimeBackendError(
1456
+ ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
1457
+ 500,
1458
+ "runtime plugin response is not JSON-serializable"
1459
+ );
1460
+ }
1461
+ try {
1462
+ JSON.stringify(value);
1463
+ } catch (error) {
1464
+ throw new RuntimeBackendError(
1465
+ ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
1466
+ 500,
1467
+ `runtime plugin response is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`
1468
+ );
1469
+ }
1470
+ }
1471
+ function normalizeResponse(value) {
1472
+ if (value === void 0 || value === null) return { status: 204, headers: {} };
1473
+ if (isRuntimePluginResponse(value)) return normalizeExplicitResponse(value);
1474
+ assertJsonSerializable(value);
1475
+ return { status: 200, headers: { "content-type": "application/json; charset=utf-8" }, body: value };
1476
+ }
1477
+ function normalizeExplicitResponse(value) {
1478
+ const status = value.status ?? (value.body === void 0 || value.body === null ? 204 : 200);
1479
+ if (!Number.isInteger(status) || status < 100 || status > 599) {
1480
+ throw new RuntimeBackendError(
1481
+ ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
1482
+ 500,
1483
+ "runtime plugin response status must be an integer HTTP status code"
1484
+ );
1485
+ }
1486
+ const headers = {};
1487
+ if (value.headers !== void 0) {
1488
+ for (const [name, headerValue] of Object.entries(value.headers)) {
1489
+ if (typeof headerValue !== "string") {
1490
+ throw new RuntimeBackendError(
1491
+ ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
1492
+ 500,
1493
+ "runtime plugin response headers must be strings"
1494
+ );
1495
+ }
1496
+ headers[name] = headerValue;
1497
+ }
1498
+ }
1499
+ if (value.body === void 0 || value.body === null) return { status, headers };
1500
+ assertJsonSerializable(value.body);
1501
+ if (!Object.keys(headers).some((name) => name.toLowerCase() === "content-type")) {
1502
+ headers["content-type"] = "application/json; charset=utf-8";
1503
+ }
1504
+ return { status, headers, body: value.body };
1505
+ }
1506
+ async function disposeSnapshot(snapshot) {
1507
+ if (!snapshot.module.dispose) return [];
1508
+ try {
1509
+ await snapshot.module.dispose();
1510
+ return [];
1511
+ } catch (error) {
1512
+ return [{
1513
+ pluginId: snapshot.pluginId,
1514
+ source: `runtime backend dispose (${snapshot.pluginId})`,
1515
+ code: ErrorCode.enum.RUNTIME_PLUGIN_LOAD_FAILED,
1516
+ message: errorMessage(error)
1517
+ }];
1518
+ }
1519
+ }
1520
+ var RuntimeBackendRegistry = class {
1521
+ snapshots = /* @__PURE__ */ new Map();
1522
+ lastDiagnostics = [];
1523
+ reloadQueue = Promise.resolve({ ok: true, diagnostics: [] });
1524
+ getDiagnostics() {
1525
+ return [...this.lastDiagnostics];
1526
+ }
1527
+ listPluginIds() {
1528
+ return [...this.snapshots.keys()].sort();
1529
+ }
1530
+ async reloadFromLoadedPlugins(plugins) {
1531
+ const run = this.reloadQueue.then(() => this.reloadOnce(plugins), () => this.reloadOnce(plugins));
1532
+ this.reloadQueue = run.then(() => ({ ok: true, diagnostics: [] }), () => ({ ok: false, diagnostics: [] }));
1533
+ return run;
1534
+ }
1535
+ async close() {
1536
+ const run = this.reloadQueue.then(() => this.closeOnce(), () => this.closeOnce());
1537
+ this.reloadQueue = run.then(() => ({ ok: true, diagnostics: [] }), () => ({ ok: false, diagnostics: [] }));
1538
+ return run;
1539
+ }
1540
+ async dispatch(request) {
1541
+ const snapshot = this.snapshots.get(request.pluginId);
1542
+ if (!snapshot) {
1543
+ throw new RuntimeBackendError(
1544
+ ErrorCode.enum.RUNTIME_PLUGIN_NOT_FOUND,
1545
+ 404,
1546
+ `runtime backend plugin not found: ${request.pluginId}`
1547
+ );
1548
+ }
1549
+ if (snapshot.source.workspaceId && snapshot.source.workspaceId !== request.workspaceId) {
1550
+ throw new RuntimeBackendError(
1551
+ ErrorCode.enum.RUNTIME_PLUGIN_NOT_FOUND,
1552
+ 404,
1553
+ `runtime backend plugin not found in workspace: ${request.pluginId}`
1554
+ );
1555
+ }
1556
+ const route = snapshot.routes.get(runtimeRouteKey(request.method, request.path)) ?? snapshot.routes.get(runtimeRouteKey("ALL", request.path));
1557
+ if (!route) {
1558
+ throw new RuntimeBackendError(
1559
+ ErrorCode.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
1560
+ 404,
1561
+ `runtime backend route not found: ${request.method.toUpperCase()} ${request.path}`
1562
+ );
1563
+ }
1564
+ const ctx = {
1565
+ pluginId: request.pluginId,
1566
+ method: request.method.toUpperCase(),
1567
+ path: request.path,
1568
+ query: request.query,
1569
+ headers: request.headers,
1570
+ signal: request.signal,
1571
+ body: request.body,
1572
+ logger: request.logger
1573
+ };
1574
+ try {
1575
+ return normalizeResponse(await route.handler(ctx));
1576
+ } catch (error) {
1577
+ if (error instanceof RuntimeBackendError) throw error;
1578
+ throw new RuntimeBackendError(
1579
+ ErrorCode.enum.RUNTIME_PLUGIN_HANDLER_FAILED,
1580
+ 500,
1581
+ error instanceof Error ? error.message : String(error)
1582
+ );
1583
+ }
1584
+ }
1585
+ async reloadOnce(plugins) {
1586
+ const diagnostics = [];
1587
+ const externalRuntimePlugins = plugins.filter((plugin) => plugin.source.kind === "external" && plugin.serverPath);
1588
+ const nextIds = new Set(externalRuntimePlugins.map((plugin) => plugin.id));
1589
+ for (const id of [...this.snapshots.keys()]) {
1590
+ if (nextIds.has(id)) continue;
1591
+ const previous = this.snapshots.get(id);
1592
+ if (!previous) continue;
1593
+ this.snapshots.delete(id);
1594
+ diagnostics.push(...await disposeSnapshot(previous));
1595
+ }
1596
+ for (const plugin of externalRuntimePlugins) {
1597
+ const serverPath = plugin.serverPath;
1598
+ if (!serverPath) continue;
1599
+ try {
1600
+ const mod = await importServerModule(serverPath, true);
1601
+ const runtimePlugin = validateRuntimeServerPlugin(moduleValue(mod));
1602
+ const routes = await captureRuntimeRoutes((router) => runtimePlugin.routes(router));
1603
+ const nextSnapshot = {
1604
+ pluginId: plugin.id,
1605
+ source: plugin.source,
1606
+ module: runtimePlugin,
1607
+ routes: toRouteMap(routes)
1608
+ };
1609
+ const previous = this.snapshots.get(plugin.id);
1610
+ this.snapshots.set(plugin.id, nextSnapshot);
1611
+ if (previous) diagnostics.push(...await disposeSnapshot(previous));
1612
+ } catch (error) {
1613
+ diagnostics.push({
1614
+ pluginId: plugin.id,
1615
+ source: `runtime backend (${plugin.id})`,
1616
+ code: ErrorCode.enum.RUNTIME_PLUGIN_LOAD_FAILED,
1617
+ message: errorMessage(error)
1618
+ });
1619
+ }
1620
+ }
1621
+ this.lastDiagnostics = diagnostics;
1622
+ return { ok: diagnostics.length === 0, diagnostics };
1623
+ }
1624
+ async closeOnce() {
1625
+ const diagnostics = [];
1626
+ for (const snapshot of this.snapshots.values()) {
1627
+ diagnostics.push(...await disposeSnapshot(snapshot));
1628
+ }
1629
+ this.snapshots.clear();
1630
+ this.lastDiagnostics = diagnostics;
1631
+ return { ok: diagnostics.length === 0, diagnostics };
1632
+ }
1633
+ };
1634
+
1635
+ // src/server/runtimeBackend/runtimeBackendGateway.ts
1636
+ import { ErrorCode as ErrorCode2 } from "@hachej/boring-agent/shared";
1637
+ var GATEWAY_PREFIX = "/api/v1/plugins/";
1638
+ function rawPathFromRequest(request) {
1639
+ const url = request.raw.url ?? request.url;
1640
+ const queryIndex = url.indexOf("?");
1641
+ return queryIndex === -1 ? url : url.slice(0, queryIndex);
1642
+ }
1643
+ function rawGatewayTail(request, pluginId) {
1644
+ const rawPath = rawPathFromRequest(request);
1645
+ const prefix = `${GATEWAY_PREFIX}${pluginId}`;
1646
+ if (rawPath === prefix || rawPath === `${prefix}/`) return "/";
1647
+ if (!rawPath.startsWith(`${prefix}/`)) {
1648
+ throw new RuntimeBackendError(
1649
+ ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
1650
+ 404,
1651
+ "runtime backend route not found"
1652
+ );
1653
+ }
1654
+ return rawPath.slice(prefix.length);
1655
+ }
1656
+ function normalizeGatewayPath(rawTail) {
1657
+ let path;
1658
+ try {
1659
+ path = decodeURIComponent(rawTail);
1660
+ } catch {
1661
+ throw new RuntimeBackendError(
1662
+ ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
1663
+ 404,
1664
+ "runtime backend route path is not valid percent-encoding"
1665
+ );
1666
+ }
1667
+ if (path.length === 0) path = "/";
1668
+ const unsafeSegment = findUnsafeRuntimePathSegment(path);
1669
+ if (unsafeSegment) {
1670
+ throw new RuntimeBackendError(
1671
+ ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
1672
+ 404,
1673
+ `runtime backend route path must not contain ${describeUnsafeRuntimePathSegment(unsafeSegment)}`
1674
+ );
1675
+ }
1676
+ return path;
1677
+ }
1678
+ function firstString(value) {
1679
+ if (Array.isArray(value)) return value[0];
1680
+ return value;
1681
+ }
1682
+ function headersFromRequest(request) {
1683
+ const headers = new Headers();
1684
+ for (const [name, value] of Object.entries(request.headers)) {
1685
+ if (value === void 0) continue;
1686
+ if (Array.isArray(value)) {
1687
+ for (const item of value) headers.append(name, item);
1688
+ } else {
1689
+ headers.set(name, String(value));
1690
+ }
1691
+ }
1692
+ return headers;
1693
+ }
1694
+ function loggerFromRequest(request) {
1695
+ return {
1696
+ debug: (arg, message) => {
1697
+ if (message === void 0) request.log.debug(arg);
1698
+ else request.log.debug(arg, message);
1699
+ },
1700
+ info: (arg, message) => {
1701
+ if (message === void 0) request.log.info(arg);
1702
+ else request.log.info(arg, message);
1703
+ },
1704
+ warn: (arg, message) => {
1705
+ if (message === void 0) request.log.warn(arg);
1706
+ else request.log.warn(arg, message);
1707
+ },
1708
+ error: (arg, message) => {
1709
+ if (message === void 0) request.log.error(arg);
1710
+ else request.log.error(arg, message);
1711
+ }
1712
+ };
1713
+ }
1714
+ function sendDispatchResponse(reply, response) {
1715
+ reply.status(response.status);
1716
+ for (const [name, value] of Object.entries(response.headers)) reply.header(name, value);
1717
+ if (response.body === void 0 || response.body === null) return reply.send();
1718
+ return reply.send(response.body);
1719
+ }
1720
+ function sendError(reply, error) {
1721
+ if (error instanceof RuntimeBackendError) {
1722
+ return reply.status(error.statusCode).send({
1723
+ error: {
1724
+ code: error.code,
1725
+ message: error.message,
1726
+ ...error.details ? { details: error.details } : {}
1727
+ }
1728
+ });
1729
+ }
1730
+ return reply.status(500).send({
1731
+ error: {
1732
+ code: ErrorCode2.enum.INTERNAL_ERROR,
1733
+ message: error instanceof Error ? error.message : String(error)
1734
+ }
1735
+ });
1736
+ }
1737
+ async function runtimeBackendGateway(app, opts) {
1738
+ const handle = async (request, reply) => {
1739
+ const { pluginId } = request.params;
1740
+ if (!isValidBoringPluginId(pluginId)) {
1741
+ return sendError(reply, new RuntimeBackendError(
1742
+ ErrorCode2.enum.RUNTIME_PLUGIN_NOT_FOUND,
1743
+ 404,
1744
+ "runtime backend plugin not found"
1745
+ ));
1746
+ }
1747
+ let path;
1748
+ try {
1749
+ path = normalizeGatewayPath(rawGatewayTail(request, pluginId));
1750
+ } catch (error) {
1751
+ return sendError(reply, error);
1752
+ }
1753
+ const abort = new AbortController();
1754
+ const close = () => abort.abort();
1755
+ request.raw.on("close", close);
1756
+ try {
1757
+ const response = await opts.registry.dispatch({
1758
+ pluginId,
1759
+ method: request.method,
1760
+ path,
1761
+ query: new URLSearchParams(request.query),
1762
+ headers: headersFromRequest(request),
1763
+ signal: abort.signal,
1764
+ body: request.body,
1765
+ logger: loggerFromRequest(request),
1766
+ ...firstString(request.headers["x-boring-workspace-id"]) ?? opts.defaultWorkspaceId ? { workspaceId: firstString(request.headers["x-boring-workspace-id"]) ?? opts.defaultWorkspaceId } : {}
1767
+ });
1768
+ return sendDispatchResponse(reply, response);
1769
+ } catch (error) {
1770
+ return sendError(reply, error);
1771
+ } finally {
1772
+ request.raw.off("close", close);
1773
+ }
1774
+ };
1775
+ app.all("/api/v1/plugins/:pluginId", handle);
1776
+ app.all("/api/v1/plugins/:pluginId/*", handle);
1777
+ }
1778
+
1779
+ // src/server/agentPlugins/aggregatePluginPrompts.ts
1780
+ function aggregatePluginPrompts(manager) {
1781
+ const prompts = manager.list().map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
1782
+ if (prompts.length === 0) return void 0;
1783
+ return `# Loaded boring-ui plugin context
1784
+
1785
+ ${prompts.join("\n\n")}`;
1786
+ }
1787
+
1788
+ // src/app/server/pluginEntryResolver.ts
1789
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1790
+ import { join as join5, resolve as resolve5 } from "path";
1791
+ function readPluginPackageJson(dir) {
1792
+ const pkgPath = resolve5(dir, "package.json");
1793
+ if (!existsSync5(pkgPath)) return null;
1794
+ try {
1795
+ return JSON.parse(readFileSync4(pkgPath, "utf8"));
1796
+ } catch {
1797
+ return null;
1798
+ }
1799
+ }
1340
1800
  function resolveDirServerEntryPath(dir) {
1341
1801
  const rootDir = resolve5(dir);
1342
1802
  const pkg = readPluginPackageJson(rootDir);
@@ -2283,7 +2743,50 @@ async function provisionWorkspaceAgentServer(opts) {
2283
2743
  force: opts.force
2284
2744
  });
2285
2745
  }
2286
- function collectBoringPluginDirs(workspaceRoot, pluginCollection, additionalPluginDirs = []) {
2746
+ function uniquePluginSources(sources) {
2747
+ const byRoot = /* @__PURE__ */ new Map();
2748
+ for (const source of sources) {
2749
+ const existing = byRoot.get(source.rootDir);
2750
+ if (!existing || !existing.workspaceId && source.workspaceId) byRoot.set(source.rootDir, source);
2751
+ }
2752
+ return [...byRoot.values()];
2753
+ }
2754
+ var REMOTE_PI_PACKAGE_SOURCE_PREFIXES = ["npm:", "git:", "github:", "http:", "https:", "ssh:"];
2755
+ function piPackageSourceValue(entry) {
2756
+ if (typeof entry === "string") return entry;
2757
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
2758
+ const source = entry.source;
2759
+ return typeof source === "string" ? source : void 0;
2760
+ }
2761
+ return void 0;
2762
+ }
2763
+ function resolveLocalPiPackageSource(settingsDir, source) {
2764
+ const path = source.startsWith("file:") ? source.slice("file:".length) : source;
2765
+ if (!path) return void 0;
2766
+ if (REMOTE_PI_PACKAGE_SOURCE_PREFIXES.some((prefix) => path.startsWith(prefix))) return void 0;
2767
+ if (!isAbsolute5(path) && path !== "." && path !== "./" && !path.startsWith("./") && !path.startsWith("../")) return void 0;
2768
+ return resolve7(settingsDir, path);
2769
+ }
2770
+ function readPiSettingsBoringPluginSources(settingsPath, workspaceId) {
2771
+ let raw;
2772
+ try {
2773
+ raw = JSON.parse(readFileSync6(settingsPath, "utf8"));
2774
+ } catch {
2775
+ return [];
2776
+ }
2777
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
2778
+ const packages = raw.packages;
2779
+ if (!Array.isArray(packages)) return [];
2780
+ const settingsDir = dirname7(settingsPath);
2781
+ return uniquePluginSources(
2782
+ packages.map(piPackageSourceValue).map((source) => source ? resolveLocalPiPackageSource(settingsDir, source) : void 0).filter((rootDir) => Boolean(rootDir)).map((rootDir) => ({
2783
+ rootDir,
2784
+ kind: "external",
2785
+ ...workspaceId ? { workspaceId } : {}
2786
+ }))
2787
+ );
2788
+ }
2789
+ function collectBoringPluginSources(workspaceRoot, pluginCollection, additionalPluginDirs = []) {
2287
2790
  const extensionPaths = pluginCollection.agentOptions.pi?.extensionPaths ?? [];
2288
2791
  const pluginRoots = extensionPaths.flatMap((path) => {
2289
2792
  try {
@@ -2292,11 +2795,16 @@ function collectBoringPluginDirs(workspaceRoot, pluginCollection, additionalPlug
2292
2795
  return [];
2293
2796
  }
2294
2797
  });
2295
- return [.../* @__PURE__ */ new Set([
2296
- join7(workspaceRoot, ".pi", "extensions"),
2297
- ...pluginRoots,
2298
- ...additionalPluginDirs
2299
- ])];
2798
+ return uniquePluginSources([
2799
+ { rootDir: join7(workspaceRoot, ".pi", "extensions"), kind: "external", workspaceId: workspaceRoot },
2800
+ { rootDir: join7(workspaceRoot, ".pi", "npm"), kind: "external", workspaceId: workspaceRoot },
2801
+ { rootDir: join7(workspaceRoot, ".pi", "git"), kind: "external", workspaceId: workspaceRoot },
2802
+ { rootDir: join7(homedir(), ".pi", "agent", "extensions"), kind: "external" },
2803
+ ...readPiSettingsBoringPluginSources(join7(workspaceRoot, ".pi", "settings.json"), workspaceRoot),
2804
+ ...readPiSettingsBoringPluginSources(join7(homedir(), ".pi", "agent", "settings.json")),
2805
+ ...pluginRoots.map((rootDir) => ({ rootDir, kind: "internal" })),
2806
+ ...additionalPluginDirs.map((entry) => typeof entry === "string" ? { rootDir: entry, kind: "internal" } : entry)
2807
+ ]);
2300
2808
  }
2301
2809
  function mergeRuntimeProvisioningInputs(plugins) {
2302
2810
  const byId = /* @__PURE__ */ new Map();
@@ -2409,11 +2917,17 @@ async function createWorkspaceAgentServer(opts = {}) {
2409
2917
  ...pluginCollection.agentOptions.pi?.packages ?? []
2410
2918
  ];
2411
2919
  const baseStaticPiExtensionPaths = pluginCollection.agentOptions.pi?.extensionPaths ?? [];
2412
- const boringPluginDirs = [
2413
- ...collectBoringPluginDirs(workspaceRoot, pluginCollection, opts.additionalBoringPluginDirs),
2414
- ...defaultPluginPackagePaths
2415
- ];
2416
- const staticPluginPackagePiSnapshot = pluginHotReload ? emptyPackageJsonPiSnapshot() : readWorkspacePluginPackagePiSnapshot(boringPluginDirs);
2920
+ const boringPluginDirs = [];
2921
+ const refreshBoringPluginDirs = () => {
2922
+ const next = uniquePluginSources([
2923
+ ...defaultPluginPackagePaths.map((rootDir) => ({ rootDir, kind: "internal" })),
2924
+ ...collectBoringPluginSources(workspaceRoot, pluginCollection, opts.additionalBoringPluginDirs)
2925
+ ]);
2926
+ boringPluginDirs.splice(0, boringPluginDirs.length, ...next);
2927
+ return boringPluginDirs;
2928
+ };
2929
+ refreshBoringPluginDirs();
2930
+ const staticPluginPackagePiSnapshot = pluginHotReload ? emptyPackageJsonPiSnapshot() : readWorkspacePluginPackagePiSnapshot(refreshBoringPluginDirs());
2417
2931
  const staticPiSkillPaths = [
2418
2932
  ...baseStaticPiSkillPaths,
2419
2933
  ...staticPluginPackagePiSnapshot.additionalSkillPaths
@@ -2426,17 +2940,18 @@ async function createWorkspaceAgentServer(opts = {}) {
2426
2940
  ...baseStaticPiExtensionPaths,
2427
2941
  ...staticPluginPackagePiSnapshot.extensionPaths
2428
2942
  ];
2429
- const getHotReloadablePiResources = pluginHotReload ? () => readWorkspacePluginPackagePiSnapshot(boringPluginDirs) : void 0;
2943
+ const getHotReloadablePiResources = pluginHotReload ? () => readWorkspacePluginPackagePiSnapshot(refreshBoringPluginDirs()) : void 0;
2430
2944
  const boringAssetManager = new BoringPluginAssetManager({
2431
2945
  pluginDirs: boringPluginDirs,
2432
2946
  errorRoot: join7(workspaceRoot, ".pi", "extensions"),
2433
2947
  frontTargetResolver: opts.boringPluginFrontTargetResolver,
2434
2948
  includeLegacyFrontUrl: opts.boringPluginIncludeLegacyFrontUrl
2435
2949
  });
2950
+ const runtimeBackendRegistry = new RuntimeBackendRegistry();
2436
2951
  const buildRuntimeProvisioningInputs = () => {
2437
2952
  const inputs = mergeRuntimeProvisioningInputs([
2438
2953
  ...pluginCollection.runtimePlugins,
2439
- ...readWorkspacePluginPackageRuntimePlugins(boringPluginDirs)
2954
+ ...readWorkspacePluginPackageRuntimePlugins(refreshBoringPluginDirs())
2440
2955
  ]);
2441
2956
  if (resolvedMode === "direct") return omitPluginAuthoringProvisioning(inputs);
2442
2957
  return inputs;
@@ -2499,7 +3014,9 @@ async function createWorkspaceAgentServer(opts = {}) {
2499
3014
  let restart_warnings = [];
2500
3015
  let diagnostics = [];
2501
3016
  if (pluginHotReload) {
3017
+ refreshBoringPluginDirs();
2502
3018
  const scan = await boringAssetManager.load();
3019
+ const backendReload = await runtimeBackendRegistry.reloadFromLoadedPlugins(boringAssetManager.inspectLoaded());
2503
3020
  restart_warnings = collectRestartWarnings(scan.events);
2504
3021
  const scanDiagnostics = scan.errors.map((error) => ({
2505
3022
  source: `boring plugin asset scan (${error.id})`,
@@ -2507,7 +3024,7 @@ async function createWorkspaceAgentServer(opts = {}) {
2507
3024
  pluginId: error.id
2508
3025
  }));
2509
3026
  const rebuild = await rebuildPlugins();
2510
- diagnostics = [...scanDiagnostics, ...rebuild.diagnostics];
3027
+ diagnostics = [...scanDiagnostics, ...backendReload.diagnostics, ...rebuild.diagnostics];
2511
3028
  }
2512
3029
  await runRuntimeProvisioning();
2513
3030
  const callerResult = await opts.beforeReload?.();
@@ -2533,19 +3050,26 @@ async function createWorkspaceAgentServer(opts = {}) {
2533
3050
  },
2534
3051
  systemPromptDynamic: pluginHotReload ? () => aggregatePluginPrompts(boringAssetManager) : void 0
2535
3052
  });
3053
+ refreshBoringPluginDirs();
2536
3054
  await boringAssetManager.load();
3055
+ await runtimeBackendRegistry.reloadFromLoadedPlugins(boringAssetManager.inspectLoaded());
3056
+ if (typeof app.addHook === "function") {
3057
+ app.addHook("onClose", async () => {
3058
+ await runtimeBackendRegistry.close();
3059
+ });
3060
+ }
2537
3061
  await app.register(uiRoutes, { bridge, preserveStateKeys: pluginCollection.preservedUiStateKeys });
2538
3062
  await app.register(boringPluginRoutes, {
2539
- manager: boringAssetManager,
2540
- rebuildPlugins,
2541
- enableReloadRoute: pluginHotReload
3063
+ manager: boringAssetManager
2542
3064
  });
3065
+ await app.register(runtimeBackendGateway, { registry: runtimeBackendRegistry, defaultWorkspaceId: workspaceRoot });
2543
3066
  for (const { routes } of pluginCollection.routeContributions) {
2544
3067
  await app.register(routes);
2545
3068
  }
2546
3069
  ;
2547
3070
  app.__boringRebuildPlugins = rebuildPlugins;
2548
3071
  app.__boringAssetManager = boringAssetManager;
3072
+ app.__boringRuntimeBackendRegistry = runtimeBackendRegistry;
2549
3073
  return app;
2550
3074
  }
2551
3075
  export {