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