@wrongstack/webui-server 0.299.0 → 0.300.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 +577 -184
- package/dist/protocol/client-conversation.d.ts +1 -1
- package/dist/protocol/index.js +19 -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/server/context-editor.d.ts +9 -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 +499 -162
- package/dist/server/fallback-choice.d.ts +19 -0
- package/dist/server/http-server.d.ts +49 -0
- package/dist/server/instance-registry.d.ts +21 -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
|
@@ -77,6 +77,10 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
|
77
77
|
// Display-only toggles (purely visual, persisted in localStorage via Zustand).
|
|
78
78
|
"groupToolCalls",
|
|
79
79
|
"showThinkingLogs",
|
|
80
|
+
// v15: auto-collapse of the chat input under the history (opt-in display
|
|
81
|
+
// toggle, default off). Whitelisted so the key survives `prefs.update`
|
|
82
|
+
// round-trips without tripping the "unknown preference key" rejection.
|
|
83
|
+
"autoCollapseInput",
|
|
80
84
|
// v11 Display parity: inverse fsAccess flag.
|
|
81
85
|
"allowOutsideProjectRoot",
|
|
82
86
|
// v13 Display parity (TUI SettingsPicker fields 42 & 43): the read tool
|
|
@@ -401,6 +405,51 @@ function validateModelSwitchPayload(payload) {
|
|
|
401
405
|
}
|
|
402
406
|
};
|
|
403
407
|
}
|
|
408
|
+
function validateModelFallbackChoicePayload(payload) {
|
|
409
|
+
if (!isRecord2(payload)) {
|
|
410
|
+
return {
|
|
411
|
+
ok: false,
|
|
412
|
+
message: "model.fallback_choice payload must be an object"
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const requestId = payload["requestId"];
|
|
416
|
+
if (typeof requestId !== "string" || requestId.trim().length === 0) {
|
|
417
|
+
return {
|
|
418
|
+
ok: false,
|
|
419
|
+
message: "model.fallback_choice payload.requestId must be a non-empty string"
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const providerId = payload["providerId"];
|
|
423
|
+
const model = payload["model"];
|
|
424
|
+
const autoSwitch = payload["autoSwitch"];
|
|
425
|
+
if (providerId !== void 0 && typeof providerId !== "string") {
|
|
426
|
+
return {
|
|
427
|
+
ok: false,
|
|
428
|
+
message: "model.fallback_choice payload.providerId must be a string when provided"
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (model !== void 0 && typeof model !== "string") {
|
|
432
|
+
return {
|
|
433
|
+
ok: false,
|
|
434
|
+
message: "model.fallback_choice payload.model must be a string when provided"
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
if (autoSwitch !== void 0 && typeof autoSwitch !== "boolean") {
|
|
438
|
+
return {
|
|
439
|
+
ok: false,
|
|
440
|
+
message: "model.fallback_choice payload.autoSwitch must be a boolean when provided"
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
ok: true,
|
|
445
|
+
value: {
|
|
446
|
+
requestId: requestId.trim(),
|
|
447
|
+
...typeof providerId === "string" ? { providerId } : {},
|
|
448
|
+
...typeof model === "string" ? { model } : {},
|
|
449
|
+
...typeof autoSwitch === "boolean" ? { autoSwitch } : {}
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
404
453
|
var AUTONOMY_VALUES2 = /* @__PURE__ */ new Set(["off", "suggest", "auto", "eternal", "eternal-parallel"]);
|
|
405
454
|
function validateMailboxMessagesPayload(payload) {
|
|
406
455
|
if (payload === void 0) return { ok: true, value: void 0 };
|
|
@@ -4331,13 +4380,10 @@ function createConversationOperations(ctx) {
|
|
|
4331
4380
|
|
|
4332
4381
|
// src/server/context-editor.ts
|
|
4333
4382
|
import { createHash } from "node:crypto";
|
|
4334
|
-
import net from "node:net";
|
|
4335
4383
|
import {
|
|
4336
4384
|
ALLOWED_IMAGE_MEDIA_TYPES,
|
|
4337
4385
|
base64DecodedBytes,
|
|
4338
4386
|
isAllowedImageMediaType,
|
|
4339
|
-
isPrivateIPv4,
|
|
4340
|
-
isPrivateIPv6,
|
|
4341
4387
|
isValidImageBase64,
|
|
4342
4388
|
MAX_INCOMING_IMAGE_BYTES,
|
|
4343
4389
|
repairToolUseAdjacency
|
|
@@ -4405,40 +4451,7 @@ var REVISION_PREFIX = "wrongstack-context-editor-v1\0";
|
|
|
4405
4451
|
var MAX_MESSAGE_COUNT_GROWTH = 10;
|
|
4406
4452
|
var MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
|
|
4407
4453
|
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
|
-
}
|
|
4454
|
+
var MAX_REMOVAL_COUNT = 4096;
|
|
4442
4455
|
function isRecord3(value) {
|
|
4443
4456
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4444
4457
|
}
|
|
@@ -4447,7 +4460,7 @@ function canonicalize(value) {
|
|
|
4447
4460
|
if (isRecord3(value)) {
|
|
4448
4461
|
const sorted = {};
|
|
4449
4462
|
for (const key of Object.keys(value).sort()) {
|
|
4450
|
-
if (key === "_estTokens") continue;
|
|
4463
|
+
if (key === "_estTokens" || key === "_toolErrorInfo") continue;
|
|
4451
4464
|
const item = value[key];
|
|
4452
4465
|
if (item === void 0) continue;
|
|
4453
4466
|
sorted[key] = canonicalize(item);
|
|
@@ -4478,6 +4491,12 @@ function isMessageRole(value) {
|
|
|
4478
4491
|
function isPlainJsonObject(value) {
|
|
4479
4492
|
return isRecord3(value);
|
|
4480
4493
|
}
|
|
4494
|
+
function splitsSurrogatePair(text, offset) {
|
|
4495
|
+
if (offset <= 0 || offset >= text.length) return false;
|
|
4496
|
+
const previous = text.charCodeAt(offset - 1);
|
|
4497
|
+
const next = text.charCodeAt(offset);
|
|
4498
|
+
return previous >= 55296 && previous <= 56319 && next >= 56320 && next <= 57343;
|
|
4499
|
+
}
|
|
4481
4500
|
function validateCacheControl(value, path30, errors) {
|
|
4482
4501
|
if (value === void 0) return void 0;
|
|
4483
4502
|
if (!isRecord3(value) || value["type"] !== "ephemeral") {
|
|
@@ -4688,19 +4707,13 @@ function validateBlock(value, path30, errors) {
|
|
|
4688
4707
|
);
|
|
4689
4708
|
return void 0;
|
|
4690
4709
|
}
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
source: {
|
|
4699
|
-
type: "url",
|
|
4700
|
-
...typeof mediaType === "string" ? { media_type: mediaType } : {},
|
|
4701
|
-
url
|
|
4702
|
-
}
|
|
4703
|
-
};
|
|
4710
|
+
error(
|
|
4711
|
+
errors,
|
|
4712
|
+
`${path30}/source/url`,
|
|
4713
|
+
"UNSAFE_IMAGE_URL",
|
|
4714
|
+
"URL image sources are not allowed in context editor proposals; use an ingested base64 image."
|
|
4715
|
+
);
|
|
4716
|
+
return void 0;
|
|
4704
4717
|
}
|
|
4705
4718
|
case "thinking": {
|
|
4706
4719
|
const thinking = value["thinking"];
|
|
@@ -4857,6 +4870,14 @@ function warningsForMessage(message, index) {
|
|
|
4857
4870
|
message: "This block contains provider replay metadata and should only be removed with the whole turn if no longer needed."
|
|
4858
4871
|
});
|
|
4859
4872
|
}
|
|
4873
|
+
if (block.type === "image" && block.source.type === "url") {
|
|
4874
|
+
warnings.push({
|
|
4875
|
+
path: `/messages/${index}/content/${blockIndex}/source/url`,
|
|
4876
|
+
code: "UNSAFE_IMAGE_URL",
|
|
4877
|
+
severity: "danger",
|
|
4878
|
+
message: "URL image sources cannot be retained in context editor proposals; remove the whole message before applying other edits."
|
|
4879
|
+
});
|
|
4880
|
+
}
|
|
4860
4881
|
if (block.type === "tool_result" && block.content.length > 2e4) {
|
|
4861
4882
|
warnings.push({
|
|
4862
4883
|
path: `/messages/${index}/content/${blockIndex}`,
|
|
@@ -4884,6 +4905,21 @@ function metricFor(ctx, messages, tools) {
|
|
|
4884
4905
|
fullRequestTokens: breakdown.total
|
|
4885
4906
|
};
|
|
4886
4907
|
}
|
|
4908
|
+
function isToolResultMessage(message) {
|
|
4909
|
+
return Boolean(
|
|
4910
|
+
message?.role === "user" && Array.isArray(message.content) && message.content.length > 0 && message.content.every((block) => block.type === "tool_result")
|
|
4911
|
+
);
|
|
4912
|
+
}
|
|
4913
|
+
function pairedAssistantIndices(messages, userIndex) {
|
|
4914
|
+
if (messages[userIndex]?.role !== "user" || isToolResultMessage(messages[userIndex])) return [];
|
|
4915
|
+
const paired = [];
|
|
4916
|
+
for (let index = userIndex + 1; index < messages.length; index += 1) {
|
|
4917
|
+
const message = messages[index];
|
|
4918
|
+
if (message?.role === "user" && !isToolResultMessage(message)) break;
|
|
4919
|
+
if (message?.role === "assistant") paired.push(index);
|
|
4920
|
+
}
|
|
4921
|
+
return paired;
|
|
4922
|
+
}
|
|
4887
4923
|
function buildContextEditorSnapshot(ctx, tools) {
|
|
4888
4924
|
const messages = ctx.messages.map(
|
|
4889
4925
|
(message) => ({
|
|
@@ -4913,11 +4949,165 @@ function buildContextEditorSnapshot(ctx, tools) {
|
|
|
4913
4949
|
tokens: messageTokens(message.content),
|
|
4914
4950
|
preview: breakdown.messages.breakdown[index]?.preview ?? "",
|
|
4915
4951
|
blockCount: Array.isArray(message.content) ? message.content.length : null,
|
|
4916
|
-
warnings: warningsForMessage(message, index)
|
|
4952
|
+
warnings: warningsForMessage(message, index),
|
|
4953
|
+
pairedAssistantIndices: pairedAssistantIndices(ctx.messages, index)
|
|
4917
4954
|
})),
|
|
4918
4955
|
diagnostics: toolDiagnostics(ctx.messages)
|
|
4919
4956
|
};
|
|
4920
4957
|
}
|
|
4958
|
+
function validateRemovalPlan(value, originalMessages, proposedMessages) {
|
|
4959
|
+
const errors = [];
|
|
4960
|
+
if (value === void 0) {
|
|
4961
|
+
error(
|
|
4962
|
+
errors,
|
|
4963
|
+
"/removals",
|
|
4964
|
+
"REMOVAL_PLAN_REQUIRED",
|
|
4965
|
+
"A removal plan is required for every context editor proposal."
|
|
4966
|
+
);
|
|
4967
|
+
return { errors };
|
|
4968
|
+
}
|
|
4969
|
+
if (!Array.isArray(value)) {
|
|
4970
|
+
error(errors, "/removals", "INVALID_REMOVALS", "removals must be an array.");
|
|
4971
|
+
return { errors };
|
|
4972
|
+
}
|
|
4973
|
+
if (value.length > MAX_REMOVAL_COUNT) {
|
|
4974
|
+
error(
|
|
4975
|
+
errors,
|
|
4976
|
+
"/removals",
|
|
4977
|
+
"TOO_MANY_REMOVALS",
|
|
4978
|
+
`removals must contain at most ${MAX_REMOVAL_COUNT} entries.`
|
|
4979
|
+
);
|
|
4980
|
+
return { errors };
|
|
4981
|
+
}
|
|
4982
|
+
const wholeMessages = /* @__PURE__ */ new Set();
|
|
4983
|
+
const touchedUsers = /* @__PURE__ */ new Set();
|
|
4984
|
+
const ranges = [];
|
|
4985
|
+
for (const [removalIndex, raw] of value.entries()) {
|
|
4986
|
+
const path30 = `/removals/${removalIndex}`;
|
|
4987
|
+
if (!isRecord3(raw) || !Number.isInteger(raw["messageIndex"])) {
|
|
4988
|
+
error(errors, path30, "INVALID_REMOVAL", "Removal must include an integer messageIndex.");
|
|
4989
|
+
continue;
|
|
4990
|
+
}
|
|
4991
|
+
const messageIndex = raw["messageIndex"];
|
|
4992
|
+
const original = originalMessages[messageIndex];
|
|
4993
|
+
if (!original) {
|
|
4994
|
+
error(errors, `${path30}/messageIndex`, "INVALID_MESSAGE_INDEX", "Removal messageIndex is out of range.");
|
|
4995
|
+
continue;
|
|
4996
|
+
}
|
|
4997
|
+
const start = raw["start"];
|
|
4998
|
+
const end = raw["end"];
|
|
4999
|
+
const blockIndex = raw["blockIndex"];
|
|
5000
|
+
if (start === void 0 && end === void 0 && blockIndex === void 0) {
|
|
5001
|
+
wholeMessages.add(messageIndex);
|
|
5002
|
+
if (original.role === "user") touchedUsers.add(messageIndex);
|
|
5003
|
+
continue;
|
|
5004
|
+
}
|
|
5005
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start) {
|
|
5006
|
+
error(errors, path30, "INVALID_RANGE", "Range removal requires integer start/end with 0 <= start < end.");
|
|
5007
|
+
continue;
|
|
5008
|
+
}
|
|
5009
|
+
let text;
|
|
5010
|
+
if (blockIndex === void 0 && typeof original.content === "string") text = original.content;
|
|
5011
|
+
if (Number.isInteger(blockIndex) && Array.isArray(original.content)) {
|
|
5012
|
+
const block = original.content[blockIndex];
|
|
5013
|
+
if (block?.type === "text") text = block.text;
|
|
5014
|
+
}
|
|
5015
|
+
if (text === void 0 || end > text.length) {
|
|
5016
|
+
error(errors, path30, "INVALID_RANGE_TARGET", "Range must target existing string or text-block content.");
|
|
5017
|
+
continue;
|
|
5018
|
+
}
|
|
5019
|
+
if (splitsSurrogatePair(text, start) || splitsSurrogatePair(text, end)) {
|
|
5020
|
+
error(
|
|
5021
|
+
errors,
|
|
5022
|
+
path30,
|
|
5023
|
+
"INVALID_UNICODE_RANGE",
|
|
5024
|
+
"Range boundaries must not split a Unicode surrogate pair."
|
|
5025
|
+
);
|
|
5026
|
+
continue;
|
|
5027
|
+
}
|
|
5028
|
+
ranges.push({
|
|
5029
|
+
messageIndex,
|
|
5030
|
+
...blockIndex === void 0 ? {} : { blockIndex },
|
|
5031
|
+
start,
|
|
5032
|
+
end
|
|
5033
|
+
});
|
|
5034
|
+
if (original.role === "user") touchedUsers.add(messageIndex);
|
|
5035
|
+
}
|
|
5036
|
+
const rangesByTarget = /* @__PURE__ */ new Map();
|
|
5037
|
+
for (const range of ranges) {
|
|
5038
|
+
const key = `${range.messageIndex}:${range.blockIndex ?? "string"}`;
|
|
5039
|
+
const targetRanges = rangesByTarget.get(key) ?? [];
|
|
5040
|
+
targetRanges.push(range);
|
|
5041
|
+
rangesByTarget.set(key, targetRanges);
|
|
5042
|
+
}
|
|
5043
|
+
for (const targetRanges of rangesByTarget.values()) {
|
|
5044
|
+
targetRanges.sort((left, right) => (left.start ?? 0) - (right.start ?? 0));
|
|
5045
|
+
for (let index = 1; index < targetRanges.length; index += 1) {
|
|
5046
|
+
const previous = targetRanges[index - 1];
|
|
5047
|
+
const current2 = targetRanges[index];
|
|
5048
|
+
if (previous?.end !== void 0 && current2?.start !== void 0 && current2.start < previous.end) {
|
|
5049
|
+
error(
|
|
5050
|
+
errors,
|
|
5051
|
+
"/removals",
|
|
5052
|
+
"OVERLAPPING_RANGES",
|
|
5053
|
+
"Removal ranges targeting the same text must not overlap."
|
|
5054
|
+
);
|
|
5055
|
+
break;
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
for (const userIndex of touchedUsers) {
|
|
5060
|
+
for (const assistantIndex of pairedAssistantIndices(originalMessages, userIndex)) {
|
|
5061
|
+
if (wholeMessages.has(assistantIndex)) continue;
|
|
5062
|
+
error(
|
|
5063
|
+
errors,
|
|
5064
|
+
"/removals",
|
|
5065
|
+
"MISSING_ASSISTANT_PAIR",
|
|
5066
|
+
`Editing user message ${userIndex} must also remove assistant message ${assistantIndex}.`
|
|
5067
|
+
);
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
const expectedMessages = structuredClone(originalMessages);
|
|
5071
|
+
for (const targetRanges of rangesByTarget.values()) {
|
|
5072
|
+
const first = targetRanges[0];
|
|
5073
|
+
if (!first) continue;
|
|
5074
|
+
const message = expectedMessages[first.messageIndex];
|
|
5075
|
+
if (!message) continue;
|
|
5076
|
+
let text;
|
|
5077
|
+
if (first.blockIndex === void 0 && typeof message.content === "string") {
|
|
5078
|
+
text = message.content;
|
|
5079
|
+
} else if (first.blockIndex !== void 0 && Array.isArray(message.content)) {
|
|
5080
|
+
const block = message.content[first.blockIndex];
|
|
5081
|
+
if (block?.type === "text") text = block.text;
|
|
5082
|
+
}
|
|
5083
|
+
if (text === void 0) continue;
|
|
5084
|
+
const pieces = [];
|
|
5085
|
+
let cursor = 0;
|
|
5086
|
+
for (const range of targetRanges) {
|
|
5087
|
+
if (range.start === void 0 || range.end === void 0) continue;
|
|
5088
|
+
pieces.push(text.slice(cursor, range.start));
|
|
5089
|
+
cursor = range.end;
|
|
5090
|
+
}
|
|
5091
|
+
pieces.push(text.slice(cursor));
|
|
5092
|
+
const nextText = pieces.join("");
|
|
5093
|
+
if (first.blockIndex === void 0 && typeof message.content === "string") {
|
|
5094
|
+
message.content = nextText;
|
|
5095
|
+
} else if (first.blockIndex !== void 0 && Array.isArray(message.content)) {
|
|
5096
|
+
const block = message.content[first.blockIndex];
|
|
5097
|
+
if (block?.type === "text") block.text = nextText;
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
const expectedProposal = expectedMessages.filter((_, index) => !wholeMessages.has(index));
|
|
5101
|
+
if (JSON.stringify(canonicalize(expectedProposal)) !== JSON.stringify(canonicalize(proposedMessages))) {
|
|
5102
|
+
error(
|
|
5103
|
+
errors,
|
|
5104
|
+
"/messages",
|
|
5105
|
+
"REMOVAL_PLAN_MISMATCH",
|
|
5106
|
+
"Submitted messages do not exactly match the declared removal plan."
|
|
5107
|
+
);
|
|
5108
|
+
}
|
|
5109
|
+
return errors.length > 0 ? { errors } : { errors, messages: expectedProposal };
|
|
5110
|
+
}
|
|
4921
5111
|
function validateContextEditorProposal(input) {
|
|
4922
5112
|
const currentRevision = contextEditorRevision(input.ctx.messages);
|
|
4923
5113
|
const before = metricFor(input.ctx, input.ctx.messages, input.tools);
|
|
@@ -4969,7 +5159,23 @@ function validateContextEditorProposal(input) {
|
|
|
4969
5159
|
repair: emptyRepair
|
|
4970
5160
|
};
|
|
4971
5161
|
}
|
|
4972
|
-
const
|
|
5162
|
+
const removalPlan = validateRemovalPlan(
|
|
5163
|
+
input.removals,
|
|
5164
|
+
input.ctx.messages,
|
|
5165
|
+
parsed.messages
|
|
5166
|
+
);
|
|
5167
|
+
if (removalPlan.errors.length > 0 || !removalPlan.messages) {
|
|
5168
|
+
return {
|
|
5169
|
+
ok: false,
|
|
5170
|
+
baseRevision: input.baseRevision,
|
|
5171
|
+
currentRevision,
|
|
5172
|
+
before,
|
|
5173
|
+
validationErrors: removalPlan.errors,
|
|
5174
|
+
warnings: [],
|
|
5175
|
+
repair: emptyRepair
|
|
5176
|
+
};
|
|
5177
|
+
}
|
|
5178
|
+
const repaired = repairToolUseAdjacency(removalPlan.messages);
|
|
4973
5179
|
const repair = {
|
|
4974
5180
|
changed: repaired.report.changed,
|
|
4975
5181
|
removedToolUses: repaired.report.removedToolUses,
|
|
@@ -8485,19 +8691,22 @@ function extractToken(url) {
|
|
|
8485
8691
|
function extractTokenFromCookie(cookieHeader) {
|
|
8486
8692
|
if (!cookieHeader) return void 0;
|
|
8487
8693
|
const raw = Array.isArray(cookieHeader) ? cookieHeader.join("; ") : cookieHeader;
|
|
8694
|
+
let plain;
|
|
8488
8695
|
for (const part of raw.split(";")) {
|
|
8489
8696
|
const eq = part.indexOf("=");
|
|
8490
8697
|
if (eq < 0) continue;
|
|
8491
8698
|
const name2 = part.slice(0, eq).trim();
|
|
8492
|
-
if (name2
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8699
|
+
if (name2 !== "ws_token" && name2 !== "__Host-ws_token") continue;
|
|
8700
|
+
let value;
|
|
8701
|
+
try {
|
|
8702
|
+
value = decodeURIComponent(part.slice(eq + 1).trim());
|
|
8703
|
+
} catch {
|
|
8704
|
+
value = part.slice(eq + 1).trim();
|
|
8498
8705
|
}
|
|
8706
|
+
if (name2 === "__Host-ws_token") return value;
|
|
8707
|
+
plain ??= value;
|
|
8499
8708
|
}
|
|
8500
|
-
return
|
|
8709
|
+
return plain;
|
|
8501
8710
|
}
|
|
8502
8711
|
function hostHeaderOk(input) {
|
|
8503
8712
|
if (!isLoopbackBind(input.wsHost)) return true;
|
|
@@ -8539,7 +8748,8 @@ function verifyClient(input) {
|
|
|
8539
8748
|
expectedToken,
|
|
8540
8749
|
requireToken,
|
|
8541
8750
|
allowedHostnames,
|
|
8542
|
-
allowBrowserUrlToken
|
|
8751
|
+
allowBrowserUrlToken,
|
|
8752
|
+
allowCrossPortLoopbackCookie
|
|
8543
8753
|
} = input;
|
|
8544
8754
|
const urlTokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
|
|
8545
8755
|
const cookieTokenOk = tokenMatches(extractTokenFromCookie(cookieHeader), expectedToken);
|
|
@@ -8554,7 +8764,10 @@ function verifyClient(input) {
|
|
|
8554
8764
|
const { hostname: originHostname } = new URL(origin);
|
|
8555
8765
|
if (isLoopbackHostname(originHostname)) {
|
|
8556
8766
|
if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
|
|
8557
|
-
|
|
8767
|
+
if (!isTrustedLoopbackOrigin(origin, hostHeader)) {
|
|
8768
|
+
return Boolean(allowCrossPortLoopbackCookie) && cookieTokenOk;
|
|
8769
|
+
}
|
|
8770
|
+
return true;
|
|
8558
8771
|
}
|
|
8559
8772
|
return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
|
|
8560
8773
|
} catch {
|
|
@@ -8589,11 +8802,22 @@ ${out}`;
|
|
|
8589
8802
|
function firstHeader(value) {
|
|
8590
8803
|
return Array.isArray(value) ? value[0] : value;
|
|
8591
8804
|
}
|
|
8592
|
-
|
|
8593
|
-
|
|
8805
|
+
var WS_TOKEN_COOKIE = "ws_token";
|
|
8806
|
+
var WS_TOKEN_COOKIE_SECURE = "__Host-ws_token";
|
|
8807
|
+
function wsTokenCookie(token, secure) {
|
|
8808
|
+
const name2 = secure ? WS_TOKEN_COOKIE_SECURE : WS_TOKEN_COOKIE;
|
|
8809
|
+
const parts = [
|
|
8810
|
+
`${name2}=${encodeURIComponent(token)}`,
|
|
8811
|
+
"HttpOnly",
|
|
8812
|
+
"SameSite=Strict",
|
|
8813
|
+
"Path=/",
|
|
8814
|
+
"Max-Age=3600"
|
|
8815
|
+
];
|
|
8816
|
+
if (secure) parts.push("Secure");
|
|
8817
|
+
return parts.join("; ");
|
|
8594
8818
|
}
|
|
8595
|
-
function setAuthCookieHeaders(res, token) {
|
|
8596
|
-
res.setHeader("Set-Cookie", wsTokenCookie(token));
|
|
8819
|
+
function setAuthCookieHeaders(res, token, secure) {
|
|
8820
|
+
res.setHeader("Set-Cookie", wsTokenCookie(token, secure));
|
|
8597
8821
|
res.setHeader("Cache-Control", "no-store");
|
|
8598
8822
|
}
|
|
8599
8823
|
function setStaticSecurityHeaders(res) {
|
|
@@ -8601,8 +8825,16 @@ function setStaticSecurityHeaders(res) {
|
|
|
8601
8825
|
res.setHeader("X-Frame-Options", "DENY");
|
|
8602
8826
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
8603
8827
|
}
|
|
8604
|
-
function requestToken(req, url) {
|
|
8605
|
-
|
|
8828
|
+
function requestToken(req, url, opts = {}) {
|
|
8829
|
+
const queryToken = url.searchParams.get("token") ?? void 0;
|
|
8830
|
+
if (queryToken !== void 0 && (opts.allowQuery === true || isLoopbackPeer(req))) {
|
|
8831
|
+
return queryToken;
|
|
8832
|
+
}
|
|
8833
|
+
return firstHeader(req.headers["x-ws-token"]) ?? extractTokenFromCookie(req.headers.cookie);
|
|
8834
|
+
}
|
|
8835
|
+
function isLoopbackPeer(req) {
|
|
8836
|
+
const address = req.socket.remoteAddress?.replace(/^::ffff:/i, "");
|
|
8837
|
+
return address !== void 0 && isLoopbackHostname(address);
|
|
8606
8838
|
}
|
|
8607
8839
|
function formatCspHostname(hostname) {
|
|
8608
8840
|
return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
@@ -8657,7 +8889,8 @@ function strictDecodeParam(segment, res) {
|
|
|
8657
8889
|
function createHttpServer(opts) {
|
|
8658
8890
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
8659
8891
|
const distDir = path13.resolve(opts.distDir);
|
|
8660
|
-
const requireAccessToken =
|
|
8892
|
+
const requireAccessToken = true;
|
|
8893
|
+
const secureCookies = opts.secureCookies ?? (opts.publicWsUrl?.trim().toLowerCase().startsWith("wss:") ?? false);
|
|
8661
8894
|
const trustedHostnames = (() => {
|
|
8662
8895
|
const names = [...opts.allowedHostnames ?? []];
|
|
8663
8896
|
if (opts.publicWsUrl) {
|
|
@@ -8696,13 +8929,13 @@ function createHttpServer(opts) {
|
|
|
8696
8929
|
const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
|
|
8697
8930
|
const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
|
|
8698
8931
|
if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
|
|
8699
|
-
const provided = requestToken(req, url);
|
|
8932
|
+
const provided = requestToken(req, url, { allowQuery: true });
|
|
8700
8933
|
if (!provided || !opts.apiToken || !tokenMatches(provided, opts.apiToken)) {
|
|
8701
8934
|
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
8702
8935
|
res.end("Unauthorized");
|
|
8703
8936
|
return;
|
|
8704
8937
|
}
|
|
8705
|
-
setAuthCookieHeaders(res, opts.apiToken);
|
|
8938
|
+
setAuthCookieHeaders(res, opts.apiToken, secureCookies);
|
|
8706
8939
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
8707
8940
|
res.end("ok");
|
|
8708
8941
|
return;
|
|
@@ -8716,7 +8949,7 @@ function createHttpServer(opts) {
|
|
|
8716
8949
|
return;
|
|
8717
8950
|
}
|
|
8718
8951
|
if (shouldSetAuthCookie && opts.apiToken) {
|
|
8719
|
-
setAuthCookieHeaders(res, opts.apiToken);
|
|
8952
|
+
setAuthCookieHeaders(res, opts.apiToken, secureCookies);
|
|
8720
8953
|
}
|
|
8721
8954
|
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
8722
8955
|
if (requireAccessToken && !accessTokenOk) {
|
|
@@ -11359,6 +11592,55 @@ import {
|
|
|
11359
11592
|
restartMcp,
|
|
11360
11593
|
updateMcp
|
|
11361
11594
|
} from "@wrongstack/mcp";
|
|
11595
|
+
|
|
11596
|
+
// src/server/privileged-actions.ts
|
|
11597
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
11598
|
+
import {
|
|
11599
|
+
isTrustDecisionAllowed
|
|
11600
|
+
} from "@wrongstack/core/security";
|
|
11601
|
+
async function authorizeWebUIAction(boundary, action, logger) {
|
|
11602
|
+
const request = {
|
|
11603
|
+
version: 1,
|
|
11604
|
+
requestId: randomUUID3(),
|
|
11605
|
+
actor: {
|
|
11606
|
+
kind: "remote-client",
|
|
11607
|
+
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
11608
|
+
},
|
|
11609
|
+
surface: "webui",
|
|
11610
|
+
capability: action.capability,
|
|
11611
|
+
subject: action.subject,
|
|
11612
|
+
risk: action.risk,
|
|
11613
|
+
scope: {
|
|
11614
|
+
...action.cwd ? { cwd: action.cwd } : {},
|
|
11615
|
+
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
11616
|
+
},
|
|
11617
|
+
authContext: { method: "session" },
|
|
11618
|
+
...action.metadata ? { metadata: action.metadata } : {}
|
|
11619
|
+
};
|
|
11620
|
+
const decision = await boundary.evaluate(request);
|
|
11621
|
+
logger?.debug?.(
|
|
11622
|
+
`[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
|
|
11623
|
+
);
|
|
11624
|
+
return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
|
|
11625
|
+
}
|
|
11626
|
+
|
|
11627
|
+
// src/server/mcp-handlers.ts
|
|
11628
|
+
async function authorizeMcpMutation(ws, operation, serverName, trustBoundary) {
|
|
11629
|
+
if (!trustBoundary) return true;
|
|
11630
|
+
const authorization = await authorizeWebUIAction(trustBoundary, {
|
|
11631
|
+
capability: "mcp.server.configure",
|
|
11632
|
+
subject: { kind: "process", id: serverName },
|
|
11633
|
+
risk: "elevated",
|
|
11634
|
+
metadata: { transport: "websocket", operation }
|
|
11635
|
+
});
|
|
11636
|
+
if (!authorization.allowed) {
|
|
11637
|
+
send(ws, {
|
|
11638
|
+
type: "mcp.operation_result",
|
|
11639
|
+
payload: { success: false, message: `${operation} denied: ${authorization.reason}` }
|
|
11640
|
+
});
|
|
11641
|
+
}
|
|
11642
|
+
return authorization.allowed;
|
|
11643
|
+
}
|
|
11362
11644
|
function mapStatus(raw) {
|
|
11363
11645
|
switch (raw) {
|
|
11364
11646
|
case "connected":
|
|
@@ -11433,7 +11715,7 @@ async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
|
|
|
11433
11715
|
payload: { servers: servers.map((server) => toView(server, health.get(server.name))) }
|
|
11434
11716
|
});
|
|
11435
11717
|
}
|
|
11436
|
-
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
11718
|
+
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry, trustBoundary) {
|
|
11437
11719
|
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
11438
11720
|
if (!d) return;
|
|
11439
11721
|
const validated = validateMcpServerPayload(msg.payload, "mcp.add");
|
|
@@ -11444,6 +11726,7 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
11444
11726
|
});
|
|
11445
11727
|
return;
|
|
11446
11728
|
}
|
|
11729
|
+
if (!await authorizeMcpMutation(ws, "mcp.add", name(msg), trustBoundary)) return;
|
|
11447
11730
|
const result = await addMcp(validated.value, d);
|
|
11448
11731
|
if (result.ok && result.server) {
|
|
11449
11732
|
send(ws, { type: "mcp.server.added", payload: { server: toView(result.server) } });
|
|
@@ -11461,7 +11744,7 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
11461
11744
|
payload: { success: result.ok, message: result.message }
|
|
11462
11745
|
});
|
|
11463
11746
|
}
|
|
11464
|
-
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
11747
|
+
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry, trustBoundary) {
|
|
11465
11748
|
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
11466
11749
|
if (!d) return;
|
|
11467
11750
|
const validated = validateMcpServerPayload(msg.payload, "mcp.update");
|
|
@@ -11472,6 +11755,7 @@ async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
11472
11755
|
});
|
|
11473
11756
|
return;
|
|
11474
11757
|
}
|
|
11758
|
+
if (!await authorizeMcpMutation(ws, "mcp.update", name(msg), trustBoundary)) return;
|
|
11475
11759
|
const result = await updateMcp(validated.value, d);
|
|
11476
11760
|
if (result.ok && result.server) {
|
|
11477
11761
|
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
@@ -12572,11 +12856,11 @@ function createModelOperations(context) {
|
|
|
12572
12856
|
}
|
|
12573
12857
|
|
|
12574
12858
|
// src/server/port-utils.ts
|
|
12575
|
-
import * as
|
|
12859
|
+
import * as net from "node:net";
|
|
12576
12860
|
import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
12577
12861
|
function isPortFree(host, port) {
|
|
12578
12862
|
return new Promise((resolve15) => {
|
|
12579
|
-
const srv =
|
|
12863
|
+
const srv = net.createServer();
|
|
12580
12864
|
srv.once("error", () => resolve15(false));
|
|
12581
12865
|
srv.once("listening", () => {
|
|
12582
12866
|
srv.close(() => resolve15(true));
|
|
@@ -12821,6 +13105,8 @@ async function handleBrainAsk(ctx, ws, question) {
|
|
|
12821
13105
|
|
|
12822
13106
|
// src/server/context-meta.ts
|
|
12823
13107
|
import { FallbackProfileManager } from "@wrongstack/core/agent";
|
|
13108
|
+
import { resolvePluginEnablement } from "@wrongstack/core/plugin";
|
|
13109
|
+
import { FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
|
|
12824
13110
|
function seedContextMeta(config, context) {
|
|
12825
13111
|
const meta = context.meta;
|
|
12826
13112
|
const autonomyCfg = config.autonomy ?? {};
|
|
@@ -12897,6 +13183,21 @@ function seedContextMeta(config, context) {
|
|
|
12897
13183
|
meta["tgDelegate"] = tgExt?.["notifyOnDelegate"] !== false;
|
|
12898
13184
|
const tgMs = tgExt?.["longToolThresholdMs"];
|
|
12899
13185
|
meta["tgLongToolMs"] = typeof tgMs === "number" ? tgMs : 3e4;
|
|
13186
|
+
{
|
|
13187
|
+
const pluginsEnabled = {};
|
|
13188
|
+
const record = (name2) => {
|
|
13189
|
+
if (FORBIDDEN_PROTO_KEYS2.has(name2) || name2 in pluginsEnabled) return;
|
|
13190
|
+
pluginsEnabled[name2] = resolvePluginEnablement({ name: name2, config }).enabled;
|
|
13191
|
+
};
|
|
13192
|
+
for (const entry of config.plugins ?? []) {
|
|
13193
|
+
const name2 = typeof entry === "string" ? entry : entry?.name;
|
|
13194
|
+
if (typeof name2 === "string") record(name2);
|
|
13195
|
+
}
|
|
13196
|
+
for (const [name2, options] of Object.entries(config.extensions ?? {})) {
|
|
13197
|
+
if (typeof options?.["enabled"] === "boolean") record(name2);
|
|
13198
|
+
}
|
|
13199
|
+
if (Object.keys(pluginsEnabled).length > 0) meta["pluginsEnabled"] = pluginsEnabled;
|
|
13200
|
+
}
|
|
12900
13201
|
const chimeraExt = config.extensions?.["wstack-chimera"];
|
|
12901
13202
|
meta["chimeraEnabled"] = chimeraExt?.["enabled"] === true;
|
|
12902
13203
|
meta["chimeraProvider"] = chimeraExt?.["provider"] ?? "";
|
|
@@ -12934,8 +13235,9 @@ function seedContextMeta(config, context) {
|
|
|
12934
13235
|
// src/server/pref-helpers.ts
|
|
12935
13236
|
import * as fs13 from "node:fs/promises";
|
|
12936
13237
|
import * as path15 from "node:path";
|
|
13238
|
+
import { pluginEntryMatchesName } from "@wrongstack/core/plugin";
|
|
12937
13239
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
12938
|
-
import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as
|
|
13240
|
+
import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS3 } from "@wrongstack/core/utils";
|
|
12939
13241
|
var PREF_KEYS = [
|
|
12940
13242
|
"autonomy",
|
|
12941
13243
|
"autonomyDelayMs",
|
|
@@ -13016,6 +13318,8 @@ var PREF_KEYS = [
|
|
|
13016
13318
|
// Display-only toggles (purely visual WebUI prefs, not persisted to config).
|
|
13017
13319
|
"groupToolCalls",
|
|
13018
13320
|
"showThinkingLogs",
|
|
13321
|
+
// v15: chat-input auto-collapse (opt-in display toggle, default off).
|
|
13322
|
+
"autoCollapseInput",
|
|
13019
13323
|
// Per-plugin enable/disable map (parity with the embedded server).
|
|
13020
13324
|
"pluginsEnabled",
|
|
13021
13325
|
// Fleet chat verbosity: off | full (migrated from streamFleet boolean).
|
|
@@ -13070,6 +13374,8 @@ async function updateGlobalConfig(deps2, holder, mutate, errorLabel) {
|
|
|
13070
13374
|
var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set([
|
|
13071
13375
|
"groupToolCalls",
|
|
13072
13376
|
"showThinkingLogs",
|
|
13377
|
+
// v15: chat-input auto-collapse (opt-in display toggle, default off).
|
|
13378
|
+
"autoCollapseInput",
|
|
13073
13379
|
"autoReviewFallbackModels",
|
|
13074
13380
|
// v11 Display parity: agent-swarm panel + inverse fsAccess flag.
|
|
13075
13381
|
// The TUI settings picker mirrors these so the browser can keep the
|
|
@@ -13280,15 +13586,29 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
13280
13586
|
decrypted.debugStream = payload["debugStream"];
|
|
13281
13587
|
if (typeof payload["pluginsEnabled"] === "object" && payload["pluginsEnabled"] !== null) {
|
|
13282
13588
|
const ext = decrypted.extensions ?? {};
|
|
13589
|
+
const toggled = [];
|
|
13283
13590
|
for (const [pluginName, enabled] of Object.entries(
|
|
13284
13591
|
payload["pluginsEnabled"]
|
|
13285
13592
|
)) {
|
|
13286
|
-
if (
|
|
13593
|
+
if (FORBIDDEN_PROTO_KEYS3.has(pluginName)) continue;
|
|
13594
|
+
if (typeof enabled !== "boolean") continue;
|
|
13287
13595
|
const pExt = ext[pluginName] ?? {};
|
|
13288
13596
|
pExt["enabled"] = enabled;
|
|
13289
13597
|
ext[pluginName] = pExt;
|
|
13598
|
+
toggled.push([pluginName, enabled]);
|
|
13290
13599
|
}
|
|
13291
13600
|
decrypted.extensions = ext;
|
|
13601
|
+
if (Array.isArray(decrypted.plugins) && toggled.length > 0) {
|
|
13602
|
+
decrypted.plugins = decrypted.plugins.map((entry) => {
|
|
13603
|
+
const entryName = typeof entry === "string" ? entry : entry?.name;
|
|
13604
|
+
if (typeof entryName !== "string") return entry;
|
|
13605
|
+
const hit = toggled.find(([name2]) => pluginEntryMatchesName(entryName, name2));
|
|
13606
|
+
if (!hit) return entry;
|
|
13607
|
+
const [, enabled] = hit;
|
|
13608
|
+
if (typeof entry === "string") return enabled ? entry : { name: entry, enabled: false };
|
|
13609
|
+
return { ...entry, enabled };
|
|
13610
|
+
});
|
|
13611
|
+
}
|
|
13292
13612
|
}
|
|
13293
13613
|
const chimeraTouched = typeof payload["chimeraEnabled"] === "boolean" || typeof payload["chimeraProvider"] === "string" || typeof payload["chimeraModel"] === "string" || typeof payload["chimeraMaxFiles"] === "number" || typeof payload["chimeraAutoFix"] === "string";
|
|
13294
13614
|
if (chimeraTouched) {
|
|
@@ -13397,39 +13717,6 @@ async function handlePrefsRoute(ws, msg, handlers) {
|
|
|
13397
13717
|
// src/server/process-handlers.ts
|
|
13398
13718
|
import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
|
|
13399
13719
|
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
13720
|
function handleProcessList(ws) {
|
|
13434
13721
|
try {
|
|
13435
13722
|
const procs = getProcessRegistry2().list();
|
|
@@ -14245,8 +14532,10 @@ function createProviderOperations(deps2) {
|
|
|
14245
14532
|
if (result.ok) {
|
|
14246
14533
|
deps2.log?.(`[WebUI] Provider "${payload.id}" added via provider.add`);
|
|
14247
14534
|
}
|
|
14535
|
+
return result.ok;
|
|
14248
14536
|
} catch (err) {
|
|
14249
14537
|
sendOperationResult(ws, false, errMessage(err));
|
|
14538
|
+
return false;
|
|
14250
14539
|
}
|
|
14251
14540
|
}
|
|
14252
14541
|
async function handleProviderRemove(ws, providerId) {
|
|
@@ -14567,6 +14856,7 @@ var CLIENT_CONVERSATION_MESSAGE_TYPES = [
|
|
|
14567
14856
|
"completion.request",
|
|
14568
14857
|
"model.switch",
|
|
14569
14858
|
"model.refine",
|
|
14859
|
+
"model.fallback_choice",
|
|
14570
14860
|
"autonomy.switch",
|
|
14571
14861
|
"context.clear",
|
|
14572
14862
|
"context.compact",
|
|
@@ -14838,6 +15128,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
|
|
|
14838
15128
|
"provider.active_blocked",
|
|
14839
15129
|
"provider.error",
|
|
14840
15130
|
"provider.fallback",
|
|
15131
|
+
"provider.fallback_pending",
|
|
14841
15132
|
"provider.response",
|
|
14842
15133
|
"provider.retry",
|
|
14843
15134
|
"provider.status_changed",
|
|
@@ -14879,6 +15170,7 @@ var SERVER_COLLABORATION_MESSAGE_TYPES = [
|
|
|
14879
15170
|
"collab.state",
|
|
14880
15171
|
"mailbox.action_result",
|
|
14881
15172
|
"mailbox.agent_registered",
|
|
15173
|
+
"mailbox.agent_deregistered",
|
|
14882
15174
|
"mailbox.agents",
|
|
14883
15175
|
"mailbox.cleared",
|
|
14884
15176
|
"mailbox.compacted",
|
|
@@ -15746,6 +16038,7 @@ function createSessionHandlers(ctx) {
|
|
|
15746
16038
|
tools: ctx.listTools?.() ?? ctx.toolRegistry?.list(),
|
|
15747
16039
|
baseRevision: typeof payload["baseRevision"] === "string" ? payload["baseRevision"] : "",
|
|
15748
16040
|
messages: payload["messages"],
|
|
16041
|
+
removals: payload["removals"],
|
|
15749
16042
|
allowRepair: payload["allowRepair"] === true,
|
|
15750
16043
|
runActive: ctx.isRunActive?.() === true
|
|
15751
16044
|
});
|
|
@@ -15762,6 +16055,7 @@ function createSessionHandlers(ctx) {
|
|
|
15762
16055
|
tools: ctx.listTools?.() ?? ctx.toolRegistry?.list(),
|
|
15763
16056
|
baseRevision: typeof payload["baseRevision"] === "string" ? payload["baseRevision"] : "",
|
|
15764
16057
|
messages: payload["messages"],
|
|
16058
|
+
removals: payload["removals"],
|
|
15765
16059
|
allowRepair: payload["allowRepair"] === true,
|
|
15766
16060
|
runActive: ctx.isRunActive?.() === true
|
|
15767
16061
|
});
|
|
@@ -16581,6 +16875,19 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
|
|
|
16581
16875
|
return true;
|
|
16582
16876
|
}
|
|
16583
16877
|
|
|
16878
|
+
// src/server/fallback-choice.ts
|
|
16879
|
+
function emitFallbackChoice(events, msg) {
|
|
16880
|
+
const parsed = validateModelFallbackChoicePayload(msg.payload);
|
|
16881
|
+
if (!parsed.ok) return parsed;
|
|
16882
|
+
events?.emit("provider.fallback_choice", {
|
|
16883
|
+
requestId: parsed.value.requestId,
|
|
16884
|
+
...parsed.value.providerId ? { providerId: parsed.value.providerId } : {},
|
|
16885
|
+
...parsed.value.model ? { model: parsed.value.model } : {},
|
|
16886
|
+
...parsed.value.autoSwitch ? { autoSwitch: true } : {}
|
|
16887
|
+
});
|
|
16888
|
+
return { ok: true };
|
|
16889
|
+
}
|
|
16890
|
+
|
|
16584
16891
|
// src/server/agent-roster-routes.ts
|
|
16585
16892
|
async function handleAgentRosterRoute(ws, msg, handlers) {
|
|
16586
16893
|
if (!msg.type.startsWith("agent-roster.")) return false;
|
|
@@ -16745,13 +17052,17 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16745
17052
|
case "model.refine":
|
|
16746
17053
|
await routes.refineModel(ws, msg);
|
|
16747
17054
|
return true;
|
|
17055
|
+
case "model.fallback_choice":
|
|
17056
|
+
await routes.fallbackChoice(ws, msg);
|
|
17057
|
+
return true;
|
|
16748
17058
|
case "key.add":
|
|
16749
17059
|
case "key.update": {
|
|
16750
17060
|
const payload = asPayloadRecord(msg);
|
|
16751
17061
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16752
17062
|
const label = payload ? requiredString(payload, "label") : null;
|
|
16753
17063
|
const apiKey = payload ? requiredString(payload, "apiKey") : null;
|
|
16754
|
-
if (!providerId || !label || !apiKey)
|
|
17064
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label || !apiKey)
|
|
17065
|
+
return invalidPayload(ws, msg.type);
|
|
16755
17066
|
await routes.providerHandlers.handleKeyUpsert(ws, providerId, label, apiKey);
|
|
16756
17067
|
return true;
|
|
16757
17068
|
}
|
|
@@ -16759,7 +17070,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16759
17070
|
const payload = asPayloadRecord(msg);
|
|
16760
17071
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16761
17072
|
const label = payload ? requiredString(payload, "label") : null;
|
|
16762
|
-
if (!providerId || !
|
|
17073
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label)
|
|
17074
|
+
return invalidPayload(ws, msg.type);
|
|
16763
17075
|
await routes.providerHandlers.handleKeyDelete(ws, providerId, label);
|
|
16764
17076
|
return true;
|
|
16765
17077
|
}
|
|
@@ -16767,7 +17079,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16767
17079
|
const payload = asPayloadRecord(msg);
|
|
16768
17080
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16769
17081
|
const label = payload ? requiredString(payload, "label") : null;
|
|
16770
|
-
if (!providerId || !
|
|
17082
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label)
|
|
17083
|
+
return invalidPayload(ws, msg.type);
|
|
16771
17084
|
await routes.providerHandlers.handleKeySetActive(ws, providerId, label);
|
|
16772
17085
|
return true;
|
|
16773
17086
|
}
|
|
@@ -16779,11 +17092,11 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16779
17092
|
const apiKey = payload?.["apiKey"];
|
|
16780
17093
|
const models = payload ? optionalStringArray(payload, "models") : null;
|
|
16781
17094
|
const customModels = payload ? optionalCustomModels(payload) : null;
|
|
16782
|
-
if (!id || !family) return invalidPayload(ws, msg.type);
|
|
17095
|
+
if (!id || !SAFE_CONFIG_KEY.test(id) || !family) return invalidPayload(ws, msg.type);
|
|
16783
17096
|
if (baseUrl !== void 0 && typeof baseUrl !== "string") return invalidPayload(ws, msg.type);
|
|
16784
17097
|
if (apiKey !== void 0 && typeof apiKey !== "string") return invalidPayload(ws, msg.type);
|
|
16785
17098
|
if (models === null || customModels === null) return invalidPayload(ws, msg.type);
|
|
16786
|
-
await routes.providerHandlers.handleProviderAdd(ws, {
|
|
17099
|
+
const added = await routes.providerHandlers.handleProviderAdd(ws, {
|
|
16787
17100
|
id,
|
|
16788
17101
|
family,
|
|
16789
17102
|
baseUrl,
|
|
@@ -16791,20 +17104,22 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16791
17104
|
models,
|
|
16792
17105
|
customModels
|
|
16793
17106
|
});
|
|
16794
|
-
|
|
17107
|
+
if (added) {
|
|
17108
|
+
void routes.adoptDefaultProviderIfUnset(id).catch(() => void 0);
|
|
17109
|
+
}
|
|
16795
17110
|
return true;
|
|
16796
17111
|
}
|
|
16797
17112
|
case "provider.remove": {
|
|
16798
17113
|
const payload = asPayloadRecord(msg);
|
|
16799
17114
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16800
|
-
if (!providerId) return invalidPayload(ws, msg.type);
|
|
17115
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId)) return invalidPayload(ws, msg.type);
|
|
16801
17116
|
await routes.providerHandlers.handleProviderRemove(ws, providerId);
|
|
16802
17117
|
return true;
|
|
16803
17118
|
}
|
|
16804
17119
|
case "provider.clear_models": {
|
|
16805
17120
|
const payload = asPayloadRecord(msg);
|
|
16806
17121
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16807
|
-
if (!providerId) return invalidPayload(ws, msg.type);
|
|
17122
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId)) return invalidPayload(ws, msg.type);
|
|
16808
17123
|
await routes.providerHandlers.handleProviderClearModels(ws, providerId);
|
|
16809
17124
|
return true;
|
|
16810
17125
|
}
|
|
@@ -16836,7 +17151,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16836
17151
|
const payload = asPayloadRecord(msg);
|
|
16837
17152
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16838
17153
|
const previousModels = payload ? optionalStringArray(payload, "previousModels") : null;
|
|
16839
|
-
if (!providerId || !
|
|
17154
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !previousModels)
|
|
17155
|
+
return invalidPayload(ws, msg.type);
|
|
16840
17156
|
await routes.providerHandlers.handleProviderUndoClear(ws, providerId, previousModels);
|
|
16841
17157
|
return true;
|
|
16842
17158
|
}
|
|
@@ -16846,7 +17162,7 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16846
17162
|
const envVars = payload ? optionalStringArray(payload, "envVars") : null;
|
|
16847
17163
|
const models = payload ? optionalStringArray(payload, "models") : null;
|
|
16848
17164
|
const customModels = payload ? optionalCustomModels(payload) : null;
|
|
16849
|
-
if (!payload || !id || envVars === null || models === null || customModels === null)
|
|
17165
|
+
if (!payload || !id || !SAFE_CONFIG_KEY.test(id) || envVars === null || models === null || customModels === null)
|
|
16850
17166
|
return invalidPayload(ws, msg.type);
|
|
16851
17167
|
for (const key of ["family", "baseUrl"]) {
|
|
16852
17168
|
if (payload[key] !== void 0 && typeof payload[key] !== "string")
|
|
@@ -16866,7 +17182,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16866
17182
|
const payload = asPayloadRecord(msg);
|
|
16867
17183
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16868
17184
|
const timeoutMs = payload ? optionalNumber(payload, "timeoutMs") : null;
|
|
16869
|
-
if (!providerId || timeoutMs === null)
|
|
17185
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || timeoutMs === null)
|
|
17186
|
+
return invalidPayload(ws, msg.type);
|
|
16870
17187
|
await routes.providerHandlers.handleProviderProbe(ws, providerId, timeoutMs);
|
|
16871
17188
|
return true;
|
|
16872
17189
|
}
|
|
@@ -16875,7 +17192,7 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16875
17192
|
const kind = oauthKind(payload);
|
|
16876
17193
|
const providerId = payload?.["providerId"];
|
|
16877
17194
|
if (!kind) return invalidPayload(ws, msg.type);
|
|
16878
|
-
if (providerId !== void 0 && typeof providerId !== "string") {
|
|
17195
|
+
if (providerId !== void 0 && (typeof providerId !== "string" || !SAFE_CONFIG_KEY.test(providerId))) {
|
|
16879
17196
|
return invalidPayload(ws, msg.type);
|
|
16880
17197
|
}
|
|
16881
17198
|
await routes.providerHandlers.handleOAuthStart(ws, kind, providerId);
|
|
@@ -16914,7 +17231,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16914
17231
|
const payload = asPayloadRecord(msg);
|
|
16915
17232
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16916
17233
|
const model = payload ? requiredString(payload, "model") : null;
|
|
16917
|
-
if (!providerId || !
|
|
17234
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !model)
|
|
17235
|
+
return invalidPayload(ws, msg.type);
|
|
16918
17236
|
const released = routes.statusTracker.retryNow(providerId, model);
|
|
16919
17237
|
sendResult2(
|
|
16920
17238
|
ws,
|
|
@@ -16931,7 +17249,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16931
17249
|
const payload = asPayloadRecord(msg);
|
|
16932
17250
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16933
17251
|
const model = payload ? requiredString(payload, "model") : null;
|
|
16934
|
-
if (!providerId || !
|
|
17252
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !model)
|
|
17253
|
+
return invalidPayload(ws, msg.type);
|
|
16935
17254
|
routes.statusTracker.clear(providerId, model);
|
|
16936
17255
|
sendResult2(ws, true, `Cleared tracking for ${providerId}/${model}.`);
|
|
16937
17256
|
return true;
|
|
@@ -18961,7 +19280,22 @@ function setupEvents(deps2) {
|
|
|
18961
19280
|
from: e.from,
|
|
18962
19281
|
to: e.to,
|
|
18963
19282
|
status: e.status,
|
|
18964
|
-
providerSwitched: e.providerSwitched
|
|
19283
|
+
providerSwitched: e.providerSwitched,
|
|
19284
|
+
...e.requestId ? { requestId: e.requestId } : {}
|
|
19285
|
+
})
|
|
19286
|
+
});
|
|
19287
|
+
});
|
|
19288
|
+
on("provider.fallback_pending", (e) => {
|
|
19289
|
+
broadcast2(clients, {
|
|
19290
|
+
type: "provider.fallback_pending",
|
|
19291
|
+
payload: sessionPayload2({
|
|
19292
|
+
sessionId: e.sessionId,
|
|
19293
|
+
from: e.from,
|
|
19294
|
+
status: e.status,
|
|
19295
|
+
candidates: e.candidates,
|
|
19296
|
+
autoSwitchSeconds: e.autoSwitchSeconds,
|
|
19297
|
+
requestId: e.requestId,
|
|
19298
|
+
timestamp: e.timestamp
|
|
18965
19299
|
})
|
|
18966
19300
|
});
|
|
18967
19301
|
});
|
|
@@ -19046,6 +19380,15 @@ function setupEvents(deps2) {
|
|
|
19046
19380
|
type: "mailbox.agent_registered",
|
|
19047
19381
|
payload
|
|
19048
19382
|
});
|
|
19383
|
+
}),
|
|
19384
|
+
// Deregistration (subagent retirement) must reach the browser too —
|
|
19385
|
+
// otherwise dead agents linger in the client roster until an unrelated
|
|
19386
|
+
// refresh. Emitted by sqlite-mailbox.deregisterAgent with { agentId }.
|
|
19387
|
+
events.onPattern("mailbox.agent_deregistered", (_e, payload) => {
|
|
19388
|
+
broadcast2(clients, {
|
|
19389
|
+
type: "mailbox.agent_deregistered",
|
|
19390
|
+
payload
|
|
19391
|
+
});
|
|
19049
19392
|
})
|
|
19050
19393
|
);
|
|
19051
19394
|
const forwardSubagent = (kind, payload) => broadcast2(clients, { type: "subagent.event", payload: sessionPayload2({ kind, ...payload }) });
|
|
@@ -21330,29 +21673,10 @@ import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/too
|
|
|
21330
21673
|
// src/server/model-auto-discovery.ts
|
|
21331
21674
|
import * as fs19 from "node:fs/promises";
|
|
21332
21675
|
import * as path24 from "node:path";
|
|
21333
|
-
import {
|
|
21676
|
+
import { discoverOpenAICompatibleModels, resolveDiscoveryTargets } from "@wrongstack/providers";
|
|
21334
21677
|
function isOverlayRegistry(value) {
|
|
21335
21678
|
return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
|
|
21336
21679
|
}
|
|
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
21680
|
async function readCache(file) {
|
|
21357
21681
|
try {
|
|
21358
21682
|
return JSON.parse(await fs19.readFile(file, "utf8"));
|
|
@@ -21363,14 +21687,13 @@ async function readCache(file) {
|
|
|
21363
21687
|
async function discoverAndMergeWebuiProviders(opts) {
|
|
21364
21688
|
const registry = opts.registry;
|
|
21365
21689
|
if (!isOverlayRegistry(registry)) return;
|
|
21366
|
-
const targets =
|
|
21690
|
+
const targets = resolveDiscoveryTargets(opts.config);
|
|
21367
21691
|
if (targets.length === 0) return;
|
|
21368
21692
|
const cacheFile = path24.join(opts.cacheDir, "discovered-models-cache.json");
|
|
21369
21693
|
const cache2 = await readCache(cacheFile);
|
|
21370
21694
|
let cacheDirty = false;
|
|
21371
21695
|
await Promise.all(
|
|
21372
|
-
targets.map(async ({ id, cfg, baseUrl, apiKey }) => {
|
|
21373
|
-
const cacheKey = `${id}\0${baseUrl}`;
|
|
21696
|
+
targets.map(async ({ id, cfg, baseUrl, apiKey, cacheKey }) => {
|
|
21374
21697
|
const provider = await discoverOpenAICompatibleModels(id, {
|
|
21375
21698
|
baseUrl,
|
|
21376
21699
|
apiKey,
|
|
@@ -21736,15 +22059,6 @@ async function createPreContextServices(input) {
|
|
|
21736
22059
|
logger.warn(`models.dev refresh failed (${toErrorMessage11(err)}); using cached catalog`);
|
|
21737
22060
|
}
|
|
21738
22061
|
}
|
|
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
22062
|
try {
|
|
21749
22063
|
await discoverAndMergeWebuiProviders({
|
|
21750
22064
|
config,
|
|
@@ -21755,6 +22069,15 @@ async function createPreContextServices(input) {
|
|
|
21755
22069
|
} catch (err) {
|
|
21756
22070
|
logger.debug(`provider auto-discovery skipped: ${toErrorMessage11(err)}`);
|
|
21757
22071
|
}
|
|
22072
|
+
try {
|
|
22073
|
+
await installCatalogModelOutputLimits({
|
|
22074
|
+
registry: modelsRegistry,
|
|
22075
|
+
getConfig: () => config,
|
|
22076
|
+
log: (message) => logger.debug(message)
|
|
22077
|
+
});
|
|
22078
|
+
} catch (err) {
|
|
22079
|
+
logger.debug(`model output-limit index skipped: ${toErrorMessage11(err)}`);
|
|
22080
|
+
}
|
|
21758
22081
|
const events = opts.services?.events ?? new EventBus();
|
|
21759
22082
|
events.setLogger(logger);
|
|
21760
22083
|
const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry, events });
|
|
@@ -22141,7 +22464,16 @@ function buildRoutes(state, deps2, cb) {
|
|
|
22141
22464
|
refineModel: (ws, msg) => modelOperations.refineModel(
|
|
22142
22465
|
ws,
|
|
22143
22466
|
msg.payload
|
|
22144
|
-
)
|
|
22467
|
+
),
|
|
22468
|
+
fallbackChoice: async (ws, msg) => {
|
|
22469
|
+
const result = emitFallbackChoice(deps2.events, msg);
|
|
22470
|
+
if (!result.ok) {
|
|
22471
|
+
send(ws, {
|
|
22472
|
+
type: "error",
|
|
22473
|
+
payload: { phase: "invalid_request", message: result.message }
|
|
22474
|
+
});
|
|
22475
|
+
}
|
|
22476
|
+
}
|
|
22145
22477
|
};
|
|
22146
22478
|
const sessionRoutes = createSessionHandlers({
|
|
22147
22479
|
config: state.getConfig(),
|
|
@@ -22319,8 +22651,10 @@ function buildRoutes(state, deps2, cb) {
|
|
|
22319
22651
|
});
|
|
22320
22652
|
const mcpRoutes = {
|
|
22321
22653
|
list: (ws, msg) => handleMcpList(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
22322
|
-
add
|
|
22323
|
-
|
|
22654
|
+
// add/update are the spawn-capable pair — they take a `command`/`args`
|
|
22655
|
+
// from the wire and start it. They go past the trust boundary (M1).
|
|
22656
|
+
add: (ws, msg) => handleMcpAdd(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry, deps2.trustBoundary),
|
|
22657
|
+
update: (ws, msg) => handleMcpUpdate(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry, deps2.trustBoundary),
|
|
22324
22658
|
remove: (ws, msg) => handleMcpRemove(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
22325
22659
|
enable: (ws, msg) => handleMcpEnable(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
22326
22660
|
disable: (ws, msg) => handleMcpDisable(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
@@ -22492,7 +22826,10 @@ function createWsServers(httpServer, ports, accessToken) {
|
|
|
22492
22826
|
expectedToken: wsToken,
|
|
22493
22827
|
requireToken: ports.requireToken,
|
|
22494
22828
|
allowedHostnames: publicHostnames,
|
|
22495
|
-
allowBrowserUrlToken: Boolean(ports.publicWsUrl)
|
|
22829
|
+
allowBrowserUrlToken: Boolean(ports.publicWsUrl),
|
|
22830
|
+
// WS-003 opt-out for the Vite dev loop only (app and WS server cannot
|
|
22831
|
+
// share a port). Off unless explicitly requested — see ws-auth.ts.
|
|
22832
|
+
allowCrossPortLoopbackCookie: process.env["WRONGSTACK_WEBUI_DEV_CROSS_PORT_WS"] === "1"
|
|
22496
22833
|
});
|
|
22497
22834
|
const WS_MAX_PAYLOAD = 20 * 1024 * 1024;
|
|
22498
22835
|
const wssPrimary = new WebSocketServer({
|