@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.
- package/dist/index.js +1331 -469
- package/dist/protocol/index.js +1 -0
- package/dist/protocol/registry.d.ts +1 -1
- package/dist/protocol/server-conversation.d.ts +1 -1
- package/dist/server/backend-services.d.ts +2 -0
- package/dist/server/codemap-cache.d.ts +1 -0
- package/dist/server/connection-lifecycle.d.ts +13 -0
- package/dist/server/connections-health-route.d.ts +7 -1
- package/dist/server/context-editor.d.ts +1 -2
- package/dist/server/entry.js +1043 -466
- package/dist/server/frontend-static-serve.d.ts +7 -0
- package/dist/server/governance-runtime.d.ts +30 -0
- package/dist/server/http-server.d.ts +10 -1
- package/dist/server/mcp-handlers.d.ts +3 -1
- package/dist/server/memory-handlers.d.ts +3 -3
- package/dist/server/pending-confirms.d.ts +9 -0
- package/dist/server/sdd-board-ws-handler.d.ts +1 -0
- package/dist/server/sdd-wizard-ws-handler.d.ts +2 -0
- package/dist/server/specs-ws-handler.d.ts +1 -0
- package/dist/server/types.d.ts +3 -1
- package/dist/server/ws-auth.d.ts +24 -0
- package/dist/server/ws-payload-validation.d.ts +18 -0
- package/dist/server/ws-utils.d.ts +12 -1
- package/package.json +13 -12
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -7
- package/dist/protocol/client-conversation.d.ts.map +0 -1
- package/dist/protocol/client-integrations.d.ts.map +0 -1
- package/dist/protocol/client-operations.d.ts.map +0 -1
- package/dist/protocol/client-workspace.d.ts.map +0 -1
- package/dist/protocol/connection-fsm.d.ts.map +0 -1
- package/dist/protocol/decoder.d.ts.map +0 -1
- package/dist/protocol/index.d.ts.map +0 -1
- package/dist/protocol/index.js.map +0 -7
- package/dist/protocol/projections.d.ts.map +0 -1
- package/dist/protocol/registry.d.ts.map +0 -1
- package/dist/protocol/replay-payload.d.ts.map +0 -1
- package/dist/protocol/server-conversation.d.ts.map +0 -1
- package/dist/protocol/server-integrations.d.ts.map +0 -1
- package/dist/protocol/server-operations.d.ts.map +0 -1
- package/dist/protocol/server-workspace.d.ts.map +0 -1
- package/dist/protocol/types.d.ts.map +0 -1
- package/dist/protocol/version.d.ts.map +0 -1
- package/dist/server/agent-roster-handlers.d.ts.map +0 -1
- package/dist/server/agent-roster-routes.d.ts.map +0 -1
- package/dist/server/autonomy-routes.d.ts.map +0 -1
- package/dist/server/backend-services.d.ts.map +0 -1
- package/dist/server/boot.d.ts.map +0 -1
- package/dist/server/brain-handlers.d.ts.map +0 -1
- package/dist/server/brain-routes.d.ts.map +0 -1
- package/dist/server/chronicle-routes.d.ts.map +0 -1
- package/dist/server/client-presence.d.ts.map +0 -1
- package/dist/server/client-transport-routes.d.ts.map +0 -1
- package/dist/server/codebase-index-server-control.d.ts.map +0 -1
- package/dist/server/codebase-indexing.d.ts.map +0 -1
- package/dist/server/codemap-cache.d.ts.map +0 -1
- package/dist/server/codemap-handlers.d.ts.map +0 -1
- package/dist/server/codemap-telemetry.d.ts.map +0 -1
- package/dist/server/collaboration-ws-handler.d.ts.map +0 -1
- package/dist/server/completion-handlers.d.ts.map +0 -1
- package/dist/server/completion-routes.d.ts.map +0 -1
- package/dist/server/config-doctor.d.ts.map +0 -1
- package/dist/server/connection-handler.d.ts.map +0 -1
- package/dist/server/connection-lifecycle.d.ts.map +0 -1
- package/dist/server/connections-health-route.d.ts.map +0 -1
- package/dist/server/content-routes.d.ts.map +0 -1
- package/dist/server/context-editor.d.ts.map +0 -1
- package/dist/server/context-meta.d.ts.map +0 -1
- package/dist/server/conversation-operations.d.ts.map +0 -1
- package/dist/server/conversation-routes.d.ts.map +0 -1
- package/dist/server/custom-context-modes.d.ts.map +0 -1
- package/dist/server/deadcode-handlers.d.ts.map +0 -1
- package/dist/server/design-handlers.d.ts.map +0 -1
- package/dist/server/discover-mailbox-bridge.d.ts.map +0 -1
- package/dist/server/embedded-host-adapters.d.ts.map +0 -1
- package/dist/server/embedded-lifecycle.d.ts.map +0 -1
- package/dist/server/embedded-message-router.d.ts.map +0 -1
- package/dist/server/entry.d.ts.map +0 -1
- package/dist/server/entry.js.map +0 -7
- package/dist/server/eternal-iteration-broadcast.d.ts.map +0 -1
- package/dist/server/file-handlers.d.ts.map +0 -1
- package/dist/server/file-picker.d.ts.map +0 -1
- package/dist/server/frontend-static-serve.d.ts.map +0 -1
- package/dist/server/git-handlers.d.ts.map +0 -1
- package/dist/server/git-process.d.ts.map +0 -1
- package/dist/server/goal-handlers.d.ts.map +0 -1
- package/dist/server/goal-routes.d.ts.map +0 -1
- package/dist/server/goal-snapshot-routes.d.ts.map +0 -1
- package/dist/server/goal-ws-handler.d.ts.map +0 -1
- package/dist/server/handlers/index.d.ts.map +0 -1
- package/dist/server/handlers/worklist-handlers.d.ts.map +0 -1
- package/dist/server/handlers.js.map +0 -7
- package/dist/server/host-routes.d.ts.map +0 -1
- package/dist/server/http-server/analytics-handler.d.ts.map +0 -1
- package/dist/server/http-server/api-handlers.d.ts.map +0 -1
- package/dist/server/http-server.d.ts.map +0 -1
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/instance-registry.d.ts.map +0 -1
- package/dist/server/introspection-routes.d.ts.map +0 -1
- package/dist/server/kanban-board-watcher.d.ts.map +0 -1
- package/dist/server/kanban-daemon-subscriber.d.ts.map +0 -1
- package/dist/server/kanban-decomposition-routes.d.ts.map +0 -1
- package/dist/server/kanban-dispatch.d.ts.map +0 -1
- package/dist/server/kanban-host-routes.d.ts.map +0 -1
- package/dist/server/kanban-route-helpers.d.ts.map +0 -1
- package/dist/server/kanban-route-pagination.d.ts.map +0 -1
- package/dist/server/kanban-route-protocol.d.ts.map +0 -1
- package/dist/server/kanban-routes.d.ts.map +0 -1
- package/dist/server/kanban-run-mirror.d.ts.map +0 -1
- package/dist/server/kanban-supervisor.d.ts.map +0 -1
- package/dist/server/kanban-task-routes.d.ts.map +0 -1
- package/dist/server/lifecycle.d.ts.map +0 -1
- package/dist/server/mailbox-handlers.d.ts.map +0 -1
- package/dist/server/mailbox-routes.d.ts.map +0 -1
- package/dist/server/mcp-handlers.d.ts.map +0 -1
- package/dist/server/mcp-routes.d.ts.map +0 -1
- package/dist/server/memory-diagnostics.d.ts.map +0 -1
- package/dist/server/memory-handlers.d.ts.map +0 -1
- package/dist/server/memory-routes.d.ts.map +0 -1
- package/dist/server/message-dispatcher.d.ts.map +0 -1
- package/dist/server/mode-handlers.d.ts.map +0 -1
- package/dist/server/mode-operations.d.ts.map +0 -1
- package/dist/server/mode-routes.d.ts.map +0 -1
- package/dist/server/model-auto-discovery.d.ts.map +0 -1
- package/dist/server/model-catalog-search.d.ts.map +0 -1
- package/dist/server/model-catalog.d.ts.map +0 -1
- package/dist/server/model-operations.d.ts.map +0 -1
- package/dist/server/network-info.d.ts.map +0 -1
- package/dist/server/open-browser.d.ts.map +0 -1
- package/dist/server/path-containment.d.ts.map +0 -1
- package/dist/server/pending-confirms.d.ts.map +0 -1
- package/dist/server/port-utils.d.ts.map +0 -1
- package/dist/server/pre-context-services.d.ts.map +0 -1
- package/dist/server/pref-helpers.d.ts.map +0 -1
- package/dist/server/prefs-handlers.d.ts.map +0 -1
- package/dist/server/prefs-routes.d.ts.map +0 -1
- package/dist/server/privileged-actions.d.ts.map +0 -1
- package/dist/server/process-handlers.d.ts.map +0 -1
- package/dist/server/process-routes.d.ts.map +0 -1
- package/dist/server/project-handlers.d.ts.map +0 -1
- package/dist/server/project-routes.d.ts.map +0 -1
- package/dist/server/projects-manifest.d.ts.map +0 -1
- package/dist/server/prompts-handlers.d.ts.map +0 -1
- package/dist/server/provider-config-io.d.ts.map +0 -1
- package/dist/server/provider-config-standalone.d.ts.map +0 -1
- package/dist/server/provider-handlers.d.ts.map +0 -1
- package/dist/server/provider-keys.d.ts.map +0 -1
- package/dist/server/provider-routes.d.ts.map +0 -1
- package/dist/server/provider-store.d.ts.map +0 -1
- package/dist/server/route-family-dispatcher.d.ts.map +0 -1
- package/dist/server/routes.d.ts.map +0 -1
- package/dist/server/sdd-board-routes.d.ts.map +0 -1
- package/dist/server/sdd-board-ws-handler.d.ts.map +0 -1
- package/dist/server/sdd-wizard-routes.d.ts.map +0 -1
- package/dist/server/sdd-wizard-wiring.d.ts.map +0 -1
- package/dist/server/sdd-wizard-ws-handler.d.ts.map +0 -1
- package/dist/server/server-runtime.d.ts.map +0 -1
- package/dist/server/session-handlers.d.ts.map +0 -1
- package/dist/server/session-history.d.ts.map +0 -1
- package/dist/server/session-routes.d.ts.map +0 -1
- package/dist/server/setup-event-projection.d.ts.map +0 -1
- package/dist/server/setup-events-core-watchers.d.ts.map +0 -1
- package/dist/server/setup-events-fleet-broadcaster.d.ts.map +0 -1
- package/dist/server/setup-events-provider-handlers.d.ts.map +0 -1
- package/dist/server/setup-events-session-helpers.d.ts.map +0 -1
- package/dist/server/setup-events-status-watcher.d.ts.map +0 -1
- package/dist/server/setup-events-watcher.d.ts.map +0 -1
- package/dist/server/setup-events.d.ts.map +0 -1
- package/dist/server/setup-screen.d.ts.map +0 -1
- package/dist/server/shell-git-routes.d.ts.map +0 -1
- package/dist/server/shell-open.d.ts.map +0 -1
- package/dist/server/skills-handlers.d.ts.map +0 -1
- package/dist/server/specs-routes.d.ts.map +0 -1
- package/dist/server/specs-ws-handler.d.ts.map +0 -1
- package/dist/server/standalone-session-identity.d.ts.map +0 -1
- package/dist/server/start-webui.d.ts.map +0 -1
- package/dist/server/stream-coalescer.d.ts.map +0 -1
- package/dist/server/techstack-handlers.d.ts.map +0 -1
- package/dist/server/terminal-ws-handler.d.ts.map +0 -1
- package/dist/server/token-estimator.d.ts.map +0 -1
- package/dist/server/types.d.ts.map +0 -1
- package/dist/server/usage-cost.d.ts.map +0 -1
- package/dist/server/worklist-routes.d.ts.map +0 -1
- package/dist/server/worktree-routes.d.ts.map +0 -1
- package/dist/server/worktree-ws-handler.d.ts.map +0 -1
- package/dist/server/ws-auth.d.ts.map +0 -1
- package/dist/server/ws-payload-preferences.d.ts.map +0 -1
- package/dist/server/ws-payload-validation.d.ts.map +0 -1
- package/dist/server/ws-utils.d.ts.map +0 -1
- package/dist/server/zip.d.ts.map +0 -1
package/dist/server/entry.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
297
|
+
if (!isRecord(value) || !Object.values(value).every(
|
|
250
298
|
(v) => Array.isArray(v) && v.every((item) => typeof item === "string")
|
|
251
|
-
)
|
|
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
|
|
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
|
-
|
|
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))
|
|
1393
|
+
if (cache.has(key)) deleteCachedCodemapBody(key);
|
|
1394
|
+
if (body.length > MAX_CACHE_BODY_CHARS) return;
|
|
1270
1395
|
cache.set(key, { version, body });
|
|
1271
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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")
|
|
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(
|
|
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(
|
|
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 (
|
|
4388
|
-
error(
|
|
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
|
-
|
|
4391
|
-
|
|
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:
|
|
4699
|
+
type: "url",
|
|
4397
4700
|
...typeof mediaType === "string" ? { media_type: mediaType } : {},
|
|
4398
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|
|
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(
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
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 {
|
|
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:
|
|
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 ?
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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'"
|
|
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"
|
|
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
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
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
|
|
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
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
}
|
|
8888
|
-
|
|
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
|
|
8931
|
-
const completedTask =
|
|
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 (
|
|
9331
|
+
if (completedBoard) {
|
|
8937
9332
|
ctx.broadcast?.({
|
|
8938
9333
|
type: "kanban.get",
|
|
8939
|
-
payload: { success: true, data: { board:
|
|
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
|
|
8951
|
-
const updated = await store.updateTaskAssignment(
|
|
9345
|
+
const started = await store.startKanbanDispatch({
|
|
8952
9346
|
boardId,
|
|
8953
|
-
task.id,
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
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 (
|
|
8988
|
-
ctx.broadcast?.({ type: "kanban.get", payload: { success: true, data: { board:
|
|
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.
|
|
9364
|
+
await store.failKanbanDispatch({
|
|
8994
9365
|
boardId,
|
|
8995
|
-
task.id,
|
|
8996
|
-
|
|
8997
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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"] ?? "
|
|
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.
|
|
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([
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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 =
|
|
16861
|
-
const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug :
|
|
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
|
|
17052
|
-
import * as
|
|
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
|
|
17558
|
+
import * as path21 from "node:path";
|
|
17056
17559
|
function statusProjectHashFromWatchFilename(projectsDir, filename) {
|
|
17057
17560
|
const raw = String(filename);
|
|
17058
|
-
const relative5 =
|
|
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 =
|
|
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
|
|
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 =
|
|
17153
|
-
const content = await
|
|
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
|
|
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
|
|
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/
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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-${
|
|
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
|
});
|