@wrongstack/webui-server 0.297.0 → 0.298.0

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.
Files changed (187) hide show
  1. package/dist/index.js +1238 -398
  2. package/dist/server/backend-services.d.ts +2 -0
  3. package/dist/server/codemap-cache.d.ts +1 -0
  4. package/dist/server/connection-lifecycle.d.ts +13 -0
  5. package/dist/server/connections-health-route.d.ts +7 -1
  6. package/dist/server/context-editor.d.ts +1 -2
  7. package/dist/server/entry.js +940 -385
  8. package/dist/server/frontend-static-serve.d.ts +7 -0
  9. package/dist/server/governance-runtime.d.ts +30 -0
  10. package/dist/server/http-server.d.ts +7 -0
  11. package/dist/server/mcp-handlers.d.ts +3 -1
  12. package/dist/server/memory-handlers.d.ts +3 -3
  13. package/dist/server/pending-confirms.d.ts +9 -0
  14. package/dist/server/sdd-board-ws-handler.d.ts +1 -0
  15. package/dist/server/sdd-wizard-ws-handler.d.ts +2 -0
  16. package/dist/server/specs-ws-handler.d.ts +1 -0
  17. package/dist/server/types.d.ts +3 -1
  18. package/dist/server/ws-auth.d.ts +24 -0
  19. package/dist/server/ws-payload-validation.d.ts +18 -0
  20. package/dist/server/ws-utils.d.ts +12 -1
  21. package/package.json +12 -11
  22. package/dist/index.d.ts.map +0 -1
  23. package/dist/index.js.map +0 -7
  24. package/dist/protocol/client-conversation.d.ts.map +0 -1
  25. package/dist/protocol/client-integrations.d.ts.map +0 -1
  26. package/dist/protocol/client-operations.d.ts.map +0 -1
  27. package/dist/protocol/client-workspace.d.ts.map +0 -1
  28. package/dist/protocol/connection-fsm.d.ts.map +0 -1
  29. package/dist/protocol/decoder.d.ts.map +0 -1
  30. package/dist/protocol/index.d.ts.map +0 -1
  31. package/dist/protocol/index.js.map +0 -7
  32. package/dist/protocol/projections.d.ts.map +0 -1
  33. package/dist/protocol/registry.d.ts.map +0 -1
  34. package/dist/protocol/replay-payload.d.ts.map +0 -1
  35. package/dist/protocol/server-conversation.d.ts.map +0 -1
  36. package/dist/protocol/server-integrations.d.ts.map +0 -1
  37. package/dist/protocol/server-operations.d.ts.map +0 -1
  38. package/dist/protocol/server-workspace.d.ts.map +0 -1
  39. package/dist/protocol/types.d.ts.map +0 -1
  40. package/dist/protocol/version.d.ts.map +0 -1
  41. package/dist/server/agent-roster-handlers.d.ts.map +0 -1
  42. package/dist/server/agent-roster-routes.d.ts.map +0 -1
  43. package/dist/server/autonomy-routes.d.ts.map +0 -1
  44. package/dist/server/backend-services.d.ts.map +0 -1
  45. package/dist/server/boot.d.ts.map +0 -1
  46. package/dist/server/brain-handlers.d.ts.map +0 -1
  47. package/dist/server/brain-routes.d.ts.map +0 -1
  48. package/dist/server/chronicle-routes.d.ts.map +0 -1
  49. package/dist/server/client-presence.d.ts.map +0 -1
  50. package/dist/server/client-transport-routes.d.ts.map +0 -1
  51. package/dist/server/codebase-index-server-control.d.ts.map +0 -1
  52. package/dist/server/codebase-indexing.d.ts.map +0 -1
  53. package/dist/server/codemap-cache.d.ts.map +0 -1
  54. package/dist/server/codemap-handlers.d.ts.map +0 -1
  55. package/dist/server/codemap-telemetry.d.ts.map +0 -1
  56. package/dist/server/collaboration-ws-handler.d.ts.map +0 -1
  57. package/dist/server/completion-handlers.d.ts.map +0 -1
  58. package/dist/server/completion-routes.d.ts.map +0 -1
  59. package/dist/server/config-doctor.d.ts.map +0 -1
  60. package/dist/server/connection-handler.d.ts.map +0 -1
  61. package/dist/server/connection-lifecycle.d.ts.map +0 -1
  62. package/dist/server/connections-health-route.d.ts.map +0 -1
  63. package/dist/server/content-routes.d.ts.map +0 -1
  64. package/dist/server/context-editor.d.ts.map +0 -1
  65. package/dist/server/context-meta.d.ts.map +0 -1
  66. package/dist/server/conversation-operations.d.ts.map +0 -1
  67. package/dist/server/conversation-routes.d.ts.map +0 -1
  68. package/dist/server/custom-context-modes.d.ts.map +0 -1
  69. package/dist/server/deadcode-handlers.d.ts.map +0 -1
  70. package/dist/server/design-handlers.d.ts.map +0 -1
  71. package/dist/server/discover-mailbox-bridge.d.ts.map +0 -1
  72. package/dist/server/embedded-host-adapters.d.ts.map +0 -1
  73. package/dist/server/embedded-lifecycle.d.ts.map +0 -1
  74. package/dist/server/embedded-message-router.d.ts.map +0 -1
  75. package/dist/server/entry.d.ts.map +0 -1
  76. package/dist/server/entry.js.map +0 -7
  77. package/dist/server/eternal-iteration-broadcast.d.ts.map +0 -1
  78. package/dist/server/file-handlers.d.ts.map +0 -1
  79. package/dist/server/file-picker.d.ts.map +0 -1
  80. package/dist/server/frontend-static-serve.d.ts.map +0 -1
  81. package/dist/server/git-handlers.d.ts.map +0 -1
  82. package/dist/server/git-process.d.ts.map +0 -1
  83. package/dist/server/goal-handlers.d.ts.map +0 -1
  84. package/dist/server/goal-routes.d.ts.map +0 -1
  85. package/dist/server/goal-snapshot-routes.d.ts.map +0 -1
  86. package/dist/server/goal-ws-handler.d.ts.map +0 -1
  87. package/dist/server/handlers/index.d.ts.map +0 -1
  88. package/dist/server/handlers/worklist-handlers.d.ts.map +0 -1
  89. package/dist/server/handlers.js.map +0 -7
  90. package/dist/server/host-routes.d.ts.map +0 -1
  91. package/dist/server/http-server/analytics-handler.d.ts.map +0 -1
  92. package/dist/server/http-server/api-handlers.d.ts.map +0 -1
  93. package/dist/server/http-server.d.ts.map +0 -1
  94. package/dist/server/index.d.ts.map +0 -1
  95. package/dist/server/instance-registry.d.ts.map +0 -1
  96. package/dist/server/introspection-routes.d.ts.map +0 -1
  97. package/dist/server/kanban-board-watcher.d.ts.map +0 -1
  98. package/dist/server/kanban-daemon-subscriber.d.ts.map +0 -1
  99. package/dist/server/kanban-decomposition-routes.d.ts.map +0 -1
  100. package/dist/server/kanban-dispatch.d.ts.map +0 -1
  101. package/dist/server/kanban-host-routes.d.ts.map +0 -1
  102. package/dist/server/kanban-route-helpers.d.ts.map +0 -1
  103. package/dist/server/kanban-route-pagination.d.ts.map +0 -1
  104. package/dist/server/kanban-route-protocol.d.ts.map +0 -1
  105. package/dist/server/kanban-routes.d.ts.map +0 -1
  106. package/dist/server/kanban-run-mirror.d.ts.map +0 -1
  107. package/dist/server/kanban-supervisor.d.ts.map +0 -1
  108. package/dist/server/kanban-task-routes.d.ts.map +0 -1
  109. package/dist/server/lifecycle.d.ts.map +0 -1
  110. package/dist/server/mailbox-handlers.d.ts.map +0 -1
  111. package/dist/server/mailbox-routes.d.ts.map +0 -1
  112. package/dist/server/mcp-handlers.d.ts.map +0 -1
  113. package/dist/server/mcp-routes.d.ts.map +0 -1
  114. package/dist/server/memory-diagnostics.d.ts.map +0 -1
  115. package/dist/server/memory-handlers.d.ts.map +0 -1
  116. package/dist/server/memory-routes.d.ts.map +0 -1
  117. package/dist/server/message-dispatcher.d.ts.map +0 -1
  118. package/dist/server/mode-handlers.d.ts.map +0 -1
  119. package/dist/server/mode-operations.d.ts.map +0 -1
  120. package/dist/server/mode-routes.d.ts.map +0 -1
  121. package/dist/server/model-auto-discovery.d.ts.map +0 -1
  122. package/dist/server/model-catalog-search.d.ts.map +0 -1
  123. package/dist/server/model-catalog.d.ts.map +0 -1
  124. package/dist/server/model-operations.d.ts.map +0 -1
  125. package/dist/server/network-info.d.ts.map +0 -1
  126. package/dist/server/open-browser.d.ts.map +0 -1
  127. package/dist/server/path-containment.d.ts.map +0 -1
  128. package/dist/server/pending-confirms.d.ts.map +0 -1
  129. package/dist/server/port-utils.d.ts.map +0 -1
  130. package/dist/server/pre-context-services.d.ts.map +0 -1
  131. package/dist/server/pref-helpers.d.ts.map +0 -1
  132. package/dist/server/prefs-handlers.d.ts.map +0 -1
  133. package/dist/server/prefs-routes.d.ts.map +0 -1
  134. package/dist/server/privileged-actions.d.ts.map +0 -1
  135. package/dist/server/process-handlers.d.ts.map +0 -1
  136. package/dist/server/process-routes.d.ts.map +0 -1
  137. package/dist/server/project-handlers.d.ts.map +0 -1
  138. package/dist/server/project-routes.d.ts.map +0 -1
  139. package/dist/server/projects-manifest.d.ts.map +0 -1
  140. package/dist/server/prompts-handlers.d.ts.map +0 -1
  141. package/dist/server/provider-config-io.d.ts.map +0 -1
  142. package/dist/server/provider-config-standalone.d.ts.map +0 -1
  143. package/dist/server/provider-handlers.d.ts.map +0 -1
  144. package/dist/server/provider-keys.d.ts.map +0 -1
  145. package/dist/server/provider-routes.d.ts.map +0 -1
  146. package/dist/server/provider-store.d.ts.map +0 -1
  147. package/dist/server/route-family-dispatcher.d.ts.map +0 -1
  148. package/dist/server/routes.d.ts.map +0 -1
  149. package/dist/server/sdd-board-routes.d.ts.map +0 -1
  150. package/dist/server/sdd-board-ws-handler.d.ts.map +0 -1
  151. package/dist/server/sdd-wizard-routes.d.ts.map +0 -1
  152. package/dist/server/sdd-wizard-wiring.d.ts.map +0 -1
  153. package/dist/server/sdd-wizard-ws-handler.d.ts.map +0 -1
  154. package/dist/server/server-runtime.d.ts.map +0 -1
  155. package/dist/server/session-handlers.d.ts.map +0 -1
  156. package/dist/server/session-history.d.ts.map +0 -1
  157. package/dist/server/session-routes.d.ts.map +0 -1
  158. package/dist/server/setup-event-projection.d.ts.map +0 -1
  159. package/dist/server/setup-events-core-watchers.d.ts.map +0 -1
  160. package/dist/server/setup-events-fleet-broadcaster.d.ts.map +0 -1
  161. package/dist/server/setup-events-provider-handlers.d.ts.map +0 -1
  162. package/dist/server/setup-events-session-helpers.d.ts.map +0 -1
  163. package/dist/server/setup-events-status-watcher.d.ts.map +0 -1
  164. package/dist/server/setup-events-watcher.d.ts.map +0 -1
  165. package/dist/server/setup-events.d.ts.map +0 -1
  166. package/dist/server/setup-screen.d.ts.map +0 -1
  167. package/dist/server/shell-git-routes.d.ts.map +0 -1
  168. package/dist/server/shell-open.d.ts.map +0 -1
  169. package/dist/server/skills-handlers.d.ts.map +0 -1
  170. package/dist/server/specs-routes.d.ts.map +0 -1
  171. package/dist/server/specs-ws-handler.d.ts.map +0 -1
  172. package/dist/server/standalone-session-identity.d.ts.map +0 -1
  173. package/dist/server/start-webui.d.ts.map +0 -1
  174. package/dist/server/stream-coalescer.d.ts.map +0 -1
  175. package/dist/server/techstack-handlers.d.ts.map +0 -1
  176. package/dist/server/terminal-ws-handler.d.ts.map +0 -1
  177. package/dist/server/token-estimator.d.ts.map +0 -1
  178. package/dist/server/types.d.ts.map +0 -1
  179. package/dist/server/usage-cost.d.ts.map +0 -1
  180. package/dist/server/worklist-routes.d.ts.map +0 -1
  181. package/dist/server/worktree-routes.d.ts.map +0 -1
  182. package/dist/server/worktree-ws-handler.d.ts.map +0 -1
  183. package/dist/server/ws-auth.d.ts.map +0 -1
  184. package/dist/server/ws-payload-preferences.d.ts.map +0 -1
  185. package/dist/server/ws-payload-validation.d.ts.map +0 -1
  186. package/dist/server/ws-utils.d.ts.map +0 -1
  187. package/dist/server/zip.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ function isDestructivePendingConfirm(confirm) {
8
8
  function resolveYoloEligiblePendingConfirms(pendingConfirms) {
9
9
  for (const [id, confirm] of pendingConfirms) {
10
10
  if (confirm.boundaryReason) continue;
11
+ if (isDestructivePendingConfirm(confirm)) continue;
11
12
  pendingConfirms.delete(id);
12
13
  confirm.resolve("yes");
13
14
  }
@@ -71,7 +72,18 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
71
72
  "showModelReasoning",
72
73
  // Display-only toggles (purely visual, persisted in localStorage via Zustand).
73
74
  "groupToolCalls",
74
- "showThinkingLogs"
75
+ "showThinkingLogs",
76
+ // v11 Display parity: agent-swarm panel + inverse fsAccess flag.
77
+ "showAgentSwarmPanel",
78
+ "allowOutsideProjectRoot",
79
+ // v13 Display parity (TUI SettingsPicker fields 42 & 43): the read tool
80
+ // includes codebase-index symbols, and SAGE memory-inject blocks are
81
+ // surfaced in tool results. Both are pure WebUI-display toggles mirrored
82
+ // against the TUI's settings picker; the server mirrors them through
83
+ // `prefs.update` exactly so the panel does not error with
84
+ // "unknown preference key".
85
+ "readSymbols",
86
+ "showSageMemoryInject"
75
87
  ]);
76
88
  var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set([
77
89
  "fallbackModels",
@@ -92,14 +104,39 @@ var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
92
104
  "maxIterations",
93
105
  "maxConcurrent",
94
106
  "enhanceDelayMs",
107
+ "enhanceCountdownMs",
95
108
  "tgLongToolMs",
96
109
  "breakerAutoKillResetMs",
97
110
  // Chimera + auto-review numeric knobs
98
111
  "chimeraMaxFiles",
99
112
  "autoReviewDebounceMs",
100
113
  "autoReviewMaxFilesPerBatch",
101
- "autoReviewMaxConcurrentReviews"
114
+ "autoReviewMaxConcurrentReviews",
115
+ // v13 Display parity (TUI SettingsPicker fields 41, 44, 21): the
116
+ // pre-refine grace countdown (seconds), the SAGE memory-inject relation
117
+ // floor (0..1), and the minimum file count for the multi-diff summary
118
+ // footer. All three are driven by the WebUI sliders in DisplaySection
119
+ // and previously tripped the "unknown preference key" rejection.
120
+ "preRefineSeconds",
121
+ "sageMemoryInjectThreshold",
122
+ "multiDiffSummaryThreshold"
102
123
  ]);
