@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
@@ -6,9 +6,13 @@ import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/ty
6
6
  import { getProcessRegistry } from "@wrongstack/tools";
7
7
 
8
8
  // src/server/pending-confirms.ts
9
+ function isDestructivePendingConfirm(confirm) {
10
+ return confirm.riskTier === "destructive" || confirm.decisionSource === "yolo_destructive";
11
+ }
9
12
  function resolveYoloEligiblePendingConfirms(pendingConfirms) {
10
13
  for (const [id, confirm] of pendingConfirms) {
11
14
  if (confirm.boundaryReason) continue;
15
+ if (isDestructivePendingConfirm(confirm)) continue;
12
16
  pendingConfirms.delete(id);
13
17
  confirm.resolve("yes");
14
18
  }
@@ -72,7 +76,18 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
72
76
  "showModelReasoning",
73
77
  // Display-only toggles (purely visual, persisted in localStorage via Zustand).
74
78
  "groupToolCalls",
75
- "showThinkingLogs"
79
+ "showThinkingLogs",
80
+ // v11 Display parity: agent-swarm panel + inverse fsAccess flag.
81
+ "showAgentSwarmPanel",
82
+ "allowOutsideProjectRoot",
83
+ // v13 Display parity (TUI SettingsPicker fields 42 & 43): the read tool
84
+ // includes codebase-index symbols, and SAGE memory-inject blocks are
85
+ // surfaced in tool results. Both are pure WebUI-display toggles mirrored
86
+ // against the TUI's settings picker; the server mirrors them through
87
+ // `prefs.update` exactly so the panel does not error with
88
+ // "unknown preference key".
89
+ "readSymbols",
90
+ "showSageMemoryInject"
76
91
  ]);
77
92
  var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set([
78
93
  "fallbackModels",
@@ -93,14 +108,39 @@ var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
93
108
  "maxIterations",
94
109
  "maxConcurrent",
95
110
  "enhanceDelayMs",
111
+ "enhanceCountdownMs",
96
112
  "tgLongToolMs",
97
113
  "breakerAutoKillResetMs",
98
114
  // Chimera + auto-review numeric knobs
99
115
  "chimeraMaxFiles",
100
116
  "autoReviewDebounceMs",
101
117
  "autoReviewMaxFilesPerBatch",
102
- "autoReviewMaxConcurrentReviews"
118
+ "autoReviewMaxConcurrentReviews",
119
+ // v13 Display parity (TUI SettingsPicker fields 41, 44, 21): the
120
+ // pre-refine grace countdown (seconds), the SAGE memory-inject relation
121
+ // floor (0..1), and the minimum file count for the multi-diff summary
122
+ // footer. All three are driven by the WebUI sliders in DisplaySection
123
+ // and previously tripped the "unknown preference key" rejection.
124
+ "preRefineSeconds",
125
+ "sageMemoryInjectThreshold",
126
+ "multiDiffSummaryThreshold"
103
127
  ]);
