@wrongstack/webui-server 0.297.0 → 0.298.1

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 (190) hide show
  1. package/dist/index.js +1331 -469
  2. package/dist/protocol/index.js +1 -0
  3. package/dist/protocol/registry.d.ts +1 -1
  4. package/dist/protocol/server-conversation.d.ts +1 -1
  5. package/dist/server/backend-services.d.ts +2 -0
  6. package/dist/server/codemap-cache.d.ts +1 -0
  7. package/dist/server/connection-lifecycle.d.ts +13 -0
  8. package/dist/server/connections-health-route.d.ts +7 -1
  9. package/dist/server/context-editor.d.ts +1 -2
  10. package/dist/server/entry.js +1043 -466
  11. package/dist/server/frontend-static-serve.d.ts +7 -0
  12. package/dist/server/governance-runtime.d.ts +30 -0
  13. package/dist/server/http-server.d.ts +10 -1
  14. package/dist/server/mcp-handlers.d.ts +3 -1
  15. package/dist/server/memory-handlers.d.ts +3 -3
  16. package/dist/server/pending-confirms.d.ts +9 -0
  17. package/dist/server/sdd-board-ws-handler.d.ts +1 -0
  18. package/dist/server/sdd-wizard-ws-handler.d.ts +2 -0
  19. package/dist/server/specs-ws-handler.d.ts +1 -0
  20. package/dist/server/types.d.ts +3 -1
  21. package/dist/server/ws-auth.d.ts +24 -0
  22. package/dist/server/ws-payload-validation.d.ts +18 -0
  23. package/dist/server/ws-utils.d.ts +12 -1
  24. package/package.json +13 -12
  25. package/dist/index.d.ts.map +0 -1
  26. package/dist/index.js.map +0 -7
  27. package/dist/protocol/client-conversation.d.ts.map +0 -1
  28. package/dist/protocol/client-integrations.d.ts.map +0 -1
  29. package/dist/protocol/client-operations.d.ts.map +0 -1
  30. package/dist/protocol/client-workspace.d.ts.map +0 -1
  31. package/dist/protocol/connection-fsm.d.ts.map +0 -1
  32. package/dist/protocol/decoder.d.ts.map +0 -1
  33. package/dist/protocol/index.d.ts.map +0 -1
  34. package/dist/protocol/index.js.map +0 -7
  35. package/dist/protocol/projections.d.ts.map +0 -1
  36. package/dist/protocol/registry.d.ts.map +0 -1
  37. package/dist/protocol/replay-payload.d.ts.map +0 -1
  38. package/dist/protocol/server-conversation.d.ts.map +0 -1
  39. package/dist/protocol/server-integrations.d.ts.map +0 -1
  40. package/dist/protocol/server-operations.d.ts.map +0 -1
  41. package/dist/protocol/server-workspace.d.ts.map +0 -1
  42. package/dist/protocol/types.d.ts.map +0 -1
  43. package/dist/protocol/version.d.ts.map +0 -1
  44. package/dist/server/agent-roster-handlers.d.ts.map +0 -1
  45. package/dist/server/agent-roster-routes.d.ts.map +0 -1
  46. package/dist/server/autonomy-routes.d.ts.map +0 -1
  47. package/dist/server/backend-services.d.ts.map +0 -1
  48. package/dist/server/boot.d.ts.map +0 -1
  49. package/dist/server/brain-handlers.d.ts.map +0 -1
  50. package/dist/server/brain-routes.d.ts.map +0 -1
  51. package/dist/server/chronicle-routes.d.ts.map +0 -1
  52. package/dist/server/client-presence.d.ts.map +0 -1
  53. package/dist/server/client-transport-routes.d.ts.map +0 -1
  54. package/dist/server/codebase-index-server-control.d.ts.map +0 -1
  55. package/dist/server/codebase-indexing.d.ts.map +0 -1
  56. package/dist/server/codemap-cache.d.ts.map +0 -1
  57. package/dist/server/codemap-handlers.d.ts.map +0 -1
  58. package/dist/server/codemap-telemetry.d.ts.map +0 -1
  59. package/dist/server/collaboration-ws-handler.d.ts.map +0 -1
  60. package/dist/server/completion-handlers.d.ts.map +0 -1
  61. package/dist/server/completion-routes.d.ts.map +0 -1
  62. package/dist/server/config-doctor.d.ts.map +0 -1
  63. package/dist/server/connection-handler.d.ts.map +0 -1
  64. package/dist/server/connection-lifecycle.d.ts.map +0 -1
  65. package/dist/server/connections-health-route.d.ts.map +0 -1
  66. package/dist/server/content-routes.d.ts.map +0 -1
  67. package/dist/server/context-editor.d.ts.map +0 -1
  68. package/dist/server/context-meta.d.ts.map +0 -1
  69. package/dist/server/conversation-operations.d.ts.map +0 -1
  70. package/dist/server/conversation-routes.d.ts.map +0 -1
  71. package/dist/server/custom-context-modes.d.ts.map +0 -1
  72. package/dist/server/deadcode-handlers.d.ts.map +0 -1
  73. package/dist/server/design-handlers.d.ts.map +0 -1
  74. package/dist/server/discover-mailbox-bridge.d.ts.map +0 -1
  75. package/dist/server/embedded-host-adapters.d.ts.map +0 -1
  76. package/dist/server/embedded-lifecycle.d.ts.map +0 -1
  77. package/dist/server/embedded-message-router.d.ts.map +0 -1
  78. package/dist/server/entry.d.ts.map +0 -1
  79. package/dist/server/entry.js.map +0 -7
  80. package/dist/server/eternal-iteration-broadcast.d.ts.map +0 -1
  81. package/dist/server/file-handlers.d.ts.map +0 -1
  82. package/dist/server/file-picker.d.ts.map +0 -1
  83. package/dist/server/frontend-static-serve.d.ts.map +0 -1
  84. package/dist/server/git-handlers.d.ts.map +0 -1
  85. package/dist/server/git-process.d.ts.map +0 -1
  86. package/dist/server/goal-handlers.d.ts.map +0 -1
  87. package/dist/server/goal-routes.d.ts.map +0 -1
  88. package/dist/server/goal-snapshot-routes.d.ts.map +0 -1
  89. package/dist/server/goal-ws-handler.d.ts.map +0 -1
  90. package/dist/server/handlers/index.d.ts.map +0 -1
  91. package/dist/server/handlers/worklist-handlers.d.ts.map +0 -1
  92. package/dist/server/handlers.js.map +0 -7
  93. package/dist/server/host-routes.d.ts.map +0 -1
  94. package/dist/server/http-server/analytics-handler.d.ts.map +0 -1
  95. package/dist/server/http-server/api-handlers.d.ts.map +0 -1
  96. package/dist/server/http-server.d.ts.map +0 -1
  97. package/dist/server/index.d.ts.map +0 -1
  98. package/dist/server/instance-registry.d.ts.map +0 -1
  99. package/dist/server/introspection-routes.d.ts.map +0 -1
  100. package/dist/server/kanban-board-watcher.d.ts.map +0 -1
  101. package/dist/server/kanban-daemon-subscriber.d.ts.map +0 -1
  102. package/dist/server/kanban-decomposition-routes.d.ts.map +0 -1
  103. package/dist/server/kanban-dispatch.d.ts.map +0 -1
  104. package/dist/server/kanban-host-routes.d.ts.map +0 -1
  105. package/dist/server/kanban-route-helpers.d.ts.map +0 -1
  106. package/dist/server/kanban-route-pagination.d.ts.map +0 -1
  107. package/dist/server/kanban-route-protocol.d.ts.map +0 -1
  108. package/dist/server/kanban-routes.d.ts.map +0 -1
  109. package/dist/server/kanban-run-mirror.d.ts.map +0 -1
  110. package/dist/server/kanban-supervisor.d.ts.map +0 -1
  111. package/dist/server/kanban-task-routes.d.ts.map +0 -1
  112. package/dist/server/lifecycle.d.ts.map +0 -1
  113. package/dist/server/mailbox-handlers.d.ts.map +0 -1
  114. package/dist/server/mailbox-routes.d.ts.map +0 -1
  115. package/dist/server/mcp-handlers.d.ts.map +0 -1
  116. package/dist/server/mcp-routes.d.ts.map +0 -1
  117. package/dist/server/memory-diagnostics.d.ts.map +0 -1
  118. package/dist/server/memory-handlers.d.ts.map +0 -1
  119. package/dist/server/memory-routes.d.ts.map +0 -1
  120. package/dist/server/message-dispatcher.d.ts.map +0 -1
  121. package/dist/server/mode-handlers.d.ts.map +0 -1
  122. package/dist/server/mode-operations.d.ts.map +0 -1
  123. package/dist/server/mode-routes.d.ts.map +0 -1
  124. package/dist/server/model-auto-discovery.d.ts.map +0 -1
  125. package/dist/server/model-catalog-search.d.ts.map +0 -1
  126. package/dist/server/model-catalog.d.ts.map +0 -1
  127. package/dist/server/model-operations.d.ts.map +0 -1
  128. package/dist/server/network-info.d.ts.map +0 -1
  129. package/dist/server/open-browser.d.ts.map +0 -1
  130. package/dist/server/path-containment.d.ts.map +0 -1
  131. package/dist/server/pending-confirms.d.ts.map +0 -1
  132. package/dist/server/port-utils.d.ts.map +0 -1
  133. package/dist/server/pre-context-services.d.ts.map +0 -1
  134. package/dist/server/pref-helpers.d.ts.map +0 -1
  135. package/dist/server/prefs-handlers.d.ts.map +0 -1
  136. package/dist/server/prefs-routes.d.ts.map +0 -1
  137. package/dist/server/privileged-actions.d.ts.map +0 -1
  138. package/dist/server/process-handlers.d.ts.map +0 -1
  139. package/dist/server/process-routes.d.ts.map +0 -1
  140. package/dist/server/project-handlers.d.ts.map +0 -1
  141. package/dist/server/project-routes.d.ts.map +0 -1
  142. package/dist/server/projects-manifest.d.ts.map +0 -1
  143. package/dist/server/prompts-handlers.d.ts.map +0 -1
  144. package/dist/server/provider-config-io.d.ts.map +0 -1
  145. package/dist/server/provider-config-standalone.d.ts.map +0 -1
  146. package/dist/server/provider-handlers.d.ts.map +0 -1
  147. package/dist/server/provider-keys.d.ts.map +0 -1
  148. package/dist/server/provider-routes.d.ts.map +0 -1
  149. package/dist/server/provider-store.d.ts.map +0 -1
  150. package/dist/server/route-family-dispatcher.d.ts.map +0 -1
  151. package/dist/server/routes.d.ts.map +0 -1
  152. package/dist/server/sdd-board-routes.d.ts.map +0 -1
  153. package/dist/server/sdd-board-ws-handler.d.ts.map +0 -1
  154. package/dist/server/sdd-wizard-routes.d.ts.map +0 -1
  155. package/dist/server/sdd-wizard-wiring.d.ts.map +0 -1
  156. package/dist/server/sdd-wizard-ws-handler.d.ts.map +0 -1
  157. package/dist/server/server-runtime.d.ts.map +0 -1
  158. package/dist/server/session-handlers.d.ts.map +0 -1
  159. package/dist/server/session-history.d.ts.map +0 -1
  160. package/dist/server/session-routes.d.ts.map +0 -1
  161. package/dist/server/setup-event-projection.d.ts.map +0 -1
  162. package/dist/server/setup-events-core-watchers.d.ts.map +0 -1
  163. package/dist/server/setup-events-fleet-broadcaster.d.ts.map +0 -1
  164. package/dist/server/setup-events-provider-handlers.d.ts.map +0 -1
  165. package/dist/server/setup-events-session-helpers.d.ts.map +0 -1
  166. package/dist/server/setup-events-status-watcher.d.ts.map +0 -1
  167. package/dist/server/setup-events-watcher.d.ts.map +0 -1
  168. package/dist/server/setup-events.d.ts.map +0 -1
  169. package/dist/server/setup-screen.d.ts.map +0 -1
  170. package/dist/server/shell-git-routes.d.ts.map +0 -1
  171. package/dist/server/shell-open.d.ts.map +0 -1
  172. package/dist/server/skills-handlers.d.ts.map +0 -1
  173. package/dist/server/specs-routes.d.ts.map +0 -1
  174. package/dist/server/specs-ws-handler.d.ts.map +0 -1
  175. package/dist/server/standalone-session-identity.d.ts.map +0 -1
  176. package/dist/server/start-webui.d.ts.map +0 -1
  177. package/dist/server/stream-coalescer.d.ts.map +0 -1
  178. package/dist/server/techstack-handlers.d.ts.map +0 -1
  179. package/dist/server/terminal-ws-handler.d.ts.map +0 -1
  180. package/dist/server/token-estimator.d.ts.map +0 -1
  181. package/dist/server/types.d.ts.map +0 -1
  182. package/dist/server/usage-cost.d.ts.map +0 -1
  183. package/dist/server/worklist-routes.d.ts.map +0 -1
  184. package/dist/server/worktree-routes.d.ts.map +0 -1
  185. package/dist/server/worktree-ws-handler.d.ts.map +0 -1
  186. package/dist/server/ws-auth.d.ts.map +0 -1
  187. package/dist/server/ws-payload-preferences.d.ts.map +0 -1
  188. package/dist/server/ws-payload-validation.d.ts.map +0 -1
  189. package/dist/server/ws-utils.d.ts.map +0 -1
  190. 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,17 @@ 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: inverse fsAccess flag.
