@hachej/boring-workspace 0.1.33 → 0.1.35

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,13 +104,19 @@ 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`.",
110
111
  "- Dependency installs: do NOT install plugin UI dependencies at the workspace root. Install them inside `.pi/extensions/<name>/` and keep React/workspace/boring-ui-kit imports as host singletons, not plugin dependencies.",
111
112
  "- Hot-reload agent tools: do NOT put them in `.pi/extensions/<name>/server/index.ts`; use `pi.extensions` instead. `boring.server` requires static composition plus process restart."
112
113
  ].join("\n"),
114
+ [
115
+ "## Installing an existing or published plugin",
116
+ "To ADD an existing or published plugin (not author a new one), use `boring-ui-plugin install <source>` via bash \u2014 `<source>` is `npm:<package>`, `git:<repo>`, `github:<owner>/<repo>`, an `http(s)` git URL, or a local path; add `--global` for all workspaces (default is this workspace).",
117
+ "A bare `npm install <package>` does NOT register it as a plugin (no `.pi/settings.json` package source), so it will NOT load \u2014 always use `boring-ui-plugin install`, then ask the user to `/reload` (a `boring.server` backend also needs a process restart).",
118
+ "Inspect with `boring-ui-plugin list [--json]`; remove with `boring-ui-plugin remove <id-or-source>`."
119
+ ].join("\n"),
113
120
  docsBlock
114
121
  ].join("\n\n");
115
122
  }
@@ -348,6 +355,14 @@ function resolveSafePluginEntryPath({
348
355
  }
349
356
 
350
357
  // src/server/agentPlugins/scan.ts
358
+ function normalizeBoringPluginSource(input) {
359
+ if (typeof input === "string") return { rootDir: resolve2(input), kind: "internal" };
360
+ return {
361
+ rootDir: resolve2(input.rootDir),
362
+ kind: input.kind,
363
+ ...input.workspaceId ? { workspaceId: input.workspaceId } : {}
364
+ };
365
+ }
351
366
  function pluginIdFromPackageJson(pkg, rootDir) {
352
367
  const explicitId = typeof pkg.boring?.id === "string" && pkg.boring.id.trim() ? pkg.boring.id.trim() : void 0;
353
368
  if (explicitId) return explicitId;
@@ -406,10 +421,11 @@ function packagePathContainmentIssues(rootDir, pkg) {
406
421
  return issues;
407
422
  }
408
423
  function discoverBoringPluginDirs(pluginDirs) {
409
- const out = /* @__PURE__ */ new Set();
424
+ const out = /* @__PURE__ */ new Map();
410
425
  const missingPackageJson = [];
411
426
  for (const raw of pluginDirs) {
412
- const dir = resolve2(raw);
427
+ const source = normalizeBoringPluginSource(raw);
428
+ const dir = source.rootDir;
413
429
  if (!existsSync2(dir)) continue;
414
430
  const info = statSync(dir);
415
431
  if (!info.isDirectory()) continue;
@@ -420,13 +436,19 @@ function discoverBoringPluginDirs(pluginDirs) {
420
436
  const child = join2(dir, entry.name);
421
437
  if (existsSync2(join2(child, "package.json"))) childPackageDirs.push(child);
422
438
  }
423
- if (hasPackageJson) out.add(dir);
424
- for (const child of childPackageDirs) out.add(child);
425
- if (!hasPackageJson && childPackageDirs.length === 0 && basename(dir) !== "extensions") {
439
+ if (hasPackageJson && !out.has(dir)) out.set(dir, source);
440
+ for (const child of childPackageDirs) {
441
+ if (!out.has(child)) out.set(child, { ...source, rootDir: child });
442
+ }
443
+ const collectionDirNames = /* @__PURE__ */ new Set(["extensions", "npm", "git"]);
444
+ if (!hasPackageJson && childPackageDirs.length === 0 && !collectionDirNames.has(basename(dir))) {
426
445
  missingPackageJson.push(dir);
427
446
  }
428
447
  }
429
- return { dirs: [...out].sort(), missingPackageJson: [...new Set(missingPackageJson)].sort() };
448
+ return {
449
+ sources: [...out.values()].sort((a, b) => a.rootDir.localeCompare(b.rootDir)),
450
+ missingPackageJson: [...new Set(missingPackageJson)].sort()
451
+ };
430
452
  }
431
453
  function scanBoringPlugins(pluginDirs) {
432
454
  const errors = [];
@@ -436,7 +458,8 @@ function scanBoringPlugins(pluginDirs) {
436
458
  for (const pluginDir of discovered.missingPackageJson) {
437
459
  errors.push({ pluginDir, code: "MISSING_PACKAGE_JSON", message: "package.json is missing" });
438
460
  }
439
- for (const rootDir of discovered.dirs) {
461
+ for (const source of discovered.sources) {
462
+ const rootDir = source.rootDir;
440
463
  let raw;
441
464
  try {
442
465
  raw = parsePackageJson(rootDir);
@@ -474,15 +497,26 @@ function scanBoringPlugins(pluginDirs) {
474
497
  } else {
475
498
  const previous = seenIds.get(id);
476
499
  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
500
  const previousPluginIndex = plugins.findIndex((plugin) => plugin.id === id);
484
- if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
485
- canAddPlugin = false;
501
+ const previousPlugin = previousPluginIndex >= 0 ? plugins[previousPluginIndex] : void 0;
502
+ const currentIsWorkspaceLocal = Boolean(source.workspaceId);
503
+ const previousIsWorkspaceLocal = Boolean(previousPlugin?.source.workspaceId);
504
+ const currentMayShadowPrevious = source.kind === "external" && currentIsWorkspaceLocal && previousPlugin?.source.kind === "external" && !previousIsWorkspaceLocal;
505
+ if (currentMayShadowPrevious) {
506
+ if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
507
+ seenIds.set(id, rootDir);
508
+ } else if (!currentIsWorkspaceLocal && previousIsWorkspaceLocal) {
509
+ canAddPlugin = false;
510
+ } else {
511
+ errors.push({
512
+ pluginDir: rootDir,
513
+ pluginId: id,
514
+ code: "INVALID_PLUGIN_METADATA",
515
+ message: `duplicate plugin id "${id}" also declared by ${previous}`
516
+ });
517
+ if (previousPluginIndex >= 0) plugins.splice(previousPluginIndex, 1);
518
+ canAddPlugin = false;
519
+ }
486
520
  } else {
487
521
  seenIds.set(id, rootDir);
488
522
  }
@@ -494,6 +528,7 @@ function scanBoringPlugins(pluginDirs) {
494
528
  }
495
529
  if (!canAddPlugin) continue;
496
530
  const pkg = result.packageJson;
531
+ const hasBoring = pkg.boring !== void 0;
497
532
  const boring = pkg.boring ?? {};
498
533
  const pi = pkg.pi;
499
534
  const frontPath = resolvePluginPath(rootDir, boring.front, { mustExist: true });
@@ -506,11 +541,13 @@ function scanBoringPlugins(pluginDirs) {
506
541
  rootDir,
507
542
  version,
508
543
  boring,
544
+ hasBoring,
509
545
  ...pi ? { pi } : {},
510
546
  ...frontPath ? { frontPath, frontUrl: `/@fs/${frontPath}` } : {},
511
547
  ...serverPath ? { serverPath } : {},
512
548
  ...extensionPaths.length > 0 ? { extensionPaths } : {},
513
- ...skillPaths.length > 0 ? { skillPaths } : {}
549
+ ...skillPaths.length > 0 ? { skillPaths } : {},
550
+ source
514
551
  });
515
552
  }
516
553
  const preflight = { ok: errors.length === 0, errors };
@@ -933,10 +970,11 @@ function frontSignatureRoot(plugin) {
933
970
  return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel) ? frontRoot : dirname5(plugin.frontPath);
934
971
  }
935
972
  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");
973
+ 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
974
  }
938
975
  function computeRequiresRestart(previous, next) {
939
976
  if (!previous) return [];
977
+ if (previous.source.kind === "external" && next.source.kind === "external") return [];
940
978
  const prevHasServer = !!previous.serverPath;
941
979
  const nextHasServer = !!next.serverPath;
942
980
  if (!prevHasServer && !nextHasServer) return [];
@@ -982,8 +1020,10 @@ var BoringPluginAssetManager = class {
982
1020
  version: plugin.version,
983
1021
  revision: plugin.revision,
984
1022
  rootDir: plugin.rootDir,
1023
+ source: plugin.source,
985
1024
  ...plugin.frontPath ? { frontPath: plugin.frontPath } : {},
986
- ...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {}
1025
+ ...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {},
1026
+ ...plugin.serverPath ? { serverPath: plugin.serverPath } : {}
987
1027
  }));
988
1028
  }
989
1029
  inspectLoadedPiSnapshot() {
@@ -1023,7 +1063,7 @@ ${prompts.join("\n\n")}` } : {}
1023
1063
  async doLoadOnce() {
1024
1064
  this.lastErrors.clear();
1025
1065
  const scan = scanBoringPlugins(this.pluginDirs);
1026
- const nextPlugins = scan.plugins;
1066
+ const nextPlugins = scan.plugins.filter((plugin) => plugin.hasBoring);
1027
1067
  const nextIds = new Set(nextPlugins.map((plugin) => plugin.id));
1028
1068
  const invalidPluginDirs = new Set(scan.preflight.errors.map((error) => resolve4(error.pluginDir)));
1029
1069
  const events = [];
@@ -1187,31 +1227,7 @@ function collectRestartWarnings(events) {
1187
1227
  return warnings;
1188
1228
  }
1189
1229
  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
- }
1230
+ const { manager } = opts;
1215
1231
  const listPlugins = async () => manager.list();