128
+ var NUMBER_PREF_BOUNDS = {
129
+ // Iteration / concurrency — must be at least 1 to make progress.
130
+ maxIterations: { min: 1, max: Number.POSITIVE_INFINITY },
131
+ maxConcurrent: { min: 1, max: Number.POSITIVE_INFINITY },
132
+ autoProceedMaxIterations: { min: 1, max: Number.POSITIVE_INFINITY },
133
+ chimeraMaxFiles: { min: 1, max: Number.POSITIVE_INFINITY },
134
+ autoReviewMaxFilesPerBatch: { min: 1, max: Number.POSITIVE_INFINITY },
135
+ autoReviewMaxConcurrentReviews: { min: 1, max: Number.POSITIVE_INFINITY },
136
+ // Counts (must be >= 1 to render a footer at all).
137
+ multiDiffSummaryThreshold: { min: 1, max: Number.POSITIVE_INFINITY },
138
+ preRefineSeconds: { min: 0, max: Number.POSITIVE_INFINITY },
139
+ // Probability — must lie in [0, 1].
140
+ sageMemoryInjectThreshold: { min: 0, max: 1 },
141
+ // Debounce / delay — non-negative ms.
142
+ autoReviewDebounceMs: { min: 0, max: Number.POSITIVE_INFINITY }
143
+ };
104
144
  var STRING_PREF_KEYS = /* @__PURE__ */ new Set([
105
145
  "hqUrl",
106
146
  "hqToken",
@@ -222,7 +262,15 @@ function validatePreferenceValue(key, value) {
222
262
  return typeof value === "boolean" ? null : `prefs.update payload.${key} must be a boolean`;
223
263
  }
224
264
  if (NUMBER_PREF_KEYS.has(key)) {
225
- return typeof value === "number" && Number.isFinite(value) ? null : `prefs.update payload.${key} must be a finite number`;
265
+ if (typeof value !== "number" || !Number.isFinite(value)) {
266
+ return `prefs.update payload.${key} must be a finite number`;
267
+ }
268
+ const bounds = NUMBER_PREF_BOUNDS[key];
269
+ if (bounds && (value < bounds.min || value > bounds.max)) {
270
+ const maxStr = bounds.max === Number.POSITIVE_INFINITY ? "\u221E" : String(bounds.max);
271
+ return `prefs.update payload.${key} must be in [${bounds.min}, ${maxStr}]`;
272
+ }
273
+ return null;
226
274
  }
227
275
  if (STRING_PREF_KEYS.has(key)) {
228
276
  return typeof value === "string" ? null : `prefs.update payload.${key} must be a string`;
@@ -246,9 +294,16 @@ function validatePreferenceValue(key, value) {
246
294
  return null;
247
295
  }
248
296
  if (STRING_ARRAY_RECORD_PREF_KEYS.has(key)) {
249
- return isRecord(value) && Object.values(value).every(
297
+ if (!isRecord(value) || !Object.values(value).every(
250
298
  (v) => Array.isArray(v) && v.every((item) => typeof item === "string")
251
- ) ? null : `prefs.update payload.${key} must be an object of string arrays`;
299
+ )) {
300
+ return `prefs.update payload.${key} must be an object of string arrays`;
301
+ }
302
+ const badKey = Object.keys(value).find((k) => FORBIDDEN_PROTO_KEYS.has(k));
303
+ if (badKey) {
304
+ return `prefs.update payload.${key} contains a forbidden key: ${badKey}`;
305
+ }
306
+ return null;
252
307
  }
253
308
  if (BOOLEAN_RECORD_PREF_KEYS.has(key)) {
254
309
  if (!isRecord(value) || !Object.values(value).every((v) => typeof v === "boolean")) {
@@ -262,6 +317,10 @@ function validatePreferenceValue(key, value) {
262
317
  }
263
318
  if (MODEL_MATRIX_PREF_KEYS.has(key)) {
264
319
  if (!isRecord(value)) return `prefs.update payload.${key} must be an object`;
320
+ const badMatrixKey = Object.keys(value).find((k) => FORBIDDEN_PROTO_KEYS.has(k));
321
+ if (badMatrixKey) {
322
+ return `prefs.update payload.${key} contains a forbidden key: ${badMatrixKey}`;
323
+ }
265
324
  for (const entry of Object.values(value)) {
266
325
  if (!isRecord(entry)) return `prefs.update payload.${key} entries must be objects`;
267
326
  const provider = entry["provider"];
@@ -839,6 +898,58 @@ function validateProjectsSelectPayload(payload) {
839
898
  value: { root, ...typeof name2 === "string" ? { name: name2 } : {} }
840
899
  };
841
900
  }
901
+ var MCP_MAX_STRING = 4096;
902
+ var MCP_MAX_ARRAY = 256;
903
+ function isStringArray(value, max = MCP_MAX_ARRAY) {
904
+ return Array.isArray(value) && value.length <= max && value.every((item) => typeof item === "string" && item.length <= MCP_MAX_STRING);
905
+ }
906
+ function isStringRecord(value) {
907
+ if (!isRecord2(value)) return false;
908
+ const entries = Object.entries(value);
909
+ if (entries.length > MCP_MAX_ARRAY) return false;
910
+ return entries.every(
911
+ ([key, item]) => typeof key === "string" && key.length <= MCP_MAX_STRING && typeof item === "string" && item.length <= MCP_MAX_STRING
912
+ );
913
+ }
914
+ function validateMcpServerPayload(payload, label) {
915
+ if (!isRecord2(payload)) {
916
+ return { ok: false, message: `${label} payload must be an object` };
917
+ }
918
+ const name2 = payload["name"];
919
+ if (typeof name2 !== "string" || name2.trim().length === 0 || name2.length > MCP_MAX_STRING) {
920
+ return { ok: false, message: `${label} payload.name must be a non-empty string` };
921
+ }
922
+ for (const key of ["description", "command", "url", "transport", "permission"]) {
923
+ const value = payload[key];
924
+ if (value !== void 0 && (typeof value !== "string" || value.length > MCP_MAX_STRING)) {
925
+ if (!(key === "transport" && isRecord2(value))) {
926
+ return { ok: false, message: `${label} payload.${key} must be a string` };
927
+ }
928
+ }
929
+ }
930
+ for (const key of ["enabled", "lazy"]) {
931
+ const value = payload[key];
932
+ if (value !== void 0 && typeof value !== "boolean") {
933
+ return { ok: false, message: `${label} payload.${key} must be a boolean` };
934
+ }
935
+ }
936
+ for (const key of ["args", "allowedTools", "passthroughEnv"]) {
937
+ const value = payload[key];
938
+ if (value !== void 0 && !isStringArray(value)) {
939
+ return { ok: false, message: `${label} payload.${key} must be an array of strings` };
940
+ }
941
+ }
942
+ for (const key of ["env", "headers"]) {
943
+ const value = payload[key];
944
+ if (value !== void 0 && !isStringRecord(value)) {
945
+ return {
946
+ ok: false,
947
+ message: `${label} payload.${key} must be an object of string values`
948
+ };
949
+ }
950
+ }
951
+ return { ok: true, value: payload };
952
+ }
842
953
 
843
954
  // src/server/prefs-handlers.ts
844
955
  function sendResult(ctx, ws, success, message) {
@@ -875,6 +986,9 @@ function handlePrefsUpdate(ctx, ws, input) {
875
986
  const payload = parsed.value.prefs;
876
987
  for (const [key, value] of Object.entries(payload)) ctx.meta[key] = value;
877
988
  void ctx.persist(payload);
989
+ if (typeof payload["autonomy"] === "string") {
990
+ ctx.setAutonomy?.(payload["autonomy"]);
991
+ }
878
992
  if (typeof payload["yolo"] === "boolean") {
879
993
  ctx.setYolo?.(payload["yolo"]);
880
994
  if (payload["yolo"]) resolveYoloEligiblePendingConfirms(ctx.pendingConfirms);
@@ -911,6 +1025,7 @@ function handleAutonomySwitch(ctx, ws, mode) {
911
1025
 
912
1026
  // src/server/ws-utils.ts
913
1027
  import { randomBytes } from "node:crypto";
1028
+ import { scrubErrorDetail } from "@wrongstack/core/security";
914
1029
  import { WebSocket } from "ws";
915
1030
  var WEBUI_WS_MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
916
1031
  function sendSerialized(ws, data) {
@@ -948,7 +1063,7 @@ function sendResult2(ws, success, message) {
948
1063
  send(ws, { type: "key.operation_result", payload: { success, message } });
949
1064
  }
950
1065
  function errMessage(err) {
951
- return err instanceof Error ? err.message : String(err);
1066
+ return scrubErrorDetail(err);
952
1067
  }
953
1068
  function generateAuthToken() {
954
1069
  return randomBytes(16).toString("hex");
@@ -1241,7 +1356,16 @@ import * as path from "node:path";
1241
1356
  import { resolveIndexDir } from "@wrongstack/tools/codebase-index/index";
1242
1357
  var DB_FILE = "index.db";
1243
1358
  var MAX_CACHE_ENTRIES = 128;
1359
+ var MAX_CACHE_BODY_CHARS = 4 * 1024 * 1024;
1360
+ var MAX_CACHE_TOTAL_CHARS = 16 * 1024 * 1024;
1244
1361
  var cache = /* @__PURE__ */ new Map();
1362
+ var cacheChars = 0;
1363
+ function deleteCachedCodemapBody(key) {
1364
+ const entry = cache.get(key);
1365
+ if (!entry) return false;
1366
+ cacheChars = Math.max(0, cacheChars - entry.body.length);
1367
+ return cache.delete(key);
1368
+ }
1245
1369
  function indexDbVersion(projectRoot, indexDir) {
1246
1370
  try {
1247
1371
  const dir = resolveIndexDir(projectRoot, indexDir);
@@ -1258,7 +1382,7 @@ function getCachedCodemapBody(key, version) {
1258
1382
  const entry = cache.get(key);
1259
1383
  if (!entry) return void 0;
1260
1384
  if (entry.version !== version) {
1261
- cache.delete(key);
1385
+ deleteCachedCodemapBody(key);
1262
1386
  return void 0;
1263
1387
  }
1264
1388
  cache.delete(key);
@@ -1266,16 +1390,19 @@ function getCachedCodemapBody(key, version) {
1266
1390
  return entry.body;
1267
1391
  }
1268
1392
  function setCachedCodemapBody(key, version, body) {
1269
- if (cache.has(key)) cache.delete(key);
1393
+ if (cache.has(key)) deleteCachedCodemapBody(key);
1394
+ if (body.length > MAX_CACHE_BODY_CHARS) return;
1270
1395
  cache.set(key, { version, body });
1271
- while (cache.size > MAX_CACHE_ENTRIES) {
1396
+ cacheChars += body.length;
1397
+ while (cache.size > MAX_CACHE_ENTRIES || cacheChars > MAX_CACHE_TOTAL_CHARS) {
1272
1398
  const oldest = cache.keys().next().value;
1273
1399
  if (oldest === void 0) break;
1274
- cache.delete(oldest);
1400
+ deleteCachedCodemapBody(oldest);
1275
1401
  }
1276
1402
  }
1277
1403
  function clearCodemapGraphCache() {
1278
1404
  cache.clear();
1405
+ cacheChars = 0;
1279
1406
  }
1280
1407
 
1281
1408
  // src/server/codebase-indexing.ts
@@ -1566,6 +1693,7 @@ var CollaborationWebSocketHandler = class {
1566
1693
  this.offs.length = 0;
1567
1694
  this.stopBroadcast();
1568
1695
  this.clients.clear();
1696
+ this.bySession.clear();
1569
1697
  }
1570
1698
  // ── Inbound client messages ────────────────────────────────────────────
1571
1699
  /**
@@ -2769,6 +2897,7 @@ import {
2769
2897
  materializeTokens,
2770
2898
  recordKitChoice,
2771
2899
  recordOverrides,
2900
+ resolveMaterializeTarget,
2772
2901
  resolveSemanticTune,
2773
2902
  runDesignVerify,
2774
2903
  setActiveKit,
@@ -2980,7 +3109,7 @@ async function handleDesignMaterialize(ws, ctx, msg) {
2980
3109
  kitId: active.kit,
2981
3110
  outPath: typeof payload.out === "string" ? payload.out : void 0
2982
3111
  });
2983
- const abs = path5.join(ctx.projectRoot, result.path);
3112
+ const abs = await resolveMaterializeTarget(result.path, ctx.projectRoot);
2984
3113
  await fs3.mkdir(path5.dirname(abs), { recursive: true });
2985
3114
  await fs3.writeFile(abs, result.content);
2986
3115
  send(ws, {
@@ -3133,7 +3262,29 @@ async function resolveFileInsideProject(projectRoot, filePath) {
3133
3262
  if (!isPathInside(realProjectRoot, realFull)) {
3134
3263
  throw new ToolValidationError2({ message: "Path outside project root", field: "path" });
3135
3264
  }
3136
- return realFull;
3265
+ const realFinal = await canonicalizeFinalComponent(realFull);
3266
+ if (!isPathInside(realProjectRoot, realFinal)) {
3267
+ throw new ToolValidationError2({ message: "Path outside project root", field: "path" });
3268
+ }
3269
+ return realFinal;
3270
+ }
3271
+ async function canonicalizeFinalComponent(p) {
3272
+ let stat3;
3273
+ try {
3274
+ stat3 = await fs5.lstat(p);
3275
+ } catch (err) {
3276
+ if (err.code === "ENOENT") return p;
3277
+ throw err;
3278
+ }
3279
+ if (!stat3.isSymbolicLink()) return p;
3280
+ try {
3281
+ return await fs5.realpath(p);
3282
+ } catch (err) {
3283
+ if (err.code === "ENOENT") {
3284
+ throw new ToolValidationError2({ message: "Path outside project root", field: "path" });
3285
+ }
3286
+ throw err;
3287
+ }
3137
3288
  }
3138
3289
  function splitParentAndBase(p) {
3139
3290
  const base = path7.basename(p);
@@ -4166,13 +4317,12 @@ function createConversationOperations(ctx) {
4166
4317
  },
4167
4318
  ping: (ws) => ctx.send(ws, { type: "pong", payload: {} }),
4168
4319
  confirmTool: (_ws, msg) => {
4169
- const requested = requestedSessionId(msg);
4170
- const current2 = ctx.getSessionId();
4171
- if (requested && current2 && requested !== current2) return;
4172
4320
  const { id, decision } = msg.payload ?? {};
4173
4321
  if (typeof id !== "string") return;
4174
4322
  const confirm = ctx.pendingConfirms.get(id);
4175
4323
  if (!confirm) return;
4324
+ const claimedSession = requestedSessionId(msg) ?? ctx.getSessionId();
4325
+ if (confirm.sessionId !== void 0 && confirm.sessionId !== claimedSession) return;
4176
4326
  ctx.pendingConfirms.delete(id);
4177
4327
  confirm.resolve(decision);
4178
4328
  }
@@ -4181,7 +4331,17 @@ function createConversationOperations(ctx) {
4181
4331
 
4182
4332
  // src/server/context-editor.ts
4183
4333
  import { createHash } from "node:crypto";
4184
- import { repairToolUseAdjacency } from "@wrongstack/core/utils";
4334
+ import net from "node:net";
4335
+ import {
4336
+ ALLOWED_IMAGE_MEDIA_TYPES,
4337
+ base64DecodedBytes,
4338
+ isAllowedImageMediaType,
4339
+ isPrivateIPv4,
4340
+ isPrivateIPv6,
4341
+ isValidImageBase64,
4342
+ MAX_INCOMING_IMAGE_BYTES,
4343
+ repairToolUseAdjacency
4344
+ } from "@wrongstack/core/utils";
4185
4345
 
4186
4346
  // src/server/token-estimator.ts
4187
4347
  function estimateTokens(s) {
@@ -4245,6 +4405,40 @@ var REVISION_PREFIX = "wrongstack-context-editor-v1\0";
4245
4405
  var MAX_MESSAGE_COUNT_GROWTH = 10;
4246
4406
  var MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
4247
4407
  var MAX_STRING_LENGTH = 8 * 1024 * 1024;
4408
+ var MAX_IMAGE_URL_LENGTH = 2048;
4409
+ function imageUrlRejectionReason(url) {
4410
+ if (url.length > MAX_IMAGE_URL_LENGTH) {
4411
+ return `image.source.url exceeds ${MAX_IMAGE_URL_LENGTH} characters.`;
4412
+ }
4413
+ let parsed;
4414
+ try {
4415
+ parsed = new URL(url);
4416
+ } catch {
4417
+ return "image.source.url must be an absolute URL.";
4418
+ }
4419
+ if (parsed.protocol !== "https:") {
4420
+ return `image.source.url must use https (got "${parsed.protocol}").`;
4421
+ }
4422
+ if (parsed.username !== "" || parsed.password !== "") {
4423
+ return "image.source.url must not embed credentials.";
4424
+ }
4425
+ const host = parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]") ? parsed.hostname.slice(1, -1) : parsed.hostname;
4426
+ const bareHost = host.endsWith(".") ? host.slice(0, -1) : host;
4427
+ if (bareHost === "") {
4428
+ return "image.source.url must include a hostname.";
4429
+ }
4430
+ if (bareHost === "localhost" || bareHost.endsWith(".localhost")) {
4431
+ return "image.source.url must not target localhost.";
4432
+ }
4433
+ const family = net.isIP(bareHost);
4434
+ if (family === 4 && isPrivateIPv4(bareHost)) {
4435
+ return `image.source.url must not target a private or loopback address ("${bareHost}").`;
4436
+ }
4437
+ if (family === 6 && isPrivateIPv6(bareHost)) {
4438
+ return `image.source.url must not target a private or loopback address ("${bareHost}").`;
4439
+ }
4440
+ return void 0;
4441
+ }
4248
4442
  function isRecord3(value) {
4249
4443
  return value !== null && typeof value === "object" && !Array.isArray(value);
4250
4444
  }
@@ -4317,7 +4511,11 @@ function validateBlock(value, path30, errors) {
4317
4511
  error(errors, `${path30}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
4318
4512
  return void 0;
4319
4513
  }
4320
- const cacheControl = validateCacheControl(value["cache_control"], `${path30}/cache_control`, errors);
4514
+ const cacheControl = validateCacheControl(
4515
+ value["cache_control"],
4516
+ `${path30}/cache_control`,
4517
+ errors
4518
+ );
4321
4519
  return cacheControl ? { type: "text", text, cache_control: cacheControl } : { type: "text", text };
4322
4520
  }
4323
4521
  case "tool_use": {
@@ -4325,15 +4523,29 @@ function validateBlock(value, path30, errors) {
4325
4523
  const name2 = value["name"];
4326
4524
  const input = value["input"];
4327
4525
  if (typeof id !== "string" || id.length === 0) {
4328
- error(errors, `${path30}/id`, "INVALID_TOOL_USE_ID", "tool_use.id must be a non-empty string.");
4526
+ error(
4527
+ errors,
4528
+ `${path30}/id`,
4529
+ "INVALID_TOOL_USE_ID",
4530
+ "tool_use.id must be a non-empty string."
4531
+ );
4329
4532
  }
4330
4533
  if (typeof name2 !== "string" || name2.length === 0) {
4331
- error(errors, `${path30}/name`, "INVALID_TOOL_NAME", "tool_use.name must be a non-empty string.");
4534
+ error(
4535
+ errors,
4536
+ `${path30}/name`,
4537
+ "INVALID_TOOL_NAME",
4538
+ "tool_use.name must be a non-empty string."
4539
+ );
4332
4540
  }
4333
4541
  if (!isPlainJsonObject(input)) {
4334
4542
  error(errors, `${path30}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
4335
4543
  }
4336
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path30}/providerMeta`, errors);
4544
+ const providerMeta = validateProviderMeta(
4545
+ value["providerMeta"],
4546
+ `${path30}/providerMeta`,
4547
+ errors
4548
+ );
4337
4549
  if (typeof id !== "string" || id.length === 0 || typeof name2 !== "string" || name2.length === 0 || !isPlainJsonObject(input)) {
4338
4550
  return void 0;
4339
4551
  }
@@ -4345,20 +4557,46 @@ function validateBlock(value, path30, errors) {
4345
4557
  const content = value["content"];
4346
4558
  const isError = value["is_error"];
4347
4559
  if (typeof toolUseId !== "string" || toolUseId.length === 0) {
4348
- error(errors, `${path30}/tool_use_id`, "INVALID_TOOL_RESULT_ID", "tool_result.tool_use_id must be a non-empty string.");
4560
+ error(
4561
+ errors,
4562
+ `${path30}/tool_use_id`,
4563
+ "INVALID_TOOL_RESULT_ID",
4564
+ "tool_result.tool_use_id must be a non-empty string."
4565
+ );
4349
4566
  }
4350
4567
  if (name2 !== void 0 && typeof name2 !== "string") {
4351
- error(errors, `${path30}/name`, "INVALID_TOOL_RESULT_NAME", "tool_result.name must be a string.");
4568
+ error(
4569
+ errors,
4570
+ `${path30}/name`,
4571
+ "INVALID_TOOL_RESULT_NAME",
4572
+ "tool_result.name must be a string."
4573
+ );
4352
4574
  }
4353
4575
  if (typeof content !== "string") {
4354
- error(errors, `${path30}/content`, "INVALID_TOOL_RESULT_CONTENT", "tool_result.content must be a string.");
4576
+ error(
4577
+ errors,
4578
+ `${path30}/content`,
4579
+ "INVALID_TOOL_RESULT_CONTENT",
4580
+ "tool_result.content must be a string."
4581
+ );
4355
4582
  } else if (content.length > MAX_STRING_LENGTH) {
4356
- error(errors, `${path30}/content`, "TOOL_RESULT_TOO_LARGE", "tool_result.content is too large.");
4583
+ error(
4584
+ errors,
4585
+ `${path30}/content`,
4586
+ "TOOL_RESULT_TOO_LARGE",
4587
+ "tool_result.content is too large."
4588
+ );
4357
4589
  }
4358
4590
  if (isError !== void 0 && typeof isError !== "boolean") {
4359
- error(errors, `${path30}/is_error`, "INVALID_TOOL_RESULT_ERROR", "tool_result.is_error must be boolean.");
4591
+ error(
4592
+ errors,
4593
+ `${path30}/is_error`,
4594
+ "INVALID_TOOL_RESULT_ERROR",
4595
+ "tool_result.is_error must be boolean."
4596
+ );
4360
4597
  }
4361
- if (typeof toolUseId !== "string" || toolUseId.length === 0 || typeof content !== "string") return void 0;
4598
+ if (typeof toolUseId !== "string" || toolUseId.length === 0 || typeof content !== "string")
4599
+ return void 0;
4362
4600
  return {
4363
4601
  type: "tool_result",
4364
4602
  tool_use_id: toolUseId,
@@ -4375,28 +4613,92 @@ function validateBlock(value, path30, errors) {
4375
4613
  }
4376
4614
  const sourceType = source["type"];
4377
4615
  if (sourceType !== "base64" && sourceType !== "url") {
4378
- error(errors, `${path30}/source/type`, "INVALID_IMAGE_SOURCE_TYPE", "image.source.type must be base64 or url.");
4616
+ error(
4617
+ errors,
4618
+ `${path30}/source/type`,
4619
+ "INVALID_IMAGE_SOURCE_TYPE",
4620
+ "image.source.type must be base64 or url."
4621
+ );
4379
4622
  return void 0;
4380
4623
  }
4381
4624
  const mediaType = source["media_type"];
4382
4625
  const data = source["data"];
4383
4626
  const url = source["url"];
4384
4627
  if (mediaType !== void 0 && typeof mediaType !== "string") {
4385
- error(errors, `${path30}/source/media_type`, "INVALID_IMAGE_MEDIA_TYPE", "image.source.media_type must be a string.");
4628
+ error(
4629
+ errors,
4630
+ `${path30}/source/media_type`,
4631
+ "INVALID_IMAGE_MEDIA_TYPE",
4632
+ "image.source.media_type must be a string."
4633
+ );
4634
+ return void 0;
4635
+ }
4636
+ if (typeof mediaType === "string" && !isAllowedImageMediaType(mediaType)) {
4637
+ error(
4638
+ errors,
4639
+ `${path30}/source/media_type`,
4640
+ "UNSUPPORTED_IMAGE_MEDIA_TYPE",
4641
+ `image.source.media_type must be one of: ${[...ALLOWED_IMAGE_MEDIA_TYPES].join(", ")}.`
4642
+ );
4643
+ return void 0;
4644
+ }
4645
+ if (sourceType === "base64") {
4646
+ if (typeof data !== "string" || data.length === 0) {
4647
+ error(
4648
+ errors,
4649
+ `${path30}/source/data`,
4650
+ "INVALID_IMAGE_DATA",
4651
+ "image.source.data must be a non-empty string for a base64 source."
4652
+ );
4653
+ return void 0;
4654
+ }
4655
+ if (!isValidImageBase64(data)) {
4656
+ error(
4657
+ errors,
4658
+ `${path30}/source/data`,
4659
+ "INVALID_IMAGE_DATA",
4660
+ "image.source.data is not valid base64."
4661
+ );
4662
+ return void 0;
4663
+ }
4664
+ if (base64DecodedBytes(data) > MAX_INCOMING_IMAGE_BYTES) {
4665
+ error(
4666
+ errors,
4667
+ `${path30}/source/data`,
4668
+ "IMAGE_TOO_LARGE",
4669
+ `image.source.data exceeds the ${MAX_INCOMING_IMAGE_BYTES / (1024 * 1024)} MB limit.`
4670
+ );
4671
+ return void 0;
4672
+ }
4673
+ return {
4674
+ type: "image",
4675
+ source: {
4676
+ type: "base64",
4677
+ ...typeof mediaType === "string" ? { media_type: mediaType } : {},
4678
+ data
4679
+ }
4680
+ };
4386
4681
  }
4387
- if (data !== void 0 && typeof data !== "string") {
4388
- error(errors, `${path30}/source/data`, "INVALID_IMAGE_DATA", "image.source.data must be a string.");
4682
+ if (typeof url !== "string" || url.length === 0) {
4683
+ error(
4684
+ errors,
4685
+ `${path30}/source/url`,
4686
+ "INVALID_IMAGE_URL",
4687
+ "image.source.url must be a non-empty string for a url source."
4688
+ );
4689
+ return void 0;
4389
4690
  }
4390
- if (url !== void 0 && typeof url !== "string") {
4391
- error(errors, `${path30}/source/url`, "INVALID_IMAGE_URL", "image.source.url must be a string.");
4691
+ const urlError = imageUrlRejectionReason(url);
4692
+ if (urlError !== void 0) {
4693
+ error(errors, `${path30}/source/url`, "UNSAFE_IMAGE_URL", urlError);
4694
+ return void 0;
4392
4695
  }
4393
4696
  return {
4394
4697
  type: "image",
4395
4698
  source: {
4396
- type: sourceType,
4699
+ type: "url",
4397
4700
  ...typeof mediaType === "string" ? { media_type: mediaType } : {},
4398
- ...typeof data === "string" ? { data } : {},
4399
- ...typeof url === "string" ? { url } : {}
4701
+ url
4400
4702
  }
4401
4703
  };
4402
4704
  }
@@ -4404,13 +4706,27 @@ function validateBlock(value, path30, errors) {
4404
4706
  const thinking = value["thinking"];
4405
4707
  const signature = value["signature"];
4406
4708
  if (typeof thinking !== "string") {
4407
- error(errors, `${path30}/thinking`, "INVALID_THINKING", "thinking.thinking must be a string.");
4709
+ error(
4710
+ errors,
4711
+ `${path30}/thinking`,
4712
+ "INVALID_THINKING",
4713
+ "thinking.thinking must be a string."
4714
+ );
4408
4715
  return void 0;
4409
4716
  }
4410
4717
  if (signature !== void 0 && typeof signature !== "string") {
4411
- error(errors, `${path30}/signature`, "INVALID_THINKING_SIGNATURE", "thinking.signature must be a string.");
4718
+ error(
4719
+ errors,
4720
+ `${path30}/signature`,
4721
+ "INVALID_THINKING_SIGNATURE",
4722
+ "thinking.signature must be a string."
4723
+ );
4412
4724
  }
4413
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path30}/providerMeta`, errors);
4725
+ const providerMeta = validateProviderMeta(
4726
+ value["providerMeta"],
4727
+ `${path30}/providerMeta`,
4728
+ errors
4729
+ );
4414
4730
  return {
4415
4731
  type: "thinking",
4416
4732
  thinking,
@@ -4419,7 +4735,12 @@ function validateBlock(value, path30, errors) {
4419
4735
  };
4420
4736
  }
4421
4737
  default:
4422
- error(errors, `${path30}/type`, "UNKNOWN_BLOCK_TYPE", `Unknown content block type: ${String(type)}`);
4738
+ error(
4739
+ errors,
4740
+ `${path30}/type`,
4741
+ "UNKNOWN_BLOCK_TYPE",
4742
+ `Unknown content block type: ${String(type)}`
4743
+ );
4423
4744
  return void 0;
4424
4745
  }
4425
4746
  }
@@ -4431,7 +4752,12 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4431
4752
  return { messages, errors };
4432
4753
  }
4433
4754
  if (value.length > currentMessageCount + MAX_MESSAGE_COUNT_GROWTH) {
4434
- error(errors, "/messages", "TOO_MANY_MESSAGES", "Context editor is deletion-oriented; proposed message count grew too much.");
4755
+ error(
4756
+ errors,
4757
+ "/messages",
4758
+ "TOO_MANY_MESSAGES",
4759
+ "Context editor is deletion-oriented; proposed message count grew too much."
4760
+ );
4435
4761
  }
4436
4762
  if (jsonByteLength(value) > MAX_PAYLOAD_BYTES) {
4437
4763
  error(errors, "/messages", "PAYLOAD_TOO_LARGE", "Context editor payload is too large.");
@@ -4444,7 +4770,12 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4444
4770
  }
4445
4771
  const role = item["role"];
4446
4772
  if (!isMessageRole(role)) {
4447
- error(errors, `${path30}/role`, "INVALID_ROLE", "Message role must be user, assistant, or system.");
4773
+ error(
4774
+ errors,
4775
+ `${path30}/role`,
4776
+ "INVALID_ROLE",
4777
+ "Message role must be user, assistant, or system."
4778
+ );
4448
4779
  return;
4449
4780
  }
4450
4781
  const rawContent = item["content"];
@@ -4463,13 +4794,23 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4463
4794
  });
4464
4795
  content = blocks;
4465
4796
  } else {
4466
- error(errors, `${path30}/content`, "INVALID_CONTENT", "Message content must be a string or content block array.");
4797
+ error(
4798
+ errors,
4799
+ `${path30}/content`,
4800
+ "INVALID_CONTENT",
4801
+ "Message content must be a string or content block array."
4802
+ );
4467
4803
  return;
4468
4804
  }
4469
4805
  const ts = item["ts"];
4470
4806
  if (ts !== void 0) {
4471
4807
  if (typeof ts !== "string" || Number.isNaN(Date.parse(ts))) {
4472
- error(errors, `${path30}/ts`, "INVALID_TIMESTAMP", "Message ts must be an ISO-like timestamp string.");
4808
+ error(
4809
+ errors,
4810
+ `${path30}/ts`,
4811
+ "INVALID_TIMESTAMP",
4812
+ "Message ts must be an ISO-like timestamp string."
4813
+ );
4473
4814
  return;
4474
4815
  }
4475
4816
  }
@@ -4479,7 +4820,8 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4479
4820
  }
4480
4821
  function countBlocks(messages) {
4481
4822
  let total = 0;
4482
- for (const message of messages) total += Array.isArray(message.content) ? message.content.length : 1;
4823
+ for (const message of messages)
4824
+ total += Array.isArray(message.content) ? message.content.length : 1;
4483
4825
  return total;
4484
4826
  }
4485
4827
  function toolDiagnostics(messages) {
@@ -4543,11 +4885,13 @@ function metricFor(ctx, messages, tools) {
4543
4885
  };
4544
4886
  }
4545
4887
  function buildContextEditorSnapshot(ctx, tools) {
4546
- const messages = ctx.messages.map((message) => ({
4547
- role: message.role,
4548
- content: message.content,
4549
- ...message.ts ? { ts: message.ts } : {}
4550
- }));
4888
+ const messages = ctx.messages.map(
4889
+ (message) => ({
4890
+ role: message.role,
4891
+ content: message.content,
4892
+ ...message.ts ? { ts: message.ts } : {}
4893
+ })
4894
+ );
4551
4895
  const breakdown = estimateContextBreakdown({
4552
4896
  systemPrompt: ctx.systemPrompt,
4553
4897
  tools: tools ?? ctx.tools ?? [],
@@ -4592,7 +4936,10 @@ function validateContextEditorProposal(input) {
4592
4936
  validationErrors: [],
4593
4937
  warnings: [],
4594
4938
  repair: emptyRepair,
4595
- conflict: { code: "RUN_ACTIVE", message: "The agent is currently running. Abort or wait before applying context edits." }
4939
+ conflict: {
4940
+ code: "RUN_ACTIVE",
4941
+ message: "The agent is currently running. Abort or wait before applying context edits."
4942
+ }
4596
4943
  };
4597
4944
  }
4598
4945
  if (input.baseRevision !== currentRevision) {
@@ -4604,7 +4951,10 @@ function validateContextEditorProposal(input) {
4604
4951
  validationErrors: [],
4605
4952
  warnings: [],
4606
4953
  repair: emptyRepair,
4607
- conflict: { code: "CONTEXT_REVISION_CONFLICT", message: "Context changed while the editor was open. Reload before applying." }
4954
+ conflict: {
4955
+ code: "CONTEXT_REVISION_CONFLICT",
4956
+ message: "Context changed while the editor was open. Reload before applying."
4957
+ }
4608
4958
  };
4609
4959
  }
4610
4960
  const parsed = validateContextEditorMessages(input.messages, input.ctx.messages.length);
@@ -4687,6 +5037,7 @@ async function applyContextEditorProposal(input) {
4687
5037
  }
4688
5038
 
4689
5039
  // src/server/connection-lifecycle.ts
5040
+ var RATE_LIMIT_EXEMPT_TYPES = /* @__PURE__ */ new Set(["ping", "pong"]);
4690
5041
  function createConnectionLifecycle(options) {
4691
5042
  const rateLimitMax = options.rateLimitMax ?? 0;
4692
5043
  const rateLimitWindowMs = options.rateLimitWindowMs ?? 6e4;
@@ -4757,25 +5108,31 @@ function createConnectionLifecycle(options) {
4757
5108
  }
4758
5109
  let messageCount = 0;
4759
5110
  let rateWindowResetAt = Date.now() + rateLimitWindowMs;
4760
- ws.on("message", async (data) => {
4761
- if (rateLimitMax > 0) {
4762
- const now = Date.now();
4763
- if (now > rateWindowResetAt) {
4764
- messageCount = 0;
4765
- rateWindowResetAt = now + rateLimitWindowMs;
4766
- }
4767
- if (++messageCount > rateLimitMax) {
4768
- options.send(ws, {
4769
- type: "error",
4770
- payload: options.sessionPayload({
4771
- phase: "rate_limit",
4772
- message: options.rateLimitMessage ?? "Too many messages. Please wait."
4773
- })
4774
- });
4775
- return;
4776
- }
5111
+ let rateLimitNotified = false;
5112
+ const chargeRateLimit = () => {
5113
+ if (!Number.isFinite(rateLimitMax) || rateLimitMax <= 0) return true;
5114
+ const now = Date.now();
5115
+ if (now > rateWindowResetAt) {
5116
+ messageCount = 0;
5117
+ rateWindowResetAt = now + rateLimitWindowMs;
5118
+ rateLimitNotified = false;
5119
+ }
5120
+ if (++messageCount <= rateLimitMax) return true;
5121
+ if (!rateLimitNotified) {
5122
+ rateLimitNotified = true;
5123
+ options.send(ws, {
5124
+ type: "error",
5125
+ payload: options.sessionPayload({
5126
+ phase: "rate_limit",
5127
+ message: options.rateLimitMessage ?? "Too many messages. Please wait."
5128
+ })
5129
+ });
4777
5130
  }
5131
+ return false;
5132
+ };
5133
+ ws.on("message", async (data) => {
4778
5134
  const decoded = options.decode(data.toString());
5135
+ if (!decoded.ok && !chargeRateLimit()) return;
4779
5136
  if (!decoded.ok) {
4780
5137
  const eventName = decoded.issue.message === "Protocol frame is not valid JSON" ? "webui_server.message_parse_failed" : "webui_server.message_rejected";
4781
5138
  const level = decoded.issue.code === "unsafe_key" || decoded.issue.code === "too_deep" ? "error" : "warn";
@@ -4813,6 +5170,10 @@ function createConnectionLifecycle(options) {
4813
5170
  });
4814
5171
  return;
4815
5172
  }
5173
+ const messageType = decoded.message.type;
5174
+ if ((typeof messageType !== "string" || !RATE_LIMIT_EXEMPT_TYPES.has(messageType)) && !chargeRateLimit()) {
5175
+ return;
5176
+ }
4816
5177
  try {
4817
5178
  await options.dispatch(ws, client, decoded.message);
4818
5179
  } catch (error2) {
@@ -4853,10 +5214,16 @@ import {
4853
5214
  MailboxProjectServerConnection
4854
5215
  } from "@wrongstack/core/coordination";
4855
5216
  import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
4856
- import { getKanbanServerConnection } from "@wrongstack/kanban";
5217
+ import {
5218
+ closeKanbanServerConnections,
5219
+ getKanbanServerConnection,
5220
+ isKanbanServerAvailable
5221
+ } from "@wrongstack/kanban";
5222
+ import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
4857
5223
  import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
4858
5224
  import {
4859
5225
  checkCodebaseIndexServerHealth,
5226
+ ensureCodebaseIndexServer as ensureCodebaseIndexServer2,
4860
5227
  getIndexState,
4861
5228
  resolveProjectIndexDaemonAvailability,
4862
5229
  shutdownCodebaseIndexServer
@@ -4885,7 +5252,8 @@ async function collectConnectionsHealth(options) {
4885
5252
  codebaseIndexHealth(options.projectRoot, options.indexDir),
4886
5253
  sageHealth(options.projectRoot),
4887
5254
  kanbanHealth(options.projectRoot),
4888
- mailboxHealth(options.projectRoot)
5255
+ mailboxHealth(options.projectRoot),
5256
+ governanceHealth(options.projectRoot)
4889
5257
  ]);
4890
5258
  const required = services.filter((service) => service.required);
4891
5259
  const overall = required.some((service) => service.status === "error") ? "error" : required.some((service) => service.status !== "healthy") ? "degraded" : "healthy";
@@ -5190,6 +5558,42 @@ async function mailboxHealth(projectRoot) {
5190
5558
  connection.close();
5191
5559
  }
5192
5560
  }
5561
+ async function governanceHealth(projectRoot) {
5562
+ const startedAt = Date.now();
5563
+ const result = await readGovernanceDaemonOperatorStatus(projectRoot);
5564
+ if (!result.available) {
5565
+ const missing = result.code === "broker_missing";
5566
+ return {
5567
+ id: "governance",
5568
+ label: "Governance control plane",
5569
+ status: missing ? "offline" : "error",
5570
+ required: false,
5571
+ mode: missing ? "compatibility-default-off" : "project-daemon",
5572
+ detail: missing ? "Not active for this project. Existing agent and model execution remains unchanged." : `Read-only governance status is unavailable: ${result.message}`,
5573
+ latencyMs: Date.now() - startedAt,
5574
+ control: "none",
5575
+ ...missing ? {} : { lastError: result.message }
5576
+ };
5577
+ }
5578
+ const { status } = result;
5579
+ return {
5580
+ id: "governance",
5581
+ label: "Governance control plane",
5582
+ status: status.signal.level === "healthy" ? "healthy" : "degraded",
5583
+ required: false,
5584
+ mode: "project-daemon-advisory",
5585
+ detail: `${status.signal.message} Execution continues; no automatic task or model stop.`,
5586
+ ownerPid: status.pid,
5587
+ uptimeMs: Math.max(0, Date.now() - Date.parse(status.startedAt)),
5588
+ latencyMs: Date.now() - startedAt,
5589
+ control: "none",
5590
+ advisory: {
5591
+ code: status.signal.code,
5592
+ operatorAction: status.signal.operatorAction,
5593
+ executionDisposition: status.signal.executionDisposition
5594
+ }
5595
+ };
5596
+ }
5193
5597
  function failureService(id, label, required, mode, error2, latencyMs) {
5194
5598
  const message = error2 instanceof Error ? error2.message : String(error2);
5195
5599
  return {
@@ -5466,11 +5870,11 @@ async function handleGitDiff(ws, projectRoot, path30) {
5466
5870
  try {
5467
5871
  const git = makeGit(cwd);
5468
5872
  const { readFile: readFile11 } = await import("node:fs/promises");
5469
- const { join: join16 } = await import("node:path");
5873
+ const { join: join15 } = await import("node:path");
5470
5874
  const oldText = await git(["show", `HEAD:${path30}`]);
5471
5875
  let newText = "";
5472
5876
  try {
5473
- const abs = cwd ? join16(cwd, path30) : path30;
5877
+ const abs = cwd ? join15(cwd, path30) : path30;
5474
5878
  const buf = await readFile11(abs);
5475
5879
  if (buf.includes(0)) {
5476
5880
  reply2({ oldText: "", newText: "", binary: true });
@@ -5636,8 +6040,11 @@ var GoalWebSocketHandler = class {
5636
6040
  this.assessAbort?.abort();
5637
6041
  this.assessAbort = null;
5638
6042
  this.orchestrator?.stop();
6043
+ this.orchestrator = null;
5639
6044
  this.stopBroadcast();
5640
6045
  this.clients.clear();
6046
+ this.usedNicknames.clear();
6047
+ this.worktrees = null;
5641
6048
  }
5642
6049
  async handleMessage(ws, msg) {
5643
6050
  switch (msg.type) {
@@ -6673,84 +7080,9 @@ import * as fs10 from "node:fs/promises";
6673
7080
  import * as http from "node:http";
6674
7081
  import * as path13 from "node:path";
6675
7082
  import * as v8 from "node:v8";
7083
+ import { sanitizeApiError as sanitizeApiError2 } from "@wrongstack/core/security";
6676
7084
  import { getIndexState as getIndexState2 } from "@wrongstack/tools";
6677
7085
 
6678
- // src/server/codemap-handlers.ts
6679
- import { fileGraphService, packageGraphService, symbolGraphService } from "@wrongstack/tools";
6680
- function sendJsonBody(res, status, body, cacheStatus) {
6681
- res.writeHead(status, {
6682
- "Content-Type": "application/json",
6683
- "X-Codemap-Cache": cacheStatus
6684
- });
6685
- res.end(body);
6686
- }
6687
- function sendJson(res, status, data, cacheStatus = "BYPASS") {
6688
- sendJsonBody(res, status, JSON.stringify(data), cacheStatus);
6689
- }
6690
- async function serveCachedGraph(res, deps2, scope, compute) {
6691
- try {
6692
- const version = indexDbVersion(deps2.projectRoot, deps2.indexDir);
6693
- const key = codemapCacheKey(deps2.projectRoot, deps2.indexDir, scope);
6694
- const hit = getCachedCodemapBody(key, version);
6695
- if (hit !== void 0) {
6696
- sendJsonBody(res, 200, hit, "HIT");
6697
- return;
6698
- }
6699
- const graph = await compute();
6700
- const body = JSON.stringify(graph);
6701
- if (version !== "missing") {
6702
- setCachedCodemapBody(key, version, body);
6703
- }
6704
- sendJsonBody(res, 200, body, "MISS");
6705
- } catch (err) {
6706
- const msg = err instanceof Error ? err.message : String(err);
6707
- sendJson(res, 503, { error: "CodeMap index unavailable", detail: msg }, "BYPASS");
6708
- }
6709
- }
6710
- async function handleCodemapPackages(res, deps2) {
6711
- await serveCachedGraph(
6712
- res,
6713
- deps2,
6714
- "packages",
6715
- () => packageGraphService({
6716
- projectRoot: deps2.projectRoot,
6717
- ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
6718
- })
6719
- );
6720
- }
6721
- async function handleCodemapFiles(res, deps2, pkg) {
6722
- if (!pkg) {
6723
- sendJson(res, 400, { error: 'Missing "package" query parameter' });
6724
- return;
6725
- }
6726
- await serveCachedGraph(
6727
- res,
6728
- deps2,
6729
- `files:${pkg}`,
6730
- () => fileGraphService({
6731
- projectRoot: deps2.projectRoot,
6732
- packageFilter: pkg,
6733
- ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
6734
- })
6735
- );
6736
- }
6737
- async function handleCodemapSymbols(res, deps2, file) {
6738
- if (!file) {
6739
- sendJson(res, 400, { error: 'Missing "file" query parameter' });
6740
- return;
6741
- }
6742
- await serveCachedGraph(
6743
- res,
6744
- deps2,
6745
- `symbols:${file}`,
6746
- () => symbolGraphService({
6747
- projectRoot: deps2.projectRoot,
6748
- fileFilter: file,
6749
- ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
6750
- })
6751
- );
6752
- }
6753
-
6754
7086
  // src/server/deadcode-handlers.ts
6755
7087
  import * as path10 from "node:path";
6756
7088
  import { runDeadCodeScan } from "@wrongstack/tools/codebase-index";
@@ -6901,7 +7233,84 @@ function buildActionPlan(result) {
6901
7233
  };
6902
7234
  }
6903
7235
 
7236
+ // src/server/codemap-handlers.ts
7237
+ import { fileGraphService, packageGraphService, symbolGraphService } from "@wrongstack/tools";
7238
+ function sendJsonBody(res, status, body, cacheStatus) {
7239
+ res.writeHead(status, {
7240
+ "Content-Type": "application/json",
7241
+ "X-Codemap-Cache": cacheStatus
7242
+ });
7243
+ res.end(body);
7244
+ }
7245
+ function sendJson(res, status, data, cacheStatus = "BYPASS") {
7246
+ sendJsonBody(res, status, JSON.stringify(data), cacheStatus);
7247
+ }
7248
+ async function serveCachedGraph(res, deps2, scope, compute) {
7249
+ try {
7250
+ const version = indexDbVersion(deps2.projectRoot, deps2.indexDir);
7251
+ const key = codemapCacheKey(deps2.projectRoot, deps2.indexDir, scope);
7252
+ const hit = getCachedCodemapBody(key, version);
7253
+ if (hit !== void 0) {
7254
+ sendJsonBody(res, 200, hit, "HIT");
7255
+ return;
7256
+ }
7257
+ const graph = await compute();
7258
+ const body = JSON.stringify(graph);
7259
+ if (version !== "missing") {
7260
+ setCachedCodemapBody(key, version, body);
7261
+ }
7262
+ sendJsonBody(res, 200, body, "MISS");
7263
+ } catch (err) {
7264
+ const msg = err instanceof Error ? err.message : String(err);
7265
+ sendJson(res, 503, { error: "CodeMap index unavailable", detail: msg }, "BYPASS");
7266
+ }
7267
+ }
7268
+ async function handleCodemapPackages(res, deps2) {
7269
+ await serveCachedGraph(
7270
+ res,
7271
+ deps2,
7272
+ "packages",
7273
+ () => packageGraphService({
7274
+ projectRoot: deps2.projectRoot,
7275
+ ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
7276
+ })
7277
+ );
7278
+ }
7279
+ async function handleCodemapFiles(res, deps2, pkg) {
7280
+ if (!pkg) {
7281
+ sendJson(res, 400, { error: 'Missing "package" query parameter' });
7282
+ return;
7283
+ }
7284
+ await serveCachedGraph(
7285
+ res,
7286
+ deps2,
7287
+ `files:${pkg}`,
7288
+ () => fileGraphService({
7289
+ projectRoot: deps2.projectRoot,
7290
+ packageFilter: pkg,
7291
+ ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
7292
+ })
7293
+ );
7294
+ }
7295
+ async function handleCodemapSymbols(res, deps2, file) {
7296
+ if (!file) {
7297
+ sendJson(res, 400, { error: 'Missing "file" query parameter' });
7298
+ return;
7299
+ }
7300
+ await serveCachedGraph(
7301
+ res,
7302
+ deps2,
7303
+ `symbols:${file}`,
7304
+ () => symbolGraphService({
7305
+ projectRoot: deps2.projectRoot,
7306
+ fileFilter: file,
7307
+ ...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
7308
+ })
7309
+ );
7310
+ }
7311
+
6904
7312
  // src/server/http-server/api-handlers.ts
7313
+ import { sanitizeApiError } from "@wrongstack/core/security";
6905
7314
  async function handleApiSessions(res, globalRoot) {
6906
7315
  if (!globalRoot) {
6907
7316
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -6937,7 +7346,7 @@ async function handleApiSessions(res, globalRoot) {
6937
7346
  res.end(JSON.stringify(result));
6938
7347
  } catch (err) {
6939
7348
  res.writeHead(500, { "Content-Type": "application/json" });
6940
- res.end(JSON.stringify({ error: String(err) }));
7349
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
6941
7350
  }
6942
7351
  }
6943
7352
  async function handleApiSessionAgents(res, globalRoot, sessionId) {
@@ -6972,7 +7381,7 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
6972
7381
  }));
6973
7382
  } catch (err) {
6974
7383
  res.writeHead(500, { "Content-Type": "application/json" });
6975
- res.end(JSON.stringify({ error: String(err) }));
7384
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
6976
7385
  }
6977
7386
  }
6978
7387
  function blocksToText(content) {
@@ -7119,11 +7528,16 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
7119
7528
  );
7120
7529
  } catch (err) {
7121
7530
  res.writeHead(500, { "Content-Type": "application/json" });
7122
- res.end(JSON.stringify({ error: String(err) }));
7531
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7123
7532
  }
7124
7533
  }
7125
7534
  function readJsonBody2(req) {
7126
7535
  return new Promise((resolve15, reject) => {
7536
+ const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
7537
+ if (contentType !== "application/json") {
7538
+ reject(new Error(`Unsupported Content-Type: ${contentType || "(absent)"}`));
7539
+ return;
7540
+ }
7127
7541
  let data = "";
7128
7542
  req.on("data", (chunk) => {
7129
7543
  data += chunk;
@@ -7188,7 +7602,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
7188
7602
  res.end(JSON.stringify({ ok: true, id: sent.id, to, type, delivered: entry.status }));
7189
7603
  } catch (err) {
7190
7604
  res.writeHead(500, { "Content-Type": "application/json" });
7191
- res.end(JSON.stringify({ error: String(err) }));
7605
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7192
7606
  }
7193
7607
  }
7194
7608
  async function handleApiSessionMailbox(res, globalRoot, sessionId) {
@@ -7241,7 +7655,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
7241
7655
  res.end(JSON.stringify({ sessionId, leader: leaderAddr, status: entry.status, thread }));
7242
7656
  } catch (err) {
7243
7657
  res.writeHead(500, { "Content-Type": "application/json" });
7244
- res.end(JSON.stringify({ error: String(err) }));
7658
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7245
7659
  }
7246
7660
  }
7247
7661
  async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
@@ -7282,7 +7696,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
7282
7696
  res.end(JSON.stringify({ ok: true, id: sent.id, to, delivered: entry.status }));
7283
7697
  } catch (err) {
7284
7698
  res.writeHead(500, { "Content-Type": "application/json" });
7285
- res.end(JSON.stringify({ error: String(err) }));
7699
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7286
7700
  }
7287
7701
  }
7288
7702
  async function handleApiFleetBroadcast(res, req, globalRoot) {
@@ -7351,7 +7765,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7351
7765
  res.end(JSON.stringify({ ok: true, delivered, targets: targets.length }));
7352
7766
  } catch (err) {
7353
7767
  res.writeHead(500, { "Content-Type": "application/json" });
7354
- res.end(JSON.stringify({ error: String(err) }));
7768
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
7355
7769
  }
7356
7770
  }
7357
7771
 
@@ -7782,11 +8196,20 @@ import { timingSafeEqual } from "node:crypto";
7782
8196
  function isLoopbackHostname(hostname) {
7783
8197
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
7784
8198
  }
7785
- function isTrustedLoopbackOrigin(origin) {
8199
+ function effectivePort(url) {
8200
+ if (url.port) return url.port;
8201
+ return url.protocol === "https:" ? "443" : "80";
8202
+ }
8203
+ function isTrustedLoopbackOrigin(origin, hostHeader) {
7786
8204
  try {
7787
8205
  const url = new URL(origin);
7788
8206
  if (url.protocol !== "http:" && url.protocol !== "https:") return false;
7789
- return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]";
8207
+ if (!isLoopbackHostname(url.hostname)) return false;
8208
+ const host = (hostHeader ?? "").trim();
8209
+ if (!host) return false;
8210
+ const hostUrl = new URL(`${url.protocol}//${host}`);
8211
+ if (!isLoopbackHostname(hostUrl.hostname)) return false;
8212
+ return effectivePort(url) === effectivePort(hostUrl);
7790
8213
  } catch {
7791
8214
  return false;
7792
8215
  }
@@ -7845,6 +8268,23 @@ function hostHeaderOk(input) {
7845
8268
  }
7846
8269
  return isLoopbackHostname(hostname) || allowedHostname(hostname, input.allowedHostnames);
7847
8270
  }
8271
+ function httpRequestOriginOk(input) {
8272
+ const { origin, hostHeader, wsHost, allowedHostnames } = input;
8273
+ if (!hostHeaderOk({ hostHeader, wsHost, allowedHostnames })) return false;
8274
+ if (origin === void 0 || origin === "") return true;
8275
+ if (origin === "null") return false;
8276
+ try {
8277
+ const parsed = new URL(origin);
8278
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
8279
+ const rawHost = (hostHeader ?? "").trim();
8280
+ if (!rawHost) return false;
8281
+ const requestHost = new URL(`${parsed.protocol}//${rawHost}`).host.toLowerCase();
8282
+ if (parsed.host.toLowerCase() === requestHost) return true;
8283
+ return allowedHostname(parsed.hostname, allowedHostnames);
8284
+ } catch {
8285
+ return false;
8286
+ }
8287
+ }
7848
8288
  function verifyClient(input) {
7849
8289
  const {
7850
8290
  origin,
@@ -7865,13 +8305,13 @@ function verifyClient(input) {
7865
8305
  const remoteIp = remoteAddress ?? "";
7866
8306
  const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
7867
8307
  if (!isRemoteLoopback && isWildcardBind(wsHost) && !urlTokenOk && !cookieTokenOk) return false;
7868
- return urlTokenOk || cookieTokenOk || isLoopbackBind(wsHost) && !requireToken;
8308
+ return urlTokenOk || cookieTokenOk;
7869
8309
  }
7870
8310
  try {
7871
8311
  const { hostname: originHostname } = new URL(origin);
7872
8312
  if (isLoopbackHostname(originHostname)) {
7873
8313
  if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
7874
- return isTrustedLoopbackOrigin(origin);
8314
+ return cookieTokenOk || isTrustedLoopbackOrigin(origin, hostHeader);
7875
8315
  }
7876
8316
  return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
7877
8317
  } catch {
@@ -7933,7 +8373,7 @@ function cspSourceFromUrl(rawUrl) {
7933
8373
  return void 0;
7934
8374
  }
7935
8375
  }
7936
- var EXTRA_SCRIPT_SOURCES = ["'wasm-unsafe-eval'", "'unsafe-inline'"];
8376
+ var EXTRA_SCRIPT_SOURCES = ["'wasm-unsafe-eval'"];
7937
8377
  function buildCspHeader(publicWsUrl, host, port) {
7938
8378
  const connect = /* @__PURE__ */ new Set(["'self'"]);
7939
8379
  const publicWsSource = publicWsUrl ? cspSourceFromUrl(publicWsUrl) : void 0;
@@ -7975,6 +8415,16 @@ function createHttpServer(opts) {
7975
8415
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
7976
8416
  const distDir = path13.resolve(opts.distDir);
7977
8417
  const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
8418
+ const trustedHostnames = (() => {
8419
+ const names = [...opts.allowedHostnames ?? []];
8420
+ if (opts.publicWsUrl) {
8421
+ try {
8422
+ names.push(new URL(opts.publicWsUrl).hostname);
8423
+ } catch {
8424
+ }
8425
+ }
8426
+ return names;
8427
+ })();
7978
8428
  let techStackRuntime = null;
7979
8429
  const getTechStackRuntime = async () => {
7980
8430
  if (!opts.projectRoot) throw new Error("Project root not configured");
@@ -7989,6 +8439,16 @@ function createHttpServer(opts) {
7989
8439
  const server = http.createServer(async (req, res) => {
7990
8440
  try {
7991
8441
  const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
8442
+ if (!httpRequestOriginOk({
8443
+ origin: req.headers.origin,
8444
+ hostHeader: req.headers.host,
8445
+ wsHost: opts.host,
8446
+ allowedHostnames: trustedHostnames
8447
+ })) {
8448
+ res.writeHead(403, { "Content-Type": "application/json", "Cache-Control": "no-store" });
8449
+ res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
8450
+ return;
8451
+ }
7992
8452
  const providedAccessToken = requestToken(req, url);
7993
8453
  const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
7994
8454
  const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
@@ -8192,42 +8652,6 @@ function createHttpServer(opts) {
8192
8652
  );
8193
8653
  return;
8194
8654
  }
8195
- if (url.pathname === "/api/deadcode/scan" && req.method === "POST") {
8196
- if (requireAccessToken && !accessTokenOk) {
8197
- res.writeHead(401, { "Content-Type": "application/json" });
8198
- res.end(JSON.stringify({ error: "Unauthorized" }));
8199
- return;
8200
- }
8201
- if (!opts.projectRoot) {
8202
- res.writeHead(503, { "Content-Type": "application/json" });
8203
- res.end(JSON.stringify({ error: "Project root not configured" }));
8204
- return;
8205
- }
8206
- const deadCodeDeps = {
8207
- projectRoot: opts.projectRoot,
8208
- ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8209
- };
8210
- await handleDeadCodeScan(res, deadCodeDeps, req);
8211
- return;
8212
- }
8213
- if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
8214
- if (requireAccessToken && !accessTokenOk) {
8215
- res.writeHead(401, { "Content-Type": "application/json" });
8216
- res.end(JSON.stringify({ error: "Unauthorized" }));
8217
- return;
8218
- }
8219
- if (!opts.projectRoot) {
8220
- res.writeHead(503, { "Content-Type": "application/json" });
8221
- res.end(JSON.stringify({ error: "Project root not configured" }));
8222
- return;
8223
- }
8224
- const deadCodeDeps = {
8225
- projectRoot: opts.projectRoot,
8226
- ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8227
- };
8228
- await handleDeadCodeActionPlan(res, deadCodeDeps, req);
8229
- return;
8230
- }
8231
8655
  if (url.pathname.startsWith("/api/techstack/")) {
8232
8656
  if (requireAccessToken && !accessTokenOk) {
8233
8657
  res.writeHead(401, { "Content-Type": "application/json" });
@@ -8309,7 +8733,7 @@ function createHttpServer(opts) {
8309
8733
  res.end(
8310
8734
  JSON.stringify({
8311
8735
  error: "TechStack store unavailable",
8312
- detail: error2 instanceof Error ? error2.message : String(error2)
8736
+ detail: sanitizeApiError2(error2)
8313
8737
  })
8314
8738
  );
8315
8739
  return;
@@ -8356,6 +8780,30 @@ function createHttpServer(opts) {
8356
8780
  );
8357
8781
  return;
8358
8782
  }
8783
+ if (url.pathname === "/api/deadcode/scan" && req.method === "POST") {
8784
+ if (!opts.projectRoot) {
8785
+ res.writeHead(400, { "Content-Type": "application/json" });
8786
+ res.end(JSON.stringify({ error: "Project root not configured" }));
8787
+ return;
8788
+ }
8789
+ await handleDeadCodeScan(res, {
8790
+ projectRoot: opts.projectRoot,
8791
+ indexDir: opts.indexDir
8792
+ }, req);
8793
+ return;
8794
+ }
8795
+ if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
8796
+ await handleDeadCodeActionPlan(res, {
8797
+ projectRoot: opts.projectRoot ?? "",
8798
+ indexDir: opts.indexDir
8799
+ }, req);
8800
+ return;
8801
+ }
8802
+ if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
8803
+ res.writeHead(404, { "Content-Type": "application/json", "Cache-Control": "no-store" });
8804
+ res.end(JSON.stringify({ error: "Not found" }));
8805
+ return;
8806
+ }
8359
8807
  let filePath;
8360
8808
  if (url.pathname === "/" || url.pathname === "") {
8361
8809
  filePath = path13.join(distDir, "index.html");
@@ -8654,6 +9102,11 @@ async function handleIntrospectionRoute(ctx, ws, message) {
8654
9102
  cwd: ctx.getProjectRoot(),
8655
9103
  sessionId: ctx.getSessionId(),
8656
9104
  tools: { count: tools.length, names: tools.map((tool) => tool.name) },
9105
+ maxTools: actx.provider.maxToolsCount ?? 0,
9106
+ droppedTools: (() => {
9107
+ const mt = actx.provider.maxToolsCount ?? 0;
9108
+ return mt > 0 ? Math.max(0, tools.length - mt) : 0;
9109
+ })(),
8657
9110
  features: {
8658
9111
  memory: !!config.features?.memory,
8659
9112
  skills: !!config.features?.skills,
@@ -8755,40 +9208,17 @@ async function handleIntrospectionRoute(ctx, ws, message) {
8755
9208
  }
8756
9209
 
8757
9210
  // src/server/kanban-dispatch.ts
8758
- import { randomUUID as randomUUID3 } from "node:crypto";
8759
9211
  import {
8760
9212
  areDependenciesMet,
8761
9213
  getServerKanbanStore
8762
9214
  } from "@wrongstack/kanban";
8763
9215
  import { recordKanbanVerificationEvidence } from "@wrongstack/tools";
8764
- function parseResolvedDispatchRoute(summary) {
8765
- const tags = summary.match(/Spawned subagent\s+\S+\s+\((.*?)\)\s+for task/i)?.[1];
8766
- if (!tags) return {};
8767
- const parts = tags.split(/\s+\/\s+/).map((part) => part.trim());
8768
- const positional = parts.filter((part) => !part.includes("=") && !part.startsWith('"'));
8769
- const fallbackProfile = parts.find((part) => part.startsWith("profile="))?.slice(8);
8770
- const fallback = parts.find((part) => part.startsWith("fallback="))?.slice(9);
8771
- return {
8772
- ...positional[0] ? { provider: positional[0] } : {},
8773
- ...positional[1] ? { model: positional[1] } : {},
8774
- ...fallbackProfile ? { fallbackProfile } : {},
8775
- ...fallback ? { fallbackModels: fallback.split(",").filter(Boolean) } : {}
8776
- };
8777
- }
8778
9216
  function reply(ws, type, success, value) {
8779
9217
  send(ws, {
8780
9218
  type,
8781
9219
  payload: success ? { success: true, data: value } : { success: false, error: String(value) }
8782
9220
  });
8783
9221
  }
8784
- function activityContext(ctx, note, expectedLeaseId) {
8785
- const sessionId = ctx.context?.session?.id;
8786
- return {
8787
- ...sessionId ? { sessionId } : {},
8788
- ...note?.trim() ? { note: note.trim() } : {},
8789
- ...expectedLeaseId ? { expectedLeaseId } : {}
8790
- };
8791
- }
8792
9222
  async function handleKanbanTaskDispatch(ws, payload, ctx) {
8793
9223
  const boardId = payload?.boardId;
8794
9224
  const taskId = payload?.taskId;
@@ -8827,45 +9257,39 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
8827
9257
  }
8828
9258
  const modelRouting = payload?.modelRouting ?? task.assignment?.modelRouting ?? (payload?.provider || payload?.model || task.assignment?.provider || task.assignment?.model ? "fixed" : "session");
8829
9259
  const useSessionModel = modelRouting === "session";
8830
- const leaseId = randomUUID3();
8831
- const now = (/* @__PURE__ */ new Date()).toISOString();
8832
9260
  const leaseTtlMs = 30 * 60 * 1e3;
8833
- const leaseExpiresAt = new Date(Date.now() + leaseTtlMs).toISOString();
8834
- const assignment = {
8835
- agentId: payload?.agentId ?? task.assignment?.agentId ?? task.assignedAgent,
8836
- name: payload?.name ?? task.assignment?.name ?? task.assignedAgent,
8837
- role: payload?.role ?? task.assignment?.role,
8838
- modelRouting,
8839
- provider: useSessionModel ? ctx.context?.provider.id : payload?.provider ?? task.assignment?.provider,
8840
- model: useSessionModel ? ctx.context?.model : payload?.model ?? task.assignment?.model,
8841
- fallbackProfile: modelRouting === "fallback_profile" ? payload?.fallbackProfile ?? task.assignment?.fallbackProfile : void 0,
8842
- fallbackModels: payload?.fallbackModels ?? task.assignment?.fallbackModels,
8843
- skills: payload?.skills ?? task.assignment?.skills,
8844
- tools: payload?.tools ?? task.assignment?.tools,
8845
- allowedCapabilities: payload?.allowedCapabilities ?? task.assignment?.allowedCapabilities,
8846
- maxAttempts: payload?.maxAttempts ?? task.assignment?.maxAttempts,
8847
- costCeilingUsd: payload?.costCeilingUsd ?? task.assignment?.costCeilingUsd ?? task.costCeilingUsd,
8848
- retryPolicy: payload?.retryPolicy ?? task.assignment?.retryPolicy ?? task.retryPolicy,
8849
- attempt: (task.assignment?.attempt ?? 0) + 1,
8850
- status: "queued",
8851
- dispatchedAt: now,
8852
- leaseId,
8853
- claimedAt: now,
8854
- heartbeatAt: now,
8855
- leaseExpiresAt
8856
- };
8857
- const assignedBoard = await store.assignTask(
9261
+ const reserved = await store.reserveKanbanDispatch({
8858
9262
  boardId,
8859
- task.id,
8860
- assignment,
8861
- activityContext(ctx, payload?.activityNote)
8862
- );
8863
- if (!assignedBoard) {
8864
- 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.`);
9263
+ taskId: task.id,
9264
+ routing: {
9265
+ agentId: payload?.agentId ?? task.assignment?.agentId ?? task.assignedAgent,
9266
+ name: payload?.name ?? task.assignment?.name ?? task.assignedAgent,
9267
+ role: payload?.role ?? task.assignment?.role,
9268
+ modelRouting,
9269
+ provider: useSessionModel ? ctx.context?.provider.id : payload?.provider ?? task.assignment?.provider,
9270
+ model: useSessionModel ? ctx.context?.model : payload?.model ?? task.assignment?.model,
9271
+ fallbackProfile: modelRouting === "fallback_profile" ? payload?.fallbackProfile ?? task.assignment?.fallbackProfile : void 0,
9272
+ fallbackModels: payload?.fallbackModels ?? task.assignment?.fallbackModels,
9273
+ skills: payload?.skills ?? task.assignment?.skills,
9274
+ tools: payload?.tools ?? task.assignment?.tools,
9275
+ allowedCapabilities: payload?.allowedCapabilities ?? task.assignment?.allowedCapabilities
9276
+ },
9277
+ budget: {
9278
+ maxAttempts: payload?.maxAttempts ?? task.assignment?.maxAttempts,
9279
+ costCeilingUsd: payload?.costCeilingUsd ?? task.assignment?.costCeilingUsd ?? task.costCeilingUsd,
9280
+ retryPolicy: payload?.retryPolicy ?? task.assignment?.retryPolicy ?? task.retryPolicy
9281
+ },
9282
+ leaseTtlMs,
9283
+ heartbeatIntervalMs: Math.floor(leaseTtlMs / 2)
9284
+ });
9285
+ if (!reserved) {
9286
+ reply(ws, "kanban.task.dispatch", false, `Failed to reserve task "${task.title}". The task may have been claimed by another agent or removed.`);
8865
9287
  return;
8866
9288
  }
9289
+ const dispatchLeaseId = reserved.lease.leaseId;
8867
9290
  try {
8868
- const summary = await ctx.dispatchTask(buildKanbanAgentPrompt(board, task, assignment, leaseId), {
9291
+ const assignment = reserved.task.assignment ?? { status: "queued" };
9292
+ const summary = await ctx.dispatchTask(buildKanbanAgentPrompt(board, task, assignment, dispatchLeaseId), {
8869
9293
  ...assignment.provider ? { provider: assignment.provider } : {},
8870
9294
  ...assignment.model ? { model: assignment.model } : {},
8871
9295
  ...assignment.fallbackModels ? { fallbackModels: assignment.fallbackModels } : {},
@@ -8874,69 +9298,40 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
8874
9298
  ...assignment.tools ? { tools: assignment.tools } : {},
8875
9299
  ...assignment.name ? { name: assignment.name } : {},
8876
9300
  ...assignment.allowedCapabilities ? { allowedCapabilities: assignment.allowedCapabilities } : {},
8877
- context: { kanban: { boardId, taskId: task.id, projectRoot: ctx.projectRoot, leaseId } },
9301
+ context: { kanban: { boardId, taskId: task.id, projectRoot: ctx.projectRoot, leaseId: dispatchLeaseId } },
8878
9302
  onDone: async (result) => {
8879
- const terminalWrite = await store.updateTaskAssignment(
8880
- boardId,
8881
- task.id,
8882
- {
8883
- ...assignment,
8884
- status: result.status,
8885
- ...result.result !== void 0 ? { lastResult: result.result } : {},
8886
- ...result.error !== void 0 ? { error: result.error } : {}
8887
- },
8888
- activityContext(ctx, void 0, leaseId)
8889
- );
8890
- if (result.status === "completed" && terminalWrite) {
8891
- try {
8892
- const finalized = await store.finalizeTaskCompletion(boardId, task.id, {
8893
- eventContext: activityContext(ctx, void 0, leaseId)
8894
- });
8895
- if (finalized?.gate.report && ctx.context) {
8896
- recordKanbanVerificationEvidence(ctx.context, finalized.gate.report);
8897
- }
8898
- } catch (error2) {
8899
- console.warn(
8900
- `[kanban-dispatch] completion gate failed for task ${task.id}: ${error2 instanceof Error ? error2.message : String(error2)}`
8901
- );
8902
- }
8903
- if (terminalWrite.lifecycle?.mode === "managed") {
8904
- const managedTask = terminalWrite.tasks.find(
8905
- (candidate) => candidate.id === task.id
8906
- );
8907
- if (managedTask && managedTask.lifecycle?.currentStage === "running") {
8908
- const actor = ctx.context?.agentId ?? "kanban-agent";
8909
- const comment = typeof result.result === "string" && result.result.trim().length > 0 ? result.result.trim().slice(0, 1e3) : "Work completed.";
8910
- try {
8911
- await store.transitionTask(boardId, task.id, {
8912
- to: "review",
8913
- actor,
8914
- comment,
8915
- attachment: {
8916
- url: `kanban://task/${task.id}/result`,
8917
- title: "Worker completion result",
8918
- type: "file"
8919
- }
8920
- });
8921
- } catch (transitionError) {
8922
- console.warn(
8923
- `[kanban-dispatch] auto-transition to review failed for task ${task.id}: ${transitionError instanceof Error ? transitionError.message : String(transitionError)}`
8924
- );
8925
- }
8926
- }
9303
+ if (result.status === "completed") {
9304
+ const completed = await store.completeKanbanDispatch({
9305
+ boardId,
9306
+ taskId: task.id,
9307
+ leaseId: dispatchLeaseId,
9308
+ actor: ctx.context?.agentId ?? "kanban-agent",
9309
+ result: result.result,
9310
+ evidence: result.result ? { url: `kanban://task/${task.id}/result`, title: "Worker completion result", type: "file" } : void 0
9311
+ });
9312
+ if (completed?.task.verificationReport && ctx.context) {
9313
+ recordKanbanVerificationEvidence(ctx.context, completed.task.verificationReport);
8927
9314
  }
9315
+ } else {
9316
+ await store.failKanbanDispatch({
9317
+ boardId,
9318
+ taskId: task.id,
9319
+ leaseId: dispatchLeaseId,
9320
+ actor: ctx.context?.agentId ?? "kanban-agent",
9321
+ error: result.error ?? "Task failed."
9322
+ });
8928
9323
  }
8929
9324
  const reconciled = await store.reconcileBoard(boardId);
8930
- const completed = reconciled?.board ?? await store.getBoard(boardId);
8931
- const completedTask = completed?.tasks.find((candidate) => candidate.id === task.id) ?? task;
9325
+ const completedBoard = reconciled?.board ?? await store.getBoard(boardId);
9326
+ const completedTask = completedBoard?.tasks.find((candidate) => candidate.id === task.id) ?? task;
8932
9327
  ctx.broadcast?.({
8933
9328
  type: "kanban.task.update",
8934
9329
  payload: { success: true, data: { boardId: board.id, task: completedTask } }
8935
9330
  });
8936
- if (completed) {
9331
+ if (completedBoard) {
8937
9332
  ctx.broadcast?.({
8938
9333
  type: "kanban.get",
8939
- payload: { success: true, data: { board: completed } }
9334
+ payload: { success: true, data: { board: completedBoard } }
8940
9335
  });
8941
9336
  }
8942
9337
  ctx.broadcast?.({
@@ -8947,55 +9342,32 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
8947
9342
  });
8948
9343
  const subagentId = summary.match(/Spawned subagent\s+([^\s]+)/)?.[1];
8949
9344
  const runTaskId = summary.match(/\bfor task\s+([^\s.]+)/i)?.[1];
8950
- const resolvedRoute = parseResolvedDispatchRoute(summary);
8951
- const updated = await store.updateTaskAssignment(
9345
+ const started = await store.startKanbanDispatch({
8952
9346
  boardId,
8953
- task.id,
8954
- {
8955
- ...assignment,
8956
- ...resolvedRoute,
8957
- status: "running",
8958
- ...subagentId ? { subagentId } : {},
8959
- ...runTaskId ? { runTaskId } : {},
8960
- lastResult: summary
8961
- },
8962
- activityContext(ctx, void 0, leaseId)
8963
- );
8964
- let transitionBoard = updated;
8965
- if (updated?.lifecycle?.mode === "managed") {
8966
- const managedTask = updated.tasks.find((candidate) => candidate.id === task.id);
8967
- if (managedTask && managedTask.lifecycle?.currentStage === "todo") {
8968
- try {
8969
- const tr = await store.transitionTask(boardId, task.id, {
8970
- to: "running",
8971
- actor: ctx.context?.agentId ?? "kanban-agent",
8972
- comment: "Work started."
8973
- });
8974
- if (tr) transitionBoard = tr.board;
8975
- } catch (transitionErr) {
8976
- console.warn(
8977
- `[kanban-dispatch] auto-transition to Running failed for task ${task.id}: ${transitionErr instanceof Error ? transitionErr.message : String(transitionErr)}`
8978
- );
8979
- }
8980
- }
8981
- }
8982
- const runningTask = transitionBoard?.tasks.find((candidate) => candidate.id === task.id) ?? task;
9347
+ taskId: task.id,
9348
+ leaseId: dispatchLeaseId,
9349
+ actor: ctx.context?.agentId ?? "kanban-agent",
9350
+ ...subagentId ? { subagentId } : {},
9351
+ ...runTaskId ? { runTaskId } : {}
9352
+ });
9353
+ const runningTask = started?.task ?? (started?.board.tasks.find((candidate) => candidate.id === task.id) ?? task);
8983
9354
  ctx.broadcast?.({
8984
9355
  type: "kanban.task.update",
8985
9356
  payload: { success: true, data: { boardId: board.id, task: runningTask } }
8986
9357
  });
8987
- if (updated) {
8988
- ctx.broadcast?.({ type: "kanban.get", payload: { success: true, data: { board: updated } } });
9358
+ if (started?.board) {
9359
+ ctx.broadcast?.({ type: "kanban.get", payload: { success: true, data: { board: started.board } } });
8989
9360
  }
8990
9361
  reply(ws, "kanban.task.dispatch", true, { boardId: board.id, task: runningTask, summary });
8991
9362
  } catch (error2) {
8992
9363
  const message = error2 instanceof Error ? error2.message : String(error2);
8993
- await store.updateTaskAssignment(
9364
+ await store.failKanbanDispatch({
8994
9365
  boardId,
8995
- task.id,
8996
- { ...assignment, status: "failed", error: message },
8997
- activityContext(ctx, void 0, leaseId)
8998
- );
9366
+ taskId: task.id,
9367
+ leaseId: dispatchLeaseId,
9368
+ actor: ctx.context?.agentId ?? "kanban-agent",
9369
+ error: message
9370
+ });
8999
9371
  reply(ws, "kanban.task.dispatch", false, message);
9000
9372
  }
9001
9373
  }
@@ -9149,7 +9521,7 @@ function fail(ws, type, message) {
9149
9521
  function has(payload, key) {
9150
9522
  return payload !== void 0 && Object.hasOwn(payload, key);
9151
9523
  }
9152
- function activityContext2(ctx, actor, note) {
9524
+ function activityContext(ctx, actor, note) {
9153
9525
  const sessionId = ctx.context?.session?.id;
9154
9526
  return {
9155
9527
  ...sessionId ? { sessionId } : {},
@@ -9386,7 +9758,7 @@ async function handleTaskActivityAdd(ws, type, payload, ctx) {
9386
9758
  outcome,
9387
9759
  ...typeof payload?.details === "string" && payload.details.trim() ? { details: payload.details.trim() } : {}
9388
9760
  },
9389
- activityContext2(
9761
+ activityContext(
9390
9762
  ctx,
9391
9763
  payload?.actor ?? ctx.context?.agentId ?? "webui"
9392
9764
  )
@@ -9700,7 +10072,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
9700
10072
  ...payload?.labels ? { labels: payload.labels } : {},
9701
10073
  ...has(payload, "boundary") ? { boundary: payload?.boundary } : {}
9702
10074
  },
9703
- activityContext2(ctx, "webui", payload?.activityNote)
10075
+ activityContext(ctx, "webui", payload?.activityNote)
9704
10076
  );
9705
10077
  result ? ok(ws, type, result.task) : fail(ws, type, `Board not found: ${boardId}`);
9706
10078
  return true;
@@ -9784,7 +10156,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
9784
10156
  boundary: payload?.boundary ?? null
9785
10157
  } : {}
9786
10158
  },
9787
- activityContext2(ctx, "webui", payload?.activityNote)
10159
+ activityContext(ctx, "webui", payload?.activityNote)
9788
10160
  );
9789
10161
  if (!board) {
9790
10162
  fail(ws, type, "Board or task not found");
@@ -9835,7 +10207,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
9835
10207
  taskId,
9836
10208
  columnId,
9837
10209
  payload?.order,
9838
- activityContext2(ctx, "webui", payload?.activityNote)
10210
+ activityContext(ctx, "webui", payload?.activityNote)
9839
10211
  );
9840
10212
  if (!board) {
9841
10213
  fail(ws, type, "Move failed");
@@ -9952,7 +10324,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
9952
10324
  ...payload?.unit ? { unit: payload.unit } : {},
9953
10325
  ...payload?.notes ? { notes: payload.notes } : {}
9954
10326
  },
9955
- activityContext2(
10327
+ activityContext(
9956
10328
  ctx,
9957
10329
  "webui",
9958
10330
  payload?.activityNote ?? `Goal metric added: ${name2}.`
@@ -9982,7 +10354,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
9982
10354
  ...payload?.unit ? { unit: payload.unit } : {},
9983
10355
  ...payload?.notes ? { notes: payload.notes } : {}
9984
10356
  },
9985
- activityContext2(
10357
+ activityContext(
9986
10358
  ctx,
9987
10359
  "webui",
9988
10360
  payload?.activityNote ?? "Goal metric updated in WebUI."
@@ -10019,7 +10391,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
10019
10391
  ...payload?.costCeilingUsd !== void 0 ? { costCeilingUsd: Number(payload.costCeilingUsd) } : {},
10020
10392
  ...payload?.retryPolicy ? { retryPolicy: payload.retryPolicy } : {}
10021
10393
  },
10022
- activityContext2(ctx, void 0, payload?.activityNote)
10394
+ activityContext(ctx, void 0, payload?.activityNote)
10023
10395
  );
10024
10396
  board ? ok(ws, type, findTask2(board.tasks, taskId)) : fail(ws, type, "Board or task not found");
10025
10397
  return true;
@@ -10041,7 +10413,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
10041
10413
  type: payload?.checkType ?? "manual",
10042
10414
  status: payload?.status ?? "pending"
10043
10415
  },
10044
- activityContext2(
10416
+ activityContext(
10045
10417
  ctx,
10046
10418
  "webui",
10047
10419
  payload?.activityNote ?? `Acceptance check added: ${description}.`
@@ -10066,7 +10438,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
10066
10438
  {
10067
10439
  ...has(payload, "status") ? { status: payload?.status } : {}
10068
10440
  },
10069
- activityContext2(
10441
+ activityContext(
10070
10442
  ctx,
10071
10443
  "webui",
10072
10444
  payload?.activityNote ?? `Acceptance check updated${payload?.status ? ` to ${String(payload.status)}` : ""}.`
@@ -10093,7 +10465,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
10093
10465
  author,
10094
10466
  content
10095
10467
  },
10096
- activityContext2(ctx, author)
10468
+ activityContext(ctx, author)
10097
10469
  );
10098
10470
  board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
10099
10471
  return true;
@@ -10642,12 +11014,14 @@ async function handleMailboxRoute(ws, msg, handlers) {
10642
11014
 
10643
11015
  // src/server/mcp-handlers.ts
10644
11016
  import { allServers } from "@wrongstack/core/infrastructure";
11017
+ import { DefaultSecretScrubber, isSecretField } from "@wrongstack/core/security";
10645
11018
  import {
10646
11019
  addMcp,
10647
11020
  disableMcp,
10648
11021
  discoverMcp,
10649
11022
  enableMcp,
10650
11023
  listMcp,
11024
+ MCP_ENV_MASK,
10651
11025
  removeMcp,
10652
11026
  restartMcp,
10653
11027
  updateMcp
@@ -10667,6 +11041,15 @@ function mapStatus(raw) {
10667
11041
  return "stopped";
10668
11042
  }
10669
11043
  }
11044
+ var envScrubber = new DefaultSecretScrubber();
11045
+ function maskServerEnv(env) {
11046
+ const out = {};
11047
+ for (const [key, value] of Object.entries(env)) {
11048
+ const secret = isSecretField(key) || typeof value === "string" && envScrubber.scrub(value) !== value;
11049
+ out[key] = secret ? MCP_ENV_MASK : value;
11050
+ }
11051
+ return out;
11052
+ }
10670
11053
  function toView(info, health) {
10671
11054
  const view = {
10672
11055
  name: info.name,
@@ -10681,7 +11064,7 @@ function toView(info, health) {
10681
11064
  if (info.lazy !== void 0) view.lazy = info.lazy;
10682
11065
  if (info.command !== void 0) view.command = info.command;
10683
11066
  if (info.args !== void 0) view.args = info.args;
10684
- if (info.env !== void 0) view.env = info.env;
11067
+ if (info.env !== void 0) view.env = maskServerEnv(info.env);
10685
11068
  if (info.url !== void 0) view.url = info.url;
10686
11069
  if (health !== void 0) view.health = health;
10687
11070
  return view;
@@ -10720,7 +11103,15 @@ async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
10720
11103
  async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
10721
11104
  const d = deps(ws, globalConfigPath, mcpRegistry);
10722
11105
  if (!d) return;
10723
- const result = await addMcp(msg.payload, d);
11106
+ const validated = validateMcpServerPayload(msg.payload, "mcp.add");
11107
+ if (!validated.ok) {
11108
+ send(ws, {
11109
+ type: "mcp.operation_result",
11110
+ payload: { success: false, message: validated.message }
11111
+ });
11112
+ return;
11113
+ }
11114
+ const result = await addMcp(validated.value, d);
10724
11115
  if (result.ok && result.server) {
10725
11116
  send(ws, { type: "mcp.server.added", payload: { server: toView(result.server) } });
10726
11117
  if (result.registryError) {
@@ -10740,7 +11131,15 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
10740
11131
  async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
10741
11132
  const d = deps(ws, globalConfigPath, mcpRegistry);
10742
11133
  if (!d) return;
10743
- const result = await updateMcp(msg.payload, d);
11134
+ const validated = validateMcpServerPayload(msg.payload, "mcp.update");
11135
+ if (!validated.ok) {
11136
+ send(ws, {
11137
+ type: "mcp.operation_result",
11138
+ payload: { success: false, message: validated.message }
11139
+ });
11140
+ return;
11141
+ }
11142
+ const result = await updateMcp(validated.value, d);
10744
11143
  if (result.ok && result.server) {
10745
11144
  send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
10746
11145
  }
@@ -11257,7 +11656,7 @@ async function handleSageDelete(ws, msg, memoryStore) {
11257
11656
  }
11258
11657
  try {
11259
11658
  await Sage.deleteSage(id, reason, {
11260
- force: force ?? true,
11659
+ force: force === true,
11261
11660
  neverInject: neverInject === true
11262
11661
  });
11263
11662
  send(ws, {
@@ -11840,11 +12239,11 @@ function createModelOperations(context) {
11840
12239
  }
11841
12240
 
11842
12241
  // src/server/port-utils.ts
11843
- import * as net from "node:net";
12242
+ import * as net2 from "node:net";
11844
12243
  import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
11845
12244
  function isPortFree(host, port) {
11846
12245
  return new Promise((resolve15) => {
11847
- const srv = net.createServer();
12246
+ const srv = net2.createServer();
11848
12247
  srv.once("error", () => resolve15(false));
11849
12248
  srv.once("listening", () => {
11850
12249
  srv.close(() => resolve15(true));
@@ -12072,6 +12471,7 @@ function seedContextMeta(config, context) {
12072
12471
  meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
12073
12472
  meta["nextPrediction"] = config.nextPrediction ?? false;
12074
12473
  meta["fallbackModels"] = config.fallbackModels ?? [];
12474
+ meta["fallbackBridge"] = config.fallbackBridge ?? "";
12075
12475
  meta["fallbackProfiles"] = config.fallbackProfiles ?? {};
12076
12476
  meta["favoriteModels"] = config.favoriteModels ?? [];
12077
12477
  meta["favoriteModelsOnly"] = config.favoriteModelsOnly === true;
@@ -12108,7 +12508,7 @@ function seedContextMeta(config, context) {
12108
12508
  meta["refinerModel"] = autonomyCfg["refinerModel"] ?? "";
12109
12509
  meta["refinerFallbackProfile"] = autonomyCfg["refinerFallbackProfile"] ?? "";
12110
12510
  meta["thinkingWord"] = autonomyCfg["thinkingWord"] ?? "thinking";
12111
- meta["statuslineMode"] = autonomyCfg["statuslineMode"] ?? "detailed";
12511
+ meta["statuslineMode"] = autonomyCfg["statuslineMode"] ?? "minimum";
12112
12512
  meta["animationStyle"] = autonomyCfg["animationStyle"] ?? "rainbow";
12113
12513
  meta["showModelReasoning"] = autonomyCfg["showModelReasoning"] !== false;
12114
12514
  meta["breakerEnabled"] = config.circuitBreaker?.enabled === true;
@@ -12154,7 +12554,10 @@ function seedContextMeta(config, context) {
12154
12554
  try {
12155
12555
  const mgr = new FallbackProfileManager(config);
12156
12556
  const named = autoReviewExt?.["fallbackProfile"];
12157
- resolvedChain = typeof named === "string" && named.length > 0 ? mgr.resolve(named) : mgr.resolveEffective({ fallbackAuto: true });
12557
+ resolvedChain = typeof named === "string" && named.length > 0 ? mgr.resolveEffective({ fallbackProfile: named, fallbackAuto: false }) : mgr.resolveEffective({
12558
+ fallbackModels: config.fallbackModels,
12559
+ fallbackAuto: config.fallbackAuto !== false
12560
+ });
12158
12561
  } catch {
12159
12562
  resolvedChain = [];
12160
12563
  }
@@ -12298,7 +12701,28 @@ async function updateGlobalConfig(deps2, holder, mutate, errorLabel) {
12298
12701
  );
12299
12702
  }
12300
12703
  }
12301
- var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set(["groupToolCalls", "showThinkingLogs", "autoReviewFallbackModels"]);
12704
+ var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set([
12705
+ "groupToolCalls",
12706
+ "showThinkingLogs",
12707
+ "autoReviewFallbackModels",
12708
+ // v11 Display parity: agent-swarm panel + inverse fsAccess flag.
12709
+ // The TUI settings picker mirrors these so the browser can keep the
12710
+ // panel state in sync without round-tripping through config.
12711
+ "showAgentSwarmPanel",
12712
+ "allowOutsideProjectRoot",
12713
+ // v13 Display parity: codebase-index read-tool, SAGE memory-inject
12714
+ // surfacing, pre-refine countdown, inject threshold, multi-diff
12715
+ // footer threshold, refine-panel enhance countdown. All six are
12716
+ // pure panel-toggles; the underlying engine knobs (when applicable)
12717
+ // live in the TUI keyboard shortcut or the agent runtime, not in
12718
+ // config.
12719
+ "readSymbols",
12720
+ "showSageMemoryInject",
12721
+ "preRefineSeconds",
12722
+ "sageMemoryInjectThreshold",
12723
+ "multiDiffSummaryThreshold",
12724
+ "enhanceCountdownMs"
12725
+ ]);
12302
12726
  async function persistPrefsToConfig(deps2, holder, payload) {
12303
12727
  payload = { ...payload };
12304
12728
  for (const k of DISPLAY_ONLY_KEYS) delete payload[k];
@@ -12313,7 +12737,7 @@ async function persistPrefsToConfig(deps2, holder, payload) {
12313
12737
  autonomyCfg[key] = val;
12314
12738
  autonomyTouched = true;
12315
12739
  };
12316
- if (typeof payload["autonomy"] === "string" && ["off", "suggest", "auto"].includes(payload["autonomy"])) {
12740
+ if (typeof payload["autonomy"] === "string" && ["off", "suggest", "auto", "eternal", "eternal-parallel"].includes(payload["autonomy"])) {
12317
12741
  setAutonomy("defaultMode", payload["autonomy"]);
12318
12742
  }
12319
12743
  if (typeof payload["autonomyDelayMs"] === "number")
@@ -12609,14 +13033,14 @@ import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
12609
13033
  import { getProcessRegistry as getProcessRegistry2 } from "@wrongstack/tools";
12610
13034
 
12611
13035
  // src/server/privileged-actions.ts
12612
- import { randomUUID as randomUUID4 } from "node:crypto";
13036
+ import { randomUUID as randomUUID3 } from "node:crypto";
12613
13037
  import {
12614
13038
  isTrustDecisionAllowed
12615
13039
  } from "@wrongstack/core/security";
12616
13040
  async function authorizeWebUIAction(boundary, action, logger) {
12617
13041
  const request = {
12618
13042
  version: 1,
12619
- requestId: randomUUID4(),
13043
+ requestId: randomUUID3(),
12620
13044
  actor: {
12621
13045
  kind: "remote-client",
12622
13046
  ...action.sessionId ? { sessionId: action.sessionId } : {}
@@ -12975,7 +13399,7 @@ function createProjectHandlers(ctx) {
12975
13399
 
12976
13400
  // src/server/provider-handlers.ts
12977
13401
  import { resolveProviderModelList } from "@wrongstack/core/models";
12978
- import { DefaultSecretScrubber } from "@wrongstack/core/security";
13402
+ import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
12979
13403
  import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
12980
13404
  import {
12981
13405
  beginOAuthLogin
@@ -13247,7 +13671,7 @@ function projectSavedProviders(providers) {
13247
13671
  return view;
13248
13672
  });
13249
13673
  }
13250
- var probeScrubber = new DefaultSecretScrubber();
13674
+ var probeScrubber = new DefaultSecretScrubber2();
13251
13675
  async function probeModelDescriptors(cfg) {
13252
13676
  if (!cfg.baseUrl) return [];
13253
13677
  try {
@@ -16073,6 +16497,7 @@ var SddBoardWebSocketHandler = class {
16073
16497
  poll = null;
16074
16498
  pollInFlight = false;
16075
16499
  unsub = null;
16500
+ disposed = false;
16076
16501
  constructor(boardsDir, events, lifecycle, security) {
16077
16502
  this.store = new SddBoardStore({ baseDir: boardsDir });
16078
16503
  this.lifecycle = lifecycle;
@@ -16087,6 +16512,7 @@ var SddBoardWebSocketHandler = class {
16087
16512
  }
16088
16513
  }
16089
16514
  addClient(ws) {
16515
+ if (this.disposed) return;
16090
16516
  const client = { ws, id: crypto.randomUUID() };
16091
16517
  this.clients.add(client);
16092
16518
  const remove = () => {
@@ -16228,9 +16654,12 @@ var SddBoardWebSocketHandler = class {
16228
16654
  }
16229
16655
  }
16230
16656
  dispose() {
16657
+ this.disposed = true;
16231
16658
  this.stopPolling();
16232
16659
  this.unsub?.();
16233
16660
  this.unsub = null;
16661
+ this.clients.clear();
16662
+ this.latest = null;
16234
16663
  }
16235
16664
  // ── internal ────────────────────────────────────────────────────────────
16236
16665
  async pollLatest() {
@@ -16238,6 +16667,7 @@ var SddBoardWebSocketHandler = class {
16238
16667
  this.pollInFlight = true;
16239
16668
  try {
16240
16669
  const snap = await this.loadLatestSnapshot();
16670
+ if (this.disposed) return;
16241
16671
  if (!snap) return;
16242
16672
  if (this.latest && this.latest.updatedAt >= snap.updatedAt && this.latest.runId === snap.runId) {
16243
16673
  return;
@@ -16551,6 +16981,7 @@ var SddWizardWebSocketHandler = class {
16551
16981
  * flight (or after the probe has settled).
16552
16982
  */
16553
16983
  resumeProbe = null;
16984
+ disposed = false;
16554
16985
  async bootstrap() {
16555
16986
  try {
16556
16987
  await this.deps.ensureReady?.();
@@ -16563,23 +16994,28 @@ var SddWizardWebSocketHandler = class {
16563
16994
  if (this.driver) return;
16564
16995
  try {
16565
16996
  const driver = this.deps.makeDriver();
16566
- if (await driver.loadExisting()) {
16997
+ const loaded = await driver.loadExisting();
16998
+ if (this.disposed) return;
16999
+ if (loaded) {
16567
17000
  this.driver = driver;
16568
17001
  this.lastAgentText = driver.getLastAgentText() ?? "";
16569
17002
  }
16570
17003
  this.resumeError = null;
16571
17004
  } catch (error2) {
17005
+ if (this.disposed) return;
16572
17006
  this.driver = null;
16573
17007
  this.lastAgentText = "";
16574
17008
  this.resumeError = error2 instanceof Error ? error2.message : String(error2);
16575
17009
  }
16576
17010
  }
16577
17011
  addClient(ws) {
17012
+ if (this.disposed) return;
16578
17013
  const client = { ws, id: crypto.randomUUID() };
16579
17014
  this.clients.add(client);
16580
17015
  ws.on("close", () => this.clients.delete(client));
16581
17016
  ws.on("error", () => this.clients.delete(client));
16582
17017
  void this.ready.then(() => {
17018
+ if (this.disposed || !this.clients.has(client)) return;
16583
17019
  if (this.resumeError) {
16584
17020
  this.send(client, { type: "sdd.spec.error", payload: { message: this.resumeError } });
16585
17021
  return;
@@ -16592,7 +17028,16 @@ var SddWizardWebSocketHandler = class {
16592
17028
  }
16593
17029
  });
16594
17030
  }
17031
+ dispose() {
17032
+ this.disposed = true;
17033
+ this.clients.clear();
17034
+ this.driver = null;
17035
+ this.lastAgentText = "";
17036
+ this.busy = false;
17037
+ this.resumeProbe = null;
17038
+ }
16595
17039
  async handleMessage(msg) {
17040
+ if (this.disposed) return;
16596
17041
  try {
16597
17042
  await this.ready;
16598
17043
  if (this.resumeError) {
@@ -16810,6 +17255,7 @@ var SddWizardWebSocketHandler = class {
16810
17255
  this.broadcast(this.snapshotMsg());
16811
17256
  try {
16812
17257
  const text = await this.deps.runInterviewTurn(prompt);
17258
+ if (this.disposed) return;
16813
17259
  this.lastAgentText = text;
16814
17260
  if (this.driver) {
16815
17261
  await this.driver.ingestAgentOutput(text);
@@ -16835,6 +17281,7 @@ var SddWizardWebSocketHandler = class {
16835
17281
  }
16836
17282
  }
16837
17283
  send(client, msg) {
17284
+ if (!this.clients.has(client)) return;
16838
17285
  sendSerialized(client.ws, JSON.stringify(msg));
16839
17286
  }
16840
17287
  };
@@ -17601,6 +18048,7 @@ function setupEvents(deps2) {
17601
18048
  });
17602
18049
  pendingConfirms.set(id, {
17603
18050
  resolve: e.resolve,
18051
+ sessionId: e.sessionId,
17604
18052
  decisionSource: e.decisionSource,
17605
18053
  riskTier: e.riskTier,
17606
18054
  boundaryReason: e.boundaryReason,
@@ -18129,6 +18577,9 @@ var SpecsWebSocketHandler = class {
18129
18577
  ws.on("error", () => this.clients.delete(client));
18130
18578
  void this.sendList(client);
18131
18579
  }
18580
+ dispose() {
18581
+ this.clients.clear();
18582
+ }
18132
18583
  async handleMessage(msg) {
18133
18584
  switch (msg.type) {
18134
18585
  case "specs.list":
@@ -18264,12 +18715,13 @@ var SpecsWebSocketHandler = class {
18264
18715
  }
18265
18716
  }
18266
18717
  send(client, msg) {
18718
+ if (!this.clients.has(client)) return;
18267
18719
  sendSerialized(client.ws, JSON.stringify(msg));
18268
18720
  }
18269
18721
  };
18270
18722
 
18271
18723
  // src/server/start-webui.ts
18272
- import { randomUUID as randomUUID5 } from "node:crypto";
18724
+ import { randomUUID as randomUUID4 } from "node:crypto";
18273
18725
  import * as http2 from "node:http";
18274
18726
  import * as path29 from "node:path";
18275
18727
  import { createDefaultPipelines } from "@wrongstack/core/agent";
@@ -18294,7 +18746,7 @@ import { toLanguagePackageInput } from "@wrongstack/techstack";
18294
18746
  import { ensureSessionShell } from "@wrongstack/tools";
18295
18747
 
18296
18748
  // src/server/backend-services.ts
18297
- import { join as join13 } from "node:path";
18749
+ import { join as join12 } from "node:path";
18298
18750
  import { Agent } from "@wrongstack/core/agent";
18299
18751
  import {
18300
18752
  BrainDecisionLedger,
@@ -18307,7 +18759,7 @@ import {
18307
18759
  mailboxSessionTag,
18308
18760
  ObservableBrainArbiter as ObservableBrainArbiterCtor
18309
18761
  } from "@wrongstack/core/coordination";
18310
- import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/defaults";
18762
+ import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/types";
18311
18763
  import { installDesignStudioMiddleware } from "@wrongstack/core/design";
18312
18764
  import {
18313
18765
  AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
@@ -18337,7 +18789,7 @@ import {
18337
18789
  import { spawn as spawn2 } from "node:child_process";
18338
18790
  import { createRequire } from "node:module";
18339
18791
  import { existsSync } from "node:fs";
18340
- import { dirname as dirname7, join as join11 } from "node:path";
18792
+ import { dirname as dirname7, join as join10 } from "node:path";
18341
18793
  import { resolveProjectDir as resolveProjectDir2 } from "@wrongstack/core/coordination";
18342
18794
  import { wstackGlobalRoot } from "@wrongstack/core/utils";
18343
18795
  import { readLiveLock } from "@wrongstack/core/coordination";
@@ -18466,7 +18918,7 @@ function mailboxServeInvocation(projectRoot) {
18466
18918
  function findWorkspaceCliEntry(projectRoot) {
18467
18919
  let dir = projectRoot;
18468
18920
  for (let i = 0; i < 6; i++) {
18469
- const candidate = join11(dir, "packages", "cli", "dist", "index.js");
18921
+ const candidate = join10(dir, "packages", "cli", "dist", "index.js");
18470
18922
  if (existsSync(candidate)) return candidate;
18471
18923
  const parent = dirname7(dir);
18472
18924
  if (parent === dir) return null;
@@ -18711,7 +19163,7 @@ function clampDim(value, fallback) {
18711
19163
  }
18712
19164
 
18713
19165
  // src/server/worktree-ws-handler.ts
18714
- import { join as join12, resolve as resolve12, sep as sep5 } from "node:path";
19166
+ import { join as join11, resolve as resolve12, sep as sep5 } from "node:path";
18715
19167
  import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
18716
19168
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
18717
19169
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
@@ -18788,11 +19240,15 @@ var WorktreeWebSocketHandler = class {
18788
19240
  for (const off of this.offs) off();
18789
19241
  this.offs.length = 0;
18790
19242
  this.stopBroadcast();
19243
+ this.clients.clear();
19244
+ this.handles.clear();
19245
+ this.baseBranch = "";
19246
+ this.scanRescanNeeded = false;
18791
19247
  }
18792
19248
  // ── orphan management ─────────────────────────────────────────────────────
18793
19249
  /** Absolute managed-worktrees root for this project. */
18794
19250
  worktreesRoot() {
18795
- return resolve12(join12(this.management.projectRoot, ".wrongstack", "worktrees"));
19251
+ return resolve12(join11(this.management.projectRoot, ".wrongstack", "worktrees"));
18796
19252
  }
18797
19253
  /** True iff `dir` resolves strictly inside the managed worktrees root. */
18798
19254
  underRoot(dir) {
@@ -19185,7 +19641,13 @@ async function createAgentServices(input) {
19185
19641
  minImportance: config.Sage?.inject?.minImportance,
19186
19642
  repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
19187
19643
  verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
19188
- triggers: config.Sage?.inject?.triggers
19644
+ triggers: config.Sage?.inject?.triggers,
19645
+ // Resolve the live session so retrieval and cooldown are session-scoped
19646
+ // (matching the CLI wiring in wiring/sage.ts). Without this, the
19647
+ // middleware falls back to ctx.session.id for cooldown but passes
19648
+ // undefined to retrieval, causing owned session-scoped memories to be
19649
+ // silently excluded from tool-call injection.
19650
+ getSessionId: () => input.sessionGetter().id
19189
19651
  })
19190
19652
  );
19191
19653
  }
@@ -19309,6 +19771,7 @@ async function createAgentServices(input) {
19309
19771
  perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
19310
19772
  tracer: void 0
19311
19773
  });
19774
+ input.installToolBoundary?.(pipelines);
19312
19775
  const webuiLogger = container.resolve(TOKENS.Logger);
19313
19776
  void discoverMailboxBridgeForWebui({
19314
19777
  projectRoot,
@@ -19348,7 +19811,7 @@ async function createAgentServices(input) {
19348
19811
  const brainCfg = resolveBrainConfigDefaults(config.brain, {
19349
19812
  fallbackModels: config.fallbackModels
19350
19813
  });
19351
- const brainLedgerPath = join13(wpaths.projectDir, "brain-ledger.jsonl");
19814
+ const brainLedgerPath = join12(wpaths.projectDir, "brain-ledger.jsonl");
19352
19815
  let brainLedgerEnabled = brainCfg.ledger?.enabled !== false;
19353
19816
  let brainLedger;
19354
19817
  const startBrainLedger = () => {
@@ -19521,7 +19984,8 @@ async function createAgentServices(input) {
19521
19984
  // subagent's fallback extension's tracker hooks are undefined and
19522
19985
  // round-robin keeps reassigning the doomed model. Mirrors the CLI
19523
19986
  // factory wiring at host-subagent-factory.ts:337.
19524
- statusTracker: container.safeResolve(TOKENS.ProviderModelStatusTracker)
19987
+ statusTracker: container.safeResolve(TOKENS.ProviderModelStatusTracker),
19988
+ ...input.installToolBoundary ? { installToolBoundary: input.installToolBoundary } : {}
19525
19989
  }),
19526
19990
  paths: {
19527
19991
  projectSpecs: wpaths.projectSpecs,
@@ -19560,6 +20024,8 @@ async function createAgentServices(input) {
19560
20024
  for (const off of brainLogOffs) off();
19561
20025
  goalHandler.dispose();
19562
20026
  sddBoardHandler.dispose();
20027
+ sddWizardHandler.dispose();
20028
+ specsHandler.dispose();
19563
20029
  worktreeHandler.dispose();
19564
20030
  terminalHandler.dispose();
19565
20031
  collabHandler.dispose();
@@ -19595,7 +20061,10 @@ async function createAgentServices(input) {
19595
20061
  }
19596
20062
 
19597
20063
  // src/server/connection-handler.ts
19598
- var RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
20064
+ var DEFAULT_WS_RATE_LIMIT = 600;
20065
+ var rawRateLimit = process.env["WEBUI_RATE_LIMIT"];
20066
+ var parsedRateLimit = rawRateLimit ? Number.parseInt(rawRateLimit, 10) : DEFAULT_WS_RATE_LIMIT;
20067
+ var RATE_LIMIT_MESSAGES = Number.isFinite(parsedRateLimit) && parsedRateLimit >= 0 ? parsedRateLimit : DEFAULT_WS_RATE_LIMIT;
19599
20068
  var VERBOSE_WS_LOGGING = process.env["WEBUI_VERBOSE_WS"] === "1" || process.env["WEBUI_VERBOSE_WS"] === "true";
19600
20069
  function readRemoteAddress(ws) {
19601
20070
  const socket = ws._socket;
@@ -19675,6 +20144,72 @@ function createConnectionHandler(options) {
19675
20144
  };
19676
20145
  }
19677
20146
 
20147
+ // src/server/governance-runtime.ts
20148
+ import {
20149
+ createGovernanceMutationSnapshotBridge
20150
+ } from "@wrongstack/runtime/governance-mutation-snapshot-bridge";
20151
+ import { sanitizeGovernanceMessage } from "@wrongstack/runtime/governance-sanitize";
20152
+ var WRONGSTACK_GOVERNANCE_ENV = "WRONGSTACK_GOVERNANCE";
20153
+ var DEFAULT_DEPENDENCIES = {
20154
+ load: () => import("@wrongstack/runtime/governance-bootstrap"),
20155
+ createMutationBridge: createGovernanceMutationSnapshotBridge
20156
+ };
20157
+ function webUiGovernanceRuntimeEnabled(environment) {
20158
+ return environment[WRONGSTACK_GOVERNANCE_ENV]?.trim() === "1";
20159
+ }
20160
+ async function setupWebUiGovernance(input, dependencies = DEFAULT_DEPENDENCIES) {
20161
+ if (!webUiGovernanceRuntimeEnabled(input.environment)) return void 0;
20162
+ let loaded;
20163
+ try {
20164
+ loaded = await dependencies.load();
20165
+ } catch (error2) {
20166
+ input.logger.warn("governance: standalone WebUI runtime module unavailable", {
20167
+ message: sanitizeGovernanceMessage(error2 instanceof Error ? error2.message : String(error2))
20168
+ });
20169
+ return void 0;
20170
+ }
20171
+ let result;
20172
+ try {
20173
+ result = await loaded.bootstrapGovernanceRuntime({
20174
+ projectRoot: input.projectRoot,
20175
+ projectId: input.projectId,
20176
+ adminClientId: `webui-control-${process.pid}-${input.sessionId}`,
20177
+ modelClientId: `webui-model-${input.sessionId}`,
20178
+ modelCapabilities: ["task_read", "command_submit", "shadow_observe"]
20179
+ });
20180
+ } catch (error2) {
20181
+ input.logger.warn("governance: standalone WebUI bootstrap failed open", {
20182
+ message: sanitizeGovernanceMessage(error2 instanceof Error ? error2.message : String(error2))
20183
+ });
20184
+ return void 0;
20185
+ }
20186
+ if (result.mode !== "governed") {
20187
+ input.contextMeta["governance"] = result;
20188
+ input.logger.warn(`governance: standalone WebUI legacy fallback (${result.code})`, {
20189
+ phase: result.phase,
20190
+ cleanup: result.cleanup,
20191
+ message: result.message
20192
+ });
20193
+ return void 0;
20194
+ }
20195
+ const snapshot = result.handle.snapshot();
20196
+ input.contextMeta["governance"] = snapshot;
20197
+ input.logger.info(`governance: ${snapshot.source} standalone WebUI control plane`);
20198
+ const mutationSnapshots = dependencies.createMutationBridge({
20199
+ events: input.events,
20200
+ sink: result.handle,
20201
+ captureWorkspaceCheckpoint: input.captureWorkspaceCheckpoint,
20202
+ logger: input.logger
20203
+ });
20204
+ return Object.freeze({
20205
+ installToolBoundary: (pipelines) => mutationSnapshots.installToolBoundary(pipelines),
20206
+ close: async () => {
20207
+ await mutationSnapshots.close();
20208
+ return result.handle.close();
20209
+ }
20210
+ });
20211
+ }
20212
+
19678
20213
  // src/server/message-dispatcher.ts
19679
20214
  import path23 from "node:path";
19680
20215
  function createMessageDispatcher(opts) {
@@ -21379,6 +21914,17 @@ async function startWebUI(opts = {}) {
21379
21914
  const prefSnapshot2 = () => prefSnapshot(context.meta);
21380
21915
  const persistPrefsToConfig2 = async (payload) => persistPrefsToConfig(prefHelperDeps, configWriteLock, payload);
21381
21916
  const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary3({ policyId: "webui-trusted-host-compat-v1" });
21917
+ const governanceHandle = opts.installToolBoundary === void 0 ? await setupWebUiGovernance({
21918
+ environment: process.env,
21919
+ projectRoot,
21920
+ projectId: wpaths.projectSlug,
21921
+ sessionId: session.id,
21922
+ contextMeta: context.meta,
21923
+ events,
21924
+ logger,
21925
+ captureWorkspaceCheckpoint: async () => sessionStore.captureWorkspaceCheckpoint?.(session.id, 0)
21926
+ }) : void 0;
21927
+ const installToolBoundary = opts.installToolBoundary ?? governanceHandle?.installToolBoundary;
21382
21928
  const agentServices = await createAgentServices({
21383
21929
  trustBoundary,
21384
21930
  config,
@@ -21401,6 +21947,7 @@ async function startWebUI(opts = {}) {
21401
21947
  skillInstaller,
21402
21948
  tokenCounter,
21403
21949
  pipelines: createDefaultPipelines(),
21950
+ ...installToolBoundary ? { installToolBoundary } : {},
21404
21951
  modelCapabilitiesRef,
21405
21952
  sessionGetter: () => session,
21406
21953
  sessionReader,
@@ -21482,7 +22029,7 @@ async function startWebUI(opts = {}) {
21482
22029
  const input = toLanguagePackageInput(operation, workspace);
21483
22030
  const use = {
21484
22031
  type: "tool_use",
21485
- id: `techstack-remediation-${randomUUID5()}`,
22032
+ id: `techstack-remediation-${randomUUID4()}`,
21486
22033
  name: "language_package",
21487
22034
  input: { ...input }
21488
22035
  };
@@ -21751,7 +22298,8 @@ async function startWebUI(opts = {}) {
21751
22298
  patchConfig(state.getConfig(), {
21752
22299
  providers: snapshot.providers,
21753
22300
  ...snapshot.apiKey !== void 0 ? { apiKey: snapshot.apiKey } : {},
21754
- ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {}
22301
+ ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {},
22302
+ fallbackBridge: snapshot.fallbackBridge ?? ""
21755
22303
  })
21756
22304
  );
21757
22305
  deps2.configStore.update({
@@ -21759,6 +22307,7 @@ async function startWebUI(opts = {}) {
21759
22307
  ...snapshot.apiKey !== void 0 ? { apiKey: snapshot.apiKey } : {},
21760
22308
  ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {},
21761
22309
  ...snapshot.fallbackModels !== void 0 ? { fallbackModels: snapshot.fallbackModels } : {},
22310
+ fallbackBridge: snapshot.fallbackBridge ?? "",
21762
22311
  ...snapshot.fallbackProfiles !== void 0 ? { fallbackProfiles: snapshot.fallbackProfiles } : {},
21763
22312
  ...snapshot.favoriteModels !== void 0 ? { favoriteModels: snapshot.favoriteModels } : {},
21764
22313
  ...snapshot.favoriteModelsOnly !== void 0 ? { favoriteModelsOnly: snapshot.favoriteModelsOnly } : {},
@@ -21893,6 +22442,12 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
21893
22442
  await stopHeapWatchdog();
21894
22443
  credentialWatcherClose?.();
21895
22444
  disposeRealtimeHandlers();
22445
+ const governanceCleanup = await governanceHandle?.close();
22446
+ if (governanceCleanup && !governanceCleanup.ok) {
22447
+ logger.warn(`governance: standalone WebUI ${governanceCleanup.action} cleanup failed`, {
22448
+ message: governanceCleanup.message
22449
+ });
22450
+ }
21896
22451
  brainMonitor.stop();
21897
22452
  await agentServices.brainLedger?.stop().catch(() => {
21898
22453
  });