@hachej/boring-workspace 0.1.24 → 0.1.26

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.
@@ -54,7 +54,7 @@ function buildBoringSystemPrompt(opts) {
54
54
  if (opts.scaffoldCommand) {
55
55
  n += 1;
56
56
  steps.push(
57
- `**${n}. Scaffold.** Bash \`${opts.scaffoldCommand} <kebab-name> "$BORING_AGENT_WORKSPACE_ROOT"\` \u2014 writes canonical files under \`$BORING_AGENT_WORKSPACE_ROOT/.pi/extensions/<kebab-name>/\`. Read the generated \`package.json\` + \`front/index.tsx\`. Do NOT skip this or write from memory. Never \`cd\` to a parent repo or write plugins outside \`$BORING_AGENT_WORKSPACE_ROOT/.pi/extensions/\`.`
57
+ `**${n}. Check plugin-root support, then scaffold.** Bash \`boring-ui plugin-status --json\`; continue only if \`workspaceLocalPluginRoots\` is \`true\`. Then bash \`${opts.scaffoldCommand} <kebab-name> "$BORING_AGENT_WORKSPACE_ROOT"\`. Read generated \`package.json\` + \`front/index.tsx\`; do NOT write from memory.`
58
58
  );
59
59
  } else {
60
60
  n += 1;
@@ -80,7 +80,7 @@ function buildBoringSystemPrompt(opts) {
80
80
  steps.push(`**${n}. Ask the user to run \`/reload\`** to publish the change.`);
81
81
  const docsBlock = boringPiRoot ? [
82
82
  "## boring-ui plugin authoring documentation",
83
- "Read these only when the user asks to build, modify, or debug a workspace plugin. Use your `read` tool with the absolute path; the agent runtime guarantees these files exist on the host:",
83
+ "Read these only when the user asks to build, modify, or debug a workspace plugin. Use your `read` tool with these workspace-relative paths; the agent runtime guarantees they exist inside `$BORING_AGENT_WORKSPACE_ROOT`:",
84
84
  ...buildDocsRefs(boringPiRoot).map((r) => `- ${r.topic}: ${r.path}`),
85
85
  "Follow .md cross-references when present (e.g. SKILL.md may link to a reference doc \u2014 read both)."
86
86
  ].join("\n") : [
@@ -88,7 +88,7 @@ function buildBoringSystemPrompt(opts) {
88
88
  "The `boring-plugin-authoring` skill listed under `<available_skills>` is the authoritative reference (read its `<location>`). Additional reference docs (`panels.md`, `bridge.md`, `plugins.md`) are unavailable on this host \u2014 `@hachej/boring-pi` is not installed."
89
89
  ].join("\n");
90
90
  return [
91
- "You are operating inside boring-ui. Workspace root: `$BORING_AGENT_WORKSPACE_ROOT`; plugin files go under `$BORING_AGENT_WORKSPACE_ROOT/.pi/extensions/<name>/`.",
91
+ "You are operating inside boring-ui. Before `.pi/extensions/<name>/`, run `boring-ui plugin-status --json`; continue only when `workspaceLocalPluginRoots` is `true`. Default to `.pi/extensions/<name>/`. Global `~/.pi/agent/extensions/` only for explicit requests.",
92
92
  [
93
93
  "## Plugin authoring \u2014 required workflow",
94
94
  "",
@@ -98,7 +98,7 @@ function buildBoringSystemPrompt(opts) {
98
98
  "- API factories: `createPlugin`, `defineFrontPlugin`, `defineComponent` \u2014 use `definePlugin({id, panels, commands, ...})` from `@hachej/boring-workspace/plugin`.",
99
99
  "- 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).",
100
100
  "- 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.",
101
- '- File visualizers: for `.csv`/file-tree opens, import `WORKSPACE_OPEN_PATH_SURFACE_KIND` (and `PaneProps`) from `@hachej/boring-workspace/plugin`, read `request.target`, and fetch `/api/v1/files/raw?path=${encodeURIComponent(request.target)}`. Never import these from the root package, use `/workspace/read`, or string kind `"WORKSPACE_OPEN_PATH_SURFACE_KIND"`.',
101
+ '- 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"`.',
102
102
  "- Pi extension tools: `defineTool` and `export const tools` do NOT exist. Export `default function (pi) { pi.registerTool({ name, description, execute }) }`.",
103
103
  '- Server/Pi tool method: `handler` \u2014 use `execute`. Return shape: `{ content: [{ type: "text", text }] }` (NEVER a bare string).',
104
104
  "- 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.",
@@ -112,7 +112,7 @@ function buildBoringSystemPrompt(opts) {
112
112
  // src/server/agentPlugins/manager.ts
113
113
  import { createHash } from "crypto";
114
114
  import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
115
- import { dirname as dirname5, isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve3 } from "path";
115
+ import { dirname as dirname5, isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve4 } from "path";
116
116
 
117
117
  // src/shared/plugins/manifest.ts
118
118
  var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
@@ -551,632 +551,721 @@ function clearPluginSignatureCache(pluginRootDir) {
551
551
  if (existsSync3(path)) rmSync(path, { force: true });
552
552
  }
553
553
 
554
- // src/server/agentPlugins/manager.ts
555
- function preflightErrorId(pluginDir) {
556
- return `preflight-${createHash("sha256").update(pluginDir).digest("hex").slice(0, 12)}`;
554
+ // src/server/agentPlugins/piPackages.ts
555
+ import { resolve as resolve3 } from "path";
556
+ var REMOTE_PI_PACKAGE_PREFIXES2 = ["npm:", "git:", "github:", "http:", "https:", "ssh:"];
557
+ function isRemotePiPackageSource2(source) {
558
+ return REMOTE_PI_PACKAGE_PREFIXES2.some((prefix) => source.startsWith(prefix));
557
559
  }
558
- function directorySignature(root) {
559
- if (!root || !existsSync4(root)) return "missing";
560
- const hash = createHash("sha256");
561
- const visited = /* @__PURE__ */ new Set();
562
- let rootReal;
563
- try {
564
- rootReal = realpathSync2(root);
565
- } catch {
566
- return "missing";
560
+ function packageLocalPathFromSource(source) {
561
+ if (isRemotePiPackageSource2(source)) return null;
562
+ return source.startsWith("file:") ? source.slice("file:".length) : source;
563
+ }
564
+ function normalizeLocalPiPackageSource(pluginRoot, source) {
565
+ const localPath = packageLocalPathFromSource(source);
566
+ if (localPath == null) return source;
567
+ if (localPath === "." || localPath === "./") return resolve3(pluginRoot);
568
+ const normalized = localPath.startsWith("./") ? localPath.slice(2) : localPath;
569
+ if (!isSafePluginRelativePath(normalized)) {
570
+ throw new Error(`unsafe Pi package source: ${source}`);
567
571
  }
568
- visited.add(rootReal);
569
- let count = 0;
570
- const visit = (dir, depth) => {
571
- if (depth > 8 || count > 5e4) return;
572
- const entries = readdirSync2(dir, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".") && entry.name !== "node_modules").sort((a, b) => a.name.localeCompare(b.name));
573
- for (const entry of entries) {
574
- count++;
575
- const path = join4(dir, entry.name);
576
- const rel = relative2(root, path);
577
- const stat2 = lstatSync(path);
578
- if (stat2.isSymbolicLink()) {
579
- let target;
580
- try {
581
- target = realpathSync2(path);
582
- } catch {
583
- continue;
584
- }
585
- if (visited.has(target)) {
586
- hash.update(rel);
587
- hash.update("symlink-cycle");
588
- continue;
589
- }
590
- visited.add(target);
591
- const targetStat = statSync3(target);
592
- hash.update(rel);
593
- hash.update("symlink:");
594
- hash.update(target);
595
- if (targetStat.isDirectory()) visit(target, depth + 1);
596
- else if (targetStat.isFile()) {
597
- hash.update(String(targetStat.mtimeMs));
598
- hash.update(String(targetStat.size));
599
- }
600
- continue;
601
- }
602
- hash.update(rel);
603
- hash.update(String(stat2.mtimeMs));
604
- hash.update(String(stat2.size));
605
- if (stat2.isDirectory()) {
606
- visit(path, depth + 1);
607
- }
608
- }
572
+ return resolve3(pluginRoot, normalized);
573
+ }
574
+ function normalizeBoringPluginPiPackageSource(pluginRoot, source) {
575
+ if (typeof source === "string") return normalizeLocalPiPackageSource(pluginRoot, source);
576
+ return {
577
+ source: normalizeLocalPiPackageSource(pluginRoot, source.source),
578
+ ...source.extensions ? { extensions: source.extensions } : {},
579
+ ...source.skills ? { skills: source.skills } : {},
580
+ ...source.prompts ? { prompts: source.prompts } : {},
581
+ ...source.themes ? { themes: source.themes } : {}
609
582
  };
610
- visit(root, 0);
611
- return hash.digest("hex");
612
583
  }
613
- function pluginSignature(plugin) {
614
- 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(plugin.frontPath ? dirname5(plugin.frontPath) : void 0)).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");
584
+ function normalizeBoringPluginPiPackages(plugins) {
585
+ return plugins.flatMap(
586
+ (plugin) => (plugin.pi?.packages ?? []).map(
587
+ (source) => normalizeBoringPluginPiPackageSource(plugin.rootDir, source)
588
+ )
589
+ );
615
590
  }
616
- function computeRequiresRestart(previous, next) {
617
- if (!previous) return [];
618
- const prevHasServer = !!previous.serverPath;
619
- const nextHasServer = !!next.serverPath;
620
- if (!prevHasServer && !nextHasServer) return [];
621
- if (prevHasServer !== nextHasServer) return ["routes", "agentTools"];
622
- const nextSig = pluginFileSignature(next.serverPath);
623
- if (previous.serverSignature === nextSig) return [];
624
- return ["routes", "agentTools"];
591
+
592
+ // src/server/plugins/piPackages.ts
593
+ import {
594
+ compactPiPackages,
595
+ PI_PACKAGE_RESOURCE_FILTERS
596
+ } from "@hachej/boring-agent/server";
597
+
598
+ // src/server/plugins/defineServerPlugin.ts
599
+ function fail(pluginId, message) {
600
+ throw new Error(`server plugin "${pluginId}": ${message}`);
625
601
  }
626
- var BoringPluginAssetManager = class {
627
- pluginDirs;
628
- errorRoot;
629
- loaded = /* @__PURE__ */ new Map();
630
- revisions = /* @__PURE__ */ new Map();
631
- listeners = /* @__PURE__ */ new Set();
632
- loading = null;
633
- reloadQueued = false;
634
- constructor(options) {
635
- this.pluginDirs = options.pluginDirs;
636
- this.errorRoot = options.errorRoot ?? join4(process.cwd(), ".pi", "extensions");
602
+ function isUrl(value) {
603
+ return value instanceof URL;
604
+ }
605
+ function isPathLike(value) {
606
+ return typeof value === "string" && value.length > 0 || isUrl(value);
607
+ }
608
+ function validateAgentTool(pluginId, tool, index) {
609
+ if (!tool || typeof tool !== "object") {
610
+ fail(pluginId, `agentTools[${index}] must be an object`);
637
611
  }
638
- preflight() {
639
- return preflightBoringPlugins(this.pluginDirs);
612
+ const candidate = tool;
613
+ if (!candidate.name || typeof candidate.name !== "string") {
614
+ fail(pluginId, `agentTools[${index}].name must be a non-empty string`);
640
615
  }
641
- list() {
642
- return [...this.loaded.values()].map((plugin) => ({
643
- id: plugin.id,
644
- boring: plugin.boring,
645
- ...plugin.pi ? { pi: plugin.pi } : {},
646
- version: plugin.version,
647
- revision: plugin.revision,
648
- ...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {}
649
- }));
616
+ if (typeof candidate.description !== "string") {
617
+ fail(pluginId, `agentTools[${index}].description must be a string`);
650
618
  }
651
- getError(pluginId) {
652
- const path = this.errorPath(pluginId);
653
- if (!path || !existsSync4(path)) return null;
654
- return readFileSync3(path, "utf8");
619
+ if (!candidate.parameters || typeof candidate.parameters !== "object") {
620
+ fail(pluginId, `agentTools[${index}].parameters must be an object`);
655
621
  }
656
- subscribe(listener) {
657
- this.listeners.add(listener);
658
- return () => this.listeners.delete(listener);
622
+ if (typeof candidate.execute !== "function") {
623
+ fail(pluginId, `agentTools[${index}].execute must be a function`);
659
624
  }
660
- async load() {
661
- if (this.loading) {
662
- this.reloadQueued = true;
663
- return this.loading;
625
+ }
626
+ function validatePiPackages2(pluginId, piPackages) {
627
+ for (let i = 0; i < piPackages.length; i++) {
628
+ const source = piPackages[i];
629
+ if (typeof source === "string") {
630
+ if (source.length === 0) {
631
+ fail(pluginId, `piPackages[${i}] must be a non-empty string`);
632
+ }
633
+ continue;
634
+ }
635
+ if (!source || typeof source !== "object" || Array.isArray(source)) {
636
+ fail(pluginId, `piPackages[${i}] must be a string or package source object`);
637
+ }
638
+ const candidate = source;
639
+ if (typeof candidate.source !== "string" || candidate.source.length === 0) {
640
+ fail(pluginId, `piPackages[${i}].source must be a non-empty string`);
641
+ }
642
+ for (const key of PI_PACKAGE_RESOURCE_FILTERS) {
643
+ const value = candidate[key];
644
+ if (value === void 0) continue;
645
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.length === 0)) {
646
+ fail(pluginId, `piPackages[${i}].${key} must be a string array when provided`);
647
+ }
664
648
  }
665
- this.loading = this.drainLoads().finally(() => {
666
- this.loading = null;
667
- });
668
- return this.loading;
669
649
  }
670
- async drainLoads() {
671
- let result;
672
- do {
673
- this.reloadQueued = false;
674
- result = await this.doLoadOnce();
675
- } while (this.reloadQueued);
676
- return result;
650
+ }
651
+ function validateSkills(pluginId, skills) {
652
+ for (let i = 0; i < skills.length; i++) {
653
+ const skill = skills[i];
654
+ if (!skill || typeof skill !== "object") {
655
+ fail(pluginId, `skills[${i}] must be an object`);
656
+ }
657
+ if (!skill.name || typeof skill.name !== "string") {
658
+ fail(pluginId, `skills[${i}].name must be a non-empty string`);
659
+ }
660
+ if (!isPathLike(skill.source)) {
661
+ fail(pluginId, `skills[${i}].source must be a string or URL`);
662
+ }
677
663
  }
678
- async doLoadOnce() {
679
- const scan = scanBoringPlugins(this.pluginDirs);
680
- const nextPlugins = scan.plugins;
681
- const nextIds = new Set(nextPlugins.map((plugin) => plugin.id));
682
- const invalidPluginDirs = new Set(scan.preflight.errors.map((error) => resolve3(error.pluginDir)));
683
- const events = [];
684
- const errors = [];
685
- this.collectPreflightErrors(scan.preflight, events, errors);
686
- for (const id of [...this.loaded.keys()]) {
687
- if (nextIds.has(id)) continue;
688
- const previous = this.loaded.get(id);
689
- if (previous && invalidPluginDirs.has(resolve3(previous.rootDir))) continue;
690
- const revision = this.bumpRevision(id);
691
- this.loaded.delete(id);
692
- if (previous) {
693
- try {
694
- clearPluginSignatureCache(previous.rootDir);
695
- } catch {
696
- }
664
+ }
665
+ function validateProvisioning(pluginId, provisioning) {
666
+ if (!provisioning || typeof provisioning !== "object") {
667
+ fail(pluginId, "provisioning must be an object");
668
+ }
669
+ if (provisioning.templateDirs !== void 0) {
670
+ if (!Array.isArray(provisioning.templateDirs)) {
671
+ fail(pluginId, "provisioning.templateDirs must be an array when provided");
672
+ }
673
+ for (let i = 0; i < provisioning.templateDirs.length; i++) {
674
+ const contribution = provisioning.templateDirs[i];
675
+ if (!contribution || typeof contribution !== "object") {
676
+ fail(pluginId, `provisioning.templateDirs[${i}] must be an object`);
677
+ }
678
+ if (!contribution.id || typeof contribution.id !== "string") {
679
+ fail(pluginId, `provisioning.templateDirs[${i}].id must be a non-empty string`);
680
+ }
681
+ if (!isPathLike(contribution.path)) {
682
+ fail(pluginId, `provisioning.templateDirs[${i}].path must be a string or URL`);
683
+ }
684
+ if (contribution.target !== void 0 && typeof contribution.target !== "string") {
685
+ fail(pluginId, `provisioning.templateDirs[${i}].target must be a string when provided`);
697
686
  }
698
- const event = { type: "boring.plugin.unload", id, revision };
699
- events.push(event);
700
- this.emit(event);
701
687
  }
702
- for (const plugin of nextPlugins) {
703
- try {
704
- const signature = pluginSignature(plugin);
705
- const previous = this.loaded.get(plugin.id);
706
- if (previous?.signature === signature) continue;
707
- const revision = this.bumpRevision(plugin.id);
708
- const serverSignature = plugin.serverPath ? pluginFileSignature(plugin.serverPath) : null;
709
- const record = { ...plugin, revision, signature, serverSignature };
710
- this.loaded.set(plugin.id, record);
711
- this.clearError(plugin.id);
712
- try {
713
- writePluginSignatureCache(plugin.rootDir, { serverSignature });
714
- } catch {
688
+ }
689
+ if (provisioning.nodePackages !== void 0) {
690
+ if (!Array.isArray(provisioning.nodePackages)) {
691
+ fail(pluginId, "provisioning.nodePackages must be an array when provided");
692
+ }
693
+ for (let i = 0; i < provisioning.nodePackages.length; i++) {
694
+ const spec = provisioning.nodePackages[i];
695
+ if (!spec || typeof spec !== "object") {
696
+ fail(pluginId, `provisioning.nodePackages[${i}] must be an object`);
697
+ }
698
+ if (!spec.id || typeof spec.id !== "string") {
699
+ fail(pluginId, `provisioning.nodePackages[${i}].id must be a non-empty string`);
700
+ }
701
+ if (!spec.packageName || typeof spec.packageName !== "string") {
702
+ fail(pluginId, `provisioning.nodePackages[${i}].packageName must be a non-empty string`);
703
+ }
704
+ if (spec.packageRoot !== void 0 && !isPathLike(spec.packageRoot)) {
705
+ fail(pluginId, `provisioning.nodePackages[${i}].packageRoot must be a string or URL`);
706
+ }
707
+ }
708
+ }
709
+ if (provisioning.python !== void 0) {
710
+ if (!Array.isArray(provisioning.python)) {
711
+ fail(pluginId, "provisioning.python must be an array when provided");
712
+ }
713
+ for (let i = 0; i < provisioning.python.length; i++) {
714
+ const spec = provisioning.python[i];
715
+ if (!spec || typeof spec !== "object") {
716
+ fail(pluginId, `provisioning.python[${i}] must be an object`);
717
+ }
718
+ if (!spec.id || typeof spec.id !== "string") {
719
+ fail(pluginId, `provisioning.python[${i}].id must be a non-empty string`);
720
+ }
721
+ if (!isPathLike(spec.projectFile)) {
722
+ fail(pluginId, `provisioning.python[${i}].projectFile must be a string or URL`);
723
+ }
724
+ if (spec.extraLibs !== void 0 && (!Array.isArray(spec.extraLibs) || spec.extraLibs.some((item) => typeof item !== "string"))) {
725
+ fail(pluginId, `provisioning.python[${i}].extraLibs must be a string array when provided`);
726
+ }
727
+ if (spec.env !== void 0) {
728
+ if (!spec.env || typeof spec.env !== "object" || Array.isArray(spec.env)) {
729
+ fail(pluginId, `provisioning.python[${i}].env must be an object when provided`);
730
+ }
731
+ for (const [key, value] of Object.entries(spec.env)) {
732
+ if (!key || !isPathLike(value)) {
733
+ fail(pluginId, `provisioning.python[${i}].env values must be strings or URLs`);
734
+ }
715
735
  }
716
- const requiresRestart = computeRequiresRestart(previous, plugin);
717
- const event = {
718
- type: "boring.plugin.load",
719
- id: plugin.id,
720
- boring: plugin.boring,
721
- version: plugin.version,
722
- revision,
723
- ...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {},
724
- ...requiresRestart.length > 0 ? { requiresRestart } : {}
725
- };
726
- events.push(event);
727
- this.emit(event);
728
- } catch (error) {
729
- const revision = this.bumpRevision(plugin.id);
730
- const message = error instanceof Error ? error.stack ?? error.message : String(error);
731
- this.writeError(plugin.id, message);
732
- const event = { type: "boring.plugin.error", id: plugin.id, revision, message };
733
- errors.push({ id: plugin.id, revision, message });
734
- events.push(event);
735
- this.emit(event);
736
736
  }
737
737
  }
738
- return { loaded: this.list(), events, errors };
739
738
  }
740
- collectPreflightErrors(preflight, events, errors) {
741
- for (const error of preflight.errors) {
742
- const id = error.pluginId ?? preflightErrorId(error.pluginDir);
743
- const revision = this.bumpRevision(id);
744
- const message = `${error.code}: ${error.message}
745
-
746
- Plugin dir: ${error.pluginDir}`;
747
- const loadError = { id, revision, message };
748
- errors.push(loadError);
749
- this.writeError(id, message);
750
- const event = { type: "boring.plugin.error", id, revision, message };
751
- events.push(event);
752
- this.emit(event);
753
- }
739
+ }
740
+ function validateServerPlugin(plugin) {
741
+ if (!plugin.id || typeof plugin.id !== "string") {
742
+ fail(plugin.id ?? "<unknown>", "id must be a non-empty string");
754
743
  }
755
- bumpRevision(id) {
756
- const next = (this.revisions.get(id) ?? 0) + 1;
757
- this.revisions.set(id, next);
758
- return next;
744
+ if (plugin.label !== void 0 && typeof plugin.label !== "string") {
745
+ fail(plugin.id, "label must be a string when provided");
759
746
  }
760
- emit(event) {
761
- for (const listener of [...this.listeners]) {
762
- try {
763
- listener(event);
764
- } catch (error) {
765
- const message = error instanceof Error ? error.message : String(error);
766
- console.error(`[BoringPluginAssetManager] listener threw on ${event.type} for ${event.id}: ${message}`);
747
+ if (plugin.systemPrompt !== void 0 && typeof plugin.systemPrompt !== "string") {
748
+ fail(plugin.id, "systemPrompt must be a string when provided");
749
+ }
750
+ if (plugin.piPackages !== void 0) {
751
+ if (!Array.isArray(plugin.piPackages)) {
752
+ fail(plugin.id, "piPackages must be an array when provided");
753
+ }
754
+ validatePiPackages2(plugin.id, plugin.piPackages);
755
+ }
756
+ if (plugin.extensionPaths !== void 0) {
757
+ if (!Array.isArray(plugin.extensionPaths)) {
758
+ fail(plugin.id, "extensionPaths must be an array when provided");
759
+ }
760
+ plugin.extensionPaths.forEach((path, index) => {
761
+ if (typeof path !== "string" || path.length === 0) {
762
+ fail(plugin.id, `extensionPaths[${index}] must be a non-empty string`);
767
763
  }
764
+ });
765
+ }
766
+ if (plugin.skills !== void 0) {
767
+ if (!Array.isArray(plugin.skills)) {
768
+ fail(plugin.id, "skills must be an array when provided");
768
769
  }
770
+ validateSkills(plugin.id, plugin.skills);
769
771
  }
770
- errorPath(pluginId) {
771
- if (!isValidBoringPluginId(pluginId)) return null;
772
- const root = resolve3(this.errorRoot);
773
- const path = resolve3(root, pluginId, ".error");
774
- const rel = relative2(root, path);
775
- if (rel.startsWith("..") || isAbsolute2(rel)) return null;
776
- return path;
772
+ if (plugin.agentTools !== void 0) {
773
+ if (!Array.isArray(plugin.agentTools)) {
774
+ fail(plugin.id, "agentTools must be an array when provided");
775
+ }
776
+ plugin.agentTools.forEach((tool, index) => validateAgentTool(plugin.id, tool, index));
777
777
  }
778
- writeError(pluginId, message) {
779
- const path = this.errorPath(pluginId);
780
- if (!path) return;
781
- mkdirSync2(dirname5(path), { recursive: true });
782
- writeFileSync2(path, message, "utf8");
778
+ if (plugin.routes !== void 0 && typeof plugin.routes !== "function") {
779
+ fail(plugin.id, "routes must be a Fastify plugin function when provided");
783
780
  }
784
- clearError(pluginId) {
785
- const path = this.errorPath(pluginId);
786
- if (path && existsSync4(path)) rmSync2(path, { force: true });
781
+ if (plugin.preservedUiStateKeys !== void 0) {
782
+ if (!Array.isArray(plugin.preservedUiStateKeys) || plugin.preservedUiStateKeys.some((key) => typeof key !== "string" || key.length === 0)) {
783
+ fail(plugin.id, "preservedUiStateKeys must be a non-empty string array when provided");
784
+ }
787
785
  }
788
- };
789
-
790
- // src/server/agentPlugins/routes.ts
791
- function collectRestartWarnings(events) {
792
- const warnings = [];
793
- for (const event of events) {
794
- if (event.type !== "boring.plugin.load") continue;
795
- const surfaces = event.requiresRestart;
796
- if (!surfaces || surfaces.length === 0) continue;
797
- warnings.push({
798
- id: event.id,
799
- surfaces: [...surfaces],
800
- message: `${event.id} reloaded \u2014 front bundle is live, but server-side ${surfaces.join(" + ")} were wired at boot and still run the old code. Stop and restart the workspace process (Ctrl-C, then re-run your dev command) to pick up changes.`
801
- });
786
+ if (plugin.provisioning !== void 0) {
787
+ validateProvisioning(plugin.id, plugin.provisioning);
802
788
  }
803
- return warnings;
804
789
  }
805
- async function boringPluginRoutes(app, opts) {
806
- const { manager, rebuildPlugins, enableReloadRoute = true } = opts;
807
- if (enableReloadRoute) {
808
- app.post("/api/boring.reload", async (_request, reply) => {
809
- const scan = await manager.load();
810
- const rebuild = rebuildPlugins ? await rebuildPlugins() : { ok: true, diagnostics: [] };
811
- const restart_warnings = collectRestartWarnings(scan.events);
812
- const hasFailures = scan.errors.length > 0 || rebuild.diagnostics.length > 0;
813
- if (hasFailures) {
814
- return reply.status(422).send({
815
- ok: false,
816
- errors: scan.errors,
817
- diagnostics: rebuild.diagnostics,
818
- plugins: scan.loaded,
819
- // Even on failure, emit warnings for plugins that DID reload
820
- // — partial-failure tolerance means some loaded successfully.
821
- ...restart_warnings.length > 0 ? { restart_warnings } : {}
822
- });
823
- }
824
- return reply.send({
825
- ok: true,
826
- plugins: scan.loaded,
827
- ...restart_warnings.length > 0 ? { restart_warnings } : {}
828
- });
829
- });
830
- }
831
- const listPlugins = async () => manager.list();
832
- app.get("/api/v1/agent-plugins", listPlugins);
833
- const getPluginError = async (request, reply) => {
834
- const error = manager.getError(request.params.id);
835
- if (error == null) return reply.status(404).send({ error: "not_found" });
836
- return reply.type("text/plain").send(error);
837
- };
838
- app.get("/api/v1/agent-plugins/:id/error", getPluginError);
839
- app.get("/api/v1/agent-plugins/events", async (request, reply) => {
840
- reply.hijack();
841
- const res = reply.raw;
842
- res.statusCode = 200;
843
- res.setHeader("Content-Type", "text/event-stream");
844
- res.setHeader("Cache-Control", "no-cache, no-transform");
845
- res.setHeader("Connection", "keep-alive");
846
- res.setHeader("X-Accel-Buffering", "no");
847
- res.flushHeaders?.();
848
- const write = (event) => {
849
- try {
850
- res.write(`event: ${event.type}
851
- `);
852
- res.write(`data: ${JSON.stringify(event)}
853
790
 
854
- `);
855
- } catch {
856
- }
857
- };
858
- for (const plugin of manager.list()) {
859
- write({
860
- type: "boring.plugin.load",
861
- id: plugin.id,
862
- boring: plugin.boring,
863
- version: plugin.version,
864
- revision: plugin.revision,
865
- ...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {}
866
- });
791
+ // src/server/plugins/bootstrapServer.ts
792
+ function bootstrapServer(options) {
793
+ const excludedDefaults = new Set(options.excludeDefaults ?? []);
794
+ const finalPlugins = [
795
+ ...(options.defaults ?? []).filter((p) => !excludedDefaults.has(p.id)),
796
+ ...options.plugins ?? []
797
+ ];
798
+ const seenIds = /* @__PURE__ */ new Set();
799
+ for (const plugin of finalPlugins) {
800
+ validateServerPlugin(plugin);
801
+ if (seenIds.has(plugin.id)) {
802
+ throw new Error(`plugin "${plugin.id}" registered twice`);
867
803
  }
868
- const unsubscribe = manager.subscribe(write);
869
- const heartbeat = setInterval(() => {
870
- try {
871
- res.write(": heartbeat\n\n");
872
- } catch {
873
- }
874
- }, 25e3);
875
- request.raw.on("close", () => {
876
- clearInterval(heartbeat);
877
- unsubscribe();
878
- });
879
- });
880
- }
881
-
882
- // src/server/agentPlugins/aggregatePluginPrompts.ts
883
- function aggregatePluginPrompts(manager) {
884
- const prompts = manager.list().map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
885
- if (prompts.length === 0) return void 0;
886
- return `# Loaded boring-ui plugin context
887
-
888
- ${prompts.join("\n\n")}`;
804
+ seenIds.add(plugin.id);
805
+ }
806
+ const agentTools = [];
807
+ for (const plugin of finalPlugins) {
808
+ for (const tool of plugin.agentTools ?? []) {
809
+ agentTools.push(tool);
810
+ }
811
+ }
812
+ const systemPromptAppend = finalPlugins.filter((p) => p.systemPrompt && p.systemPrompt.trim()).map((p) => p.systemPrompt.trim()).join("\n\n");
813
+ const piPackages = compactPiPackages(finalPlugins.flatMap((plugin) => plugin.piPackages ?? []));
814
+ const extensionPaths = finalPlugins.flatMap((p) => p.extensionPaths ?? []);
815
+ const provisioningContributions = finalPlugins.filter((p) => p.provisioning).map((p) => ({ id: p.id, provisioning: p.provisioning }));
816
+ const runtimePlugins = finalPlugins.map((plugin) => ({
817
+ id: plugin.id,
818
+ ...plugin.skills ? { skills: plugin.skills } : {},
819
+ ...plugin.provisioning ? { provisioning: plugin.provisioning } : {}
820
+ }));
821
+ const routeContributions = finalPlugins.filter((p) => p.routes).map((p) => ({ id: p.id, routes: p.routes }));
822
+ const preservedUiStateKeys = [...new Set(finalPlugins.flatMap((p) => p.preservedUiStateKeys ?? []))];
823
+ return {
824
+ registered: finalPlugins.map((p) => p.id),
825
+ systemPromptAppend,
826
+ piPackages,
827
+ extensionPaths,
828
+ agentTools,
829
+ runtimePlugins,
830
+ provisioningContributions,
831
+ routeContributions,
832
+ preservedUiStateKeys
833
+ };
889
834
  }
890
835
 
891
- // src/server/agentPlugins/piPackages.ts
892
- import { resolve as resolve4 } from "path";
893
- var REMOTE_PI_PACKAGE_PREFIXES2 = ["npm:", "git:", "github:", "http:", "https:", "ssh:"];
894
- function isRemotePiPackageSource2(source) {
895
- return REMOTE_PI_PACKAGE_PREFIXES2.some((prefix) => source.startsWith(prefix));
836
+ // src/server/agentPlugins/manager.ts
837
+ function skillPathForPiLoader(path) {
838
+ return existsSync4(join4(path, "SKILL.md")) ? dirname5(path) : path;
896
839
  }
897
- function packageLocalPathFromSource(source) {
898
- if (isRemotePiPackageSource2(source)) return null;
899
- return source.startsWith("file:") ? source.slice("file:".length) : source;
840
+ function preflightErrorId(pluginDir) {
841
+ return `preflight-${createHash("sha256").update(pluginDir).digest("hex").slice(0, 12)}`;
900
842
  }
901
- function normalizeLocalPiPackageSource(pluginRoot, source) {
902
- const localPath = packageLocalPathFromSource(source);
903
- if (localPath == null) return source;
904
- if (localPath === "." || localPath === "./") return resolve4(pluginRoot);
905
- const normalized = localPath.startsWith("./") ? localPath.slice(2) : localPath;
906
- if (!isSafePluginRelativePath(normalized)) {
907
- throw new Error(`unsafe Pi package source: ${source}`);
843
+ function directorySignature(root) {
844
+ if (!root || !existsSync4(root)) return "missing";
845
+ const hash = createHash("sha256");
846
+ const visited = /* @__PURE__ */ new Set();
847
+ let rootReal;
848
+ try {
849
+ rootReal = realpathSync2(root);
850
+ } catch {
851
+ return "missing";
908
852
  }
909
- return resolve4(pluginRoot, normalized);
910
- }
911
- function normalizeBoringPluginPiPackageSource(pluginRoot, source) {
912
- if (typeof source === "string") return normalizeLocalPiPackageSource(pluginRoot, source);
913
- return {
914
- source: normalizeLocalPiPackageSource(pluginRoot, source.source),
915
- ...source.extensions ? { extensions: source.extensions } : {},
916
- ...source.skills ? { skills: source.skills } : {},
917
- ...source.prompts ? { prompts: source.prompts } : {},
918
- ...source.themes ? { themes: source.themes } : {}
853
+ visited.add(rootReal);
854
+ let count = 0;
855
+ const visit = (dir, depth) => {
856
+ if (depth > 8 || count > 5e4) return;
857
+ const entries = readdirSync2(dir, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".") && entry.name !== "node_modules").sort((a, b) => a.name.localeCompare(b.name));
858
+ for (const entry of entries) {
859
+ count++;
860
+ const path = join4(dir, entry.name);
861
+ const rel = relative2(root, path);
862
+ const stat2 = lstatSync(path);
863
+ if (stat2.isSymbolicLink()) {
864
+ let target;
865
+ try {
866
+ target = realpathSync2(path);
867
+ } catch {
868
+ continue;
869
+ }
870
+ if (visited.has(target)) {
871
+ hash.update(rel);
872
+ hash.update("symlink-cycle");
873
+ continue;
874
+ }
875
+ visited.add(target);
876
+ const targetStat = statSync3(target);
877
+ hash.update(rel);
878
+ hash.update("symlink:");
879
+ hash.update(target);
880
+ if (targetStat.isDirectory()) visit(target, depth + 1);
881
+ else if (targetStat.isFile()) {
882
+ hash.update(String(targetStat.mtimeMs));
883
+ hash.update(String(targetStat.size));
884
+ }
885
+ continue;
886
+ }
887
+ hash.update(rel);
888
+ hash.update(String(stat2.mtimeMs));
889
+ hash.update(String(stat2.size));
890
+ if (stat2.isDirectory()) {
891
+ visit(path, depth + 1);
892
+ }
893
+ }
919
894
  };
895
+ visit(root, 0);
896
+ return hash.digest("hex");
920
897
  }
921
- function normalizeBoringPluginPiPackages(plugins) {
922
- return plugins.flatMap(
923
- (plugin) => (plugin.pi?.packages ?? []).map(
924
- (source) => normalizeBoringPluginPiPackageSource(plugin.rootDir, source)
925
- )
926
- );
898
+ function normalizePluginSubpath(rootDir, path) {
899
+ return relative2(rootDir, path).replaceAll("\\", "/");
927
900
  }
928
-
929
- // src/app/server/pluginEntryResolver.ts
930
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
931
- import { join as join5, resolve as resolve5 } from "path";
932
- import { createRequire as createRequire2 } from "module";
933
- import { pathToFileURL } from "url";
934
-
935
- // src/server/plugins/piPackages.ts
936
- import {
937
- compactPiPackages,
938
- PI_PACKAGE_RESOURCE_FILTERS
939
- } from "@hachej/boring-agent/server";
940
-
941
- // src/server/plugins/defineServerPlugin.ts
942
- function fail(pluginId, message) {
943
- throw new Error(`server plugin "${pluginId}": ${message}`);
901
+ function frontSignatureRoot(plugin) {
902
+ if (!plugin.frontPath) return void 0;
903
+ const frontRoot = join4(plugin.rootDir, "front");
904
+ const rel = relative2(frontRoot, plugin.frontPath);
905
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel) ? frontRoot : dirname5(plugin.frontPath);
944
906
  }
945
- function isUrl(value) {
946
- return value instanceof URL;
907
+ function pluginSignature(plugin) {
908
+ 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");
947
909
  }
948
- function isPathLike(value) {
949
- return typeof value === "string" && value.length > 0 || isUrl(value);
910
+ function computeRequiresRestart(previous, next) {
911
+ if (!previous) return [];
912
+ const prevHasServer = !!previous.serverPath;
913
+ const nextHasServer = !!next.serverPath;
914
+ if (!prevHasServer && !nextHasServer) return [];
915
+ if (prevHasServer !== nextHasServer) return ["routes", "agentTools"];
916
+ const nextSig = pluginFileSignature(next.serverPath);
917
+ if (previous.serverSignature === nextSig) return [];
918
+ return ["routes", "agentTools"];
950
919
  }
951
- function validateAgentTool(pluginId, tool, index) {
952
- if (!tool || typeof tool !== "object") {
953
- fail(pluginId, `agentTools[${index}] must be an object`);
920
+ var BoringPluginAssetManager = class {
921
+ pluginDirs;
922
+ errorRoot;
923
+ frontTargetResolver;
924
+ includeLegacyFrontUrl;
925
+ loaded = /* @__PURE__ */ new Map();
926
+ revisions = /* @__PURE__ */ new Map();
927
+ listeners = /* @__PURE__ */ new Set();
928
+ lastErrors = /* @__PURE__ */ new Map();
929
+ loading = null;
930
+ reloadQueued = false;
931
+ constructor(options) {
932
+ this.pluginDirs = options.pluginDirs;
933
+ this.errorRoot = options.errorRoot ?? join4(process.cwd(), ".pi", "extensions");
934
+ this.frontTargetResolver = options.frontTargetResolver;
935
+ this.includeLegacyFrontUrl = options.includeLegacyFrontUrl ?? true;
954
936
  }
955
- const candidate = tool;
956
- if (!candidate.name || typeof candidate.name !== "string") {
957
- fail(pluginId, `agentTools[${index}].name must be a non-empty string`);
937
+ preflight() {
938
+ return preflightBoringPlugins(this.pluginDirs);
958
939
  }
959
- if (typeof candidate.description !== "string") {
960
- fail(pluginId, `agentTools[${index}].description must be a string`);
940
+ list() {
941
+ return [...this.loaded.values()].map((plugin) => this.toListEntry(plugin));
961
942
  }
962
- if (!candidate.parameters || typeof candidate.parameters !== "object") {
963
- fail(pluginId, `agentTools[${index}].parameters must be an object`);
943
+ getError(pluginId) {
944
+ const path = this.errorPath(pluginId);
945
+ if (!path || !existsSync4(path)) return null;
946
+ return readFileSync3(path, "utf8");
964
947
  }
965
- if (typeof candidate.execute !== "function") {
966
- fail(pluginId, `agentTools[${index}].execute must be a function`);
948
+ getErrors() {
949
+ return [...this.lastErrors.values()];
967
950
  }
968
- }
969
- function validatePiPackages2(pluginId, piPackages) {
970
- for (let i = 0; i < piPackages.length; i++) {
971
- const source = piPackages[i];
972
- if (typeof source === "string") {
973
- if (source.length === 0) {
974
- fail(pluginId, `piPackages[${i}] must be a non-empty string`);
975
- }
976
- continue;
977
- }
978
- if (!source || typeof source !== "object" || Array.isArray(source)) {
979
- fail(pluginId, `piPackages[${i}] must be a string or package source object`);
980
- }
981
- const candidate = source;
982
- if (typeof candidate.source !== "string" || candidate.source.length === 0) {
983
- fail(pluginId, `piPackages[${i}].source must be a non-empty string`);
984
- }
985
- for (const key of PI_PACKAGE_RESOURCE_FILTERS) {
986
- const value = candidate[key];
987
- if (value === void 0) continue;
988
- if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.length === 0)) {
989
- fail(pluginId, `piPackages[${i}].${key} must be a string array when provided`);
990
- }
991
- }
951
+ inspectLoaded() {
952
+ return [...this.loaded.values()].map((plugin) => ({
953
+ id: plugin.id,
954
+ version: plugin.version,
955
+ revision: plugin.revision,
956
+ rootDir: plugin.rootDir,
957
+ ...plugin.frontPath ? { frontPath: plugin.frontPath } : {},
958
+ ...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {}
959
+ }));
992
960
  }
993
- }
994
- function validateSkills(pluginId, skills) {
995
- for (let i = 0; i < skills.length; i++) {
996
- const skill = skills[i];
997
- if (!skill || typeof skill !== "object") {
998
- fail(pluginId, `skills[${i}] must be an object`);
999
- }
1000
- if (!skill.name || typeof skill.name !== "string") {
1001
- fail(pluginId, `skills[${i}].name must be a non-empty string`);
1002
- }
1003
- if (!isPathLike(skill.source)) {
1004
- fail(pluginId, `skills[${i}].source must be a string or URL`);
1005
- }
961
+ inspectLoadedPiSnapshot() {
962
+ const plugins = [...this.loaded.values()];
963
+ const prompts = plugins.map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
964
+ return {
965
+ additionalSkillPaths: [...new Set(plugins.flatMap((plugin) => plugin.skillPaths ?? []).map(skillPathForPiLoader))],
966
+ packages: compactPiPackages(normalizeBoringPluginPiPackages(plugins)),
967
+ extensionPaths: plugins.flatMap((plugin) => plugin.extensionPaths ?? []),
968
+ ...prompts.length > 0 ? { systemPromptAppend: `# Loaded boring-ui plugin context
969
+
970
+ ${prompts.join("\n\n")}` } : {}
971
+ };
1006
972
  }
1007
- }
1008
- function validateProvisioning(pluginId, provisioning) {
1009
- if (!provisioning || typeof provisioning !== "object") {
1010
- fail(pluginId, "provisioning must be an object");
973
+ subscribe(listener) {
974
+ this.listeners.add(listener);
975
+ return () => this.listeners.delete(listener);
1011
976
  }
1012
- if (provisioning.templateDirs !== void 0) {
1013
- if (!Array.isArray(provisioning.templateDirs)) {
1014
- fail(pluginId, "provisioning.templateDirs must be an array when provided");
1015
- }
1016
- for (let i = 0; i < provisioning.templateDirs.length; i++) {
1017
- const contribution = provisioning.templateDirs[i];
1018
- if (!contribution || typeof contribution !== "object") {
1019
- fail(pluginId, `provisioning.templateDirs[${i}] must be an object`);
1020
- }
1021
- if (!contribution.id || typeof contribution.id !== "string") {
1022
- fail(pluginId, `provisioning.templateDirs[${i}].id must be a non-empty string`);
1023
- }
1024
- if (!isPathLike(contribution.path)) {
1025
- fail(pluginId, `provisioning.templateDirs[${i}].path must be a string or URL`);
1026
- }
1027
- if (contribution.target !== void 0 && typeof contribution.target !== "string") {
1028
- fail(pluginId, `provisioning.templateDirs[${i}].target must be a string when provided`);
1029
- }
977
+ async load() {
978
+ if (this.loading) {
979
+ this.reloadQueued = true;
980
+ return this.loading;
1030
981
  }
982
+ this.loading = this.drainLoads().finally(() => {
983
+ this.loading = null;
984
+ });
985
+ return this.loading;
1031
986
  }
1032
- if (provisioning.nodePackages !== void 0) {
1033
- if (!Array.isArray(provisioning.nodePackages)) {
1034
- fail(pluginId, "provisioning.nodePackages must be an array when provided");
1035
- }
1036
- for (let i = 0; i < provisioning.nodePackages.length; i++) {
1037
- const spec = provisioning.nodePackages[i];
1038
- if (!spec || typeof spec !== "object") {
1039
- fail(pluginId, `provisioning.nodePackages[${i}] must be an object`);
1040
- }
1041
- if (!spec.id || typeof spec.id !== "string") {
1042
- fail(pluginId, `provisioning.nodePackages[${i}].id must be a non-empty string`);
1043
- }
1044
- if (!spec.packageName || typeof spec.packageName !== "string") {
1045
- fail(pluginId, `provisioning.nodePackages[${i}].packageName must be a non-empty string`);
1046
- }
1047
- if (spec.packageRoot !== void 0 && !isPathLike(spec.packageRoot)) {
1048
- fail(pluginId, `provisioning.nodePackages[${i}].packageRoot must be a string or URL`);
1049
- }
1050
- }
987
+ async drainLoads() {
988
+ let result;
989
+ do {
990
+ this.reloadQueued = false;
991
+ result = await this.doLoadOnce();
992
+ } while (this.reloadQueued);
993
+ return result;
1051
994
  }
1052
- if (provisioning.python !== void 0) {
1053
- if (!Array.isArray(provisioning.python)) {
1054
- fail(pluginId, "provisioning.python must be an array when provided");
1055
- }
1056
- for (let i = 0; i < provisioning.python.length; i++) {
1057
- const spec = provisioning.python[i];
1058
- if (!spec || typeof spec !== "object") {
1059
- fail(pluginId, `provisioning.python[${i}] must be an object`);
1060
- }
1061
- if (!spec.id || typeof spec.id !== "string") {
1062
- fail(pluginId, `provisioning.python[${i}].id must be a non-empty string`);
1063
- }
1064
- if (!isPathLike(spec.projectFile)) {
1065
- fail(pluginId, `provisioning.python[${i}].projectFile must be a string or URL`);
1066
- }
1067
- if (spec.extraLibs !== void 0 && (!Array.isArray(spec.extraLibs) || spec.extraLibs.some((item) => typeof item !== "string"))) {
1068
- fail(pluginId, `provisioning.python[${i}].extraLibs must be a string array when provided`);
1069
- }
1070
- if (spec.env !== void 0) {
1071
- if (!spec.env || typeof spec.env !== "object" || Array.isArray(spec.env)) {
1072
- fail(pluginId, `provisioning.python[${i}].env must be an object when provided`);
995
+ async doLoadOnce() {
996
+ this.lastErrors.clear();
997
+ const scan = scanBoringPlugins(this.pluginDirs);
998
+ const nextPlugins = scan.plugins;
999
+ const nextIds = new Set(nextPlugins.map((plugin) => plugin.id));
1000
+ const invalidPluginDirs = new Set(scan.preflight.errors.map((error) => resolve4(error.pluginDir)));
1001
+ const events = [];
1002
+ const errors = [];
1003
+ this.collectPreflightErrors(scan.preflight, events, errors);
1004
+ for (const id of [...this.loaded.keys()]) {
1005
+ if (nextIds.has(id)) continue;
1006
+ const previous = this.loaded.get(id);
1007
+ if (previous && invalidPluginDirs.has(resolve4(previous.rootDir))) continue;
1008
+ const revision = this.bumpRevision(id);
1009
+ this.loaded.delete(id);
1010
+ this.lastErrors.delete(id);
1011
+ if (previous) {
1012
+ try {
1013
+ clearPluginSignatureCache(previous.rootDir);
1014
+ } catch {
1073
1015
  }
1074
- for (const [key, value] of Object.entries(spec.env)) {
1075
- if (!key || !isPathLike(value)) {
1076
- fail(pluginId, `provisioning.python[${i}].env values must be strings or URLs`);
1077
- }
1016
+ }
1017
+ const event = { type: "boring.plugin.unload", id, revision };
1018
+ events.push(event);
1019
+ this.emit(event);
1020
+ }
1021
+ for (const plugin of nextPlugins) {
1022
+ try {
1023
+ const signature = pluginSignature(plugin);
1024
+ const previous = this.loaded.get(plugin.id);
1025
+ if (previous?.signature === signature) continue;
1026
+ const revision = this.bumpRevision(plugin.id);
1027
+ const frontTarget = this.resolveFrontTarget(plugin, revision);
1028
+ const serverSignature = plugin.serverPath ? pluginFileSignature(plugin.serverPath) : null;
1029
+ const record = {
1030
+ ...plugin,
1031
+ revision,
1032
+ signature,
1033
+ ...frontTarget ? { frontTarget } : {},
1034
+ serverSignature
1035
+ };
1036
+ this.loaded.set(plugin.id, record);
1037
+ this.lastErrors.delete(plugin.id);
1038
+ this.clearError(plugin.id);
1039
+ try {
1040
+ writePluginSignatureCache(plugin.rootDir, { serverSignature });
1041
+ } catch {
1078
1042
  }
1043
+ const requiresRestart = computeRequiresRestart(previous, plugin);
1044
+ const event = {
1045
+ type: "boring.plugin.load",
1046
+ id: plugin.id,
1047
+ boring: plugin.boring,
1048
+ version: plugin.version,
1049
+ revision,
1050
+ ...this.frontUrlPayload(plugin.frontUrl),
1051
+ ...frontTarget ? { frontTarget } : {},
1052
+ ...requiresRestart.length > 0 ? { requiresRestart } : {}
1053
+ };
1054
+ events.push(event);
1055
+ this.emit(event);
1056
+ } catch (error) {
1057
+ const revision = this.bumpRevision(plugin.id);
1058
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
1059
+ this.writeError(plugin.id, message);
1060
+ const event = { type: "boring.plugin.error", id: plugin.id, revision, message };
1061
+ const loadError = { id: plugin.id, revision, message };
1062
+ this.lastErrors.set(plugin.id, loadError);
1063
+ errors.push(loadError);
1064
+ events.push(event);
1065
+ this.emit(event);
1079
1066
  }
1080
1067
  }
1068
+ return { loaded: this.list(), events, errors };
1081
1069
  }
1082
- }
1083
- function validateServerPlugin(plugin) {
1084
- if (!plugin.id || typeof plugin.id !== "string") {
1085
- fail(plugin.id ?? "<unknown>", "id must be a non-empty string");
1070
+ collectPreflightErrors(preflight, events, errors) {
1071
+ for (const error of preflight.errors) {
1072
+ const id = error.pluginId ?? preflightErrorId(error.pluginDir);
1073
+ const revision = this.bumpRevision(id);
1074
+ const message = `${error.code}: ${error.message}
1075
+
1076
+ Plugin dir: ${error.pluginDir}`;
1077
+ const loadError = { id, revision, message };
1078
+ this.lastErrors.set(id, loadError);
1079
+ errors.push(loadError);
1080
+ this.writeError(id, message);
1081
+ const event = { type: "boring.plugin.error", id, revision, message };
1082
+ events.push(event);
1083
+ this.emit(event);
1084
+ }
1086
1085
  }
1087
- if (plugin.label !== void 0 && typeof plugin.label !== "string") {
1088
- fail(plugin.id, "label must be a string when provided");
1086
+ bumpRevision(id) {
1087
+ const next = (this.revisions.get(id) ?? 0) + 1;
1088
+ this.revisions.set(id, next);
1089
+ return next;
1089
1090
  }
1090
- if (plugin.systemPrompt !== void 0 && typeof plugin.systemPrompt !== "string") {
1091
- fail(plugin.id, "systemPrompt must be a string when provided");
1091
+ toListEntry(plugin) {
1092
+ return {
1093
+ id: plugin.id,
1094
+ boring: plugin.boring,
1095
+ ...plugin.pi ? { pi: plugin.pi } : {},
1096
+ version: plugin.version,
1097
+ revision: plugin.revision,
1098
+ ...this.frontUrlPayload(plugin.frontUrl),
1099
+ ...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {}
1100
+ };
1092
1101
  }
1093
- if (plugin.piPackages !== void 0) {
1094
- if (!Array.isArray(plugin.piPackages)) {
1095
- fail(plugin.id, "piPackages must be an array when provided");
1096
- }
1097
- validatePiPackages2(plugin.id, plugin.piPackages);
1102
+ frontUrlPayload(frontUrl) {
1103
+ if (!this.includeLegacyFrontUrl || !frontUrl) return {};
1104
+ return { frontUrl };
1098
1105
  }
1099
- if (plugin.extensionPaths !== void 0) {
1100
- if (!Array.isArray(plugin.extensionPaths)) {
1101
- fail(plugin.id, "extensionPaths must be an array when provided");
1102
- }
1103
- plugin.extensionPaths.forEach((path, index) => {
1104
- if (typeof path !== "string" || path.length === 0) {
1105
- fail(plugin.id, `extensionPaths[${index}] must be a non-empty string`);
1106
- }
1106
+ resolveFrontTarget(plugin, revision) {
1107
+ if (!plugin.frontPath || !this.frontTargetResolver) return void 0;
1108
+ const frontEntrySubpath = typeof plugin.boring.front === "string" ? plugin.boring.front.replace(/^\.\//, "") : normalizePluginSubpath(plugin.rootDir, plugin.frontPath);
1109
+ const frontTarget = this.frontTargetResolver(plugin, {
1110
+ revision,
1111
+ frontEntrySubpath
1107
1112
  });
1113
+ if (!frontTarget) return void 0;
1114
+ return { ...frontTarget, revision };
1108
1115
  }
1109
- if (plugin.skills !== void 0) {
1110
- if (!Array.isArray(plugin.skills)) {
1111
- fail(plugin.id, "skills must be an array when provided");
1112
- }
1113
- validateSkills(plugin.id, plugin.skills);
1114
- }
1115
- if (plugin.agentTools !== void 0) {
1116
- if (!Array.isArray(plugin.agentTools)) {
1117
- fail(plugin.id, "agentTools must be an array when provided");
1116
+ emit(event) {
1117
+ for (const listener of [...this.listeners]) {
1118
+ try {
1119
+ listener(event);
1120
+ } catch (error) {
1121
+ const message = error instanceof Error ? error.message : String(error);
1122
+ console.error(`[BoringPluginAssetManager] listener threw on ${event.type} for ${event.id}: ${message}`);
1123
+ }
1118
1124
  }
1119
- plugin.agentTools.forEach((tool, index) => validateAgentTool(plugin.id, tool, index));
1120
1125
  }
1121
- if (plugin.routes !== void 0 && typeof plugin.routes !== "function") {
1122
- fail(plugin.id, "routes must be a Fastify plugin function when provided");
1126
+ errorPath(pluginId) {
1127
+ if (!isValidBoringPluginId(pluginId)) return null;
1128
+ const root = resolve4(this.errorRoot);
1129
+ const path = resolve4(root, pluginId, ".error");
1130
+ const rel = relative2(root, path);
1131
+ if (rel.startsWith("..") || isAbsolute2(rel)) return null;
1132
+ return path;
1123
1133
  }
1124
- if (plugin.preservedUiStateKeys !== void 0) {
1125
- if (!Array.isArray(plugin.preservedUiStateKeys) || plugin.preservedUiStateKeys.some((key) => typeof key !== "string" || key.length === 0)) {
1126
- fail(plugin.id, "preservedUiStateKeys must be a non-empty string array when provided");
1127
- }
1134
+ writeError(pluginId, message) {
1135
+ const path = this.errorPath(pluginId);
1136
+ if (!path) return;
1137
+ mkdirSync2(dirname5(path), { recursive: true });
1138
+ writeFileSync2(path, message, "utf8");
1128
1139
  }
1129
- if (plugin.provisioning !== void 0) {
1130
- validateProvisioning(plugin.id, plugin.provisioning);
1140
+ clearError(pluginId) {
1141
+ const path = this.errorPath(pluginId);
1142
+ if (path && existsSync4(path)) rmSync2(path, { force: true });
1131
1143
  }
1132
- }
1144
+ };
1133
1145
 
1134
- // src/server/plugins/bootstrapServer.ts
1135
- function bootstrapServer(options) {
1136
- const excludedDefaults = new Set(options.excludeDefaults ?? []);
1137
- const finalPlugins = [
1138
- ...(options.defaults ?? []).filter((p) => !excludedDefaults.has(p.id)),
1139
- ...options.plugins ?? []
1140
- ];
1141
- const seenIds = /* @__PURE__ */ new Set();
1142
- for (const plugin of finalPlugins) {
1143
- validateServerPlugin(plugin);
1144
- if (seenIds.has(plugin.id)) {
1145
- throw new Error(`plugin "${plugin.id}" registered twice`);
1146
- }
1147
- seenIds.add(plugin.id);
1146
+ // src/server/agentPlugins/routes.ts
1147
+ function collectRestartWarnings(events) {
1148
+ const warnings = [];
1149
+ for (const event of events) {
1150
+ if (event.type !== "boring.plugin.load") continue;
1151
+ const surfaces = event.requiresRestart;
1152
+ if (!surfaces || surfaces.length === 0) continue;
1153
+ warnings.push({
1154
+ id: event.id,
1155
+ surfaces: [...surfaces],
1156
+ message: `${event.id} reloaded \u2014 front bundle is live, but server-side ${surfaces.join(" + ")} were wired at boot and still run the old code. Stop and restart the workspace process (Ctrl-C, then re-run your dev command) to pick up changes.`
1157
+ });
1148
1158
  }
1149
- const agentTools = [];
1150
- for (const plugin of finalPlugins) {
1151
- for (const tool of plugin.agentTools ?? []) {
1152
- agentTools.push(tool);
1153
- }
1159
+ return warnings;
1160
+ }
1161
+ async function boringPluginRoutes(app, opts) {
1162
+ const { manager, rebuildPlugins, enableReloadRoute = true } = opts;
1163
+ if (enableReloadRoute) {
1164
+ app.post("/api/boring.reload", async (_request, reply) => {
1165
+ const scan = await manager.load();
1166
+ const rebuild = rebuildPlugins ? await rebuildPlugins() : { ok: true, diagnostics: [] };
1167
+ const restart_warnings = collectRestartWarnings(scan.events);
1168
+ const hasFailures = scan.errors.length > 0 || rebuild.diagnostics.length > 0;
1169
+ if (hasFailures) {
1170
+ return reply.status(422).send({
1171
+ ok: false,
1172
+ errors: scan.errors,
1173
+ diagnostics: rebuild.diagnostics,
1174
+ plugins: scan.loaded,
1175
+ // Even on failure, emit warnings for plugins that DID reload
1176
+ // — partial-failure tolerance means some loaded successfully.
1177
+ ...restart_warnings.length > 0 ? { restart_warnings } : {}
1178
+ });
1179
+ }
1180
+ return reply.send({
1181
+ ok: true,
1182
+ plugins: scan.loaded,
1183
+ ...restart_warnings.length > 0 ? { restart_warnings } : {}
1184
+ });
1185
+ });
1154
1186
  }
1155
- const systemPromptAppend = finalPlugins.filter((p) => p.systemPrompt && p.systemPrompt.trim()).map((p) => p.systemPrompt.trim()).join("\n\n");
1156
- const piPackages = compactPiPackages(finalPlugins.flatMap((plugin) => plugin.piPackages ?? []));
1157
- const extensionPaths = finalPlugins.flatMap((p) => p.extensionPaths ?? []);
1158
- const provisioningContributions = finalPlugins.filter((p) => p.provisioning).map((p) => ({ id: p.id, provisioning: p.provisioning }));
1159
- const runtimePlugins = finalPlugins.map((plugin) => ({
1160
- id: plugin.id,
1161
- ...plugin.skills ? { skills: plugin.skills } : {},
1162
- ...plugin.provisioning ? { provisioning: plugin.provisioning } : {}
1163
- }));
1164
- const routeContributions = finalPlugins.filter((p) => p.routes).map((p) => ({ id: p.id, routes: p.routes }));
1165
- const preservedUiStateKeys = [...new Set(finalPlugins.flatMap((p) => p.preservedUiStateKeys ?? []))];
1166
- return {
1167
- registered: finalPlugins.map((p) => p.id),
1168
- systemPromptAppend,
1169
- piPackages,
1170
- extensionPaths,
1171
- agentTools,
1172
- runtimePlugins,
1173
- provisioningContributions,
1174
- routeContributions,
1175
- preservedUiStateKeys
1187
+ const listPlugins = async () => manager.list();
1188
+ app.get("/api/v1/agent-plugins", listPlugins);
1189
+ const getPluginError = async (request, reply) => {
1190
+ const error = manager.getError(request.params.id);
1191
+ if (error == null) return reply.status(404).send({ error: "not_found" });
1192
+ return reply.type("text/plain").send(error);
1176
1193
  };
1194
+ app.get("/api/v1/agent-plugins/:id/error", getPluginError);
1195
+ app.get("/api/v1/agent-plugins/events", async (request, reply) => {
1196
+ reply.hijack();
1197
+ const res = reply.raw;
1198
+ res.statusCode = 200;
1199
+ res.setHeader("Content-Type", "text/event-stream");
1200
+ res.setHeader("Cache-Control", "no-cache, no-transform");
1201
+ res.setHeader("Connection", "keep-alive");
1202
+ res.setHeader("X-Accel-Buffering", "no");
1203
+ res.flushHeaders?.();
1204
+ const write = (eventName, payload) => {
1205
+ try {
1206
+ res.write(`event: ${eventName}
1207
+ `);
1208
+ res.write(`data: ${JSON.stringify(payload)}
1209
+
1210
+ `);
1211
+ } catch {
1212
+ }
1213
+ };
1214
+ const liveQueue = [];
1215
+ let replaying = true;
1216
+ const unsubscribe = manager.subscribe((event) => {
1217
+ const payload = { ...event, replay: false };
1218
+ if (replaying) {
1219
+ liveQueue.push({ eventName: event.type, payload });
1220
+ return;
1221
+ }
1222
+ write(event.type, payload);
1223
+ });
1224
+ for (const plugin of manager.list()) {
1225
+ write("boring.plugin.load", {
1226
+ type: "boring.plugin.load",
1227
+ id: plugin.id,
1228
+ boring: plugin.boring,
1229
+ version: plugin.version,
1230
+ revision: plugin.revision,
1231
+ ...plugin.frontUrl ? { frontUrl: plugin.frontUrl } : {},
1232
+ ...plugin.frontTarget ? { frontTarget: plugin.frontTarget } : {},
1233
+ replay: true
1234
+ });
1235
+ }
1236
+ write("boring.plugin.replay-complete", {
1237
+ type: "boring.plugin.replay-complete",
1238
+ replay: true
1239
+ });
1240
+ replaying = false;
1241
+ for (const event of liveQueue) write(event.eventName, event.payload);
1242
+ const heartbeat = setInterval(() => {
1243
+ try {
1244
+ res.write(": heartbeat\n\n");
1245
+ } catch {
1246
+ }
1247
+ }, 25e3);
1248
+ request.raw.on("close", () => {
1249
+ clearInterval(heartbeat);
1250
+ unsubscribe();
1251
+ });
1252
+ });
1253
+ }
1254
+
1255
+ // src/server/agentPlugins/aggregatePluginPrompts.ts
1256
+ function aggregatePluginPrompts(manager) {
1257
+ const prompts = manager.list().map((plugin) => plugin.pi?.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
1258
+ if (prompts.length === 0) return void 0;
1259
+ return `# Loaded boring-ui plugin context
1260
+
1261
+ ${prompts.join("\n\n")}`;
1177
1262
  }
1178
1263
 
1179
1264
  // src/app/server/pluginEntryResolver.ts
1265
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1266
+ import { join as join5, resolve as resolve5 } from "path";
1267
+ import { createRequire as createRequire2 } from "module";
1268
+ import { pathToFileURL } from "url";
1180
1269
  function readPluginPackageJson(dir) {
1181
1270
  const pkgPath = resolve5(dir, "package.json");
1182
1271
  if (!existsSync5(pkgPath)) return null;
@@ -1832,9 +1921,10 @@ data: ${JSON.stringify({ v: UI_BRIDGE_PROTOCOL_VERSION })}
1832
1921
  var __dirname = dirname7(fileURLToPath(import.meta.url));
1833
1922
  var require4 = createRequire4(import.meta.url);
1834
1923
  function boringPiRootVisibleToAgentTools(workspaceRoot, resolvedMode, provisioned) {
1924
+ void workspaceRoot;
1925
+ void resolvedMode;
1835
1926
  if (!provisioned) return void 0;
1836
- if (resolvedMode === "local") return "/workspace/node_modules/@hachej/boring-pi";
1837
- return join7(workspaceRoot, "node_modules", "@hachej", "boring-pi");
1927
+ return "/workspace/.boring-agent/node/node_modules/@hachej/boring-pi";
1838
1928
  }
1839
1929
  function resolveWorkspacePackageRoot() {
1840
1930
  const candidates = [
@@ -1949,7 +2039,7 @@ function createBoringUiCliPackageProvisioningContribution() {
1949
2039
  "boring-ui-cli-package",
1950
2040
  "boring-ui-cli",
1951
2041
  "@hachej/boring-ui-cli",
1952
- readPackageVersion(packageRoot) ?? readPackageVersion(resolveWorkspacePackageRoot()),
2042
+ packageRoot === join7(resolveWorkspacePackageRoot(), "..", "cli") ? void 0 : readPackageVersion(packageRoot) ?? readPackageVersion(resolveWorkspacePackageRoot()),
1953
2043
  ["boring-ui"]
1954
2044
  );
1955
2045
  }
@@ -2026,7 +2116,7 @@ async function provisionWorkspaceAgentServer(opts) {
2026
2116
  force: opts.force
2027
2117
  });
2028
2118
  }
2029
- function collectBoringPluginDirs(workspaceRoot, pluginCollection) {
2119
+ function collectBoringPluginDirs(workspaceRoot, pluginCollection, additionalPluginDirs = []) {
2030
2120
  const extensionPaths = pluginCollection.agentOptions.pi?.extensionPaths ?? [];
2031
2121
  const pluginRoots = extensionPaths.flatMap((path) => {
2032
2122
  try {
@@ -2035,10 +2125,11 @@ function collectBoringPluginDirs(workspaceRoot, pluginCollection) {
2035
2125
  return [];
2036
2126
  }
2037
2127
  });
2038
- return [
2128
+ return [.../* @__PURE__ */ new Set([
2039
2129
  join7(workspaceRoot, ".pi", "extensions"),
2040
- ...pluginRoots
2041
- ];
2130
+ ...pluginRoots,
2131
+ ...additionalPluginDirs
2132
+ ])];
2042
2133
  }
2043
2134
  function mergeRuntimeProvisioningInputs(plugins) {
2044
2135
  const byId = /* @__PURE__ */ new Map();
@@ -2064,7 +2155,7 @@ function skillNameFromResolvedPath(path) {
2064
2155
  if (leaf.toLowerCase() !== "skill.md") return leaf;
2065
2156
  return path.split(/[\\/]/).filter(Boolean).at(-2) ?? "skill";
2066
2157
  }
2067
- function skillPathForPiLoader(path) {
2158
+ function skillPathForPiLoader2(path) {
2068
2159
  return existsSync7(join7(path, "SKILL.md")) ? dirname7(path) : path;
2069
2160
  }
2070
2161
  function uniqueStrings(values) {
@@ -2095,7 +2186,7 @@ function readWorkspacePluginPackagePiSnapshot(pluginDirs) {
2095
2186
  const systemPromptAppend = aggregatePluginSystemPromptsFromScan(scan);
2096
2187
  return {
2097
2188
  additionalSkillPaths: uniqueStrings(
2098
- scan.plugins.flatMap((plugin) => plugin.skillPaths ?? []).map(skillPathForPiLoader)
2189
+ scan.plugins.flatMap((plugin) => plugin.skillPaths ?? []).map(skillPathForPiLoader2)
2099
2190
  ),
2100
2191
  packages: compactPiPackages(normalizeBoringPluginPiPackages(scan.plugins)),
2101
2192
  extensionPaths: scan.plugins.flatMap((plugin) => plugin.extensionPaths ?? []),
@@ -2150,7 +2241,7 @@ async function createWorkspaceAgentServer(opts = {}) {
2150
2241
  ];
2151
2242
  const baseStaticPiExtensionPaths = pluginCollection.agentOptions.pi?.extensionPaths ?? [];
2152
2243
  const boringPluginDirs = [
2153
- ...collectBoringPluginDirs(workspaceRoot, pluginCollection),
2244
+ ...collectBoringPluginDirs(workspaceRoot, pluginCollection, opts.additionalBoringPluginDirs),
2154
2245
  ...defaultPluginPackagePaths
2155
2246
  ];
2156
2247
  const staticPluginPackagePiSnapshot = pluginHotReload ? emptyPackageJsonPiSnapshot() : readWorkspacePluginPackagePiSnapshot(boringPluginDirs);
@@ -2169,7 +2260,9 @@ async function createWorkspaceAgentServer(opts = {}) {
2169
2260
  const getHotReloadablePiResources = pluginHotReload ? () => readWorkspacePluginPackagePiSnapshot(boringPluginDirs) : void 0;
2170
2261
  const boringAssetManager = new BoringPluginAssetManager({
2171
2262
  pluginDirs: boringPluginDirs,
2172
- errorRoot: join7(workspaceRoot, ".pi", "extensions")
2263
+ errorRoot: join7(workspaceRoot, ".pi", "extensions"),
2264
+ frontTargetResolver: opts.boringPluginFrontTargetResolver,
2265
+ includeLegacyFrontUrl: opts.boringPluginIncludeLegacyFrontUrl
2173
2266
  });
2174
2267
  const buildRuntimeProvisioningInputs = () => mergeRuntimeProvisioningInputs([
2175
2268
  ...pluginCollection.runtimePlugins,
@@ -2185,11 +2278,18 @@ async function createWorkspaceAgentServer(opts = {}) {
2185
2278
  sessionId: opts.sessionId ?? "default"
2186
2279
  });
2187
2280
  if (!adapter) return currentRuntimeProvisioning;
2188
- currentRuntimeProvisioning = await provisionWorkspaceRuntime({
2281
+ const provisioned = await provisionWorkspaceRuntime({
2189
2282
  plugins: buildRuntimeProvisioningInputs(),
2190
2283
  adapter,
2191
2284
  runtimeLayout
2192
2285
  });
2286
+ currentRuntimeProvisioning = provisioned ? {
2287
+ ...provisioned,
2288
+ env: {
2289
+ ...provisioned.env,
2290
+ BORING_AGENT_WORKSPACE_LOCAL_PLUGIN_ROOTS: workspaceFsCapability === "strong" ? "1" : "0"
2291
+ }
2292
+ } : currentRuntimeProvisioning;
2193
2293
  return currentRuntimeProvisioning;
2194
2294
  };
2195
2295
  await runRuntimeProvisioning();
@@ -2274,6 +2374,7 @@ async function createWorkspaceAgentServer(opts = {}) {
2274
2374
  }
2275
2375
  ;
2276
2376
  app.__boringRebuildPlugins = rebuildPlugins;
2377
+ app.__boringAssetManager = boringAssetManager;
2277
2378
  return app;
2278
2379
  }
2279
2380
  export {