1216
1232
  app.get("/api/v1/agent-plugins", listPlugins);
1217
1233
  const getPluginError = async (request, reply) => {
@@ -1280,29 +1296,106 @@ async function boringPluginRoutes(app, opts) {
1280
1296
  });
1281
1297
  }
1282
1298
 
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
1299
+ // src/server/runtimeBackend/defineRuntimeServerPlugin.ts
1300
+ function isPlainObject(value) {
1301
+ if (typeof value !== "object" || value === null) return false;
1302
+ const proto = Object.getPrototypeOf(value);
1303
+ return proto === Object.prototype || proto === null;
1304
+ }
1305
+ function validateRuntimeServerPlugin(value) {
1306
+ if (!isPlainObject(value)) {
1307
+ throw new Error("runtime server plugin default export must be a plain object");
1308
+ }
1309
+ if ("id" in value) {
1310
+ throw new Error("runtime server plugin must not declare id; the host supplies plugin id from package metadata");
1311
+ }
1312
+ if (typeof value.routes !== "function") {
1313
+ throw new Error("runtime server plugin default export must define routes(router)");
1314
+ }
1315
+ if (value.dispose !== void 0 && typeof value.dispose !== "function") {
1316
+ throw new Error("runtime server plugin dispose must be a function when provided");
1317
+ }
1318
+ return value;
1319
+ }
1320
+ function isRuntimePluginResponse(value) {
1321
+ return isPlainObject(value) && value.kind === "response";
1322
+ }
1288
1323
 
