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