81
+ "allowOutsideProjectRoot",
82
+ // v13 Display parity (TUI SettingsPicker fields 42 & 43): the read tool
83
+ // includes codebase-index symbols, and SAGE memory-inject blocks are
84
+ // surfaced in tool results. Both are pure WebUI-display toggles mirrored
85
+ // against the TUI's settings picker; the server mirrors them through
86
+ // `prefs.update` exactly so the panel does not error with
87
+ // "unknown preference key".
88
+ "readSymbols",
89
+ "showSageMemoryInject"
76
90
  ]);
77
91
  var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set([
78
92
  "fallbackModels",
@@ -93,14 +107,39 @@ var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
93
107
  "maxIterations",
94
108
  "maxConcurrent",
95
109
  "enhanceDelayMs",
110
+ "enhanceCountdownMs",
96
111
  "tgLongToolMs",
97
112
  "breakerAutoKillResetMs",
98
113
  // Chimera + auto-review numeric knobs
99
114
  "chimeraMaxFiles",
100
115
  "autoReviewDebounceMs",
101
116
  "autoReviewMaxFilesPerBatch",
102
- "autoReviewMaxConcurrentReviews"
117
+ "autoReviewMaxConcurrentReviews",
118
+ // v13 Display parity (TUI SettingsPicker fields 41, 44, 21): the
119
+ // pre-refine grace countdown (seconds), the SAGE memory-inject relation
120
+ // floor (0..1), and the minimum file count for the multi-diff summary
121
+ // footer. All three are driven by the WebUI sliders in DisplaySection
122
+ // and previously tripped the "unknown preference key" rejection.
123
+ "preRefineSeconds",
124
+ "sageMemoryInjectThreshold",
125
+ "multiDiffSummaryThreshold"
103
126
  ]);