1289
- ${prompts.join("\n\n")}`;
1324
+ // src/server/runtimeBackend/runtimePathSegments.ts
1325
+ function findUnsafeRuntimePathSegment(path) {
1326
+ if (path.includes("\\")) return "\\";
1327
+ for (const segment of path.split("/")) {
1328
+ if (segment === "." || segment === "..") return segment;
1329
+ }
1330
+ return null;
1331
+ }
1332
+ function describeUnsafeRuntimePathSegment(segment) {
1333
+ if (segment === "\\") return "backslashes";
1334
+ return `${segment} segments`;
1290
1335
  }
1291
1336
 
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;
1337
+ // src/server/runtimeBackend/routerCapture.ts
1338
+ var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "ALL"];
1339
+ function routeKey(method, path) {
1340
+ return `${method} ${path}`;
1341
+ }
1342
+ function runtimeRouteKey(method, path) {
1343
+ return routeKey(method.toUpperCase(), path);
1344
+ }
1345
+ function validateRuntimeRoutePath(path) {
1346
+ if (typeof path !== "string" || path.length === 0) {
1347
+ throw new Error("runtime route path must be a non-empty string");
1348
+ }
1349
+ if (!path.startsWith("/")) {
1350
+ throw new Error(`runtime route path must start with /: ${path}`);
1351
+ }
1352
+ if (path.includes("?") || path.includes("#")) {
1353
+ throw new Error(`runtime route path must not include query strings or fragments: ${path}`);
1354
+ }
1355
+ let decodedPath;
1300
1356
  try {
1301
- return JSON.parse(readFileSync4(pkgPath, "utf8"));
1357
+ decodedPath = decodeURIComponent(path);
1302
1358
  } catch {
1303
- return null;
1359
+ throw new Error(`runtime route path must be valid percent-encoding: ${path}`);
1360
+ }
1361
+ const unsafeSegment = findUnsafeRuntimePathSegment(decodedPath);
1362
+ if (unsafeSegment) {
1363
+ throw new Error(`runtime route path must not contain ${describeUnsafeRuntimePathSegment(unsafeSegment)}: ${path}`);
1364
+ }
1365
+ if (path.includes(":")) {
1366
+ throw new Error(`runtime route path must be exact and must not contain params: ${path}`);
1304
1367
  }
1368
+ if (path.includes("*")) {
1369
+ throw new Error(`runtime route path must be exact and must not contain wildcards: ${path}`);
1370
+ }
1371
+ return path;
1372
+ }
1373
+ async function captureRuntimeRoutes(register) {
1374
+ const routes = [];
1375
+ const seen = /* @__PURE__ */ new Set();
1376
+ const add = (method, path, handler) => {
1377
+ if (typeof handler !== "function") {
1378
+ throw new Error(`runtime route ${method} ${path} handler must be a function`);
1379
+ }
1380
+ const normalizedPath = validateRuntimeRoutePath(path);
1381
+ const key = routeKey(method, normalizedPath);
1382
+ if (seen.has(key)) throw new Error(`duplicate runtime route: ${key}`);
1383
+ seen.add(key);
1384
+ routes.push({ method, path: normalizedPath, handler });
1385
+ };
1386
+ const router = Object.fromEntries(
1387
+ METHODS.map((method) => [method.toLowerCase(), (path, handler) => add(method, path, handler)])
1388
+ );
1389
+ await register(router);
1390
+ return routes;
1305
1391
  }
1392
+
1393
+ // src/server/runtimeBackend/runtimeBackendRegistry.ts
1394
+ import { ErrorCode } from "@hachej/boring-agent/shared";
1395
+
1396
+ // src/server/pluginImports/importServerModule.ts
1397
+ import { createRequire as createRequire2 } from "module";
1398
+ import { pathToFileURL } from "url";
1306
1399
  var require3 = createRequire2(import.meta.url);
1307
1400
  var warnedJitiMissing = false;
1308
1401
  function warnJitiUnavailable(serverPath, reason) {
@@ -1337,6 +1430,379 @@ async function importServerModule(serverPath, hotReload) {
1337
1430
  href
1338
1431
  );
1339
1432
  }
1433
+
1434
+ // src/server/runtimeBackend/runtimeBackendRegistry.ts
1435
+ var RuntimeBackendError = class extends Error {
1436
+ constructor(code, statusCode, message, details) {
1437
+ super(message);
1438
+ this.code = code;
1439
+ this.statusCode = statusCode;
1440
+ this.details = details;
1441
+ this.name = "RuntimeBackendError";
1442
+ }
1443
+ code;
1444
+ statusCode;
1445
+ details;
1446
+ };
1447
+ function errorMessage(error) {
1448
+ return error instanceof Error ? error.stack ?? error.message : String(error);
1449
+ }
1450
+ function moduleValue(mod) {
1451
+ return typeof mod === "object" && mod !== null && "default" in mod ? mod.default : mod;
1452
+ }
1453
+ function toRouteMap(routes) {
1454
+ const map = /* @__PURE__ */ new Map();
1455
+ for (const route of routes) map.set(runtimeRouteKey(route.method, route.path), route);
1456
+ return map;
1457
+ }
1458
+ function assertJsonSerializable(value) {
1459
+ if (value === void 0 || value === null) return;
1460
+ if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
1461
+ throw new RuntimeBackendError(
1462
+ ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
1463
+ 500,
1464
+ "runtime plugin response is not JSON-serializable"
1465
+ );
1466
+ }
1467
+ try {
1468
+ JSON.stringify(value);
1469
+ } catch (error) {
1470
+ throw new RuntimeBackendError(
1471
+ ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
1472
+ 500,
1473
+ `runtime plugin response is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`
1474
+ );
1475
+ }
1476
+ }
1477
+ function normalizeResponse(value) {
1478
+ if (value === void 0 || value === null) return { status: 204, headers: {} };
1479
+ if (isRuntimePluginResponse(value)) return normalizeExplicitResponse(value);
1480
+ assertJsonSerializable(value);
1481
+ return { status: 200, headers: { "content-type": "application/json; charset=utf-8" }, body: value };
1482
+ }
1483
+ function normalizeExplicitResponse(value) {
1484
+ const status = value.status ?? (value.body === void 0 || value.body === null ? 204 : 200);
1485
+ if (!Number.isInteger(status) || status < 100 || status > 599) {
1486
+ throw new RuntimeBackendError(
1487
+ ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
1488
+ 500,
1489
+ "runtime plugin response status must be an integer HTTP status code"
1490
+ );
1491
+ }
1492
+ const headers = {};
1493
+ if (value.headers !== void 0) {
1494
+ for (const [name, headerValue] of Object.entries(value.headers)) {
1495
+ if (typeof headerValue !== "string") {
1496
+ throw new RuntimeBackendError(
1497
+ ErrorCode.enum.RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED,
1498
+ 500,
1499
+ "runtime plugin response headers must be strings"
1500
+ );
1501
+ }
1502
+ headers[name] = headerValue;
1503
+ }
1504
+ }
1505
+ if (value.body === void 0 || value.body === null) return { status, headers };
1506
+ assertJsonSerializable(value.body);
1507
+ if (!Object.keys(headers).some((name) => name.toLowerCase() === "content-type")) {
1508
+ headers["content-type"] = "application/json; charset=utf-8";
1509
+ }
1510
+ return { status, headers, body: value.body };
1511
+ }
1512
+ async function disposeSnapshot(snapshot) {
1513
+ if (!snapshot.module.dispose) return [];
1514
+ try {
1515
+ await snapshot.module.dispose();
1516
+ return [];
1517
+ } catch (error) {
1518
+ return [{
1519
+ pluginId: snapshot.pluginId,
1520
+ source: `runtime backend dispose (${snapshot.pluginId})`,
1521
+ code: ErrorCode.enum.RUNTIME_PLUGIN_LOAD_FAILED,
1522
+ message: errorMessage(error)
1523
+ }];
1524
+ }
1525
+ }
1526
+ var RuntimeBackendRegistry = class {
1527
+ snapshots = /* @__PURE__ */ new Map();
1528
+ lastDiagnostics = [];
1529
+ reloadQueue = Promise.resolve({ ok: true, diagnostics: [] });
1530
+ getDiagnostics() {
1531
+ return [...this.lastDiagnostics];
1532
+ }
1533
+ listPluginIds() {
1534
+ return [...this.snapshots.keys()].sort();
1535
+ }
1536
+ async reloadFromLoadedPlugins(plugins) {
1537
+ const run = this.reloadQueue.then(() => this.reloadOnce(plugins), () => this.reloadOnce(plugins));
1538
+ this.reloadQueue = run.then(() => ({ ok: true, diagnostics: [] }), () => ({ ok: false, diagnostics: [] }));
1539
+ return run;
1540
+ }
1541
+ async close() {
1542
+ const run = this.reloadQueue.then(() => this.closeOnce(), () => this.closeOnce());
1543
+ this.reloadQueue = run.then(() => ({ ok: true, diagnostics: [] }), () => ({ ok: false, diagnostics: [] }));
1544
+ return run;
1545
+ }
1546
+ async dispatch(request) {
1547
+ const snapshot = this.snapshots.get(request.pluginId);
1548
+ if (!snapshot) {
1549
+ throw new RuntimeBackendError(
1550
+ ErrorCode.enum.RUNTIME_PLUGIN_NOT_FOUND,
1551
+ 404,
1552
+ `runtime backend plugin not found: ${request.pluginId}`
1553
+ );
1554
+ }
1555
+ if (snapshot.source.workspaceId && snapshot.source.workspaceId !== request.workspaceId) {
1556
+ throw new RuntimeBackendError(
1557
+ ErrorCode.enum.RUNTIME_PLUGIN_NOT_FOUND,
1558
+ 404,
1559
+ `runtime backend plugin not found in workspace: ${request.pluginId}`
1560
+ );
1561
+ }
1562
+ const route = snapshot.routes.get(runtimeRouteKey(request.method, request.path)) ?? snapshot.routes.get(runtimeRouteKey("ALL", request.path));
1563
+ if (!route) {
1564
+ throw new RuntimeBackendError(
1565
+ ErrorCode.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
1566
+ 404,
1567
+ `runtime backend route not found: ${request.method.toUpperCase()} ${request.path}`
1568
+ );
1569
+ }
1570
+ const ctx = {
1571
+ pluginId: request.pluginId,
1572
+ method: request.method.toUpperCase(),
1573
+ path: request.path,
1574
+ query: request.query,
1575
+ headers: request.headers,
1576
+ signal: request.signal,
1577
+ body: request.body,
1578
+ logger: request.logger
1579
+ };
1580
+ try {
1581
+ return normalizeResponse(await route.handler(ctx));
1582
+ } catch (error) {
1583
+ if (error instanceof RuntimeBackendError) throw error;
1584
+ throw new RuntimeBackendError(
1585
+ ErrorCode.enum.RUNTIME_PLUGIN_HANDLER_FAILED,
1586
+ 500,
1587
+ error instanceof Error ? error.message : String(error)
1588
+ );
1589
+ }
1590
+ }
1591
+ async reloadOnce(plugins) {
1592
+ const diagnostics = [];
1593
+ const externalRuntimePlugins = plugins.filter((plugin) => plugin.source.kind === "external" && plugin.serverPath);
1594
+ const nextIds = new Set(externalRuntimePlugins.map((plugin) => plugin.id));
1595
+ for (const id of [...this.snapshots.keys()]) {
1596
+ if (nextIds.has(id)) continue;
1597
+ const previous = this.snapshots.get(id);
1598
+ if (!previous) continue;
1599
+ this.snapshots.delete(id);
1600
+ diagnostics.push(...await disposeSnapshot(previous));
1601
+ }
1602
+ for (const plugin of externalRuntimePlugins) {
1603
+ const serverPath = plugin.serverPath;
1604
+ if (!serverPath) continue;
1605
+ try {
1606
+ const mod = await importServerModule(serverPath, true);
1607
+ const runtimePlugin = validateRuntimeServerPlugin(moduleValue(mod));
1608
+ const routes = await captureRuntimeRoutes((router) => runtimePlugin.routes(router));
1609
+ const nextSnapshot = {
1610
+ pluginId: plugin.id,
1611
+ source: plugin.source,
1612
+ module: runtimePlugin,
1613
+ routes: toRouteMap(routes)
1614
+ };
1615
+ const previous = this.snapshots.get(plugin.id);
1616
+ this.snapshots.set(plugin.id, nextSnapshot);
1617
+ if (previous) diagnostics.push(...await disposeSnapshot(previous));
1618
+ } catch (error) {
1619
+ diagnostics.push({
1620
+ pluginId: plugin.id,
1621
+ source: `runtime backend (${plugin.id})`,
1622
+ code: ErrorCode.enum.RUNTIME_PLUGIN_LOAD_FAILED,
1623
+ message: errorMessage(error)
1624
+ });
1625
+ }
1626
+ }
1627
+ this.lastDiagnostics = diagnostics;
1628
+ return { ok: diagnostics.length === 0, diagnostics };
1629
+ }
1630
+ async closeOnce() {
1631
+ const diagnostics = [];
1632
+ for (const snapshot of this.snapshots.values()) {
1633
+ diagnostics.push(...await disposeSnapshot(snapshot));
1634
+ }
1635
+ this.snapshots.clear();
1636
+ this.lastDiagnostics = diagnostics;
1637
+ return { ok: diagnostics.length === 0, diagnostics };
1638
+ }
1639
+ };
1640
+
1641
+ // src/server/runtimeBackend/runtimeBackendGateway.ts
1642
+ import { ErrorCode as ErrorCode2 } from "@hachej/boring-agent/shared";
1643
+ var GATEWAY_PREFIX = "/api/v1/plugins/";
1644
+ function rawPathFromRequest(request) {
1645
+ const url = request.raw.url ?? request.url;
1646
+ const queryIndex = url.indexOf("?");
1647
+ return queryIndex === -1 ? url : url.slice(0, queryIndex);
1648
+ }
1649
+ function rawGatewayTail(request, pluginId) {
1650
+ const rawPath = rawPathFromRequest(request);
1651
+ const prefix = `${GATEWAY_PREFIX}${pluginId}`;
1652
+ if (rawPath === prefix || rawPath === `${prefix}/`) return "/";
1653
+ if (!rawPath.startsWith(`${prefix}/`)) {
1654
+ throw new RuntimeBackendError(
1655
+ ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
1656
+ 404,
1657
+ "runtime backend route not found"
1658
+ );
1659
+ }
1660
+ return rawPath.slice(prefix.length);
1661
+ }
1662
+ function normalizeGatewayPath(rawTail) {
1663
+ let path;
1664
+ try {
1665
+ path = decodeURIComponent(rawTail);
1666
+ } catch {
1667
+ throw new RuntimeBackendError(
1668
+ ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
1669
+ 404,
1670
+ "runtime backend route path is not valid percent-encoding"
1671
+ );
1672
+ }
1673
+ if (path.length === 0) path = "/";
1674
+ const unsafeSegment = findUnsafeRuntimePathSegment(path);
1675
+ if (unsafeSegment) {
1676
+ throw new RuntimeBackendError(
1677
+ ErrorCode2.enum.RUNTIME_PLUGIN_ROUTE_NOT_FOUND,
1678
+ 404,
1679
+ `runtime backend route path must not contain ${describeUnsafeRuntimePathSegment(unsafeSegment)}`
1680
+ );
1681
+ }
1682
+ return path;
1683
+ }
1684
+ function firstString(value) {
1685
+ if (Array.isArray(value)) return value[0];
1686
+ return value;
1687
+ }
1688
+ function headersFromRequest(request) {
1689
+ const headers = new Headers();
1690
+ for (const [name, value] of Object.entries(request.headers)) {
1691
+ if (value === void 0) continue;
1692
+ if (Array.isArray(value)) {
1693
+ for (const item of value) headers.append(name, item);
1694
+ } else {
1695
+ headers.set(name, String(value));
1696
+ }
1697
+ }
1698
+ return headers;
1699
+ }
1700
+ function loggerFromRequest(request) {
1701
+ return {
1702
+ debug: (arg, message) => {
1703
+ if (message === void 0) request.log.debug(arg);
1704
+ else request.log.debug(arg, message);
1705
+ },
1706
+ info: (arg, message) => {
1707
+ if (message === void 0) request.log.info(arg);
1708
+ else request.log.info(arg, message);
1709
+ },
1710
+ warn: (arg, message) => {
1711
+ if (message === void 0) request.log.warn(arg);
1712
+ else request.log.warn(arg, message);
1713
+ },
1714
+ error: (arg, message) => {
1715
+ if (message === void 0) request.log.error(arg);
1716
+ else request.log.error(arg, message);
1717
+ }
1718
+ };
1719
+ }
1720
+ function sendDispatchResponse(reply, response) {
1721
+ reply.status(response.status);
1722
+ for (const [name, value] of Object.entries(response.headers)) reply.header(name, value);
1723
+ if (response.body === void 0 || response.body === null) return reply.send();
1724
+ return reply.send(response.body);
1725
+ }
1726
+ function sendError(reply, error) {
1727
+ if (error instanceof RuntimeBackendError) {
1728
+ return reply.status(error.statusCode).send({
1729
+ error: {
1730
+ code: error.code,
1731
+ message: error.message,
1732
+ ...error.details ? { details: error.details } : {}
1733
+ }
1734
+ });
1735
+ }
1736
+ return reply.status(500).send({
1737
+ error: {
1738
+ code: ErrorCode2.enum.INTERNAL_ERROR,
1739
+ message: error instanceof Error ? error.message : String(error)
1740
+ }
1741
+ });
1742
+ }
1743
+ async function runtimeBackendGateway(app, opts) {
1744
+ const handle = async (request, reply) => {
1745
+ const { pluginId } = request.params;
1746
+ if (!isValidBoringPluginId(pluginId)) {
1747
+ return sendError(reply, new RuntimeBackendError(
1748
+ ErrorCode2.enum.RUNTIME_PLUGIN_NOT_FOUND,
1749
+ 404,
1750
+ "runtime backend plugin not found"
1751
+ ));
1752
+ }
1753
+ let path;
1754
+ try {
1755
+ path = normalizeGatewayPath(rawGatewayTail(request, pluginId));
1756
+ } catch (error) {
1757
+ return sendError(reply, error);
1758
+ }
1759
+ const abort = new AbortController();
1760
+ const close = () => abort.abort();
1761
+ request.raw.on("close", close);
1762
+ try {
1763
+ const response = await opts.registry.dispatch({
1764
+ pluginId,
1765
+ method: request.method,
1766
+ path,
1767
+ query: new URLSearchParams(request.query),
1768
+ headers: headersFromRequest(request),
1769
+ signal: abort.signal,
1770
+ body: request.body,
1771
+ logger: loggerFromRequest(request),
1772
+ ...firstString(request.headers["x-boring-workspace-id"]) ?? opts.defaultWorkspaceId ? { workspaceId: firstString(request.headers["x-boring-workspace-id"]) ?? opts.defaultWorkspaceId } : {}
1773
+ });
1774
+ return sendDispatchResponse(reply, response);
1775
+ } catch (error) {
1776
+ return sendError(reply, error);
1777
+ } finally {
1778
+ request.raw.off("close", close);
1779
+ }
1780
+ };
1781
+ app.all("/api/v1/plugins/:pluginId", handle);
1782
+ app.all("/api/v1/plugins/:pluginId/*", handle);
1783
+ }
1784
+
1785
+ // src/server/agentPlugins/aggregatePluginPrompts.ts
1786
+ function aggregatePluginPrompts(manager) {
1787
+ const prompts = manager.list().map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
1788
+ if (prompts.length === 0) return void 0;
1789
+ return `# Loaded boring-ui plugin context
1790
+
1791
+ ${prompts.join("\n\n")}`;
1792
+ }
1793
+
1794
+ // src/app/server/pluginEntryResolver.ts
1795
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1796
+ import { join as join5, resolve as resolve5 } from "path";
1797
+ function readPluginPackageJson(dir) {
1798
+ const pkgPath = resolve5(dir, "package.json");
1799
+ if (!existsSync5(pkgPath)) return null;
1800
+ try {
1801
+ return JSON.parse(readFileSync4(pkgPath, "utf8"));
1802
+ } catch {
1803
+ return null;
1804
+ }
1805
+ }
1340
1806
  function resolveDirServerEntryPath(dir) {
1341
1807
  const rootDir = resolve5(dir);
1342
1808
  const pkg = readPluginPackageJson(rootDir);
@@ -2283,7 +2749,50 @@ async function provisionWorkspaceAgentServer(opts) {
2283
2749
  force: opts.force
2284
2750
  });
2285
2751
  }
2286
- function collectBoringPluginDirs(workspaceRoot, pluginCollection, additionalPluginDirs = []) {
2752
+ function uniquePluginSources(sources) {
2753
+ const byRoot = /* @__PURE__ */ new Map();
2754
+ for (const source of sources) {
2755
+ const existing = byRoot.get(source.rootDir);
2756
+ if (!existing || !existing.workspaceId && source.workspaceId) byRoot.set(source.rootDir, source);
2757
+ }
2758
+ return [...byRoot.values()];
2759
+ }
2760
+ var REMOTE_PI_PACKAGE_SOURCE_PREFIXES = ["npm:", "git:", "github:", "http:", "https:", "ssh:"];
2761
+ function piPackageSourceValue(entry) {
2762
+ if (typeof entry === "string") return entry;
2763
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
2764
+ const source = entry.source;
2765
+ return typeof source === "string" ? source : void 0;
2766
+ }
2767
+ return void 0;
2768
+ }
2769
+ function resolveLocalPiPackageSource(settingsDir, source) {
2770
+ const path = source.startsWith("file:") ? source.slice("file:".length) : source;
2771
+ if (!path) return void 0;
2772
+ if (REMOTE_PI_PACKAGE_SOURCE_PREFIXES.some((prefix) => path.startsWith(prefix))) return void 0;
2773
+ if (!isAbsolute5(path) && path !== "." && path !== "./" && !path.startsWith("./") && !path.startsWith("../")) return void 0;
2774
+ return resolve7(settingsDir, path);
2775
+ }
2776
+ function readPiSettingsBoringPluginSources(settingsPath, workspaceId) {
2777
+ let raw;
2778
+ try {
2779
+ raw = JSON.parse(readFileSync6(settingsPath, "utf8"));
2780
+ } catch {
2781
+ return [];
2782
+ }
2783
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
2784
+ const packages = raw.packages;
2785
+ if (!Array.isArray(packages)) return [];
2786
+ const settingsDir = dirname7(settingsPath);
2787
+ return uniquePluginSources(
2788
+ packages.map(piPackageSourceValue).map((source) => source ? resolveLocalPiPackageSource(settingsDir, source) : void 0).filter((rootDir) => Boolean(rootDir)).map((rootDir) => ({
2789
+ rootDir,
2790
+ kind: "external",
2791
+ ...workspaceId ? { workspaceId } : {}
2792
+ }))
2793
+ );
2794
+ }
2795
+ function collectBoringPluginSources(workspaceRoot, pluginCollection, additionalPluginDirs = []) {
2287
2796
  const extensionPaths = pluginCollection.agentOptions.pi?.extensionPaths ?? [];
2288
2797
  const pluginRoots = extensionPaths.flatMap((path) => {
2289
2798
  try {
@@ -2292,11 +2801,16 @@ function collectBoringPluginDirs(workspaceRoot, pluginCollection, additionalPlug
2292
2801
  return [];
2293
2802
  }
2294
2803
  });
2295
- return [.../* @__PURE__ */ new Set([
2296
- join7(workspaceRoot, ".pi", "extensions"),
2297
- ...pluginRoots,
2298
- ...additionalPluginDirs
2299
- ])];
2804
+ return uniquePluginSources([
2805
+ { rootDir: join7(workspaceRoot, ".pi", "extensions"), kind: "external", workspaceId: workspaceRoot },
2806
+ { rootDir: join7(workspaceRoot, ".pi", "npm"), kind: "external", workspaceId: workspaceRoot },
2807
+ { rootDir: join7(workspaceRoot, ".pi", "git"), kind: "external", workspaceId: workspaceRoot },
2808
+ { rootDir: join7(homedir(), ".pi", "agent", "extensions"), kind: "external" },
2809
+ ...readPiSettingsBoringPluginSources(join7(workspaceRoot, ".pi", "settings.json"), workspaceRoot),
2810
+ ...readPiSettingsBoringPluginSources(join7(homedir(), ".pi", "agent", "settings.json")),
2811
+ ...pluginRoots.map((rootDir) => ({ rootDir, kind: "internal" })),
2812
+ ...additionalPluginDirs.map((entry) => typeof entry === "string" ? { rootDir: entry, kind: "internal" } : entry)
2813
+ ]);
2300
2814
  }
2301
2815
  function mergeRuntimeProvisioningInputs(plugins) {
2302
2816
  const byId = /* @__PURE__ */ new Map();
@@ -2409,11 +2923,17 @@ async function createWorkspaceAgentServer(opts = {}) {
2409
2923
  ...pluginCollection.agentOptions.pi?.packages ?? []
2410
2924
  ];
2411
2925
  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);
2926
+ const boringPluginDirs = [];
2927
+ const refreshBoringPluginDirs = () => {
2928
+ const next = uniquePluginSources([
2929
+ ...defaultPluginPackagePaths.map((rootDir) => ({ rootDir, kind: "internal" })),
2930
+ ...collectBoringPluginSources(workspaceRoot, pluginCollection, opts.additionalBoringPluginDirs)
2931
+ ]);
2932
+ boringPluginDirs.splice(0, boringPluginDirs.length, ...next);
2933
+ return boringPluginDirs;
2934
+ };
2935
+ refreshBoringPluginDirs();
2936
+ const staticPluginPackagePiSnapshot = pluginHotReload ? emptyPackageJsonPiSnapshot() : readWorkspacePluginPackagePiSnapshot(refreshBoringPluginDirs());
2417
2937
  const staticPiSkillPaths = [
2418
2938
  ...baseStaticPiSkillPaths,
2419
2939
  ...staticPluginPackagePiSnapshot.additionalSkillPaths
@@ -2426,17 +2946,18 @@ async function createWorkspaceAgentServer(opts = {}) {
2426
2946
  ...baseStaticPiExtensionPaths,
2427
2947
  ...staticPluginPackagePiSnapshot.extensionPaths
2428
2948
  ];
2429
- const getHotReloadablePiResources = pluginHotReload ? () => readWorkspacePluginPackagePiSnapshot(boringPluginDirs) : void 0;
2949
+ const getHotReloadablePiResources = pluginHotReload ? () => readWorkspacePluginPackagePiSnapshot(refreshBoringPluginDirs()) : void 0;
2430
2950
  const boringAssetManager = new BoringPluginAssetManager({
2431
2951
  pluginDirs: boringPluginDirs,
2432
2952
  errorRoot: join7(workspaceRoot, ".pi", "extensions"),
2433
2953
  frontTargetResolver: opts.boringPluginFrontTargetResolver,
2434
2954
  includeLegacyFrontUrl: opts.boringPluginIncludeLegacyFrontUrl
2435
2955
  });
2956
+ const runtimeBackendRegistry = new RuntimeBackendRegistry();
2436
2957
  const buildRuntimeProvisioningInputs = () => {
2437
2958
  const inputs = mergeRuntimeProvisioningInputs([
2438
2959
  ...pluginCollection.runtimePlugins,
2439
- ...readWorkspacePluginPackageRuntimePlugins(boringPluginDirs)
2960
+ ...readWorkspacePluginPackageRuntimePlugins(refreshBoringPluginDirs())
2440
2961
  ]);
2441
2962
  if (resolvedMode === "direct") return omitPluginAuthoringProvisioning(inputs);
2442
2963
  return inputs;
@@ -2499,7 +3020,9 @@ async function createWorkspaceAgentServer(opts = {}) {
2499
3020
  let restart_warnings = [];
2500
3021
  let diagnostics = [];
2501
3022
  if (pluginHotReload) {
3023
+ refreshBoringPluginDirs();
2502
3024
  const scan = await boringAssetManager.load();
3025
+ const backendReload = await runtimeBackendRegistry.reloadFromLoadedPlugins(boringAssetManager.inspectLoaded());
2503
3026
  restart_warnings = collectRestartWarnings(scan.events);
2504
3027
  const scanDiagnostics = scan.errors.map((error) => ({
2505
3028
  source: `boring plugin asset scan (${error.id})`,
@@ -2507,7 +3030,7 @@ async function createWorkspaceAgentServer(opts = {}) {
2507
3030
  pluginId: error.id
2508
3031
  }));
2509
3032
  const rebuild = await rebuildPlugins();
2510
- diagnostics = [...scanDiagnostics, ...rebuild.diagnostics];
3033
+ diagnostics = [...scanDiagnostics, ...backendReload.diagnostics, ...rebuild.diagnostics];
2511
3034
  }
2512
3035
  await runRuntimeProvisioning();
2513
3036
  const callerResult = await opts.beforeReload?.();
@@ -2533,19 +3056,26 @@ async function createWorkspaceAgentServer(opts = {}) {
2533
3056
  },
2534
3057
  systemPromptDynamic: pluginHotReload ? () => aggregatePluginPrompts(boringAssetManager) : void 0
2535
3058
  });
3059
+ refreshBoringPluginDirs();
2536
3060
  await boringAssetManager.load();
3061
+ await runtimeBackendRegistry.reloadFromLoadedPlugins(boringAssetManager.inspectLoaded());
3062
+ if (typeof app.addHook === "function") {
3063
+ app.addHook("onClose", async () => {
3064
+ await runtimeBackendRegistry.close();
3065
+ });
3066
+ }
2537
3067
  await app.register(uiRoutes, { bridge, preserveStateKeys: pluginCollection.preservedUiStateKeys });
2538
3068
  await app.register(boringPluginRoutes, {
2539
- manager: boringAssetManager,
2540
- rebuildPlugins,
2541
- enableReloadRoute: pluginHotReload
3069
+ manager: boringAssetManager
2542
3070
  });
3071
+ await app.register(runtimeBackendGateway, { registry: runtimeBackendRegistry, defaultWorkspaceId: workspaceRoot });
2543
3072
  for (const { routes } of pluginCollection.routeContributions) {
2544
3073
  await app.register(routes);
2545
3074
  }
2546
3075
  ;
2547
3076
  app.__boringRebuildPlugins = rebuildPlugins;
2548
3077
  app.__boringAssetManager = boringAssetManager;
3078
+ app.__boringRuntimeBackendRegistry = runtimeBackendRegistry;
2549
3079
  return app;
2550
3080
  }
2551
3081
  export {