124
+ var NUMBER_PREF_BOUNDS = {
125
+ // Iteration / concurrency — must be at least 1 to make progress.
126
+ maxIterations: { min: 1, max: Number.POSITIVE_INFINITY },
127
+ maxConcurrent: { min: 1, max: Number.POSITIVE_INFINITY },
128
+ autoProceedMaxIterations: { min: 1, max: Number.POSITIVE_INFINITY },
129
+ chimeraMaxFiles: { min: 1, max: Number.POSITIVE_INFINITY },
130
+ autoReviewMaxFilesPerBatch: { min: 1, max: Number.POSITIVE_INFINITY },
131
+ autoReviewMaxConcurrentReviews: { min: 1, max: Number.POSITIVE_INFINITY },
132
+ // Counts (must be >= 1 to render a footer at all).
133
+ multiDiffSummaryThreshold: { min: 1, max: Number.POSITIVE_INFINITY },
134
+ preRefineSeconds: { min: 0, max: Number.POSITIVE_INFINITY },
135
+ // Probability — must lie in [0, 1].
136
+ sageMemoryInjectThreshold: { min: 0, max: 1 },
137
+ // Debounce / delay — non-negative ms.
138
+ autoReviewDebounceMs: { min: 0, max: Number.POSITIVE_INFINITY }
139
+ };
103
140
  var STRING_PREF_KEYS = /* @__PURE__ */ new Set([
104
141
  "hqUrl",
105
142
  "hqToken",
@@ -221,7 +258,15 @@ function validatePreferenceValue(key, value) {
221
258
  return typeof value === "boolean" ? null : `prefs.update payload.${key} must be a boolean`;
222
259
  }
223
260
  if (NUMBER_PREF_KEYS.has(key)) {
224
- return typeof value === "number" && Number.isFinite(value) ? null : `prefs.update payload.${key} must be a finite number`;
261
+ if (typeof value !== "number" || !Number.isFinite(value)) {
262
+ return `prefs.update payload.${key} must be a finite number`;
263
+ }
264
+ const bounds = NUMBER_PREF_BOUNDS[key];
265
+ if (bounds && (value < bounds.min || value > bounds.max)) {
266
+ const maxStr = bounds.max === Number.POSITIVE_INFINITY ? "\u221E" : String(bounds.max);
267
+ return `prefs.update payload.${key} must be in [${bounds.min}, ${maxStr}]`;
268
+ }
269
+ return null;
225
270
  }
226
271
  if (STRING_PREF_KEYS.has(key)) {
227
272
  return typeof value === "string" ? null : `prefs.update payload.${key} must be a string`;
@@ -245,9 +290,16 @@ function validatePreferenceValue(key, value) {
245
290
  return null;
246
291
  }
247
292
  if (STRING_ARRAY_RECORD_PREF_KEYS.has(key)) {
248
- return isRecord(value) && Object.values(value).every(
293
+ if (!isRecord(value) || !Object.values(value).every(
249
294
  (v) => Array.isArray(v) && v.every((item) => typeof item === "string")
250
- ) ? null : `prefs.update payload.${key} must be an object of string arrays`;
295
+ )) {
296
+ return `prefs.update payload.${key} must be an object of string arrays`;
297
+ }
298
+ const badKey = Object.keys(value).find((k) => FORBIDDEN_PROTO_KEYS.has(k));
299
+ if (badKey) {
300
+ return `prefs.update payload.${key} contains a forbidden key: ${badKey}`;
301
+ }
302
+ return null;
251
303
  }
252
304
  if (BOOLEAN_RECORD_PREF_KEYS.has(key)) {
253
305
  if (!isRecord(value) || !Object.values(value).every((v) => typeof v === "boolean")) {
@@ -261,6 +313,10 @@ function validatePreferenceValue(key, value) {
261
313
  }
262
314
  if (MODEL_MATRIX_PREF_KEYS.has(key)) {
263
315
  if (!isRecord(value)) return `prefs.update payload.${key} must be an object`;
316
+ const badMatrixKey = Object.keys(value).find((k) => FORBIDDEN_PROTO_KEYS.has(k));
317
+ if (badMatrixKey) {
318
+ return `prefs.update payload.${key} contains a forbidden key: ${badMatrixKey}`;
319
+ }
264
320
  for (const entry of Object.values(value)) {
265
321
  if (!isRecord(entry)) return `prefs.update payload.${key} entries must be objects`;
266
322
  const provider = entry["provider"];
@@ -862,6 +918,58 @@ function validateProjectsSelectPayload(payload) {
862
918
  value: { root, ...typeof name2 === "string" ? { name: name2 } : {} }
863
919
  };
864
920
  }
921
+ var MCP_MAX_STRING = 4096;
922
+ var MCP_MAX_ARRAY = 256;
923
+ function isStringArray(value, max = MCP_MAX_ARRAY) {
924
+ return Array.isArray(value) && value.length <= max && value.every((item) => typeof item === "string" && item.length <= MCP_MAX_STRING);
925
+ }
926
+ function isStringRecord(value) {
927
+ if (!isRecord2(value)) return false;
928
+ const entries = Object.entries(value);
929
+ if (entries.length > MCP_MAX_ARRAY) return false;
930
+ return entries.every(
931
+ ([key, item]) => typeof key === "string" && key.length <= MCP_MAX_STRING && typeof item === "string" && item.length <= MCP_MAX_STRING
932
+ );
933
+ }
934
+ function validateMcpServerPayload(payload, label) {
935
+ if (!isRecord2(payload)) {
936
+ return { ok: false, message: `${label} payload must be an object` };
937
+ }
938
+ const name2 = payload["name"];
939
+ if (typeof name2 !== "string" || name2.trim().length === 0 || name2.length > MCP_MAX_STRING) {
940
+ return { ok: false, message: `${label} payload.name must be a non-empty string` };
941
+ }
942
+ for (const key of ["description", "command", "url", "transport", "permission"]) {
943
+ const value = payload[key];
944
+ if (value !== void 0 && (typeof value !== "string" || value.length > MCP_MAX_STRING)) {
945
+ if (!(key === "transport" && isRecord2(value))) {
946
+ return { ok: false, message: `${label} payload.${key} must be a string` };
947
+ }
948
+ }
949
+ }
950
+ for (const key of ["enabled", "lazy"]) {
951
+ const value = payload[key];
952
+ if (value !== void 0 && typeof value !== "boolean") {
953
+ return { ok: false, message: `${label} payload.${key} must be a boolean` };
954
+ }
955
+ }
956
+ for (const key of ["args", "allowedTools", "passthroughEnv"]) {
957
+ const value = payload[key];
958
+ if (value !== void 0 && !isStringArray(value)) {
959
+ return { ok: false, message: `${label} payload.${key} must be an array of strings` };
960
+ }
961
+ }
962
+ for (const key of ["env", "headers"]) {
963
+ const value = payload[key];
964
+ if (value !== void 0 && !isStringRecord(value)) {
965
+ return {
966
+ ok: false,
967
+ message: `${label} payload.${key} must be an object of string values`
968
+ };
969
+ }
970
+ }
971
+ return { ok: true, value: payload };
972
+ }
865
973
 
866
974
  // src/server/prefs-handlers.ts
867
975
  function sendResult(ctx, ws, success, message) {
@@ -898,6 +1006,9 @@ function handlePrefsUpdate(ctx, ws, input) {
898
1006
  const payload = parsed.value.prefs;
899
1007
  for (const [key, value] of Object.entries(payload)) ctx.meta[key] = value;
900
1008
  void ctx.persist(payload);
1009
+ if (typeof payload["autonomy"] === "string") {
1010
+ ctx.setAutonomy?.(payload["autonomy"]);
1011
+ }
901
1012
  if (typeof payload["yolo"] === "boolean") {
902
1013
  ctx.setYolo?.(payload["yolo"]);
903
1014
  if (payload["yolo"]) resolveYoloEligiblePendingConfirms(ctx.pendingConfirms);
@@ -934,6 +1045,7 @@ function handleAutonomySwitch(ctx, ws, mode) {
934
1045
 
935
1046
  // src/server/ws-utils.ts
936
1047
  import { randomBytes } from "node:crypto";
1048
+ import { scrubErrorDetail } from "@wrongstack/core/security";
937
1049
  import { WebSocket } from "ws";
938
1050
  var WEBUI_WS_MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
939
1051
  function sendSerialized(ws, data) {
@@ -971,7 +1083,7 @@ function sendResult2(ws, success, message) {
971
1083
  send(ws, { type: "key.operation_result", payload: { success, message } });
972
1084
  }
973
1085
  function errMessage(err) {
974
- return err instanceof Error ? err.message : String(err);
1086
+ return scrubErrorDetail(err);
975
1087
  }
976
1088
  function generateAuthToken() {
977
1089
  return randomBytes(16).toString("hex");
@@ -1287,7 +1399,16 @@ import * as path from "node:path";
1287
1399
  import { resolveIndexDir } from "@wrongstack/tools/codebase-index/index";
1288
1400
  var DB_FILE = "index.db";
1289
1401
  var MAX_CACHE_ENTRIES = 128;
1402
+ var MAX_CACHE_BODY_CHARS = 4 * 1024 * 1024;
1403
+ var MAX_CACHE_TOTAL_CHARS = 16 * 1024 * 1024;
1290
1404
  var cache = /* @__PURE__ */ new Map();
1405
+ var cacheChars = 0;
1406
+ function deleteCachedCodemapBody(key) {
1407
+ const entry = cache.get(key);
1408
+ if (!entry) return false;
1409
+ cacheChars = Math.max(0, cacheChars - entry.body.length);
1410
+ return cache.delete(key);
1411
+ }
1291
1412
  function indexDbVersion(projectRoot, indexDir) {
1292
1413
  try {
1293
1414
  const dir = resolveIndexDir(projectRoot, indexDir);
@@ -1304,7 +1425,7 @@ function getCachedCodemapBody(key, version) {
1304
1425
  const entry = cache.get(key);
1305
1426
  if (!entry) return void 0;
1306
1427
  if (entry.version !== version) {
1307
- cache.delete(key);
1428
+ deleteCachedCodemapBody(key);
1308
1429
  return void 0;
1309
1430
  }
1310
1431
  cache.delete(key);
@@ -1312,16 +1433,19 @@ function getCachedCodemapBody(key, version) {
1312
1433
  return entry.body;
1313
1434
  }
1314
1435
  function setCachedCodemapBody(key, version, body) {
1315
- if (cache.has(key)) cache.delete(key);
1436
+ if (cache.has(key)) deleteCachedCodemapBody(key);
1437
+ if (body.length > MAX_CACHE_BODY_CHARS) return;
1316
1438
  cache.set(key, { version, body });
1317
- while (cache.size > MAX_CACHE_ENTRIES) {
1439
+ cacheChars += body.length;
1440
+ while (cache.size > MAX_CACHE_ENTRIES || cacheChars > MAX_CACHE_TOTAL_CHARS) {
1318
1441
  const oldest = cache.keys().next().value;
1319
1442
  if (oldest === void 0) break;
1320
- cache.delete(oldest);
1443
+ deleteCachedCodemapBody(oldest);
1321
1444
  }
1322
1445
  }
1323
1446
  function clearCodemapGraphCache() {
1324
1447
  cache.clear();
1448
+ cacheChars = 0;
1325
1449
  }
1326
1450
 
1327
1451
  // src/server/codebase-indexing.ts
@@ -1612,6 +1736,7 @@ var CollaborationWebSocketHandler = class {
1612
1736
  this.offs.length = 0;
1613
1737
  this.stopBroadcast();
1614
1738
  this.clients.clear();
1739
+ this.bySession.clear();
1615
1740
  }
1616
1741
  // ── Inbound client messages ────────────────────────────────────────────
1617
1742
  /**
@@ -2815,6 +2940,7 @@ import {
2815
2940
  materializeTokens,
2816
2941
  recordKitChoice,
2817
2942
  recordOverrides,
2943
+ resolveMaterializeTarget,
2818
2944
  resolveSemanticTune,
2819
2945
  runDesignVerify,
2820
2946
  setActiveKit,
@@ -3026,7 +3152,7 @@ async function handleDesignMaterialize(ws, ctx, msg) {
3026
3152
  kitId: active.kit,
3027
3153
  outPath: typeof payload.out === "string" ? payload.out : void 0
3028
3154
  });
3029
- const abs = path5.join(ctx.projectRoot, result.path);
3155
+ const abs = await resolveMaterializeTarget(result.path, ctx.projectRoot);
3030
3156
  await fs3.mkdir(path5.dirname(abs), { recursive: true });
3031
3157
  await fs3.writeFile(abs, result.content);
3032
3158
  send(ws, {
@@ -3179,7 +3305,29 @@ async function resolveFileInsideProject(projectRoot, filePath) {
3179
3305
  if (!isPathInside(realProjectRoot, realFull)) {
3180
3306
  throw new ToolValidationError2({ message: "Path outside project root", field: "path" });
3181
3307
  }
3182
- return realFull;
3308
+ const realFinal = await canonicalizeFinalComponent(realFull);
3309
+ if (!isPathInside(realProjectRoot, realFinal)) {
3310
+ throw new ToolValidationError2({ message: "Path outside project root", field: "path" });
3311
+ }
3312
+ return realFinal;
3313
+ }
3314
+ async function canonicalizeFinalComponent(p) {
3315
+ let stat3;
3316
+ try {
3317
+ stat3 = await fs5.lstat(p);
3318
+ } catch (err) {
3319
+ if (err.code === "ENOENT") return p;
3320
+ throw err;
3321
+ }
3322
+ if (!stat3.isSymbolicLink()) return p;
3323
+ try {
3324
+ return await fs5.realpath(p);
3325
+ } catch (err) {
3326
+ if (err.code === "ENOENT") {
3327
+ throw new ToolValidationError2({ message: "Path outside project root", field: "path" });
3328
+ }
3329
+ throw err;
3330
+ }
3183
3331
  }
3184
3332
  function splitParentAndBase(p) {
3185
3333
  const base = path7.basename(p);
@@ -3683,10 +3831,11 @@ function readZipEntries(archive) {
3683
3831
  const dataStart = nameStart + nameLength + extraLength;
3684
3832
  const dataEnd = dataStart + compressedSize;
3685
3833
  if (dataEnd > buffer.length) throw new Error("Truncated ZIP entry.");
3686
- const name2 = buffer.subarray(nameStart, nameStart + nameLength).toString("utf8");
3687
- if (!isSafeZipPath(name2)) throw new Error(`Unsafe ZIP entry path: ${name2}`);
3834
+ const rawName = buffer.subarray(nameStart, nameStart + nameLength).toString("utf8");
3835
+ const name2 = normalizeZipPath(rawName);
3688
3836
  const compressed = buffer.subarray(dataStart, dataEnd);
3689
- const data = method === 0 ? Buffer.from(compressed) : method === 8 ? inflateRawSync(compressed) : void 0;
3837
+ const inflateLimit = Math.min(expectedSize, maxEntryBytes) + 1;
3838
+ const data = method === 0 ? Buffer.from(compressed) : method === 8 ? inflateRawSync(compressed, { maxOutputLength: inflateLimit }) : void 0;
3690
3839
  if (!data) throw new Error(`Unsupported ZIP compression method: ${method}`);
3691
3840
  if (data.length !== expectedSize) throw new Error(`ZIP size mismatch for ${name2}`);
3692
3841
  if (crc32(data) !== buffer.readUInt32LE(offset + 14)) throw new Error(`ZIP CRC mismatch for ${name2}`);
@@ -4254,13 +4403,12 @@ function createConversationOperations(ctx) {
4254
4403
  },
4255
4404
  ping: (ws) => ctx.send(ws, { type: "pong", payload: {} }),
4256
4405
  confirmTool: (_ws, msg) => {
4257
- const requested = requestedSessionId(msg);
4258
- const current2 = ctx.getSessionId();
4259
- if (requested && current2 && requested !== current2) return;
4260
4406
  const { id, decision } = msg.payload ?? {};
4261
4407
  if (typeof id !== "string") return;
4262
4408
  const confirm = ctx.pendingConfirms.get(id);
4263
4409
  if (!confirm) return;
4410
+ const claimedSession = requestedSessionId(msg) ?? ctx.getSessionId();
4411
+ if (confirm.sessionId !== void 0 && confirm.sessionId !== claimedSession) return;
4264
4412
  ctx.pendingConfirms.delete(id);
4265
4413
  confirm.resolve(decision);
4266
4414
  }
@@ -4269,7 +4417,17 @@ function createConversationOperations(ctx) {
4269
4417
 
4270
4418
  // src/server/context-editor.ts
4271
4419
  import { createHash } from "node:crypto";
4272
- import { repairToolUseAdjacency } from "@wrongstack/core/utils";
4420
+ import net from "node:net";
4421
+ import {
4422
+ ALLOWED_IMAGE_MEDIA_TYPES,
4423
+ base64DecodedBytes,
4424
+ isAllowedImageMediaType,
4425
+ isPrivateIPv4,
4426
+ isPrivateIPv6,
4427
+ isValidImageBase64,
4428
+ MAX_INCOMING_IMAGE_BYTES,
4429
+ repairToolUseAdjacency
4430
+ } from "@wrongstack/core/utils";
4273
4431
 
4274
4432
  // src/server/token-estimator.ts
4275
4433
  function estimateTokens(s) {
@@ -4333,6 +4491,40 @@ var REVISION_PREFIX = "wrongstack-context-editor-v1\0";
4333
4491
  var MAX_MESSAGE_COUNT_GROWTH = 10;
4334
4492
  var MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
4335
4493
  var MAX_STRING_LENGTH = 8 * 1024 * 1024;
4494
+ var MAX_IMAGE_URL_LENGTH = 2048;
4495
+ function imageUrlRejectionReason(url) {
4496
+ if (url.length > MAX_IMAGE_URL_LENGTH) {
4497
+ return `image.source.url exceeds ${MAX_IMAGE_URL_LENGTH} characters.`;
4498
+ }
4499
+ let parsed;
4500
+ try {
4501
+ parsed = new URL(url);
4502
+ } catch {
4503
+ return "image.source.url must be an absolute URL.";
4504
+ }
4505
+ if (parsed.protocol !== "https:") {
4506
+ return `image.source.url must use https (got "${parsed.protocol}").`;
4507
+ }
4508
+ if (parsed.username !== "" || parsed.password !== "") {
4509
+ return "image.source.url must not embed credentials.";
4510
+ }
4511
+ const host = parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]") ? parsed.hostname.slice(1, -1) : parsed.hostname;
4512
+ const bareHost = host.endsWith(".") ? host.slice(0, -1) : host;
4513
+ if (bareHost === "") {
4514
+ return "image.source.url must include a hostname.";
4515
+ }
4516
+ if (bareHost === "localhost" || bareHost.endsWith(".localhost")) {
4517
+ return "image.source.url must not target localhost.";
4518
+ }
4519
+ const family = net.isIP(bareHost);
4520
+ if (family === 4 && isPrivateIPv4(bareHost)) {
4521
+ return `image.source.url must not target a private or loopback address ("${bareHost}").`;
4522
+ }
4523
+ if (family === 6 && isPrivateIPv6(bareHost)) {
4524
+ return `image.source.url must not target a private or loopback address ("${bareHost}").`;
4525
+ }
4526
+ return void 0;
4527
+ }
4336
4528
  function isRecord3(value) {
4337
4529
  return value !== null && typeof value === "object" && !Array.isArray(value);
4338
4530
  }
@@ -4405,7 +4597,11 @@ function validateBlock(value, path35, errors) {
4405
4597
  error(errors, `${path35}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
4406
4598
  return void 0;
4407
4599
  }
4408
- const cacheControl = validateCacheControl(value["cache_control"], `${path35}/cache_control`, errors);
4600
+ const cacheControl = validateCacheControl(
4601
+ value["cache_control"],
4602
+ `${path35}/cache_control`,
4603
+ errors
4604
+ );
4409
4605
  return cacheControl ? { type: "text", text: text2, cache_control: cacheControl } : { type: "text", text: text2 };
4410
4606
  }
4411
4607
  case "tool_use": {
@@ -4413,15 +4609,29 @@ function validateBlock(value, path35, errors) {
4413
4609
  const name2 = value["name"];
4414
4610
  const input = value["input"];
4415
4611
  if (typeof id !== "string" || id.length === 0) {
4416
- error(errors, `${path35}/id`, "INVALID_TOOL_USE_ID", "tool_use.id must be a non-empty string.");
4612
+ error(
4613
+ errors,
4614
+ `${path35}/id`,
4615
+ "INVALID_TOOL_USE_ID",
4616
+ "tool_use.id must be a non-empty string."
4617
+ );
4417
4618
  }
4418
4619
  if (typeof name2 !== "string" || name2.length === 0) {
4419
- error(errors, `${path35}/name`, "INVALID_TOOL_NAME", "tool_use.name must be a non-empty string.");
4620
+ error(
4621
+ errors,
4622
+ `${path35}/name`,
4623
+ "INVALID_TOOL_NAME",
4624
+ "tool_use.name must be a non-empty string."
4625
+ );
4420
4626
  }
4421
4627
  if (!isPlainJsonObject(input)) {
4422
4628
  error(errors, `${path35}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
4423
4629
  }
4424
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path35}/providerMeta`, errors);
4630
+ const providerMeta = validateProviderMeta(
4631
+ value["providerMeta"],
4632
+ `${path35}/providerMeta`,
4633
+ errors
4634
+ );
4425
4635
  if (typeof id !== "string" || id.length === 0 || typeof name2 !== "string" || name2.length === 0 || !isPlainJsonObject(input)) {
4426
4636
  return void 0;
4427
4637
  }
@@ -4433,20 +4643,46 @@ function validateBlock(value, path35, errors) {
4433
4643
  const content = value["content"];
4434
4644
  const isError = value["is_error"];
4435
4645
  if (typeof toolUseId !== "string" || toolUseId.length === 0) {
4436
- error(errors, `${path35}/tool_use_id`, "INVALID_TOOL_RESULT_ID", "tool_result.tool_use_id must be a non-empty string.");
4646
+ error(
4647
+ errors,
4648
+ `${path35}/tool_use_id`,
4649
+ "INVALID_TOOL_RESULT_ID",
4650
+ "tool_result.tool_use_id must be a non-empty string."
4651
+ );
4437
4652
  }
4438
4653
  if (name2 !== void 0 && typeof name2 !== "string") {
4439
- error(errors, `${path35}/name`, "INVALID_TOOL_RESULT_NAME", "tool_result.name must be a string.");
4654
+ error(
4655
+ errors,
4656
+ `${path35}/name`,
4657
+ "INVALID_TOOL_RESULT_NAME",
4658
+ "tool_result.name must be a string."
4659
+ );
4440
4660
  }
4441
4661
  if (typeof content !== "string") {
4442
- error(errors, `${path35}/content`, "INVALID_TOOL_RESULT_CONTENT", "tool_result.content must be a string.");
4662
+ error(
4663
+ errors,
4664
+ `${path35}/content`,
4665
+ "INVALID_TOOL_RESULT_CONTENT",
4666
+ "tool_result.content must be a string."
4667
+ );
4443
4668
  } else if (content.length > MAX_STRING_LENGTH) {
4444
- error(errors, `${path35}/content`, "TOOL_RESULT_TOO_LARGE", "tool_result.content is too large.");
4669
+ error(
4670
+ errors,
4671
+ `${path35}/content`,
4672
+ "TOOL_RESULT_TOO_LARGE",
4673
+ "tool_result.content is too large."
4674
+ );
4445
4675
  }
4446
4676
  if (isError !== void 0 && typeof isError !== "boolean") {
4447
- error(errors, `${path35}/is_error`, "INVALID_TOOL_RESULT_ERROR", "tool_result.is_error must be boolean.");
4677
+ error(
4678
+ errors,
4679
+ `${path35}/is_error`,
4680
+ "INVALID_TOOL_RESULT_ERROR",
4681
+ "tool_result.is_error must be boolean."
4682
+ );
4448
4683
  }
4449
- if (typeof toolUseId !== "string" || toolUseId.length === 0 || typeof content !== "string") return void 0;
4684
+ if (typeof toolUseId !== "string" || toolUseId.length === 0 || typeof content !== "string")
4685
+ return void 0;
4450
4686
  return {
4451
4687
  type: "tool_result",
4452
4688
  tool_use_id: toolUseId,
@@ -4463,28 +4699,92 @@ function validateBlock(value, path35, errors) {
4463
4699
  }
4464
4700
  const sourceType = source["type"];
4465
4701
  if (sourceType !== "base64" && sourceType !== "url") {
4466
- error(errors, `${path35}/source/type`, "INVALID_IMAGE_SOURCE_TYPE", "image.source.type must be base64 or url.");
4702
+ error(
4703
+ errors,
4704
+ `${path35}/source/type`,
4705
+ "INVALID_IMAGE_SOURCE_TYPE",
4706
+ "image.source.type must be base64 or url."
4707
+ );
4467
4708
  return void 0;
4468
4709
  }
4469
4710
  const mediaType = source["media_type"];
4470
4711
  const data = source["data"];
4471
4712
  const url = source["url"];
4472
4713
  if (mediaType !== void 0 && typeof mediaType !== "string") {
4473
- error(errors, `${path35}/source/media_type`, "INVALID_IMAGE_MEDIA_TYPE", "image.source.media_type must be a string.");
4714
+ error(
4715
+ errors,
4716
+ `${path35}/source/media_type`,
4717
+ "INVALID_IMAGE_MEDIA_TYPE",
4718
+ "image.source.media_type must be a string."
4719
+ );
4720
+ return void 0;
4721
+ }
4722
+ if (typeof mediaType === "string" && !isAllowedImageMediaType(mediaType)) {
4723
+ error(
4724
+ errors,
4725
+ `${path35}/source/media_type`,
4726
+ "UNSUPPORTED_IMAGE_MEDIA_TYPE",
4727
+ `image.source.media_type must be one of: ${[...ALLOWED_IMAGE_MEDIA_TYPES].join(", ")}.`
4728
+ );
4729
+ return void 0;
4474
4730
  }
4475
- if (data !== void 0 && typeof data !== "string") {
4476
- error(errors, `${path35}/source/data`, "INVALID_IMAGE_DATA", "image.source.data must be a string.");
4731
+ if (sourceType === "base64") {
4732
+ if (typeof data !== "string" || data.length === 0) {
4733
+ error(
4734
+ errors,
4735
+ `${path35}/source/data`,
4736
+ "INVALID_IMAGE_DATA",
4737
+ "image.source.data must be a non-empty string for a base64 source."
4738
+ );
4739
+ return void 0;
4740
+ }
4741
+ if (!isValidImageBase64(data)) {
4742
+ error(
4743
+ errors,
4744
+ `${path35}/source/data`,
4745
+ "INVALID_IMAGE_DATA",
4746
+ "image.source.data is not valid base64."
4747
+ );
4748
+ return void 0;
4749
+ }
4750
+ if (base64DecodedBytes(data) > MAX_INCOMING_IMAGE_BYTES) {
4751
+ error(
4752
+ errors,
4753
+ `${path35}/source/data`,
4754
+ "IMAGE_TOO_LARGE",
4755
+ `image.source.data exceeds the ${MAX_INCOMING_IMAGE_BYTES / (1024 * 1024)} MB limit.`
4756
+ );
4757
+ return void 0;
4758
+ }
4759
+ return {
4760
+ type: "image",
4761
+ source: {
4762
+ type: "base64",
4763
+ ...typeof mediaType === "string" ? { media_type: mediaType } : {},
4764
+ data
4765
+ }
4766
+ };
4767
+ }
4768
+ if (typeof url !== "string" || url.length === 0) {
4769
+ error(
4770
+ errors,
4771
+ `${path35}/source/url`,
4772
+ "INVALID_IMAGE_URL",
4773
+ "image.source.url must be a non-empty string for a url source."
4774
+ );
4775
+ return void 0;
4477
4776
  }
4478
- if (url !== void 0 && typeof url !== "string") {
4479
- error(errors, `${path35}/source/url`, "INVALID_IMAGE_URL", "image.source.url must be a string.");
4777
+ const urlError = imageUrlRejectionReason(url);
4778
+ if (urlError !== void 0) {
4779
+ error(errors, `${path35}/source/url`, "UNSAFE_IMAGE_URL", urlError);
4780
+ return void 0;
4480
4781
  }
4481
4782
  return {
4482
4783
  type: "image",
4483
4784
  source: {
4484
- type: sourceType,
4785
+ type: "url",
4485
4786
  ...typeof mediaType === "string" ? { media_type: mediaType } : {},
4486
- ...typeof data === "string" ? { data } : {},
4487
- ...typeof url === "string" ? { url } : {}
4787
+ url
4488
4788
  }
4489
4789
  };
4490
4790
  }
@@ -4492,13 +4792,27 @@ function validateBlock(value, path35, errors) {
4492
4792
  const thinking = value["thinking"];
4493
4793
  const signature = value["signature"];
4494
4794
  if (typeof thinking !== "string") {
4495
- error(errors, `${path35}/thinking`, "INVALID_THINKING", "thinking.thinking must be a string.");
4795
+ error(
4796
+ errors,
4797
+ `${path35}/thinking`,
4798
+ "INVALID_THINKING",
4799
+ "thinking.thinking must be a string."
4800
+ );
4496
4801
  return void 0;
4497
4802
  }
4498
4803
  if (signature !== void 0 && typeof signature !== "string") {
4499
- error(errors, `${path35}/signature`, "INVALID_THINKING_SIGNATURE", "thinking.signature must be a string.");
4804
+ error(
4805
+ errors,
4806
+ `${path35}/signature`,
4807
+ "INVALID_THINKING_SIGNATURE",
4808
+ "thinking.signature must be a string."
4809
+ );
4500
4810
  }
4501
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path35}/providerMeta`, errors);
4811
+ const providerMeta = validateProviderMeta(
4812
+ value["providerMeta"],
4813
+ `${path35}/providerMeta`,
4814
+ errors
4815
+ );
4502
4816
  return {
4503
4817
  type: "thinking",
4504
4818
  thinking,
@@ -4507,7 +4821,12 @@ function validateBlock(value, path35, errors) {
4507
4821
  };
4508
4822
  }
4509
4823
  default:
4510
- error(errors, `${path35}/type`, "UNKNOWN_BLOCK_TYPE", `Unknown content block type: ${String(type)}`);
4824
+ error(
4825
+ errors,
4826
+ `${path35}/type`,
4827
+ "UNKNOWN_BLOCK_TYPE",
4828
+ `Unknown content block type: ${String(type)}`
4829
+ );
4511
4830
  return void 0;
4512
4831
  }
4513
4832
  }
@@ -4519,7 +4838,12 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4519
4838
  return { messages, errors };
4520
4839
  }
4521
4840
  if (value.length > currentMessageCount + MAX_MESSAGE_COUNT_GROWTH) {
4522
- error(errors, "/messages", "TOO_MANY_MESSAGES", "Context editor is deletion-oriented; proposed message count grew too much.");
4841
+ error(
4842
+ errors,
4843
+ "/messages",
4844
+ "TOO_MANY_MESSAGES",
4845
+ "Context editor is deletion-oriented; proposed message count grew too much."
4846
+ );
4523
4847
  }
4524
4848
  if (jsonByteLength(value) > MAX_PAYLOAD_BYTES) {
4525
4849
  error(errors, "/messages", "PAYLOAD_TOO_LARGE", "Context editor payload is too large.");
@@ -4532,7 +4856,12 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4532
4856
  }
4533
4857
  const role = item["role"];
4534
4858
  if (!isMessageRole(role)) {
4535
- error(errors, `${path35}/role`, "INVALID_ROLE", "Message role must be user, assistant, or system.");
4859
+ error(
4860
+ errors,
4861
+ `${path35}/role`,
4862
+ "INVALID_ROLE",
4863
+ "Message role must be user, assistant, or system."
4864
+ );
4536
4865
  return;
4537
4866
  }
4538
4867
  const rawContent = item["content"];
@@ -4551,13 +4880,23 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4551
4880
  });
4552
4881
  content = blocks;
4553
4882
  } else {
4554
- error(errors, `${path35}/content`, "INVALID_CONTENT", "Message content must be a string or content block array.");
4883
+ error(
4884
+ errors,
4885
+ `${path35}/content`,
4886
+ "INVALID_CONTENT",
4887
+ "Message content must be a string or content block array."
4888
+ );
4555
4889
  return;
4556
4890
  }
4557
4891
  const ts = item["ts"];
4558
4892
  if (ts !== void 0) {
4559
4893
  if (typeof ts !== "string" || Number.isNaN(Date.parse(ts))) {
4560
- error(errors, `${path35}/ts`, "INVALID_TIMESTAMP", "Message ts must be an ISO-like timestamp string.");
4894
+ error(
4895
+ errors,
4896
+ `${path35}/ts`,
4897
+ "INVALID_TIMESTAMP",
4898
+ "Message ts must be an ISO-like timestamp string."
4899
+ );
4561
4900
  return;
4562
4901
  }
4563
4902
  }
@@ -4567,7 +4906,8 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4567
4906
  }
4568
4907
  function countBlocks(messages) {
4569
4908
  let total = 0;
4570
- for (const message of messages) total += Array.isArray(message.content) ? message.content.length : 1;
4909
+ for (const message of messages)
4910
+ total += Array.isArray(message.content) ? message.content.length : 1;
4571
4911
  return total;
4572
4912
  }
4573
4913
  function toolDiagnostics(messages) {
@@ -4631,11 +4971,13 @@ function metricFor(ctx, messages, tools) {
4631
4971
  };
4632
4972
  }
4633
4973
  function buildContextEditorSnapshot(ctx, tools) {
4634
- const messages = ctx.messages.map((message) => ({
4635
- role: message.role,
4636
- content: message.content,
4637
- ...message.ts ? { ts: message.ts } : {}
4638
- }));
4974
+ const messages = ctx.messages.map(
4975
+ (message) => ({
4976
+ role: message.role,
4977
+ content: message.content,
4978
+ ...message.ts ? { ts: message.ts } : {}
4979
+ })
4980
+ );
4639
4981
  const breakdown = estimateContextBreakdown({
4640
4982
  systemPrompt: ctx.systemPrompt,
4641
4983
  tools: tools ?? ctx.tools ?? [],
@@ -4680,7 +5022,10 @@ function validateContextEditorProposal(input) {
4680
5022
  validationErrors: [],
4681
5023
  warnings: [],
4682
5024
  repair: emptyRepair,
4683
- conflict: { code: "RUN_ACTIVE", message: "The agent is currently running. Abort or wait before applying context edits." }
5025
+ conflict: {
5026
+ code: "RUN_ACTIVE",
5027
+ message: "The agent is currently running. Abort or wait before applying context edits."
5028
+ }
4684
5029
  };
4685
5030
  }
4686
5031
  if (input.baseRevision !== currentRevision) {
@@ -4692,7 +5037,10 @@ function validateContextEditorProposal(input) {
4692
5037
  validationErrors: [],
4693
5038
  warnings: [],
4694
5039
  repair: emptyRepair,
4695
- conflict: { code: "CONTEXT_REVISION_CONFLICT", message: "Context changed while the editor was open. Reload before applying." }
5040
+ conflict: {
5041
+ code: "CONTEXT_REVISION_CONFLICT",
5042
+ message: "Context changed while the editor was open. Reload before applying."
5043
+ }
4696
5044
  };
4697
5045
  }
4698
5046
  const parsed = validateContextEditorMessages(input.messages, input.ctx.messages.length);
@@ -4775,6 +5123,7 @@ async function applyContextEditorProposal(input) {
4775
5123
  }
4776
5124
 
4777
5125
  // src/server/connection-lifecycle.ts
5126
+ var RATE_LIMIT_EXEMPT_TYPES = /* @__PURE__ */ new Set(["ping", "pong"]);
4778
5127
  function createConnectionLifecycle(options) {
4779
5128
  const rateLimitMax = options.rateLimitMax ?? 0;
4780
5129
  const rateLimitWindowMs = options.rateLimitWindowMs ?? 6e4;
@@ -4845,25 +5194,31 @@ function createConnectionLifecycle(options) {
4845
5194
  }
4846
5195
  let messageCount = 0;
4847
5196
  let rateWindowResetAt = Date.now() + rateLimitWindowMs;
4848
- ws.on("message", async (data) => {
4849
- if (rateLimitMax > 0) {
4850
- const now = Date.now();
4851
- if (now > rateWindowResetAt) {
4852
- messageCount = 0;
4853
- rateWindowResetAt = now + rateLimitWindowMs;
4854
- }
4855
- if (++messageCount > rateLimitMax) {
4856
- options.send(ws, {
4857
- type: "error",
4858
- payload: options.sessionPayload({
4859
- phase: "rate_limit",
4860
- message: options.rateLimitMessage ?? "Too many messages. Please wait."
4861
- })
4862
- });
4863
- return;
4864
- }
5197
+ let rateLimitNotified = false;
5198
+ const chargeRateLimit = () => {
5199
+ if (!Number.isFinite(rateLimitMax) || rateLimitMax <= 0) return true;
5200
+ const now = Date.now();
5201
+ if (now > rateWindowResetAt) {
5202
+ messageCount = 0;
5203
+ rateWindowResetAt = now + rateLimitWindowMs;
5204
+ rateLimitNotified = false;
5205
+ }
5206
+ if (++messageCount <= rateLimitMax) return true;
5207
+ if (!rateLimitNotified) {
5208
+ rateLimitNotified = true;
5209
+ options.send(ws, {
5210
+ type: "error",
5211
+ payload: options.sessionPayload({
5212
+ phase: "rate_limit",
5213
+ message: options.rateLimitMessage ?? "Too many messages. Please wait."
5214
+ })
5215
+ });
4865
5216
  }
5217
+ return false;
5218
+ };
5219
+ ws.on("message", async (data) => {
4866
5220
  const decoded = options.decode(data.toString());
5221
+ if (!decoded.ok && !chargeRateLimit()) return;
4867
5222
  if (!decoded.ok) {
4868
5223
  const eventName = decoded.issue.message === "Protocol frame is not valid JSON" ? "webui_server.message_parse_failed" : "webui_server.message_rejected";
4869
5224
  const level = decoded.issue.code === "unsafe_key" || decoded.issue.code === "too_deep" ? "error" : "warn";
@@ -4901,6 +5256,10 @@ function createConnectionLifecycle(options) {
4901
5256
  });
4902
5257
  return;
4903
5258
  }
5259
+ const messageType = decoded.message.type;
5260
+ if ((typeof messageType !== "string" || !RATE_LIMIT_EXEMPT_TYPES.has(messageType)) && !chargeRateLimit()) {
5261
+ return;
5262
+ }
4904
5263
  try {
4905
5264
  await options.dispatch(ws, client, decoded.message);
4906
5265
  } catch (error2) {
@@ -4941,10 +5300,17 @@ import {
4941
5300
  MailboxProjectServerConnection
4942
5301
  } from "@wrongstack/core/coordination";
4943
5302
  import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
4944
- import { getKanbanServerConnection } from "@wrongstack/kanban";
5303
+ import {
5304
+ closeKanbanServerConnections,
5305
+ getKanbanServerConnection,
5306
+ isKanbanServerAvailable
5307
+ } from "@wrongstack/kanban";
5308
+ import * as net2 from "node:net";
5309
+ import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
4945
5310
  import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
4946
5311
  import {
4947
5312
  checkCodebaseIndexServerHealth,
5313
+ ensureCodebaseIndexServer as ensureCodebaseIndexServer2,
4948
5314
  getIndexState,
4949
5315
  resolveProjectIndexDaemonAvailability,
4950
5316
  shutdownCodebaseIndexServer
@@ -4973,7 +5339,8 @@ async function collectConnectionsHealth(options) {
4973
5339
  codebaseIndexHealth(options.projectRoot, options.indexDir),
4974
5340
  sageHealth(options.projectRoot),
4975
5341
  kanbanHealth(options.projectRoot),
4976
- mailboxHealth(options.projectRoot)
5342
+ mailboxHealth(options.projectRoot),
5343
+ governanceHealth(options.projectRoot)
4977
5344
  ]);
4978
5345
  const required = services.filter((service) => service.required);
4979
5346
  const overall = required.some((service) => service.status === "error") ? "error" : required.some((service) => service.status !== "healthy") ? "degraded" : "healthy";
@@ -5278,6 +5645,42 @@ async function mailboxHealth(projectRoot) {
5278
5645
  connection.close();
5279
5646
  }
5280
5647
  }
5648
+ async function governanceHealth(projectRoot) {
5649
+ const startedAt = Date.now();
5650
+ const result = await readGovernanceDaemonOperatorStatus(projectRoot);
5651
+ if (!result.available) {
5652
+ const missing = result.code === "broker_missing";
5653
+ return {
5654
+ id: "governance",
5655
+ label: "Governance control plane",
5656
+ status: missing ? "offline" : "error",
5657
+ required: false,
5658
+ mode: missing ? "compatibility-default-off" : "project-daemon",
5659
+ detail: missing ? "Not active for this project. Existing agent and model execution remains unchanged." : `Read-only governance status is unavailable: ${result.message}`,
5660
+ latencyMs: Date.now() - startedAt,
5661
+ control: "none",
5662
+ ...missing ? {} : { lastError: result.message }
5663
+ };
5664
+ }
5665
+ const { status } = result;
5666
+ return {
5667
+ id: "governance",
5668
+ label: "Governance control plane",
5669
+ status: status.signal.level === "healthy" ? "healthy" : "degraded",
5670
+ required: false,
5671
+ mode: "project-daemon-advisory",
5672
+ detail: `${status.signal.message} Execution continues; no automatic task or model stop.`,
5673
+ ownerPid: status.pid,
5674
+ uptimeMs: Math.max(0, Date.now() - Date.parse(status.startedAt)),
5675
+ latencyMs: Date.now() - startedAt,
5676
+ control: "none",
5677
+ advisory: {
5678
+ code: status.signal.code,
5679
+ operatorAction: status.signal.operatorAction,
5680
+ executionDisposition: status.signal.executionDisposition
5681
+ }
5682
+ };
5683
+ }
5281
5684
  async function handleConnectionsServiceAction(ws, message, context) {
5282
5685
  if (message.type !== "connections.service_action") return false;
5283
5686
  const payload = message.payload;
@@ -5295,14 +5698,14 @@ async function handleConnectionsServiceAction(ws, message, context) {
5295
5698
  });
5296
5699
  return true;
5297
5700
  }
5298
- if (rawAction !== "shutdown") {
5701
+ if (rawAction !== "shutdown" && rawAction !== "restart") {
5299
5702
  context.send(ws, {
5300
5703
  type: "connections.service_action_result",
5301
5704
  payload: {
5302
5705
  serviceId,
5303
5706
  action: rawAction,
5304
5707
  success: false,
5305
- message: `Unsupported action "${rawAction}" \u2014 only "shutdown" is currently supported`
5708
+ message: `Unsupported action "${rawAction}" \u2014 only "shutdown" and "restart" are supported`
5306
5709
  }
5307
5710
  });
5308
5711
  return true;
@@ -5315,7 +5718,7 @@ async function handleConnectionsServiceAction(ws, message, context) {
5315
5718
  serviceId: "webui",
5316
5719
  action,
5317
5720
  success: false,
5318
- message: "Cannot shut down the WebUI transport itself"
5721
+ message: action === "restart" ? "Cannot restart the WebUI transport itself" : "Cannot shut down the WebUI transport itself"
5319
5722
  }
5320
5723
  });
5321
5724
  return true;
@@ -5356,6 +5759,13 @@ async function executeServiceAction(serviceId, action, projectRoot, indexDir) {
5356
5759
  return killCodebaseIndexServer(projectRoot, indexDir, action);
5357
5760
  case "mailbox":
5358
5761
  return killMailboxServer(projectRoot, action);
5762
+ case "governance":
5763
+ return {
5764
+ serviceId: "governance",
5765
+ action,
5766
+ success: false,
5767
+ message: "Governance health is read-only; daemon shutdown requires a separate admin control capability."
5768
+ };
5359
5769
  default:
5360
5770
  return {
5361
5771
  serviceId,
@@ -5397,11 +5807,24 @@ async function killKanbanServer(projectRoot, action) {
5397
5807
  const result = await connection.request("shutdown", {
5398
5808
  reason: `WebUI request: ${action}`
5399
5809
  });
5810
+ if (!result.stopping) {
5811
+ return {
5812
+ serviceId: "kanban",
5813
+ action,
5814
+ success: false,
5815
+ message: "Kanban IPC daemon shutdown failed (not confirmed)"
5816
+ };
5817
+ }
5818
+ if (action === "restart") {
5819
+ closeKanbanServerConnections();
5820
+ const restartResult = await restartKanbanServer(projectRoot);
5821
+ return restartResult;
5822
+ }
5400
5823
  return {
5401
5824
  serviceId: "kanban",
5402
5825
  action,
5403
- success: result.stopping,
5404
- message: result.stopping ? `Kanban IPC daemon ${action} requested` : `Kanban IPC daemon ${action} failed (shutdown not confirmed)`
5826
+ success: true,
5827
+ message: "Kanban IPC daemon shutdown requested"
5405
5828
  };
5406
5829
  } catch (error2) {
5407
5830
  return {
@@ -5412,6 +5835,34 @@ async function killKanbanServer(projectRoot, action) {
5412
5835
  };
5413
5836
  }
5414
5837
  }
5838
+ async function restartKanbanServer(projectRoot) {
5839
+ await waitForShutdown(() => isKanbanServerAvailable(projectRoot));
5840
+ try {
5841
+ const connection = await getKanbanServerConnection(projectRoot);
5842
+ if (!connection) {
5843
+ return {
5844
+ serviceId: "kanban",
5845
+ action: "restart",
5846
+ success: false,
5847
+ message: "Kanban IPC daemon failed to restart (no connection after re-init)"
5848
+ };
5849
+ }
5850
+ await connection.request("ping", {}, { timeoutMs: 1e4 });
5851
+ return {
5852
+ serviceId: "kanban",
5853
+ action: "restart",
5854
+ success: true,
5855
+ message: "Kanban IPC daemon restarted successfully"
5856
+ };
5857
+ } catch (error2) {
5858
+ return {
5859
+ serviceId: "kanban",
5860
+ action: "restart",
5861
+ success: false,
5862
+ message: `Kanban IPC daemon restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
5863
+ };
5864
+ }
5865
+ }
5415
5866
  async function killSageServer(projectRoot, action) {
5416
5867
  if (!isSageProjectServerAvailable()) {
5417
5868
  return {
@@ -5424,11 +5875,22 @@ async function killSageServer(projectRoot, action) {
5424
5875
  const connection = new SageProjectServerConnection(projectRoot);
5425
5876
  try {
5426
5877
  const result = await connection.shutdown(`WebUI request: ${action}`);
5878
+ if (!result.stopped) {
5879
+ return {
5880
+ serviceId: "sage",
5881
+ action,
5882
+ success: false,
5883
+ message: `SAGE memory server shutdown failed: ${result.reason ?? "unknown"}`
5884
+ };
5885
+ }
5886
+ if (action === "restart") {
5887
+ return await restartSageServer(projectRoot);
5888
+ }
5427
5889
  return {
5428
5890
  serviceId: "sage",
5429
5891
  action,
5430
- success: result.stopped,
5431
- message: result.stopped ? `SAGE memory server ${action} requested` : `SAGE memory server ${action} failed: ${result.reason ?? "unknown"}`
5892
+ success: true,
5893
+ message: "SAGE memory server shutdown requested"
5432
5894
  };
5433
5895
  } catch (error2) {
5434
5896
  return {
@@ -5441,16 +5903,56 @@ async function killSageServer(projectRoot, action) {
5441
5903
  connection.close();
5442
5904
  }
5443
5905
  }
5906
+ async function restartSageServer(projectRoot) {
5907
+ await waitForShutdown(async () => {
5908
+ const probe = new SageProjectServerConnection(projectRoot);
5909
+ try {
5910
+ return await probe.status() !== null;
5911
+ } finally {
5912
+ probe.close();
5913
+ }
5914
+ });
5915
+ const verifyConn = new SageProjectServerConnection(projectRoot);
5916
+ try {
5917
+ await verifyConn.call("ping", {}, { timeoutMs: 1e4, meta: { clientId: `sage-restart-${process.pid}` } });
5918
+ return {
5919
+ serviceId: "sage",
5920
+ action: "restart",
5921
+ success: true,
5922
+ message: "SAGE memory server restarted successfully"
5923
+ };
5924
+ } catch (error2) {
5925
+ return {
5926
+ serviceId: "sage",
5927
+ action: "restart",
5928
+ success: false,
5929
+ message: `SAGE memory server restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
5930
+ };
5931
+ } finally {
5932
+ verifyConn.close();
5933
+ }
5934
+ }
5444
5935
  async function killChronicleServer(projectRoot, action) {
5445
5936
  const options = resolveChronicleProjectServerOptions({ projectRoot });
5446
5937
  const client = new ChronicleProjectServerClient(options);
5447
5938
  try {
5448
5939
  const result = await client.shutdown(`WebUI request: ${action}`);
5940
+ if (!result.stopped) {
5941
+ return {
5942
+ serviceId: "chronicle",
5943
+ action,
5944
+ success: false,
5945
+ message: `Chronicle telemetry server shutdown failed: ${result.reason ?? "unknown"}`
5946
+ };
5947
+ }
5948
+ if (action === "restart") {
5949
+ return await restartChronicleServer(projectRoot);
5950
+ }
5449
5951
  return {
5450
5952
  serviceId: "chronicle",
5451
5953
  action,
5452
- success: result.stopped,
5453
- message: result.stopped ? `Chronicle telemetry server ${action} requested` : `Chronicle telemetry server ${action} failed: ${result.reason ?? "unknown"}`
5954
+ success: true,
5955
+ message: "Chronicle telemetry server shutdown requested"
5454
5956
  };
5455
5957
  } catch (error2) {
5456
5958
  return {
@@ -5463,6 +5965,39 @@ async function killChronicleServer(projectRoot, action) {
5463
5965
  client.close();
5464
5966
  }
5465
5967
  }
5968
+ async function restartChronicleServer(projectRoot) {
5969
+ const options = resolveChronicleProjectServerOptions({ projectRoot });
5970
+ const endpoint = new ChronicleProjectServerClient(options).endpoint;
5971
+ await waitForShutdown(async () => isEndpointAlive(endpoint));
5972
+ let access2;
5973
+ try {
5974
+ access2 = createChronicleProjectAccess2({ projectRoot });
5975
+ await access2.call("ping", {}, { timeoutMs: 1e4 });
5976
+ if (access2.mode !== "server") {
5977
+ return {
5978
+ serviceId: "chronicle",
5979
+ action: "restart",
5980
+ success: false,
5981
+ message: `Chronicle telemetry server restarted but running in ${access2.mode} mode (expected server)`
5982
+ };
5983
+ }
5984
+ return {
5985
+ serviceId: "chronicle",
5986
+ action: "restart",
5987
+ success: true,
5988
+ message: "Chronicle telemetry server restarted successfully"
5989
+ };
5990
+ } catch (error2) {
5991
+ return {
5992
+ serviceId: "chronicle",
5993
+ action: "restart",
5994
+ success: false,
5995
+ message: `Chronicle telemetry server restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
5996
+ };
5997
+ } finally {
5998
+ await access2?.close();
5999
+ }
6000
+ }
5466
6001
  async function killCodebaseIndexServer(projectRoot, indexDir, action) {
5467
6002
  try {
5468
6003
  const result = await shutdownCodebaseIndexServer(
@@ -5470,11 +6005,22 @@ async function killCodebaseIndexServer(projectRoot, indexDir, action) {
5470
6005
  indexDir,
5471
6006
  `websocket-request:${action}`
5472
6007
  );
6008
+ if (!result.stopped) {
6009
+ return {
6010
+ serviceId: "codebase-index",
6011
+ action,
6012
+ success: false,
6013
+ message: `Codebase index server shutdown failed: ${result.reason ?? "unknown"}`
6014
+ };
6015
+ }
6016
+ if (action === "restart") {
6017
+ return await restartCodebaseIndexServer(projectRoot, indexDir);
6018
+ }
5473
6019
  return {
5474
6020
  serviceId: "codebase-index",
5475
6021
  action,
5476
- success: result.stopped,
5477
- message: result.stopped ? `Codebase index server ${action} requested` : `Codebase index server ${action} failed: ${result.reason ?? "unknown"}`
6022
+ success: true,
6023
+ message: "Codebase index server shutdown requested"
5478
6024
  };
5479
6025
  } catch (error2) {
5480
6026
  return {
@@ -5485,6 +6031,45 @@ async function killCodebaseIndexServer(projectRoot, indexDir, action) {
5485
6031
  };
5486
6032
  }
5487
6033
  }
6034
+ async function restartCodebaseIndexServer(projectRoot, indexDir) {
6035
+ await waitForShutdown(async () => {
6036
+ try {
6037
+ await checkCodebaseIndexServerHealth(projectRoot, indexDir, {
6038
+ timeoutMs: 1e3
6039
+ });
6040
+ return true;
6041
+ } catch {
6042
+ return false;
6043
+ }
6044
+ });
6045
+ try {
6046
+ await ensureCodebaseIndexServer2({ projectRoot, indexDir });
6047
+ const health = await checkCodebaseIndexServerHealth(projectRoot, indexDir, {
6048
+ timeoutMs: 1e4
6049
+ });
6050
+ if (health.status === "unresponsive") {
6051
+ return {
6052
+ serviceId: "codebase-index",
6053
+ action: "restart",
6054
+ success: false,
6055
+ message: "Codebase index server restarted but is unresponsive"
6056
+ };
6057
+ }
6058
+ return {
6059
+ serviceId: "codebase-index",
6060
+ action: "restart",
6061
+ success: true,
6062
+ message: "Codebase index server restarted successfully"
6063
+ };
6064
+ } catch (error2) {
6065
+ return {
6066
+ serviceId: "codebase-index",
6067
+ action: "restart",
6068
+ success: false,
6069
+ message: `Codebase index server restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
6070
+ };
6071
+ }
6072
+ }
5488
6073
  async function killMailboxServer(projectRoot, action) {
5489
6074
  if (!isMailboxProjectServerAvailable()) {
5490
6075
  return {
@@ -5499,11 +6084,22 @@ async function killMailboxServer(projectRoot, action) {
5499
6084
  );
5500
6085
  try {
5501
6086
  const result = await connection.shutdown(`WebUI request: ${action}`);
6087
+ if (!result.stopped) {
6088
+ return {
6089
+ serviceId: "mailbox",
6090
+ action,
6091
+ success: false,
6092
+ message: `Mailbox IPC server shutdown failed: ${result.reason ?? "unknown"}`
6093
+ };
6094
+ }
6095
+ if (action === "restart") {
6096
+ return await restartMailboxServer(projectRoot);
6097
+ }
5502
6098
  return {
5503
6099
  serviceId: "mailbox",
5504
6100
  action,
5505
- success: result.stopped,
5506
- message: result.stopped ? `Mailbox IPC server ${action} requested` : `Mailbox IPC server ${action} failed: ${result.reason ?? "unknown"}`
6101
+ success: true,
6102
+ message: "Mailbox IPC server shutdown requested"
5507
6103
  };
5508
6104
  } catch (error2) {
5509
6105
  return {
@@ -5516,6 +6112,77 @@ async function killMailboxServer(projectRoot, action) {
5516
6112
  connection.close();
5517
6113
  }
5518
6114
  }
6115
+ async function restartMailboxServer(projectRoot) {
6116
+ await waitForShutdown(async () => {
6117
+ const probe = new MailboxProjectServerConnection(
6118
+ resolveWstackPaths2({ projectRoot }).projectDir
6119
+ );
6120
+ try {
6121
+ return await probe.probeStatus() !== null;
6122
+ } finally {
6123
+ probe.close();
6124
+ }
6125
+ });
6126
+ const verifyConn = new MailboxProjectServerConnection(
6127
+ resolveWstackPaths2({ projectRoot }).projectDir
6128
+ );
6129
+ try {
6130
+ await verifyConn.call("ping", {}, { timeoutMs: 1e4 });
6131
+ return {
6132
+ serviceId: "mailbox",
6133
+ action: "restart",
6134
+ success: true,
6135
+ message: "Mailbox IPC server restarted successfully"
6136
+ };
6137
+ } catch (error2) {
6138
+ return {
6139
+ serviceId: "mailbox",
6140
+ action: "restart",
6141
+ success: false,
6142
+ message: `Mailbox IPC server restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
6143
+ };
6144
+ } finally {
6145
+ verifyConn.close();
6146
+ }
6147
+ }
6148
+ var RESTART_POLL_INTERVAL_MS = 250;
6149
+ var RESTART_DEADLINE_MS = 3e3;
6150
+ function isEndpointAlive(endpoint) {
6151
+ return new Promise((resolve16) => {
6152
+ const sock = net2.createConnection(endpoint);
6153
+ const timer = setTimeout(() => {
6154
+ sock.destroy();
6155
+ resolve16(false);
6156
+ }, 500);
6157
+ timer.unref?.();
6158
+ sock.once("connect", () => {
6159
+ clearTimeout(timer);
6160
+ sock.destroy();
6161
+ resolve16(true);
6162
+ });
6163
+ sock.once("error", () => {
6164
+ clearTimeout(timer);
6165
+ sock.destroy();
6166
+ resolve16(false);
6167
+ });
6168
+ });
6169
+ }
6170
+ async function waitForShutdown(probe) {
6171
+ if (!probe) {
6172
+ await new Promise((resolve16) => setTimeout(resolve16, RESTART_POLL_INTERVAL_MS));
6173
+ return;
6174
+ }
6175
+ const deadline = Date.now() + RESTART_DEADLINE_MS;
6176
+ while (Date.now() < deadline) {
6177
+ try {
6178
+ const stillUp = await probe();
6179
+ if (!stillUp) return;
6180
+ } catch {
6181
+ return;
6182
+ }
6183
+ await new Promise((resolve16) => setTimeout(resolve16, RESTART_POLL_INTERVAL_MS));
6184
+ }
6185
+ }
5519
6186
  function failureService(id, label, required, mode, error2, latencyMs) {
5520
6187
  const message = error2 instanceof Error ? error2.message : String(error2);
5521
6188
  return {
@@ -5792,11 +6459,11 @@ async function handleGitDiff(ws, projectRoot, path35) {
5792
6459
  try {
5793
6460
  const git = makeGit(cwd);
5794
6461
  const { readFile: readFile13 } = await import("node:fs/promises");
5795
- const { join: join19 } = await import("node:path");
6462
+ const { join: join18 } = await import("node:path");
5796
6463
  const oldText = await git(["show", `HEAD:${path35}`]);
5797
6464
  let newText = "";
5798
6465
  try {
5799
- const abs = cwd ? join19(cwd, path35) : path35;
6466
+ const abs = cwd ? join18(cwd, path35) : path35;
5800
6467
  const buf = await readFile13(abs);
5801
6468
  if (buf.includes(0)) {
5802
6469
  reply2({ oldText: "", newText: "", binary: true });
@@ -5962,8 +6629,11 @@ var GoalWebSocketHandler = class {
5962
6629
  this.assessAbort?.abort();
5963
6630
  this.assessAbort = null;
5964
6631
  this.orchestrator?.stop();
6632
+ this.orchestrator = null;
5965
6633
  this.stopBroadcast();
5966
6634
  this.clients.clear();
6635
+ this.usedNicknames.clear();
6636
+ this.worktrees = null;
5967
6637
  }
5968
6638
  async handleMessage(ws, msg) {
5969
6639
  switch (msg.type) {
@@ -6998,92 +7668,17 @@ function clearAnalyticsBuffer() {
6998
7668
  EVENT_COUNTS.clear();
6999
7669
  CATEGORY_COUNTS.clear();
7000
7670
  }
7001
- function getAnalyticsBuffer() {
7002
- return [...EVENT_BUFFER];
7003
- }
7004
-
7005
- // src/server/http-server.ts
7006
- import * as fs10 from "node:fs/promises";
7007
- import * as http from "node:http";
7008
- import * as path13 from "node:path";
7009
- import * as v8 from "node:v8";
7010
- import { getIndexState as getIndexState2 } from "@wrongstack/tools";
7011
-
7012
- // src/server/codemap-handlers.ts
7013
- import { fileGraphService, packageGraphService, symbolGraphService } from "@wrongstack/tools";
7014
- function sendJsonBody(res, status, body, cacheStatus) {
7015
- res.writeHead(status, {
7016
- "Content-Type": "application/json",
7017
- "X-Codemap-Cache": cacheStatus
7018
- });
7019
- res.end(body);
7020
- }
7021
- function sendJson(res, status, data, cacheStatus = "BYPASS") {
7022
- sendJsonBody(res, status, JSON.stringify(data), cacheStatus);
7023
- }
7024
- async function serveCachedGraph(res, deps2, scope, compute) {
7025
- try {
7026
- const version = indexDbVersion(deps2.projectRoot, deps2.indexDir);
7027
- const key = codemapCacheKey(deps2.projectRoot, deps2.indexDir, scope);
7028
- const hit = getCachedCodemapBody(key, version);
7029
- if (hit !== void 0) {
7030
- sendJsonBody(res, 200, hit, "HIT");
7031
- return;
7032
- }
7033
- const graph = await compute();
7034
- const body = JSON.stringify(graph);
7035
- if (version !== "missing") {
7036
- setCachedCodemapBody(key, version, body);
7037
- }
7038
- sendJsonBody(res, 200, body, "MISS");
7039
- } catch (err) {
7040
- const msg = err instanceof Error ? err.message : String(err);
7041
- sendJson(res, 503, { error: "CodeMap index unavailable", detail: msg }, "BYPASS");
7042
- }
7043
- }
7044
- async function handleCodemapPackages(res, deps2) {
7045
- await serveCachedGraph(
7046
- res,
7047
- deps2,
7048
- "packages",
7049
- () => packageGraphService({
7050
- projectRoot: deps2.projectRoot,
7051
- ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
7052
- })
7053
- );
7054
- }
7055
- async function handleCodemapFiles(res, deps2, pkg) {
7056
- if (!pkg) {
7057
- sendJson(res, 400, { error: 'Missing "package" query parameter' });
7058
- return;
7059
- }
7060
- await serveCachedGraph(
7061
- res,
7062
- deps2,
7063
- `files:${pkg}`,
7064
- () => fileGraphService({
7065
- projectRoot: deps2.projectRoot,
7066
- packageFilter: pkg,
7067
- ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
7068
- })
7069
- );
7070
- }
7071
- async function handleCodemapSymbols(res, deps2, file) {
7072
- if (!file) {
7073
- sendJson(res, 400, { error: 'Missing "file" query parameter' });
7074
- return;
7075
- }
7076
- await serveCachedGraph(
7077
- res,
7078
- deps2,
7079
- `symbols:${file}`,
7080
- () => symbolGraphService({
7081
- projectRoot: deps2.projectRoot,
7082
- fileFilter: file,
7083
- ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
7084
- })
7085
- );
7086
- }
7671
+ function getAnalyticsBuffer() {
7672
+ return [...EVENT_BUFFER];
7673
+ }
7674
+
7675
+ // src/server/http-server.ts
7676
+ import * as fs10 from "node:fs/promises";
7677
+ import * as http from "node:http";
7678
+ import * as path13 from "node:path";
7679
+ import * as v8 from "node:v8";
7680
+ import { sanitizeApiError as sanitizeApiError2 } from "@wrongstack/core/security";
7681
+ import { getIndexState as getIndexState2 } from "@wrongstack/tools";
7087
7682
 
7088
7683
  // src/server/deadcode-handlers.ts
7089
7684
  import * as path10 from "node:path";
@@ -7235,7 +7830,84 @@ function buildActionPlan(result) {
7235
7830
  };
7236
7831
  }
7237
7832
 
7833
+ // src/server/codemap-handlers.ts
7834
+ import { fileGraphService, packageGraphService, symbolGraphService } from "@wrongstack/tools";
7835
+ function sendJsonBody(res, status, body, cacheStatus) {
7836
+ res.writeHead(status, {
7837
+ "Content-Type": "application/json",
7838
+ "X-Codemap-Cache": cacheStatus
7839
+ });
7840
+ res.end(body);
7841
+ }
7842
+ function sendJson(res, status, data, cacheStatus = "BYPASS") {
7843
+ sendJsonBody(res, status, JSON.stringify(data), cacheStatus);
7844
+ }
7845
+ async function serveCachedGraph(res, deps2, scope, compute) {
7846
+ try {
7847
+ const version = indexDbVersion(deps2.projectRoot, deps2.indexDir);
7848
+ const key = codemapCacheKey(deps2.projectRoot, deps2.indexDir, scope);
7849
+ const hit = getCachedCodemapBody(key, version);
7850
+ if (hit !== void 0) {
7851
+ sendJsonBody(res, 200, hit, "HIT");
7852
+ return;
7853
+ }
7854
+ const graph = await compute();
7855
+ const body = JSON.stringify(graph);
7856
+ if (version !== "missing") {
7857
+ setCachedCodemapBody(key, version, body);
7858
+ }
7859
+ sendJsonBody(res, 200, body, "MISS");
7860
+ } catch (err) {
7861
+ const msg = err instanceof Error ? err.message : String(err);
7862
+ sendJson(res, 503, { error: "CodeMap index unavailable", detail: msg }, "BYPASS");
7863
+ }
7864
+ }
7865
+ async function handleCodemapPackages(res, deps2) {
7866
+ await serveCachedGraph(
7867
+ res,
7868
+ deps2,
7869
+ "packages",
7870
+ () => packageGraphService({
7871
+ projectRoot: deps2.projectRoot,
7872
+ ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
7873
+ })
7874
+ );
7875
+ }
7876
+ async function handleCodemapFiles(res, deps2, pkg) {
7877
+ if (!pkg) {
7878
+ sendJson(res, 400, { error: 'Missing "package" query parameter' });
7879
+ return;
7880
+ }
7881
+ await serveCachedGraph(
7882
+ res,
7883
+ deps2,
7884
+ `files:${pkg}`,
7885
+ () => fileGraphService({
7886
+ projectRoot: deps2.projectRoot,
7887
+ packageFilter: pkg,
7888
+ ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
7889
+ })
7890
+ );
7891
+ }
7892
+ async function handleCodemapSymbols(res, deps2, file) {
7893
+ if (!file) {
7894
+ sendJson(res, 400, { error: 'Missing "file" query parameter' });
7895
+ return;
7896
+ }
7897
+ await serveCachedGraph(
7898
+ res,
7899
+ deps2,
7900
+ `symbols:${file}`,
7901
+ () => symbolGraphService({
7902
+ projectRoot: deps2.projectRoot,
7903
+ fileFilter: file,
7904
+ ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
7905
+ })
7906
+ );
7907
+ }
7908
+
7238
7909
  // src/server/http-server/api-handlers.ts
7910
+ import { sanitizeApiError } from "@wrongstack/core/security";
7239
7911
  async function handleApiSessions(res, globalRoot) {
7240
7912
  if (!globalRoot) {
7241
7913
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -7271,7 +7943,7 @@ async function handleApiSessions(res, globalRoot) {
7271
7943
  res.end(JSON.stringify(result));
7272
7944
  } catch (err) {
7273
7945
  res.writeHead(500, { "Content-Type": "application/json" });
7274
- res.end(JSON.stringify({ error: String(err) }));
7946
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7275
7947
  }
7276
7948
  }
7277
7949
  async function handleApiSessionAgents(res, globalRoot, sessionId) {
@@ -7306,7 +7978,7 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
7306
7978
  }));
7307
7979
  } catch (err) {
7308
7980
  res.writeHead(500, { "Content-Type": "application/json" });
7309
- res.end(JSON.stringify({ error: String(err) }));
7981
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7310
7982
  }
7311
7983
  }
7312
7984
  function blocksToText(content) {
@@ -7453,11 +8125,16 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
7453
8125
  );
7454
8126
  } catch (err) {
7455
8127
  res.writeHead(500, { "Content-Type": "application/json" });
7456
- res.end(JSON.stringify({ error: String(err) }));
8128
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7457
8129
  }
7458
8130
  }
7459
8131
  function readJsonBody2(req) {
7460
8132
  return new Promise((resolve16, reject) => {
8133
+ const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
8134
+ if (contentType !== "application/json") {
8135
+ reject(new Error(`Unsupported Content-Type: ${contentType || "(absent)"}`));
8136
+ return;
8137
+ }
7461
8138
  let data = "";
7462
8139
  req.on("data", (chunk) => {
7463
8140
  data += chunk;
@@ -7522,7 +8199,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
7522
8199
  res.end(JSON.stringify({ ok: true, id: sent.id, to, type, delivered: entry.status }));
7523
8200
  } catch (err) {
7524
8201
  res.writeHead(500, { "Content-Type": "application/json" });
7525
- res.end(JSON.stringify({ error: String(err) }));
8202
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7526
8203
  }
7527
8204
  }
7528
8205
  async function handleApiSessionMailbox(res, globalRoot, sessionId) {
@@ -7575,7 +8252,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
7575
8252
  res.end(JSON.stringify({ sessionId, leader: leaderAddr, status: entry.status, thread }));
7576
8253
  } catch (err) {
7577
8254
  res.writeHead(500, { "Content-Type": "application/json" });
7578
- res.end(JSON.stringify({ error: String(err) }));
8255
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7579
8256
  }
7580
8257
  }
7581
8258
  async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
@@ -7616,7 +8293,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
7616
8293
  res.end(JSON.stringify({ ok: true, id: sent.id, to, delivered: entry.status }));
7617
8294
  } catch (err) {
7618
8295
  res.writeHead(500, { "Content-Type": "application/json" });
7619
- res.end(JSON.stringify({ error: String(err) }));
8296
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7620
8297
  }
7621
8298
  }
7622
8299
  async function handleApiFleetBroadcast(res, req, globalRoot) {
@@ -7685,7 +8362,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7685
8362
  res.end(JSON.stringify({ ok: true, delivered, targets: targets.length }));
7686
8363
  } catch (err) {
7687
8364
  res.writeHead(500, { "Content-Type": "application/json" });
7688
- res.end(JSON.stringify({ error: String(err) }));
8365
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7689
8366
  }
7690
8367
  }
7691
8368
 
@@ -8116,11 +8793,20 @@ import { timingSafeEqual } from "node:crypto";
8116
8793
  function isLoopbackHostname(hostname) {
8117
8794
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
8118
8795
  }
8119
- function isTrustedLoopbackOrigin(origin) {
8796
+ function effectivePort(url) {
8797
+ if (url.port) return url.port;
8798
+ return url.protocol === "https:" ? "443" : "80";
8799
+ }
8800
+ function isTrustedLoopbackOrigin(origin, hostHeader) {
8120
8801
  try {
8121
8802
  const url = new URL(origin);
8122
8803
  if (url.protocol !== "http:" && url.protocol !== "https:") return false;
8123
- return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]";
8804
+ if (!isLoopbackHostname(url.hostname)) return false;
8805
+ const host = (hostHeader ?? "").trim();
8806
+ if (!host) return false;
8807
+ const hostUrl = new URL(`${url.protocol}//${host}`);
8808
+ if (!isLoopbackHostname(hostUrl.hostname)) return false;
8809
+ return effectivePort(url) === effectivePort(hostUrl);
8124
8810
  } catch {
8125
8811
  return false;
8126
8812
  }
@@ -8179,6 +8865,23 @@ function hostHeaderOk(input) {
8179
8865
  }
8180
8866
  return isLoopbackHostname(hostname) || allowedHostname(hostname, input.allowedHostnames);
8181
8867
  }
8868
+ function httpRequestOriginOk(input) {
8869
+ const { origin, hostHeader, wsHost, allowedHostnames } = input;
8870
+ if (!hostHeaderOk({ hostHeader, wsHost, allowedHostnames })) return false;
8871
+ if (origin === void 0 || origin === "") return true;
8872
+ if (origin === "null") return false;
8873
+ try {
8874
+ const parsed = new URL(origin);
8875
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
8876
+ const rawHost = (hostHeader ?? "").trim();
8877
+ if (!rawHost) return false;
8878
+ const requestHost = new URL(`${parsed.protocol}//${rawHost}`).host.toLowerCase();
8879
+ if (parsed.host.toLowerCase() === requestHost) return true;
8880
+ return allowedHostname(parsed.hostname, allowedHostnames);
8881
+ } catch {
8882
+ return false;
8883
+ }
8884
+ }
8182
8885
  function verifyClient(input) {
8183
8886
  const {
8184
8887
  origin,
@@ -8199,13 +8902,13 @@ function verifyClient(input) {
8199
8902
  const remoteIp = remoteAddress ?? "";
8200
8903
  const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
8201
8904
  if (!isRemoteLoopback && isWildcardBind(wsHost) && !urlTokenOk && !cookieTokenOk) return false;
8202
- return urlTokenOk || cookieTokenOk || isLoopbackBind(wsHost) && !requireToken;
8905
+ return urlTokenOk || cookieTokenOk;
8203
8906
  }
8204
8907
  try {
8205
8908
  const { hostname: originHostname } = new URL(origin);
8206
8909
  if (isLoopbackHostname(originHostname)) {
8207
8910
  if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
8208
- return isTrustedLoopbackOrigin(origin);
8911
+ return cookieTokenOk || isTrustedLoopbackOrigin(origin, hostHeader);
8209
8912
  }
8210
8913
  return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
8211
8914
  } catch {
@@ -8267,7 +8970,7 @@ function cspSourceFromUrl(rawUrl) {
8267
8970
  return void 0;
8268
8971
  }
8269
8972
  }
8270
- var EXTRA_SCRIPT_SOURCES = ["'wasm-unsafe-eval'", "'unsafe-inline'"];
8973
+ var EXTRA_SCRIPT_SOURCES = ["'wasm-unsafe-eval'"];
8271
8974
  function buildCspHeader(publicWsUrl, host, port) {
8272
8975
  const connect = /* @__PURE__ */ new Set(["'self'"]);
8273
8976
  const publicWsSource = publicWsUrl ? cspSourceFromUrl(publicWsUrl) : void 0;
@@ -8309,6 +9012,16 @@ function createHttpServer(opts) {
8309
9012
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
8310
9013
  const distDir = path13.resolve(opts.distDir);
8311
9014
  const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
9015
+ const trustedHostnames = (() => {
9016
+ const names = [...opts.allowedHostnames ?? []];
9017
+ if (opts.publicWsUrl) {
9018
+ try {
9019
+ names.push(new URL(opts.publicWsUrl).hostname);
9020
+ } catch {
9021
+ }
9022
+ }
9023
+ return names;
9024
+ })();
8312
9025
  let techStackRuntime = null;
8313
9026
  const getTechStackRuntime = async () => {
8314
9027
  if (!opts.projectRoot) throw new Error("Project root not configured");
@@ -8323,6 +9036,16 @@ function createHttpServer(opts) {
8323
9036
  const server = http.createServer(async (req, res) => {
8324
9037
  try {
8325
9038
  const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
9039
+ if (!httpRequestOriginOk({
9040
+ origin: req.headers.origin,
9041
+ hostHeader: req.headers.host,
9042
+ wsHost: opts.host,
9043
+ allowedHostnames: trustedHostnames
9044
+ })) {
9045
+ res.writeHead(403, { "Content-Type": "application/json", "Cache-Control": "no-store" });
9046
+ res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
9047
+ return;
9048
+ }
8326
9049
  const providedAccessToken = requestToken(req, url);
8327
9050
  const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
8328
9051
  const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
@@ -8526,42 +9249,6 @@ function createHttpServer(opts) {
8526
9249
  );
8527
9250
  return;
8528
9251
  }
8529
- if (url.pathname === "/api/deadcode/scan" && req.method === "POST") {
8530
- if (requireAccessToken && !accessTokenOk) {
8531
- res.writeHead(401, { "Content-Type": "application/json" });
8532
- res.end(JSON.stringify({ error: "Unauthorized" }));
8533
- return;
8534
- }
8535
- if (!opts.projectRoot) {
8536
- res.writeHead(503, { "Content-Type": "application/json" });
8537
- res.end(JSON.stringify({ error: "Project root not configured" }));
8538
- return;
8539
- }
8540
- const deadCodeDeps = {
8541
- projectRoot: opts.projectRoot,
8542
- ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8543
- };
8544
- await handleDeadCodeScan(res, deadCodeDeps, req);
8545
- return;
8546
- }
8547
- if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
8548
- if (requireAccessToken && !accessTokenOk) {
8549
- res.writeHead(401, { "Content-Type": "application/json" });
8550
- res.end(JSON.stringify({ error: "Unauthorized" }));
8551
- return;
8552
- }
8553
- if (!opts.projectRoot) {
8554
- res.writeHead(503, { "Content-Type": "application/json" });
8555
- res.end(JSON.stringify({ error: "Project root not configured" }));
8556
- return;
8557
- }
8558
- const deadCodeDeps = {
8559
- projectRoot: opts.projectRoot,
8560
- ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8561
- };
8562
- await handleDeadCodeActionPlan(res, deadCodeDeps, req);
8563
- return;
8564
- }
8565
9252
  if (url.pathname.startsWith("/api/techstack/")) {
8566
9253
  if (requireAccessToken && !accessTokenOk) {
8567
9254
  res.writeHead(401, { "Content-Type": "application/json" });
@@ -8643,7 +9330,7 @@ function createHttpServer(opts) {
8643
9330
  res.end(
8644
9331
  JSON.stringify({
8645
9332
  error: "TechStack store unavailable",
8646
- detail: error2 instanceof Error ? error2.message : String(error2)
9333
+ detail: sanitizeApiError2(error2)
8647
9334
  })
8648
9335
  );
8649
9336
  return;
@@ -8690,6 +9377,30 @@ function createHttpServer(opts) {
8690
9377
  );
8691
9378
  return;
8692
9379
  }
9380
+ if (url.pathname === "/api/deadcode/scan" && req.method === "POST") {
9381
+ if (!opts.projectRoot) {
9382
+ res.writeHead(400, { "Content-Type": "application/json" });
9383
+ res.end(JSON.stringify({ error: "Project root not configured" }));
9384
+ return;
9385
+ }
9386
+ await handleDeadCodeScan(res, {
9387
+ projectRoot: opts.projectRoot,
9388
+ indexDir: opts.indexDir
9389
+ }, req);
9390
+ return;
9391
+ }
9392
+ if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
9393
+ await handleDeadCodeActionPlan(res, {
9394
+ projectRoot: opts.projectRoot ?? "",
9395
+ indexDir: opts.indexDir
9396
+ }, req);
9397
+ return;
9398
+ }
9399
+ if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
9400
+ res.writeHead(404, { "Content-Type": "application/json", "Cache-Control": "no-store" });
9401
+ res.end(JSON.stringify({ error: "Not found" }));
9402
+ return;
9403
+ }
8693
9404
  let filePath;
8694
9405
  if (url.pathname === "/" || url.pathname === "") {
8695
9406
  filePath = path13.join(distDir, "index.html");
@@ -8995,6 +9706,11 @@ async function handleIntrospectionRoute(ctx, ws, message) {
8995
9706
  cwd: ctx.getProjectRoot(),
8996
9707
  sessionId: ctx.getSessionId(),
8997
9708
  tools: { count: tools.length, names: tools.map((tool) => tool.name) },
9709
+ maxTools: actx.provider.maxToolsCount ?? 0,
9710
+ droppedTools: (() => {
9711
+ const mt = actx.provider.maxToolsCount ?? 0;
9712
+ return mt > 0 ? Math.max(0, tools.length - mt) : 0;
9713
+ })(),
8998
9714
  features: {
8999
9715
  memory: !!config.features?.memory,
9000
9716
  skills: !!config.features?.skills,
@@ -9096,7 +9812,6 @@ async function handleIntrospectionRoute(ctx, ws, message) {
9096
9812
  }
9097
9813
 
9098
9814
  // src/server/kanban-dispatch.ts
9099
- import { randomUUID as randomUUID3 } from "node:crypto";
9100
9815
  import {
9101
9816
  areDependenciesMet,
9102
9817
  getServerKanbanStore
@@ -9122,14 +9837,6 @@ function reply(ws, type, success, value) {
9122
9837
  payload: success ? { success: true, data: value } : { success: false, error: String(value) }
9123
9838
  });
9124
9839
  }
9125
- function activityContext(ctx, note, expectedLeaseId) {
9126
- const sessionId = ctx.context?.session?.id;
9127
- return {
9128
- ...sessionId ? { sessionId } : {},
9129
- ...note?.trim() ? { note: note.trim() } : {},
9130
- ...expectedLeaseId ? { expectedLeaseId } : {}
9131
- };
9132
- }
9133
9840
  async function handleKanbanTaskDispatch(ws, payload, ctx) {
9134
9841
  const boardId = payload?.boardId;
9135
9842
  const taskId = payload?.taskId;
@@ -9168,45 +9875,39 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
9168
9875
  }
9169
9876
  const modelRouting = payload?.modelRouting ?? task.assignment?.modelRouting ?? (payload?.provider || payload?.model || task.assignment?.provider || task.assignment?.model ? "fixed" : "session");
9170
9877
  const useSessionModel = modelRouting === "session";
9171
- const leaseId = randomUUID3();
9172
- const now = (/* @__PURE__ */ new Date()).toISOString();
9173
9878
  const leaseTtlMs = 30 * 60 * 1e3;
9174
- const leaseExpiresAt = new Date(Date.now() + leaseTtlMs).toISOString();
9175
- const assignment = {
9176
- agentId: payload?.agentId ?? task.assignment?.agentId ?? task.assignedAgent,
9177
- name: payload?.name ?? task.assignment?.name ?? task.assignedAgent,
9178
- role: payload?.role ?? task.assignment?.role,
9179
- modelRouting,
9180
- provider: useSessionModel ? ctx.context?.provider.id : payload?.provider ?? task.assignment?.provider,
9181
- model: useSessionModel ? ctx.context?.model : payload?.model ?? task.assignment?.model,
9182
- fallbackProfile: modelRouting === "fallback_profile" ? payload?.fallbackProfile ?? task.assignment?.fallbackProfile : void 0,
9183
- fallbackModels: payload?.fallbackModels ?? task.assignment?.fallbackModels,
9184
- skills: payload?.skills ?? task.assignment?.skills,
9185
- tools: payload?.tools ?? task.assignment?.tools,
9186
- allowedCapabilities: payload?.allowedCapabilities ?? task.assignment?.allowedCapabilities,
9187
- maxAttempts: payload?.maxAttempts ?? task.assignment?.maxAttempts,
9188
- costCeilingUsd: payload?.costCeilingUsd ?? task.assignment?.costCeilingUsd ?? task.costCeilingUsd,
9189
- retryPolicy: payload?.retryPolicy ?? task.assignment?.retryPolicy ?? task.retryPolicy,
9190
- attempt: (task.assignment?.attempt ?? 0) + 1,
9191
- status: "queued",
9192
- dispatchedAt: now,
9193
- leaseId,
9194
- claimedAt: now,
9195
- heartbeatAt: now,
9196
- leaseExpiresAt
9197
- };
9198
- const assignedBoard = await store.assignTask(
9879
+ const reserved = await store.reserveKanbanDispatch({
9199
9880
  boardId,
9200
- task.id,
9201
- assignment,
9202
- activityContext(ctx, payload?.activityNote)
9203
- );
9204
- if (!assignedBoard) {
9205
- reply(ws, "kanban.task.dispatch", false, `Failed to assign task "${task.title}". The task may have been removed or the board does not allow external assignment.`);
9881
+ taskId: task.id,
9882
+ routing: {
9883
+ agentId: payload?.agentId ?? task.assignment?.agentId ?? task.assignedAgent,
9884
+ name: payload?.name ?? task.assignment?.name ?? task.assignedAgent,
9885
+ role: payload?.role ?? task.assignment?.role,
9886
+ modelRouting,
9887
+ provider: useSessionModel ? ctx.context?.provider.id : payload?.provider ?? task.assignment?.provider,
9888
+ model: useSessionModel ? ctx.context?.model : payload?.model ?? task.assignment?.model,
9889
+ fallbackProfile: modelRouting === "fallback_profile" ? payload?.fallbackProfile ?? task.assignment?.fallbackProfile : void 0,
9890
+ fallbackModels: payload?.fallbackModels ?? task.assignment?.fallbackModels,
9891
+ skills: payload?.skills ?? task.assignment?.skills,
9892
+ tools: payload?.tools ?? task.assignment?.tools,
9893
+ allowedCapabilities: payload?.allowedCapabilities ?? task.assignment?.allowedCapabilities
9894
+ },
9895
+ budget: {
9896
+ maxAttempts: payload?.maxAttempts ?? task.assignment?.maxAttempts,
9897
+ costCeilingUsd: payload?.costCeilingUsd ?? task.assignment?.costCeilingUsd ?? task.costCeilingUsd,
9898
+ retryPolicy: payload?.retryPolicy ?? task.assignment?.retryPolicy ?? task.retryPolicy
9899
+ },
9900
+ leaseTtlMs,
9901
+ heartbeatIntervalMs: Math.floor(leaseTtlMs / 2)
9902
+ });
9903
+ if (!reserved) {
9904
+ reply(ws, "kanban.task.dispatch", false, `Failed to reserve task "${task.title}". The task may have been claimed by another agent or removed.`);
9206
9905
  return;
9207
9906
  }
9907
+ const dispatchLeaseId = reserved.lease.leaseId;
9208
9908
  try {
9209
- const summary = await ctx.dispatchTask(buildKanbanAgentPrompt(board, task, assignment, leaseId), {
9909
+ const assignment = reserved.task.assignment ?? { status: "queued" };
9910
+ const summary = await ctx.dispatchTask(buildKanbanAgentPrompt(board, task, assignment, dispatchLeaseId), {
9210
9911
  ...assignment.provider ? { provider: assignment.provider } : {},
9211
9912
  ...assignment.model ? { model: assignment.model } : {},
9212
9913
  ...assignment.fallbackModels ? { fallbackModels: assignment.fallbackModels } : {},
@@ -9215,69 +9916,40 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
9215
9916
  ...assignment.tools ? { tools: assignment.tools } : {},
9216
9917
  ...assignment.name ? { name: assignment.name } : {},
9217
9918
  ...assignment.allowedCapabilities ? { allowedCapabilities: assignment.allowedCapabilities } : {},
9218
- context: { kanban: { boardId, taskId: task.id, projectRoot: ctx.projectRoot, leaseId } },
9919
+ context: { kanban: { boardId, taskId: task.id, projectRoot: ctx.projectRoot, leaseId: dispatchLeaseId } },
9219
9920
  onDone: async (result) => {
9220
- const terminalWrite = await store.updateTaskAssignment(
9221
- boardId,
9222
- task.id,
9223
- {
9224
- ...assignment,
9225
- status: result.status,
9226
- ...result.result !== void 0 ? { lastResult: result.result } : {},
9227
- ...result.error !== void 0 ? { error: result.error } : {}
9228
- },
9229
- activityContext(ctx, void 0, leaseId)
9230
- );
9231
- if (result.status === "completed" && terminalWrite) {
9232
- try {
9233
- const finalized = await store.finalizeTaskCompletion(boardId, task.id, {
9234
- eventContext: activityContext(ctx, void 0, leaseId)
9235
- });
9236
- if (finalized?.gate.report && ctx.context) {
9237
- recordKanbanVerificationEvidence(ctx.context, finalized.gate.report);
9238
- }
9239
- } catch (error2) {
9240
- console.warn(
9241
- `[kanban-dispatch] completion gate failed for task ${task.id}: ${error2 instanceof Error ? error2.message : String(error2)}`
9242
- );
9243
- }
9244
- if (terminalWrite.lifecycle?.mode === "managed") {
9245
- const managedTask = terminalWrite.tasks.find(
9246
- (candidate) => candidate.id === task.id
9247
- );
9248
- if (managedTask && managedTask.lifecycle?.currentStage === "running") {
9249
- const actor = ctx.context?.agentId ?? "kanban-agent";
9250
- const comment = typeof result.result === "string" && result.result.trim().length > 0 ? result.result.trim().slice(0, 1e3) : "Work completed.";
9251
- try {
9252
- await store.transitionTask(boardId, task.id, {
9253
- to: "review",
9254
- actor,
9255
- comment,
9256
- attachment: {
9257
- url: `kanban://task/${task.id}/result`,
9258
- title: "Worker completion result",
9259
- type: "file"
9260
- }
9261
- });
9262
- } catch (transitionError) {
9263
- console.warn(
9264
- `[kanban-dispatch] auto-transition to review failed for task ${task.id}: ${transitionError instanceof Error ? transitionError.message : String(transitionError)}`
9265
- );
9266
- }
9267
- }
9921
+ if (result.status === "completed") {
9922
+ const completed = await store.completeKanbanDispatch({
9923
+ boardId,
9924
+ taskId: task.id,
9925
+ leaseId: dispatchLeaseId,
9926
+ actor: ctx.context?.agentId ?? "kanban-agent",
9927
+ result: result.result,
9928
+ evidence: result.result ? { url: `kanban://task/${task.id}/result`, title: "Worker completion result", type: "file" } : void 0
9929
+ });
9930
+ if (completed?.task.verificationReport && ctx.context) {
9931
+ recordKanbanVerificationEvidence(ctx.context, completed.task.verificationReport);
9268
9932
  }
9933
+ } else {
9934
+ await store.failKanbanDispatch({
9935
+ boardId,
9936
+ taskId: task.id,
9937
+ leaseId: dispatchLeaseId,
9938
+ actor: ctx.context?.agentId ?? "kanban-agent",
9939
+ error: result.error ?? "Task failed."
9940
+ });
9269
9941
  }
9270
9942
  const reconciled = await store.reconcileBoard(boardId);
9271
- const completed = reconciled?.board ?? await store.getBoard(boardId);
9272
- const completedTask = completed?.tasks.find((candidate) => candidate.id === task.id) ?? task;
9943
+ const completedBoard = reconciled?.board ?? await store.getBoard(boardId);
9944
+ const completedTask = completedBoard?.tasks.find((candidate) => candidate.id === task.id) ?? task;
9273
9945
  ctx.broadcast?.({
9274
9946
  type: "kanban.task.update",
9275
9947
  payload: { success: true, data: { boardId: board.id, task: completedTask } }
9276
9948
  });
9277
- if (completed) {
9949
+ if (completedBoard) {
9278
9950
  ctx.broadcast?.({
9279
9951
  type: "kanban.get",
9280
- payload: { success: true, data: { board: completed } }
9952
+ payload: { success: true, data: { board: completedBoard } }
9281
9953
  });
9282
9954
  }
9283
9955
  ctx.broadcast?.({
@@ -9288,55 +9960,32 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
9288
9960
  });
9289
9961
  const subagentId = summary.match(/Spawned subagent\s+([^\s]+)/)?.[1];
9290
9962
  const runTaskId = summary.match(/\bfor task\s+([^\s.]+)/i)?.[1];
9291
- const resolvedRoute = parseResolvedDispatchRoute(summary);
9292
- const updated = await store.updateTaskAssignment(
9963
+ const started = await store.startKanbanDispatch({
9293
9964
  boardId,
9294
- task.id,
9295
- {
9296
- ...assignment,
9297
- ...resolvedRoute,
9298
- status: "running",
9299
- ...subagentId ? { subagentId } : {},
9300
- ...runTaskId ? { runTaskId } : {},
9301
- lastResult: summary
9302
- },
9303
- activityContext(ctx, void 0, leaseId)
9304
- );
9305
- let transitionBoard = updated;
9306
- if (updated?.lifecycle?.mode === "managed") {
9307
- const managedTask = updated.tasks.find((candidate) => candidate.id === task.id);
9308
- if (managedTask && managedTask.lifecycle?.currentStage === "todo") {
9309
- try {
9310
- const tr = await store.transitionTask(boardId, task.id, {
9311
- to: "running",
9312
- actor: ctx.context?.agentId ?? "kanban-agent",
9313
- comment: "Work started."
9314
- });
9315
- if (tr) transitionBoard = tr.board;
9316
- } catch (transitionErr) {
9317
- console.warn(
9318
- `[kanban-dispatch] auto-transition to Running failed for task ${task.id}: ${transitionErr instanceof Error ? transitionErr.message : String(transitionErr)}`
9319
- );
9320
- }
9321
- }
9322
- }
9323
- const runningTask = transitionBoard?.tasks.find((candidate) => candidate.id === task.id) ?? task;
9965
+ taskId: task.id,
9966
+ leaseId: dispatchLeaseId,
9967
+ actor: ctx.context?.agentId ?? "kanban-agent",
9968
+ ...subagentId ? { subagentId } : {},
9969
+ ...runTaskId ? { runTaskId } : {}
9970
+ });
9971
+ const runningTask = started?.task ?? (started?.board.tasks.find((candidate) => candidate.id === task.id) ?? task);
9324
9972
  ctx.broadcast?.({
9325
9973
  type: "kanban.task.update",
9326
9974
  payload: { success: true, data: { boardId: board.id, task: runningTask } }
9327
9975
  });
9328
- if (updated) {
9329
- ctx.broadcast?.({ type: "kanban.get", payload: { success: true, data: { board: updated } } });
9976
+ if (started?.board) {
9977
+ ctx.broadcast?.({ type: "kanban.get", payload: { success: true, data: { board: started.board } } });
9330
9978
  }
9331
9979
  reply(ws, "kanban.task.dispatch", true, { boardId: board.id, task: runningTask, summary });
9332
9980
  } catch (error2) {
9333
9981
  const message = error2 instanceof Error ? error2.message : String(error2);
9334
- await store.updateTaskAssignment(
9982
+ await store.failKanbanDispatch({
9335
9983
  boardId,
9336
- task.id,
9337
- { ...assignment, status: "failed", error: message },
9338
- activityContext(ctx, void 0, leaseId)
9339
- );
9984
+ taskId: task.id,
9985
+ leaseId: dispatchLeaseId,
9986
+ actor: ctx.context?.agentId ?? "kanban-agent",
9987
+ error: message
9988
+ });
9340
9989
  reply(ws, "kanban.task.dispatch", false, message);
9341
9990
  }
9342
9991
  }
@@ -9490,7 +10139,7 @@ function fail(ws, type, message) {
9490
10139
  function has(payload, key) {
9491
10140
  return payload !== void 0 && Object.hasOwn(payload, key);
9492
10141
  }
9493
- function activityContext2(ctx, actor, note) {
10142
+ function activityContext(ctx, actor, note) {
9494
10143
  const sessionId = ctx.context?.session?.id;
9495
10144
  return {
9496
10145
  ...sessionId ? { sessionId } : {},
@@ -9727,7 +10376,7 @@ async function handleTaskActivityAdd(ws, type, payload, ctx) {
9727
10376
  outcome,
9728
10377
  ...typeof payload?.details === "string" && payload.details.trim() ? { details: payload.details.trim() } : {}
9729
10378
  },
9730
- activityContext2(
10379
+ activityContext(
9731
10380
  ctx,
9732
10381
  payload?.actor ?? ctx.context?.agentId ?? "webui"
9733
10382
  )
@@ -10088,7 +10737,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
10088
10737
  ...payload?.labels ? { labels: payload.labels } : {},
10089
10738
  ...has(payload, "boundary") ? { boundary: payload?.boundary } : {}
10090
10739
  },
10091
- activityContext2(ctx, "webui", payload?.activityNote)
10740
+ activityContext(ctx, "webui", payload?.activityNote)
10092
10741
  );
10093
10742
  result ? ok(ws, type, result.task) : fail(ws, type, `Board not found: ${boardId}`);
10094
10743
  return true;
@@ -10172,7 +10821,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
10172
10821
  boundary: payload?.boundary ?? null
10173
10822
  } : {}
10174
10823
  },
10175
- activityContext2(ctx, "webui", payload?.activityNote)
10824
+ activityContext(ctx, "webui", payload?.activityNote)
10176
10825
  );
10177
10826
  if (!board) {
10178
10827
  fail(ws, type, "Board or task not found");
@@ -10223,7 +10872,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
10223
10872
  taskId,
10224
10873
  columnId,
10225
10874
  payload?.order,
10226
- activityContext2(ctx, "webui", payload?.activityNote)
10875
+ activityContext(ctx, "webui", payload?.activityNote)
10227
10876
  );
10228
10877
  if (!board) {
10229
10878
  fail(ws, type, "Move failed");
@@ -10340,7 +10989,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
10340
10989
  ...payload?.unit ? { unit: payload.unit } : {},
10341
10990
  ...payload?.notes ? { notes: payload.notes } : {}
10342
10991
  },
10343
- activityContext2(
10992
+ activityContext(
10344
10993
  ctx,
10345
10994
  "webui",
10346
10995
  payload?.activityNote ?? `Goal metric added: ${name2}.`
@@ -10370,7 +11019,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
10370
11019
  ...payload?.unit ? { unit: payload.unit } : {},
10371
11020
  ...payload?.notes ? { notes: payload.notes } : {}
10372
11021
  },
10373
- activityContext2(
11022
+ activityContext(
10374
11023
  ctx,
10375
11024
  "webui",
10376
11025
  payload?.activityNote ?? "Goal metric updated in WebUI."
@@ -10407,7 +11056,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
10407
11056
  ...payload?.costCeilingUsd !== void 0 ? { costCeilingUsd: Number(payload.costCeilingUsd) } : {},
10408
11057
  ...payload?.retryPolicy ? { retryPolicy: payload.retryPolicy } : {}
10409
11058
  },
10410
- activityContext2(ctx, void 0, payload?.activityNote)
11059
+ activityContext(ctx, void 0, payload?.activityNote)
10411
11060
  );
10412
11061
  board ? ok(ws, type, findTask2(board.tasks, taskId)) : fail(ws, type, "Board or task not found");
10413
11062
  return true;
@@ -10429,7 +11078,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
10429
11078
  type: payload?.checkType ?? "manual",
10430
11079
  status: payload?.status ?? "pending"
10431
11080
  },
10432
- activityContext2(
11081
+ activityContext(
10433
11082
  ctx,
10434
11083
  "webui",
10435
11084
  payload?.activityNote ?? `Acceptance check added: ${description}.`
@@ -10454,7 +11103,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
10454
11103
  {
10455
11104
  ...has(payload, "status") ? { status: payload?.status } : {}
10456
11105
  },
10457
- activityContext2(
11106
+ activityContext(
10458
11107
  ctx,
10459
11108
  "webui",
10460
11109
  payload?.activityNote ?? `Acceptance check updated${payload?.status ? ` to ${String(payload.status)}` : ""}.`
@@ -10481,7 +11130,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
10481
11130
  author,
10482
11131
  content
10483
11132
  },
10484
- activityContext2(ctx, author)
11133
+ activityContext(ctx, author)
10485
11134
  );
10486
11135
  board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
10487
11136
  return true;
@@ -11035,12 +11684,14 @@ async function handleMailboxRoute(ws, msg, handlers) {
11035
11684
 
11036
11685
  // src/server/mcp-handlers.ts
11037
11686
  import { allServers } from "@wrongstack/core/infrastructure";
11687
+ import { DefaultSecretScrubber, isSecretField } from "@wrongstack/core/security";
11038
11688
  import {
11039
11689
  addMcp,
11040
11690
  disableMcp,
11041
11691
  discoverMcp,
11042
11692
  enableMcp,
11043
11693
  listMcp,
11694
+ MCP_ENV_MASK,
11044
11695
  removeMcp,
11045
11696
  restartMcp,
11046
11697
  updateMcp
@@ -11060,6 +11711,15 @@ function mapStatus(raw) {
11060
11711
  return "stopped";
11061
11712
  }
11062
11713
  }
11714
+ var envScrubber = new DefaultSecretScrubber();
11715
+ function maskServerEnv(env) {
11716
+ const out = {};
11717
+ for (const [key, value] of Object.entries(env)) {
11718
+ const secret = isSecretField(key) || typeof value === "string" && envScrubber.scrub(value) !== value;
11719
+ out[key] = secret ? MCP_ENV_MASK : value;
11720
+ }
11721
+ return out;
11722
+ }
11063
11723
  function toView(info, health) {
11064
11724
  const view = {
11065
11725
  name: info.name,
@@ -11074,7 +11734,7 @@ function toView(info, health) {
11074
11734
  if (info.lazy !== void 0) view.lazy = info.lazy;
11075
11735
  if (info.command !== void 0) view.command = info.command;
11076
11736
  if (info.args !== void 0) view.args = info.args;
11077
- if (info.env !== void 0) view.env = info.env;
11737
+ if (info.env !== void 0) view.env = maskServerEnv(info.env);
11078
11738
  if (info.url !== void 0) view.url = info.url;
11079
11739
  if (health !== void 0) view.health = health;
11080
11740
  return view;
@@ -11113,7 +11773,15 @@ async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
11113
11773
  async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
11114
11774
  const d = deps(ws, globalConfigPath, mcpRegistry);
11115
11775
  if (!d) return;
11116
- const result = await addMcp(msg.payload, d);
11776
+ const validated = validateMcpServerPayload(msg.payload, "mcp.add");
11777
+ if (!validated.ok) {
11778
+ send(ws, {
11779
+ type: "mcp.operation_result",
11780
+ payload: { success: false, message: validated.message }
11781
+ });
11782
+ return;
11783
+ }
11784
+ const result = await addMcp(validated.value, d);
11117
11785
  if (result.ok && result.server) {
11118
11786
  send(ws, { type: "mcp.server.added", payload: { server: toView(result.server) } });
11119
11787
  if (result.registryError) {
@@ -11133,7 +11801,15 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
11133
11801
  async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
11134
11802
  const d = deps(ws, globalConfigPath, mcpRegistry);
11135
11803
  if (!d) return;
11136
- const result = await updateMcp(msg.payload, d);
11804
+ const validated = validateMcpServerPayload(msg.payload, "mcp.update");
11805
+ if (!validated.ok) {
11806
+ send(ws, {
11807
+ type: "mcp.operation_result",
11808
+ payload: { success: false, message: validated.message }
11809
+ });
11810
+ return;
11811
+ }
11812
+ const result = await updateMcp(validated.value, d);
11137
11813
  if (result.ok && result.server) {
11138
11814
  send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
11139
11815
  }
@@ -11650,7 +12326,7 @@ async function handleSageDelete(ws, msg, memoryStore) {
11650
12326
  }
11651
12327
  try {
11652
12328
  await Sage.deleteSage(id, reason, {
11653
- force: force ?? true,
12329
+ force: force === true,
11654
12330
  neverInject: neverInject === true
11655
12331
  });
11656
12332
  send(ws, {
@@ -12276,7 +12952,7 @@ function openBrowser(url, platform = process.platform) {
12276
12952
  }
12277
12953
 
12278
12954
  // src/server/port-utils.ts
12279
- import * as net from "node:net";
12955
+ import * as net3 from "node:net";
12280
12956
  import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
12281
12957
  var SURFACE_DEFAULT_PORTS = {
12282
12958
  webui: { http: 3456 },
@@ -12290,7 +12966,7 @@ function getSurfaceDefaultPorts(surface) {
12290
12966
  }
12291
12967
  function isPortFree(host, port) {
12292
12968
  return new Promise((resolve16) => {
12293
- const srv = net.createServer();
12969
+ const srv = net3.createServer();
12294
12970
  srv.once("error", () => resolve16(false));
12295
12971
  srv.once("listening", () => {
12296
12972
  srv.close(() => resolve16(true));
@@ -12419,7 +13095,8 @@ async function startStaticServe(opts, deps2 = {}) {
12419
13095
  projectRoot: opts.projectRoot,
12420
13096
  publicWsUrl: opts.publicWsUrl,
12421
13097
  apiToken: opts.apiToken,
12422
- requireToken: opts.requireToken
13098
+ requireToken: opts.requireToken,
13099
+ allowedHostnames: opts.allowedHostnames
12423
13100
  });
12424
13101
  if (!opts.deferListen) {
12425
13102
  server.listen(opts.httpPort, opts.host);
@@ -12753,6 +13430,7 @@ function createWebuiClientPresence(deps2) {
12753
13430
  let heartbeatTimer = null;
12754
13431
  let stopTelemetry;
12755
13432
  let hqConnection;
13433
+ let closed = false;
12756
13434
  const register = async () => {
12757
13435
  if (!deps2.projectRoot) return null;
12758
13436
  try {
@@ -12792,6 +13470,7 @@ function createWebuiClientPresence(deps2) {
12792
13470
  }
12793
13471
  });
12794
13472
  clientId = `webui@${crypto2.randomUUID().slice(0, 8)}`;
13473
+ closed = false;
12795
13474
  await nextMailbox.registerClient({
12796
13475
  clientId,
12797
13476
  sessionId: deps2.getSessionId(),
@@ -12799,6 +13478,10 @@ function createWebuiClientPresence(deps2) {
12799
13478
  source: "webui",
12800
13479
  pid: process.pid
12801
13480
  });
13481
+ if (closed) {
13482
+ nextMailbox.deregisterClient(clientId).catch(() => void 0);
13483
+ return null;
13484
+ }
12802
13485
  heartbeatTimer = setInterval(() => {
12803
13486
  nextMailbox.clientHeartbeat({ clientId, sessionId: deps2.getSessionId() }).catch(() => void 0);
12804
13487
  }, CLIENT_HEARTBEAT_MS);
@@ -12809,6 +13492,7 @@ function createWebuiClientPresence(deps2) {
12809
13492
  }
12810
13493
  };
12811
13494
  const unregister = () => {
13495
+ closed = true;
12812
13496
  if (heartbeatTimer) clearInterval(heartbeatTimer);
12813
13497
  heartbeatTimer = null;
12814
13498
  stopTelemetry?.();
@@ -13682,6 +14366,7 @@ function seedContextMeta(config, context) {
13682
14366
  meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
13683
14367
  meta["nextPrediction"] = config.nextPrediction ?? false;
13684
14368
  meta["fallbackModels"] = config.fallbackModels ?? [];
14369
+ meta["fallbackBridge"] = config.fallbackBridge ?? "";
13685
14370
  meta["fallbackProfiles"] = config.fallbackProfiles ?? {};
13686
14371
  meta["favoriteModels"] = config.favoriteModels ?? [];
13687
14372
  meta["favoriteModelsOnly"] = config.favoriteModelsOnly === true;
@@ -13718,7 +14403,7 @@ function seedContextMeta(config, context) {
13718
14403
  meta["refinerModel"] = autonomyCfg["refinerModel"] ?? "";
13719
14404
  meta["refinerFallbackProfile"] = autonomyCfg["refinerFallbackProfile"] ?? "";
13720
14405
  meta["thinkingWord"] = autonomyCfg["thinkingWord"] ?? "thinking";
13721
- meta["statuslineMode"] = autonomyCfg["statuslineMode"] ?? "detailed";
14406
+ meta["statuslineMode"] = autonomyCfg["statuslineMode"] ?? "minimum";
13722
14407
  meta["animationStyle"] = autonomyCfg["animationStyle"] ?? "rainbow";
13723
14408
  meta["showModelReasoning"] = autonomyCfg["showModelReasoning"] !== false;
13724
14409
  meta["breakerEnabled"] = config.circuitBreaker?.enabled === true;
@@ -13764,7 +14449,10 @@ function seedContextMeta(config, context) {
13764
14449
  try {
13765
14450
  const mgr = new FallbackProfileManager(config);
13766
14451
  const named = autoReviewExt?.["fallbackProfile"];
13767
- resolvedChain = typeof named === "string" && named.length > 0 ? mgr.resolve(named) : mgr.resolveEffective({ fallbackAuto: true });
14452
+ resolvedChain = typeof named === "string" && named.length > 0 ? mgr.resolveEffective({ fallbackProfile: named, fallbackAuto: false }) : mgr.resolveEffective({
14453
+ fallbackModels: config.fallbackModels,
14454
+ fallbackAuto: config.fallbackAuto !== false
14455
+ });
13768
14456
  } catch {
13769
14457
  resolvedChain = [];
13770
14458
  }
@@ -13908,7 +14596,28 @@ async function updateGlobalConfig(deps2, holder, mutate, errorLabel) {
13908
14596
  );
13909
14597
  }
13910
14598
  }
13911
- var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set(["groupToolCalls", "showThinkingLogs", "autoReviewFallbackModels"]);
14599
+ var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set([
14600
+ "groupToolCalls",
14601
+ "showThinkingLogs",
14602
+ "autoReviewFallbackModels",
14603
+ // v11 Display parity: agent-swarm panel + inverse fsAccess flag.
14604
+ // The TUI settings picker mirrors these so the browser can keep the
14605
+ // panel state in sync without round-tripping through config.
14606
+ "showAgentSwarmPanel",
14607
+ "allowOutsideProjectRoot",
14608
+ // v13 Display parity: codebase-index read-tool, SAGE memory-inject
14609
+ // surfacing, pre-refine countdown, inject threshold, multi-diff
14610
+ // footer threshold, refine-panel enhance countdown. All six are
14611
+ // pure panel-toggles; the underlying engine knobs (when applicable)
14612
+ // live in the TUI keyboard shortcut or the agent runtime, not in
14613
+ // config.
14614
+ "readSymbols",
14615
+ "showSageMemoryInject",
14616
+ "preRefineSeconds",
14617
+ "sageMemoryInjectThreshold",
14618
+ "multiDiffSummaryThreshold",
14619
+ "enhanceCountdownMs"
14620
+ ]);
13912
14621
  async function persistPrefsToConfig(deps2, holder, payload) {
13913
14622
  payload = { ...payload };
13914
14623
  for (const k of DISPLAY_ONLY_KEYS) delete payload[k];
@@ -13923,7 +14632,7 @@ async function persistPrefsToConfig(deps2, holder, payload) {
13923
14632
  autonomyCfg[key] = val;
13924
14633
  autonomyTouched = true;
13925
14634
  };
13926
- if (typeof payload["autonomy"] === "string" && ["off", "suggest", "auto"].includes(payload["autonomy"])) {
14635
+ if (typeof payload["autonomy"] === "string" && ["off", "suggest", "auto", "eternal", "eternal-parallel"].includes(payload["autonomy"])) {
13927
14636
  setAutonomy("defaultMode", payload["autonomy"]);
13928
14637
  }
13929
14638
  if (typeof payload["autonomyDelayMs"] === "number")
@@ -14219,14 +14928,14 @@ import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
14219
14928
  import { getProcessRegistry as getProcessRegistry2 } from "@wrongstack/tools";
14220
14929
 
14221
14930
  // src/server/privileged-actions.ts
14222
- import { randomUUID as randomUUID5 } from "node:crypto";
14931
+ import { randomUUID as randomUUID4 } from "node:crypto";
14223
14932
  import {
14224
14933
  isTrustDecisionAllowed
14225
14934
  } from "@wrongstack/core/security";
14226
14935
  async function authorizeWebUIAction(boundary, action, logger) {
14227
14936
  const request = {
14228
14937
  version: 1,
14229
- requestId: randomUUID5(),
14938
+ requestId: randomUUID4(),
14230
14939
  actor: {
14231
14940
  kind: "remote-client",
14232
14941
  ...action.sessionId ? { sessionId: action.sessionId } : {}
@@ -14593,7 +15302,7 @@ function createProjectHandlers(ctx) {
14593
15302
 
14594
15303
  // src/server/provider-handlers.ts
14595
15304
  import { resolveProviderModelList } from "@wrongstack/core/models";
14596
- import { DefaultSecretScrubber } from "@wrongstack/core/security";
15305
+ import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
14597
15306
  import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
14598
15307
  import {
14599
15308
  beginOAuthLogin
@@ -14865,7 +15574,7 @@ function projectSavedProviders(providers) {
14865
15574
  return view;
14866
15575
  });
14867
15576
  }
14868
- var probeScrubber = new DefaultSecretScrubber();
15577
+ var probeScrubber = new DefaultSecretScrubber2();
14869
15578
  async function probeModelDescriptors(cfg) {
14870
15579
  if (!cfg.baseUrl) return [];
14871
15580
  try {
@@ -18552,6 +19261,7 @@ var SddBoardWebSocketHandler = class {
18552
19261
  poll = null;
18553
19262
  pollInFlight = false;
18554
19263
  unsub = null;
19264
+ disposed = false;
18555
19265
  constructor(boardsDir, events, lifecycle, security) {
18556
19266
  this.store = new SddBoardStore({ baseDir: boardsDir });
18557
19267
  this.lifecycle = lifecycle;
@@ -18566,6 +19276,7 @@ var SddBoardWebSocketHandler = class {
18566
19276
  }
18567
19277
  }
18568
19278
  addClient(ws) {
19279
+ if (this.disposed) return;
18569
19280
  const client = { ws, id: crypto.randomUUID() };
18570
19281
  this.clients.add(client);
18571
19282
  const remove = () => {
@@ -18707,9 +19418,12 @@ var SddBoardWebSocketHandler = class {
18707
19418
  }
18708
19419
  }
18709
19420
  dispose() {
19421
+ this.disposed = true;
18710
19422
  this.stopPolling();
18711
19423
  this.unsub?.();
18712
19424
  this.unsub = null;
19425
+ this.clients.clear();
19426
+ this.latest = null;
18713
19427
  }
18714
19428
  // ── internal ────────────────────────────────────────────────────────────
18715
19429
  async pollLatest() {
@@ -18717,6 +19431,7 @@ var SddBoardWebSocketHandler = class {
18717
19431
  this.pollInFlight = true;
18718
19432
  try {
18719
19433
  const snap = await this.loadLatestSnapshot();
19434
+ if (this.disposed) return;
18720
19435
  if (!snap) return;
18721
19436
  if (this.latest && this.latest.updatedAt >= snap.updatedAt && this.latest.runId === snap.runId) {
18722
19437
  return;
@@ -19030,6 +19745,7 @@ var SddWizardWebSocketHandler = class {
19030
19745
  * flight (or after the probe has settled).
19031
19746
  */
19032
19747
  resumeProbe = null;
19748
+ disposed = false;
19033
19749
  async bootstrap() {
19034
19750
  try {
19035
19751
  await this.deps.ensureReady?.();
@@ -19042,23 +19758,28 @@ var SddWizardWebSocketHandler = class {
19042
19758
  if (this.driver) return;
19043
19759
  try {
19044
19760
  const driver = this.deps.makeDriver();
19045
- if (await driver.loadExisting()) {
19761
+ const loaded = await driver.loadExisting();
19762
+ if (this.disposed) return;
19763
+ if (loaded) {
19046
19764
  this.driver = driver;
19047
19765
  this.lastAgentText = driver.getLastAgentText() ?? "";
19048
19766
  }
19049
19767
  this.resumeError = null;
19050
19768
  } catch (error2) {
19769
+ if (this.disposed) return;
19051
19770
  this.driver = null;
19052
19771
  this.lastAgentText = "";
19053
19772
  this.resumeError = error2 instanceof Error ? error2.message : String(error2);
19054
19773
  }
19055
19774
  }
19056
19775
  addClient(ws) {
19776
+ if (this.disposed) return;
19057
19777
  const client = { ws, id: crypto.randomUUID() };
19058
19778
  this.clients.add(client);
19059
19779
  ws.on("close", () => this.clients.delete(client));
19060
19780
  ws.on("error", () => this.clients.delete(client));
19061
19781
  void this.ready.then(() => {
19782
+ if (this.disposed || !this.clients.has(client)) return;
19062
19783
  if (this.resumeError) {
19063
19784
  this.send(client, { type: "sdd.spec.error", payload: { message: this.resumeError } });
19064
19785
  return;
@@ -19071,7 +19792,16 @@ var SddWizardWebSocketHandler = class {
19071
19792
  }
19072
19793
  });
19073
19794
  }
19795
+ dispose() {
19796
+ this.disposed = true;
19797
+ this.clients.clear();
19798
+ this.driver = null;
19799
+ this.lastAgentText = "";
19800
+ this.busy = false;
19801
+ this.resumeProbe = null;
19802
+ }
19074
19803
  async handleMessage(msg) {
19804
+ if (this.disposed) return;
19075
19805
  try {
19076
19806
  await this.ready;
19077
19807
  if (this.resumeError) {
@@ -19289,6 +20019,7 @@ var SddWizardWebSocketHandler = class {
19289
20019
  this.broadcast(this.snapshotMsg());
19290
20020
  try {
19291
20021
  const text2 = await this.deps.runInterviewTurn(prompt);
20022
+ if (this.disposed) return;
19292
20023
  this.lastAgentText = text2;
19293
20024
  if (this.driver) {
19294
20025
  await this.driver.ingestAgentOutput(text2);
@@ -19314,6 +20045,7 @@ var SddWizardWebSocketHandler = class {
19314
20045
  }
19315
20046
  }
19316
20047
  send(client, msg) {
20048
+ if (!this.clients.has(client)) return;
19317
20049
  sendSerialized(client.ws, JSON.stringify(msg));
19318
20050
  }
19319
20051
  };
@@ -20080,6 +20812,7 @@ function setupEvents(deps2) {
20080
20812
  });
20081
20813
  pendingConfirms.set(id, {
20082
20814
  resolve: e.resolve,
20815
+ sessionId: e.sessionId,
20083
20816
  decisionSource: e.decisionSource,
20084
20817
  riskTier: e.riskTier,
20085
20818
  boundaryReason: e.boundaryReason,
@@ -20608,6 +21341,9 @@ var SpecsWebSocketHandler = class {
20608
21341
  ws.on("error", () => this.clients.delete(client));
20609
21342
  void this.sendList(client);
20610
21343
  }
21344
+ dispose() {
21345
+ this.clients.clear();
21346
+ }
20611
21347
  async handleMessage(msg) {
20612
21348
  switch (msg.type) {
20613
21349
  case "specs.list":
@@ -20743,12 +21479,13 @@ var SpecsWebSocketHandler = class {
20743
21479
  }
20744
21480
  }
20745
21481
  send(client, msg) {
21482
+ if (!this.clients.has(client)) return;
20746
21483
  sendSerialized(client.ws, JSON.stringify(msg));
20747
21484
  }
20748
21485
  };
20749
21486
 
20750
21487
  // src/server/start-webui.ts
20751
- import { randomUUID as randomUUID6 } from "node:crypto";
21488
+ import { randomUUID as randomUUID5 } from "node:crypto";
20752
21489
  import * as http2 from "node:http";
20753
21490
  import * as path34 from "node:path";
20754
21491
  import { createDefaultPipelines } from "@wrongstack/core/agent";
@@ -20773,7 +21510,7 @@ import { toLanguagePackageInput } from "@wrongstack/techstack";
20773
21510
  import { ensureSessionShell } from "@wrongstack/tools";
20774
21511
 
20775
21512
  // src/server/backend-services.ts
20776
- import { join as join16 } from "node:path";
21513
+ import { join as join15 } from "node:path";
20777
21514
  import { Agent } from "@wrongstack/core/agent";
20778
21515
  import {
20779
21516
  BrainDecisionLedger,
@@ -20786,7 +21523,7 @@ import {
20786
21523
  mailboxSessionTag,
20787
21524
  ObservableBrainArbiter as ObservableBrainArbiterCtor
20788
21525
  } from "@wrongstack/core/coordination";
20789
- import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/defaults";
21526
+ import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/types";
20790
21527
  import { installDesignStudioMiddleware } from "@wrongstack/core/design";
20791
21528
  import {
20792
21529
  AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
@@ -20816,7 +21553,7 @@ import {
20816
21553
  import { spawn as spawn4 } from "node:child_process";
20817
21554
  import { createRequire } from "node:module";
20818
21555
  import { existsSync as existsSync2 } from "node:fs";
20819
- import { dirname as dirname10, join as join14 } from "node:path";
21556
+ import { dirname as dirname10, join as join13 } from "node:path";
20820
21557
  import { resolveProjectDir as resolveProjectDir3 } from "@wrongstack/core/coordination";
20821
21558
  import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
20822
21559
  import { readLiveLock } from "@wrongstack/core/coordination";
@@ -20945,7 +21682,7 @@ function mailboxServeInvocation(projectRoot) {
20945
21682
  function findWorkspaceCliEntry(projectRoot) {
20946
21683
  let dir = projectRoot;
20947
21684
  for (let i = 0; i < 6; i++) {
20948
- const candidate = join14(dir, "packages", "cli", "dist", "index.js");
21685
+ const candidate = join13(dir, "packages", "cli", "dist", "index.js");
20949
21686
  if (existsSync2(candidate)) return candidate;
20950
21687
  const parent = dirname10(dir);
20951
21688
  if (parent === dir) return null;
@@ -21190,7 +21927,7 @@ function clampDim(value, fallback) {
21190
21927
  }
21191
21928
 
21192
21929
  // src/server/worktree-ws-handler.ts
21193
- import { join as join15, resolve as resolve13, sep as sep5 } from "node:path";
21930
+ import { join as join14, resolve as resolve13, sep as sep5 } from "node:path";
21194
21931
  import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
21195
21932
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
21196
21933
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
@@ -21267,11 +22004,15 @@ var WorktreeWebSocketHandler = class {
21267
22004
  for (const off of this.offs) off();
21268
22005
  this.offs.length = 0;
21269
22006
  this.stopBroadcast();
22007
+ this.clients.clear();
22008
+ this.handles.clear();
22009
+ this.baseBranch = "";
22010
+ this.scanRescanNeeded = false;
21270
22011
  }
21271
22012
  // ── orphan management ─────────────────────────────────────────────────────
21272
22013
  /** Absolute managed-worktrees root for this project. */
21273
22014
  worktreesRoot() {
21274
- return resolve13(join15(this.management.projectRoot, ".wrongstack", "worktrees"));
22015
+ return resolve13(join14(this.management.projectRoot, ".wrongstack", "worktrees"));
21275
22016
  }
21276
22017
  /** True iff `dir` resolves strictly inside the managed worktrees root. */
21277
22018
  underRoot(dir) {
@@ -21664,7 +22405,13 @@ async function createAgentServices(input) {
21664
22405
  minImportance: config.Sage?.inject?.minImportance,
21665
22406
  repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
21666
22407
  verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
21667
- triggers: config.Sage?.inject?.triggers
22408
+ triggers: config.Sage?.inject?.triggers,
22409
+ // Resolve the live session so retrieval and cooldown are session-scoped
22410
+ // (matching the CLI wiring in wiring/sage.ts). Without this, the
22411
+ // middleware falls back to ctx.session.id for cooldown but passes
22412
+ // undefined to retrieval, causing owned session-scoped memories to be
22413
+ // silently excluded from tool-call injection.
22414
+ getSessionId: () => input.sessionGetter().id
21668
22415
  })
21669
22416
  );
21670
22417
  }
@@ -21788,6 +22535,7 @@ async function createAgentServices(input) {
21788
22535
  perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
21789
22536
  tracer: void 0
21790
22537
  });
22538
+ input.installToolBoundary?.(pipelines);
21791
22539
  const webuiLogger = container.resolve(TOKENS2.Logger);
21792
22540
  void discoverMailboxBridgeForWebui({
21793
22541
  projectRoot,
@@ -21827,7 +22575,7 @@ async function createAgentServices(input) {
21827
22575
  const brainCfg = resolveBrainConfigDefaults(config.brain, {
21828
22576
  fallbackModels: config.fallbackModels
21829
22577
  });
21830
- const brainLedgerPath = join16(wpaths.projectDir, "brain-ledger.jsonl");
22578
+ const brainLedgerPath = join15(wpaths.projectDir, "brain-ledger.jsonl");
21831
22579
  let brainLedgerEnabled = brainCfg.ledger?.enabled !== false;
21832
22580
  let brainLedger;
21833
22581
  const startBrainLedger = () => {
@@ -22000,7 +22748,8 @@ async function createAgentServices(input) {
22000
22748
  // subagent's fallback extension's tracker hooks are undefined and
22001
22749
  // round-robin keeps reassigning the doomed model. Mirrors the CLI
22002
22750
  // factory wiring at host-subagent-factory.ts:337.
22003
- statusTracker: container.safeResolve(TOKENS2.ProviderModelStatusTracker)
22751
+ statusTracker: container.safeResolve(TOKENS2.ProviderModelStatusTracker),
22752
+ ...input.installToolBoundary ? { installToolBoundary: input.installToolBoundary } : {}
22004
22753
  }),
22005
22754
  paths: {
22006
22755
  projectSpecs: wpaths.projectSpecs,
@@ -22039,6 +22788,8 @@ async function createAgentServices(input) {
22039
22788
  for (const off of brainLogOffs) off();
22040
22789
  goalHandler.dispose();
22041
22790
  sddBoardHandler.dispose();
22791
+ sddWizardHandler.dispose();
22792
+ specsHandler.dispose();
22042
22793
  worktreeHandler.dispose();
22043
22794
  terminalHandler.dispose();
22044
22795
  collabHandler.dispose();
@@ -22074,7 +22825,10 @@ async function createAgentServices(input) {
22074
22825
  }
22075
22826
 
22076
22827
  // src/server/connection-handler.ts
22077
- var RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
22828
+ var DEFAULT_WS_RATE_LIMIT = 600;
22829
+ var rawRateLimit = process.env["WEBUI_RATE_LIMIT"];
22830
+ var parsedRateLimit = rawRateLimit ? Number.parseInt(rawRateLimit, 10) : DEFAULT_WS_RATE_LIMIT;
22831
+ var RATE_LIMIT_MESSAGES = Number.isFinite(parsedRateLimit) && parsedRateLimit >= 0 ? parsedRateLimit : DEFAULT_WS_RATE_LIMIT;
22078
22832
  var VERBOSE_WS_LOGGING = process.env["WEBUI_VERBOSE_WS"] === "1" || process.env["WEBUI_VERBOSE_WS"] === "true";
22079
22833
  function readRemoteAddress(ws) {
22080
22834
  const socket = ws._socket;
@@ -22154,6 +22908,72 @@ function createConnectionHandler(options) {
22154
22908
  };
22155
22909
  }
22156
22910
 
22911
+ // src/server/governance-runtime.ts
22912
+ import {
22913
+ createGovernanceMutationSnapshotBridge
22914
+ } from "@wrongstack/runtime/governance-mutation-snapshot-bridge";
22915
+ import { sanitizeGovernanceMessage } from "@wrongstack/runtime/governance-sanitize";
22916
+ var WRONGSTACK_GOVERNANCE_ENV = "WRONGSTACK_GOVERNANCE";
22917
+ var DEFAULT_DEPENDENCIES = {
22918
+ load: () => import("@wrongstack/runtime/governance-bootstrap"),
22919
+ createMutationBridge: createGovernanceMutationSnapshotBridge
22920
+ };
22921
+ function webUiGovernanceRuntimeEnabled(environment) {
22922
+ return environment[WRONGSTACK_GOVERNANCE_ENV]?.trim() === "1";
22923
+ }
22924
+ async function setupWebUiGovernance(input, dependencies = DEFAULT_DEPENDENCIES) {
22925
+ if (!webUiGovernanceRuntimeEnabled(input.environment)) return void 0;
22926
+ let loaded;
22927
+ try {
22928
+ loaded = await dependencies.load();
22929
+ } catch (error2) {
22930
+ input.logger.warn("governance: standalone WebUI runtime module unavailable", {
22931
+ message: sanitizeGovernanceMessage(error2 instanceof Error ? error2.message : String(error2))
22932
+ });
22933
+ return void 0;
22934
+ }
22935
+ let result;
22936
+ try {
22937
+ result = await loaded.bootstrapGovernanceRuntime({
22938
+ projectRoot: input.projectRoot,
22939
+ projectId: input.projectId,
22940
+ adminClientId: `webui-control-${process.pid}-${input.sessionId}`,
22941
+ modelClientId: `webui-model-${input.sessionId}`,
22942
+ modelCapabilities: ["task_read", "command_submit", "shadow_observe"]
22943
+ });
22944
+ } catch (error2) {
22945
+ input.logger.warn("governance: standalone WebUI bootstrap failed open", {
22946
+ message: sanitizeGovernanceMessage(error2 instanceof Error ? error2.message : String(error2))
22947
+ });
22948
+ return void 0;
22949
+ }
22950
+ if (result.mode !== "governed") {
22951
+ input.contextMeta["governance"] = result;
22952
+ input.logger.warn(`governance: standalone WebUI legacy fallback (${result.code})`, {
22953
+ phase: result.phase,
22954
+ cleanup: result.cleanup,
22955
+ message: result.message
22956
+ });
22957
+ return void 0;
22958
+ }
22959
+ const snapshot = result.handle.snapshot();
22960
+ input.contextMeta["governance"] = snapshot;
22961
+ input.logger.info(`governance: ${snapshot.source} standalone WebUI control plane`);
22962
+ const mutationSnapshots = dependencies.createMutationBridge({
22963
+ events: input.events,
22964
+ sink: result.handle,
22965
+ captureWorkspaceCheckpoint: input.captureWorkspaceCheckpoint,
22966
+ logger: input.logger
22967
+ });
22968
+ return Object.freeze({
22969
+ installToolBoundary: (pipelines) => mutationSnapshots.installToolBoundary(pipelines),
22970
+ close: async () => {
22971
+ await mutationSnapshots.close();
22972
+ return result.handle.close();
22973
+ }
22974
+ });
22975
+ }
22976
+
22157
22977
  // src/server/message-dispatcher.ts
22158
22978
  import path28 from "node:path";
22159
22979
  function createMessageDispatcher(opts) {
@@ -23858,6 +24678,17 @@ async function startWebUI(opts = {}) {
23858
24678
  const prefSnapshot2 = () => prefSnapshot(context.meta);
23859
24679
  const persistPrefsToConfig2 = async (payload) => persistPrefsToConfig(prefHelperDeps, configWriteLock, payload);
23860
24680
  const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary3({ policyId: "webui-trusted-host-compat-v1" });
24681
+ const governanceHandle = opts.installToolBoundary === void 0 ? await setupWebUiGovernance({
24682
+ environment: process.env,
24683
+ projectRoot,
24684
+ projectId: wpaths.projectSlug,
24685
+ sessionId: session.id,
24686
+ contextMeta: context.meta,
24687
+ events,
24688
+ logger,
24689
+ captureWorkspaceCheckpoint: async () => sessionStore.captureWorkspaceCheckpoint?.(session.id, 0)
24690
+ }) : void 0;
24691
+ const installToolBoundary = opts.installToolBoundary ?? governanceHandle?.installToolBoundary;
23861
24692
  const agentServices = await createAgentServices({
23862
24693
  trustBoundary,
23863
24694
  config,
@@ -23880,6 +24711,7 @@ async function startWebUI(opts = {}) {
23880
24711
  skillInstaller,
23881
24712
  tokenCounter,
23882
24713
  pipelines: createDefaultPipelines(),
24714
+ ...installToolBoundary ? { installToolBoundary } : {},
23883
24715
  modelCapabilitiesRef,
23884
24716
  sessionGetter: () => session,
23885
24717
  sessionReader,
@@ -23961,7 +24793,7 @@ async function startWebUI(opts = {}) {
23961
24793
  const input = toLanguagePackageInput(operation, workspace);
23962
24794
  const use = {
23963
24795
  type: "tool_use",
23964
- id: `techstack-remediation-${randomUUID6()}`,
24796
+ id: `techstack-remediation-${randomUUID5()}`,
23965
24797
  name: "language_package",
23966
24798
  input: { ...input }
23967
24799
  };
@@ -24230,7 +25062,8 @@ async function startWebUI(opts = {}) {
24230
25062
  patchConfig(state.getConfig(), {
24231
25063
  providers: snapshot.providers,
24232
25064
  ...snapshot.apiKey !== void 0 ? { apiKey: snapshot.apiKey } : {},
24233
- ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {}
25065
+ ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {},
25066
+ fallbackBridge: snapshot.fallbackBridge ?? ""
24234
25067
  })
24235
25068
  );
24236
25069
  deps2.configStore.update({
@@ -24238,6 +25071,7 @@ async function startWebUI(opts = {}) {
24238
25071
  ...snapshot.apiKey !== void 0 ? { apiKey: snapshot.apiKey } : {},
24239
25072
  ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {},
24240
25073
  ...snapshot.fallbackModels !== void 0 ? { fallbackModels: snapshot.fallbackModels } : {},
25074
+ fallbackBridge: snapshot.fallbackBridge ?? "",
24241
25075
  ...snapshot.fallbackProfiles !== void 0 ? { fallbackProfiles: snapshot.fallbackProfiles } : {},
24242
25076
  ...snapshot.favoriteModels !== void 0 ? { favoriteModels: snapshot.favoriteModels } : {},
24243
25077
  ...snapshot.favoriteModelsOnly !== void 0 ? { favoriteModelsOnly: snapshot.favoriteModelsOnly } : {},
@@ -24372,6 +25206,12 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
24372
25206
  await stopHeapWatchdog();
24373
25207
  credentialWatcherClose?.();
24374
25208
  disposeRealtimeHandlers();
25209
+ const governanceCleanup = await governanceHandle?.close();
25210
+ if (governanceCleanup && !governanceCleanup.ok) {
25211
+ logger.warn(`governance: standalone WebUI ${governanceCleanup.action} cleanup failed`, {
25212
+ message: governanceCleanup.message
25213
+ });
25214
+ }
24375
25215
  brainMonitor.stop();
24376
25216
  await agentServices.brainLedger?.stop().catch(() => {
24377
25217
  });