@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/index.js
CHANGED
|
@@ -73,6 +73,10 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
|
73
73
|
// Display-only toggles (purely visual, persisted in localStorage via Zustand).
|
|
74
74
|
"groupToolCalls",
|
|
75
75
|
"showThinkingLogs",
|
|
76
|
+
// v15: auto-collapse of the chat input under the history (opt-in display
|
|
77
|
+
// toggle, default off). Whitelisted so the key survives `prefs.update`
|
|
78
|
+
// round-trips without tripping the "unknown preference key" rejection.
|
|
79
|
+
"autoCollapseInput",
|
|
76
80
|
// v11 Display parity: inverse fsAccess flag.
|
|
77
81
|
"allowOutsideProjectRoot",
|
|
78
82
|
// v13 Display parity (TUI SettingsPicker fields 42 & 43): the read tool
|
|
@@ -397,6 +401,51 @@ function validateModelSwitchPayload(payload) {
|
|
|
397
401
|
}
|
|
398
402
|
};
|
|
399
403
|
}
|
|
404
|
+
function validateModelFallbackChoicePayload(payload) {
|
|
405
|
+
if (!isRecord2(payload)) {
|
|
406
|
+
return {
|
|
407
|
+
ok: false,
|
|
408
|
+
message: "model.fallback_choice payload must be an object"
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const requestId = payload["requestId"];
|
|
412
|
+
if (typeof requestId !== "string" || requestId.trim().length === 0) {
|
|
413
|
+
return {
|
|
414
|
+
ok: false,
|
|
415
|
+
message: "model.fallback_choice payload.requestId must be a non-empty string"
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const providerId = payload["providerId"];
|
|
419
|
+
const model = payload["model"];
|
|
420
|
+
const autoSwitch = payload["autoSwitch"];
|
|
421
|
+
if (providerId !== void 0 && typeof providerId !== "string") {
|
|
422
|
+
return {
|
|
423
|
+
ok: false,
|
|
424
|
+
message: "model.fallback_choice payload.providerId must be a string when provided"
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (model !== void 0 && typeof model !== "string") {
|
|
428
|
+
return {
|
|
429
|
+
ok: false,
|
|
430
|
+
message: "model.fallback_choice payload.model must be a string when provided"
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (autoSwitch !== void 0 && typeof autoSwitch !== "boolean") {
|
|
434
|
+
return {
|
|
435
|
+
ok: false,
|
|
436
|
+
message: "model.fallback_choice payload.autoSwitch must be a boolean when provided"
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
ok: true,
|
|
441
|
+
value: {
|
|
442
|
+
requestId: requestId.trim(),
|
|
443
|
+
...typeof providerId === "string" ? { providerId } : {},
|
|
444
|
+
...typeof model === "string" ? { model } : {},
|
|
445
|
+
...typeof autoSwitch === "boolean" ? { autoSwitch } : {}
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
400
449
|
var AUTONOMY_VALUES2 = /* @__PURE__ */ new Set(["off", "suggest", "auto", "eternal", "eternal-parallel"]);
|
|
401
450
|
function validateMailboxMessagesPayload(payload) {
|
|
402
451
|
if (payload === void 0) return { ok: true, value: void 0 };
|
|
@@ -4417,13 +4466,10 @@ function createConversationOperations(ctx) {
|
|
|
4417
4466
|
|
|
4418
4467
|
// src/server/context-editor.ts
|
|
4419
4468
|
import { createHash } from "node:crypto";
|
|
4420
|
-
import net from "node:net";
|
|
4421
4469
|
import {
|
|
4422
4470
|
ALLOWED_IMAGE_MEDIA_TYPES,
|
|
4423
4471
|
base64DecodedBytes,
|
|
4424
4472
|
isAllowedImageMediaType,
|
|
4425
|
-
isPrivateIPv4,
|
|
4426
|
-
isPrivateIPv6,
|
|
4427
4473
|
isValidImageBase64,
|
|
4428
4474
|
MAX_INCOMING_IMAGE_BYTES,
|
|
4429
4475
|
repairToolUseAdjacency
|
|
@@ -4491,40 +4537,7 @@ var REVISION_PREFIX = "wrongstack-context-editor-v1\0";
|
|
|
4491
4537
|
var MAX_MESSAGE_COUNT_GROWTH = 10;
|
|
4492
4538
|
var MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
|
|
4493
4539
|
var MAX_STRING_LENGTH = 8 * 1024 * 1024;
|
|
4494
|
-
var
|
|
4495
|
-
function imageUrlRejectionReason(url) {
|
|
4496
|
-
if (url.length > MAX_IMAGE_URL_LENGTH) {
|
|
4497
|
-
return `image.source.url exceeds ${MAX_IMAGE_URL_LENGTH} characters.`;
|
|
4498
|
-
}
|
|
4499
|
-
let parsed;
|
|
4500
|
-
try {
|
|
4501
|
-
parsed = new URL(url);
|
|
4502
|
-
} catch {
|
|
4503
|
-
return "image.source.url must be an absolute URL.";
|
|
4504
|
-
}
|
|
4505
|
-
if (parsed.protocol !== "https:") {
|
|
4506
|
-
return `image.source.url must use https (got "${parsed.protocol}").`;
|
|
4507
|
-
}
|
|
4508
|
-
if (parsed.username !== "" || parsed.password !== "") {
|
|
4509
|
-
return "image.source.url must not embed credentials.";
|
|
4510
|
-
}
|
|
4511
|
-
const host = parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]") ? parsed.hostname.slice(1, -1) : parsed.hostname;
|
|
4512
|
-
const bareHost = host.endsWith(".") ? host.slice(0, -1) : host;
|
|
4513
|
-
if (bareHost === "") {
|
|
4514
|
-
return "image.source.url must include a hostname.";
|
|
4515
|
-
}
|
|
4516
|
-
if (bareHost === "localhost" || bareHost.endsWith(".localhost")) {
|
|
4517
|
-
return "image.source.url must not target localhost.";
|
|
4518
|
-
}
|
|
4519
|
-
const family = net.isIP(bareHost);
|
|
4520
|
-
if (family === 4 && isPrivateIPv4(bareHost)) {
|
|
4521
|
-
return `image.source.url must not target a private or loopback address ("${bareHost}").`;
|
|
4522
|
-
}
|
|
4523
|
-
if (family === 6 && isPrivateIPv6(bareHost)) {
|
|
4524
|
-
return `image.source.url must not target a private or loopback address ("${bareHost}").`;
|
|
4525
|
-
}
|
|
4526
|
-
return void 0;
|
|
4527
|
-
}
|
|
4540
|
+
var MAX_REMOVAL_COUNT = 4096;
|
|
4528
4541
|
function isRecord3(value) {
|
|
4529
4542
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4530
4543
|
}
|
|
@@ -4533,7 +4546,7 @@ function canonicalize(value) {
|
|
|
4533
4546
|
if (isRecord3(value)) {
|
|
4534
4547
|
const sorted = {};
|
|
4535
4548
|
for (const key of Object.keys(value).sort()) {
|
|
4536
|
-
if (key === "_estTokens") continue;
|
|
4549
|
+
if (key === "_estTokens" || key === "_toolErrorInfo") continue;
|
|
4537
4550
|
const item = value[key];
|
|
4538
4551
|
if (item === void 0) continue;
|
|
4539
4552
|
sorted[key] = canonicalize(item);
|
|
@@ -4564,6 +4577,12 @@ function isMessageRole(value) {
|
|
|
4564
4577
|
function isPlainJsonObject(value) {
|
|
4565
4578
|
return isRecord3(value);
|
|
4566
4579
|
}
|
|
4580
|
+
function splitsSurrogatePair(text2, offset) {
|
|
4581
|
+
if (offset <= 0 || offset >= text2.length) return false;
|
|
4582
|
+
const previous = text2.charCodeAt(offset - 1);
|
|
4583
|
+
const next = text2.charCodeAt(offset);
|
|
4584
|
+
return previous >= 55296 && previous <= 56319 && next >= 56320 && next <= 57343;
|
|
4585
|
+
}
|
|
4567
4586
|
function validateCacheControl(value, path35, errors) {
|
|
4568
4587
|
if (value === void 0) return void 0;
|
|
4569
4588
|
if (!isRecord3(value) || value["type"] !== "ephemeral") {
|
|
@@ -4774,19 +4793,13 @@ function validateBlock(value, path35, errors) {
|
|
|
4774
4793
|
);
|
|
4775
4794
|
return void 0;
|
|
4776
4795
|
}
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
source: {
|
|
4785
|
-
type: "url",
|
|
4786
|
-
...typeof mediaType === "string" ? { media_type: mediaType } : {},
|
|
4787
|
-
url
|
|
4788
|
-
}
|
|
4789
|
-
};
|
|
4796
|
+
error(
|
|
4797
|
+
errors,
|
|
4798
|
+
`${path35}/source/url`,
|
|
4799
|
+
"UNSAFE_IMAGE_URL",
|
|
4800
|
+
"URL image sources are not allowed in context editor proposals; use an ingested base64 image."
|
|
4801
|
+
);
|
|
4802
|
+
return void 0;
|
|
4790
4803
|
}
|
|
4791
4804
|
case "thinking": {
|
|
4792
4805
|
const thinking = value["thinking"];
|
|
@@ -4943,6 +4956,14 @@ function warningsForMessage(message, index) {
|
|
|
4943
4956
|
message: "This block contains provider replay metadata and should only be removed with the whole turn if no longer needed."
|
|
4944
4957
|
});
|
|
4945
4958
|
}
|
|
4959
|
+
if (block.type === "image" && block.source.type === "url") {
|
|
4960
|
+
warnings.push({
|
|
4961
|
+
path: `/messages/${index}/content/${blockIndex}/source/url`,
|
|
4962
|
+
code: "UNSAFE_IMAGE_URL",
|
|
4963
|
+
severity: "danger",
|
|
4964
|
+
message: "URL image sources cannot be retained in context editor proposals; remove the whole message before applying other edits."
|
|
4965
|
+
});
|
|
4966
|
+
}
|
|
4946
4967
|
if (block.type === "tool_result" && block.content.length > 2e4) {
|
|
4947
4968
|
warnings.push({
|
|
4948
4969
|
path: `/messages/${index}/content/${blockIndex}`,
|
|
@@ -4970,6 +4991,21 @@ function metricFor(ctx, messages, tools) {
|
|
|
4970
4991
|
fullRequestTokens: breakdown.total
|
|
4971
4992
|
};
|
|
4972
4993
|
}
|
|
4994
|
+
function isToolResultMessage(message) {
|
|
4995
|
+
return Boolean(
|
|
4996
|
+
message?.role === "user" && Array.isArray(message.content) && message.content.length > 0 && message.content.every((block) => block.type === "tool_result")
|
|
4997
|
+
);
|
|
4998
|
+
}
|
|
4999
|
+
function pairedAssistantIndices(messages, userIndex) {
|
|
5000
|
+
if (messages[userIndex]?.role !== "user" || isToolResultMessage(messages[userIndex])) return [];
|
|
5001
|
+
const paired = [];
|
|
5002
|
+
for (let index = userIndex + 1; index < messages.length; index += 1) {
|
|
5003
|
+
const message = messages[index];
|
|
5004
|
+
if (message?.role === "user" && !isToolResultMessage(message)) break;
|
|
5005
|
+
if (message?.role === "assistant") paired.push(index);
|
|
5006
|
+
}
|
|
5007
|
+
return paired;
|
|
5008
|
+
}
|
|
4973
5009
|
function buildContextEditorSnapshot(ctx, tools) {
|
|
4974
5010
|
const messages = ctx.messages.map(
|
|
4975
5011
|
(message) => ({
|
|
@@ -4999,11 +5035,165 @@ function buildContextEditorSnapshot(ctx, tools) {
|
|
|
4999
5035
|
tokens: messageTokens(message.content),
|
|
5000
5036
|
preview: breakdown.messages.breakdown[index]?.preview ?? "",
|
|
5001
5037
|
blockCount: Array.isArray(message.content) ? message.content.length : null,
|
|
5002
|
-
warnings: warningsForMessage(message, index)
|
|
5038
|
+
warnings: warningsForMessage(message, index),
|
|
5039
|
+
pairedAssistantIndices: pairedAssistantIndices(ctx.messages, index)
|
|
5003
5040
|
})),
|
|
5004
5041
|
diagnostics: toolDiagnostics(ctx.messages)
|
|
5005
5042
|
};
|
|
5006
5043
|
}
|
|
5044
|
+
function validateRemovalPlan(value, originalMessages, proposedMessages) {
|
|
5045
|
+
const errors = [];
|
|
5046
|
+
if (value === void 0) {
|
|
5047
|
+
error(
|
|
5048
|
+
errors,
|
|
5049
|
+
"/removals",
|
|
5050
|
+
"REMOVAL_PLAN_REQUIRED",
|
|
5051
|
+
"A removal plan is required for every context editor proposal."
|
|
5052
|
+
);
|
|
5053
|
+
return { errors };
|
|
5054
|
+
}
|
|
5055
|
+
if (!Array.isArray(value)) {
|
|
5056
|
+
error(errors, "/removals", "INVALID_REMOVALS", "removals must be an array.");
|
|
5057
|
+
return { errors };
|
|
5058
|
+
}
|
|
5059
|
+
if (value.length > MAX_REMOVAL_COUNT) {
|
|
5060
|
+
error(
|
|
5061
|
+
errors,
|
|
5062
|
+
"/removals",
|
|
5063
|
+
"TOO_MANY_REMOVALS",
|
|
5064
|
+
`removals must contain at most ${MAX_REMOVAL_COUNT} entries.`
|
|
5065
|
+
);
|
|
5066
|
+
return { errors };
|
|
5067
|
+
}
|
|
5068
|
+
const wholeMessages = /* @__PURE__ */ new Set();
|
|
5069
|
+
const touchedUsers = /* @__PURE__ */ new Set();
|
|
5070
|
+
const ranges = [];
|
|
5071
|
+
for (const [removalIndex, raw] of value.entries()) {
|
|
5072
|
+
const path35 = `/removals/${removalIndex}`;
|
|
5073
|
+
if (!isRecord3(raw) || !Number.isInteger(raw["messageIndex"])) {
|
|
5074
|
+
error(errors, path35, "INVALID_REMOVAL", "Removal must include an integer messageIndex.");
|
|
5075
|
+
continue;
|
|
5076
|
+
}
|
|
5077
|
+
const messageIndex = raw["messageIndex"];
|
|
5078
|
+
const original = originalMessages[messageIndex];
|
|
5079
|
+
if (!original) {
|
|
5080
|
+
error(errors, `${path35}/messageIndex`, "INVALID_MESSAGE_INDEX", "Removal messageIndex is out of range.");
|
|
5081
|
+
continue;
|
|
5082
|
+
}
|
|
5083
|
+
const start = raw["start"];
|
|
5084
|
+
const end = raw["end"];
|
|
5085
|
+
const blockIndex = raw["blockIndex"];
|
|
5086
|
+
if (start === void 0 && end === void 0 && blockIndex === void 0) {
|
|
5087
|
+
wholeMessages.add(messageIndex);
|
|
5088
|
+
if (original.role === "user") touchedUsers.add(messageIndex);
|
|
5089
|
+
continue;
|
|
5090
|
+
}
|
|
5091
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start) {
|
|
5092
|
+
error(errors, path35, "INVALID_RANGE", "Range removal requires integer start/end with 0 <= start < end.");
|
|
5093
|
+
continue;
|
|
5094
|
+
}
|
|
5095
|
+
let text2;
|
|
5096
|
+
if (blockIndex === void 0 && typeof original.content === "string") text2 = original.content;
|
|
5097
|
+
if (Number.isInteger(blockIndex) && Array.isArray(original.content)) {
|
|
5098
|
+
const block = original.content[blockIndex];
|
|
5099
|
+
if (block?.type === "text") text2 = block.text;
|
|
5100
|
+
}
|
|
5101
|
+
if (text2 === void 0 || end > text2.length) {
|
|
5102
|
+
error(errors, path35, "INVALID_RANGE_TARGET", "Range must target existing string or text-block content.");
|
|
5103
|
+
continue;
|
|
5104
|
+
}
|
|
5105
|
+
if (splitsSurrogatePair(text2, start) || splitsSurrogatePair(text2, end)) {
|
|
5106
|
+
error(
|
|
5107
|
+
errors,
|
|
5108
|
+
path35,
|
|
5109
|
+
"INVALID_UNICODE_RANGE",
|
|
5110
|
+
"Range boundaries must not split a Unicode surrogate pair."
|
|
5111
|
+
);
|
|
5112
|
+
continue;
|
|
5113
|
+
}
|
|
5114
|
+
ranges.push({
|
|
5115
|
+
messageIndex,
|
|
5116
|
+
...blockIndex === void 0 ? {} : { blockIndex },
|
|
5117
|
+
start,
|
|
5118
|
+
end
|
|
5119
|
+
});
|
|
5120
|
+
if (original.role === "user") touchedUsers.add(messageIndex);
|
|
5121
|
+
}
|
|
5122
|
+
const rangesByTarget = /* @__PURE__ */ new Map();
|
|
5123
|
+
for (const range of ranges) {
|
|
5124
|
+
const key = `${range.messageIndex}:${range.blockIndex ?? "string"}`;
|
|
5125
|
+
const targetRanges = rangesByTarget.get(key) ?? [];
|
|
5126
|
+
targetRanges.push(range);
|
|
5127
|
+
rangesByTarget.set(key, targetRanges);
|
|
5128
|
+
}
|
|
5129
|
+
for (const targetRanges of rangesByTarget.values()) {
|
|
5130
|
+
targetRanges.sort((left, right) => (left.start ?? 0) - (right.start ?? 0));
|
|
5131
|
+
for (let index = 1; index < targetRanges.length; index += 1) {
|
|
5132
|
+
const previous = targetRanges[index - 1];
|
|
5133
|
+
const current2 = targetRanges[index];
|
|
5134
|
+
if (previous?.end !== void 0 && current2?.start !== void 0 && current2.start < previous.end) {
|
|
5135
|
+
error(
|
|
5136
|
+
errors,
|
|
5137
|
+
"/removals",
|
|
5138
|
+
"OVERLAPPING_RANGES",
|
|
5139
|
+
"Removal ranges targeting the same text must not overlap."
|
|
5140
|
+
);
|
|
5141
|
+
break;
|
|
5142
|
+
}
|
|
5143
|
+
}
|
|
5144
|
+
}
|
|
5145
|
+
for (const userIndex of touchedUsers) {
|
|
5146
|
+
for (const assistantIndex of pairedAssistantIndices(originalMessages, userIndex)) {
|
|
5147
|
+
if (wholeMessages.has(assistantIndex)) continue;
|
|
5148
|
+
error(
|
|
5149
|
+
errors,
|
|
5150
|
+
"/removals",
|
|
5151
|
+
"MISSING_ASSISTANT_PAIR",
|
|
5152
|
+
`Editing user message ${userIndex} must also remove assistant message ${assistantIndex}.`
|
|
5153
|
+
);
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
const expectedMessages = structuredClone(originalMessages);
|
|
5157
|
+
for (const targetRanges of rangesByTarget.values()) {
|
|
5158
|
+
const first = targetRanges[0];
|
|
5159
|
+
if (!first) continue;
|
|
5160
|
+
const message = expectedMessages[first.messageIndex];
|
|
5161
|
+
if (!message) continue;
|
|
5162
|
+
let text2;
|
|
5163
|
+
if (first.blockIndex === void 0 && typeof message.content === "string") {
|
|
5164
|
+
text2 = message.content;
|
|
5165
|
+
} else if (first.blockIndex !== void 0 && Array.isArray(message.content)) {
|
|
5166
|
+
const block = message.content[first.blockIndex];
|
|
5167
|
+
if (block?.type === "text") text2 = block.text;
|
|
5168
|
+
}
|
|
5169
|
+
if (text2 === void 0) continue;
|
|
5170
|
+
const pieces = [];
|
|
5171
|
+
let cursor = 0;
|
|
5172
|
+
for (const range of targetRanges) {
|
|
5173
|
+
if (range.start === void 0 || range.end === void 0) continue;
|
|
5174
|
+
pieces.push(text2.slice(cursor, range.start));
|
|
5175
|
+
cursor = range.end;
|
|
5176
|
+
}
|
|
5177
|
+
pieces.push(text2.slice(cursor));
|
|
5178
|
+
const nextText = pieces.join("");
|
|
5179
|
+
if (first.blockIndex === void 0 && typeof message.content === "string") {
|
|
5180
|
+
message.content = nextText;
|
|
5181
|
+
} else if (first.blockIndex !== void 0 && Array.isArray(message.content)) {
|
|
5182
|
+
const block = message.content[first.blockIndex];
|
|
5183
|
+
if (block?.type === "text") block.text = nextText;
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
const expectedProposal = expectedMessages.filter((_, index) => !wholeMessages.has(index));
|
|
5187
|
+
if (JSON.stringify(canonicalize(expectedProposal)) !== JSON.stringify(canonicalize(proposedMessages))) {
|
|
5188
|
+
error(
|
|
5189
|
+
errors,
|
|
5190
|
+
"/messages",
|
|
5191
|
+
"REMOVAL_PLAN_MISMATCH",
|
|
5192
|
+
"Submitted messages do not exactly match the declared removal plan."
|
|
5193
|
+
);
|
|
5194
|
+
}
|
|
5195
|
+
return errors.length > 0 ? { errors } : { errors, messages: expectedProposal };
|
|
5196
|
+
}
|
|
5007
5197
|
function validateContextEditorProposal(input) {
|
|
5008
5198
|
const currentRevision = contextEditorRevision(input.ctx.messages);
|
|
5009
5199
|
const before = metricFor(input.ctx, input.ctx.messages, input.tools);
|
|
@@ -5055,7 +5245,23 @@ function validateContextEditorProposal(input) {
|
|
|
5055
5245
|
repair: emptyRepair
|
|
5056
5246
|
};
|
|
5057
5247
|
}
|
|
5058
|
-
const
|
|
5248
|
+
const removalPlan = validateRemovalPlan(
|
|
5249
|
+
input.removals,
|
|
5250
|
+
input.ctx.messages,
|
|
5251
|
+
parsed.messages
|
|
5252
|
+
);
|
|
5253
|
+
if (removalPlan.errors.length > 0 || !removalPlan.messages) {
|
|
5254
|
+
return {
|
|
5255
|
+
ok: false,
|
|
5256
|
+
baseRevision: input.baseRevision,
|
|
5257
|
+
currentRevision,
|
|
5258
|
+
before,
|
|
5259
|
+
validationErrors: removalPlan.errors,
|
|
5260
|
+
warnings: [],
|
|
5261
|
+
repair: emptyRepair
|
|
5262
|
+
};
|
|
5263
|
+
}
|
|
5264
|
+
const repaired = repairToolUseAdjacency(removalPlan.messages);
|
|
5059
5265
|
const repair = {
|
|
5060
5266
|
changed: repaired.report.changed,
|
|
5061
5267
|
removedToolUses: repaired.report.removedToolUses,
|
|
@@ -5305,7 +5511,7 @@ import {
|
|
|
5305
5511
|
getKanbanServerConnection,
|
|
5306
5512
|
isKanbanServerAvailable
|
|
5307
5513
|
} from "@wrongstack/kanban";
|
|
5308
|
-
import * as
|
|
5514
|
+
import * as net from "node:net";
|
|
5309
5515
|
import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
|
|
5310
5516
|
import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
|
|
5311
5517
|
import {
|
|
@@ -6149,7 +6355,7 @@ var RESTART_POLL_INTERVAL_MS = 250;
|
|
|
6149
6355
|
var RESTART_DEADLINE_MS = 3e3;
|
|
6150
6356
|
function isEndpointAlive(endpoint) {
|
|
6151
6357
|
return new Promise((resolve16) => {
|
|
6152
|
-
const sock =
|
|
6358
|
+
const sock = net.createConnection(endpoint);
|
|
6153
6359
|
const timer = setTimeout(() => {
|
|
6154
6360
|
sock.destroy();
|
|
6155
6361
|
resolve16(false);
|
|
@@ -9082,19 +9288,22 @@ function extractToken(url) {
|
|
|
9082
9288
|
function extractTokenFromCookie(cookieHeader) {
|
|
9083
9289
|
if (!cookieHeader) return void 0;
|
|
9084
9290
|
const raw = Array.isArray(cookieHeader) ? cookieHeader.join("; ") : cookieHeader;
|
|
9291
|
+
let plain;
|
|
9085
9292
|
for (const part of raw.split(";")) {
|
|
9086
9293
|
const eq = part.indexOf("=");
|
|
9087
9294
|
if (eq < 0) continue;
|
|
9088
9295
|
const name2 = part.slice(0, eq).trim();
|
|
9089
|
-
if (name2
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9296
|
+
if (name2 !== "ws_token" && name2 !== "__Host-ws_token") continue;
|
|
9297
|
+
let value;
|
|
9298
|
+
try {
|
|
9299
|
+
value = decodeURIComponent(part.slice(eq + 1).trim());
|
|
9300
|
+
} catch {
|
|
9301
|
+
value = part.slice(eq + 1).trim();
|
|
9095
9302
|
}
|
|
9303
|
+
if (name2 === "__Host-ws_token") return value;
|
|
9304
|
+
plain ??= value;
|
|
9096
9305
|
}
|
|
9097
|
-
return
|
|
9306
|
+
return plain;
|
|
9098
9307
|
}
|
|
9099
9308
|
function hostHeaderOk(input) {
|
|
9100
9309
|
if (!isLoopbackBind(input.wsHost)) return true;
|
|
@@ -9136,7 +9345,8 @@ function verifyClient(input) {
|
|
|
9136
9345
|
expectedToken,
|
|
9137
9346
|
requireToken,
|
|
9138
9347
|
allowedHostnames,
|
|
9139
|
-
allowBrowserUrlToken
|
|
9348
|
+
allowBrowserUrlToken,
|
|
9349
|
+
allowCrossPortLoopbackCookie
|
|
9140
9350
|
} = input;
|
|
9141
9351
|
const urlTokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
|
|
9142
9352
|
const cookieTokenOk = tokenMatches(extractTokenFromCookie(cookieHeader), expectedToken);
|
|
@@ -9151,7 +9361,10 @@ function verifyClient(input) {
|
|
|
9151
9361
|
const { hostname: originHostname } = new URL(origin);
|
|
9152
9362
|
if (isLoopbackHostname(originHostname)) {
|
|
9153
9363
|
if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
|
|
9154
|
-
|
|
9364
|
+
if (!isTrustedLoopbackOrigin(origin, hostHeader)) {
|
|
9365
|
+
return Boolean(allowCrossPortLoopbackCookie) && cookieTokenOk;
|
|
9366
|
+
}
|
|
9367
|
+
return true;
|
|
9155
9368
|
}
|
|
9156
9369
|
return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
|
|
9157
9370
|
} catch {
|
|
@@ -9186,11 +9399,22 @@ ${out}`;
|
|
|
9186
9399
|
function firstHeader(value) {
|
|
9187
9400
|
return Array.isArray(value) ? value[0] : value;
|
|
9188
9401
|
}
|
|
9189
|
-
|
|
9190
|
-
|
|
9402
|
+
var WS_TOKEN_COOKIE = "ws_token";
|
|
9403
|
+
var WS_TOKEN_COOKIE_SECURE = "__Host-ws_token";
|
|
9404
|
+
function wsTokenCookie(token, secure) {
|
|
9405
|
+
const name2 = secure ? WS_TOKEN_COOKIE_SECURE : WS_TOKEN_COOKIE;
|
|
9406
|
+
const parts = [
|
|
9407
|
+
`${name2}=${encodeURIComponent(token)}`,
|
|
9408
|
+
"HttpOnly",
|
|
9409
|
+
"SameSite=Strict",
|
|
9410
|
+
"Path=/",
|
|
9411
|
+
"Max-Age=3600"
|
|
9412
|
+
];
|
|
9413
|
+
if (secure) parts.push("Secure");
|
|
9414
|
+
return parts.join("; ");
|
|
9191
9415
|
}
|
|
9192
|
-
function setAuthCookieHeaders(res, token) {
|
|
9193
|
-
res.setHeader("Set-Cookie", wsTokenCookie(token));
|
|
9416
|
+
function setAuthCookieHeaders(res, token, secure) {
|
|
9417
|
+
res.setHeader("Set-Cookie", wsTokenCookie(token, secure));
|
|
9194
9418
|
res.setHeader("Cache-Control", "no-store");
|
|
9195
9419
|
}
|
|
9196
9420
|
function setStaticSecurityHeaders(res) {
|
|
@@ -9198,8 +9422,16 @@ function setStaticSecurityHeaders(res) {
|
|
|
9198
9422
|
res.setHeader("X-Frame-Options", "DENY");
|
|
9199
9423
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
9200
9424
|
}
|
|
9201
|
-
function requestToken(req, url) {
|
|
9202
|
-
|
|
9425
|
+
function requestToken(req, url, opts = {}) {
|
|
9426
|
+
const queryToken = url.searchParams.get("token") ?? void 0;
|
|
9427
|
+
if (queryToken !== void 0 && (opts.allowQuery === true || isLoopbackPeer(req))) {
|
|
9428
|
+
return queryToken;
|
|
9429
|
+
}
|
|
9430
|
+
return firstHeader(req.headers["x-ws-token"]) ?? extractTokenFromCookie(req.headers.cookie);
|
|
9431
|
+
}
|
|
9432
|
+
function isLoopbackPeer(req) {
|
|
9433
|
+
const address = req.socket.remoteAddress?.replace(/^::ffff:/i, "");
|
|
9434
|
+
return address !== void 0 && isLoopbackHostname(address);
|
|
9203
9435
|
}
|
|
9204
9436
|
function formatCspHostname(hostname) {
|
|
9205
9437
|
return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
@@ -9254,7 +9486,8 @@ function strictDecodeParam(segment, res) {
|
|
|
9254
9486
|
function createHttpServer(opts) {
|
|
9255
9487
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
9256
9488
|
const distDir = path13.resolve(opts.distDir);
|
|
9257
|
-
const requireAccessToken =
|
|
9489
|
+
const requireAccessToken = true;
|
|
9490
|
+
const secureCookies = opts.secureCookies ?? (opts.publicWsUrl?.trim().toLowerCase().startsWith("wss:") ?? false);
|
|
9258
9491
|
const trustedHostnames = (() => {
|
|
9259
9492
|
const names = [...opts.allowedHostnames ?? []];
|
|
9260
9493
|
if (opts.publicWsUrl) {
|
|
@@ -9293,13 +9526,13 @@ function createHttpServer(opts) {
|
|
|
9293
9526
|
const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
|
|
9294
9527
|
const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
|
|
9295
9528
|
if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
|
|
9296
|
-
const provided = requestToken(req, url);
|
|
9529
|
+
const provided = requestToken(req, url, { allowQuery: true });
|
|
9297
9530
|
if (!provided || !opts.apiToken || !tokenMatches(provided, opts.apiToken)) {
|
|
9298
9531
|
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
9299
9532
|
res.end("Unauthorized");
|
|
9300
9533
|
return;
|
|
9301
9534
|
}
|
|
9302
|
-
setAuthCookieHeaders(res, opts.apiToken);
|
|
9535
|
+
setAuthCookieHeaders(res, opts.apiToken, secureCookies);
|
|
9303
9536
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
9304
9537
|
res.end("ok");
|
|
9305
9538
|
return;
|
|
@@ -9313,7 +9546,7 @@ function createHttpServer(opts) {
|
|
|
9313
9546
|
return;
|
|
9314
9547
|
}
|
|
9315
9548
|
if (shouldSetAuthCookie && opts.apiToken) {
|
|
9316
|
-
setAuthCookieHeaders(res, opts.apiToken);
|
|
9549
|
+
setAuthCookieHeaders(res, opts.apiToken, secureCookies);
|
|
9317
9550
|
}
|
|
9318
9551
|
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
9319
9552
|
if (requireAccessToken && !accessTokenOk) {
|
|
@@ -12029,6 +12262,55 @@ import {
|
|
|
12029
12262
|
restartMcp,
|
|
12030
12263
|
updateMcp
|
|
12031
12264
|
} from "@wrongstack/mcp";
|
|
12265
|
+
|
|
12266
|
+
// src/server/privileged-actions.ts
|
|
12267
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
12268
|
+
import {
|
|
12269
|
+
isTrustDecisionAllowed
|
|
12270
|
+
} from "@wrongstack/core/security";
|
|
12271
|
+
async function authorizeWebUIAction(boundary, action, logger) {
|
|
12272
|
+
const request = {
|
|
12273
|
+
version: 1,
|
|
12274
|
+
requestId: randomUUID3(),
|
|
12275
|
+
actor: {
|
|
12276
|
+
kind: "remote-client",
|
|
12277
|
+
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
12278
|
+
},
|
|
12279
|
+
surface: "webui",
|
|
12280
|
+
capability: action.capability,
|
|
12281
|
+
subject: action.subject,
|
|
12282
|
+
risk: action.risk,
|
|
12283
|
+
scope: {
|
|
12284
|
+
...action.cwd ? { cwd: action.cwd } : {},
|
|
12285
|
+
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
12286
|
+
},
|
|
12287
|
+
authContext: { method: "session" },
|
|
12288
|
+
...action.metadata ? { metadata: action.metadata } : {}
|
|
12289
|
+
};
|
|
12290
|
+
const decision = await boundary.evaluate(request);
|
|
12291
|
+
logger?.debug?.(
|
|
12292
|
+
`[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
|
|
12293
|
+
);
|
|
12294
|
+
return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
|
|
12295
|
+
}
|
|
12296
|
+
|
|
12297
|
+
// src/server/mcp-handlers.ts
|
|
12298
|
+
async function authorizeMcpMutation(ws, operation, serverName, trustBoundary) {
|
|
12299
|
+
if (!trustBoundary) return true;
|
|
12300
|
+
const authorization = await authorizeWebUIAction(trustBoundary, {
|
|
12301
|
+
capability: "mcp.server.configure",
|
|
12302
|
+
subject: { kind: "process", id: serverName },
|
|
12303
|
+
risk: "elevated",
|
|
12304
|
+
metadata: { transport: "websocket", operation }
|
|
12305
|
+
});
|
|
12306
|
+
if (!authorization.allowed) {
|
|
12307
|
+
send(ws, {
|
|
12308
|
+
type: "mcp.operation_result",
|
|
12309
|
+
payload: { success: false, message: `${operation} denied: ${authorization.reason}` }
|
|
12310
|
+
});
|
|
12311
|
+
}
|
|
12312
|
+
return authorization.allowed;
|
|
12313
|
+
}
|
|
12032
12314
|
function mapStatus(raw) {
|
|
12033
12315
|
switch (raw) {
|
|
12034
12316
|
case "connected":
|
|
@@ -12103,7 +12385,7 @@ async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
|
|
|
12103
12385
|
payload: { servers: servers.map((server) => toView(server, health.get(server.name))) }
|
|
12104
12386
|
});
|
|
12105
12387
|
}
|
|
12106
|
-
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
12388
|
+
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry, trustBoundary) {
|
|
12107
12389
|
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
12108
12390
|
if (!d) return;
|
|
12109
12391
|
const validated = validateMcpServerPayload(msg.payload, "mcp.add");
|
|
@@ -12114,6 +12396,7 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
12114
12396
|
});
|
|
12115
12397
|
return;
|
|
12116
12398
|
}
|
|
12399
|
+
if (!await authorizeMcpMutation(ws, "mcp.add", name(msg), trustBoundary)) return;
|
|
12117
12400
|
const result = await addMcp(validated.value, d);
|
|
12118
12401
|
if (result.ok && result.server) {
|
|
12119
12402
|
send(ws, { type: "mcp.server.added", payload: { server: toView(result.server) } });
|
|
@@ -12131,7 +12414,7 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
12131
12414
|
payload: { success: result.ok, message: result.message }
|
|
12132
12415
|
});
|
|
12133
12416
|
}
|
|
12134
|
-
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
12417
|
+
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry, trustBoundary) {
|
|
12135
12418
|
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
12136
12419
|
if (!d) return;
|
|
12137
12420
|
const validated = validateMcpServerPayload(msg.payload, "mcp.update");
|
|
@@ -12142,6 +12425,7 @@ async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
12142
12425
|
});
|
|
12143
12426
|
return;
|
|
12144
12427
|
}
|
|
12428
|
+
if (!await authorizeMcpMutation(ws, "mcp.update", name(msg), trustBoundary)) return;
|
|
12145
12429
|
const result = await updateMcp(validated.value, d);
|
|
12146
12430
|
if (result.ok && result.server) {
|
|
12147
12431
|
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
@@ -13285,7 +13569,7 @@ function openBrowser(url, platform = process.platform) {
|
|
|
13285
13569
|
}
|
|
13286
13570
|
|
|
13287
13571
|
// src/server/port-utils.ts
|
|
13288
|
-
import * as
|
|
13572
|
+
import * as net2 from "node:net";
|
|
13289
13573
|
import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
13290
13574
|
var SURFACE_DEFAULT_PORTS = {
|
|
13291
13575
|
webui: { http: 3456 },
|
|
@@ -13299,7 +13583,7 @@ function getSurfaceDefaultPorts(surface) {
|
|
|
13299
13583
|
}
|
|
13300
13584
|
function isPortFree(host, port) {
|
|
13301
13585
|
return new Promise((resolve16) => {
|
|
13302
|
-
const srv =
|
|
13586
|
+
const srv = net2.createServer();
|
|
13303
13587
|
srv.once("error", () => resolve16(false));
|
|
13304
13588
|
srv.once("listening", () => {
|
|
13305
13589
|
srv.close(() => resolve16(true));
|
|
@@ -13560,7 +13844,8 @@ function registerWebuiInstance(p, deps2 = {}) {
|
|
|
13560
13844
|
host: p.host,
|
|
13561
13845
|
port: p.httpPort,
|
|
13562
13846
|
publicUrl: p.publicUrl
|
|
13563
|
-
})
|
|
13847
|
+
}),
|
|
13848
|
+
...p.authToken ? { authToken: p.authToken } : {}
|
|
13564
13849
|
},
|
|
13565
13850
|
p.registryBaseDir
|
|
13566
13851
|
).catch(() => {
|
|
@@ -13594,10 +13879,31 @@ ${extraBlock}`
|
|
|
13594
13879
|
if (p.open) launch(openUrl);
|
|
13595
13880
|
});
|
|
13596
13881
|
}
|
|
13882
|
+
var DEFAULT_CHILD_CLEANUP_TIMEOUT_MS = 1e4;
|
|
13883
|
+
async function runBounded(work, timeoutMs, label, debug) {
|
|
13884
|
+
let timer;
|
|
13885
|
+
try {
|
|
13886
|
+
await Promise.race([
|
|
13887
|
+
Promise.resolve().then(() => work()).catch((err) => {
|
|
13888
|
+
debug(`[webui-server] ${label} failed: ${err}`);
|
|
13889
|
+
}),
|
|
13890
|
+
new Promise((resolve16) => {
|
|
13891
|
+
timer = setTimeout(() => {
|
|
13892
|
+
debug(`[webui-server] ${label} timed out after ${timeoutMs}ms`);
|
|
13893
|
+
resolve16();
|
|
13894
|
+
}, timeoutMs);
|
|
13895
|
+
timer.unref?.();
|
|
13896
|
+
})
|
|
13897
|
+
]);
|
|
13898
|
+
} finally {
|
|
13899
|
+
if (timer) clearTimeout(timer);
|
|
13900
|
+
}
|
|
13901
|
+
}
|
|
13597
13902
|
function createWebuiShutdown(res) {
|
|
13598
13903
|
const log = res.log ?? ((m) => console.log(m));
|
|
13599
13904
|
const debug = res.debug ?? ((m) => console.debug(m));
|
|
13600
13905
|
const unregister = res.unregisterFn ?? unregisterInstance;
|
|
13906
|
+
const childTimeout = Math.max(1, res.childCleanupTimeoutMs ?? DEFAULT_CHILD_CLEANUP_TIMEOUT_MS);
|
|
13601
13907
|
let started = false;
|
|
13602
13908
|
return () => {
|
|
13603
13909
|
if (started) return;
|
|
@@ -13605,17 +13911,30 @@ function createWebuiShutdown(res) {
|
|
|
13605
13911
|
log("[WebUI] Shutting down...");
|
|
13606
13912
|
res.abortInFlight();
|
|
13607
13913
|
res.unsubscribeEvents();
|
|
13608
|
-
|
|
13609
|
-
|
|
13610
|
-
|
|
13611
|
-
|
|
13612
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
13914
|
+
void (async () => {
|
|
13915
|
+
if (res.stopOwnedChildren) {
|
|
13916
|
+
await runBounded(res.stopOwnedChildren, childTimeout, "stopOwnedChildren", debug);
|
|
13917
|
+
}
|
|
13918
|
+
if (res.disposeResources) {
|
|
13919
|
+
await runBounded(res.disposeResources, Math.min(childTimeout, 5e3), "disposeResources", debug);
|
|
13920
|
+
}
|
|
13921
|
+
res.closeClients();
|
|
13922
|
+
res.closeHttpServer();
|
|
13923
|
+
const unregistered = unregister(res.pid, res.registryBaseDir).catch(
|
|
13924
|
+
(err) => debug(`[webui-server] unregister failed: ${err}`)
|
|
13925
|
+
);
|
|
13926
|
+
await new Promise((resolve16) => {
|
|
13927
|
+
res.wss.close(() => resolve16());
|
|
13618
13928
|
});
|
|
13929
|
+
await unregistered;
|
|
13930
|
+
log("[WebUI] Server stopped");
|
|
13931
|
+
res.onStopped();
|
|
13932
|
+
})().catch((err) => {
|
|
13933
|
+
debug(`[webui-server] shutdown sequence failed: ${err}`);
|
|
13934
|
+
try {
|
|
13935
|
+
res.onStopped();
|
|
13936
|
+
} catch {
|
|
13937
|
+
}
|
|
13619
13938
|
});
|
|
13620
13939
|
};
|
|
13621
13940
|
}
|
|
@@ -14723,6 +15042,8 @@ function buildAuditPrompt(board, health) {
|
|
|
14723
15042
|
|
|
14724
15043
|
// src/server/context-meta.ts
|
|
14725
15044
|
import { FallbackProfileManager } from "@wrongstack/core/agent";
|
|
15045
|
+
import { resolvePluginEnablement } from "@wrongstack/core/plugin";
|
|
15046
|
+
import { FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
|
|
14726
15047
|
function seedContextMeta(config, context) {
|
|
14727
15048
|
const meta = context.meta;
|
|
14728
15049
|
const autonomyCfg = config.autonomy ?? {};
|
|
@@ -14799,6 +15120,21 @@ function seedContextMeta(config, context) {
|
|
|
14799
15120
|
meta["tgDelegate"] = tgExt?.["notifyOnDelegate"] !== false;
|
|
14800
15121
|
const tgMs = tgExt?.["longToolThresholdMs"];
|
|
14801
15122
|
meta["tgLongToolMs"] = typeof tgMs === "number" ? tgMs : 3e4;
|
|
15123
|
+
{
|
|
15124
|
+
const pluginsEnabled = {};
|
|
15125
|
+
const record2 = (name2) => {
|
|
15126
|
+
if (FORBIDDEN_PROTO_KEYS2.has(name2) || name2 in pluginsEnabled) return;
|
|
15127
|
+
pluginsEnabled[name2] = resolvePluginEnablement({ name: name2, config }).enabled;
|
|
15128
|
+
};
|
|
15129
|
+
for (const entry of config.plugins ?? []) {
|
|
15130
|
+
const name2 = typeof entry === "string" ? entry : entry?.name;
|
|
15131
|
+
if (typeof name2 === "string") record2(name2);
|
|
15132
|
+
}
|
|
15133
|
+
for (const [name2, options] of Object.entries(config.extensions ?? {})) {
|
|
15134
|
+
if (typeof options?.["enabled"] === "boolean") record2(name2);
|
|
15135
|
+
}
|
|
15136
|
+
if (Object.keys(pluginsEnabled).length > 0) meta["pluginsEnabled"] = pluginsEnabled;
|
|
15137
|
+
}
|
|
14802
15138
|
const chimeraExt = config.extensions?.["wstack-chimera"];
|
|
14803
15139
|
meta["chimeraEnabled"] = chimeraExt?.["enabled"] === true;
|
|
14804
15140
|
meta["chimeraProvider"] = chimeraExt?.["provider"] ?? "";
|
|
@@ -14836,8 +15172,9 @@ function seedContextMeta(config, context) {
|
|
|
14836
15172
|
// src/server/pref-helpers.ts
|
|
14837
15173
|
import * as fs13 from "node:fs/promises";
|
|
14838
15174
|
import * as path18 from "node:path";
|
|
15175
|
+
import { pluginEntryMatchesName } from "@wrongstack/core/plugin";
|
|
14839
15176
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
14840
|
-
import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as
|
|
15177
|
+
import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS3 } from "@wrongstack/core/utils";
|
|
14841
15178
|
var PREF_KEYS = [
|
|
14842
15179
|
"autonomy",
|
|
14843
15180
|
"autonomyDelayMs",
|
|
@@ -14918,6 +15255,8 @@ var PREF_KEYS = [
|
|
|
14918
15255
|
// Display-only toggles (purely visual WebUI prefs, not persisted to config).
|
|
14919
15256
|
"groupToolCalls",
|
|
14920
15257
|
"showThinkingLogs",
|
|
15258
|
+
// v15: chat-input auto-collapse (opt-in display toggle, default off).
|
|
15259
|
+
"autoCollapseInput",
|
|
14921
15260
|
// Per-plugin enable/disable map (parity with the embedded server).
|
|
14922
15261
|
"pluginsEnabled",
|
|
14923
15262
|
// Fleet chat verbosity: off | full (migrated from streamFleet boolean).
|
|
@@ -14972,6 +15311,8 @@ async function updateGlobalConfig(deps2, holder, mutate, errorLabel) {
|
|
|
14972
15311
|
var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set([
|
|
14973
15312
|
"groupToolCalls",
|
|
14974
15313
|
"showThinkingLogs",
|
|
15314
|
+
// v15: chat-input auto-collapse (opt-in display toggle, default off).
|
|
15315
|
+
"autoCollapseInput",
|
|
14975
15316
|
"autoReviewFallbackModels",
|
|
14976
15317
|
// v11 Display parity: agent-swarm panel + inverse fsAccess flag.
|
|
14977
15318
|
// The TUI settings picker mirrors these so the browser can keep the
|
|
@@ -15182,15 +15523,29 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
15182
15523
|
decrypted.debugStream = payload["debugStream"];
|
|
15183
15524
|
if (typeof payload["pluginsEnabled"] === "object" && payload["pluginsEnabled"] !== null) {
|
|
15184
15525
|
const ext = decrypted.extensions ?? {};
|
|
15526
|
+
const toggled = [];
|
|
15185
15527
|
for (const [pluginName, enabled] of Object.entries(
|
|
15186
15528
|
payload["pluginsEnabled"]
|
|
15187
15529
|
)) {
|
|
15188
|
-
if (
|
|
15530
|
+
if (FORBIDDEN_PROTO_KEYS3.has(pluginName)) continue;
|
|
15531
|
+
if (typeof enabled !== "boolean") continue;
|
|
15189
15532
|
const pExt = ext[pluginName] ?? {};
|
|
15190
15533
|
pExt["enabled"] = enabled;
|
|
15191
15534
|
ext[pluginName] = pExt;
|
|
15535
|
+
toggled.push([pluginName, enabled]);
|
|
15192
15536
|
}
|
|
15193
15537
|
decrypted.extensions = ext;
|
|
15538
|
+
if (Array.isArray(decrypted.plugins) && toggled.length > 0) {
|
|
15539
|
+
decrypted.plugins = decrypted.plugins.map((entry) => {
|
|
15540
|
+
const entryName = typeof entry === "string" ? entry : entry?.name;
|
|
15541
|
+
if (typeof entryName !== "string") return entry;
|
|
15542
|
+
const hit = toggled.find(([name2]) => pluginEntryMatchesName(entryName, name2));
|
|
15543
|
+
if (!hit) return entry;
|
|
15544
|
+
const [, enabled] = hit;
|
|
15545
|
+
if (typeof entry === "string") return enabled ? entry : { name: entry, enabled: false };
|
|
15546
|
+
return { ...entry, enabled };
|
|
15547
|
+
});
|
|
15548
|
+
}
|
|
15194
15549
|
}
|
|
15195
15550
|
const chimeraTouched = typeof payload["chimeraEnabled"] === "boolean" || typeof payload["chimeraProvider"] === "string" || typeof payload["chimeraModel"] === "string" || typeof payload["chimeraMaxFiles"] === "number" || typeof payload["chimeraAutoFix"] === "string";
|
|
15196
15551
|
if (chimeraTouched) {
|
|
@@ -15299,39 +15654,6 @@ async function handlePrefsRoute(ws, msg, handlers) {
|
|
|
15299
15654
|
// src/server/process-handlers.ts
|
|
15300
15655
|
import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
|
|
15301
15656
|
import { getProcessRegistry as getProcessRegistry2 } from "@wrongstack/tools";
|
|
15302
|
-
|
|
15303
|
-
// src/server/privileged-actions.ts
|
|
15304
|
-
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
15305
|
-
import {
|
|
15306
|
-
isTrustDecisionAllowed
|
|
15307
|
-
} from "@wrongstack/core/security";
|
|
15308
|
-
async function authorizeWebUIAction(boundary, action, logger) {
|
|
15309
|
-
const request = {
|
|
15310
|
-
version: 1,
|
|
15311
|
-
requestId: randomUUID4(),
|
|
15312
|
-
actor: {
|
|
15313
|
-
kind: "remote-client",
|
|
15314
|
-
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
15315
|
-
},
|
|
15316
|
-
surface: "webui",
|
|
15317
|
-
capability: action.capability,
|
|
15318
|
-
subject: action.subject,
|
|
15319
|
-
risk: action.risk,
|
|
15320
|
-
scope: {
|
|
15321
|
-
...action.cwd ? { cwd: action.cwd } : {},
|
|
15322
|
-
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
15323
|
-
},
|
|
15324
|
-
authContext: { method: "session" },
|
|
15325
|
-
...action.metadata ? { metadata: action.metadata } : {}
|
|
15326
|
-
};
|
|
15327
|
-
const decision = await boundary.evaluate(request);
|
|
15328
|
-
logger?.debug?.(
|
|
15329
|
-
`[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
|
|
15330
|
-
);
|
|
15331
|
-
return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
|
|
15332
|
-
}
|
|
15333
|
-
|
|
15334
|
-
// src/server/process-handlers.ts
|
|
15335
15657
|
function handleProcessList(ws) {
|
|
15336
15658
|
try {
|
|
15337
15659
|
const procs = getProcessRegistry2().list();
|
|
@@ -16155,8 +16477,10 @@ function createProviderOperations(deps2) {
|
|
|
16155
16477
|
if (result.ok) {
|
|
16156
16478
|
deps2.log?.(`[WebUI] Provider "${payload.id}" added via provider.add`);
|
|
16157
16479
|
}
|
|
16480
|
+
return result.ok;
|
|
16158
16481
|
} catch (err) {
|
|
16159
16482
|
sendOperationResult(ws, false, errMessage(err));
|
|
16483
|
+
return false;
|
|
16160
16484
|
}
|
|
16161
16485
|
}
|
|
16162
16486
|
async function handleProviderRemove(ws, providerId) {
|
|
@@ -16540,6 +16864,7 @@ var CLIENT_CONVERSATION_MESSAGE_TYPES = [
|
|
|
16540
16864
|
"completion.request",
|
|
16541
16865
|
"model.switch",
|
|
16542
16866
|
"model.refine",
|
|
16867
|
+
"model.fallback_choice",
|
|
16543
16868
|
"autonomy.switch",
|
|
16544
16869
|
"context.clear",
|
|
16545
16870
|
"context.compact",
|
|
@@ -16811,6 +17136,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
|
|
|
16811
17136
|
"provider.active_blocked",
|
|
16812
17137
|
"provider.error",
|
|
16813
17138
|
"provider.fallback",
|
|
17139
|
+
"provider.fallback_pending",
|
|
16814
17140
|
"provider.response",
|
|
16815
17141
|
"provider.retry",
|
|
16816
17142
|
"provider.status_changed",
|
|
@@ -16852,6 +17178,7 @@ var SERVER_COLLABORATION_MESSAGE_TYPES = [
|
|
|
16852
17178
|
"collab.state",
|
|
16853
17179
|
"mailbox.action_result",
|
|
16854
17180
|
"mailbox.agent_registered",
|
|
17181
|
+
"mailbox.agent_deregistered",
|
|
16855
17182
|
"mailbox.agents",
|
|
16856
17183
|
"mailbox.cleared",
|
|
16857
17184
|
"mailbox.compacted",
|
|
@@ -17231,6 +17558,13 @@ function projectToolMessage(message) {
|
|
|
17231
17558
|
}
|
|
17232
17559
|
return null;
|
|
17233
17560
|
}
|
|
17561
|
+
function optionalFinite(value) {
|
|
17562
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
17563
|
+
return value;
|
|
17564
|
+
}
|
|
17565
|
+
function optionalString2(value) {
|
|
17566
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
17567
|
+
}
|
|
17234
17568
|
function projectFleetMessage(message) {
|
|
17235
17569
|
const payload = record(message.payload);
|
|
17236
17570
|
if (!payload) return null;
|
|
@@ -17239,7 +17573,15 @@ function projectFleetMessage(message) {
|
|
|
17239
17573
|
return {
|
|
17240
17574
|
kind: "concurrency",
|
|
17241
17575
|
active: finite(payload["fleetConcurrency"]),
|
|
17242
|
-
maximum: finite(payload["fleetConcurrencyMax"])
|
|
17576
|
+
maximum: finite(payload["fleetConcurrencyMax"]),
|
|
17577
|
+
maxSpawns: optionalFinite(payload["maxSpawns"]),
|
|
17578
|
+
usedSpawns: optionalFinite(payload["usedSpawns"]),
|
|
17579
|
+
remainingSpawns: optionalFinite(payload["remainingSpawns"]),
|
|
17580
|
+
maxSpawnsSource: optionalString2(payload["maxSpawnsSource"]),
|
|
17581
|
+
maxConcurrentSource: optionalString2(payload["maxConcurrentSource"]),
|
|
17582
|
+
effectiveSource: optionalString2(payload["effectiveSource"]),
|
|
17583
|
+
checkpointMaxSpawns: optionalFinite(payload["checkpointMaxSpawns"]),
|
|
17584
|
+
ceilingMismatch: payload["ceilingMismatch"] === true ? true : void 0
|
|
17243
17585
|
};
|
|
17244
17586
|
case "client.status_update":
|
|
17245
17587
|
return { kind: "client-status", status: payload };
|
|
@@ -17895,6 +18237,7 @@ function createSessionHandlers(ctx) {
|
|
|
17895
18237
|
tools: ctx.listTools?.() ?? ctx.toolRegistry?.list(),
|
|
17896
18238
|
baseRevision: typeof payload["baseRevision"] === "string" ? payload["baseRevision"] : "",
|
|
17897
18239
|
messages: payload["messages"],
|
|
18240
|
+
removals: payload["removals"],
|
|
17898
18241
|
allowRepair: payload["allowRepair"] === true,
|
|
17899
18242
|
runActive: ctx.isRunActive?.() === true
|
|
17900
18243
|
});
|
|
@@ -17911,6 +18254,7 @@ function createSessionHandlers(ctx) {
|
|
|
17911
18254
|
tools: ctx.listTools?.() ?? ctx.toolRegistry?.list(),
|
|
17912
18255
|
baseRevision: typeof payload["baseRevision"] === "string" ? payload["baseRevision"] : "",
|
|
17913
18256
|
messages: payload["messages"],
|
|
18257
|
+
removals: payload["removals"],
|
|
17914
18258
|
allowRepair: payload["allowRepair"] === true,
|
|
17915
18259
|
runActive: ctx.isRunActive?.() === true
|
|
17916
18260
|
});
|
|
@@ -18892,6 +19236,19 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
|
|
|
18892
19236
|
return true;
|
|
18893
19237
|
}
|
|
18894
19238
|
|
|
19239
|
+
// src/server/fallback-choice.ts
|
|
19240
|
+
function emitFallbackChoice(events, msg) {
|
|
19241
|
+
const parsed = validateModelFallbackChoicePayload(msg.payload);
|
|
19242
|
+
if (!parsed.ok) return parsed;
|
|
19243
|
+
events?.emit("provider.fallback_choice", {
|
|
19244
|
+
requestId: parsed.value.requestId,
|
|
19245
|
+
...parsed.value.providerId ? { providerId: parsed.value.providerId } : {},
|
|
19246
|
+
...parsed.value.model ? { model: parsed.value.model } : {},
|
|
19247
|
+
...parsed.value.autoSwitch ? { autoSwitch: true } : {}
|
|
19248
|
+
});
|
|
19249
|
+
return { ok: true };
|
|
19250
|
+
}
|
|
19251
|
+
|
|
18895
19252
|
// src/server/agent-roster-routes.ts
|
|
18896
19253
|
async function handleAgentRosterRoute(ws, msg, handlers) {
|
|
18897
19254
|
if (!msg.type.startsWith("agent-roster.")) return false;
|
|
@@ -19056,13 +19413,17 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19056
19413
|
case "model.refine":
|
|
19057
19414
|
await routes.refineModel(ws, msg);
|
|
19058
19415
|
return true;
|
|
19416
|
+
case "model.fallback_choice":
|
|
19417
|
+
await routes.fallbackChoice(ws, msg);
|
|
19418
|
+
return true;
|
|
19059
19419
|
case "key.add":
|
|
19060
19420
|
case "key.update": {
|
|
19061
19421
|
const payload = asPayloadRecord(msg);
|
|
19062
19422
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19063
19423
|
const label = payload ? requiredString(payload, "label") : null;
|
|
19064
19424
|
const apiKey = payload ? requiredString(payload, "apiKey") : null;
|
|
19065
|
-
if (!providerId || !label || !apiKey)
|
|
19425
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label || !apiKey)
|
|
19426
|
+
return invalidPayload(ws, msg.type);
|
|
19066
19427
|
await routes.providerHandlers.handleKeyUpsert(ws, providerId, label, apiKey);
|
|
19067
19428
|
return true;
|
|
19068
19429
|
}
|
|
@@ -19070,7 +19431,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19070
19431
|
const payload = asPayloadRecord(msg);
|
|
19071
19432
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19072
19433
|
const label = payload ? requiredString(payload, "label") : null;
|
|
19073
|
-
if (!providerId || !
|
|
19434
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label)
|
|
19435
|
+
return invalidPayload(ws, msg.type);
|
|
19074
19436
|
await routes.providerHandlers.handleKeyDelete(ws, providerId, label);
|
|
19075
19437
|
return true;
|
|
19076
19438
|
}
|
|
@@ -19078,7 +19440,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19078
19440
|
const payload = asPayloadRecord(msg);
|
|
19079
19441
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19080
19442
|
const label = payload ? requiredString(payload, "label") : null;
|
|
19081
|
-
if (!providerId || !
|
|
19443
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label)
|
|
19444
|
+
return invalidPayload(ws, msg.type);
|
|
19082
19445
|
await routes.providerHandlers.handleKeySetActive(ws, providerId, label);
|
|
19083
19446
|
return true;
|
|
19084
19447
|
}
|
|
@@ -19090,11 +19453,11 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19090
19453
|
const apiKey = payload?.["apiKey"];
|
|
19091
19454
|
const models = payload ? optionalStringArray(payload, "models") : null;
|
|
19092
19455
|
const customModels = payload ? optionalCustomModels(payload) : null;
|
|
19093
|
-
if (!id || !family) return invalidPayload(ws, msg.type);
|
|
19456
|
+
if (!id || !SAFE_CONFIG_KEY.test(id) || !family) return invalidPayload(ws, msg.type);
|
|
19094
19457
|
if (baseUrl !== void 0 && typeof baseUrl !== "string") return invalidPayload(ws, msg.type);
|
|
19095
19458
|
if (apiKey !== void 0 && typeof apiKey !== "string") return invalidPayload(ws, msg.type);
|
|
19096
19459
|
if (models === null || customModels === null) return invalidPayload(ws, msg.type);
|
|
19097
|
-
await routes.providerHandlers.handleProviderAdd(ws, {
|
|
19460
|
+
const added = await routes.providerHandlers.handleProviderAdd(ws, {
|
|
19098
19461
|
id,
|
|
19099
19462
|
family,
|
|
19100
19463
|
baseUrl,
|
|
@@ -19102,20 +19465,22 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19102
19465
|
models,
|
|
19103
19466
|
customModels
|
|
19104
19467
|
});
|
|
19105
|
-
|
|
19468
|
+
if (added) {
|
|
19469
|
+
void routes.adoptDefaultProviderIfUnset(id).catch(() => void 0);
|
|
19470
|
+
}
|
|
19106
19471
|
return true;
|
|
19107
19472
|
}
|
|
19108
19473
|
case "provider.remove": {
|
|
19109
19474
|
const payload = asPayloadRecord(msg);
|
|
19110
19475
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19111
|
-
if (!providerId) return invalidPayload(ws, msg.type);
|
|
19476
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId)) return invalidPayload(ws, msg.type);
|
|
19112
19477
|
await routes.providerHandlers.handleProviderRemove(ws, providerId);
|
|
19113
19478
|
return true;
|
|
19114
19479
|
}
|
|
19115
19480
|
case "provider.clear_models": {
|
|
19116
19481
|
const payload = asPayloadRecord(msg);
|
|
19117
19482
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19118
|
-
if (!providerId) return invalidPayload(ws, msg.type);
|
|
19483
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId)) return invalidPayload(ws, msg.type);
|
|
19119
19484
|
await routes.providerHandlers.handleProviderClearModels(ws, providerId);
|
|
19120
19485
|
return true;
|
|
19121
19486
|
}
|
|
@@ -19147,7 +19512,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19147
19512
|
const payload = asPayloadRecord(msg);
|
|
19148
19513
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19149
19514
|
const previousModels = payload ? optionalStringArray(payload, "previousModels") : null;
|
|
19150
|
-
if (!providerId || !
|
|
19515
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !previousModels)
|
|
19516
|
+
return invalidPayload(ws, msg.type);
|
|
19151
19517
|
await routes.providerHandlers.handleProviderUndoClear(ws, providerId, previousModels);
|
|
19152
19518
|
return true;
|
|
19153
19519
|
}
|
|
@@ -19157,7 +19523,7 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19157
19523
|
const envVars = payload ? optionalStringArray(payload, "envVars") : null;
|
|
19158
19524
|
const models = payload ? optionalStringArray(payload, "models") : null;
|
|
19159
19525
|
const customModels = payload ? optionalCustomModels(payload) : null;
|
|
19160
|
-
if (!payload || !id || envVars === null || models === null || customModels === null)
|
|
19526
|
+
if (!payload || !id || !SAFE_CONFIG_KEY.test(id) || envVars === null || models === null || customModels === null)
|
|
19161
19527
|
return invalidPayload(ws, msg.type);
|
|
19162
19528
|
for (const key of ["family", "baseUrl"]) {
|
|
19163
19529
|
if (payload[key] !== void 0 && typeof payload[key] !== "string")
|
|
@@ -19177,7 +19543,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19177
19543
|
const payload = asPayloadRecord(msg);
|
|
19178
19544
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19179
19545
|
const timeoutMs = payload ? optionalNumber(payload, "timeoutMs") : null;
|
|
19180
|
-
if (!providerId || timeoutMs === null)
|
|
19546
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || timeoutMs === null)
|
|
19547
|
+
return invalidPayload(ws, msg.type);
|
|
19181
19548
|
await routes.providerHandlers.handleProviderProbe(ws, providerId, timeoutMs);
|
|
19182
19549
|
return true;
|
|
19183
19550
|
}
|
|
@@ -19186,7 +19553,7 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19186
19553
|
const kind = oauthKind(payload);
|
|
19187
19554
|
const providerId = payload?.["providerId"];
|
|
19188
19555
|
if (!kind) return invalidPayload(ws, msg.type);
|
|
19189
|
-
if (providerId !== void 0 && typeof providerId !== "string") {
|
|
19556
|
+
if (providerId !== void 0 && (typeof providerId !== "string" || !SAFE_CONFIG_KEY.test(providerId))) {
|
|
19190
19557
|
return invalidPayload(ws, msg.type);
|
|
19191
19558
|
}
|
|
19192
19559
|
await routes.providerHandlers.handleOAuthStart(ws, kind, providerId);
|
|
@@ -19225,7 +19592,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19225
19592
|
const payload = asPayloadRecord(msg);
|
|
19226
19593
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19227
19594
|
const model = payload ? requiredString(payload, "model") : null;
|
|
19228
|
-
if (!providerId || !
|
|
19595
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !model)
|
|
19596
|
+
return invalidPayload(ws, msg.type);
|
|
19229
19597
|
const released = routes.statusTracker.retryNow(providerId, model);
|
|
19230
19598
|
sendResult2(
|
|
19231
19599
|
ws,
|
|
@@ -19242,7 +19610,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19242
19610
|
const payload = asPayloadRecord(msg);
|
|
19243
19611
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19244
19612
|
const model = payload ? requiredString(payload, "model") : null;
|
|
19245
|
-
if (!providerId || !
|
|
19613
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !model)
|
|
19614
|
+
return invalidPayload(ws, msg.type);
|
|
19246
19615
|
routes.statusTracker.clear(providerId, model);
|
|
19247
19616
|
sendResult2(ws, true, `Cleared tracking for ${providerId}/${model}.`);
|
|
19248
19617
|
return true;
|
|
@@ -19600,8 +19969,10 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
19600
19969
|
};
|
|
19601
19970
|
const mcp = {
|
|
19602
19971
|
list: (ws, msg) => handleMcpList(ws, msg, opts.profileConfigPath, opts.mcpRegistry),
|
|
19603
|
-
add
|
|
19604
|
-
|
|
19972
|
+
// add/update are the spawn-capable pair — they take a `command`/`args`
|
|
19973
|
+
// from the wire and start it. They go past the trust boundary (M1).
|
|
19974
|
+
add: (ws, msg) => handleMcpAdd(ws, msg, opts.profileConfigPath, opts.mcpRegistry, deps2.trustBoundary),
|
|
19975
|
+
update: (ws, msg) => handleMcpUpdate(ws, msg, opts.profileConfigPath, opts.mcpRegistry, deps2.trustBoundary),
|
|
19605
19976
|
remove: (ws, msg) => handleMcpRemove(ws, msg, opts.profileConfigPath, opts.mcpRegistry),
|
|
19606
19977
|
enable: (ws, msg) => handleMcpEnable(ws, msg, opts.profileConfigPath, opts.mcpRegistry),
|
|
19607
19978
|
disable: (ws, msg) => handleMcpDisable(ws, msg, opts.profileConfigPath, opts.mcpRegistry),
|
|
@@ -19688,6 +20059,15 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
19688
20059
|
searchProviderModels: (ws, query, limit) => providerOperations.handleProviderModelsSearch(ws, query, limit),
|
|
19689
20060
|
switchModel: (ws, msg) => modelOperations.switchModel(ws, msg.payload),
|
|
19690
20061
|
refineModel: (ws, msg) => modelOperations.refineModel(ws, msg.payload),
|
|
20062
|
+
fallbackChoice: async (ws, msg) => {
|
|
20063
|
+
const result = emitFallbackChoice(deps2.sessionCtx.opts.events, msg);
|
|
20064
|
+
if (!result.ok) {
|
|
20065
|
+
send2(ws, {
|
|
20066
|
+
type: "error",
|
|
20067
|
+
payload: { phase: "invalid_request", message: result.message }
|
|
20068
|
+
});
|
|
20069
|
+
}
|
|
20070
|
+
},
|
|
19691
20071
|
adoptDefaultProviderIfUnset: providerOperations.adoptDefaultProviderIfUnset,
|
|
19692
20072
|
providerHandlers: providerOperations,
|
|
19693
20073
|
statusTracker: deps2.statusTracker
|
|
@@ -19981,17 +20361,12 @@ function createProviderStore(deps2) {
|
|
|
19981
20361
|
cfg.activeKey = active.label;
|
|
19982
20362
|
}
|
|
19983
20363
|
}
|
|
19984
|
-
function maskedKey2(key) {
|
|
19985
|
-
if (!key) return "\u2014";
|
|
19986
|
-
if (key.length <= 8) return "\u2022".repeat(key.length);
|
|
19987
|
-
return `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
|
|
19988
|
-
}
|
|
19989
20364
|
return {
|
|
19990
20365
|
load: loadSavedProviders2,
|
|
19991
20366
|
save: saveProviders2,
|
|
19992
20367
|
normalizeKeys: normalizeKeys2,
|
|
19993
20368
|
writeKeysBack: writeKeysBack2,
|
|
19994
|
-
maskedKey
|
|
20369
|
+
maskedKey
|
|
19995
20370
|
};
|
|
19996
20371
|
}
|
|
19997
20372
|
|
|
@@ -21732,7 +22107,22 @@ function setupEvents(deps2) {
|
|
|
21732
22107
|
from: e.from,
|
|
21733
22108
|
to: e.to,
|
|
21734
22109
|
status: e.status,
|
|
21735
|
-
providerSwitched: e.providerSwitched
|
|
22110
|
+
providerSwitched: e.providerSwitched,
|
|
22111
|
+
...e.requestId ? { requestId: e.requestId } : {}
|
|
22112
|
+
})
|
|
22113
|
+
});
|
|
22114
|
+
});
|
|
22115
|
+
on("provider.fallback_pending", (e) => {
|
|
22116
|
+
broadcast2(clients, {
|
|
22117
|
+
type: "provider.fallback_pending",
|
|
22118
|
+
payload: sessionPayload2({
|
|
22119
|
+
sessionId: e.sessionId,
|
|
22120
|
+
from: e.from,
|
|
22121
|
+
status: e.status,
|
|
22122
|
+
candidates: e.candidates,
|
|
22123
|
+
autoSwitchSeconds: e.autoSwitchSeconds,
|
|
22124
|
+
requestId: e.requestId,
|
|
22125
|
+
timestamp: e.timestamp
|
|
21736
22126
|
})
|
|
21737
22127
|
});
|
|
21738
22128
|
});
|
|
@@ -21817,6 +22207,15 @@ function setupEvents(deps2) {
|
|
|
21817
22207
|
type: "mailbox.agent_registered",
|
|
21818
22208
|
payload
|
|
21819
22209
|
});
|
|
22210
|
+
}),
|
|
22211
|
+
// Deregistration (subagent retirement) must reach the browser too —
|
|
22212
|
+
// otherwise dead agents linger in the client roster until an unrelated
|
|
22213
|
+
// refresh. Emitted by sqlite-mailbox.deregisterAgent with { agentId }.
|
|
22214
|
+
events.onPattern("mailbox.agent_deregistered", (_e, payload) => {
|
|
22215
|
+
broadcast2(clients, {
|
|
22216
|
+
type: "mailbox.agent_deregistered",
|
|
22217
|
+
payload
|
|
22218
|
+
});
|
|
21820
22219
|
})
|
|
21821
22220
|
);
|
|
21822
22221
|
const forwardSubagent = (kind, payload) => broadcast2(clients, { type: "subagent.event", payload: sessionPayload2({ kind, ...payload }) });
|
|
@@ -24101,29 +24500,10 @@ import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/too
|
|
|
24101
24500
|
// src/server/model-auto-discovery.ts
|
|
24102
24501
|
import * as fs21 from "node:fs/promises";
|
|
24103
24502
|
import * as path29 from "node:path";
|
|
24104
|
-
import {
|
|
24503
|
+
import { discoverOpenAICompatibleModels, resolveDiscoveryTargets } from "@wrongstack/providers";
|
|
24105
24504
|
function isOverlayRegistry(value) {
|
|
24106
24505
|
return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
|
|
24107
24506
|
}
|
|
24108
|
-
function resolveKey(cfg) {
|
|
24109
|
-
if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
|
|
24110
|
-
const active = cfg.activeKey ? cfg.apiKeys.find((key) => key.label === cfg.activeKey) : void 0;
|
|
24111
|
-
return (active ?? cfg.apiKeys[0])?.apiKey;
|
|
24112
|
-
}
|
|
24113
|
-
return cfg.apiKey && cfg.apiKey.length > 0 ? cfg.apiKey : void 0;
|
|
24114
|
-
}
|
|
24115
|
-
function eligibleProviders(config) {
|
|
24116
|
-
const out = [];
|
|
24117
|
-
for (const [id, cfg] of Object.entries(config.providers ?? {})) {
|
|
24118
|
-
const preset = COMPATIBLE_PRESETS[id];
|
|
24119
|
-
const enabled = cfg.autoDiscoverModels ?? preset?.autoDiscover ?? false;
|
|
24120
|
-
if (!enabled) continue;
|
|
24121
|
-
const baseUrl = cfg.baseUrl ?? preset?.defaultBaseUrl;
|
|
24122
|
-
if (!baseUrl) continue;
|
|
24123
|
-
out.push({ id, cfg, baseUrl, apiKey: resolveKey(cfg) });
|
|
24124
|
-
}
|
|
24125
|
-
return out;
|
|
24126
|
-
}
|
|
24127
24507
|
async function readCache(file) {
|
|
24128
24508
|
try {
|
|
24129
24509
|
return JSON.parse(await fs21.readFile(file, "utf8"));
|
|
@@ -24134,14 +24514,13 @@ async function readCache(file) {
|
|
|
24134
24514
|
async function discoverAndMergeWebuiProviders(opts) {
|
|
24135
24515
|
const registry = opts.registry;
|
|
24136
24516
|
if (!isOverlayRegistry(registry)) return;
|
|
24137
|
-
const targets =
|
|
24517
|
+
const targets = resolveDiscoveryTargets(opts.config);
|
|
24138
24518
|
if (targets.length === 0) return;
|
|
24139
24519
|
const cacheFile = path29.join(opts.cacheDir, "discovered-models-cache.json");
|
|
24140
24520
|
const cache2 = await readCache(cacheFile);
|
|
24141
24521
|
let cacheDirty = false;
|
|
24142
24522
|
await Promise.all(
|
|
24143
|
-
targets.map(async ({ id, cfg, baseUrl, apiKey }) => {
|
|
24144
|
-
const cacheKey = `${id}\0${baseUrl}`;
|
|
24523
|
+
targets.map(async ({ id, cfg, baseUrl, apiKey, cacheKey }) => {
|
|
24145
24524
|
const provider = await discoverOpenAICompatibleModels(id, {
|
|
24146
24525
|
baseUrl,
|
|
24147
24526
|
apiKey,
|
|
@@ -24507,15 +24886,6 @@ async function createPreContextServices(input) {
|
|
|
24507
24886
|
logger.warn(`models.dev refresh failed (${toErrorMessage12(err)}); using cached catalog`);
|
|
24508
24887
|
}
|
|
24509
24888
|
}
|
|
24510
|
-
try {
|
|
24511
|
-
await installCatalogModelOutputLimits({
|
|
24512
|
-
registry: modelsRegistry,
|
|
24513
|
-
getConfig: () => config,
|
|
24514
|
-
log: (message) => logger.debug(message)
|
|
24515
|
-
});
|
|
24516
|
-
} catch (err) {
|
|
24517
|
-
logger.debug(`model output-limit index skipped: ${toErrorMessage12(err)}`);
|
|
24518
|
-
}
|
|
24519
24889
|
try {
|
|
24520
24890
|
await discoverAndMergeWebuiProviders({
|
|
24521
24891
|
config,
|
|
@@ -24526,6 +24896,15 @@ async function createPreContextServices(input) {
|
|
|
24526
24896
|
} catch (err) {
|
|
24527
24897
|
logger.debug(`provider auto-discovery skipped: ${toErrorMessage12(err)}`);
|
|
24528
24898
|
}
|
|
24899
|
+
try {
|
|
24900
|
+
await installCatalogModelOutputLimits({
|
|
24901
|
+
registry: modelsRegistry,
|
|
24902
|
+
getConfig: () => config,
|
|
24903
|
+
log: (message) => logger.debug(message)
|
|
24904
|
+
});
|
|
24905
|
+
} catch (err) {
|
|
24906
|
+
logger.debug(`model output-limit index skipped: ${toErrorMessage12(err)}`);
|
|
24907
|
+
}
|
|
24529
24908
|
const events = opts.services?.events ?? new EventBus();
|
|
24530
24909
|
events.setLogger(logger);
|
|
24531
24910
|
const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry, events });
|
|
@@ -24912,7 +25291,16 @@ function buildRoutes(state, deps2, cb) {
|
|
|
24912
25291
|
refineModel: (ws, msg) => modelOperations.refineModel(
|
|
24913
25292
|
ws,
|
|
24914
25293
|
msg.payload
|
|
24915
|
-
)
|
|
25294
|
+
),
|
|
25295
|
+
fallbackChoice: async (ws, msg) => {
|
|
25296
|
+
const result = emitFallbackChoice(deps2.events, msg);
|
|
25297
|
+
if (!result.ok) {
|
|
25298
|
+
send(ws, {
|
|
25299
|
+
type: "error",
|
|
25300
|
+
payload: { phase: "invalid_request", message: result.message }
|
|
25301
|
+
});
|
|
25302
|
+
}
|
|
25303
|
+
}
|
|
24916
25304
|
};
|
|
24917
25305
|
const sessionRoutes = createSessionHandlers({
|
|
24918
25306
|
config: state.getConfig(),
|
|
@@ -25090,8 +25478,10 @@ function buildRoutes(state, deps2, cb) {
|
|
|
25090
25478
|
});
|
|
25091
25479
|
const mcpRoutes = {
|
|
25092
25480
|
list: (ws, msg) => handleMcpList(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
25093
|
-
add
|
|
25094
|
-
|
|
25481
|
+
// add/update are the spawn-capable pair — they take a `command`/`args`
|
|
25482
|
+
// from the wire and start it. They go past the trust boundary (M1).
|
|
25483
|
+
add: (ws, msg) => handleMcpAdd(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry, deps2.trustBoundary),
|
|
25484
|
+
update: (ws, msg) => handleMcpUpdate(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry, deps2.trustBoundary),
|
|
25095
25485
|
remove: (ws, msg) => handleMcpRemove(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
25096
25486
|
enable: (ws, msg) => handleMcpEnable(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
25097
25487
|
disable: (ws, msg) => handleMcpDisable(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
@@ -25263,7 +25653,10 @@ function createWsServers(httpServer, ports, accessToken) {
|
|
|
25263
25653
|
expectedToken: wsToken,
|
|
25264
25654
|
requireToken: ports.requireToken,
|
|
25265
25655
|
allowedHostnames: publicHostnames,
|
|
25266
|
-
allowBrowserUrlToken: Boolean(ports.publicWsUrl)
|
|
25656
|
+
allowBrowserUrlToken: Boolean(ports.publicWsUrl),
|
|
25657
|
+
// WS-003 opt-out for the Vite dev loop only (app and WS server cannot
|
|
25658
|
+
// share a port). Off unless explicitly requested — see ws-auth.ts.
|
|
25659
|
+
allowCrossPortLoopbackCookie: process.env["WRONGSTACK_WEBUI_DEV_CROSS_PORT_WS"] === "1"
|
|
25267
25660
|
});
|
|
25268
25661
|
const WS_MAX_PAYLOAD = 20 * 1024 * 1024;
|
|
25269
25662
|
const wssPrimary = new WebSocketServer({
|