@hachej/boring-agent 0.1.75 → 0.1.77

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.
@@ -7,16 +7,18 @@ import {
7
7
  createAgent,
8
8
  createAgentRuntimeBridge,
9
9
  normalizeMeteringUsage
10
- } from "../chunk-AQVEZYLH.js";
11
- import "../chunk-WSQ5QNIY.js";
10
+ } from "../chunk-3WWLQAJB.js";
11
+ import "../chunk-ZUEITFIJ.js";
12
12
  import {
13
+ CommandReceiptSchema,
13
14
  FollowUpPayloadSchema,
14
15
  InterruptPayloadSchema,
15
16
  PiChatSnapshotSchema,
16
17
  PromptPayloadSchema,
17
18
  QueueClearPayloadSchema,
18
- StopPayloadSchema
19
- } from "../chunk-4LXA7OOV.js";
19
+ StopPayloadSchema,
20
+ StopReceiptSchema
21
+ } from "../chunk-ORURYKNY.js";
20
22
  import {
21
23
  PI_PACKAGE_RESOURCE_FILTERS,
22
24
  PiSessionStore,
@@ -31,7 +33,7 @@ import {
31
33
  registerConfiguredModelProviders,
32
34
  setEnvDefault,
33
35
  withPiHarnessDefaults
34
- } from "../chunk-FDGWCMJD.js";
36
+ } from "../chunk-TS3QGFKK.js";
35
37
  import {
36
38
  safeCapture
37
39
  } from "../chunk-AQBXNPMD.js";
@@ -40,7 +42,7 @@ import {
40
42
  } from "../chunk-AJZHR626.js";
41
43
  import {
42
44
  ErrorCode
43
- } from "../chunk-XZKU7FBV.js";
45
+ } from "../chunk-6AEK34XU.js";
44
46
 
45
47
  // src/server/sandbox/direct/createDirectSandbox.ts
46
48
  import { spawn } from "child_process";
