@wrongstack/webui-server 0.299.0 → 0.301.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 +854 -268
- package/dist/protocol/client-conversation.d.ts +1 -1
- package/dist/protocol/index.js +23 -1
- package/dist/protocol/projections.d.ts +8 -0
- package/dist/protocol/registry.d.ts +2 -2
- package/dist/protocol/server-conversation.d.ts +2 -2
- package/dist/protocol/version.d.ts +2 -0
- package/dist/server/connections-health-route.d.ts +10 -0
- package/dist/server/context-editor.d.ts +9 -0
- package/dist/server/conversation-routes.d.ts +1 -0
- package/dist/server/discover-mailbox-bridge.d.ts +5 -0
- package/dist/server/embedded-host-adapters.d.ts +1 -1
- package/dist/server/embedded-lifecycle.d.ts +23 -5
- package/dist/server/entry.js +1296 -262
- package/dist/server/fallback-choice.d.ts +19 -0
- package/dist/server/goal-ws-handler.d.ts +12 -0
- package/dist/server/http-server.d.ts +49 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/instance-registry.d.ts +21 -0
- package/dist/server/kanban-broadcast.d.ts +37 -0
- package/dist/server/lifecycle.d.ts +6 -0
- package/dist/server/mcp-handlers.d.ts +3 -2
- package/dist/server/pref-helpers.d.ts +1 -1
- package/dist/server/provider-handlers.d.ts +2 -2
- package/dist/server/provider-routes.d.ts +3 -1
- package/dist/server/ws-auth.d.ts +18 -0
- package/dist/server/ws-payload-validation.d.ts +7 -0
- package/package.json +11 -11
package/dist/server/entry.js
CHANGED
|
@@ -52,6 +52,7 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
|
52
52
|
"chime",
|
|
53
53
|
"confirmExit",
|
|
54
54
|
"nextPrediction",
|
|
55
|
+
"nextStepsTool",
|
|
55
56
|
"titleAnimation",
|
|
56
57
|
"enhanceEnabled",
|
|
57
58
|
"featureMcp",
|
|
@@ -77,6 +78,10 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
|
77
78
|
// Display-only toggles (purely visual, persisted in localStorage via Zustand).
|
|
78
79
|
"groupToolCalls",
|
|
79
80
|
"showThinkingLogs",
|
|
81
|
+
// v15: auto-collapse of the chat input under the history (opt-in display
|
|
82
|
+
// toggle, default off). Whitelisted so the key survives `prefs.update`
|
|
83
|
+
// round-trips without tripping the "unknown preference key" rejection.
|
|
84
|
+
"autoCollapseInput",
|
|
80
85
|
// v11 Display parity: inverse fsAccess flag.
|
|
81
86
|
"allowOutsideProjectRoot",
|
|
82
87
|
// v13 Display parity (TUI SettingsPicker fields 42 & 43): the read tool
|
|
@@ -171,6 +176,7 @@ var ENUM_PREF_KEYS = {
|
|
|
171
176
|
fsAccess: /* @__PURE__ */ new Set(["unrestricted", "project"]),
|
|
172
177
|
// Chimera autoFix + auto-review cascade threshold
|
|
173
178
|
chimeraAutoFix: /* @__PURE__ */ new Set(["off", "ask", "auto"]),
|
|
179
|
+
autoReviewModelSelection: /* @__PURE__ */ new Set(["round-robin", "random"]),
|
|
174
180
|
autoReviewCascadeOn: /* @__PURE__ */ new Set(["off", "critical", "high"]),
|
|
175
181
|
fleetChatVerbosity: /* @__PURE__ */ new Set(["off", "full"]),
|
|
176
182
|
showAgentSwarmPanel: /* @__PURE__ */ new Set(["bottom", "sidebar", "off"])
|
|
@@ -401,6 +407,51 @@ function validateModelSwitchPayload(payload) {
|
|
|
401
407
|
}
|
|
402
408
|
};
|
|
403
409
|
}
|
|
410
|
+
function validateModelFallbackChoicePayload(payload) {
|
|
411
|
+
if (!isRecord2(payload)) {
|
|
412
|
+
return {
|
|
413
|
+
ok: false,
|
|
414
|
+
message: "model.fallback_choice payload must be an object"
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const requestId = payload["requestId"];
|
|
418
|
+
if (typeof requestId !== "string" || requestId.trim().length === 0) {
|
|
419
|
+
return {
|
|
420
|
+
ok: false,
|
|
421
|
+
message: "model.fallback_choice payload.requestId must be a non-empty string"
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const providerId = payload["providerId"];
|
|
425
|
+
const model = payload["model"];
|
|
426
|
+
const autoSwitch = payload["autoSwitch"];
|
|
427
|
+
if (providerId !== void 0 && typeof providerId !== "string") {
|
|
428
|
+
return {
|
|
429
|
+
ok: false,
|
|
430
|
+
message: "model.fallback_choice payload.providerId must be a string when provided"
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (model !== void 0 && typeof model !== "string") {
|
|
434
|
+
return {
|
|
435
|
+
ok: false,
|
|
436
|
+
message: "model.fallback_choice payload.model must be a string when provided"
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
if (autoSwitch !== void 0 && typeof autoSwitch !== "boolean") {
|
|
440
|
+
return {
|
|
441
|
+
ok: false,
|
|
442
|
+
message: "model.fallback_choice payload.autoSwitch must be a boolean when provided"
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
ok: true,
|
|
447
|
+
value: {
|
|
448
|
+
requestId: requestId.trim(),
|
|
449
|
+
...typeof providerId === "string" ? { providerId } : {},
|
|
450
|
+
...typeof model === "string" ? { model } : {},
|
|
451
|
+
...typeof autoSwitch === "boolean" ? { autoSwitch } : {}
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
404
455
|
var AUTONOMY_VALUES2 = /* @__PURE__ */ new Set(["off", "suggest", "auto", "eternal", "eternal-parallel"]);
|
|
405
456
|
function validateMailboxMessagesPayload(payload) {
|
|
406
457
|
if (payload === void 0) return { ok: true, value: void 0 };
|
|
@@ -4181,6 +4232,9 @@ async function handleConversationRoute(ws, msg, handlers) {
|
|
|
4181
4232
|
case "user_message":
|
|
4182
4233
|
await handlers.userMessage(ws, msg);
|
|
4183
4234
|
return true;
|
|
4235
|
+
case "topic.advice":
|
|
4236
|
+
await handlers.topicAdvice(ws, msg);
|
|
4237
|
+
return true;
|
|
4184
4238
|
case "abort":
|
|
4185
4239
|
await handlers.abort(ws, msg);
|
|
4186
4240
|
return true;
|
|
@@ -4196,6 +4250,7 @@ async function handleConversationRoute(ws, msg, handlers) {
|
|
|
4196
4250
|
}
|
|
4197
4251
|
|
|
4198
4252
|
// src/server/conversation-operations.ts
|
|
4253
|
+
import { startFreshTopicContext, TopicShiftAdvisor } from "@wrongstack/core/execution";
|
|
4199
4254
|
import {
|
|
4200
4255
|
buildUserContentBlocks,
|
|
4201
4256
|
IncomingImageError,
|
|
@@ -4212,6 +4267,7 @@ function requestedSessionId(msg) {
|
|
|
4212
4267
|
return payload && typeof payload === "object" && typeof payload.sessionId === "string" ? payload.sessionId : void 0;
|
|
4213
4268
|
}
|
|
4214
4269
|
function createConversationOperations(ctx) {
|
|
4270
|
+
const topicShiftAdvisor = new TopicShiftAdvisor();
|
|
4215
4271
|
const sessionPayload2 = (payload) => {
|
|
4216
4272
|
const provided = payload["sessionId"];
|
|
4217
4273
|
const sessionId = typeof provided === "string" && provided.length > 0 ? provided : ctx.getSessionId();
|
|
@@ -4232,6 +4288,38 @@ function createConversationOperations(ctx) {
|
|
|
4232
4288
|
return false;
|
|
4233
4289
|
};
|
|
4234
4290
|
return {
|
|
4291
|
+
topicAdvice: async (ws, msg) => {
|
|
4292
|
+
if (!ensureCurrentSession(ws, msg, "topic.advice")) return;
|
|
4293
|
+
const payload = msg.payload ?? {};
|
|
4294
|
+
if (typeof payload.requestId !== "string" || typeof payload.prompt !== "string") {
|
|
4295
|
+
ctx.send(ws, {
|
|
4296
|
+
type: "topic.advice_result",
|
|
4297
|
+
payload: sessionPayload2({
|
|
4298
|
+
requestId: typeof payload.requestId === "string" ? payload.requestId : "",
|
|
4299
|
+
suggestNewContext: false,
|
|
4300
|
+
confidence: 0,
|
|
4301
|
+
reason: "Invalid topic advice request.",
|
|
4302
|
+
source: "local"
|
|
4303
|
+
})
|
|
4304
|
+
});
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4307
|
+
const agent = ctx.getAgent();
|
|
4308
|
+
const configuredMax = agent.ctx.meta["effectiveMaxContext"];
|
|
4309
|
+
const maxContext = typeof configuredMax === "number" ? configuredMax : agent.ctx.provider.capabilities.maxContext;
|
|
4310
|
+
const advice = await topicShiftAdvisor.advise({
|
|
4311
|
+
prompt: payload.prompt,
|
|
4312
|
+
messages: agent.ctx.messages,
|
|
4313
|
+
provider: agent.ctx.provider,
|
|
4314
|
+
model: agent.ctx.model,
|
|
4315
|
+
contextTokens: agent.ctx.lastRequestTokens,
|
|
4316
|
+
maxContext
|
|
4317
|
+
});
|
|
4318
|
+
ctx.send(ws, {
|
|
4319
|
+
type: "topic.advice_result",
|
|
4320
|
+
payload: sessionPayload2({ requestId: payload.requestId, ...advice })
|
|
4321
|
+
});
|
|
4322
|
+
},
|
|
4235
4323
|
userMessage: async (ws, msg) => {
|
|
4236
4324
|
if (!ensureCurrentSession(ws, msg, "user_message")) return;
|
|
4237
4325
|
const payload = msg.payload ?? {};
|
|
@@ -4249,6 +4337,7 @@ function createConversationOperations(ctx) {
|
|
|
4249
4337
|
const originSessionId = ctx.getSessionId();
|
|
4250
4338
|
try {
|
|
4251
4339
|
const agent = ctx.getAgent();
|
|
4340
|
+
if (payload.freshContext === true) await startFreshTopicContext(agent.ctx);
|
|
4252
4341
|
const content = typeof payload.content === "string" ? payload.content : "";
|
|
4253
4342
|
let input = content;
|
|
4254
4343
|
const imageBlocks = parseIncomingImages(payload.images, payload.imageBase64);
|
|
@@ -4331,13 +4420,10 @@ function createConversationOperations(ctx) {
|
|
|
4331
4420
|
|
|
4332
4421
|
// src/server/context-editor.ts
|
|
4333
4422
|
import { createHash } from "node:crypto";
|
|
4334
|
-
import net from "node:net";
|
|
4335
4423
|
import {
|
|
4336
4424
|
ALLOWED_IMAGE_MEDIA_TYPES,
|
|
4337
4425
|
base64DecodedBytes,
|
|
4338
4426
|
isAllowedImageMediaType,
|
|
4339
|
-
isPrivateIPv4,
|
|
4340
|
-
isPrivateIPv6,
|
|
4341
4427
|
isValidImageBase64,
|
|
4342
4428
|
MAX_INCOMING_IMAGE_BYTES,
|
|
4343
4429
|
repairToolUseAdjacency
|
|
@@ -4405,40 +4491,7 @@ var REVISION_PREFIX = "wrongstack-context-editor-v1\0";
|
|
|
4405
4491
|
var MAX_MESSAGE_COUNT_GROWTH = 10;
|
|
4406
4492
|
var MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
|
|
4407
4493
|
var MAX_STRING_LENGTH = 8 * 1024 * 1024;
|
|
4408
|
-
var
|
|
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
|
-
}
|
|
4494
|
+
var MAX_REMOVAL_COUNT = 4096;
|
|
4442
4495
|
function isRecord3(value) {
|
|
4443
4496
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4444
4497
|
}
|
|
@@ -4447,7 +4500,7 @@ function canonicalize(value) {
|
|
|
4447
4500
|
if (isRecord3(value)) {
|
|
4448
4501
|
const sorted = {};
|
|
4449
4502
|
for (const key of Object.keys(value).sort()) {
|
|
4450
|
-
if (key === "_estTokens") continue;
|
|
4503
|
+
if (key === "_estTokens" || key === "_toolErrorInfo") continue;
|
|
4451
4504
|
const item = value[key];
|
|
4452
4505
|
if (item === void 0) continue;
|
|
4453
4506
|
sorted[key] = canonicalize(item);
|
|
@@ -4478,6 +4531,12 @@ function isMessageRole(value) {
|
|
|
4478
4531
|
function isPlainJsonObject(value) {
|
|
4479
4532
|
return isRecord3(value);
|
|
4480
4533
|
}
|
|
4534
|
+
function splitsSurrogatePair(text, offset) {
|
|
4535
|
+
if (offset <= 0 || offset >= text.length) return false;
|
|
4536
|
+
const previous = text.charCodeAt(offset - 1);
|
|
4537
|
+
const next = text.charCodeAt(offset);
|
|
4538
|
+
return previous >= 55296 && previous <= 56319 && next >= 56320 && next <= 57343;
|
|
4539
|
+
}
|
|
4481
4540
|
function validateCacheControl(value, path30, errors) {
|
|
4482
4541
|
if (value === void 0) return void 0;
|
|
4483
4542
|
if (!isRecord3(value) || value["type"] !== "ephemeral") {
|
|
@@ -4688,19 +4747,13 @@ function validateBlock(value, path30, errors) {
|
|
|
4688
4747
|
);
|
|
4689
4748
|
return void 0;
|
|
4690
4749
|
}
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
source: {
|
|
4699
|
-
type: "url",
|
|
4700
|
-
...typeof mediaType === "string" ? { media_type: mediaType } : {},
|
|
4701
|
-
url
|
|
4702
|
-
}
|
|
4703
|
-
};
|
|
4750
|
+
error(
|
|
4751
|
+
errors,
|
|
4752
|
+
`${path30}/source/url`,
|
|
4753
|
+
"UNSAFE_IMAGE_URL",
|
|
4754
|
+
"URL image sources are not allowed in context editor proposals; use an ingested base64 image."
|
|
4755
|
+
);
|
|
4756
|
+
return void 0;
|
|
4704
4757
|
}
|
|
4705
4758
|
case "thinking": {
|
|
4706
4759
|
const thinking = value["thinking"];
|
|
@@ -4857,6 +4910,14 @@ function warningsForMessage(message, index) {
|
|
|
4857
4910
|
message: "This block contains provider replay metadata and should only be removed with the whole turn if no longer needed."
|
|
4858
4911
|
});
|
|
4859
4912
|
}
|
|
4913
|
+
if (block.type === "image" && block.source.type === "url") {
|
|
4914
|
+
warnings.push({
|
|
4915
|
+
path: `/messages/${index}/content/${blockIndex}/source/url`,
|
|
4916
|
+
code: "UNSAFE_IMAGE_URL",
|
|
4917
|
+
severity: "danger",
|
|
4918
|
+
message: "URL image sources cannot be retained in context editor proposals; remove the whole message before applying other edits."
|
|
4919
|
+
});
|
|
4920
|
+
}
|
|
4860
4921
|
if (block.type === "tool_result" && block.content.length > 2e4) {
|
|
4861
4922
|
warnings.push({
|
|
4862
4923
|
path: `/messages/${index}/content/${blockIndex}`,
|
|
@@ -4884,6 +4945,21 @@ function metricFor(ctx, messages, tools) {
|
|
|
4884
4945
|
fullRequestTokens: breakdown.total
|
|
4885
4946
|
};
|
|
4886
4947
|
}
|
|
4948
|
+
function isToolResultMessage(message) {
|
|
4949
|
+
return Boolean(
|
|
4950
|
+
message?.role === "user" && Array.isArray(message.content) && message.content.length > 0 && message.content.every((block) => block.type === "tool_result")
|
|
4951
|
+
);
|
|
4952
|
+
}
|
|
4953
|
+
function pairedAssistantIndices(messages, userIndex) {
|
|
4954
|
+
if (messages[userIndex]?.role !== "user" || isToolResultMessage(messages[userIndex])) return [];
|
|
4955
|
+
const paired = [];
|
|
4956
|
+
for (let index = userIndex + 1; index < messages.length; index += 1) {
|
|
4957
|
+
const message = messages[index];
|
|
4958
|
+
if (message?.role === "user" && !isToolResultMessage(message)) break;
|
|
4959
|
+
if (message?.role === "assistant") paired.push(index);
|
|
4960
|
+
}
|
|
4961
|
+
return paired;
|
|
4962
|
+
}
|
|
4887
4963
|
function buildContextEditorSnapshot(ctx, tools) {
|
|
4888
4964
|
const messages = ctx.messages.map(
|
|
4889
4965
|
(message) => ({
|
|
@@ -4913,11 +4989,165 @@ function buildContextEditorSnapshot(ctx, tools) {
|
|
|
4913
4989
|
tokens: messageTokens(message.content),
|
|
4914
4990
|
preview: breakdown.messages.breakdown[index]?.preview ?? "",
|
|
4915
4991
|
blockCount: Array.isArray(message.content) ? message.content.length : null,
|
|
4916
|
-
warnings: warningsForMessage(message, index)
|
|
4992
|
+
warnings: warningsForMessage(message, index),
|
|
4993
|
+
pairedAssistantIndices: pairedAssistantIndices(ctx.messages, index)
|
|
4917
4994
|
})),
|
|
4918
4995
|
diagnostics: toolDiagnostics(ctx.messages)
|
|
4919
4996
|
};
|
|
4920
4997
|
}
|
|
4998
|
+
function validateRemovalPlan(value, originalMessages, proposedMessages) {
|
|
4999
|
+
const errors = [];
|
|
5000
|
+
if (value === void 0) {
|
|
5001
|
+
error(
|
|
5002
|
+
errors,
|
|
5003
|
+
"/removals",
|
|
5004
|
+
"REMOVAL_PLAN_REQUIRED",
|
|
5005
|
+
"A removal plan is required for every context editor proposal."
|
|
5006
|
+
);
|
|
5007
|
+
return { errors };
|
|
5008
|
+
}
|
|
5009
|
+
if (!Array.isArray(value)) {
|
|
5010
|
+
error(errors, "/removals", "INVALID_REMOVALS", "removals must be an array.");
|
|
5011
|
+
return { errors };
|
|
5012
|
+
}
|
|
5013
|
+
if (value.length > MAX_REMOVAL_COUNT) {
|
|
5014
|
+
error(
|
|
5015
|
+
errors,
|
|
5016
|
+
"/removals",
|
|
5017
|
+
"TOO_MANY_REMOVALS",
|
|
5018
|
+
`removals must contain at most ${MAX_REMOVAL_COUNT} entries.`
|
|
5019
|
+
);
|
|
5020
|
+
return { errors };
|
|
5021
|
+
}
|
|
5022
|
+
const wholeMessages = /* @__PURE__ */ new Set();
|
|
5023
|
+
const touchedUsers = /* @__PURE__ */ new Set();
|
|
5024
|
+
const ranges = [];
|
|
5025
|
+
for (const [removalIndex, raw] of value.entries()) {
|
|
5026
|
+
const path30 = `/removals/${removalIndex}`;
|
|
5027
|
+
if (!isRecord3(raw) || !Number.isInteger(raw["messageIndex"])) {
|
|
5028
|
+
error(errors, path30, "INVALID_REMOVAL", "Removal must include an integer messageIndex.");
|
|
5029
|
+
continue;
|
|
5030
|
+
}
|
|
5031
|
+
const messageIndex = raw["messageIndex"];
|
|
5032
|
+
const original = originalMessages[messageIndex];
|
|
5033
|
+
if (!original) {
|
|
5034
|
+
error(errors, `${path30}/messageIndex`, "INVALID_MESSAGE_INDEX", "Removal messageIndex is out of range.");
|
|
5035
|
+
continue;
|
|
5036
|
+
}
|
|
5037
|
+
const start = raw["start"];
|
|
5038
|
+
const end = raw["end"];
|
|
5039
|
+
const blockIndex = raw["blockIndex"];
|
|
5040
|
+
if (start === void 0 && end === void 0 && blockIndex === void 0) {
|
|
5041
|
+
wholeMessages.add(messageIndex);
|
|
5042
|
+
if (original.role === "user") touchedUsers.add(messageIndex);
|
|
5043
|
+
continue;
|
|
5044
|
+
}
|
|
5045
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start) {
|
|
5046
|
+
error(errors, path30, "INVALID_RANGE", "Range removal requires integer start/end with 0 <= start < end.");
|
|
5047
|
+
continue;
|
|
5048
|
+
}
|
|
5049
|
+
let text;
|
|
5050
|
+
if (blockIndex === void 0 && typeof original.content === "string") text = original.content;
|
|
5051
|
+
if (Number.isInteger(blockIndex) && Array.isArray(original.content)) {
|
|
5052
|
+
const block = original.content[blockIndex];
|
|
5053
|
+
if (block?.type === "text") text = block.text;
|
|
5054
|
+
}
|
|
5055
|
+
if (text === void 0 || end > text.length) {
|
|
5056
|
+
error(errors, path30, "INVALID_RANGE_TARGET", "Range must target existing string or text-block content.");
|
|
5057
|
+
continue;
|
|
5058
|
+
}
|
|
5059
|
+
if (splitsSurrogatePair(text, start) || splitsSurrogatePair(text, end)) {
|
|
5060
|
+
error(
|
|
5061
|
+
errors,
|
|
5062
|
+
path30,
|
|
5063
|
+
"INVALID_UNICODE_RANGE",
|
|
5064
|
+
"Range boundaries must not split a Unicode surrogate pair."
|
|
5065
|
+
);
|
|
5066
|
+
continue;
|
|
5067
|
+
}
|
|
5068
|
+
ranges.push({
|
|
5069
|
+
messageIndex,
|
|
5070
|
+
...blockIndex === void 0 ? {} : { blockIndex },
|
|
5071
|
+
start,
|
|
5072
|
+
end
|
|
5073
|
+
});
|
|
5074
|
+
if (original.role === "user") touchedUsers.add(messageIndex);
|
|
5075
|
+
}
|
|
5076
|
+
const rangesByTarget = /* @__PURE__ */ new Map();
|
|
5077
|
+
for (const range of ranges) {
|
|
5078
|
+
const key = `${range.messageIndex}:${range.blockIndex ?? "string"}`;
|
|
5079
|
+
const targetRanges = rangesByTarget.get(key) ?? [];
|
|
5080
|
+
targetRanges.push(range);
|
|
5081
|
+
rangesByTarget.set(key, targetRanges);
|
|
5082
|
+
}
|
|
5083
|
+
for (const targetRanges of rangesByTarget.values()) {
|
|
5084
|
+
targetRanges.sort((left, right) => (left.start ?? 0) - (right.start ?? 0));
|
|
5085
|
+
for (let index = 1; index < targetRanges.length; index += 1) {
|
|
5086
|
+
const previous = targetRanges[index - 1];
|
|
5087
|
+
const current2 = targetRanges[index];
|
|
5088
|
+
if (previous?.end !== void 0 && current2?.start !== void 0 && current2.start < previous.end) {
|
|
5089
|
+
error(
|
|
5090
|
+
errors,
|
|
5091
|
+
"/removals",
|
|
5092
|
+
"OVERLAPPING_RANGES",
|
|
5093
|
+
"Removal ranges targeting the same text must not overlap."
|
|
5094
|
+
);
|
|
5095
|
+
break;
|
|
5096
|
+
}
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
for (const userIndex of touchedUsers) {
|
|
5100
|
+
for (const assistantIndex of pairedAssistantIndices(originalMessages, userIndex)) {
|
|
5101
|
+
if (wholeMessages.has(assistantIndex)) continue;
|
|
5102
|
+
error(
|
|
5103
|
+
errors,
|
|
5104
|
+
"/removals",
|
|
5105
|
+
"MISSING_ASSISTANT_PAIR",
|
|
5106
|
+
`Editing user message ${userIndex} must also remove assistant message ${assistantIndex}.`
|
|
5107
|
+
);
|
|
5108
|
+
}
|
|
5109
|
+
}
|
|
5110
|
+
const expectedMessages = structuredClone(originalMessages);
|
|
5111
|
+
for (const targetRanges of rangesByTarget.values()) {
|
|
5112
|
+
const first = targetRanges[0];
|
|
5113
|
+
if (!first) continue;
|
|
5114
|
+
const message = expectedMessages[first.messageIndex];
|
|
5115
|
+
if (!message) continue;
|
|
5116
|
+
let text;
|
|
5117
|
+
if (first.blockIndex === void 0 && typeof message.content === "string") {
|
|
5118
|
+
text = message.content;
|
|
5119
|
+
} else if (first.blockIndex !== void 0 && Array.isArray(message.content)) {
|
|
5120
|
+
const block = message.content[first.blockIndex];
|
|
5121
|
+
if (block?.type === "text") text = block.text;
|
|
5122
|
+
}
|
|
5123
|
+
if (text === void 0) continue;
|
|
5124
|
+
const pieces = [];
|
|
5125
|
+
let cursor = 0;
|
|
5126
|
+
for (const range of targetRanges) {
|
|
5127
|
+
if (range.start === void 0 || range.end === void 0) continue;
|
|
5128
|
+
pieces.push(text.slice(cursor, range.start));
|
|
5129
|
+
cursor = range.end;
|
|
5130
|
+
}
|
|
5131
|
+
pieces.push(text.slice(cursor));
|
|
5132
|
+
const nextText = pieces.join("");
|
|
5133
|
+
if (first.blockIndex === void 0 && typeof message.content === "string") {
|
|
5134
|
+
message.content = nextText;
|
|
5135
|
+
} else if (first.blockIndex !== void 0 && Array.isArray(message.content)) {
|
|
5136
|
+
const block = message.content[first.blockIndex];
|
|
5137
|
+
if (block?.type === "text") block.text = nextText;
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
const expectedProposal = expectedMessages.filter((_, index) => !wholeMessages.has(index));
|
|
5141
|
+
if (JSON.stringify(canonicalize(expectedProposal)) !== JSON.stringify(canonicalize(proposedMessages))) {
|
|
5142
|
+
error(
|
|
5143
|
+
errors,
|
|
5144
|
+
"/messages",
|
|
5145
|
+
"REMOVAL_PLAN_MISMATCH",
|
|
5146
|
+
"Submitted messages do not exactly match the declared removal plan."
|
|
5147
|
+
);
|
|
5148
|
+
}
|
|
5149
|
+
return errors.length > 0 ? { errors } : { errors, messages: expectedProposal };
|
|
5150
|
+
}
|
|
4921
5151
|
function validateContextEditorProposal(input) {
|
|
4922
5152
|
const currentRevision = contextEditorRevision(input.ctx.messages);
|
|
4923
5153
|
const before = metricFor(input.ctx, input.ctx.messages, input.tools);
|
|
@@ -4969,7 +5199,23 @@ function validateContextEditorProposal(input) {
|
|
|
4969
5199
|
repair: emptyRepair
|
|
4970
5200
|
};
|
|
4971
5201
|
}
|
|
4972
|
-
const
|
|
5202
|
+
const removalPlan = validateRemovalPlan(
|
|
5203
|
+
input.removals,
|
|
5204
|
+
input.ctx.messages,
|
|
5205
|
+
parsed.messages
|
|
5206
|
+
);
|
|
5207
|
+
if (removalPlan.errors.length > 0 || !removalPlan.messages) {
|
|
5208
|
+
return {
|
|
5209
|
+
ok: false,
|
|
5210
|
+
baseRevision: input.baseRevision,
|
|
5211
|
+
currentRevision,
|
|
5212
|
+
before,
|
|
5213
|
+
validationErrors: removalPlan.errors,
|
|
5214
|
+
warnings: [],
|
|
5215
|
+
repair: emptyRepair
|
|
5216
|
+
};
|
|
5217
|
+
}
|
|
5218
|
+
const repaired = repairToolUseAdjacency(removalPlan.messages);
|
|
4973
5219
|
const repair = {
|
|
4974
5220
|
changed: repaired.report.changed,
|
|
4975
5221
|
removedToolUses: repaired.report.removedToolUses,
|
|
@@ -5219,6 +5465,40 @@ import {
|
|
|
5219
5465
|
getKanbanServerConnection,
|
|
5220
5466
|
isKanbanServerAvailable
|
|
5221
5467
|
} from "@wrongstack/kanban";
|
|
5468
|
+
import * as net from "node:net";
|
|
5469
|
+
|
|
5470
|
+
// src/server/privileged-actions.ts
|
|
5471
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
5472
|
+
import {
|
|
5473
|
+
isTrustDecisionAllowed
|
|
5474
|
+
} from "@wrongstack/core/security";
|
|
5475
|
+
async function authorizeWebUIAction(boundary, action, logger) {
|
|
5476
|
+
const request = {
|
|
5477
|
+
version: 1,
|
|
5478
|
+
requestId: randomUUID2(),
|
|
5479
|
+
actor: {
|
|
5480
|
+
kind: "remote-client",
|
|
5481
|
+
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
5482
|
+
},
|
|
5483
|
+
surface: "webui",
|
|
5484
|
+
capability: action.capability,
|
|
5485
|
+
subject: action.subject,
|
|
5486
|
+
risk: action.risk,
|
|
5487
|
+
scope: {
|
|
5488
|
+
...action.cwd ? { cwd: action.cwd } : {},
|
|
5489
|
+
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
5490
|
+
},
|
|
5491
|
+
authContext: { method: "session" },
|
|
5492
|
+
...action.metadata ? { metadata: action.metadata } : {}
|
|
5493
|
+
};
|
|
5494
|
+
const decision = await boundary.evaluate(request);
|
|
5495
|
+
logger?.debug?.(
|
|
5496
|
+
`[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
|
|
5497
|
+
);
|
|
5498
|
+
return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
|
|
5499
|
+
}
|
|
5500
|
+
|
|
5501
|
+
// src/server/connections-health-route.ts
|
|
5222
5502
|
import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
|
|
5223
5503
|
import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
|
|
5224
5504
|
import {
|
|
@@ -5558,41 +5838,579 @@ async function mailboxHealth(projectRoot) {
|
|
|
5558
5838
|
connection.close();
|
|
5559
5839
|
}
|
|
5560
5840
|
}
|
|
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";
|
|
5841
|
+
async function governanceHealth(projectRoot) {
|
|
5842
|
+
const startedAt = Date.now();
|
|
5843
|
+
const result = await readGovernanceDaemonOperatorStatus(projectRoot);
|
|
5844
|
+
if (!result.available) {
|
|
5845
|
+
const missing = result.code === "broker_missing";
|
|
5846
|
+
return {
|
|
5847
|
+
id: "governance",
|
|
5848
|
+
label: "Governance control plane",
|
|
5849
|
+
status: missing ? "offline" : "error",
|
|
5850
|
+
required: false,
|
|
5851
|
+
mode: missing ? "compatibility-default-off" : "project-daemon",
|
|
5852
|
+
detail: missing ? "Not active for this project. Existing agent and model execution remains unchanged." : `Read-only governance status is unavailable: ${result.message}`,
|
|
5853
|
+
latencyMs: Date.now() - startedAt,
|
|
5854
|
+
control: "none",
|
|
5855
|
+
...missing ? {} : { lastError: result.message }
|
|
5856
|
+
};
|
|
5857
|
+
}
|
|
5858
|
+
const { status } = result;
|
|
5859
|
+
return {
|
|
5860
|
+
id: "governance",
|
|
5861
|
+
label: "Governance control plane",
|
|
5862
|
+
status: status.signal.level === "healthy" ? "healthy" : "degraded",
|
|
5863
|
+
required: false,
|
|
5864
|
+
mode: "project-daemon-advisory",
|
|
5865
|
+
detail: `${status.signal.message} Execution continues; no automatic task or model stop.`,
|
|
5866
|
+
ownerPid: status.pid,
|
|
5867
|
+
uptimeMs: Math.max(0, Date.now() - Date.parse(status.startedAt)),
|
|
5868
|
+
latencyMs: Date.now() - startedAt,
|
|
5869
|
+
control: "none",
|
|
5870
|
+
advisory: {
|
|
5871
|
+
code: status.signal.code,
|
|
5872
|
+
operatorAction: status.signal.operatorAction,
|
|
5873
|
+
executionDisposition: status.signal.executionDisposition
|
|
5874
|
+
}
|
|
5875
|
+
};
|
|
5876
|
+
}
|
|
5877
|
+
async function handleConnectionsServiceAction(ws, message, context) {
|
|
5878
|
+
if (message.type !== "connections.service_action") return false;
|
|
5879
|
+
const payload = message.payload;
|
|
5880
|
+
const serviceId = payload?.serviceId;
|
|
5881
|
+
const rawAction = payload?.action ?? "shutdown";
|
|
5882
|
+
if (!serviceId) {
|
|
5883
|
+
context.send(ws, {
|
|
5884
|
+
type: "connections.service_action_result",
|
|
5885
|
+
payload: {
|
|
5886
|
+
serviceId: null,
|
|
5887
|
+
action: rawAction,
|
|
5888
|
+
success: false,
|
|
5889
|
+
message: "Missing serviceId in payload"
|
|
5890
|
+
}
|
|
5891
|
+
});
|
|
5892
|
+
return true;
|
|
5893
|
+
}
|
|
5894
|
+
if (rawAction !== "shutdown" && rawAction !== "restart") {
|
|
5895
|
+
context.send(ws, {
|
|
5896
|
+
type: "connections.service_action_result",
|
|
5897
|
+
payload: {
|
|
5898
|
+
serviceId,
|
|
5899
|
+
action: rawAction,
|
|
5900
|
+
success: false,
|
|
5901
|
+
message: `Unsupported action "${rawAction}" \u2014 only "shutdown" and "restart" are supported`
|
|
5902
|
+
}
|
|
5903
|
+
});
|
|
5904
|
+
return true;
|
|
5905
|
+
}
|
|
5906
|
+
const action = rawAction;
|
|
5907
|
+
if (!context.trustBoundary) {
|
|
5908
|
+
context.send(ws, {
|
|
5909
|
+
type: "connections.service_action_result",
|
|
5910
|
+
payload: {
|
|
5911
|
+
serviceId,
|
|
5912
|
+
action,
|
|
5913
|
+
success: false,
|
|
5914
|
+
message: "Service control is unavailable: no policy authority is configured."
|
|
5915
|
+
}
|
|
5916
|
+
});
|
|
5917
|
+
return true;
|
|
5918
|
+
}
|
|
5919
|
+
const projectRootForAuth = context.getProjectRoot();
|
|
5920
|
+
const authorization = await authorizeWebUIAction(
|
|
5921
|
+
context.trustBoundary,
|
|
5922
|
+
{
|
|
5923
|
+
capability: `connections.service.${action}`,
|
|
5924
|
+
subject: { kind: "process", id: `${serviceId}@${projectRootForAuth}` },
|
|
5925
|
+
risk: "elevated",
|
|
5926
|
+
cwd: projectRootForAuth,
|
|
5927
|
+
metadata: { transport: "websocket", serviceId, action }
|
|
5928
|
+
},
|
|
5929
|
+
context.logger
|
|
5930
|
+
);
|
|
5931
|
+
if (!authorization.allowed) {
|
|
5932
|
+
context.send(ws, {
|
|
5933
|
+
type: "connections.service_action_result",
|
|
5934
|
+
payload: {
|
|
5935
|
+
serviceId,
|
|
5936
|
+
action,
|
|
5937
|
+
success: false,
|
|
5938
|
+
message: authorization.reason ?? "Refused by policy."
|
|
5939
|
+
}
|
|
5940
|
+
});
|
|
5941
|
+
return true;
|
|
5942
|
+
}
|
|
5943
|
+
if (serviceId === "webui") {
|
|
5944
|
+
context.send(ws, {
|
|
5945
|
+
type: "connections.service_action_result",
|
|
5946
|
+
payload: {
|
|
5947
|
+
serviceId: "webui",
|
|
5948
|
+
action,
|
|
5949
|
+
success: false,
|
|
5950
|
+
message: action === "restart" ? "Cannot restart the WebUI transport itself" : "Cannot shut down the WebUI transport itself"
|
|
5951
|
+
}
|
|
5952
|
+
});
|
|
5953
|
+
return true;
|
|
5954
|
+
}
|
|
5955
|
+
try {
|
|
5956
|
+
const result = await executeServiceAction(
|
|
5957
|
+
serviceId,
|
|
5958
|
+
action,
|
|
5959
|
+
context.getProjectRoot(),
|
|
5960
|
+
context.getIndexDir()
|
|
5961
|
+
);
|
|
5962
|
+
context.send(ws, {
|
|
5963
|
+
type: "connections.service_action_result",
|
|
5964
|
+
payload: result
|
|
5965
|
+
});
|
|
5966
|
+
} catch (error2) {
|
|
5967
|
+
context.send(ws, {
|
|
5968
|
+
type: "connections.service_action_result",
|
|
5969
|
+
payload: {
|
|
5970
|
+
serviceId,
|
|
5971
|
+
action,
|
|
5972
|
+
success: false,
|
|
5973
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
5974
|
+
}
|
|
5975
|
+
});
|
|
5976
|
+
}
|
|
5977
|
+
return true;
|
|
5978
|
+
}
|
|
5979
|
+
async function executeServiceAction(serviceId, action, projectRoot, indexDir) {
|
|
5980
|
+
switch (serviceId) {
|
|
5981
|
+
case "kanban":
|
|
5982
|
+
return killKanbanServer(projectRoot, action);
|
|
5983
|
+
case "sage":
|
|
5984
|
+
return killSageServer(projectRoot, action);
|
|
5985
|
+
case "chronicle":
|
|
5986
|
+
return killChronicleServer(projectRoot, action);
|
|
5987
|
+
case "codebase-index":
|
|
5988
|
+
return killCodebaseIndexServer(projectRoot, indexDir, action);
|
|
5989
|
+
case "mailbox":
|
|
5990
|
+
return killMailboxServer(projectRoot, action);
|
|
5991
|
+
case "governance":
|
|
5992
|
+
return {
|
|
5993
|
+
serviceId: "governance",
|
|
5994
|
+
action,
|
|
5995
|
+
success: false,
|
|
5996
|
+
message: "Governance health is read-only; daemon shutdown requires a separate admin control capability."
|
|
5997
|
+
};
|
|
5998
|
+
default:
|
|
5999
|
+
return {
|
|
6000
|
+
serviceId,
|
|
6001
|
+
action,
|
|
6002
|
+
success: false,
|
|
6003
|
+
message: `Unknown service: ${serviceId}`
|
|
6004
|
+
};
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
6007
|
+
async function killKanbanServer(projectRoot, action) {
|
|
6008
|
+
if (process.env["WRONGSTACK_KANBAN_SERVER"] === "0") {
|
|
6009
|
+
return {
|
|
6010
|
+
serviceId: "kanban",
|
|
6011
|
+
action,
|
|
6012
|
+
success: false,
|
|
6013
|
+
message: "Kanban IPC daemon is disabled via WRONGSTACK_KANBAN_SERVER=0"
|
|
6014
|
+
};
|
|
6015
|
+
}
|
|
6016
|
+
let connection;
|
|
6017
|
+
try {
|
|
6018
|
+
connection = await getKanbanServerConnection(projectRoot);
|
|
6019
|
+
} catch (error2) {
|
|
6020
|
+
return {
|
|
6021
|
+
serviceId: "kanban",
|
|
6022
|
+
action,
|
|
6023
|
+
success: false,
|
|
6024
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
6025
|
+
};
|
|
6026
|
+
}
|
|
6027
|
+
if (!connection) {
|
|
6028
|
+
return {
|
|
6029
|
+
serviceId: "kanban",
|
|
6030
|
+
action,
|
|
6031
|
+
success: false,
|
|
6032
|
+
message: "Kanban IPC daemon is not running"
|
|
6033
|
+
};
|
|
6034
|
+
}
|
|
6035
|
+
try {
|
|
6036
|
+
const result = await connection.request("shutdown", {
|
|
6037
|
+
reason: `WebUI request: ${action}`
|
|
6038
|
+
});
|
|
6039
|
+
if (!result.stopping) {
|
|
6040
|
+
return {
|
|
6041
|
+
serviceId: "kanban",
|
|
6042
|
+
action,
|
|
6043
|
+
success: false,
|
|
6044
|
+
message: "Kanban IPC daemon shutdown failed (not confirmed)"
|
|
6045
|
+
};
|
|
6046
|
+
}
|
|
6047
|
+
if (action === "restart") {
|
|
6048
|
+
closeKanbanServerConnections();
|
|
6049
|
+
const restartResult = await restartKanbanServer(projectRoot);
|
|
6050
|
+
return restartResult;
|
|
6051
|
+
}
|
|
6052
|
+
return {
|
|
6053
|
+
serviceId: "kanban",
|
|
6054
|
+
action,
|
|
6055
|
+
success: true,
|
|
6056
|
+
message: "Kanban IPC daemon shutdown requested"
|
|
6057
|
+
};
|
|
6058
|
+
} catch (error2) {
|
|
6059
|
+
return {
|
|
6060
|
+
serviceId: "kanban",
|
|
6061
|
+
action,
|
|
6062
|
+
success: false,
|
|
6063
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
6064
|
+
};
|
|
6065
|
+
}
|
|
6066
|
+
}
|
|
6067
|
+
async function restartKanbanServer(projectRoot) {
|
|
6068
|
+
await waitForShutdown(() => isKanbanServerAvailable(projectRoot));
|
|
6069
|
+
try {
|
|
6070
|
+
const connection = await getKanbanServerConnection(projectRoot);
|
|
6071
|
+
if (!connection) {
|
|
6072
|
+
return {
|
|
6073
|
+
serviceId: "kanban",
|
|
6074
|
+
action: "restart",
|
|
6075
|
+
success: false,
|
|
6076
|
+
message: "Kanban IPC daemon failed to restart (no connection after re-init)"
|
|
6077
|
+
};
|
|
6078
|
+
}
|
|
6079
|
+
await connection.request("ping", {}, { timeoutMs: 1e4 });
|
|
6080
|
+
return {
|
|
6081
|
+
serviceId: "kanban",
|
|
6082
|
+
action: "restart",
|
|
6083
|
+
success: true,
|
|
6084
|
+
message: "Kanban IPC daemon restarted successfully"
|
|
6085
|
+
};
|
|
6086
|
+
} catch (error2) {
|
|
6087
|
+
return {
|
|
6088
|
+
serviceId: "kanban",
|
|
6089
|
+
action: "restart",
|
|
6090
|
+
success: false,
|
|
6091
|
+
message: `Kanban IPC daemon restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
6092
|
+
};
|
|
6093
|
+
}
|
|
6094
|
+
}
|
|
6095
|
+
async function killSageServer(projectRoot, action) {
|
|
6096
|
+
if (!isSageProjectServerAvailable()) {
|
|
6097
|
+
return {
|
|
6098
|
+
serviceId: "sage",
|
|
6099
|
+
action,
|
|
6100
|
+
success: false,
|
|
6101
|
+
message: "SAGE project server is unavailable in this runtime"
|
|
6102
|
+
};
|
|
6103
|
+
}
|
|
6104
|
+
const connection = new SageProjectServerConnection(projectRoot);
|
|
6105
|
+
try {
|
|
6106
|
+
const result = await connection.shutdown(`WebUI request: ${action}`);
|
|
6107
|
+
if (!result.stopped) {
|
|
6108
|
+
return {
|
|
6109
|
+
serviceId: "sage",
|
|
6110
|
+
action,
|
|
6111
|
+
success: false,
|
|
6112
|
+
message: `SAGE memory server shutdown failed: ${result.reason ?? "unknown"}`
|
|
6113
|
+
};
|
|
6114
|
+
}
|
|
6115
|
+
if (action === "restart") {
|
|
6116
|
+
return await restartSageServer(projectRoot);
|
|
6117
|
+
}
|
|
6118
|
+
return {
|
|
6119
|
+
serviceId: "sage",
|
|
6120
|
+
action,
|
|
6121
|
+
success: true,
|
|
6122
|
+
message: "SAGE memory server shutdown requested"
|
|
6123
|
+
};
|
|
6124
|
+
} catch (error2) {
|
|
6125
|
+
return {
|
|
6126
|
+
serviceId: "sage",
|
|
6127
|
+
action,
|
|
6128
|
+
success: false,
|
|
6129
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
6130
|
+
};
|
|
6131
|
+
} finally {
|
|
6132
|
+
connection.close();
|
|
6133
|
+
}
|
|
6134
|
+
}
|
|
6135
|
+
async function restartSageServer(projectRoot) {
|
|
6136
|
+
await waitForShutdown(async () => {
|
|
6137
|
+
const probe = new SageProjectServerConnection(projectRoot);
|
|
6138
|
+
try {
|
|
6139
|
+
return await probe.status() !== null;
|
|
6140
|
+
} finally {
|
|
6141
|
+
probe.close();
|
|
6142
|
+
}
|
|
6143
|
+
});
|
|
6144
|
+
const verifyConn = new SageProjectServerConnection(projectRoot);
|
|
6145
|
+
try {
|
|
6146
|
+
await verifyConn.call("ping", {}, { timeoutMs: 1e4, meta: { clientId: `sage-restart-${process.pid}` } });
|
|
6147
|
+
return {
|
|
6148
|
+
serviceId: "sage",
|
|
6149
|
+
action: "restart",
|
|
6150
|
+
success: true,
|
|
6151
|
+
message: "SAGE memory server restarted successfully"
|
|
6152
|
+
};
|
|
6153
|
+
} catch (error2) {
|
|
6154
|
+
return {
|
|
6155
|
+
serviceId: "sage",
|
|
6156
|
+
action: "restart",
|
|
6157
|
+
success: false,
|
|
6158
|
+
message: `SAGE memory server restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
6159
|
+
};
|
|
6160
|
+
} finally {
|
|
6161
|
+
verifyConn.close();
|
|
6162
|
+
}
|
|
6163
|
+
}
|
|
6164
|
+
async function killChronicleServer(projectRoot, action) {
|
|
6165
|
+
const options = resolveChronicleProjectServerOptions({ projectRoot });
|
|
6166
|
+
const client = new ChronicleProjectServerClient(options);
|
|
6167
|
+
try {
|
|
6168
|
+
const result = await client.shutdown(`WebUI request: ${action}`);
|
|
6169
|
+
if (!result.stopped) {
|
|
6170
|
+
return {
|
|
6171
|
+
serviceId: "chronicle",
|
|
6172
|
+
action,
|
|
6173
|
+
success: false,
|
|
6174
|
+
message: `Chronicle telemetry server shutdown failed: ${result.reason ?? "unknown"}`
|
|
6175
|
+
};
|
|
6176
|
+
}
|
|
6177
|
+
if (action === "restart") {
|
|
6178
|
+
return await restartChronicleServer(projectRoot);
|
|
6179
|
+
}
|
|
6180
|
+
return {
|
|
6181
|
+
serviceId: "chronicle",
|
|
6182
|
+
action,
|
|
6183
|
+
success: true,
|
|
6184
|
+
message: "Chronicle telemetry server shutdown requested"
|
|
6185
|
+
};
|
|
6186
|
+
} catch (error2) {
|
|
6187
|
+
return {
|
|
6188
|
+
serviceId: "chronicle",
|
|
6189
|
+
action,
|
|
6190
|
+
success: false,
|
|
6191
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
6192
|
+
};
|
|
6193
|
+
} finally {
|
|
6194
|
+
client.close();
|
|
6195
|
+
}
|
|
6196
|
+
}
|
|
6197
|
+
async function restartChronicleServer(projectRoot) {
|
|
6198
|
+
const options = resolveChronicleProjectServerOptions({ projectRoot });
|
|
6199
|
+
const endpoint = new ChronicleProjectServerClient(options).endpoint;
|
|
6200
|
+
await waitForShutdown(async () => isEndpointAlive(endpoint));
|
|
6201
|
+
let access2;
|
|
6202
|
+
try {
|
|
6203
|
+
access2 = createChronicleProjectAccess2({ projectRoot });
|
|
6204
|
+
await access2.call("ping", {}, { timeoutMs: 1e4 });
|
|
6205
|
+
if (access2.mode !== "server") {
|
|
6206
|
+
return {
|
|
6207
|
+
serviceId: "chronicle",
|
|
6208
|
+
action: "restart",
|
|
6209
|
+
success: false,
|
|
6210
|
+
message: `Chronicle telemetry server restarted but running in ${access2.mode} mode (expected server)`
|
|
6211
|
+
};
|
|
6212
|
+
}
|
|
6213
|
+
return {
|
|
6214
|
+
serviceId: "chronicle",
|
|
6215
|
+
action: "restart",
|
|
6216
|
+
success: true,
|
|
6217
|
+
message: "Chronicle telemetry server restarted successfully"
|
|
6218
|
+
};
|
|
6219
|
+
} catch (error2) {
|
|
6220
|
+
return {
|
|
6221
|
+
serviceId: "chronicle",
|
|
6222
|
+
action: "restart",
|
|
6223
|
+
success: false,
|
|
6224
|
+
message: `Chronicle telemetry server restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
6225
|
+
};
|
|
6226
|
+
} finally {
|
|
6227
|
+
await access2?.close();
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
async function killCodebaseIndexServer(projectRoot, indexDir, action) {
|
|
6231
|
+
try {
|
|
6232
|
+
const result = await shutdownCodebaseIndexServer(
|
|
6233
|
+
projectRoot,
|
|
6234
|
+
indexDir,
|
|
6235
|
+
`websocket-request:${action}`
|
|
6236
|
+
);
|
|
6237
|
+
if (!result.stopped) {
|
|
6238
|
+
return {
|
|
6239
|
+
serviceId: "codebase-index",
|
|
6240
|
+
action,
|
|
6241
|
+
success: false,
|
|
6242
|
+
message: `Codebase index server shutdown failed: ${result.reason ?? "unknown"}`
|
|
6243
|
+
};
|
|
6244
|
+
}
|
|
6245
|
+
if (action === "restart") {
|
|
6246
|
+
return await restartCodebaseIndexServer(projectRoot, indexDir);
|
|
6247
|
+
}
|
|
6248
|
+
return {
|
|
6249
|
+
serviceId: "codebase-index",
|
|
6250
|
+
action,
|
|
6251
|
+
success: true,
|
|
6252
|
+
message: "Codebase index server shutdown requested"
|
|
6253
|
+
};
|
|
6254
|
+
} catch (error2) {
|
|
6255
|
+
return {
|
|
6256
|
+
serviceId: "codebase-index",
|
|
6257
|
+
action,
|
|
6258
|
+
success: false,
|
|
6259
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
6260
|
+
};
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
async function restartCodebaseIndexServer(projectRoot, indexDir) {
|
|
6264
|
+
await waitForShutdown(async () => {
|
|
6265
|
+
try {
|
|
6266
|
+
await checkCodebaseIndexServerHealth(projectRoot, indexDir, {
|
|
6267
|
+
timeoutMs: 1e3
|
|
6268
|
+
});
|
|
6269
|
+
return true;
|
|
6270
|
+
} catch {
|
|
6271
|
+
return false;
|
|
6272
|
+
}
|
|
6273
|
+
});
|
|
6274
|
+
try {
|
|
6275
|
+
await ensureCodebaseIndexServer2({ projectRoot, indexDir });
|
|
6276
|
+
const health = await checkCodebaseIndexServerHealth(projectRoot, indexDir, {
|
|
6277
|
+
timeoutMs: 1e4
|
|
6278
|
+
});
|
|
6279
|
+
if (health.status === "unresponsive") {
|
|
6280
|
+
return {
|
|
6281
|
+
serviceId: "codebase-index",
|
|
6282
|
+
action: "restart",
|
|
6283
|
+
success: false,
|
|
6284
|
+
message: "Codebase index server restarted but is unresponsive"
|
|
6285
|
+
};
|
|
6286
|
+
}
|
|
6287
|
+
return {
|
|
6288
|
+
serviceId: "codebase-index",
|
|
6289
|
+
action: "restart",
|
|
6290
|
+
success: true,
|
|
6291
|
+
message: "Codebase index server restarted successfully"
|
|
6292
|
+
};
|
|
6293
|
+
} catch (error2) {
|
|
6294
|
+
return {
|
|
6295
|
+
serviceId: "codebase-index",
|
|
6296
|
+
action: "restart",
|
|
6297
|
+
success: false,
|
|
6298
|
+
message: `Codebase index server restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
6299
|
+
};
|
|
6300
|
+
}
|
|
6301
|
+
}
|
|
6302
|
+
async function killMailboxServer(projectRoot, action) {
|
|
6303
|
+
if (!isMailboxProjectServerAvailable()) {
|
|
6304
|
+
return {
|
|
6305
|
+
serviceId: "mailbox",
|
|
6306
|
+
action,
|
|
6307
|
+
success: false,
|
|
6308
|
+
message: "Mailbox project server is unavailable in this runtime"
|
|
6309
|
+
};
|
|
6310
|
+
}
|
|
6311
|
+
const connection = new MailboxProjectServerConnection(
|
|
6312
|
+
resolveWstackPaths2({ projectRoot }).projectDir
|
|
6313
|
+
);
|
|
6314
|
+
try {
|
|
6315
|
+
const result = await connection.shutdown(`WebUI request: ${action}`);
|
|
6316
|
+
if (!result.stopped) {
|
|
6317
|
+
return {
|
|
6318
|
+
serviceId: "mailbox",
|
|
6319
|
+
action,
|
|
6320
|
+
success: false,
|
|
6321
|
+
message: `Mailbox IPC server shutdown failed: ${result.reason ?? "unknown"}`
|
|
6322
|
+
};
|
|
6323
|
+
}
|
|
6324
|
+
if (action === "restart") {
|
|
6325
|
+
return await restartMailboxServer(projectRoot);
|
|
6326
|
+
}
|
|
6327
|
+
return {
|
|
6328
|
+
serviceId: "mailbox",
|
|
6329
|
+
action,
|
|
6330
|
+
success: true,
|
|
6331
|
+
message: "Mailbox IPC server shutdown requested"
|
|
6332
|
+
};
|
|
6333
|
+
} catch (error2) {
|
|
6334
|
+
return {
|
|
6335
|
+
serviceId: "mailbox",
|
|
6336
|
+
action,
|
|
6337
|
+
success: false,
|
|
6338
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
6339
|
+
};
|
|
6340
|
+
} finally {
|
|
6341
|
+
connection.close();
|
|
6342
|
+
}
|
|
6343
|
+
}
|
|
6344
|
+
async function restartMailboxServer(projectRoot) {
|
|
6345
|
+
await waitForShutdown(async () => {
|
|
6346
|
+
const probe = new MailboxProjectServerConnection(
|
|
6347
|
+
resolveWstackPaths2({ projectRoot }).projectDir
|
|
6348
|
+
);
|
|
6349
|
+
try {
|
|
6350
|
+
return await probe.probeStatus() !== null;
|
|
6351
|
+
} finally {
|
|
6352
|
+
probe.close();
|
|
6353
|
+
}
|
|
6354
|
+
});
|
|
6355
|
+
const verifyConn = new MailboxProjectServerConnection(
|
|
6356
|
+
resolveWstackPaths2({ projectRoot }).projectDir
|
|
6357
|
+
);
|
|
6358
|
+
try {
|
|
6359
|
+
await verifyConn.call("ping", {}, { timeoutMs: 1e4 });
|
|
5566
6360
|
return {
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
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 }
|
|
6361
|
+
serviceId: "mailbox",
|
|
6362
|
+
action: "restart",
|
|
6363
|
+
success: true,
|
|
6364
|
+
message: "Mailbox IPC server restarted successfully"
|
|
5576
6365
|
};
|
|
6366
|
+
} catch (error2) {
|
|
6367
|
+
return {
|
|
6368
|
+
serviceId: "mailbox",
|
|
6369
|
+
action: "restart",
|
|
6370
|
+
success: false,
|
|
6371
|
+
message: `Mailbox IPC server restarted but verification failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
6372
|
+
};
|
|
6373
|
+
} finally {
|
|
6374
|
+
verifyConn.close();
|
|
5577
6375
|
}
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
6376
|
+
}
|
|
6377
|
+
var RESTART_POLL_INTERVAL_MS = 250;
|
|
6378
|
+
var RESTART_DEADLINE_MS = 3e3;
|
|
6379
|
+
function isEndpointAlive(endpoint) {
|
|
6380
|
+
return new Promise((resolve15) => {
|
|
6381
|
+
const sock = net.createConnection(endpoint);
|
|
6382
|
+
const timer = setTimeout(() => {
|
|
6383
|
+
sock.destroy();
|
|
6384
|
+
resolve15(false);
|
|
6385
|
+
}, 500);
|
|
6386
|
+
timer.unref?.();
|
|
6387
|
+
sock.once("connect", () => {
|
|
6388
|
+
clearTimeout(timer);
|
|
6389
|
+
sock.destroy();
|
|
6390
|
+
resolve15(true);
|
|
6391
|
+
});
|
|
6392
|
+
sock.once("error", () => {
|
|
6393
|
+
clearTimeout(timer);
|
|
6394
|
+
sock.destroy();
|
|
6395
|
+
resolve15(false);
|
|
6396
|
+
});
|
|
6397
|
+
});
|
|
6398
|
+
}
|
|
6399
|
+
async function waitForShutdown(probe) {
|
|
6400
|
+
if (!probe) {
|
|
6401
|
+
await new Promise((resolve15) => setTimeout(resolve15, RESTART_POLL_INTERVAL_MS));
|
|
6402
|
+
return;
|
|
6403
|
+
}
|
|
6404
|
+
const deadline = Date.now() + RESTART_DEADLINE_MS;
|
|
6405
|
+
while (Date.now() < deadline) {
|
|
6406
|
+
try {
|
|
6407
|
+
const stillUp = await probe();
|
|
6408
|
+
if (!stillUp) return;
|
|
6409
|
+
} catch {
|
|
6410
|
+
return;
|
|
5594
6411
|
}
|
|
5595
|
-
|
|
6412
|
+
await new Promise((resolve15) => setTimeout(resolve15, RESTART_POLL_INTERVAL_MS));
|
|
6413
|
+
}
|
|
5596
6414
|
}
|
|
5597
6415
|
function failureService(id, label, required, mode, error2, latencyMs) {
|
|
5598
6416
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
@@ -6302,12 +7120,12 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
|
|
|
6302
7120
|
...maybeVerify,
|
|
6303
7121
|
onPhaseComplete: (phase) => {
|
|
6304
7122
|
this.logger.info(`[Goal] Phase completed: ${phase.name}`);
|
|
6305
|
-
|
|
7123
|
+
this.persistDetached(graph);
|
|
6306
7124
|
this.broadcastState();
|
|
6307
7125
|
},
|
|
6308
7126
|
onPhaseFail: (phase, error2) => {
|
|
6309
7127
|
this.logger.error(`[Goal] Phase failed: ${phase.name} \u2014 ${error2.message}`);
|
|
6310
|
-
|
|
7128
|
+
this.persistDetached(graph);
|
|
6311
7129
|
this.broadcastState();
|
|
6312
7130
|
}
|
|
6313
7131
|
},
|
|
@@ -6324,7 +7142,7 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
|
|
|
6324
7142
|
this.broadcastState();
|
|
6325
7143
|
void this.orchestrator.start().then(() => {
|
|
6326
7144
|
this.orchestrator?.stop();
|
|
6327
|
-
|
|
7145
|
+
this.persistDetached(graph);
|
|
6328
7146
|
this.stopBroadcast();
|
|
6329
7147
|
const failed = graph.failedPhaseIds.length > 0;
|
|
6330
7148
|
this.broadcast(
|
|
@@ -6522,9 +7340,27 @@ ${result_.finalText.slice(0, 2e3)}`
|
|
|
6522
7340
|
this.logger.warn(`[Goal] Chimera review failed for "${task.title}": ${toErrorMessage2(err)}`);
|
|
6523
7341
|
}
|
|
6524
7342
|
}
|
|
7343
|
+
/**
|
|
7344
|
+
* Fire-and-forget persist.
|
|
7345
|
+
*
|
|
7346
|
+
* Every detached `store.save()` used to be a bare `void`, so a rejection
|
|
7347
|
+
* became an unhandled rejection and — under Node 22's default
|
|
7348
|
+
* `--unhandled-rejections=throw` — killed the process mid-run. On Windows an
|
|
7349
|
+
* AV scanner or indexer holding the `.wrongstack/phases/<id>.json` rename
|
|
7350
|
+
* target for a few hundred ms is enough (EPERM from `atomicWrite`), and in
|
|
7351
|
+
* `--webui` mode that takes the CLI session down with it. `handleStop` at
|
|
7352
|
+
* `:549` already had the `.catch`; these call sites did not.
|
|
7353
|
+
*/
|
|
7354
|
+
persistDetached(graph) {
|
|
7355
|
+
void this.store.save(graph).catch((err) => {
|
|
7356
|
+
this.logger.warn(
|
|
7357
|
+
`[Goal] Failed to persist phase graph: ${err instanceof Error ? err.message : String(err)}`
|
|
7358
|
+
);
|
|
7359
|
+
});
|
|
7360
|
+
}
|
|
6525
7361
|
/** Persist + broadcast after an interactive board mutation. */
|
|
6526
7362
|
afterBoardMutation() {
|
|
6527
|
-
if (this.graph)
|
|
7363
|
+
if (this.graph) this.persistDetached(this.graph);
|
|
6528
7364
|
this.broadcastState();
|
|
6529
7365
|
}
|
|
6530
7366
|
async handleTaskStatusChange(taskId, status) {
|
|
@@ -7508,12 +8344,21 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
7508
8344
|
const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
|
|
7509
8345
|
const store = new DefaultSessionStore3({ dir: paths.projectSessions });
|
|
7510
8346
|
const reader = new DefaultSessionReader2({ store });
|
|
7511
|
-
const
|
|
8347
|
+
const RING = Math.max(limit * 4, 2e3);
|
|
8348
|
+
const ring = [];
|
|
8349
|
+
let totalRaw = 0;
|
|
8350
|
+
let dropped = false;
|
|
7512
8351
|
for await (const ev of reader.replay(sessionId)) {
|
|
7513
8352
|
const mapped = mapWatchEntry(ev);
|
|
7514
|
-
if (mapped)
|
|
8353
|
+
if (!mapped) continue;
|
|
8354
|
+
totalRaw += 1;
|
|
8355
|
+
ring.push(mapped);
|
|
8356
|
+
if (ring.length > RING) {
|
|
8357
|
+
ring.shift();
|
|
8358
|
+
dropped = true;
|
|
8359
|
+
}
|
|
7515
8360
|
}
|
|
7516
|
-
const all = correlateToolEvents(
|
|
8361
|
+
const all = correlateToolEvents(ring);
|
|
7517
8362
|
const tail2 = all.slice(-limit);
|
|
7518
8363
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
7519
8364
|
res.end(
|
|
@@ -7522,7 +8367,12 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
7522
8367
|
status: entry.status,
|
|
7523
8368
|
clientType: entry.clientType,
|
|
7524
8369
|
projectName: entry.projectName,
|
|
7525
|
-
|
|
8370
|
+
// Exact when the whole session fit in the ring (the previous
|
|
8371
|
+
// behaviour). Past that, correlation never ran over the dropped
|
|
8372
|
+
// prefix, so report the raw event count — an upper bound — and say so
|
|
8373
|
+
// rather than silently understating the session's size.
|
|
8374
|
+
total: dropped ? totalRaw : all.length,
|
|
8375
|
+
...dropped ? { truncated: true } : {},
|
|
7526
8376
|
entries: tail2
|
|
7527
8377
|
})
|
|
7528
8378
|
);
|
|
@@ -8212,7 +9062,7 @@ async function touchProjectInManifest(options, globalConfigPath) {
|
|
|
8212
9062
|
}
|
|
8213
9063
|
|
|
8214
9064
|
// src/server/techstack-handlers.ts
|
|
8215
|
-
import { randomUUID as
|
|
9065
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
8216
9066
|
var DEEP_DIVE_TIMEOUT_MS = 6e4;
|
|
8217
9067
|
function sendJson3(res, status, data) {
|
|
8218
9068
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -8253,7 +9103,7 @@ function requireJobDeps(res, deps2) {
|
|
|
8253
9103
|
}
|
|
8254
9104
|
function startJob(res, deps2, kind) {
|
|
8255
9105
|
if (!requireJobDeps(res, deps2)) return;
|
|
8256
|
-
const jobId =
|
|
9106
|
+
const jobId = randomUUID3();
|
|
8257
9107
|
const controller = new AbortController();
|
|
8258
9108
|
deps2.runningJobs?.set(jobId, controller);
|
|
8259
9109
|
deps2.emit?.({ type: "techstack.job.started", payload: { jobId, kind } });
|
|
@@ -8485,19 +9335,22 @@ function extractToken(url) {
|
|
|
8485
9335
|
function extractTokenFromCookie(cookieHeader) {
|
|
8486
9336
|
if (!cookieHeader) return void 0;
|
|
8487
9337
|
const raw = Array.isArray(cookieHeader) ? cookieHeader.join("; ") : cookieHeader;
|
|
9338
|
+
let plain;
|
|
8488
9339
|
for (const part of raw.split(";")) {
|
|
8489
9340
|
const eq = part.indexOf("=");
|
|
8490
9341
|
if (eq < 0) continue;
|
|
8491
9342
|
const name2 = part.slice(0, eq).trim();
|
|
8492
|
-
if (name2
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
9343
|
+
if (name2 !== "ws_token" && name2 !== "__Host-ws_token") continue;
|
|
9344
|
+
let value;
|
|
9345
|
+
try {
|
|
9346
|
+
value = decodeURIComponent(part.slice(eq + 1).trim());
|
|
9347
|
+
} catch {
|
|
9348
|
+
value = part.slice(eq + 1).trim();
|
|
8498
9349
|
}
|
|
9350
|
+
if (name2 === "__Host-ws_token") return value;
|
|
9351
|
+
plain ??= value;
|
|
8499
9352
|
}
|
|
8500
|
-
return
|
|
9353
|
+
return plain;
|
|
8501
9354
|
}
|
|
8502
9355
|
function hostHeaderOk(input) {
|
|
8503
9356
|
if (!isLoopbackBind(input.wsHost)) return true;
|
|
@@ -8539,7 +9392,8 @@ function verifyClient(input) {
|
|
|
8539
9392
|
expectedToken,
|
|
8540
9393
|
requireToken,
|
|
8541
9394
|
allowedHostnames,
|
|
8542
|
-
allowBrowserUrlToken
|
|
9395
|
+
allowBrowserUrlToken,
|
|
9396
|
+
allowCrossPortLoopbackCookie
|
|
8543
9397
|
} = input;
|
|
8544
9398
|
const urlTokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
|
|
8545
9399
|
const cookieTokenOk = tokenMatches(extractTokenFromCookie(cookieHeader), expectedToken);
|
|
@@ -8554,7 +9408,10 @@ function verifyClient(input) {
|
|
|
8554
9408
|
const { hostname: originHostname } = new URL(origin);
|
|
8555
9409
|
if (isLoopbackHostname(originHostname)) {
|
|
8556
9410
|
if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
|
|
8557
|
-
|
|
9411
|
+
if (!isTrustedLoopbackOrigin(origin, hostHeader)) {
|
|
9412
|
+
return Boolean(allowCrossPortLoopbackCookie) && cookieTokenOk;
|
|
9413
|
+
}
|
|
9414
|
+
return true;
|
|
8558
9415
|
}
|
|
8559
9416
|
return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
|
|
8560
9417
|
} catch {
|
|
@@ -8589,11 +9446,22 @@ ${out}`;
|
|
|
8589
9446
|
function firstHeader(value) {
|
|
8590
9447
|
return Array.isArray(value) ? value[0] : value;
|
|
8591
9448
|
}
|
|
8592
|
-
|
|
8593
|
-
|
|
9449
|
+
var WS_TOKEN_COOKIE = "ws_token";
|
|
9450
|
+
var WS_TOKEN_COOKIE_SECURE = "__Host-ws_token";
|
|
9451
|
+
function wsTokenCookie(token, secure) {
|
|
9452
|
+
const name2 = secure ? WS_TOKEN_COOKIE_SECURE : WS_TOKEN_COOKIE;
|
|
9453
|
+
const parts = [
|
|
9454
|
+
`${name2}=${encodeURIComponent(token)}`,
|
|
9455
|
+
"HttpOnly",
|
|
9456
|
+
"SameSite=Strict",
|
|
9457
|
+
"Path=/",
|
|
9458
|
+
"Max-Age=3600"
|
|
9459
|
+
];
|
|
9460
|
+
if (secure) parts.push("Secure");
|
|
9461
|
+
return parts.join("; ");
|
|
8594
9462
|
}
|
|
8595
|
-
function setAuthCookieHeaders(res, token) {
|
|
8596
|
-
res.setHeader("Set-Cookie", wsTokenCookie(token));
|
|
9463
|
+
function setAuthCookieHeaders(res, token, secure) {
|
|
9464
|
+
res.setHeader("Set-Cookie", wsTokenCookie(token, secure));
|
|
8597
9465
|
res.setHeader("Cache-Control", "no-store");
|
|
8598
9466
|
}
|
|
8599
9467
|
function setStaticSecurityHeaders(res) {
|
|
@@ -8601,8 +9469,16 @@ function setStaticSecurityHeaders(res) {
|
|
|
8601
9469
|
res.setHeader("X-Frame-Options", "DENY");
|
|
8602
9470
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
8603
9471
|
}
|
|
8604
|
-
function requestToken(req, url) {
|
|
8605
|
-
|
|
9472
|
+
function requestToken(req, url, opts = {}) {
|
|
9473
|
+
const queryToken = url.searchParams.get("token") ?? void 0;
|
|
9474
|
+
if (queryToken !== void 0 && (opts.allowQuery === true || isLoopbackPeer(req))) {
|
|
9475
|
+
return queryToken;
|
|
9476
|
+
}
|
|
9477
|
+
return firstHeader(req.headers["x-ws-token"]) ?? extractTokenFromCookie(req.headers.cookie);
|
|
9478
|
+
}
|
|
9479
|
+
function isLoopbackPeer(req) {
|
|
9480
|
+
const address = req.socket.remoteAddress?.replace(/^::ffff:/i, "");
|
|
9481
|
+
return address !== void 0 && isLoopbackHostname(address);
|
|
8606
9482
|
}
|
|
8607
9483
|
function formatCspHostname(hostname) {
|
|
8608
9484
|
return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
@@ -8657,7 +9533,8 @@ function strictDecodeParam(segment, res) {
|
|
|
8657
9533
|
function createHttpServer(opts) {
|
|
8658
9534
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
8659
9535
|
const distDir = path13.resolve(opts.distDir);
|
|
8660
|
-
const requireAccessToken =
|
|
9536
|
+
const requireAccessToken = true;
|
|
9537
|
+
const secureCookies = opts.secureCookies ?? (opts.publicWsUrl?.trim().toLowerCase().startsWith("wss:") ?? false);
|
|
8661
9538
|
const trustedHostnames = (() => {
|
|
8662
9539
|
const names = [...opts.allowedHostnames ?? []];
|
|
8663
9540
|
if (opts.publicWsUrl) {
|
|
@@ -8692,17 +9569,17 @@ function createHttpServer(opts) {
|
|
|
8692
9569
|
res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
|
|
8693
9570
|
return;
|
|
8694
9571
|
}
|
|
8695
|
-
const providedAccessToken = requestToken(req, url);
|
|
9572
|
+
const providedAccessToken = requestToken(req, url, { allowQuery: true });
|
|
8696
9573
|
const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
|
|
8697
9574
|
const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
|
|
8698
9575
|
if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
|
|
8699
|
-
const provided = requestToken(req, url);
|
|
9576
|
+
const provided = requestToken(req, url, { allowQuery: true });
|
|
8700
9577
|
if (!provided || !opts.apiToken || !tokenMatches(provided, opts.apiToken)) {
|
|
8701
9578
|
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
8702
9579
|
res.end("Unauthorized");
|
|
8703
9580
|
return;
|
|
8704
9581
|
}
|
|
8705
|
-
setAuthCookieHeaders(res, opts.apiToken);
|
|
9582
|
+
setAuthCookieHeaders(res, opts.apiToken, secureCookies);
|
|
8706
9583
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
8707
9584
|
res.end("ok");
|
|
8708
9585
|
return;
|
|
@@ -8716,7 +9593,7 @@ function createHttpServer(opts) {
|
|
|
8716
9593
|
return;
|
|
8717
9594
|
}
|
|
8718
9595
|
if (shouldSetAuthCookie && opts.apiToken) {
|
|
8719
|
-
setAuthCookieHeaders(res, opts.apiToken);
|
|
9596
|
+
setAuthCookieHeaders(res, opts.apiToken, secureCookies);
|
|
8720
9597
|
}
|
|
8721
9598
|
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
8722
9599
|
if (requireAccessToken && !accessTokenOk) {
|
|
@@ -9546,6 +10423,27 @@ import {
|
|
|
9546
10423
|
getServerKanbanStore
|
|
9547
10424
|
} from "@wrongstack/kanban";
|
|
9548
10425
|
import { recordKanbanVerificationEvidence } from "@wrongstack/tools";
|
|
10426
|
+
|
|
10427
|
+
// src/server/kanban-broadcast.ts
|
|
10428
|
+
function kanbanBoardMessage(board) {
|
|
10429
|
+
return { type: "kanban.get", payload: { success: true, data: { board } } };
|
|
10430
|
+
}
|
|
10431
|
+
function kanbanListMessage(boards) {
|
|
10432
|
+
return { type: "kanban.list", payload: { success: true, data: boards } };
|
|
10433
|
+
}
|
|
10434
|
+
function kanbanDeletedMessage(boardId) {
|
|
10435
|
+
return { type: "kanban.delete", payload: { success: true, data: { removed: true, boardId } } };
|
|
10436
|
+
}
|
|
10437
|
+
async function publishKanbanBoard(broadcast2, board, listBoards4) {
|
|
10438
|
+
broadcast2(kanbanBoardMessage(board));
|
|
10439
|
+
if (!listBoards4) return;
|
|
10440
|
+
try {
|
|
10441
|
+
broadcast2(kanbanListMessage(await listBoards4()));
|
|
10442
|
+
} catch {
|
|
10443
|
+
}
|
|
10444
|
+
}
|
|
10445
|
+
|
|
10446
|
+
// src/server/kanban-dispatch.ts
|
|
9549
10447
|
function reply(ws, type, success, value) {
|
|
9550
10448
|
send(ws, {
|
|
9551
10449
|
type,
|
|
@@ -9662,10 +10560,7 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
|
|
|
9662
10560
|
payload: { success: true, data: { boardId: board.id, task: completedTask } }
|
|
9663
10561
|
});
|
|
9664
10562
|
if (completedBoard) {
|
|
9665
|
-
ctx.broadcast?.(
|
|
9666
|
-
type: "kanban.get",
|
|
9667
|
-
payload: { success: true, data: { board: completedBoard } }
|
|
9668
|
-
});
|
|
10563
|
+
ctx.broadcast?.(kanbanBoardMessage(completedBoard));
|
|
9669
10564
|
}
|
|
9670
10565
|
ctx.broadcast?.({
|
|
9671
10566
|
type: "kanban.list",
|
|
@@ -9689,7 +10584,7 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
|
|
|
9689
10584
|
payload: { success: true, data: { boardId: board.id, task: runningTask } }
|
|
9690
10585
|
});
|
|
9691
10586
|
if (started?.board) {
|
|
9692
|
-
ctx.broadcast?.(
|
|
10587
|
+
ctx.broadcast?.(kanbanBoardMessage(started.board));
|
|
9693
10588
|
}
|
|
9694
10589
|
reply(ws, "kanban.task.dispatch", true, { boardId: board.id, task: runningTask, summary });
|
|
9695
10590
|
} catch (error2) {
|
|
@@ -9932,14 +10827,11 @@ async function handleDecompositionResolution(ws, type, payload, ctx) {
|
|
|
9932
10827
|
type: "kanban.decomposition.applied",
|
|
9933
10828
|
payload: { success: true, data: { board: resolved.board } }
|
|
9934
10829
|
});
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
type: "kanban.list",
|
|
9941
|
-
payload: { success: true, data: await listBoards(ctx.projectRoot) }
|
|
9942
|
-
});
|
|
10830
|
+
await publishKanbanBoard(
|
|
10831
|
+
(message) => ctx.broadcast?.(message),
|
|
10832
|
+
resolved.board,
|
|
10833
|
+
() => listBoards(ctx.projectRoot)
|
|
10834
|
+
);
|
|
9943
10835
|
} else {
|
|
9944
10836
|
ctx.broadcast?.({
|
|
9945
10837
|
type: "kanban.decomposition.resolved",
|
|
@@ -9972,10 +10864,7 @@ async function handleTaskVerification(ws, type, payload, ctx) {
|
|
|
9972
10864
|
payload: { success: true, data: { boardId, task: freshTask } }
|
|
9973
10865
|
});
|
|
9974
10866
|
if (persisted) {
|
|
9975
|
-
ctx.broadcast?.(
|
|
9976
|
-
type: "kanban.get",
|
|
9977
|
-
payload: { success: true, data: { board: persisted } }
|
|
9978
|
-
});
|
|
10867
|
+
ctx.broadcast?.(kanbanBoardMessage(persisted));
|
|
9979
10868
|
}
|
|
9980
10869
|
} catch (err) {
|
|
9981
10870
|
ctx.broadcast?.({
|
|
@@ -10851,10 +11740,7 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
10851
11740
|
let connectionCount = 0;
|
|
10852
11741
|
const broadcastDeleted = (boardId) => {
|
|
10853
11742
|
knownRevisions.delete(boardId);
|
|
10854
|
-
broadcastMessage(
|
|
10855
|
-
type: "kanban.delete",
|
|
10856
|
-
payload: { success: true, data: { removed: true, boardId } }
|
|
10857
|
-
});
|
|
11743
|
+
broadcastMessage(kanbanDeletedMessage(boardId));
|
|
10858
11744
|
};
|
|
10859
11745
|
const broadcastBoard = async (boardId) => {
|
|
10860
11746
|
const board = await store.getBoard(boardId);
|
|
@@ -10863,10 +11749,7 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
10863
11749
|
return;
|
|
10864
11750
|
}
|
|
10865
11751
|
knownRevisions.set(boardId, board.updatedAt);
|
|
10866
|
-
broadcastMessage(
|
|
10867
|
-
type: "kanban.get",
|
|
10868
|
-
payload: { success: true, data: { board } }
|
|
10869
|
-
});
|
|
11752
|
+
broadcastMessage(kanbanBoardMessage(board));
|
|
10870
11753
|
};
|
|
10871
11754
|
const reconcileAfterConnect = async () => {
|
|
10872
11755
|
const summaries = await store.listBoards();
|
|
@@ -10885,20 +11768,36 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
10885
11768
|
}
|
|
10886
11769
|
}
|
|
10887
11770
|
};
|
|
10888
|
-
|
|
11771
|
+
const COALESCE_MS = 300;
|
|
11772
|
+
const pendingBroadcasts = /* @__PURE__ */ new Map();
|
|
11773
|
+
const scheduleBroadcast = (boardId) => {
|
|
11774
|
+
if (pendingBroadcasts.has(boardId)) return;
|
|
11775
|
+
const timer = setTimeout(() => {
|
|
11776
|
+
pendingBroadcasts.delete(boardId);
|
|
11777
|
+
void broadcastBoard(boardId).catch(() => {
|
|
11778
|
+
});
|
|
11779
|
+
}, COALESCE_MS);
|
|
11780
|
+
timer.unref?.();
|
|
11781
|
+
pendingBroadcasts.set(boardId, timer);
|
|
11782
|
+
};
|
|
11783
|
+
const unsubscribe = bridgeKanbanSupervisor(
|
|
10889
11784
|
projectRoot,
|
|
10890
11785
|
async (event) => {
|
|
11786
|
+
const family = event.event?.split(".")[0];
|
|
11787
|
+
if (family !== "board" && family !== "task" && family !== "column") return;
|
|
10891
11788
|
const evData = event.data;
|
|
10892
11789
|
const boardId = evData?.boardId;
|
|
10893
11790
|
if (!boardId) return;
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
|
|
11791
|
+
if (event.event === "board.deleted") {
|
|
11792
|
+
const timer = pendingBroadcasts.get(boardId);
|
|
11793
|
+
if (timer) {
|
|
11794
|
+
clearTimeout(timer);
|
|
11795
|
+
pendingBroadcasts.delete(boardId);
|
|
10898
11796
|
}
|
|
10899
|
-
|
|
10900
|
-
|
|
11797
|
+
broadcastDeleted(boardId);
|
|
11798
|
+
return;
|
|
10901
11799
|
}
|
|
11800
|
+
scheduleBroadcast(boardId);
|
|
10902
11801
|
},
|
|
10903
11802
|
{
|
|
10904
11803
|
autoReconnect: true,
|
|
@@ -10906,6 +11805,11 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
10906
11805
|
onConnected: reconcileAfterConnect
|
|
10907
11806
|
}
|
|
10908
11807
|
);
|
|
11808
|
+
return () => {
|
|
11809
|
+
for (const timer of pendingBroadcasts.values()) clearTimeout(timer);
|
|
11810
|
+
pendingBroadcasts.clear();
|
|
11811
|
+
unsubscribe();
|
|
11812
|
+
};
|
|
10909
11813
|
}
|
|
10910
11814
|
|
|
10911
11815
|
// src/server/lifecycle.ts
|
|
@@ -10922,7 +11826,13 @@ function createShutdown(res) {
|
|
|
10922
11826
|
} catch (e) {
|
|
10923
11827
|
log(`[WebUI] Error closing session: ${e instanceof Error ? e.message : String(e)}`);
|
|
10924
11828
|
}
|
|
10925
|
-
for (const ws of res.clients())
|
|
11829
|
+
for (const ws of res.clients()) {
|
|
11830
|
+
try {
|
|
11831
|
+
ws.close();
|
|
11832
|
+
ws.terminate?.();
|
|
11833
|
+
} catch {
|
|
11834
|
+
}
|
|
11835
|
+
}
|
|
10926
11836
|
for (const server of res.servers) server?.close();
|
|
10927
11837
|
if (res.onShutdown) {
|
|
10928
11838
|
try {
|
|
@@ -11359,6 +12269,22 @@ import {
|
|
|
11359
12269
|
restartMcp,
|
|
11360
12270
|
updateMcp
|
|
11361
12271
|
} from "@wrongstack/mcp";
|
|
12272
|
+
async function authorizeMcpMutation(ws, operation, serverName, trustBoundary) {
|
|
12273
|
+
if (!trustBoundary) return true;
|
|
12274
|
+
const authorization = await authorizeWebUIAction(trustBoundary, {
|
|
12275
|
+
capability: "mcp.server.configure",
|
|
12276
|
+
subject: { kind: "process", id: serverName },
|
|
12277
|
+
risk: "elevated",
|
|
12278
|
+
metadata: { transport: "websocket", operation }
|
|
12279
|
+
});
|
|
12280
|
+
if (!authorization.allowed) {
|
|
12281
|
+
send(ws, {
|
|
12282
|
+
type: "mcp.operation_result",
|
|
12283
|
+
payload: { success: false, message: `${operation} denied: ${authorization.reason}` }
|
|
12284
|
+
});
|
|
12285
|
+
}
|
|
12286
|
+
return authorization.allowed;
|
|
12287
|
+
}
|
|
11362
12288
|
function mapStatus(raw) {
|
|
11363
12289
|
switch (raw) {
|
|
11364
12290
|
case "connected":
|
|
@@ -11433,7 +12359,7 @@ async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
|
|
|
11433
12359
|
payload: { servers: servers.map((server) => toView(server, health.get(server.name))) }
|
|
11434
12360
|
});
|
|
11435
12361
|
}
|
|
11436
|
-
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
12362
|
+
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry, trustBoundary) {
|
|
11437
12363
|
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
11438
12364
|
if (!d) return;
|
|
11439
12365
|
const validated = validateMcpServerPayload(msg.payload, "mcp.add");
|
|
@@ -11444,6 +12370,7 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
11444
12370
|
});
|
|
11445
12371
|
return;
|
|
11446
12372
|
}
|
|
12373
|
+
if (!await authorizeMcpMutation(ws, "mcp.add", name(msg), trustBoundary)) return;
|
|
11447
12374
|
const result = await addMcp(validated.value, d);
|
|
11448
12375
|
if (result.ok && result.server) {
|
|
11449
12376
|
send(ws, { type: "mcp.server.added", payload: { server: toView(result.server) } });
|
|
@@ -11461,7 +12388,7 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
11461
12388
|
payload: { success: result.ok, message: result.message }
|
|
11462
12389
|
});
|
|
11463
12390
|
}
|
|
11464
|
-
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
12391
|
+
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry, trustBoundary) {
|
|
11465
12392
|
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
11466
12393
|
if (!d) return;
|
|
11467
12394
|
const validated = validateMcpServerPayload(msg.payload, "mcp.update");
|
|
@@ -11472,6 +12399,7 @@ async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
11472
12399
|
});
|
|
11473
12400
|
return;
|
|
11474
12401
|
}
|
|
12402
|
+
if (!await authorizeMcpMutation(ws, "mcp.update", name(msg), trustBoundary)) return;
|
|
11475
12403
|
const result = await updateMcp(validated.value, d);
|
|
11476
12404
|
if (result.ok && result.server) {
|
|
11477
12405
|
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
@@ -12821,6 +13749,8 @@ async function handleBrainAsk(ctx, ws, question) {
|
|
|
12821
13749
|
|
|
12822
13750
|
// src/server/context-meta.ts
|
|
12823
13751
|
import { FallbackProfileManager } from "@wrongstack/core/agent";
|
|
13752
|
+
import { resolvePluginEnablement } from "@wrongstack/core/plugin";
|
|
13753
|
+
import { FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
|
|
12824
13754
|
function seedContextMeta(config, context) {
|
|
12825
13755
|
const meta = context.meta;
|
|
12826
13756
|
const autonomyCfg = config.autonomy ?? {};
|
|
@@ -12836,6 +13766,7 @@ function seedContextMeta(config, context) {
|
|
|
12836
13766
|
meta["enhanceDelayMs"] = autonomyCfg["enhanceDelayMs"] ?? 6e4;
|
|
12837
13767
|
meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
|
|
12838
13768
|
meta["nextPrediction"] = config.nextPrediction ?? false;
|
|
13769
|
+
meta["nextStepsTool"] = config.tools?.nextsteps?.enabled === true;
|
|
12839
13770
|
meta["fallbackModels"] = config.fallbackModels ?? [];
|
|
12840
13771
|
meta["fallbackBridge"] = config.fallbackBridge ?? "";
|
|
12841
13772
|
meta["fallbackProfiles"] = config.fallbackProfiles ?? {};
|
|
@@ -12897,6 +13828,21 @@ function seedContextMeta(config, context) {
|
|
|
12897
13828
|
meta["tgDelegate"] = tgExt?.["notifyOnDelegate"] !== false;
|
|
12898
13829
|
const tgMs = tgExt?.["longToolThresholdMs"];
|
|
12899
13830
|
meta["tgLongToolMs"] = typeof tgMs === "number" ? tgMs : 3e4;
|
|
13831
|
+
{
|
|
13832
|
+
const pluginsEnabled = {};
|
|
13833
|
+
const record = (name2) => {
|
|
13834
|
+
if (FORBIDDEN_PROTO_KEYS2.has(name2) || name2 in pluginsEnabled) return;
|
|
13835
|
+
pluginsEnabled[name2] = resolvePluginEnablement({ name: name2, config }).enabled;
|
|
13836
|
+
};
|
|
13837
|
+
for (const entry of config.plugins ?? []) {
|
|
13838
|
+
const name2 = typeof entry === "string" ? entry : entry?.name;
|
|
13839
|
+
if (typeof name2 === "string") record(name2);
|
|
13840
|
+
}
|
|
13841
|
+
for (const [name2, options] of Object.entries(config.extensions ?? {})) {
|
|
13842
|
+
if (typeof options?.["enabled"] === "boolean") record(name2);
|
|
13843
|
+
}
|
|
13844
|
+
if (Object.keys(pluginsEnabled).length > 0) meta["pluginsEnabled"] = pluginsEnabled;
|
|
13845
|
+
}
|
|
12900
13846
|
const chimeraExt = config.extensions?.["wstack-chimera"];
|
|
12901
13847
|
meta["chimeraEnabled"] = chimeraExt?.["enabled"] === true;
|
|
12902
13848
|
meta["chimeraProvider"] = chimeraExt?.["provider"] ?? "";
|
|
@@ -12909,6 +13855,7 @@ function seedContextMeta(config, context) {
|
|
|
12909
13855
|
meta["autoReviewProvider"] = autoReviewExt?.["provider"] ?? "";
|
|
12910
13856
|
meta["autoReviewModel"] = autoReviewExt?.["model"] ?? "";
|
|
12911
13857
|
meta["autoReviewFallbackProfile"] = autoReviewExt?.["fallbackProfile"] ?? "";
|
|
13858
|
+
meta["autoReviewModelSelection"] = autoReviewExt?.["modelSelection"] === "random" ? "random" : "round-robin";
|
|
12912
13859
|
meta["autoReviewFallbackModels"] = Array.isArray(autoReviewExt?.["fallbackModels"]) ? autoReviewExt?.["fallbackModels"] : [];
|
|
12913
13860
|
meta["autoReviewDebounceMs"] = typeof autoReviewExt?.["debounceMs"] === "number" && autoReviewExt["debounceMs"] >= 0 ? autoReviewExt["debounceMs"] : 15e3;
|
|
12914
13861
|
meta["autoReviewMaxFilesPerBatch"] = typeof autoReviewExt?.["maxFilesPerBatch"] === "number" && autoReviewExt["maxFilesPerBatch"] >= 1 ? autoReviewExt["maxFilesPerBatch"] : 15;
|
|
@@ -12934,8 +13881,9 @@ function seedContextMeta(config, context) {
|
|
|
12934
13881
|
// src/server/pref-helpers.ts
|
|
12935
13882
|
import * as fs13 from "node:fs/promises";
|
|
12936
13883
|
import * as path15 from "node:path";
|
|
13884
|
+
import { pluginEntryMatchesName } from "@wrongstack/core/plugin";
|
|
12937
13885
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
12938
|
-
import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as
|
|
13886
|
+
import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS3 } from "@wrongstack/core/utils";
|
|
12939
13887
|
var PREF_KEYS = [
|
|
12940
13888
|
"autonomy",
|
|
12941
13889
|
"autonomyDelayMs",
|
|
@@ -12945,6 +13893,7 @@ var PREF_KEYS = [
|
|
|
12945
13893
|
"chime",
|
|
12946
13894
|
"confirmExit",
|
|
12947
13895
|
"nextPrediction",
|
|
13896
|
+
"nextStepsTool",
|
|
12948
13897
|
"enhanceEnabled",
|
|
12949
13898
|
"enhanceDelayMs",
|
|
12950
13899
|
"enhanceLanguage",
|
|
@@ -13008,6 +13957,7 @@ var PREF_KEYS = [
|
|
|
13008
13957
|
"autoReviewProvider",
|
|
13009
13958
|
"autoReviewModel",
|
|
13010
13959
|
"autoReviewFallbackProfile",
|
|
13960
|
+
"autoReviewModelSelection",
|
|
13011
13961
|
"autoReviewFallbackModels",
|
|
13012
13962
|
"autoReviewDebounceMs",
|
|
13013
13963
|
"autoReviewMaxFilesPerBatch",
|
|
@@ -13016,6 +13966,8 @@ var PREF_KEYS = [
|
|
|
13016
13966
|
// Display-only toggles (purely visual WebUI prefs, not persisted to config).
|
|
13017
13967
|
"groupToolCalls",
|
|
13018
13968
|
"showThinkingLogs",
|
|
13969
|
+
// v15: chat-input auto-collapse (opt-in display toggle, default off).
|
|
13970
|
+
"autoCollapseInput",
|
|
13019
13971
|
// Per-plugin enable/disable map (parity with the embedded server).
|
|
13020
13972
|
"pluginsEnabled",
|
|
13021
13973
|
// Fleet chat verbosity: off | full (migrated from streamFleet boolean).
|
|
@@ -13070,6 +14022,8 @@ async function updateGlobalConfig(deps2, holder, mutate, errorLabel) {
|
|
|
13070
14022
|
var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set([
|
|
13071
14023
|
"groupToolCalls",
|
|
13072
14024
|
"showThinkingLogs",
|
|
14025
|
+
// v15: chat-input auto-collapse (opt-in display toggle, default off).
|
|
14026
|
+
"autoCollapseInput",
|
|
13073
14027
|
"autoReviewFallbackModels",
|
|
13074
14028
|
// v11 Display parity: agent-swarm panel + inverse fsAccess flag.
|
|
13075
14029
|
// The TUI settings picker mirrors these so the browser can keep the
|
|
@@ -13217,6 +14171,11 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
13217
14171
|
toolsCfg.maxIterations = payload["maxIterations"];
|
|
13218
14172
|
decrypted.tools = toolsCfg;
|
|
13219
14173
|
}
|
|
14174
|
+
if (typeof payload["nextStepsTool"] === "boolean") {
|
|
14175
|
+
const toolsCfg = decrypted.tools ?? {};
|
|
14176
|
+
toolsCfg.nextsteps = { enabled: payload["nextStepsTool"] };
|
|
14177
|
+
decrypted.tools = toolsCfg;
|
|
14178
|
+
}
|
|
13220
14179
|
const hqTouched = typeof payload["hqEnabled"] === "boolean" || typeof payload["hqUrl"] === "string" || typeof payload["hqToken"] === "string" || typeof payload["hqRawContent"] === "boolean";
|
|
13221
14180
|
if (hqTouched) {
|
|
13222
14181
|
const hqCfg = decrypted.hq ?? {};
|
|
@@ -13280,15 +14239,29 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
13280
14239
|
decrypted.debugStream = payload["debugStream"];
|
|
13281
14240
|
if (typeof payload["pluginsEnabled"] === "object" && payload["pluginsEnabled"] !== null) {
|
|
13282
14241
|
const ext = decrypted.extensions ?? {};
|
|
14242
|
+
const toggled = [];
|
|
13283
14243
|
for (const [pluginName, enabled] of Object.entries(
|
|
13284
14244
|
payload["pluginsEnabled"]
|
|
13285
14245
|
)) {
|
|
13286
|
-
if (
|
|
14246
|
+
if (FORBIDDEN_PROTO_KEYS3.has(pluginName)) continue;
|
|
14247
|
+
if (typeof enabled !== "boolean") continue;
|
|
13287
14248
|
const pExt = ext[pluginName] ?? {};
|
|
13288
14249
|
pExt["enabled"] = enabled;
|
|
13289
14250
|
ext[pluginName] = pExt;
|
|
14251
|
+
toggled.push([pluginName, enabled]);
|
|
13290
14252
|
}
|
|
13291
14253
|
decrypted.extensions = ext;
|
|
14254
|
+
if (Array.isArray(decrypted.plugins) && toggled.length > 0) {
|
|
14255
|
+
decrypted.plugins = decrypted.plugins.map((entry) => {
|
|
14256
|
+
const entryName = typeof entry === "string" ? entry : entry?.name;
|
|
14257
|
+
if (typeof entryName !== "string") return entry;
|
|
14258
|
+
const hit = toggled.find(([name2]) => pluginEntryMatchesName(entryName, name2));
|
|
14259
|
+
if (!hit) return entry;
|
|
14260
|
+
const [, enabled] = hit;
|
|
14261
|
+
if (typeof entry === "string") return enabled ? entry : { name: entry, enabled: false };
|
|
14262
|
+
return { ...entry, enabled };
|
|
14263
|
+
});
|
|
14264
|
+
}
|
|
13292
14265
|
}
|
|
13293
14266
|
const chimeraTouched = typeof payload["chimeraEnabled"] === "boolean" || typeof payload["chimeraProvider"] === "string" || typeof payload["chimeraModel"] === "string" || typeof payload["chimeraMaxFiles"] === "number" || typeof payload["chimeraAutoFix"] === "string";
|
|
13294
14267
|
if (chimeraTouched) {
|
|
@@ -13310,7 +14283,7 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
13310
14283
|
ext["wstack-chimera"] = chimera;
|
|
13311
14284
|
decrypted.extensions = ext;
|
|
13312
14285
|
}
|
|
13313
|
-
const autoReviewTouched = typeof payload["autoReviewEnabled"] === "boolean" || typeof payload["autoReviewProvider"] === "string" || typeof payload["autoReviewModel"] === "string" || typeof payload["autoReviewFallbackProfile"] === "string" || Array.isArray(payload["autoReviewFallbackModels"]) || typeof payload["autoReviewDebounceMs"] === "number" || typeof payload["autoReviewMaxFilesPerBatch"] === "number" || typeof payload["autoReviewMaxConcurrentReviews"] === "number" || typeof payload["autoReviewCascadeOn"] === "string";
|
|
14286
|
+
const autoReviewTouched = typeof payload["autoReviewEnabled"] === "boolean" || typeof payload["autoReviewProvider"] === "string" || typeof payload["autoReviewModel"] === "string" || typeof payload["autoReviewFallbackProfile"] === "string" || typeof payload["autoReviewModelSelection"] === "string" || Array.isArray(payload["autoReviewFallbackModels"]) || typeof payload["autoReviewDebounceMs"] === "number" || typeof payload["autoReviewMaxFilesPerBatch"] === "number" || typeof payload["autoReviewMaxConcurrentReviews"] === "number" || typeof payload["autoReviewCascadeOn"] === "string";
|
|
13314
14287
|
if (autoReviewTouched) {
|
|
13315
14288
|
const ext = decrypted.extensions ?? {};
|
|
13316
14289
|
const ar = ext["wstack-auto-review"] ?? {};
|
|
@@ -13327,6 +14300,9 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
13327
14300
|
ar["fallbackProfile"] = payload["autoReviewFallbackProfile"];
|
|
13328
14301
|
}
|
|
13329
14302
|
}
|
|
14303
|
+
if (payload["autoReviewModelSelection"] === "round-robin" || payload["autoReviewModelSelection"] === "random") {
|
|
14304
|
+
ar["modelSelection"] = payload["autoReviewModelSelection"];
|
|
14305
|
+
}
|
|
13330
14306
|
if (typeof payload["autoReviewDebounceMs"] === "number" && payload["autoReviewDebounceMs"] >= 0) {
|
|
13331
14307
|
ar["debounceMs"] = payload["autoReviewDebounceMs"];
|
|
13332
14308
|
}
|
|
@@ -13397,39 +14373,6 @@ async function handlePrefsRoute(ws, msg, handlers) {
|
|
|
13397
14373
|
// src/server/process-handlers.ts
|
|
13398
14374
|
import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
|
|
13399
14375
|
import { getProcessRegistry as getProcessRegistry2 } from "@wrongstack/tools";
|
|
13400
|
-
|
|
13401
|
-
// src/server/privileged-actions.ts
|
|
13402
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
13403
|
-
import {
|
|
13404
|
-
isTrustDecisionAllowed
|
|
13405
|
-
} from "@wrongstack/core/security";
|
|
13406
|
-
async function authorizeWebUIAction(boundary, action, logger) {
|
|
13407
|
-
const request = {
|
|
13408
|
-
version: 1,
|
|
13409
|
-
requestId: randomUUID3(),
|
|
13410
|
-
actor: {
|
|
13411
|
-
kind: "remote-client",
|
|
13412
|
-
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
13413
|
-
},
|
|
13414
|
-
surface: "webui",
|
|
13415
|
-
capability: action.capability,
|
|
13416
|
-
subject: action.subject,
|
|
13417
|
-
risk: action.risk,
|
|
13418
|
-
scope: {
|
|
13419
|
-
...action.cwd ? { cwd: action.cwd } : {},
|
|
13420
|
-
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
13421
|
-
},
|
|
13422
|
-
authContext: { method: "session" },
|
|
13423
|
-
...action.metadata ? { metadata: action.metadata } : {}
|
|
13424
|
-
};
|
|
13425
|
-
const decision = await boundary.evaluate(request);
|
|
13426
|
-
logger?.debug?.(
|
|
13427
|
-
`[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
|
|
13428
|
-
);
|
|
13429
|
-
return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
|
|
13430
|
-
}
|
|
13431
|
-
|
|
13432
|
-
// src/server/process-handlers.ts
|
|
13433
14376
|
function handleProcessList(ws) {
|
|
13434
14377
|
try {
|
|
13435
14378
|
const procs = getProcessRegistry2().list();
|
|
@@ -13720,6 +14663,7 @@ function createProjectHandlers(ctx) {
|
|
|
13720
14663
|
ctx.context.session = next;
|
|
13721
14664
|
ctx.context.state.replaceMessages([]);
|
|
13722
14665
|
ctx.context.state.replaceTodos([]);
|
|
14666
|
+
ctx.context.clearMemoryEvidence?.();
|
|
13723
14667
|
ctx.context.readFiles.clear();
|
|
13724
14668
|
ctx.context.fileMtimes.clear();
|
|
13725
14669
|
ctx.tokenCounter.reset();
|
|
@@ -13764,7 +14708,7 @@ function createProjectHandlers(ctx) {
|
|
|
13764
14708
|
}
|
|
13765
14709
|
|
|
13766
14710
|
// src/server/provider-handlers.ts
|
|
13767
|
-
import { resolveProviderModelList } from "@wrongstack/core/models";
|
|
14711
|
+
import { hasProviderCredential, resolveProviderModelList } from "@wrongstack/core/models";
|
|
13768
14712
|
import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
|
|
13769
14713
|
import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
|
|
13770
14714
|
import {
|
|
@@ -14071,7 +15015,7 @@ function createProviderOperations(deps2) {
|
|
|
14071
15015
|
}
|
|
14072
15016
|
try {
|
|
14073
15017
|
const providers = await deps2.modelsRegistry.listProviders();
|
|
14074
|
-
const
|
|
15018
|
+
const savedProviders = await loadConfigProviders();
|
|
14075
15019
|
sendMessage(ws, {
|
|
14076
15020
|
type: "provider.catalog",
|
|
14077
15021
|
payload: {
|
|
@@ -14082,7 +15026,7 @@ function createProviderOperations(deps2) {
|
|
|
14082
15026
|
apiBase: provider.apiBase,
|
|
14083
15027
|
envVars: provider.envVars,
|
|
14084
15028
|
modelCount: provider.models.length,
|
|
14085
|
-
hasApiKey:
|
|
15029
|
+
hasApiKey: hasProviderCredential(provider, { providers: savedProviders })
|
|
14086
15030
|
}))
|
|
14087
15031
|
}
|
|
14088
15032
|
});
|
|
@@ -14245,8 +15189,10 @@ function createProviderOperations(deps2) {
|
|
|
14245
15189
|
if (result.ok) {
|
|
14246
15190
|
deps2.log?.(`[WebUI] Provider "${payload.id}" added via provider.add`);
|
|
14247
15191
|
}
|
|
15192
|
+
return result.ok;
|
|
14248
15193
|
} catch (err) {
|
|
14249
15194
|
sendOperationResult(ws, false, errMessage(err));
|
|
15195
|
+
return false;
|
|
14250
15196
|
}
|
|
14251
15197
|
}
|
|
14252
15198
|
async function handleProviderRemove(ws, providerId) {
|
|
@@ -14564,9 +15510,11 @@ var CLIENT_CONVERSATION_MESSAGE_TYPES = [
|
|
|
14564
15510
|
"ping",
|
|
14565
15511
|
"user_message",
|
|
14566
15512
|
"tool.confirm_result",
|
|
15513
|
+
"topic.advice",
|
|
14567
15514
|
"completion.request",
|
|
14568
15515
|
"model.switch",
|
|
14569
15516
|
"model.refine",
|
|
15517
|
+
"model.fallback_choice",
|
|
14570
15518
|
"autonomy.switch",
|
|
14571
15519
|
"context.clear",
|
|
14572
15520
|
"context.compact",
|
|
@@ -14838,6 +15786,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
|
|
|
14838
15786
|
"provider.active_blocked",
|
|
14839
15787
|
"provider.error",
|
|
14840
15788
|
"provider.fallback",
|
|
15789
|
+
"provider.fallback_pending",
|
|
14841
15790
|
"provider.response",
|
|
14842
15791
|
"provider.retry",
|
|
14843
15792
|
"provider.status_changed",
|
|
@@ -14864,6 +15813,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
|
|
|
14864
15813
|
"tool.loop_detected",
|
|
14865
15814
|
"tool.progress",
|
|
14866
15815
|
"tool.started",
|
|
15816
|
+
"topic.advice_result",
|
|
14867
15817
|
"tools.list",
|
|
14868
15818
|
"trust.persisted"
|
|
14869
15819
|
];
|
|
@@ -14879,6 +15829,7 @@ var SERVER_COLLABORATION_MESSAGE_TYPES = [
|
|
|
14879
15829
|
"collab.state",
|
|
14880
15830
|
"mailbox.action_result",
|
|
14881
15831
|
"mailbox.agent_registered",
|
|
15832
|
+
"mailbox.agent_deregistered",
|
|
14882
15833
|
"mailbox.agents",
|
|
14883
15834
|
"mailbox.cleared",
|
|
14884
15835
|
"mailbox.compacted",
|
|
@@ -15194,6 +16145,8 @@ var SURFACE_PROTOCOL_CAPABILITIES = [
|
|
|
15194
16145
|
"chronicle.metrics",
|
|
15195
16146
|
"chronicle.status",
|
|
15196
16147
|
"connections.health",
|
|
16148
|
+
/** Bounded topic-shift advice plus same-session provider-context boundaries. */
|
|
16149
|
+
"context.topic-boundary",
|
|
15197
16150
|
/** Interview resume/discard + lastAgentText/lastRunId continuity. */
|
|
15198
16151
|
"sdd.interview.continuity",
|
|
15199
16152
|
/** Launch multi-agent runs from a graph id or resolved spec id. */
|
|
@@ -15585,6 +16538,7 @@ function createSessionHandlers(ctx) {
|
|
|
15585
16538
|
await ctx.onBeforeSessionTodosReplaced?.(next.id, sessionsDirectory());
|
|
15586
16539
|
ctx.context.state.replaceTodos(todos);
|
|
15587
16540
|
resetContextAccounting();
|
|
16541
|
+
ctx.context.clearMemoryEvidence?.();
|
|
15588
16542
|
ctx.context.readFiles.clear();
|
|
15589
16543
|
ctx.context.fileMtimes.clear();
|
|
15590
16544
|
ctx.context.state.setMeta?.(
|
|
@@ -15632,6 +16586,7 @@ function createSessionHandlers(ctx) {
|
|
|
15632
16586
|
ctx.context.state.replaceMessages([]);
|
|
15633
16587
|
ctx.context.state.replaceTodos([]);
|
|
15634
16588
|
resetContextAccounting();
|
|
16589
|
+
ctx.context.clearMemoryEvidence?.();
|
|
15635
16590
|
ctx.context.readFiles.clear();
|
|
15636
16591
|
ctx.context.fileMtimes.clear();
|
|
15637
16592
|
ctx.tokenCounter.reset?.();
|
|
@@ -15646,6 +16601,7 @@ function createSessionHandlers(ctx) {
|
|
|
15646
16601
|
ctx.context.state.replaceMessages([]);
|
|
15647
16602
|
ctx.context.state.replaceTodos([]);
|
|
15648
16603
|
resetContextAccounting();
|
|
16604
|
+
ctx.context.clearMemoryEvidence?.();
|
|
15649
16605
|
ctx.context.readFiles.clear();
|
|
15650
16606
|
ctx.context.fileMtimes.clear();
|
|
15651
16607
|
ctx.tokenCounter.reset?.();
|
|
@@ -15746,6 +16702,7 @@ function createSessionHandlers(ctx) {
|
|
|
15746
16702
|
tools: ctx.listTools?.() ?? ctx.toolRegistry?.list(),
|
|
15747
16703
|
baseRevision: typeof payload["baseRevision"] === "string" ? payload["baseRevision"] : "",
|
|
15748
16704
|
messages: payload["messages"],
|
|
16705
|
+
removals: payload["removals"],
|
|
15749
16706
|
allowRepair: payload["allowRepair"] === true,
|
|
15750
16707
|
runActive: ctx.isRunActive?.() === true
|
|
15751
16708
|
});
|
|
@@ -15762,6 +16719,7 @@ function createSessionHandlers(ctx) {
|
|
|
15762
16719
|
tools: ctx.listTools?.() ?? ctx.toolRegistry?.list(),
|
|
15763
16720
|
baseRevision: typeof payload["baseRevision"] === "string" ? payload["baseRevision"] : "",
|
|
15764
16721
|
messages: payload["messages"],
|
|
16722
|
+
removals: payload["removals"],
|
|
15765
16723
|
allowRepair: payload["allowRepair"] === true,
|
|
15766
16724
|
runActive: ctx.isRunActive?.() === true
|
|
15767
16725
|
});
|
|
@@ -16581,6 +17539,19 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
|
|
|
16581
17539
|
return true;
|
|
16582
17540
|
}
|
|
16583
17541
|
|
|
17542
|
+
// src/server/fallback-choice.ts
|
|
17543
|
+
function emitFallbackChoice(events, msg) {
|
|
17544
|
+
const parsed = validateModelFallbackChoicePayload(msg.payload);
|
|
17545
|
+
if (!parsed.ok) return parsed;
|
|
17546
|
+
events?.emit("provider.fallback_choice", {
|
|
17547
|
+
requestId: parsed.value.requestId,
|
|
17548
|
+
...parsed.value.providerId ? { providerId: parsed.value.providerId } : {},
|
|
17549
|
+
...parsed.value.model ? { model: parsed.value.model } : {},
|
|
17550
|
+
...parsed.value.autoSwitch ? { autoSwitch: true } : {}
|
|
17551
|
+
});
|
|
17552
|
+
return { ok: true };
|
|
17553
|
+
}
|
|
17554
|
+
|
|
16584
17555
|
// src/server/agent-roster-routes.ts
|
|
16585
17556
|
async function handleAgentRosterRoute(ws, msg, handlers) {
|
|
16586
17557
|
if (!msg.type.startsWith("agent-roster.")) return false;
|
|
@@ -16745,13 +17716,17 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16745
17716
|
case "model.refine":
|
|
16746
17717
|
await routes.refineModel(ws, msg);
|
|
16747
17718
|
return true;
|
|
17719
|
+
case "model.fallback_choice":
|
|
17720
|
+
await routes.fallbackChoice(ws, msg);
|
|
17721
|
+
return true;
|
|
16748
17722
|
case "key.add":
|
|
16749
17723
|
case "key.update": {
|
|
16750
17724
|
const payload = asPayloadRecord(msg);
|
|
16751
17725
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16752
17726
|
const label = payload ? requiredString(payload, "label") : null;
|
|
16753
17727
|
const apiKey = payload ? requiredString(payload, "apiKey") : null;
|
|
16754
|
-
if (!providerId || !label || !apiKey)
|
|
17728
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label || !apiKey)
|
|
17729
|
+
return invalidPayload(ws, msg.type);
|
|
16755
17730
|
await routes.providerHandlers.handleKeyUpsert(ws, providerId, label, apiKey);
|
|
16756
17731
|
return true;
|
|
16757
17732
|
}
|
|
@@ -16759,7 +17734,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16759
17734
|
const payload = asPayloadRecord(msg);
|
|
16760
17735
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16761
17736
|
const label = payload ? requiredString(payload, "label") : null;
|
|
16762
|
-
if (!providerId || !
|
|
17737
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label)
|
|
17738
|
+
return invalidPayload(ws, msg.type);
|
|
16763
17739
|
await routes.providerHandlers.handleKeyDelete(ws, providerId, label);
|
|
16764
17740
|
return true;
|
|
16765
17741
|
}
|
|
@@ -16767,7 +17743,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16767
17743
|
const payload = asPayloadRecord(msg);
|
|
16768
17744
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16769
17745
|
const label = payload ? requiredString(payload, "label") : null;
|
|
16770
|
-
if (!providerId || !
|
|
17746
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label)
|
|
17747
|
+
return invalidPayload(ws, msg.type);
|
|
16771
17748
|
await routes.providerHandlers.handleKeySetActive(ws, providerId, label);
|
|
16772
17749
|
return true;
|
|
16773
17750
|
}
|
|
@@ -16779,11 +17756,11 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16779
17756
|
const apiKey = payload?.["apiKey"];
|
|
16780
17757
|
const models = payload ? optionalStringArray(payload, "models") : null;
|
|
16781
17758
|
const customModels = payload ? optionalCustomModels(payload) : null;
|
|
16782
|
-
if (!id || !family) return invalidPayload(ws, msg.type);
|
|
17759
|
+
if (!id || !SAFE_CONFIG_KEY.test(id) || !family) return invalidPayload(ws, msg.type);
|
|
16783
17760
|
if (baseUrl !== void 0 && typeof baseUrl !== "string") return invalidPayload(ws, msg.type);
|
|
16784
17761
|
if (apiKey !== void 0 && typeof apiKey !== "string") return invalidPayload(ws, msg.type);
|
|
16785
17762
|
if (models === null || customModels === null) return invalidPayload(ws, msg.type);
|
|
16786
|
-
await routes.providerHandlers.handleProviderAdd(ws, {
|
|
17763
|
+
const added = await routes.providerHandlers.handleProviderAdd(ws, {
|
|
16787
17764
|
id,
|
|
16788
17765
|
family,
|
|
16789
17766
|
baseUrl,
|
|
@@ -16791,20 +17768,22 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16791
17768
|
models,
|
|
16792
17769
|
customModels
|
|
16793
17770
|
});
|
|
16794
|
-
|
|
17771
|
+
if (added) {
|
|
17772
|
+
void routes.adoptDefaultProviderIfUnset(id).catch(() => void 0);
|
|
17773
|
+
}
|
|
16795
17774
|
return true;
|
|
16796
17775
|
}
|
|
16797
17776
|
case "provider.remove": {
|
|
16798
17777
|
const payload = asPayloadRecord(msg);
|
|
16799
17778
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16800
|
-
if (!providerId) return invalidPayload(ws, msg.type);
|
|
17779
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId)) return invalidPayload(ws, msg.type);
|
|
16801
17780
|
await routes.providerHandlers.handleProviderRemove(ws, providerId);
|
|
16802
17781
|
return true;
|
|
16803
17782
|
}
|
|
16804
17783
|
case "provider.clear_models": {
|
|
16805
17784
|
const payload = asPayloadRecord(msg);
|
|
16806
17785
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16807
|
-
if (!providerId) return invalidPayload(ws, msg.type);
|
|
17786
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId)) return invalidPayload(ws, msg.type);
|
|
16808
17787
|
await routes.providerHandlers.handleProviderClearModels(ws, providerId);
|
|
16809
17788
|
return true;
|
|
16810
17789
|
}
|
|
@@ -16836,7 +17815,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16836
17815
|
const payload = asPayloadRecord(msg);
|
|
16837
17816
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16838
17817
|
const previousModels = payload ? optionalStringArray(payload, "previousModels") : null;
|
|
16839
|
-
if (!providerId || !
|
|
17818
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !previousModels)
|
|
17819
|
+
return invalidPayload(ws, msg.type);
|
|
16840
17820
|
await routes.providerHandlers.handleProviderUndoClear(ws, providerId, previousModels);
|
|
16841
17821
|
return true;
|
|
16842
17822
|
}
|
|
@@ -16846,7 +17826,7 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16846
17826
|
const envVars = payload ? optionalStringArray(payload, "envVars") : null;
|
|
16847
17827
|
const models = payload ? optionalStringArray(payload, "models") : null;
|
|
16848
17828
|
const customModels = payload ? optionalCustomModels(payload) : null;
|
|
16849
|
-
if (!payload || !id || envVars === null || models === null || customModels === null)
|
|
17829
|
+
if (!payload || !id || !SAFE_CONFIG_KEY.test(id) || envVars === null || models === null || customModels === null)
|
|
16850
17830
|
return invalidPayload(ws, msg.type);
|
|
16851
17831
|
for (const key of ["family", "baseUrl"]) {
|
|
16852
17832
|
if (payload[key] !== void 0 && typeof payload[key] !== "string")
|
|
@@ -16866,7 +17846,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16866
17846
|
const payload = asPayloadRecord(msg);
|
|
16867
17847
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16868
17848
|
const timeoutMs = payload ? optionalNumber(payload, "timeoutMs") : null;
|
|
16869
|
-
if (!providerId || timeoutMs === null)
|
|
17849
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || timeoutMs === null)
|
|
17850
|
+
return invalidPayload(ws, msg.type);
|
|
16870
17851
|
await routes.providerHandlers.handleProviderProbe(ws, providerId, timeoutMs);
|
|
16871
17852
|
return true;
|
|
16872
17853
|
}
|
|
@@ -16875,7 +17856,7 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16875
17856
|
const kind = oauthKind(payload);
|
|
16876
17857
|
const providerId = payload?.["providerId"];
|
|
16877
17858
|
if (!kind) return invalidPayload(ws, msg.type);
|
|
16878
|
-
if (providerId !== void 0 && typeof providerId !== "string") {
|
|
17859
|
+
if (providerId !== void 0 && (typeof providerId !== "string" || !SAFE_CONFIG_KEY.test(providerId))) {
|
|
16879
17860
|
return invalidPayload(ws, msg.type);
|
|
16880
17861
|
}
|
|
16881
17862
|
await routes.providerHandlers.handleOAuthStart(ws, kind, providerId);
|
|
@@ -16914,7 +17895,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16914
17895
|
const payload = asPayloadRecord(msg);
|
|
16915
17896
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16916
17897
|
const model = payload ? requiredString(payload, "model") : null;
|
|
16917
|
-
if (!providerId || !
|
|
17898
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !model)
|
|
17899
|
+
return invalidPayload(ws, msg.type);
|
|
16918
17900
|
const released = routes.statusTracker.retryNow(providerId, model);
|
|
16919
17901
|
sendResult2(
|
|
16920
17902
|
ws,
|
|
@@ -16931,7 +17913,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16931
17913
|
const payload = asPayloadRecord(msg);
|
|
16932
17914
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16933
17915
|
const model = payload ? requiredString(payload, "model") : null;
|
|
16934
|
-
if (!providerId || !
|
|
17916
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !model)
|
|
17917
|
+
return invalidPayload(ws, msg.type);
|
|
16935
17918
|
routes.statusTracker.clear(providerId, model);
|
|
16936
17919
|
sendResult2(ws, true, `Cleared tracking for ${providerId}/${model}.`);
|
|
16937
17920
|
return true;
|
|
@@ -18089,7 +19072,7 @@ function registerSetupEventsClientStatusWriter(deps2) {
|
|
|
18089
19072
|
const on = (event, listener) => events.on(event, listener);
|
|
18090
19073
|
return on("client.status", async (e) => {
|
|
18091
19074
|
broadcast2(clients, { type: "client.status_update", payload: e });
|
|
18092
|
-
if (wpaths?.projectStatus) {
|
|
19075
|
+
if (wpaths?.projectStatus && e.projectHash !== "unknown") {
|
|
18093
19076
|
try {
|
|
18094
19077
|
const statusFile = wpaths.projectStatus(e.projectHash);
|
|
18095
19078
|
const dir = path19.dirname(statusFile);
|
|
@@ -18264,7 +19247,10 @@ function registerSetupEventsProviderHandlers({
|
|
|
18264
19247
|
sessionId: e.sessionId,
|
|
18265
19248
|
providerId: e.providerId,
|
|
18266
19249
|
modelId: e.modelId,
|
|
18267
|
-
maxContext: e.maxContext
|
|
19250
|
+
maxContext: e.maxContext,
|
|
19251
|
+
...e.previousMaxContext !== void 0 ? { previousMaxContext: e.previousMaxContext } : {},
|
|
19252
|
+
...e.source !== void 0 ? { source: e.source } : {},
|
|
19253
|
+
...e.decreased !== void 0 ? { decreased: e.decreased } : {}
|
|
18268
19254
|
})
|
|
18269
19255
|
});
|
|
18270
19256
|
});
|
|
@@ -18961,7 +19947,22 @@ function setupEvents(deps2) {
|
|
|
18961
19947
|
from: e.from,
|
|
18962
19948
|
to: e.to,
|
|
18963
19949
|
status: e.status,
|
|
18964
|
-
providerSwitched: e.providerSwitched
|
|
19950
|
+
providerSwitched: e.providerSwitched,
|
|
19951
|
+
...e.requestId ? { requestId: e.requestId } : {}
|
|
19952
|
+
})
|
|
19953
|
+
});
|
|
19954
|
+
});
|
|
19955
|
+
on("provider.fallback_pending", (e) => {
|
|
19956
|
+
broadcast2(clients, {
|
|
19957
|
+
type: "provider.fallback_pending",
|
|
19958
|
+
payload: sessionPayload2({
|
|
19959
|
+
sessionId: e.sessionId,
|
|
19960
|
+
from: e.from,
|
|
19961
|
+
status: e.status,
|
|
19962
|
+
candidates: e.candidates,
|
|
19963
|
+
autoSwitchSeconds: e.autoSwitchSeconds,
|
|
19964
|
+
requestId: e.requestId,
|
|
19965
|
+
timestamp: e.timestamp
|
|
18965
19966
|
})
|
|
18966
19967
|
});
|
|
18967
19968
|
});
|
|
@@ -19046,6 +20047,15 @@ function setupEvents(deps2) {
|
|
|
19046
20047
|
type: "mailbox.agent_registered",
|
|
19047
20048
|
payload
|
|
19048
20049
|
});
|
|
20050
|
+
}),
|
|
20051
|
+
// Deregistration (subagent retirement) must reach the browser too —
|
|
20052
|
+
// otherwise dead agents linger in the client roster until an unrelated
|
|
20053
|
+
// refresh. Emitted by sqlite-mailbox.deregisterAgent with { agentId }.
|
|
20054
|
+
events.onPattern("mailbox.agent_deregistered", (_e, payload) => {
|
|
20055
|
+
broadcast2(clients, {
|
|
20056
|
+
type: "mailbox.agent_deregistered",
|
|
20057
|
+
payload
|
|
20058
|
+
});
|
|
19049
20059
|
})
|
|
19050
20060
|
);
|
|
19051
20061
|
const forwardSubagent = (kind, payload) => broadcast2(clients, { type: "subagent.event", payload: sessionPayload2({ kind, ...payload }) });
|
|
@@ -19350,7 +20360,15 @@ var SpecsWebSocketHandler = class {
|
|
|
19350
20360
|
this.clients.add(client);
|
|
19351
20361
|
ws.on("close", () => this.clients.delete(client));
|
|
19352
20362
|
ws.on("error", () => this.clients.delete(client));
|
|
19353
|
-
void this.sendList(client)
|
|
20363
|
+
void this.sendList(client).catch((err) => {
|
|
20364
|
+
console.warn(
|
|
20365
|
+
JSON.stringify({
|
|
20366
|
+
level: "warn",
|
|
20367
|
+
event: "specs.initial_send_failed",
|
|
20368
|
+
message: err instanceof Error ? err.message : String(err)
|
|
20369
|
+
})
|
|
20370
|
+
);
|
|
20371
|
+
});
|
|
19354
20372
|
}
|
|
19355
20373
|
dispose() {
|
|
19356
20374
|
this.clients.clear();
|
|
@@ -19564,15 +20582,17 @@ import {
|
|
|
19564
20582
|
|
|
19565
20583
|
// src/server/discover-mailbox-bridge.ts
|
|
19566
20584
|
import { spawn as spawn2 } from "node:child_process";
|
|
19567
|
-
import { createRequire } from "node:module";
|
|
19568
20585
|
import { existsSync } from "node:fs";
|
|
20586
|
+
import { createRequire } from "node:module";
|
|
19569
20587
|
import { dirname as dirname7, join as join10 } from "node:path";
|
|
19570
|
-
import {
|
|
20588
|
+
import {
|
|
20589
|
+
readLiveLock,
|
|
20590
|
+
resolveProjectDir as resolveProjectDir2
|
|
20591
|
+
} from "@wrongstack/core/coordination";
|
|
19571
20592
|
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
19572
|
-
import { readLiveLock } from "@wrongstack/core/coordination";
|
|
19573
20593
|
var MAILBOX_BRIDGE_BOOT_TIMEOUT_MS = 5e3;
|
|
19574
20594
|
async function discoverMailboxBridgeForWebui(params) {
|
|
19575
|
-
const mode = params.config?.features?.mailboxBridge ?? "
|
|
20595
|
+
const mode = params.config?.features?.mailboxBridge ?? "off";
|
|
19576
20596
|
if (mode === "off") return;
|
|
19577
20597
|
const projectDir = resolveProjectDir2(params.projectRoot, wstackGlobalRoot());
|
|
19578
20598
|
let result = await readLiveLock(projectDir);
|
|
@@ -19834,6 +20854,13 @@ var TerminalWebSocketHandler = class {
|
|
|
19834
20854
|
this.send(ws, { type: "terminal.exit", payload: { id: payload.id, exitCode: -1 } });
|
|
19835
20855
|
return;
|
|
19836
20856
|
}
|
|
20857
|
+
if (this.sessions.get(ws) !== map) {
|
|
20858
|
+
this.logger.info?.(
|
|
20859
|
+
`terminal.create raced a disconnect (id=${payload.id}) \u2014 killing the orphan`
|
|
20860
|
+
);
|
|
20861
|
+
this.killPty(pty, "terminal create after disconnect");
|
|
20862
|
+
return;
|
|
20863
|
+
}
|
|
19837
20864
|
map.set(payload.id, pty);
|
|
19838
20865
|
this.logger.info?.(`terminal.create spawned (id=${payload.id}, pid=${pty.pid ?? "?"}) in ${cwd}`);
|
|
19839
20866
|
pty.onData((data) => {
|
|
@@ -21259,6 +22286,15 @@ function createMessageDispatcher(opts) {
|
|
|
21259
22286
|
msg
|
|
21260
22287
|
))
|
|
21261
22288
|
return;
|
|
22289
|
+
if (await handleConnectionsServiceAction(ws, msg, {
|
|
22290
|
+
trustBoundary: deps2.trustBoundary,
|
|
22291
|
+
logger: deps2.logger,
|
|
22292
|
+
getProjectRoot: state.getProjectRoot,
|
|
22293
|
+
getIndexDir: () => typeof deps2.context.meta["codebaseIndexDir"] === "string" ? deps2.context.meta["codebaseIndexDir"] : void 0,
|
|
22294
|
+
send,
|
|
22295
|
+
backend: "standalone"
|
|
22296
|
+
}))
|
|
22297
|
+
return;
|
|
21262
22298
|
if (await handleCodebaseIndexServerControl(ws, msg, {
|
|
21263
22299
|
trustBoundary: deps2.trustBoundary,
|
|
21264
22300
|
logger: deps2.logger,
|
|
@@ -21330,29 +22366,10 @@ import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/too
|
|
|
21330
22366
|
// src/server/model-auto-discovery.ts
|
|
21331
22367
|
import * as fs19 from "node:fs/promises";
|
|
21332
22368
|
import * as path24 from "node:path";
|
|
21333
|
-
import {
|
|
22369
|
+
import { discoverOpenAICompatibleModels, resolveDiscoveryTargets } from "@wrongstack/providers";
|
|
21334
22370
|
function isOverlayRegistry(value) {
|
|
21335
22371
|
return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
|
|
21336
22372
|
}
|
|
21337
|
-
function resolveKey(cfg) {
|
|
21338
|
-
if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
|
|
21339
|
-
const active = cfg.activeKey ? cfg.apiKeys.find((key) => key.label === cfg.activeKey) : void 0;
|
|
21340
|
-
return (active ?? cfg.apiKeys[0])?.apiKey;
|
|
21341
|
-
}
|
|
21342
|
-
return cfg.apiKey && cfg.apiKey.length > 0 ? cfg.apiKey : void 0;
|
|
21343
|
-
}
|
|
21344
|
-
function eligibleProviders(config) {
|
|
21345
|
-
const out = [];
|
|
21346
|
-
for (const [id, cfg] of Object.entries(config.providers ?? {})) {
|
|
21347
|
-
const preset = COMPATIBLE_PRESETS[id];
|
|
21348
|
-
const enabled = cfg.autoDiscoverModels ?? preset?.autoDiscover ?? false;
|
|
21349
|
-
if (!enabled) continue;
|
|
21350
|
-
const baseUrl = cfg.baseUrl ?? preset?.defaultBaseUrl;
|
|
21351
|
-
if (!baseUrl) continue;
|
|
21352
|
-
out.push({ id, cfg, baseUrl, apiKey: resolveKey(cfg) });
|
|
21353
|
-
}
|
|
21354
|
-
return out;
|
|
21355
|
-
}
|
|
21356
22373
|
async function readCache(file) {
|
|
21357
22374
|
try {
|
|
21358
22375
|
return JSON.parse(await fs19.readFile(file, "utf8"));
|
|
@@ -21363,14 +22380,13 @@ async function readCache(file) {
|
|
|
21363
22380
|
async function discoverAndMergeWebuiProviders(opts) {
|
|
21364
22381
|
const registry = opts.registry;
|
|
21365
22382
|
if (!isOverlayRegistry(registry)) return;
|
|
21366
|
-
const targets =
|
|
22383
|
+
const targets = resolveDiscoveryTargets(opts.config);
|
|
21367
22384
|
if (targets.length === 0) return;
|
|
21368
22385
|
const cacheFile = path24.join(opts.cacheDir, "discovered-models-cache.json");
|
|
21369
22386
|
const cache2 = await readCache(cacheFile);
|
|
21370
22387
|
let cacheDirty = false;
|
|
21371
22388
|
await Promise.all(
|
|
21372
|
-
targets.map(async ({ id, cfg, baseUrl, apiKey }) => {
|
|
21373
|
-
const cacheKey = `${id}\0${baseUrl}`;
|
|
22389
|
+
targets.map(async ({ id, cfg, baseUrl, apiKey, cacheKey }) => {
|
|
21374
22390
|
const provider = await discoverOpenAICompatibleModels(id, {
|
|
21375
22391
|
baseUrl,
|
|
21376
22392
|
apiKey,
|
|
@@ -21736,15 +22752,6 @@ async function createPreContextServices(input) {
|
|
|
21736
22752
|
logger.warn(`models.dev refresh failed (${toErrorMessage11(err)}); using cached catalog`);
|
|
21737
22753
|
}
|
|
21738
22754
|
}
|
|
21739
|
-
try {
|
|
21740
|
-
await installCatalogModelOutputLimits({
|
|
21741
|
-
registry: modelsRegistry,
|
|
21742
|
-
getConfig: () => config,
|
|
21743
|
-
log: (message) => logger.debug(message)
|
|
21744
|
-
});
|
|
21745
|
-
} catch (err) {
|
|
21746
|
-
logger.debug(`model output-limit index skipped: ${toErrorMessage11(err)}`);
|
|
21747
|
-
}
|
|
21748
22755
|
try {
|
|
21749
22756
|
await discoverAndMergeWebuiProviders({
|
|
21750
22757
|
config,
|
|
@@ -21755,6 +22762,15 @@ async function createPreContextServices(input) {
|
|
|
21755
22762
|
} catch (err) {
|
|
21756
22763
|
logger.debug(`provider auto-discovery skipped: ${toErrorMessage11(err)}`);
|
|
21757
22764
|
}
|
|
22765
|
+
try {
|
|
22766
|
+
await installCatalogModelOutputLimits({
|
|
22767
|
+
registry: modelsRegistry,
|
|
22768
|
+
getConfig: () => config,
|
|
22769
|
+
log: (message) => logger.debug(message)
|
|
22770
|
+
});
|
|
22771
|
+
} catch (err) {
|
|
22772
|
+
logger.debug(`model output-limit index skipped: ${toErrorMessage11(err)}`);
|
|
22773
|
+
}
|
|
21758
22774
|
const events = opts.services?.events ?? new EventBus();
|
|
21759
22775
|
events.setLogger(logger);
|
|
21760
22776
|
const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry, events });
|
|
@@ -21785,6 +22801,7 @@ async function createPreContextServices(input) {
|
|
|
21785
22801
|
registry: toolRegistry,
|
|
21786
22802
|
tier: normalizeTokenSavingTier(config.features.tokenSavingMode),
|
|
21787
22803
|
memory: { enabled: config.features.memory, store: memoryStore },
|
|
22804
|
+
nextSteps: { enabled: config.tools?.nextsteps?.enabled === true },
|
|
21788
22805
|
coordinationTools: [
|
|
21789
22806
|
makeMailboxTool({ projectDir: wpaths.projectDir, events }),
|
|
21790
22807
|
makeMailSendTool({ projectDir: wpaths.projectDir, events }),
|
|
@@ -22141,7 +23158,16 @@ function buildRoutes(state, deps2, cb) {
|
|
|
22141
23158
|
refineModel: (ws, msg) => modelOperations.refineModel(
|
|
22142
23159
|
ws,
|
|
22143
23160
|
msg.payload
|
|
22144
|
-
)
|
|
23161
|
+
),
|
|
23162
|
+
fallbackChoice: async (ws, msg) => {
|
|
23163
|
+
const result = emitFallbackChoice(deps2.events, msg);
|
|
23164
|
+
if (!result.ok) {
|
|
23165
|
+
send(ws, {
|
|
23166
|
+
type: "error",
|
|
23167
|
+
payload: { phase: "invalid_request", message: result.message }
|
|
23168
|
+
});
|
|
23169
|
+
}
|
|
23170
|
+
}
|
|
22145
23171
|
};
|
|
22146
23172
|
const sessionRoutes = createSessionHandlers({
|
|
22147
23173
|
config: state.getConfig(),
|
|
@@ -22319,8 +23345,10 @@ function buildRoutes(state, deps2, cb) {
|
|
|
22319
23345
|
});
|
|
22320
23346
|
const mcpRoutes = {
|
|
22321
23347
|
list: (ws, msg) => handleMcpList(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
22322
|
-
add
|
|
22323
|
-
|
|
23348
|
+
// add/update are the spawn-capable pair — they take a `command`/`args`
|
|
23349
|
+
// from the wire and start it. They go past the trust boundary (M1).
|
|
23350
|
+
add: (ws, msg) => handleMcpAdd(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry, deps2.trustBoundary),
|
|
23351
|
+
update: (ws, msg) => handleMcpUpdate(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry, deps2.trustBoundary),
|
|
22324
23352
|
remove: (ws, msg) => handleMcpRemove(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
22325
23353
|
enable: (ws, msg) => handleMcpEnable(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
22326
23354
|
disable: (ws, msg) => handleMcpDisable(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
@@ -22492,7 +23520,10 @@ function createWsServers(httpServer, ports, accessToken) {
|
|
|
22492
23520
|
expectedToken: wsToken,
|
|
22493
23521
|
requireToken: ports.requireToken,
|
|
22494
23522
|
allowedHostnames: publicHostnames,
|
|
22495
|
-
allowBrowserUrlToken: Boolean(ports.publicWsUrl)
|
|
23523
|
+
allowBrowserUrlToken: Boolean(ports.publicWsUrl),
|
|
23524
|
+
// WS-003 opt-out for the Vite dev loop only (app and WS server cannot
|
|
23525
|
+
// share a port). Off unless explicitly requested — see ws-auth.ts.
|
|
23526
|
+
allowCrossPortLoopbackCookie: process.env["WRONGSTACK_WEBUI_DEV_CROSS_PORT_WS"] === "1"
|
|
22496
23527
|
});
|
|
22497
23528
|
const WS_MAX_PAYLOAD = 20 * 1024 * 1024;
|
|
22498
23529
|
const wssPrimary = new WebSocketServer({
|
|
@@ -22958,7 +23989,8 @@ async function startWebUI(opts = {}) {
|
|
|
22958
23989
|
watcherMetricsRef
|
|
22959
23990
|
);
|
|
22960
23991
|
httpServer.listen(httpPort, wsHost, () => {
|
|
22961
|
-
|
|
23992
|
+
const tokenQuery = accessToken ? `/?token=${encodeURIComponent(accessToken)}` : "";
|
|
23993
|
+
console.log(`[WebUI] HTTP server running on http://${wsHost}:${httpPort}${tokenQuery}`);
|
|
22962
23994
|
const extraUrls = formatExternalAccessUrls({
|
|
22963
23995
|
bindHost: wsHost,
|
|
22964
23996
|
port: httpPort,
|
|
@@ -22980,8 +24012,11 @@ async function startWebUI(opts = {}) {
|
|
|
22980
24012
|
(req, socket, head2) => httpServer.emit("upgrade", req, socket, head2)
|
|
22981
24013
|
);
|
|
22982
24014
|
companionServer.on("error", (err) => {
|
|
22983
|
-
|
|
22984
|
-
|
|
24015
|
+
const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL" || err.code === "EADDRINUSE";
|
|
24016
|
+
if (!expected) {
|
|
24017
|
+
console.warn(
|
|
24018
|
+
`[WebUI] companion listener on ${companionLabel} failed (${err.code ?? "unknown"}): ${err.message}. The primary address is unaffected.`
|
|
24019
|
+
);
|
|
22985
24020
|
}
|
|
22986
24021
|
});
|
|
22987
24022
|
companionServer.listen(httpPort, companion, () => {
|
|
@@ -23223,24 +24258,23 @@ async function startWebUI(opts = {}) {
|
|
|
23223
24258
|
clients,
|
|
23224
24259
|
pendingConfirms,
|
|
23225
24260
|
onSecurityRejection: (ev) => {
|
|
23226
|
-
|
|
23227
|
-
|
|
23228
|
-
|
|
23229
|
-
|
|
23230
|
-
|
|
23231
|
-
|
|
23232
|
-
|
|
23233
|
-
body: `Decoder tripwire ${ev.issueCode}: ${ev.issueMessage}
|
|
24261
|
+
void mailbox.send({
|
|
24262
|
+
from: context.agentId,
|
|
24263
|
+
to: "*",
|
|
24264
|
+
type: "note",
|
|
24265
|
+
audience: "leaders",
|
|
24266
|
+
subject: `Security rejection: ${ev.issueCode}`,
|
|
24267
|
+
body: `Decoder tripwire ${ev.issueCode}: ${ev.issueMessage}
|
|
23234
24268
|
|
|
23235
24269
|
connectionId: ${ev.connectionId ?? "?"}
|
|
23236
24270
|
sessionId: ${ev.sessionId ?? "?"}
|
|
23237
24271
|
agentId: ${ev.agentId ?? "?"}
|
|
23238
24272
|
projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
23239
|
-
|
|
23240
|
-
|
|
23241
|
-
|
|
23242
|
-
|
|
23243
|
-
}
|
|
24273
|
+
priority: "high",
|
|
24274
|
+
senderSessionId: session.id
|
|
24275
|
+
}).catch((err) => {
|
|
24276
|
+
console.warn(`[WebUI] security-rejection mailbox note failed: ${String(err)}`);
|
|
24277
|
+
});
|
|
23244
24278
|
},
|
|
23245
24279
|
goalHandler,
|
|
23246
24280
|
specsHandler,
|