127
+ var NUMBER_PREF_BOUNDS = {
128
+ // Iteration / concurrency — must be at least 1 to make progress.
129
+ maxIterations: { min: 1, max: Number.POSITIVE_INFINITY },
130
+ maxConcurrent: { min: 1, max: Number.POSITIVE_INFINITY },
131
+ autoProceedMaxIterations: { min: 1, max: Number.POSITIVE_INFINITY },
132
+ chimeraMaxFiles: { min: 1, max: Number.POSITIVE_INFINITY },
133
+ autoReviewMaxFilesPerBatch: { min: 1, max: Number.POSITIVE_INFINITY },
134
+ autoReviewMaxConcurrentReviews: { min: 1, max: Number.POSITIVE_INFINITY },
135
+ // Counts (must be >= 1 to render a footer at all).
136
+ multiDiffSummaryThreshold: { min: 1, max: Number.POSITIVE_INFINITY },
137
+ preRefineSeconds: { min: 0, max: Number.POSITIVE_INFINITY },
138
+ // Probability — must lie in [0, 1].
139
+ sageMemoryInjectThreshold: { min: 0, max: 1 },
140
+ // Debounce / delay — non-negative ms.
141
+ autoReviewDebounceMs: { min: 0, max: Number.POSITIVE_INFINITY }
142
+ };
104
143
  var STRING_PREF_KEYS = /* @__PURE__ */ new Set([
105
144
  "hqUrl",
106
145
  "hqToken",
@@ -133,7 +172,8 @@ var ENUM_PREF_KEYS = {
133
172
  // Chimera autoFix + auto-review cascade threshold
134
173
  chimeraAutoFix: /* @__PURE__ */ new Set(["off", "ask", "auto"]),
135
174
  autoReviewCascadeOn: /* @__PURE__ */ new Set(["off", "critical", "high"]),
136
- fleetChatVerbosity: /* @__PURE__ */ new Set(["off", "full"])
175
+ fleetChatVerbosity: /* @__PURE__ */ new Set(["off", "full"]),
176
+ showAgentSwarmPanel: /* @__PURE__ */ new Set(["bottom", "sidebar", "off"])
137
177
  };
138
178
  function validateModelRuntimeValue(modelRuntime, path30) {
139
179
  const reasoning = modelRuntime["reasoning"];
@@ -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;
@@ -7941,7 +8381,7 @@ function buildCspHeader(publicWsUrl, host, port) {
7941
8381
  if (host && isLoopbackHostname(host)) {
7942
8382
  const p = port ?? 3456;
7943
8383
  if (p > 0 && p <= 65535) {
7944
- for (const h of ["127.0.0.1", "localhost", "[::1]"]) {
8384
+ for (const h of ["127.0.0.1", "localhost"]) {
7945
8385
  connect.add(`ws://${h}:${p}`);
7946
8386
  connect.add(`wss://${h}:${p}`);
7947
8387
  }
@@ -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 ?? "");
@@ -8170,47 +8630,7 @@ function createHttpServer(opts) {
8170
8630
  );
8171
8631
  return;
8172
8632
  }
8173
- if (url.pathname === "/api/codemap/symbols" && req.method === "GET") {
8174
- if (requireAccessToken && !accessTokenOk) {
8175
- res.writeHead(401, { "Content-Type": "application/json" });
8176
- res.end(JSON.stringify({ error: "Unauthorized" }));
8177
- return;
8178
- }
8179
- if (!opts.projectRoot) {
8180
- res.writeHead(503, { "Content-Type": "application/json" });
8181
- res.end(JSON.stringify({ error: "Project root not configured" }));
8182
- return;
8183
- }
8184
- const file = url.searchParams.get("file") ?? "";
8185
- await handleCodemapSymbols(
8186
- res,
8187
- {
8188
- projectRoot: opts.projectRoot,
8189
- ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8190
- },
8191
- file
8192
- );
8193
- return;
8194
- }
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") {
8633
+ if (url.pathname === "/api/codemap/symbols" && req.method === "GET") {
8214
8634
  if (requireAccessToken && !accessTokenOk) {
8215
8635
  res.writeHead(401, { "Content-Type": "application/json" });
8216
8636
  res.end(JSON.stringify({ error: "Unauthorized" }));
@@ -8221,11 +8641,15 @@ function createHttpServer(opts) {
8221
8641
  res.end(JSON.stringify({ error: "Project root not configured" }));
8222
8642
  return;
8223
8643
  }
8224
- const deadCodeDeps = {
8225
- projectRoot: opts.projectRoot,
8226
- ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8227
- };
8228
- await handleDeadCodeActionPlan(res, deadCodeDeps, req);
8644
+ const file = url.searchParams.get("file") ?? "";
8645
+ await handleCodemapSymbols(
8646
+ res,
8647
+ {
8648
+ projectRoot: opts.projectRoot,
8649
+ ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8650
+ },
8651
+ file
8652
+ );
8229
8653
  return;
8230
8654
  }
8231
8655
  if (url.pathname.startsWith("/api/techstack/")) {
@@ -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 {
@@ -13975,6 +14399,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
13975
14399
  "agent.status_changed",
13976
14400
  "agent.timeline.message",
13977
14401
  "client.status_update",
14402
+ "chimera.report_available",
13978
14403
  "compaction.failed",
13979
14404
  "completion.result",
13980
14405
  "context.compacted",
@@ -16073,6 +16498,7 @@ var SddBoardWebSocketHandler = class {
16073
16498
  poll = null;
16074
16499
  pollInFlight = false;
16075
16500
  unsub = null;
16501
+ disposed = false;
16076
16502
  constructor(boardsDir, events, lifecycle, security) {
16077
16503
  this.store = new SddBoardStore({ baseDir: boardsDir });
16078
16504
  this.lifecycle = lifecycle;
@@ -16087,6 +16513,7 @@ var SddBoardWebSocketHandler = class {
16087
16513
  }
16088
16514
  }
16089
16515
  addClient(ws) {
16516
+ if (this.disposed) return;
16090
16517
  const client = { ws, id: crypto.randomUUID() };
16091
16518
  this.clients.add(client);
16092
16519
  const remove = () => {
@@ -16228,9 +16655,12 @@ var SddBoardWebSocketHandler = class {
16228
16655
  }
16229
16656
  }
16230
16657
  dispose() {
16658
+ this.disposed = true;
16231
16659
  this.stopPolling();
16232
16660
  this.unsub?.();
16233
16661
  this.unsub = null;
16662
+ this.clients.clear();
16663
+ this.latest = null;
16234
16664
  }
16235
16665
  // ── internal ────────────────────────────────────────────────────────────
16236
16666
  async pollLatest() {
@@ -16238,6 +16668,7 @@ var SddBoardWebSocketHandler = class {
16238
16668
  this.pollInFlight = true;
16239
16669
  try {
16240
16670
  const snap = await this.loadLatestSnapshot();
16671
+ if (this.disposed) return;
16241
16672
  if (!snap) return;
16242
16673
  if (this.latest && this.latest.updatedAt >= snap.updatedAt && this.latest.runId === snap.runId) {
16243
16674
  return;
@@ -16551,6 +16982,7 @@ var SddWizardWebSocketHandler = class {
16551
16982
  * flight (or after the probe has settled).
16552
16983
  */
16553
16984
  resumeProbe = null;
16985
+ disposed = false;
16554
16986
  async bootstrap() {
16555
16987
  try {
16556
16988
  await this.deps.ensureReady?.();
@@ -16563,23 +16995,28 @@ var SddWizardWebSocketHandler = class {
16563
16995
  if (this.driver) return;
16564
16996
  try {
16565
16997
  const driver = this.deps.makeDriver();
16566
- if (await driver.loadExisting()) {
16998
+ const loaded = await driver.loadExisting();
16999
+ if (this.disposed) return;
17000
+ if (loaded) {
16567
17001
  this.driver = driver;
16568
17002
  this.lastAgentText = driver.getLastAgentText() ?? "";
16569
17003
  }
16570
17004
  this.resumeError = null;
16571
17005
  } catch (error2) {
17006
+ if (this.disposed) return;
16572
17007
  this.driver = null;
16573
17008
  this.lastAgentText = "";
16574
17009
  this.resumeError = error2 instanceof Error ? error2.message : String(error2);
16575
17010
  }
16576
17011
  }
16577
17012
  addClient(ws) {
17013
+ if (this.disposed) return;
16578
17014
  const client = { ws, id: crypto.randomUUID() };
16579
17015
  this.clients.add(client);
16580
17016
  ws.on("close", () => this.clients.delete(client));
16581
17017
  ws.on("error", () => this.clients.delete(client));
16582
17018
  void this.ready.then(() => {
17019
+ if (this.disposed || !this.clients.has(client)) return;
16583
17020
  if (this.resumeError) {
16584
17021
  this.send(client, { type: "sdd.spec.error", payload: { message: this.resumeError } });
16585
17022
  return;
@@ -16592,7 +17029,16 @@ var SddWizardWebSocketHandler = class {
16592
17029
  }
16593
17030
  });
16594
17031
  }
17032
+ dispose() {
17033
+ this.disposed = true;
17034
+ this.clients.clear();
17035
+ this.driver = null;
17036
+ this.lastAgentText = "";
17037
+ this.busy = false;
17038
+ this.resumeProbe = null;
17039
+ }
16595
17040
  async handleMessage(msg) {
17041
+ if (this.disposed) return;
16596
17042
  try {
16597
17043
  await this.ready;
16598
17044
  if (this.resumeError) {
@@ -16810,6 +17256,7 @@ var SddWizardWebSocketHandler = class {
16810
17256
  this.broadcast(this.snapshotMsg());
16811
17257
  try {
16812
17258
  const text = await this.deps.runInterviewTurn(prompt);
17259
+ if (this.disposed) return;
16813
17260
  this.lastAgentText = text;
16814
17261
  if (this.driver) {
16815
17262
  await this.driver.ingestAgentOutput(text);
@@ -16835,6 +17282,7 @@ var SddWizardWebSocketHandler = class {
16835
17282
  }
16836
17283
  }
16837
17284
  send(client, msg) {
17285
+ if (!this.clients.has(client)) return;
16838
17286
  sendSerialized(client.ws, JSON.stringify(msg));
16839
17287
  }
16840
17288
  };
@@ -16842,12 +17290,67 @@ var SddWizardWebSocketHandler = class {
16842
17290
  // src/server/setup-events.ts
16843
17291
  import { recordTaskFileActivity } from "@wrongstack/kanban";
16844
17292
 
17293
+ // src/server/setup-events-core-watchers.ts
17294
+ import * as fs17 from "node:fs/promises";
17295
+ import * as path19 from "node:path";
17296
+ function registerSetupEventsCoreWatchers(deps2) {
17297
+ const { broadcast: broadcast2, clients, context } = deps2;
17298
+ const disposers = [];
17299
+ const conversationState = context.state;
17300
+ if (typeof conversationState?.onChange === "function") {
17301
+ disposers.push(
17302
+ conversationState.onChange((change) => {
17303
+ if (change.kind !== "todos_replaced") return;
17304
+ broadcast2(clients, {
17305
+ type: "todos.updated",
17306
+ payload: {
17307
+ sessionId: context.session?.id ?? "",
17308
+ todos: [...change.todos],
17309
+ revision: conversationState.revision
17310
+ }
17311
+ });
17312
+ })
17313
+ );
17314
+ }
17315
+ const projectRoot = context.projectRoot;
17316
+ if (projectRoot) {
17317
+ disposers.push(
17318
+ subscribeKanbanDaemonEvents(projectRoot, (message) => broadcast2(clients, message))
17319
+ );
17320
+ }
17321
+ return disposers;
17322
+ }
17323
+ function registerSetupEventsClientStatusWriter(deps2) {
17324
+ const { broadcast: broadcast2, clients, events, wpaths } = deps2;
17325
+ const on = (event, listener) => events.on(event, listener);
17326
+ return on("client.status", async (e) => {
17327
+ broadcast2(clients, { type: "client.status_update", payload: e });
17328
+ if (wpaths?.projectStatus) {
17329
+ try {
17330
+ const statusFile = wpaths.projectStatus(e.projectHash);
17331
+ const dir = path19.dirname(statusFile);
17332
+ await fs17.mkdir(dir, { recursive: true });
17333
+ await fs17.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
17334
+ } catch (err) {
17335
+ console.error(
17336
+ JSON.stringify({
17337
+ level: "error",
17338
+ event: "setup_events.status_write_failed",
17339
+ message: err instanceof Error ? err.message : String(err),
17340
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
17341
+ })
17342
+ );
17343
+ }
17344
+ }
17345
+ });
17346
+ }
17347
+
16845
17348
  // src/server/setup-events-fleet-broadcaster.ts
16846
17349
  import { watch as fsWatch } from "node:fs";
16847
- import * as path19 from "node:path";
17350
+ import * as path20 from "node:path";
16848
17351
  function registerSetupEventsFleetBroadcaster(deps2) {
16849
17352
  const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
16850
- const globalRoot = globalConfigPath ? path19.dirname(globalConfigPath) : void 0;
17353
+ const globalRoot = globalConfigPath ? path20.dirname(globalConfigPath) : void 0;
16851
17354
  if (!globalRoot) return void 0;
16852
17355
  const disposers = [];
16853
17356
  const broadcastSessions = async () => {
@@ -16857,8 +17360,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
16857
17360
  const sessions = await registry.list();
16858
17361
  const ownEntry = sessions.find((s) => s.pid === process.pid);
16859
17362
  const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
16860
- const myRoot = path19.resolve(context.projectRoot);
16861
- const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug : path19.resolve(s.projectRoot) === myRoot).map((s) => ({
17363
+ const myRoot = path20.resolve(context.projectRoot);
17364
+ const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug : path20.resolve(s.projectRoot) === myRoot).map((s) => ({
16862
17365
  sessionId: s.sessionId,
16863
17366
  projectName: s.projectName,
16864
17367
  projectSlug: s.projectSlug,
@@ -17048,14 +17551,14 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
17048
17551
 
17049
17552
  // src/server/setup-events-status-watcher.ts
17050
17553
  import { watch as fsWatch2 } from "node:fs";
17051
- import * as fs17 from "node:fs/promises";
17052
- import * as path21 from "node:path";
17554
+ import * as fs18 from "node:fs/promises";
17555
+ import * as path22 from "node:path";
17053
17556
 
17054
17557
  // src/server/setup-events-watcher.ts
17055
- import * as path20 from "node:path";
17558
+ import * as path21 from "node:path";
17056
17559
  function statusProjectHashFromWatchFilename(projectsDir, filename) {
17057
17560
  const raw = String(filename);
17058
- const relative5 = path20.isAbsolute(raw) ? path20.relative(projectsDir, raw) : raw;
17561
+ const relative5 = path21.isAbsolute(raw) ? path21.relative(projectsDir, raw) : raw;
17059
17562
  const parts = relative5.split(/[\\/]+/).filter(Boolean);
17060
17563
  if (parts.length < 2 || parts.at(-1) !== "status.json") return null;
17061
17564
  return parts.at(-2) ?? null;
@@ -17090,7 +17593,7 @@ function logFileWatcherMetrics(metrics) {
17090
17593
  function registerSetupEventsStatusWatcher(deps2) {
17091
17594
  const { wpaths, watcherMetrics, clients, broadcast: broadcast2, on, isDisposed } = deps2;
17092
17595
  if (!wpaths?.projectStatus || !wpaths.globalRoot) return void 0;
17093
- const projectsDir = path21.join(wpaths.globalRoot, "projects");
17596
+ const projectsDir = path22.join(wpaths.globalRoot, "projects");
17094
17597
  const knownProjectHashes = /* @__PURE__ */ new Set();
17095
17598
  const debounceTimers = /* @__PURE__ */ new Map();
17096
17599
  const DEBOUNCE_MS = 150;
@@ -17135,7 +17638,7 @@ function registerSetupEventsStatusWatcher(deps2) {
17135
17638
  let watcher;
17136
17639
  const startWatcher = async () => {
17137
17640
  try {
17138
- await fs17.mkdir(projectsDir, { recursive: true });
17641
+ await fs18.mkdir(projectsDir, { recursive: true });
17139
17642
  if (isDisposed()) return;
17140
17643
  watcher = fsWatch2(
17141
17644
  projectsDir,
@@ -17149,8 +17652,8 @@ function registerSetupEventsStatusWatcher(deps2) {
17149
17652
  if (!knownProjectHashes.has(projectHash)) return;
17150
17653
  if (watcherMetrics) watcherMetrics.filesProcessed++;
17151
17654
  try {
17152
- const targetFile = path21.join(projectsDir, projectHash, "status.json");
17153
- const content = await fs17.readFile(targetFile, "utf-8");
17655
+ const targetFile = path22.join(projectsDir, projectHash, "status.json");
17656
+ const content = await fs18.readFile(targetFile, "utf-8");
17154
17657
  const statusData = JSON.parse(content);
17155
17658
  scheduleBroadcast(projectHash, statusData);
17156
17659
  } catch {
@@ -17206,61 +17709,6 @@ function registerSetupEventsStatusWatcher(deps2) {
17206
17709
  };
17207
17710
  }
17208
17711
 
17209
- // src/server/setup-events-core-watchers.ts
17210
- import * as fs18 from "node:fs/promises";
17211
- import * as path22 from "node:path";
17212
- function registerSetupEventsCoreWatchers(deps2) {
17213
- const { broadcast: broadcast2, clients, context } = deps2;
17214
- const disposers = [];
17215
- const conversationState = context.state;
17216
- if (typeof conversationState?.onChange === "function") {
17217
- disposers.push(
17218
- conversationState.onChange((change) => {
17219
- if (change.kind !== "todos_replaced") return;
17220
- broadcast2(clients, {
17221
- type: "todos.updated",
17222
- payload: {
17223
- sessionId: context.session?.id ?? "",
17224
- todos: [...change.todos],
17225
- revision: conversationState.revision
17226
- }
17227
- });
17228
- })
17229
- );
17230
- }
17231
- const projectRoot = context.projectRoot;
17232
- if (projectRoot) {
17233
- disposers.push(
17234
- subscribeKanbanDaemonEvents(projectRoot, (message) => broadcast2(clients, message))
17235
- );
17236
- }
17237
- return disposers;
17238
- }
17239
- function registerSetupEventsClientStatusWriter(deps2) {
17240
- const { broadcast: broadcast2, clients, events, wpaths } = deps2;
17241
- const on = (event, listener) => events.on(event, listener);
17242
- return on("client.status", async (e) => {
17243
- broadcast2(clients, { type: "client.status_update", payload: e });
17244
- if (wpaths?.projectStatus) {
17245
- try {
17246
- const statusFile = wpaths.projectStatus(e.projectHash);
17247
- const dir = path22.dirname(statusFile);
17248
- await fs18.mkdir(dir, { recursive: true });
17249
- await fs18.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
17250
- } catch (err) {
17251
- console.error(
17252
- JSON.stringify({
17253
- level: "error",
17254
- event: "setup_events.status_write_failed",
17255
- message: err instanceof Error ? err.message : String(err),
17256
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
17257
- })
17258
- );
17259
- }
17260
- }
17261
- });
17262
- }
17263
-
17264
17712
  // src/server/setup-events.ts
17265
17713
  function setupEvents(deps2) {
17266
17714
  const {
@@ -17601,6 +18049,7 @@ function setupEvents(deps2) {
17601
18049
  });
17602
18050
  pendingConfirms.set(id, {
17603
18051
  resolve: e.resolve,
18052
+ sessionId: e.sessionId,
17604
18053
  decisionSource: e.decisionSource,
17605
18054
  riskTier: e.riskTier,
17606
18055
  boundaryReason: e.boundaryReason,
@@ -17815,6 +18264,12 @@ function setupEvents(deps2) {
17815
18264
  });
17816
18265
  });
17817
18266
  disposers.push(
18267
+ events.onPattern("chimera.report_available", (_event, payload) => {
18268
+ broadcast2(clients, {
18269
+ type: "chimera.report_available",
18270
+ payload
18271
+ });
18272
+ }),
17818
18273
  events.onPattern("mailbox.received", (_e, payload) => {
17819
18274
  broadcast2(clients, { type: "mailbox.received", payload });
17820
18275
  }),
@@ -18129,6 +18584,9 @@ var SpecsWebSocketHandler = class {
18129
18584
  ws.on("error", () => this.clients.delete(client));
18130
18585
  void this.sendList(client);
18131
18586
  }
18587
+ dispose() {
18588
+ this.clients.clear();
18589
+ }
18132
18590
  async handleMessage(msg) {
18133
18591
  switch (msg.type) {
18134
18592
  case "specs.list":
@@ -18264,12 +18722,13 @@ var SpecsWebSocketHandler = class {
18264
18722
  }
18265
18723
  }
18266
18724
  send(client, msg) {
18725
+ if (!this.clients.has(client)) return;
18267
18726
  sendSerialized(client.ws, JSON.stringify(msg));
18268
18727
  }
18269
18728
  };
18270
18729
 
18271
18730
  // src/server/start-webui.ts
18272
- import { randomUUID as randomUUID5 } from "node:crypto";
18731
+ import { randomUUID as randomUUID4 } from "node:crypto";
18273
18732
  import * as http2 from "node:http";
18274
18733
  import * as path29 from "node:path";
18275
18734
  import { createDefaultPipelines } from "@wrongstack/core/agent";
@@ -18294,7 +18753,7 @@ import { toLanguagePackageInput } from "@wrongstack/techstack";
18294
18753
  import { ensureSessionShell } from "@wrongstack/tools";
18295
18754
 
18296
18755
  // src/server/backend-services.ts
18297
- import { join as join13 } from "node:path";
18756
+ import { join as join12 } from "node:path";
18298
18757
  import { Agent } from "@wrongstack/core/agent";
18299
18758
  import {
18300
18759
  BrainDecisionLedger,
@@ -18307,7 +18766,7 @@ import {
18307
18766
  mailboxSessionTag,
18308
18767
  ObservableBrainArbiter as ObservableBrainArbiterCtor
18309
18768
  } from "@wrongstack/core/coordination";
18310
- import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/defaults";
18769
+ import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/types";
18311
18770
  import { installDesignStudioMiddleware } from "@wrongstack/core/design";
18312
18771
  import {
18313
18772
  AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
@@ -18327,17 +18786,19 @@ import {
18327
18786
  } from "@wrongstack/core/utils";
18328
18787
  import { makeLightSubagentFactory } from "@wrongstack/runtime";
18329
18788
  import {
18789
+ createSageContextMonitorMiddleware,
18330
18790
  createSageToolCallMiddleware,
18331
18791
  createSageTurnMiddleware,
18332
18792
  getSageRetrieval,
18333
- getSageService
18793
+ getSageService,
18794
+ InjectionTracker
18334
18795
  } from "@wrongstack/sage";
18335
18796
 
18336
18797
  // src/server/discover-mailbox-bridge.ts
18337
18798
  import { spawn as spawn2 } from "node:child_process";
18338
18799
  import { createRequire } from "node:module";
18339
18800
  import { existsSync } from "node:fs";
18340
- import { dirname as dirname7, join as join11 } from "node:path";
18801
+ import { dirname as dirname7, join as join10 } from "node:path";
18341
18802
  import { resolveProjectDir as resolveProjectDir2 } from "@wrongstack/core/coordination";
18342
18803
  import { wstackGlobalRoot } from "@wrongstack/core/utils";
18343
18804
  import { readLiveLock } from "@wrongstack/core/coordination";
@@ -18466,7 +18927,7 @@ function mailboxServeInvocation(projectRoot) {
18466
18927
  function findWorkspaceCliEntry(projectRoot) {
18467
18928
  let dir = projectRoot;
18468
18929
  for (let i = 0; i < 6; i++) {
18469
- const candidate = join11(dir, "packages", "cli", "dist", "index.js");
18930
+ const candidate = join10(dir, "packages", "cli", "dist", "index.js");
18470
18931
  if (existsSync(candidate)) return candidate;
18471
18932
  const parent = dirname7(dir);
18472
18933
  if (parent === dir) return null;
@@ -18711,7 +19172,7 @@ function clampDim(value, fallback) {
18711
19172
  }
18712
19173
 
18713
19174
  // src/server/worktree-ws-handler.ts
18714
- import { join as join12, resolve as resolve12, sep as sep5 } from "node:path";
19175
+ import { join as join11, resolve as resolve12, sep as sep5 } from "node:path";
18715
19176
  import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
18716
19177
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
18717
19178
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
@@ -18788,11 +19249,15 @@ var WorktreeWebSocketHandler = class {
18788
19249
  for (const off of this.offs) off();
18789
19250
  this.offs.length = 0;
18790
19251
  this.stopBroadcast();
19252
+ this.clients.clear();
19253
+ this.handles.clear();
19254
+ this.baseBranch = "";
19255
+ this.scanRescanNeeded = false;
18791
19256
  }
18792
19257
  // ── orphan management ─────────────────────────────────────────────────────
18793
19258
  /** Absolute managed-worktrees root for this project. */
18794
19259
  worktreesRoot() {
18795
- return resolve12(join12(this.management.projectRoot, ".wrongstack", "worktrees"));
19260
+ return resolve12(join11(this.management.projectRoot, ".wrongstack", "worktrees"));
18796
19261
  }
18797
19262
  /** True iff `dir` resolves strictly inside the managed worktrees root. */
18798
19263
  underRoot(dir) {
@@ -19174,6 +19639,8 @@ async function createAgentServices(input) {
19174
19639
  installDesignStudioMiddleware({ pipelines, ctx: context });
19175
19640
  const memoryRetrieval = getSageRetrieval(memoryStore);
19176
19641
  if (config.features.memory !== false && config.Sage?.enabled !== false && memoryRetrieval) {
19642
+ const sageInjectionTracker = new InjectionTracker();
19643
+ const getSageSessionId = () => input.sessionGetter().id;
19177
19644
  if (config.Sage?.inject?.toolResults !== false) {
19178
19645
  pipelines.toolCall.use(
19179
19646
  createSageToolCallMiddleware({
@@ -19185,7 +19652,15 @@ async function createAgentServices(input) {
19185
19652
  minImportance: config.Sage?.inject?.minImportance,
19186
19653
  repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
19187
19654
  verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
19188
- triggers: config.Sage?.inject?.triggers
19655
+ triggers: config.Sage?.inject?.triggers,
19656
+ // Resolve the live session so retrieval and cooldown are session-scoped
19657
+ // (matching the CLI wiring in wiring/sage.ts). Without this, the
19658
+ // middleware falls back to ctx.session.id for cooldown but passes
19659
+ // undefined to retrieval, causing owned session-scoped memories to be
19660
+ // silently excluded from tool-call injection.
19661
+ getSessionId: getSageSessionId,
19662
+ tracker: sageInjectionTracker,
19663
+ events
19189
19664
  })
19190
19665
  );
19191
19666
  }
@@ -19195,10 +19670,19 @@ async function createAgentServices(input) {
19195
19670
  memory: memoryRetrieval,
19196
19671
  maxMemories: config.Sage?.inject?.maxTurnMemories,
19197
19672
  maxChars: config.Sage?.inject?.maxCharsPerTurn,
19198
- minScore: config.Sage?.inject?.minScore
19673
+ minScore: config.Sage?.inject?.minScore,
19674
+ getSessionId: getSageSessionId,
19675
+ tracker: sageInjectionTracker
19199
19676
  })
19200
19677
  );
19201
19678
  }
19679
+ pipelines.request.use(
19680
+ createSageContextMonitorMiddleware({
19681
+ tracker: sageInjectionTracker,
19682
+ events,
19683
+ getSessionId: getSageSessionId
19684
+ })
19685
+ );
19202
19686
  }
19203
19687
  const codebaseIndexing = setupWebUICodebaseIndexing({
19204
19688
  config,
@@ -19309,6 +19793,7 @@ async function createAgentServices(input) {
19309
19793
  perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
19310
19794
  tracer: void 0
19311
19795
  });
19796
+ input.installToolBoundary?.(pipelines);
19312
19797
  const webuiLogger = container.resolve(TOKENS.Logger);
19313
19798
  void discoverMailboxBridgeForWebui({
19314
19799
  projectRoot,
@@ -19348,7 +19833,7 @@ async function createAgentServices(input) {
19348
19833
  const brainCfg = resolveBrainConfigDefaults(config.brain, {
19349
19834
  fallbackModels: config.fallbackModels
19350
19835
  });
19351
- const brainLedgerPath = join13(wpaths.projectDir, "brain-ledger.jsonl");
19836
+ const brainLedgerPath = join12(wpaths.projectDir, "brain-ledger.jsonl");
19352
19837
  let brainLedgerEnabled = brainCfg.ledger?.enabled !== false;
19353
19838
  let brainLedger;
19354
19839
  const startBrainLedger = () => {
@@ -19521,7 +20006,8 @@ async function createAgentServices(input) {
19521
20006
  // subagent's fallback extension's tracker hooks are undefined and
19522
20007
  // round-robin keeps reassigning the doomed model. Mirrors the CLI
19523
20008
  // factory wiring at host-subagent-factory.ts:337.
19524
- statusTracker: container.safeResolve(TOKENS.ProviderModelStatusTracker)
20009
+ statusTracker: container.safeResolve(TOKENS.ProviderModelStatusTracker),
20010
+ ...input.installToolBoundary ? { installToolBoundary: input.installToolBoundary } : {}
19525
20011
  }),
19526
20012
  paths: {
19527
20013
  projectSpecs: wpaths.projectSpecs,
@@ -19560,6 +20046,8 @@ async function createAgentServices(input) {
19560
20046
  for (const off of brainLogOffs) off();
19561
20047
  goalHandler.dispose();
19562
20048
  sddBoardHandler.dispose();
20049
+ sddWizardHandler.dispose();
20050
+ specsHandler.dispose();
19563
20051
  worktreeHandler.dispose();
19564
20052
  terminalHandler.dispose();
19565
20053
  collabHandler.dispose();
@@ -19595,7 +20083,10 @@ async function createAgentServices(input) {
19595
20083
  }
19596
20084
 
19597
20085
  // src/server/connection-handler.ts
19598
- var RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
20086
+ var DEFAULT_WS_RATE_LIMIT = 600;
20087
+ var rawRateLimit = process.env["WEBUI_RATE_LIMIT"];
20088
+ var parsedRateLimit = rawRateLimit ? Number.parseInt(rawRateLimit, 10) : DEFAULT_WS_RATE_LIMIT;
20089
+ var RATE_LIMIT_MESSAGES = Number.isFinite(parsedRateLimit) && parsedRateLimit >= 0 ? parsedRateLimit : DEFAULT_WS_RATE_LIMIT;
19599
20090
  var VERBOSE_WS_LOGGING = process.env["WEBUI_VERBOSE_WS"] === "1" || process.env["WEBUI_VERBOSE_WS"] === "true";
19600
20091
  function readRemoteAddress(ws) {
19601
20092
  const socket = ws._socket;
@@ -19675,6 +20166,72 @@ function createConnectionHandler(options) {
19675
20166
  };
19676
20167
  }
19677
20168
 
20169
+ // src/server/governance-runtime.ts
20170
+ import {
20171
+ createGovernanceMutationSnapshotBridge
20172
+ } from "@wrongstack/runtime/governance-mutation-snapshot-bridge";
20173
+ import { sanitizeGovernanceMessage } from "@wrongstack/runtime/governance-sanitize";
20174
+ var WRONGSTACK_GOVERNANCE_ENV = "WRONGSTACK_GOVERNANCE";
20175
+ var DEFAULT_DEPENDENCIES = {
20176
+ load: () => import("@wrongstack/runtime/governance-bootstrap"),
20177
+ createMutationBridge: createGovernanceMutationSnapshotBridge
20178
+ };
20179
+ function webUiGovernanceRuntimeEnabled(environment) {
20180
+ return environment[WRONGSTACK_GOVERNANCE_ENV]?.trim() === "1";
20181
+ }
20182
+ async function setupWebUiGovernance(input, dependencies = DEFAULT_DEPENDENCIES) {
20183
+ if (!webUiGovernanceRuntimeEnabled(input.environment)) return void 0;
20184
+ let loaded;
20185
+ try {
20186
+ loaded = await dependencies.load();
20187
+ } catch (error2) {
20188
+ input.logger.warn("governance: standalone WebUI runtime module unavailable", {
20189
+ message: sanitizeGovernanceMessage(error2 instanceof Error ? error2.message : String(error2))
20190
+ });
20191
+ return void 0;
20192
+ }
20193
+ let result;
20194
+ try {
20195
+ result = await loaded.bootstrapGovernanceRuntime({
20196
+ projectRoot: input.projectRoot,
20197
+ projectId: input.projectId,
20198
+ adminClientId: `webui-control-${process.pid}-${input.sessionId}`,
20199
+ modelClientId: `webui-model-${input.sessionId}`,
20200
+ modelCapabilities: ["task_read", "command_submit", "shadow_observe"]
20201
+ });
20202
+ } catch (error2) {
20203
+ input.logger.warn("governance: standalone WebUI bootstrap failed open", {
20204
+ message: sanitizeGovernanceMessage(error2 instanceof Error ? error2.message : String(error2))
20205
+ });
20206
+ return void 0;
20207
+ }
20208
+ if (result.mode !== "governed") {
20209
+ input.contextMeta["governance"] = result;
20210
+ input.logger.warn(`governance: standalone WebUI legacy fallback (${result.code})`, {
20211
+ phase: result.phase,
20212
+ cleanup: result.cleanup,
20213
+ message: result.message
20214
+ });
20215
+ return void 0;
20216
+ }
20217
+ const snapshot = result.handle.snapshot();
20218
+ input.contextMeta["governance"] = snapshot;
20219
+ input.logger.info(`governance: ${snapshot.source} standalone WebUI control plane`);
20220
+ const mutationSnapshots = dependencies.createMutationBridge({
20221
+ events: input.events,
20222
+ sink: result.handle,
20223
+ captureWorkspaceCheckpoint: input.captureWorkspaceCheckpoint,
20224
+ logger: input.logger
20225
+ });
20226
+ return Object.freeze({
20227
+ installToolBoundary: (pipelines) => mutationSnapshots.installToolBoundary(pipelines),
20228
+ close: async () => {
20229
+ await mutationSnapshots.close();
20230
+ return result.handle.close();
20231
+ }
20232
+ });
20233
+ }
20234
+
19678
20235
  // src/server/message-dispatcher.ts
19679
20236
  import path23 from "node:path";
19680
20237
  function createMessageDispatcher(opts) {
@@ -21379,6 +21936,17 @@ async function startWebUI(opts = {}) {
21379
21936
  const prefSnapshot2 = () => prefSnapshot(context.meta);
21380
21937
  const persistPrefsToConfig2 = async (payload) => persistPrefsToConfig(prefHelperDeps, configWriteLock, payload);
21381
21938
  const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary3({ policyId: "webui-trusted-host-compat-v1" });
21939
+ const governanceHandle = opts.installToolBoundary === void 0 ? await setupWebUiGovernance({
21940
+ environment: process.env,
21941
+ projectRoot,
21942
+ projectId: wpaths.projectSlug,
21943
+ sessionId: session.id,
21944
+ contextMeta: context.meta,
21945
+ events,
21946
+ logger,
21947
+ captureWorkspaceCheckpoint: async () => sessionStore.captureWorkspaceCheckpoint?.(session.id, 0)
21948
+ }) : void 0;
21949
+ const installToolBoundary = opts.installToolBoundary ?? governanceHandle?.installToolBoundary;
21382
21950
  const agentServices = await createAgentServices({
21383
21951
  trustBoundary,
21384
21952
  config,
@@ -21401,6 +21969,7 @@ async function startWebUI(opts = {}) {
21401
21969
  skillInstaller,
21402
21970
  tokenCounter,
21403
21971
  pipelines: createDefaultPipelines(),
21972
+ ...installToolBoundary ? { installToolBoundary } : {},
21404
21973
  modelCapabilitiesRef,
21405
21974
  sessionGetter: () => session,
21406
21975
  sessionReader,
@@ -21482,7 +22051,7 @@ async function startWebUI(opts = {}) {
21482
22051
  const input = toLanguagePackageInput(operation, workspace);
21483
22052
  const use = {
21484
22053
  type: "tool_use",
21485
- id: `techstack-remediation-${randomUUID5()}`,
22054
+ id: `techstack-remediation-${randomUUID4()}`,
21486
22055
  name: "language_package",
21487
22056
  input: { ...input }
21488
22057
  };
@@ -21751,7 +22320,8 @@ async function startWebUI(opts = {}) {
21751
22320
  patchConfig(state.getConfig(), {
21752
22321
  providers: snapshot.providers,
21753
22322
  ...snapshot.apiKey !== void 0 ? { apiKey: snapshot.apiKey } : {},
21754
- ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {}
22323
+ ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {},
22324
+ fallbackBridge: snapshot.fallbackBridge ?? ""
21755
22325
  })
21756
22326
  );
21757
22327
  deps2.configStore.update({
@@ -21759,6 +22329,7 @@ async function startWebUI(opts = {}) {
21759
22329
  ...snapshot.apiKey !== void 0 ? { apiKey: snapshot.apiKey } : {},
21760
22330
  ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {},
21761
22331
  ...snapshot.fallbackModels !== void 0 ? { fallbackModels: snapshot.fallbackModels } : {},
22332
+ fallbackBridge: snapshot.fallbackBridge ?? "",
21762
22333
  ...snapshot.fallbackProfiles !== void 0 ? { fallbackProfiles: snapshot.fallbackProfiles } : {},
21763
22334
  ...snapshot.favoriteModels !== void 0 ? { favoriteModels: snapshot.favoriteModels } : {},
21764
22335
  ...snapshot.favoriteModelsOnly !== void 0 ? { favoriteModelsOnly: snapshot.favoriteModelsOnly } : {},
@@ -21893,6 +22464,12 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
21893
22464
  await stopHeapWatchdog();
21894
22465
  credentialWatcherClose?.();
21895
22466
  disposeRealtimeHandlers();
22467
+ const governanceCleanup = await governanceHandle?.close();
22468
+ if (governanceCleanup && !governanceCleanup.ok) {
22469
+ logger.warn(`governance: standalone WebUI ${governanceCleanup.action} cleanup failed`, {
22470
+ message: governanceCleanup.message
22471
+ });
22472
+ }
21896
22473
  brainMonitor.stop();
21897
22474
  await agentServices.brainLedger?.stop().catch(() => {
21898
22475
  });