@@ -180,7 +182,7 @@ function createDirectSandbox(opts = {}) {
180
182
  const maxOutputBytes = opts2?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
181
183
  const workspaceRoot = runtimeContext.runtimeCwd;
182
184
  const cwd = opts2?.cwd ?? workspaceRoot;
183
- return await new Promise((resolve10, reject) => {
185
+ return await new Promise((resolve11, reject) => {
184
186
  const child = spawn(cmd, {
185
187
  cwd,
186
188
  env: withWorkspacePythonEnv({ workspaceRoot, env: opts2?.env, preserveHostHome: true }),
@@ -209,7 +211,7 @@ function createDirectSandbox(opts = {}) {
209
211
  if (settled) return;
210
212
  settled = true;
211
213
  cleanup();
212
- resolve10({
214
+ resolve11({
213
215
  stdout: new Uint8Array(Buffer.concat(stdoutChunks)),
214
216
  stderr: new Uint8Array(Buffer.concat(stderrChunks)),
215
217
  exitCode: typeof exitCode === "number" ? exitCode : timedOut ? 124 : 1,
@@ -558,9 +560,9 @@ function createNodeWatcher(root) {
558
560
  }
559
561
  startFsw();
560
562
  if (fsw) {
561
- await new Promise((resolve10) => {
562
- fsw.once("ready", resolve10);
563
- fsw.once("error", () => resolve10());
563
+ await new Promise((resolve11) => {
564
+ fsw.once("ready", resolve11);
565
+ fsw.once("error", () => resolve11());
564
566
  });
565
567
  }
566
568
  if (closed) return { ok: false, reason: "closed" };
@@ -969,7 +971,7 @@ function createBwrapSandbox(opts = {}) {
969
971
  ...withSandboxCwd(baseArgs, sandboxCwd),
970
972
  ...buildCommandArgs(cmd, sandboxOptions.resourceLimits)
971
973
  ];
972
- return await new Promise((resolve10, reject) => {
974
+ return await new Promise((resolve11, reject) => {
973
975
  const child = spawn2("bwrap", args, {
974
976
  env: {
975
977
  ...withWorkspacePythonEnv({ workspaceRoot, env: opts2?.env, sandboxRoot: SANDBOX_HOME2 }),
@@ -999,7 +1001,7 @@ function createBwrapSandbox(opts = {}) {
999
1001
  if (settled) return;
1000
1002
  settled = true;
1001
1003
  cleanup();
1002
- resolve10({
1004
+ resolve11({
1003
1005
  stdout: new Uint8Array(Buffer.concat(stdoutChunks)),
1004
1006
  stderr: new Uint8Array(Buffer.concat(stderrChunks)),
1005
1007
  exitCode: typeof exitCode === "number" ? exitCode : timedOut ? 124 : 1,
@@ -5242,11 +5244,11 @@ async function buildTarGz(files) {
5242
5244
  }
5243
5245
  chunks.push(Buffer.alloc(1024));
5244
5246
  const tarBuffer = Buffer.concat(chunks);
5245
- return new Promise((resolve10, reject) => {
5247
+ return new Promise((resolve11, reject) => {
5246
5248
  const gzip = createGzip({ level: 6 });
5247
5249
  const gzChunks = [];
5248
5250
  gzip.on("data", (chunk) => gzChunks.push(chunk));
5249
- gzip.on("end", () => resolve10(Buffer.concat(gzChunks)));
5251
+ gzip.on("end", () => resolve11(Buffer.concat(gzChunks)));
5250
5252
  gzip.on("error", reject);
5251
5253
  Readable.from(tarBuffer).pipe(gzip);
5252
5254
  });
@@ -6466,13 +6468,13 @@ function resultTool(result) {
6466
6468
  };
6467
6469
  }
6468
6470
  function deferred() {
6469
- let resolve10;
6471
+ let resolve11;
6470
6472
  let reject;
6471
6473
  const promise = new Promise((res, rej) => {
6472
- resolve10 = res;
6474
+ resolve11 = res;
6473
6475
  reject = rej;
6474
6476
  });
6475
- return { promise, resolve: resolve10, reject };
6477
+ return { promise, resolve: resolve11, reject };
6476
6478
  }
6477
6479
  function errorTool(error) {
6478
6480
  const safe = safeError(error);
@@ -7983,33 +7985,182 @@ function buildHarnessAgentTools(bundle, runtime) {
7983
7985
  return tools;
7984
7986
  }
7985
7987
 
7986
- // src/server/http/routes/health.ts
7987
- function healthRoutes(app, opts, done) {
7988
- const startTime = Date.now();
7989
- app.get("/health", async () => {
7988
+ // src/server/http/sessionChangesTracker.ts
7989
+ var InMemorySessionChangesTracker = class _InMemorySessionChangesTracker {
7990
+ static MAX_CHANGES_PER_SESSION = 1e3;
7991
+ bySession = /* @__PURE__ */ new Map();
7992
+ record(sessionId, change) {
7993
+ const existing = this.bySession.get(sessionId);
7994
+ if (existing) {
7995
+ existing.push(change);
7996
+ if (existing.length > _InMemorySessionChangesTracker.MAX_CHANGES_PER_SESSION) {
7997
+ existing.splice(
7998
+ 0,
7999
+ existing.length - _InMemorySessionChangesTracker.MAX_CHANGES_PER_SESSION
8000
+ );
8001
+ }
8002
+ return;
8003
+ }
8004
+ this.bySession.set(sessionId, [change]);
8005
+ }
8006
+ list(sessionId) {
8007
+ return [...this.bySession.get(sessionId) ?? []];
8008
+ }
8009
+ clear(sessionId) {
8010
+ this.bySession.delete(sessionId);
8011
+ }
8012
+ };
8013
+
8014
+ // src/server/runtime/readyStatus.ts
8015
+ function defaultCapabilities(sandboxReady, harnessReady) {
8016
+ return {
8017
+ chat: { state: harnessReady ? "ready" : "preparing" },
8018
+ workspace: { state: sandboxReady ? "ready" : "preparing" },
8019
+ runtimeDependencies: { state: "ready" }
8020
+ };
8021
+ }
8022
+ var ReadyStatusTracker = class {
8023
+ _sandboxReady;
8024
+ _harnessReady;
8025
+ _degradedReason;
8026
+ _capabilities;
8027
+ subscribers = /* @__PURE__ */ new Set();
8028
+ constructor(opts) {
8029
+ this._sandboxReady = opts?.sandboxReady ?? false;
8030
+ this._harnessReady = opts?.harnessReady ?? false;
8031
+ this._capabilities = {
8032
+ ...defaultCapabilities(this._sandboxReady, this._harnessReady),
8033
+ ...opts?.capabilities ?? {}
8034
+ };
8035
+ }
8036
+ get state() {
8037
+ if (this._degradedReason) return "degraded";
8038
+ return this._sandboxReady && this._harnessReady ? "ready" : "provisioning";
8039
+ }
8040
+ isReady() {
8041
+ return this.state === "ready";
8042
+ }
8043
+ getReadiness() {
7990
8044
  return {
7991
- status: "ok",
7992
- version: opts.version,
7993
- uptime: Math.floor((Date.now() - startTime) / 1e3)
8045
+ sandboxReady: this._sandboxReady,
8046
+ harnessReady: this._harnessReady,
8047
+ capabilities: this.getCapabilities(),
8048
+ degradedReason: this._degradedReason
7994
8049
  };
7995
- });
7996
- app.get("/ready", async (_request, reply) => {
7997
- const state = opts.getReadiness();
7998
- if (state.degradedReason) {
7999
- return reply.code(503).send({
8000
- status: "degraded",
8001
- reason: state.degradedReason
8002
- });
8050
+ }
8051
+ getCapabilities() {
8052
+ return {
8053
+ chat: { ...this._capabilities.chat },
8054
+ workspace: { ...this._capabilities.workspace },
8055
+ runtimeDependencies: { ...this._capabilities.runtimeDependencies }
8056
+ };
8057
+ }
8058
+ updateCapability(name, detail) {
8059
+ this._capabilities = {
8060
+ ...this._capabilities,
8061
+ [name]: { ...detail }
8062
+ };
8063
+ this.emit();
8064
+ }
8065
+ updateRuntimeDependencies(detail) {
8066
+ this.updateCapability("runtimeDependencies", detail);
8067
+ }
8068
+ markSandboxReady() {
8069
+ if (this._sandboxReady) return;
8070
+ this._sandboxReady = true;
8071
+ if (this._capabilities.workspace.state === "preparing") {
8072
+ this._capabilities.workspace = { state: "ready" };
8003
8073
  }
8004
- if (!state.sandboxReady || !state.harnessReady) {
8005
- return reply.code(503).send({
8006
- status: "provisioning",
8007
- retryAfter: 2
8008
- });
8074
+ this.emit();
8075
+ }
8076
+ markHarnessReady() {
8077
+ if (this._harnessReady) return;
8078
+ this._harnessReady = true;
8079
+ if (this._capabilities.chat.state === "preparing") {
8080
+ this._capabilities.chat = { state: "ready" };
8081
+ }
8082
+ this.emit();
8083
+ }
8084
+ markDegraded(reason) {
8085
+ this._degradedReason = reason;
8086
+ this.emit();
8087
+ }
8088
+ clearDegraded() {
8089
+ if (!this._degradedReason) return;
8090
+ this._degradedReason = void 0;
8091
+ this.emit();
8092
+ }
8093
+ subscribe(handler) {
8094
+ this.subscribers.add(handler);
8095
+ handler(this.snapshot());
8096
+ return () => {
8097
+ this.subscribers.delete(handler);
8098
+ };
8099
+ }
8100
+ emit() {
8101
+ const event = this.snapshot();
8102
+ for (const h of this.subscribers) h(event);
8103
+ }
8104
+ snapshot() {
8105
+ return {
8106
+ state: this.state,
8107
+ sandboxReady: this._sandboxReady,
8108
+ harnessReady: this._harnessReady,
8109
+ capabilities: this.getCapabilities(),
8110
+ message: this._degradedReason,
8111
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
8112
+ };
8113
+ }
8114
+ };
8115
+
8116
+ // src/server/runtime/modeReadiness.ts
8117
+ function createRuntimeReadyStatusTracker(modeAdapter, opts) {
8118
+ const readiness = modeAdapter.readiness;
8119
+ const tracker = new ReadyStatusTracker({
8120
+ sandboxReady: readiness?.initialSandboxReady ?? true,
8121
+ harnessReady: opts.harnessReady,
8122
+ capabilities: {
8123
+ ...readiness?.initialWorkspaceReadiness ? { workspace: readiness.initialWorkspaceReadiness } : {},
8124
+ ...opts.capabilities ?? {}
8009
8125
  }
8010
- return { status: "ready" };
8011
8126
  });
8012
- done();
8127
+ readiness?.onTrackerCreated?.(tracker);
8128
+ return tracker;
8129
+ }
8130
+
8131
+ // src/server/tools/pluginDiagnostics.ts
8132
+ function createPluginDiagnosticsTool(deps) {
8133
+ return {
8134
+ name: "plugin_diagnostics",
8135
+ description: [
8136
+ "Return current plugin/skill loading errors plus the diagnostics from the",
8137
+ "last /reload. Call this after asking the user to run /reload \u2014 or whenever a",
8138
+ "plugin or skill you expected does not appear to be loaded \u2014 then fix the",
8139
+ "reported error and ask the user to /reload again. Returns a JSON object with",
8140
+ "lastReloadDiagnostics, resourceDiagnostics, and pluginErrors arrays; an empty",
8141
+ "result means no load errors were detected."
8142
+ ].join(" "),
8143
+ parameters: {
8144
+ type: "object",
8145
+ properties: {},
8146
+ additionalProperties: false
8147
+ },
8148
+ async execute(_params, ctx) {
8149
+ const harness = deps.getHarness();
8150
+ const resourceDiagnostics = harness?.getResourceDiagnostics?.(ctx.sessionId ?? "") ?? [];
8151
+ const pluginErrors = deps.getPluginErrors ? await deps.getPluginErrors() : [];
8152
+ const payload = {
8153
+ lastReloadDiagnostics: deps.getLastReloadDiagnostics(),
8154
+ resourceDiagnostics,
8155
+ pluginErrors
8156
+ };
8157
+ return {
8158
+ content: [{ type: "text", text: JSON.stringify(payload) }],
8159
+ details: payload,
8160
+ isError: false
8161
+ };
8162
+ }
8163
+ };
8013
8164
  }
8014
8165
 
8015
8166
  // src/server/http/fsEventBroadcaster.ts
@@ -8404,12 +8555,20 @@ function modelsRoutes(app, opts, done) {
8404
8555
  }
8405
8556
 
8406
8557
  // src/server/http/routes/skills.ts
8558
+ import { isAbsolute as isAbsolute9, relative as relative13, resolve as resolve10, sep as sep7 } from "path";
8407
8559
  import {
8408
8560
  DefaultPackageManager,
8409
8561
  getAgentDir,
8410
8562
  loadSkills
8411
8563
  } from "@mariozechner/pi-coding-agent";
8412
8564
  var CACHE_TTL_MS2 = 3e4;
8565
+ function pathForWorkspaceEditor(workspaceRoot, filePath) {
8566
+ const pathWithinWorkspace = relative13(resolve10(workspaceRoot), resolve10(filePath));
8567
+ if (pathWithinWorkspace === "" || pathWithinWorkspace === ".." || pathWithinWorkspace.startsWith(`..${sep7}`) || isAbsolute9(pathWithinWorkspace)) {
8568
+ return filePath;
8569
+ }
8570
+ return pathWithinWorkspace.split(sep7).join("/");
8571
+ }
8413
8572
  function skillsRoutes(app, opts, done) {
8414
8573
  const cached = /* @__PURE__ */ new Map();
8415
8574
  async function resolveSkillsForRequest(request, refresh = false) {
@@ -8448,7 +8607,7 @@ function skillsRoutes(app, opts, done) {
8448
8607
  const skills = result.skills.map((s) => ({
8449
8608
  name: String(s.name),
8450
8609
  description: String(s.description ?? ""),
8451
- ...typeof s.filePath === "string" ? { filePath: s.filePath } : {},
8610
+ ...typeof s.filePath === "string" ? { filePath: pathForWorkspaceEditor(workspaceRoot, s.filePath) } : {},
8452
8611
  ...typeof s.sourceInfo?.scope === "string" ? { source: s.sourceInfo.scope } : {}
8453
8612
  }));
8454
8613
  const entry = { skills, expiresAt: now + CACHE_TTL_MS2 };
@@ -8510,7 +8669,7 @@ function piChatRoutes(app, opts, done) {
8510
8669
  try {
8511
8670
  const service = await resolveService(opts, request);
8512
8671
  if (!service.listSessions) throw unsupportedServiceMethod("list Pi chat sessions");
8513
- return reply.send(await service.listSessions(getRequestContext(request), sessionListOptions(request)));
8672
+ return reply.send(await service.listSessions(getRequestContext(request, opts), sessionListOptions(request)));
8514
8673
  } catch (err) {
8515
8674
  return sendRouteError(reply, err, "list pi chat sessions failed");
8516
8675
  }
@@ -8521,7 +8680,7 @@ function piChatRoutes(app, opts, done) {
8521
8680
  try {
8522
8681
  const service = await resolveService(opts, request);
8523
8682
  if (!service.createSession) throw unsupportedServiceMethod("create Pi chat session");
8524
- return reply.code(201).send(await service.createSession(getRequestContext(request), body));
8683
+ return reply.code(201).send(await service.createSession(getRequestContext(request, opts), body));
8525
8684
  } catch (err) {
8526
8685
  return sendRouteError(reply, err, "create pi chat session failed");
8527
8686
  }
@@ -8532,7 +8691,7 @@ function piChatRoutes(app, opts, done) {
8532
8691
  try {
8533
8692
  const service = await resolveService(opts, request);
8534
8693
  if (!service.deleteSession) throw unsupportedServiceMethod("delete Pi chat session");
8535
- await service.deleteSession(getRequestContext(request), params.sessionId);
8694
+ await service.deleteSession(getRequestContext(request, opts), params.sessionId);
8536
8695
  return reply.code(204).send();
8537
8696
  } catch (err) {
8538
8697
  return sendRouteError(reply, err, "delete pi chat session failed");
@@ -8543,7 +8702,7 @@ function piChatRoutes(app, opts, done) {
8543
8702
  if (!params) return;
8544
8703
  try {
8545
8704
  const service = await resolveService(opts, request);
8546
- const snapshot = await service.readState(getRequestContext(request), params.sessionId);
8705
+ const snapshot = await service.readState(getRequestContext(request, opts), params.sessionId);
8547
8706
  return reply.send(PiChatSnapshotSchema.parse(snapshot));
8548
8707
  } catch (err) {
8549
8708
  return sendRouteError(reply, err, "read pi chat state failed");
@@ -8564,7 +8723,7 @@ function piChatRoutes(app, opts, done) {
8564
8723
  };
8565
8724
  try {
8566
8725
  const service = await resolveService(opts, request);
8567
- const result = await service.subscribe(getRequestContext(request), params.sessionId, query.cursor, writeFrame);
8726
+ const result = await service.subscribe(getRequestContext(request, opts), params.sessionId, query.cursor, writeFrame);
8568
8727
  if (result.type !== "ok") {
8569
8728
  return sendReplayRangeError(reply, result);
8570
8729
  }
@@ -8598,7 +8757,7 @@ function piChatRoutes(app, opts, done) {
8598
8757
  if (!body) return;
8599
8758
  try {
8600
8759
  const service = await resolveService(opts, request);
8601
- const receipt = await service.prompt(getRequestContext(request), params.sessionId, body);
8760
+ const receipt = await service.prompt(getRequestContext(request, opts), params.sessionId, body);
8602
8761
  return reply.code(202).send(receipt);
8603
8762
  } catch (err) {
8604
8763
  return sendRouteError(reply, err, "prompt rejected");
@@ -8611,7 +8770,7 @@ function piChatRoutes(app, opts, done) {
8611
8770
  if (!body) return;
8612
8771
  try {
8613
8772
  const service = await resolveService(opts, request);
8614
- const receipt = await service.followUp(getRequestContext(request), params.sessionId, body);
8773
+ const receipt = await service.followUp(getRequestContext(request, opts), params.sessionId, body);
8615
8774
  return reply.code(202).send(receipt);
8616
8775
  } catch (err) {
8617
8776
  return sendRouteError(reply, err, "follow-up rejected");
@@ -8624,7 +8783,7 @@ function piChatRoutes(app, opts, done) {
8624
8783
  if (!body) return;
8625
8784
  try {
8626
8785
  const service = await resolveService(opts, request);
8627
- const receipt = await service.clearQueue(getRequestContext(request), params.sessionId, body);
8786
+ const receipt = await service.clearQueue(getRequestContext(request, opts), params.sessionId, body);
8628
8787
  return reply.code(202).send(receipt);
8629
8788
  } catch (err) {
8630
8789
  return sendRouteError(reply, err, "queue clear rejected");
@@ -8637,7 +8796,7 @@ function piChatRoutes(app, opts, done) {
8637
8796
  if (!body) return;
8638
8797
  try {
8639
8798
  const service = await resolveService(opts, request);
8640
- const receipt = await service.interrupt(getRequestContext(request), params.sessionId, body);
8799
+ const receipt = await service.interrupt(getRequestContext(request, opts), params.sessionId, body);
8641
8800
  return reply.code(202).send(receipt);
8642
8801
  } catch (err) {
8643
8802
  return sendRouteError(reply, err, "interrupt rejected");
@@ -8650,7 +8809,7 @@ function piChatRoutes(app, opts, done) {
8650
8809
  if (!body) return;
8651
8810
  try {
8652
8811
  const service = await resolveService(opts, request);
8653
- const receipt = await service.stop(getRequestContext(request), params.sessionId, body);
8812
+ const receipt = await service.stop(getRequestContext(request, opts), params.sessionId, body);
8654
8813
  return reply.code(202).send(receipt);
8655
8814
  } catch (err) {
8656
8815
  return sendRouteError(reply, err, "stop rejected");
@@ -8710,13 +8869,14 @@ async function resolveService(opts, request) {
8710
8869
  }
8711
8870
  return service;
8712
8871
  }
8713
- function getRequestContext(request) {
8872
+ function getRequestContext(request, opts) {
8714
8873
  const storageScopeHeader = request.headers["x-boring-storage-scope"];
8715
8874
  const user = request.user;
8716
8875
  const authSubject = user?.id;
8717
8876
  const authEmail = user?.email;
8877
+ const workspaceId = opts.defaultWorkspaceId === false ? void 0 : request.workspaceContext?.workspaceId ?? opts.defaultWorkspaceId ?? DEFAULT_WORKSPACE_ID2;
8718
8878
  return {
8719
- workspaceId: request.workspaceContext?.workspaceId ?? DEFAULT_WORKSPACE_ID2,
8879
+ workspaceId,
8720
8880
  storageScope: typeof storageScopeHeader === "string" && storageScopeHeader.length > 0 ? storageScopeHeader : void 0,
8721
8881
  authSubject: typeof authSubject === "string" && authSubject.length > 0 ? authSubject : void 0,
8722
8882
  authEmail: typeof authEmail === "string" && authEmail.length > 0 ? authEmail : void 0,
@@ -8809,32 +8969,6 @@ function systemPromptRoutes(app, opts, done) {
8809
8969
  done();
8810
8970
  }
8811
8971
 
8812
- // src/server/http/sessionChangesTracker.ts
8813
- var InMemorySessionChangesTracker = class _InMemorySessionChangesTracker {
8814
- static MAX_CHANGES_PER_SESSION = 1e3;
8815
- bySession = /* @__PURE__ */ new Map();
8816
- record(sessionId, change) {
8817
- const existing = this.bySession.get(sessionId);
8818
- if (existing) {
8819
- existing.push(change);
8820
- if (existing.length > _InMemorySessionChangesTracker.MAX_CHANGES_PER_SESSION) {
8821
- existing.splice(
8822
- 0,
8823
- existing.length - _InMemorySessionChangesTracker.MAX_CHANGES_PER_SESSION
8824
- );
8825
- }
8826
- return;
8827
- }
8828
- this.bySession.set(sessionId, [change]);
8829
- }
8830
- list(sessionId) {
8831
- return [...this.bySession.get(sessionId) ?? []];
8832
- }
8833
- clear(sessionId) {
8834
- this.bySession.delete(sessionId);
8835
- }
8836
- };
8837
-
8838
8972
  // src/server/http/routes/sessionChanges.ts
8839
8973
  function requireSessionId(value, reply) {
8840
8974
  if (typeof value !== "string" || value.length === 0) {
@@ -9163,7 +9297,7 @@ function searchRoutes(app, opts, done) {
9163
9297
  }
9164
9298
 
9165
9299
  // src/server/git/gitFileUrl.ts
9166
- import { dirname as dirname12, relative as relative13 } from "path";
9300
+ import { dirname as dirname12, relative as relative14 } from "path";
9167
9301
  import { execFile as execFile3 } from "child_process";
9168
9302
  import { promisify as promisify3 } from "util";
9169
9303
 
@@ -9246,7 +9380,7 @@ async function resolveGitFileUrl(workspaceRoot, path4) {
9246
9380
  commitSha = null;
9247
9381
  }
9248
9382
  }
9249
- const repoRelativePath = relative13(repoRoot, absolutePath).replace(/\\/g, "/");
9383
+ const repoRelativePath = relative14(repoRoot, absolutePath).replace(/\\/g, "/");
9250
9384
  const url = buildGitFileUrl({ remoteUrl, repoRelativePath, branch, commitSha });
9251
9385
  if (!url) {
9252
9386
  return disabled("Only GitHub SSH/HTTPS remotes are supported right now.");
@@ -9298,167 +9432,225 @@ function gitRoutes(app, opts, done) {
9298
9432
  done();
9299
9433
  }
9300
9434
 
9301
- // src/server/runtime/readyStatus.ts
9302
- function defaultCapabilities(sandboxReady, harnessReady) {
9303
- return {
9304
- chat: { state: harnessReady ? "ready" : "preparing" },
9305
- workspace: { state: sandboxReady ? "ready" : "preparing" },
9306
- runtimeDependencies: { state: "ready" }
9307
- };
9308
- }
9309
- var ReadyStatusTracker = class {
9310
- _sandboxReady;
9311
- _harnessReady;
9312
- _degradedReason;
9313
- _capabilities;
9314
- subscribers = /* @__PURE__ */ new Set();
9315
- constructor(opts) {
9316
- this._sandboxReady = opts?.sandboxReady ?? false;
9317
- this._harnessReady = opts?.harnessReady ?? false;
9318
- this._capabilities = {
9319
- ...defaultCapabilities(this._sandboxReady, this._harnessReady),
9320
- ...opts?.capabilities ?? {}
9321
- };
9322
- }
9323
- get state() {
9324
- if (this._degradedReason) return "degraded";
9325
- return this._sandboxReady && this._harnessReady ? "ready" : "provisioning";
9326
- }
9327
- isReady() {
9328
- return this.state === "ready";
9329
- }
9330
- getReadiness() {
9435
+ // src/server/http/routes/health.ts
9436
+ function healthRoutes(app, opts, done) {
9437
+ const startTime = Date.now();
9438
+ app.get("/health", async () => {
9331
9439
  return {
9332
- sandboxReady: this._sandboxReady,
9333
- harnessReady: this._harnessReady,
9334
- capabilities: this.getCapabilities(),
9335
- degradedReason: this._degradedReason
9336
- };
9337
- }
9338
- getCapabilities() {
9339
- return {
9340
- chat: { ...this._capabilities.chat },
9341
- workspace: { ...this._capabilities.workspace },
9342
- runtimeDependencies: { ...this._capabilities.runtimeDependencies }
9343
- };
9344
- }
9345
- updateCapability(name, detail) {
9346
- this._capabilities = {
9347
- ...this._capabilities,
9348
- [name]: { ...detail }
9440
+ status: "ok",
9441
+ version: opts.version,
9442
+ uptime: Math.floor((Date.now() - startTime) / 1e3)
9349
9443
  };
9350
- this.emit();
9351
- }
9352
- updateRuntimeDependencies(detail) {
9353
- this.updateCapability("runtimeDependencies", detail);
9354
- }
9355
- markSandboxReady() {
9356
- if (this._sandboxReady) return;
9357
- this._sandboxReady = true;
9358
- if (this._capabilities.workspace.state === "preparing") {
9359
- this._capabilities.workspace = { state: "ready" };
9444
+ });
9445
+ app.get("/ready", async (_request, reply) => {
9446
+ const state = opts.getReadiness();
9447
+ if (state.degradedReason) {
9448
+ return reply.code(503).send({
9449
+ status: "degraded",
9450
+ reason: state.degradedReason
9451
+ });
9360
9452
  }
9361
- this.emit();
9362
- }
9363
- markHarnessReady() {
9364
- if (this._harnessReady) return;
9365
- this._harnessReady = true;
9366
- if (this._capabilities.chat.state === "preparing") {
9367
- this._capabilities.chat = { state: "ready" };
9453
+ if (!state.sandboxReady || !state.harnessReady) {
9454
+ return reply.code(503).send({
9455
+ status: "provisioning",
9456
+ retryAfter: 2
9457
+ });
9368
9458
  }
9369
- this.emit();
9370
- }
9371
- markDegraded(reason) {
9372
- this._degradedReason = reason;
9373
- this.emit();
9374
- }
9375
- clearDegraded() {
9376
- if (!this._degradedReason) return;
9377
- this._degradedReason = void 0;
9378
- this.emit();
9379
- }
9380
- subscribe(handler) {
9381
- this.subscribers.add(handler);
9382
- handler(this.snapshot());
9383
- return () => {
9384
- this.subscribers.delete(handler);
9385
- };
9386
- }
9387
- emit() {
9388
- const event = this.snapshot();
9389
- for (const h of this.subscribers) h(event);
9390
- }
9391
- snapshot() {
9392
- return {
9393
- state: this.state,
9394
- sandboxReady: this._sandboxReady,
9395
- harnessReady: this._harnessReady,
9396
- capabilities: this.getCapabilities(),
9397
- message: this._degradedReason,
9398
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
9399
- };
9400
- }
9401
- };
9459
+ return { status: "ready" };
9460
+ });
9461
+ done();
9462
+ }
9402
9463
 
9403
- // src/server/runtime/modeReadiness.ts
9404
- function createRuntimeReadyStatusTracker(modeAdapter, opts) {
9405
- const readiness = modeAdapter.readiness;
9406
- const tracker = new ReadyStatusTracker({
9407
- sandboxReady: readiness?.initialSandboxReady ?? true,
9408
- harnessReady: opts.harnessReady,
9409
- capabilities: {
9410
- ...readiness?.initialWorkspaceReadiness ? { workspace: readiness.initialWorkspaceReadiness } : {},
9411
- ...opts.capabilities ?? {}
9464
+ // src/server/agentRouteBindingProfile.ts
9465
+ function toolNames(tools) {
9466
+ return tools.map((tool) => tool.name);
9467
+ }
9468
+ async function registerAgentRouteBindingProfile(app, profile) {
9469
+ if (profile.dispose) {
9470
+ app.addHook("onClose", async () => {
9471
+ await profile.dispose?.();
9472
+ });
9473
+ }
9474
+ await profile.beforeRegister?.(app);
9475
+ const { register = true, ...health } = profile.health;
9476
+ if (register) await app.register(healthRoutes, health);
9477
+ if (profile.filesystem) {
9478
+ await app.register(fileRoutes, profile.filesystem.file);
9479
+ await app.register(fsEventsRoutes, profile.filesystem.fsEvents);
9480
+ await app.register(treeRoutes, profile.filesystem.tree);
9481
+ await app.register(searchRoutes, profile.filesystem.search);
9482
+ await app.register(gitRoutes, profile.filesystem.git);
9483
+ }
9484
+ await app.register(piChatRoutes, profile.chat);
9485
+ if (profile.systemPrompt) await app.register(systemPromptRoutes, profile.systemPrompt);
9486
+ await app.register(modelsRoutes, profile.models ?? {});
9487
+ if (profile.skills) await app.register(skillsRoutes, profile.skills);
9488
+ await app.register(sessionChangesRoutes, { tracker: profile.sessionChangesTracker });
9489
+ if (profile.reload) {
9490
+ if (typeof profile.reload === "function") {
9491
+ await profile.reload(app);
9492
+ } else {
9493
+ await app.register(reloadRoutes, profile.reload);
9412
9494
  }
9413
- });
9414
- readiness?.onTrackerCreated?.(tracker);
9415
- return tracker;
9495
+ }
9496
+ await app.register(catalogRoutes, profile.catalog);
9497
+ if (profile.commands) await app.register(commandsRoutes, profile.commands);
9498
+ await app.register(readyStatusRoutes, profile.readyStatus);
9416
9499
  }
9417
9500
 
9418
- // src/server/tools/pluginDiagnostics.ts
9419
- function createPluginDiagnosticsTool(deps) {
9501
+ // src/server/workspaceAgentDispatcher.ts
9502
+ function createBoundWorkspaceAgentDispatcher(agent, ctx) {
9503
+ const boundCtx = normalizeWorkspaceAgentDispatcherContext(ctx);
9420
9504
  return {
9421
- name: "plugin_diagnostics",
9422
- description: [
9423
- "Return current plugin/skill loading errors plus the diagnostics from the",
9424
- "last /reload. Call this after asking the user to run /reload \u2014 or whenever a",
9425
- "plugin or skill you expected does not appear to be loaded \u2014 then fix the",
9426
- "reported error and ask the user to /reload again. Returns a JSON object with",
9427
- "lastReloadDiagnostics, resourceDiagnostics, and pluginErrors arrays; an empty",
9428
- "result means no load errors were detected."
9429
- ].join(" "),
9430
- parameters: {
9431
- type: "object",
9432
- properties: {},
9433
- additionalProperties: false
9505
+ send(input) {
9506
+ return agent.send({ ...input, ctx: boundCtx });
9434
9507
  },
9435
- async execute(_params, ctx) {
9436
- const harness = deps.getHarness();
9437
- const resourceDiagnostics = harness?.getResourceDiagnostics?.(ctx.sessionId ?? "") ?? [];
9438
- const pluginErrors = deps.getPluginErrors ? await deps.getPluginErrors() : [];
9439
- const payload = {
9440
- lastReloadDiagnostics: deps.getLastReloadDiagnostics(),
9441
- resourceDiagnostics,
9442
- pluginErrors
9443
- };
9444
- return {
9445
- content: [{ type: "text", text: JSON.stringify(payload) }],
9446
- details: payload,
9447
- isError: false
9448
- };
9508
+ async interrupt(sessionId) {
9509
+ return parseControlReceipt(await agent.interrupt(sessionId, boundCtx), "interrupt");
9510
+ },
9511
+ async stop(sessionId) {
9512
+ return parseStopReceipt(await agent.stop(sessionId, boundCtx));
9449
9513
  }
9450
9514
  };
9451
9515
  }
9516
+ function assertWorkspaceAgentDispatcherRequestContext(ctx, request) {
9517
+ normalizeWorkspaceAgentDispatcherContext(ctx);
9518
+ if (!request) return;
9519
+ const requestWorkspaceId = request.workspaceContext?.workspaceId?.trim();
9520
+ if (!requestWorkspaceId) return;
9521
+ if (requestWorkspaceId !== ctx.workspaceId.trim()) {
9522
+ throw createWorkspaceAgentDispatcherError(
9523
+ ErrorCode.enum.UNAUTHORIZED,
9524
+ "workspace agent dispatcher context does not match request workspace",
9525
+ 401
9526
+ );
9527
+ }
9528
+ }
9529
+ function normalizeWorkspaceAgentDispatcherContext(ctx) {
9530
+ const workspaceId = ctx.workspaceId?.trim();
9531
+ const userId = ctx.userId?.trim();
9532
+ if (!workspaceId) {
9533
+ throw createWorkspaceAgentDispatcherError(
9534
+ ErrorCode.enum.WORKSPACE_UNINITIALIZED,
9535
+ "workspace id is required",
9536
+ 400
9537
+ );
9538
+ }
9539
+ if (!userId) {
9540
+ throw createWorkspaceAgentDispatcherError(
9541
+ ErrorCode.enum.UNAUTHORIZED,
9542
+ "user id is required",
9543
+ 401
9544
+ );
9545
+ }
9546
+ return { workspaceId, userId };
9547
+ }
9548
+ function parseControlReceipt(receipt, action) {
9549
+ const parsed = CommandReceiptSchema.safeParse(receipt);
9550
+ if (parsed.success) return parsed.data;
9551
+ throw createWorkspaceAgentDispatcherError(
9552
+ ErrorCode.enum.AGENT_CONTROL_RECEIPT_INVALID,
9553
+ `agent ${action} returned a malformed receipt`,
9554
+ 500
9555
+ );
9556
+ }
9557
+ function parseStopReceipt(receipt) {
9558
+ const parsed = StopReceiptSchema.safeParse(receipt);
9559
+ if (parsed.success) return parsed.data;
9560
+ throw createWorkspaceAgentDispatcherError(
9561
+ ErrorCode.enum.AGENT_CONTROL_RECEIPT_INVALID,
9562
+ "agent stop returned a malformed receipt",
9563
+ 500
9564
+ );
9565
+ }
9566
+ function createWorkspaceAgentDispatcherError(code, message, statusCode) {
9567
+ const error = new Error(message);
9568
+ error.code = code;
9569
+ error.statusCode = statusCode;
9570
+ return error;
9571
+ }
9452
9572
 
9453
9573
  // src/server/createAgentApp.ts
9454
9574
  var DEFAULT_VERSION = "0.1.0-dev";
9455
9575
  var DEFAULT_SESSION_ID = "default";
9576
+ var PURE_RUNTIME_MODE = "none";
9577
+ function createStaticWorkspaceAgentDispatcherResolver(agent, workspaceId) {
9578
+ return {
9579
+ async resolve(ctx, options) {
9580
+ const boundCtx = normalizeWorkspaceAgentDispatcherContext(ctx);
9581
+ assertWorkspaceAgentDispatcherRequestContext(boundCtx, options?.request);
9582
+ if (boundCtx.workspaceId !== workspaceId) {
9583
+ throw createWorkspaceAgentDispatcherError(
9584
+ ErrorCode.enum.UNAUTHORIZED,
9585
+ "workspace agent dispatcher context does not match bound workspace",
9586
+ 401
9587
+ );
9588
+ }
9589
+ return createBoundWorkspaceAgentDispatcher(agent, boundCtx);
9590
+ }
9591
+ };
9592
+ }
9456
9593
  async function createAgentApp(opts = {}) {
9457
- const workspaceRoot = opts.workspaceRoot ?? process.cwd();
9458
9594
  const sessionId = opts.sessionId ?? DEFAULT_SESSION_ID;
9459
- const templatePath = opts.templatePath ?? getEnv("BORING_AGENT_TEMPLATE_PATH");
9460
9595
  const app = Fastify({ logger: opts.logger ?? true, bodyLimit: 16 * 1024 * 1024 });
9461
9596
  const resolvedMode = opts.runtimeModeAdapter?.id ?? opts.mode ?? autoDetectMode();
9597
+ const profile = !opts.runtimeModeAdapter && resolvedMode === PURE_RUNTIME_MODE ? await createPureAgentAppProfile(opts, sessionId, resolvedMode) : await createWorkspaceAgentAppProfile(opts, sessionId, resolvedMode, app);
9598
+ app.addHook(
9599
+ "onRequest",
9600
+ createAuthMiddleware({
9601
+ authToken: opts.authToken,
9602
+ publicPaths: ["/health", "/ready", "/api/v1/ready-status"]
9603
+ })
9604
+ );
9605
+ await registerAgentRouteBindingProfile(app, profile);
9606
+ return app;
9607
+ }
9608
+ async function createPureAgentAppProfile(opts, sessionId, resolvedMode) {
9609
+ const runtimePi = withPiHarnessDefaults(opts.pi);
9610
+ const baseHarnessFactory = opts.harnessFactory ?? ((input) => createPiCodingAgentHarness({
9611
+ ...input,
9612
+ pi: runtimePi
9613
+ }));
9614
+ const harnessFactory = ((input) => baseHarnessFactory({
9615
+ ...input,
9616
+ sessionNamespace: opts.sessionNamespace,
9617
+ sessionRoot: opts.sessionRoot,
9618
+ sessionDir: opts.sessionDir ?? input.sessionDir
9619
+ }));
9620
+ const tools = opts.extraTools ?? [];
9621
+ const coreAgent = createAgentRuntimeBridge({
9622
+ runtime: "none",
9623
+ tools,
9624
+ harnessFactory,
9625
+ systemPromptAppend: opts.systemPromptAppend,
9626
+ systemPromptDynamic: opts.systemPromptDynamic,
9627
+ telemetry: opts.telemetry,
9628
+ metering: opts.metering,
9629
+ sessionStorageRoot: opts.sessionRoot
9630
+ });
9631
+ const agentRuntime = await coreAgent.getRuntime();
9632
+ opts.onWorkspaceAgentDispatcher?.(createStaticWorkspaceAgentDispatcherResolver(coreAgent.agent, sessionId));
9633
+ const readyTracker = new ReadyStatusTracker({ sandboxReady: true, harnessReady: true });
9634
+ return {
9635
+ runtimeMode: resolvedMode,
9636
+ capabilities: { tools: toolNames(tools) },
9637
+ sessionChangesTracker: new InMemorySessionChangesTracker(),
9638
+ health: {
9639
+ version: opts.version ?? DEFAULT_VERSION,
9640
+ getReadiness: () => ({ sandboxReady: true, harnessReady: true })
9641
+ },
9642
+ chat: {
9643
+ service: agentRuntime.service,
9644
+ defaultWorkspaceId: false
9645
+ },
9646
+ catalog: { tools },
9647
+ readyStatus: { tracker: readyTracker },
9648
+ dispose: () => coreAgent.agent.dispose()
9649
+ };
9650
+ }
9651
+ async function createWorkspaceAgentAppProfile(opts, sessionId, resolvedMode, app) {
9652
+ const workspaceRoot = opts.workspaceRoot ?? process.cwd();
9653
+ const templatePath = opts.templatePath ?? getEnv("BORING_AGENT_TEMPLATE_PATH");
9462
9654
  const modeAdapter = opts.runtimeModeAdapter ?? resolveMode(resolvedMode);
9463
9655
  let runtimeBundle = await modeAdapter.create({
9464
9656
  workspaceRoot,
@@ -9536,7 +9728,8 @@ async function createAgentApp(opts = {}) {
9536
9728
  const harnessFactory = ((input) => baseHarnessFactory({
9537
9729
  ...input,
9538
9730
  sessionNamespace: opts.sessionNamespace,
9539
- sessionDir: opts.sessionDir
9731
+ sessionRoot: opts.sessionRoot,
9732
+ sessionDir: opts.sessionDir ?? input.sessionDir
9540
9733
  }));
9541
9734
  const coreAgent = createAgentRuntimeBridge({
9542
9735
  runtime: modeAdapter,
@@ -9546,6 +9739,7 @@ async function createAgentApp(opts = {}) {
9546
9739
  systemPromptDynamic: opts.systemPromptDynamic,
9547
9740
  telemetry: opts.telemetry,
9548
9741
  metering: opts.metering,
9742
+ sessionStorageRoot: opts.sessionRoot,
9549
9743
  workdir: workspaceRoot
9550
9744
  }, {
9551
9745
  service: {
@@ -9554,23 +9748,12 @@ async function createAgentApp(opts = {}) {
9554
9748
  }
9555
9749
  });
9556
9750
  const agentRuntime = await coreAgent.getRuntime();
9751
+ opts.onWorkspaceAgentDispatcher?.(createStaticWorkspaceAgentDispatcherResolver(coreAgent.agent, sessionId));
9557
9752
  const harness = agentRuntime.harness;
9558
9753
  harnessRef = harness;
9559
- const sessionChangesTracker = new InMemorySessionChangesTracker();
9560
9754
  const readyTracker = createRuntimeReadyStatusTracker(modeAdapter, {
9561
9755
  harnessReady: true
9562
9756
  });
9563
- app.addHook(
9564
- "onRequest",
9565
- createAuthMiddleware({
9566
- authToken: opts.authToken,
9567
- publicPaths: ["/health", "/ready", "/api/v1/ready-status"]
9568
- })
9569
- );
9570
- await app.register(healthRoutes, {
9571
- version: opts.version ?? DEFAULT_VERSION,
9572
- getReadiness: () => readyTracker.getReadiness()
9573
- });
9574
9757
  const filesystemBindingsForRequest = opts.getFilesystemBindings ? (request) => {
9575
9758
  const user = request.user;
9576
9759
  return opts.getFilesystemBindings?.({
@@ -9583,46 +9766,65 @@ async function createAgentApp(opts = {}) {
9583
9766
  requestId: request.id
9584
9767
  });
9585
9768
  } : void 0;
9586
- await app.register(fileRoutes, { workspace: runtimeBundle.workspace, getFilesystemBindings: filesystemBindingsForRequest, filesystemBindings: runtimeBundle.filesystemBindings });
9587
- await app.register(fsEventsRoutes, { workspace: runtimeBundle.workspace });
9588
- await app.register(treeRoutes, { workspace: runtimeBundle.workspace, getFilesystemBindings: filesystemBindingsForRequest, filesystemBindings: runtimeBundle.filesystemBindings });
9589
- await app.register(searchRoutes, { fileSearch: runtimeBundle.fileSearch });
9590
- await app.register(gitRoutes, {
9591
- getWorkspaceRoot: () => getOptionalRuntimeBundleStorageRoot(runtimeBundle)
9592
- });
9593
- await app.register(piChatRoutes, {
9594
- service: agentRuntime.service
9595
- });
9596
- await app.register(systemPromptRoutes, { harness });
9597
- await app.register(modelsRoutes);
9598
- await app.register(skillsRoutes, {
9599
- workspaceRoot,
9600
- additionalSkillPaths: runtimePi.additionalSkillPaths,
9601
- piPackages: runtimePi.packages,
9602
- noSkills: runtimePi.noSkills,
9603
- getAdditionalSkillPaths: () => [
9604
- ...getRuntimeProvisioning()?.skillPaths ?? [],
9605
- ...opts.pi?.additionalSkillPaths ?? [],
9606
- ...opts.pi?.getHotReloadableResources?.().additionalSkillPaths ?? []
9607
- ],
9608
- getPiPackages: () => [
9609
- ...opts.pi?.packages ?? [],
9610
- ...opts.pi?.getHotReloadableResources?.().packages ?? []
9611
- ]
9612
- });
9613
- await app.register(sessionChangesRoutes, { tracker: sessionChangesTracker });
9614
- await app.register(catalogRoutes, { tools });
9615
- await app.register(commandsRoutes, { harness, defaultSessionId: sessionId, workdir: runtimeBundle.workspace.root, metering: opts.metering });
9616
- await app.register(reloadRoutes, {
9617
- harness,
9618
- defaultSessionId: sessionId,
9619
- beforeReload: opts.beforeReload,
9620
- onDiagnostics: (diagnostics) => {
9621
- lastReloadDiagnostics = diagnostics;
9622
- }
9623
- });
9624
- await app.register(readyStatusRoutes, { tracker: readyTracker });
9625
- return app;
9769
+ return {
9770
+ runtimeMode: resolvedMode,
9771
+ capabilities: { tools: toolNames(tools) },
9772
+ sessionChangesTracker: new InMemorySessionChangesTracker(),
9773
+ health: {
9774
+ version: opts.version ?? DEFAULT_VERSION,
9775
+ getReadiness: () => readyTracker.getReadiness()
9776
+ },
9777
+ filesystem: {
9778
+ file: {
9779
+ workspace: runtimeBundle.workspace,
9780
+ getFilesystemBindings: filesystemBindingsForRequest,
9781
+ filesystemBindings: runtimeBundle.filesystemBindings
9782
+ },
9783
+ fsEvents: { workspace: runtimeBundle.workspace },
9784
+ tree: {
9785
+ workspace: runtimeBundle.workspace,
9786
+ getFilesystemBindings: filesystemBindingsForRequest,
9787
+ filesystemBindings: runtimeBundle.filesystemBindings
9788
+ },
9789
+ // File search shares the same bound implementation as the model tool.
9790
+ search: { fileSearch: runtimeBundle.fileSearch },
9791
+ // Git metadata resolves against host storage, not a sandbox-internal cwd.
9792
+ git: { getWorkspaceRoot: () => getOptionalRuntimeBundleStorageRoot(runtimeBundle) }
9793
+ },
9794
+ chat: { service: agentRuntime.service },
9795
+ systemPrompt: { harness },
9796
+ skills: {
9797
+ workspaceRoot,
9798
+ additionalSkillPaths: runtimePi.additionalSkillPaths,
9799
+ piPackages: runtimePi.packages,
9800
+ noSkills: runtimePi.noSkills,
9801
+ getAdditionalSkillPaths: () => [
9802
+ ...getRuntimeProvisioning()?.skillPaths ?? [],
9803
+ ...opts.pi?.additionalSkillPaths ?? [],
9804
+ ...opts.pi?.getHotReloadableResources?.().additionalSkillPaths ?? []
9805
+ ],
9806
+ getPiPackages: () => [
9807
+ ...opts.pi?.packages ?? [],
9808
+ ...opts.pi?.getHotReloadableResources?.().packages ?? []
9809
+ ]
9810
+ },
9811
+ catalog: { tools },
9812
+ commands: {
9813
+ harness,
9814
+ defaultSessionId: sessionId,
9815
+ workdir: runtimeBundle.workspace.root,
9816
+ metering: opts.metering
9817
+ },
9818
+ reload: {
9819
+ harness,
9820
+ defaultSessionId: sessionId,
9821
+ beforeReload: opts.beforeReload,
9822
+ onDiagnostics: (diagnostics) => {
9823
+ lastReloadDiagnostics = diagnostics;
9824
+ }
9825
+ },
9826
+ readyStatus: { tracker: readyTracker }
9827
+ };
9626
9828
  }
9627
9829
 
9628
9830
  // src/server/http/csp.ts
@@ -9650,6 +9852,7 @@ function applyCspHeaders(response, opts = {}) {
9650
9852
  }
9651
9853
 
9652
9854
  // src/server/registerAgentRoutes.ts
9855
+ import { createHash as createHash6 } from "crypto";
9653
9856
  import { basename as basename2 } from "path";
9654
9857
  import { AuthStorage as AuthStorage2, ModelRegistry as ModelRegistry2 } from "@mariozechner/pi-coding-agent";
9655
9858
 
@@ -9795,6 +9998,8 @@ function buildUploadAgentTools(bundle) {
9795
9998
  // src/server/registerAgentRoutes.ts
9796
9999
  var DEFAULT_VERSION2 = "0.1.0-dev";
9797
10000
  var DEFAULT_WORKSPACE_ID4 = "default";
10001
+ var PURE_RUNTIME_MODE2 = "none";
10002
+ var MAX_PURE_BINDINGS = 256;
9798
10003
  var STANDARD_AGENT_TOOL_NAMES = ["bash", "read", "write", "edit", "find", "grep", "ls"];
9799
10004
  function pluginNameFromPath(path4) {
9800
10005
  const fileName = basename2(path4);
@@ -9814,6 +10019,20 @@ function getAvailableModelProviders() {
9814
10019
  new Set(availableModels.map((model) => model.provider))
9815
10020
  ).sort((a, b) => a.localeCompare(b));
9816
10021
  }
10022
+ function registerAgentCapabilitiesContributor(app, profile) {
10023
+ const host = app;
10024
+ if (typeof host.registerCapabilitiesContributor !== "function") return;
10025
+ host.registerCapabilitiesContributor(
10026
+ "agent",
10027
+ () => ({
10028
+ agent: {
10029
+ runtimeMode: profile.runtimeMode,
10030
+ tools: profile.capabilities.tools,
10031
+ modelProviders: getAvailableModelProviders()
10032
+ }
10033
+ })
10034
+ );
10035
+ }
9817
10036
  function getRequestWorkspaceId2(request) {
9818
10037
  return request.workspaceContext?.workspaceId ?? DEFAULT_WORKSPACE_ID4;
9819
10038
  }
@@ -9839,6 +10058,14 @@ function normalizeSessionNamespace(value) {
9839
10058
  const trimmed = value.trim();
9840
10059
  return trimmed.length > 0 ? trimmed : void 0;
9841
10060
  }
10061
+ function pureSessionNamespaceFromWorkspaceId(workspaceId) {
10062
+ const digest = createHash6("sha256").update(workspaceId).digest("hex").slice(0, 16);
10063
+ return `workspace-${digest}`;
10064
+ }
10065
+ function pureSessionNamespaceFromScope(parts) {
10066
+ const digest = createHash6("sha256").update(JSON.stringify(parts)).digest("hex").slice(0, 16);
10067
+ return `scope-${digest}`;
10068
+ }
9842
10069
  function getRequestAuthSubject2(request) {
9843
10070
  const userId = request?.user?.id;
9844
10071
  if (typeof userId === "string" && userId.trim()) return userId.trim();
@@ -9895,16 +10122,20 @@ function createRuntimeReadinessCheck(workspaceId, getRuntimeDependencies) {
9895
10122
  };
9896
10123
  }
9897
10124
  var registerAgentRoutes = async (app, opts) => {
9898
- const workspaceRoot = opts.workspaceRoot ?? process.cwd();
9899
10125
  const sessionId = opts.sessionId ?? DEFAULT_WORKSPACE_ID4;
9900
- const templatePath = opts.templatePath ?? getEnv("BORING_AGENT_TEMPLATE_PATH");
9901
10126
  const resolvedMode = opts.runtimeModeAdapter?.id ?? opts.mode ?? autoDetectMode();
10127
+ if (!opts.runtimeModeAdapter && resolvedMode === PURE_RUNTIME_MODE2) {
10128
+ await registerPureAgentRoutes(app, opts, sessionId, resolvedMode);
10129
+ return;
10130
+ }
10131
+ const workspaceRoot = opts.workspaceRoot ?? process.cwd();
10132
+ const templatePath = opts.templatePath ?? getEnv("BORING_AGENT_TEMPLATE_PATH");
9902
10133
  const modeAdapter = opts.runtimeModeAdapter ?? resolveMode(resolvedMode, { sandboxHandleStore: opts.sandboxHandleStore });
9903
10134
  app.addHook("onClose", async () => {
9904
10135
  await modeAdapter.dispose?.();
9905
10136
  });
9906
10137
  const modelsWorkspaceScoped = Boolean(opts.filterModels);
9907
- const requestScopedRuntime = typeof opts.getWorkspaceId === "function" || typeof opts.getWorkspaceRoot === "function" || typeof opts.getTemplatePath === "function" || typeof opts.getPi === "function" || typeof opts.getExtraTools === "function" || typeof opts.getSessionNamespace === "function" || typeof opts.getSystemPromptDynamic === "function";
10138
+ const requestScopedRuntime = typeof opts.getWorkspaceId === "function" || typeof opts.getWorkspaceRoot === "function" || typeof opts.getTemplatePath === "function" || typeof opts.getPi === "function" || typeof opts.getExtraTools === "function" || typeof opts.getSessionNamespace === "function" || typeof opts.getSystemPromptDynamic === "function" || typeof opts.getTrustedWorkspaceRoot === "function";
9908
10139
  const sessionChangesTracker = new InMemorySessionChangesTracker();
9909
10140
  const externalPluginsEnabled = opts.externalPlugins !== false;
9910
10141
  const runtimeBindings = /* @__PURE__ */ new Map();
@@ -9919,12 +10150,23 @@ var registerAgentRoutes = async (app, opts) => {
9919
10150
  async function resolveScopePi(workspaceId, root, request) {
9920
10151
  return withPiHarnessDefaults(opts.getPi ? await opts.getPi({ workspaceId, workspaceRoot: root, request }) : opts.pi);
9921
10152
  }
9922
- async function resolveRuntimeScope(workspaceId, request) {
9923
- const root = request && opts.getWorkspaceRoot ? await opts.getWorkspaceRoot(workspaceId, request) : workspaceRoot;
10153
+ async function resolveRuntimeScope(workspaceId, request, trustedCtx) {
10154
+ let root = workspaceRoot;
10155
+ if (request && opts.getWorkspaceRoot) {
10156
+ root = await opts.getWorkspaceRoot(workspaceId, request);
10157
+ } else if (trustedCtx && opts.getTrustedWorkspaceRoot) {
10158
+ root = await opts.getTrustedWorkspaceRoot(trustedCtx);
10159
+ } else if (!request && opts.getWorkspaceRoot) {
10160
+ throw createWorkspaceAgentDispatcherError(
10161
+ ErrorCode.enum.WORKSPACE_UNINITIALIZED,
10162
+ "workspace root resolution requires trusted workspace context",
10163
+ 400
10164
+ );
10165
+ }
9924
10166
  const scopedTemplatePath = opts.getTemplatePath ? await opts.getTemplatePath({ workspaceId, workspaceRoot: root, request }) : templatePath;
9925
10167
  const pi = await resolveScopePi(workspaceId, root, request);
9926
- const sessionNamespace = normalizeSessionNamespace(opts.getSessionNamespace ? await opts.getSessionNamespace({ workspaceId, workspaceRoot: root, request }) : opts.sessionNamespace);
9927
- const extraToolsAuthSubject = opts.getExtraTools ? getRequestAuthSubject2(request) : void 0;
10168
+ const sessionNamespace = normalizeSessionNamespace(opts.getSessionNamespace ? await opts.getSessionNamespace({ workspaceId, workspaceRoot: root, request, userId: trustedCtx?.userId }) : opts.sessionNamespace);
10169
+ const extraToolsAuthSubject = opts.getExtraTools ? trustedCtx?.userId ?? getRequestAuthSubject2(request) : void 0;
9928
10170
  return {
9929
10171
  root,
9930
10172
  templatePath: scopedTemplatePath,
@@ -9984,7 +10226,7 @@ var registerAgentRoutes = async (app, opts) => {
9984
10226
  request
9985
10227
  });
9986
10228
  }
9987
- async function createRuntimeBinding(workspaceId, scope, request) {
10229
+ async function createRuntimeBinding(workspaceId, scope, request, trustedCtx) {
9988
10230
  const root = scope.root;
9989
10231
  const modeCtx = {
9990
10232
  workspaceRoot: root,
@@ -10132,7 +10374,7 @@ var registerAgentRoutes = async (app, opts) => {
10132
10374
  workspaceRoot: root,
10133
10375
  runtimeMode: resolvedMode,
10134
10376
  workspaceFsCapability: runtimeBundle.workspace.fsCapability,
10135
- authSubject: getRequestAuthSubject2(request)
10377
+ authSubject: trustedCtx?.userId ?? getRequestAuthSubject2(request)
10136
10378
  }) : [];
10137
10379
  const tools = mergeTools({
10138
10380
  standardTools,
@@ -10208,12 +10450,12 @@ var registerAgentRoutes = async (app, opts) => {
10208
10450
  startRuntimeProvisioning(request);
10209
10451
  return binding;
10210
10452
  }
10211
- function createRuntimeBindingEntry(workspaceId, scope, request) {
10453
+ function createRuntimeBindingEntry(workspaceId, scope, request, trustedCtx) {
10212
10454
  const entry = {
10213
10455
  state: "pending",
10214
10456
  promise: Promise.resolve(null)
10215
10457
  };
10216
- entry.promise = createRuntimeBinding(workspaceId, scope, request).then(
10458
+ entry.promise = createRuntimeBinding(workspaceId, scope, request, trustedCtx).then(
10217
10459
  (binding) => {
10218
10460
  entry.state = "ready";
10219
10461
  return binding;
@@ -10229,7 +10471,7 @@ var registerAgentRoutes = async (app, opts) => {
10229
10471
  return entry;
10230
10472
  }
10231
10473
  async function getOrCreateRuntimeBinding(workspaceId, request, options = {}) {
10232
- const scope = await resolveRuntimeScope(workspaceId, request);
10474
+ const scope = await resolveRuntimeScope(workspaceId, request, options.trustedCtx);
10233
10475
  const existing = runtimeBindings.get(scope.key);
10234
10476
  if (existing) {
10235
10477
  if (options.failIfPending && existing.state === "pending") {
@@ -10247,7 +10489,7 @@ var registerAgentRoutes = async (app, opts) => {
10247
10489
  );
10248
10490
  }
10249
10491
  }
10250
- const created = createRuntimeBindingEntry(workspaceId, scope, request);
10492
+ const created = createRuntimeBindingEntry(workspaceId, scope, request, options.trustedCtx);
10251
10493
  runtimeBindings.set(scope.key, created);
10252
10494
  evictRuntimeBindings();
10253
10495
  if (options.failIfPending) {
@@ -10303,6 +10545,24 @@ var registerAgentRoutes = async (app, opts) => {
10303
10545
  const staticBinding = requestScopedRuntime ? null : await getOrCreateRuntimeBinding(sessionId);
10304
10546
  const skillsScopeByRequest = /* @__PURE__ */ new WeakMap();
10305
10547
  const earlySessionStores = /* @__PURE__ */ new Map();
10548
+ opts.onWorkspaceAgentDispatcher?.({
10549
+ async resolve(ctx, options) {
10550
+ const boundCtx = normalizeWorkspaceAgentDispatcherContext(ctx);
10551
+ assertWorkspaceAgentDispatcherRequestContext(boundCtx, options?.request);
10552
+ if (staticBinding) {
10553
+ if (boundCtx.workspaceId !== sessionId) {
10554
+ throw createWorkspaceAgentDispatcherError(
10555
+ ErrorCode.enum.UNAUTHORIZED,
10556
+ "workspace agent dispatcher context does not match bound workspace",
10557
+ 401
10558
+ );
10559
+ }
10560
+ return createBoundWorkspaceAgentDispatcher(staticBinding.agent, boundCtx);
10561
+ }
10562
+ const binding = await getOrCreateRuntimeBinding(boundCtx.workspaceId, options?.request, { trustedCtx: boundCtx });
10563
+ return createBoundWorkspaceAgentDispatcher(binding.agent, boundCtx);
10564
+ }
10565
+ });
10306
10566
  function getSkillsScopeForRequest(request) {
10307
10567
  let promise = skillsScopeByRequest.get(request);
10308
10568
  if (!promise) {
@@ -10517,6 +10777,173 @@ var registerAgentRoutes = async (app, opts) => {
10517
10777
  staticBinding ? { tracker: staticBinding.readyTracker } : { getTracker: async (request) => (await getBindingForRequest(request)).readyTracker }
10518
10778
  );
10519
10779
  };
10780
+ async function registerPureAgentRoutes(app, opts, sessionId, resolvedMode) {
10781
+ const pureBindings = /* @__PURE__ */ new Map();
10782
+ const requestScopedPure = typeof opts.getWorkspaceId === "function" || typeof opts.getWorkspaceRoot === "function" || typeof opts.getExtraTools === "function" || typeof opts.getPi === "function" || typeof opts.getSessionNamespace === "function" || typeof opts.getSystemPromptDynamic === "function" || typeof opts.getTrustedWorkspaceRoot === "function";
10783
+ function evictPureBindings() {
10784
+ if (pureBindings.size <= MAX_PURE_BINDINGS) return;
10785
+ const keys = Array.from(pureBindings.keys());
10786
+ for (let i = 0; i < keys.length - MAX_PURE_BINDINGS; i += 1) {
10787
+ const key = keys[i];
10788
+ if (!key) continue;
10789
+ const binding = pureBindings.get(key);
10790
+ pureBindings.delete(key);
10791
+ binding?.then((resolved) => resolved.agent.dispose()).catch(() => {
10792
+ });
10793
+ }
10794
+ }
10795
+ async function createPureBinding(request, trustedCtx) {
10796
+ const workspaceId = trustedCtx?.workspaceId ?? (request ? getRequestWorkspaceId2(request) : sessionId);
10797
+ let workspaceRoot = opts.workspaceRoot ?? "";
10798
+ if (request && opts.getWorkspaceRoot) {
10799
+ workspaceRoot = await opts.getWorkspaceRoot(workspaceId, request);
10800
+ } else if (trustedCtx && opts.getTrustedWorkspaceRoot) {
10801
+ workspaceRoot = await opts.getTrustedWorkspaceRoot(trustedCtx);
10802
+ } else if (!request && opts.getWorkspaceRoot) {
10803
+ throw createWorkspaceAgentDispatcherError(
10804
+ ErrorCode.enum.WORKSPACE_UNINITIALIZED,
10805
+ "workspace root resolution requires trusted workspace context",
10806
+ 400
10807
+ );
10808
+ }
10809
+ const pi = withPiHarnessDefaults(opts.getPi ? await opts.getPi({ workspaceId, workspaceRoot, request }) : opts.pi);
10810
+ const configuredSessionNamespace = normalizeSessionNamespace(opts.getSessionNamespace ? await opts.getSessionNamespace({ workspaceId, workspaceRoot, request, userId: trustedCtx?.userId }) : opts.sessionNamespace);
10811
+ const authSubject = opts.getExtraTools ? trustedCtx?.userId ?? getRequestAuthSubject2(request) : void 0;
10812
+ const pureScopeNamespace = opts.getWorkspaceId ? pureSessionNamespaceFromWorkspaceId(workspaceId) : requestScopedPure ? pureSessionNamespaceFromScope([resolvedMode, workspaceId, workspaceRoot, pi, authSubject ?? null]) : void 0;
10813
+ const sessionNamespace = configuredSessionNamespace && pureScopeNamespace ? `${configuredSessionNamespace}-${pureScopeNamespace}` : configuredSessionNamespace ?? pureScopeNamespace;
10814
+ const key = JSON.stringify([
10815
+ resolvedMode,
10816
+ workspaceId,
10817
+ workspaceRoot,
10818
+ pi,
10819
+ sessionNamespace ?? null,
10820
+ authSubject ?? null
10821
+ ]);
10822
+ const existing = pureBindings.get(key);
10823
+ if (existing) return existing;
10824
+ const bindingPromise = (async () => {
10825
+ const scopedExtraTools = opts.getExtraTools ? await opts.getExtraTools({
10826
+ workspaceId,
10827
+ workspaceRoot,
10828
+ runtimeMode: resolvedMode,
10829
+ workspaceFsCapability: "none",
10830
+ authSubject
10831
+ }) : [];
10832
+ const tools = mergeTools({
10833
+ standardTools: [],
10834
+ extraTools: [...opts.extraTools ?? [], ...scopedExtraTools],
10835
+ logger: app.log
10836
+ });
10837
+ const baseHarnessFactory = opts.harnessFactory ?? ((input) => createPiCodingAgentHarness({
10838
+ ...input,
10839
+ pi
10840
+ }));
10841
+ const systemPromptDynamic = opts.getSystemPromptDynamic ? () => opts.getSystemPromptDynamic?.({ workspaceId, workspaceRoot }) : opts.systemPromptDynamic;
10842
+ const harnessFactory = ((input) => baseHarnessFactory({
10843
+ ...input,
10844
+ sessionNamespace,
10845
+ sessionRoot: opts.sessionRoot,
10846
+ sessionDir: input.sessionDir
10847
+ }));
10848
+ const coreAgent = createAgentRuntimeBridge({
10849
+ runtime: "none",
10850
+ tools,
10851
+ harnessFactory,
10852
+ systemPromptAppend: opts.systemPromptAppend,
10853
+ systemPromptDynamic,
10854
+ telemetry: opts.telemetry,
10855
+ metering: opts.metering,
10856
+ sessionStorageRoot: opts.sessionRoot
10857
+ });
10858
+ const agentRuntime = await coreAgent.getRuntime();
10859
+ return {
10860
+ agent: coreAgent.agent,
10861
+ tools,
10862
+ readyTracker: new ReadyStatusTracker({ sandboxReady: true, harnessReady: true }),
10863
+ piChatService: agentRuntime.service
10864
+ };
10865
+ })();
10866
+ pureBindings.set(key, bindingPromise);
10867
+ bindingPromise.catch(() => {
10868
+ if (pureBindings.get(key) === bindingPromise) pureBindings.delete(key);
10869
+ });
10870
+ evictPureBindings();
10871
+ return bindingPromise;
10872
+ }
10873
+ const staticBinding = requestScopedPure ? void 0 : await createPureBinding();
10874
+ opts.onWorkspaceAgentDispatcher?.({
10875
+ async resolve(ctx, options) {
10876
+ const boundCtx = normalizeWorkspaceAgentDispatcherContext(ctx);
10877
+ assertWorkspaceAgentDispatcherRequestContext(boundCtx, options?.request);
10878
+ if (staticBinding) {
10879
+ if (boundCtx.workspaceId !== sessionId) {
10880
+ throw createWorkspaceAgentDispatcherError(
10881
+ ErrorCode.enum.UNAUTHORIZED,
10882
+ "workspace agent dispatcher context does not match bound workspace",
10883
+ 401
10884
+ );
10885
+ }
10886
+ return createBoundWorkspaceAgentDispatcher(staticBinding.agent, boundCtx);
10887
+ }
10888
+ const binding = await createPureBinding(options?.request, boundCtx);
10889
+ return createBoundWorkspaceAgentDispatcher(binding.agent, boundCtx);
10890
+ }
10891
+ });
10892
+ const profile = {
10893
+ runtimeMode: resolvedMode,
10894
+ capabilities: {
10895
+ tools: staticBinding ? toolNames(staticBinding.tools) : toolNames(opts.extraTools ?? [])
10896
+ },
10897
+ sessionChangesTracker: new InMemorySessionChangesTracker(),
10898
+ health: {
10899
+ register: opts.registerHealthRoute ?? true,
10900
+ version: opts.version ?? DEFAULT_VERSION2,
10901
+ getReadiness: () => ({ sandboxReady: true, harnessReady: true })
10902
+ },
10903
+ chat: {
10904
+ ...staticBinding ? { service: staticBinding.piChatService } : { getService: async (request) => (await createPureBinding(request)).piChatService },
10905
+ defaultWorkspaceId: false
10906
+ },
10907
+ models: { filterModels: opts.filterModels },
10908
+ catalog: staticBinding ? { tools: staticBinding.tools } : { getTools: async (request) => (await createPureBinding(request)).tools },
10909
+ readyStatus: staticBinding ? { tracker: staticBinding.readyTracker } : { getTracker: async (request) => (await createPureBinding(request)).readyTracker },
10910
+ dispose: async () => {
10911
+ const bindings = await Promise.allSettled(pureBindings.values());
10912
+ await Promise.all(bindings.filter((result) => result.status === "fulfilled").map((result) => result.value.agent.dispose()));
10913
+ },
10914
+ beforeRegister: (profileApp) => {
10915
+ profileApp.addHook("onRequest", async (request, reply) => {
10916
+ const user = request.user;
10917
+ let workspaceId = DEFAULT_WORKSPACE_ID4;
10918
+ if (opts.getWorkspaceId && !isWorkspaceAgnosticAgentRequest(request, {
10919
+ readyStatusWorkspaceScoped: requestScopedPure,
10920
+ modelsWorkspaceScoped: Boolean(opts.filterModels)
10921
+ })) {
10922
+ try {
10923
+ workspaceId = (await opts.getWorkspaceId(request)).trim();
10924
+ } catch (error) {
10925
+ const statusCode = typeof error?.statusCode === "number" ? error.statusCode : 400;
10926
+ const message = statusCode >= 500 ? "workspace scope failed" : error instanceof Error ? error.message : "workspace id is required";
10927
+ return reply.code(statusCode).send({
10928
+ error: { code: ErrorCode.enum.WORKSPACE_UNINITIALIZED, message }
10929
+ });
10930
+ }
10931
+ if (workspaceId.length === 0) {
10932
+ return reply.code(400).send({
10933
+ error: {
10934
+ code: ErrorCode.enum.WORKSPACE_UNINITIALIZED,
10935
+ message: "workspace id is required"
10936
+ }
10937
+ });
10938
+ }
10939
+ }
10940
+ request.workspaceContext = { workspaceId, authenticated: !!user };
10941
+ });
10942
+ }
10943
+ };
10944
+ registerAgentCapabilitiesContributor(app, profile);
10945
+ await registerAgentRouteBindingProfile(app, profile);
10946
+ }
10520
10947
  export {
10521
10948
  FileHandleStore,
10522
10949
  MANAGED_AGENT_MCP_DELIVERY_RULE,