@wrongstack/webui-server 0.298.3 → 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 +1375 -252
- package/dist/protocol/client-conversation.d.ts +1 -1
- package/dist/protocol/index.js +21 -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 +1289 -229
- package/dist/server/fallback-choice.d.ts +19 -0
- package/dist/server/frontend-static-serve.d.ts +9 -0
- package/dist/server/http-server.d.ts +56 -0
- package/dist/server/instance-registry.d.ts +21 -0
- package/dist/server/intake-service.d.ts +13 -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/requirement-intake-handlers.d.ts +41 -0
- package/dist/server/server-runtime.d.ts +3 -0
- package/dist/server/session-history.d.ts +42 -1
- package/dist/server/session-routes.d.ts +1 -0
- package/dist/server/ws-auth.d.ts +18 -0
- package/dist/server/ws-payload-validation.d.ts +7 -0
- package/package.json +11 -10
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,
|
|
@@ -7497,7 +7703,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
7497
7703
|
}
|
|
7498
7704
|
try {
|
|
7499
7705
|
const { SessionRegistry, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core/storage");
|
|
7500
|
-
const { resolveWstackPaths:
|
|
7706
|
+
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
7501
7707
|
const registry = new SessionRegistry(globalRoot);
|
|
7502
7708
|
const entry = await registry.get(sessionId);
|
|
7503
7709
|
if (!entry) {
|
|
@@ -7505,7 +7711,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
7505
7711
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
7506
7712
|
return;
|
|
7507
7713
|
}
|
|
7508
|
-
const paths =
|
|
7714
|
+
const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
|
|
7509
7715
|
const store = new DefaultSessionStore3({ dir: paths.projectSessions });
|
|
7510
7716
|
const reader = new DefaultSessionReader2({ store });
|
|
7511
7717
|
const rawEntries = [];
|
|
@@ -7586,7 +7792,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
|
7586
7792
|
try {
|
|
7587
7793
|
const { SessionRegistry } = await import("@wrongstack/core/storage");
|
|
7588
7794
|
const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
7589
|
-
const { resolveWstackPaths:
|
|
7795
|
+
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
7590
7796
|
const registry = new SessionRegistry(globalRoot);
|
|
7591
7797
|
const entry = await registry.get(sessionId);
|
|
7592
7798
|
if (!entry) {
|
|
@@ -7594,7 +7800,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
|
7594
7800
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
7595
7801
|
return;
|
|
7596
7802
|
}
|
|
7597
|
-
const paths =
|
|
7803
|
+
const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
|
|
7598
7804
|
const mailbox = getSharedProjectMailbox5(paths.projectDir);
|
|
7599
7805
|
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
7600
7806
|
const sent = await mailbox.send({ from, to, type, subject, body: text, priority });
|
|
@@ -7614,7 +7820,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
|
7614
7820
|
try {
|
|
7615
7821
|
const { SessionRegistry } = await import("@wrongstack/core/storage");
|
|
7616
7822
|
const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
7617
|
-
const { resolveWstackPaths:
|
|
7823
|
+
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
7618
7824
|
const registry = new SessionRegistry(globalRoot);
|
|
7619
7825
|
const entry = await registry.get(sessionId);
|
|
7620
7826
|
if (!entry) {
|
|
@@ -7622,7 +7828,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
|
7622
7828
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
7623
7829
|
return;
|
|
7624
7830
|
}
|
|
7625
|
-
const paths =
|
|
7831
|
+
const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
|
|
7626
7832
|
const mailbox = getSharedProjectMailbox5(paths.projectDir);
|
|
7627
7833
|
const leaderAddr = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
7628
7834
|
const [inbound, outbound] = await Promise.all([
|
|
@@ -7674,7 +7880,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
|
7674
7880
|
try {
|
|
7675
7881
|
const { SessionRegistry } = await import("@wrongstack/core/storage");
|
|
7676
7882
|
const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
7677
|
-
const { resolveWstackPaths:
|
|
7883
|
+
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
7678
7884
|
const registry = new SessionRegistry(globalRoot);
|
|
7679
7885
|
const entry = await registry.get(sessionId);
|
|
7680
7886
|
if (!entry) {
|
|
@@ -7682,7 +7888,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
|
7682
7888
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
7683
7889
|
return;
|
|
7684
7890
|
}
|
|
7685
|
-
const paths =
|
|
7891
|
+
const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
|
|
7686
7892
|
const mailbox = getSharedProjectMailbox5(paths.projectDir);
|
|
7687
7893
|
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
7688
7894
|
const sent = await mailbox.sendRuntimeControl({
|
|
@@ -7723,7 +7929,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
|
7723
7929
|
try {
|
|
7724
7930
|
const { SessionRegistry } = await import("@wrongstack/core/storage");
|
|
7725
7931
|
const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
7726
|
-
const { resolveWstackPaths:
|
|
7932
|
+
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
7727
7933
|
const registry = new SessionRegistry(globalRoot);
|
|
7728
7934
|
const all = await registry.list();
|
|
7729
7935
|
const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
|
|
@@ -7735,7 +7941,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
|
7735
7941
|
}
|
|
7736
7942
|
const mbByDir = /* @__PURE__ */ new Map();
|
|
7737
7943
|
const mailboxFor = (projectRoot) => {
|
|
7738
|
-
const dir =
|
|
7944
|
+
const dir = resolveWstackPaths7({ projectRoot, globalRoot }).projectDir;
|
|
7739
7945
|
let mb = mbByDir.get(dir);
|
|
7740
7946
|
if (!mb) {
|
|
7741
7947
|
mb = getSharedProjectMailbox5(dir);
|
|
@@ -7769,6 +7975,246 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
|
7769
7975
|
}
|
|
7770
7976
|
}
|
|
7771
7977
|
|
|
7978
|
+
// src/server/requirement-intake-handlers.ts
|
|
7979
|
+
import { readProjectIdentity } from "@wrongstack/core/utils";
|
|
7980
|
+
import {
|
|
7981
|
+
IntakeError,
|
|
7982
|
+
IntakeValidationError
|
|
7983
|
+
} from "@wrongstack/requirement-intake";
|
|
7984
|
+
var SERVER_ACTOR = { id: "webui-server", type: "agent" };
|
|
7985
|
+
var MAX_INTAKE_BODY_BYTES = 512e3;
|
|
7986
|
+
function intakeContext(projectId) {
|
|
7987
|
+
return { ...SERVER_ACTOR, projectId };
|
|
7988
|
+
}
|
|
7989
|
+
function sendJson2(res, status, body) {
|
|
7990
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7991
|
+
res.end(JSON.stringify(body));
|
|
7992
|
+
}
|
|
7993
|
+
function sendNotFound(res, intakeId) {
|
|
7994
|
+
sendJson2(res, 404, {
|
|
7995
|
+
error: {
|
|
7996
|
+
code: "INTAKE_NOT_FOUND",
|
|
7997
|
+
message: `Requirement intake record not found: ${intakeId}`
|
|
7998
|
+
}
|
|
7999
|
+
});
|
|
8000
|
+
}
|
|
8001
|
+
function sendIntakeError(res, error2) {
|
|
8002
|
+
if (error2 instanceof IntakeValidationError) {
|
|
8003
|
+
sendJson2(res, 400, {
|
|
8004
|
+
error: { code: error2.code, message: error2.message, issues: error2.issues }
|
|
8005
|
+
});
|
|
8006
|
+
return;
|
|
8007
|
+
}
|
|
8008
|
+
if (error2 instanceof IntakeError) {
|
|
8009
|
+
const status = error2.code === "INTAKE_UNAUTHORIZED" ? 403 : error2.code === "INTAKE_NOT_FOUND" ? 404 : error2.code === "INTAKE_SUGGESTION_ERROR" ? 502 : 409;
|
|
8010
|
+
sendJson2(res, status, { error: { code: error2.code, message: error2.message } });
|
|
8011
|
+
return;
|
|
8012
|
+
}
|
|
8013
|
+
sendJson2(res, 500, { error: { code: "INTERNAL_ERROR", message: "Internal server error" } });
|
|
8014
|
+
}
|
|
8015
|
+
function serviceOr503(res, service) {
|
|
8016
|
+
if (service) return true;
|
|
8017
|
+
sendJson2(res, 503, {
|
|
8018
|
+
error: { code: "INTAKE_UNAVAILABLE", message: "Requirement intake service not configured" }
|
|
8019
|
+
});
|
|
8020
|
+
return false;
|
|
8021
|
+
}
|
|
8022
|
+
async function readJsonBody3(res, req) {
|
|
8023
|
+
const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
|
|
8024
|
+
if (contentType !== "application/json") {
|
|
8025
|
+
sendJson2(res, 400, {
|
|
8026
|
+
error: {
|
|
8027
|
+
code: "INVALID_CONTENT_TYPE",
|
|
8028
|
+
message: `Unsupported Content-Type: ${contentType || "(absent)"}`
|
|
8029
|
+
}
|
|
8030
|
+
});
|
|
8031
|
+
return null;
|
|
8032
|
+
}
|
|
8033
|
+
return new Promise((resolve15) => {
|
|
8034
|
+
let data = "";
|
|
8035
|
+
let failed = false;
|
|
8036
|
+
const fail2 = (message) => {
|
|
8037
|
+
if (failed) return;
|
|
8038
|
+
failed = true;
|
|
8039
|
+
sendJson2(res, 400, { error: { code: "INVALID_BODY", message } });
|
|
8040
|
+
resolve15(null);
|
|
8041
|
+
};
|
|
8042
|
+
req.on("data", (chunk) => {
|
|
8043
|
+
if (failed) return;
|
|
8044
|
+
data += chunk.toString("utf8");
|
|
8045
|
+
if (data.length > MAX_INTAKE_BODY_BYTES) {
|
|
8046
|
+
req.destroy();
|
|
8047
|
+
fail2("Request body too large");
|
|
8048
|
+
}
|
|
8049
|
+
});
|
|
8050
|
+
req.on("end", () => {
|
|
8051
|
+
if (failed) return;
|
|
8052
|
+
try {
|
|
8053
|
+
resolve15(data.trim().length === 0 ? {} : JSON.parse(data));
|
|
8054
|
+
} catch {
|
|
8055
|
+
fail2("Request body is not valid JSON");
|
|
8056
|
+
}
|
|
8057
|
+
});
|
|
8058
|
+
req.on("error", () => fail2("Failed to read request body"));
|
|
8059
|
+
});
|
|
8060
|
+
}
|
|
8061
|
+
async function discoverIntake(res, service, intakeId) {
|
|
8062
|
+
const record = await service.getIntake(intakeId, intakeContext(""));
|
|
8063
|
+
if (!record) {
|
|
8064
|
+
sendNotFound(res, intakeId);
|
|
8065
|
+
return null;
|
|
8066
|
+
}
|
|
8067
|
+
return record;
|
|
8068
|
+
}
|
|
8069
|
+
async function handleRequirementIntakeListForServer(res, service, projectRoot) {
|
|
8070
|
+
if (!serviceOr503(res, service)) return;
|
|
8071
|
+
if (!projectRoot) {
|
|
8072
|
+
sendJson2(res, 503, {
|
|
8073
|
+
error: { code: "PROJECT_ROOT_NOT_CONFIGURED", message: "Project root not configured" }
|
|
8074
|
+
});
|
|
8075
|
+
return;
|
|
8076
|
+
}
|
|
8077
|
+
try {
|
|
8078
|
+
const identity = await readProjectIdentity(projectRoot);
|
|
8079
|
+
if (!identity) {
|
|
8080
|
+
sendJson2(res, 404, {
|
|
8081
|
+
error: {
|
|
8082
|
+
code: "PROJECT_IDENTITY_NOT_FOUND",
|
|
8083
|
+
message: "No project identity found \u2014 run `wstack init` first"
|
|
8084
|
+
}
|
|
8085
|
+
});
|
|
8086
|
+
return;
|
|
8087
|
+
}
|
|
8088
|
+
const projectId = identity.projectId;
|
|
8089
|
+
const records = await service.listIntakes(projectId, intakeContext(projectId));
|
|
8090
|
+
sendJson2(res, 200, { projectId, intakes: records });
|
|
8091
|
+
} catch (error2) {
|
|
8092
|
+
sendIntakeError(res, error2);
|
|
8093
|
+
}
|
|
8094
|
+
}
|
|
8095
|
+
async function handleRequirementIntakeCreate(res, req, service, projectId) {
|
|
8096
|
+
if (!serviceOr503(res, service)) return;
|
|
8097
|
+
const body = await readJsonBody3(res, req);
|
|
8098
|
+
if (body === null) return;
|
|
8099
|
+
try {
|
|
8100
|
+
const result = await service.createIntake(body, intakeContext(projectId));
|
|
8101
|
+
sendJson2(res, result.idempotent ? 200 : 201, result);
|
|
8102
|
+
} catch (error2) {
|
|
8103
|
+
sendIntakeError(res, error2);
|
|
8104
|
+
}
|
|
8105
|
+
}
|
|
8106
|
+
async function handleRequirementIntakeList(res, service, projectId) {
|
|
8107
|
+
if (!serviceOr503(res, service)) return;
|
|
8108
|
+
try {
|
|
8109
|
+
const records = await service.listIntakes(projectId, intakeContext(projectId));
|
|
8110
|
+
sendJson2(res, 200, records);
|
|
8111
|
+
} catch (error2) {
|
|
8112
|
+
sendIntakeError(res, error2);
|
|
8113
|
+
}
|
|
8114
|
+
}
|
|
8115
|
+
async function handleRequirementIntakeGet(res, service, intakeId) {
|
|
8116
|
+
if (!serviceOr503(res, service)) return;
|
|
8117
|
+
try {
|
|
8118
|
+
const record = await discoverIntake(res, service, intakeId);
|
|
8119
|
+
if (!record) return;
|
|
8120
|
+
sendJson2(res, 200, record);
|
|
8121
|
+
} catch (error2) {
|
|
8122
|
+
sendIntakeError(res, error2);
|
|
8123
|
+
}
|
|
8124
|
+
}
|
|
8125
|
+
async function handleRequirementIntakeUpdate(res, req, service, intakeId) {
|
|
8126
|
+
if (!serviceOr503(res, service)) return;
|
|
8127
|
+
const body = await readJsonBody3(res, req);
|
|
8128
|
+
if (body === null) return;
|
|
8129
|
+
try {
|
|
8130
|
+
const record = await discoverIntake(res, service, intakeId);
|
|
8131
|
+
if (!record) return;
|
|
8132
|
+
const expectedVersion = typeof body["expectedVersion"] === "number" ? body["expectedVersion"] : void 0;
|
|
8133
|
+
const { expectedVersion: _ignored, ...patch } = body;
|
|
8134
|
+
void _ignored;
|
|
8135
|
+
const updated = await service.updateIntake(
|
|
8136
|
+
intakeId,
|
|
8137
|
+
patch,
|
|
8138
|
+
intakeContext(record.projectId),
|
|
8139
|
+
expectedVersion
|
|
8140
|
+
);
|
|
8141
|
+
sendJson2(res, 200, updated);
|
|
8142
|
+
} catch (error2) {
|
|
8143
|
+
sendIntakeError(res, error2);
|
|
8144
|
+
}
|
|
8145
|
+
}
|
|
8146
|
+
async function handleRequirementIntakeAnswers(res, req, service, intakeId) {
|
|
8147
|
+
if (!serviceOr503(res, service)) return;
|
|
8148
|
+
const body = await readJsonBody3(res, req);
|
|
8149
|
+
if (body === null) return;
|
|
8150
|
+
try {
|
|
8151
|
+
const record = await discoverIntake(res, service, intakeId);
|
|
8152
|
+
if (!record) return;
|
|
8153
|
+
const updated = await service.addAnswer(
|
|
8154
|
+
intakeId,
|
|
8155
|
+
body,
|
|
8156
|
+
intakeContext(record.projectId)
|
|
8157
|
+
);
|
|
8158
|
+
sendJson2(res, 200, updated);
|
|
8159
|
+
} catch (error2) {
|
|
8160
|
+
sendIntakeError(res, error2);
|
|
8161
|
+
}
|
|
8162
|
+
}
|
|
8163
|
+
async function handleRequirementIntakeSuggestions(res, req, service, intakeId) {
|
|
8164
|
+
if (!serviceOr503(res, service)) return;
|
|
8165
|
+
const body = await readJsonBody3(res, req);
|
|
8166
|
+
if (body === null) return;
|
|
8167
|
+
try {
|
|
8168
|
+
const record = await discoverIntake(res, service, intakeId);
|
|
8169
|
+
if (!record) return;
|
|
8170
|
+
const focus = Array.isArray(body["focus"]) ? body["focus"] : void 0;
|
|
8171
|
+
const suggestions = await service.generateSuggestions(
|
|
8172
|
+
intakeId,
|
|
8173
|
+
intakeContext(record.projectId),
|
|
8174
|
+
focus
|
|
8175
|
+
);
|
|
8176
|
+
sendJson2(res, 200, { suggestions });
|
|
8177
|
+
} catch (error2) {
|
|
8178
|
+
sendIntakeError(res, error2);
|
|
8179
|
+
}
|
|
8180
|
+
}
|
|
8181
|
+
async function handleRequirementIntakeSubmit(res, service, intakeId) {
|
|
8182
|
+
if (!serviceOr503(res, service)) return;
|
|
8183
|
+
try {
|
|
8184
|
+
const record = await discoverIntake(res, service, intakeId);
|
|
8185
|
+
if (!record) return;
|
|
8186
|
+
const result = await service.submitIntake(intakeId, intakeContext(record.projectId));
|
|
8187
|
+
sendJson2(res, 200, result);
|
|
8188
|
+
} catch (error2) {
|
|
8189
|
+
sendIntakeError(res, error2);
|
|
8190
|
+
}
|
|
8191
|
+
}
|
|
8192
|
+
async function handleRequirementIntakeCancel(res, req, service, intakeId) {
|
|
8193
|
+
if (!serviceOr503(res, service)) return;
|
|
8194
|
+
const body = await readJsonBody3(res, req);
|
|
8195
|
+
if (body === null) return;
|
|
8196
|
+
try {
|
|
8197
|
+
const record = await discoverIntake(res, service, intakeId);
|
|
8198
|
+
if (!record) return;
|
|
8199
|
+
const reason = typeof body["reason"] === "string" ? body["reason"] : void 0;
|
|
8200
|
+
const updated = await service.cancelIntake(intakeId, intakeContext(record.projectId), reason);
|
|
8201
|
+
sendJson2(res, 200, updated);
|
|
8202
|
+
} catch (error2) {
|
|
8203
|
+
sendIntakeError(res, error2);
|
|
8204
|
+
}
|
|
8205
|
+
}
|
|
8206
|
+
async function handleRequirementIntakeArchive(res, service, intakeId) {
|
|
8207
|
+
if (!serviceOr503(res, service)) return;
|
|
8208
|
+
try {
|
|
8209
|
+
const record = await discoverIntake(res, service, intakeId);
|
|
8210
|
+
if (!record) return;
|
|
8211
|
+
const updated = await service.archiveIntake(intakeId, intakeContext(record.projectId));
|
|
8212
|
+
sendJson2(res, 200, updated);
|
|
8213
|
+
} catch (error2) {
|
|
8214
|
+
sendIntakeError(res, error2);
|
|
8215
|
+
}
|
|
8216
|
+
}
|
|
8217
|
+
|
|
7772
8218
|
// src/server/memory-diagnostics.ts
|
|
7773
8219
|
import * as fs8 from "node:fs/promises";
|
|
7774
8220
|
import * as path11 from "node:path";
|
|
@@ -7974,7 +8420,7 @@ async function touchProjectInManifest(options, globalConfigPath) {
|
|
|
7974
8420
|
// src/server/techstack-handlers.ts
|
|
7975
8421
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7976
8422
|
var DEEP_DIVE_TIMEOUT_MS = 6e4;
|
|
7977
|
-
function
|
|
8423
|
+
function sendJson3(res, status, data) {
|
|
7978
8424
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7979
8425
|
res.end(JSON.stringify(data));
|
|
7980
8426
|
}
|
|
@@ -7989,13 +8435,13 @@ function handleTechStackSnapshot(res, deps2) {
|
|
|
7989
8435
|
try {
|
|
7990
8436
|
const snapshot = deps2.store.getSnapshot(deps2.projectId);
|
|
7991
8437
|
if (!snapshot) {
|
|
7992
|
-
|
|
8438
|
+
sendJson3(res, 404, { snapshot: null, stale: false });
|
|
7993
8439
|
return;
|
|
7994
8440
|
}
|
|
7995
8441
|
const ageMs = Date.now() - new Date(snapshot.createdAt).getTime();
|
|
7996
|
-
|
|
8442
|
+
sendJson3(res, 200, { snapshot, stale: ageMs > 24 * 60 * 60 * 1e3 });
|
|
7997
8443
|
} catch (error2) {
|
|
7998
|
-
|
|
8444
|
+
sendJson3(res, 500, {
|
|
7999
8445
|
error: "TechStack store unavailable",
|
|
8000
8446
|
detail: errorMessage(error2)
|
|
8001
8447
|
});
|
|
@@ -8006,7 +8452,7 @@ function errorMessage(error2) {
|
|
|
8006
8452
|
}
|
|
8007
8453
|
function requireJobDeps(res, deps2) {
|
|
8008
8454
|
if (!deps2.projectRoot || !deps2.engine) {
|
|
8009
|
-
|
|
8455
|
+
sendJson3(res, 503, { error: "TechStack engine unavailable" });
|
|
8010
8456
|
return false;
|
|
8011
8457
|
}
|
|
8012
8458
|
return true;
|
|
@@ -8017,7 +8463,7 @@ function startJob(res, deps2, kind) {
|
|
|
8017
8463
|
const controller = new AbortController();
|
|
8018
8464
|
deps2.runningJobs?.set(jobId, controller);
|
|
8019
8465
|
deps2.emit?.({ type: "techstack.job.started", payload: { jobId, kind } });
|
|
8020
|
-
|
|
8466
|
+
sendJson3(res, 202, { jobId, kind, status: "queued" });
|
|
8021
8467
|
void buildResearcher(deps2, kind).catch(() => void 0).then(
|
|
8022
8468
|
(researcher) => deps2.engine.analyze(deps2.projectId, {
|
|
8023
8469
|
targetRoot: deps2.projectRoot,
|
|
@@ -8063,24 +8509,24 @@ function handleTechStackCancel(res, deps2, jobId) {
|
|
|
8063
8509
|
if (controller && !controller.signal.aborted) controller.abort();
|
|
8064
8510
|
deps2.store.updateJobStatus(jobId, "cancelled");
|
|
8065
8511
|
deps2.emit?.({ type: "techstack.job.cancelled", payload: { jobId } });
|
|
8066
|
-
|
|
8512
|
+
sendJson3(res, 200, { jobId, status: "cancelled" });
|
|
8067
8513
|
}
|
|
8068
8514
|
async function handleTechStackDependencyResearch(res, deps2, dependencyId) {
|
|
8069
8515
|
const snapshot = deps2.store.getSnapshot(deps2.projectId);
|
|
8070
8516
|
const dependency = snapshot?.dependencies.find((dep) => dep.id === dependencyId);
|
|
8071
8517
|
if (!dependency) {
|
|
8072
|
-
|
|
8518
|
+
sendJson3(res, 404, { error: "Dependency not found in the current snapshot" });
|
|
8073
8519
|
return;
|
|
8074
8520
|
}
|
|
8075
8521
|
let researcher;
|
|
8076
8522
|
try {
|
|
8077
8523
|
researcher = await buildResearcher(deps2, "analyze");
|
|
8078
8524
|
} catch (error2) {
|
|
8079
|
-
|
|
8525
|
+
sendJson3(res, 503, { error: "Research unavailable", detail: errorMessage(error2) });
|
|
8080
8526
|
return;
|
|
8081
8527
|
}
|
|
8082
8528
|
if (!researcher) {
|
|
8083
|
-
|
|
8529
|
+
sendJson3(res, 503, {
|
|
8084
8530
|
error: "No model configured \u2014 connect a provider to run LLM analysis."
|
|
8085
8531
|
});
|
|
8086
8532
|
return;
|
|
@@ -8097,9 +8543,9 @@ async function handleTechStackDependencyResearch(res, deps2, dependencyId) {
|
|
|
8097
8543
|
[triaged ?? { dependency, cluster: "breaking_change", priority: 0 }],
|
|
8098
8544
|
{ signal: controller.signal }
|
|
8099
8545
|
);
|
|
8100
|
-
|
|
8546
|
+
sendJson3(res, 200, { dependencyId, findings });
|
|
8101
8547
|
} catch (error2) {
|
|
8102
|
-
|
|
8548
|
+
sendJson3(res, 500, { error: "Research failed", detail: errorMessage(error2) });
|
|
8103
8549
|
} finally {
|
|
8104
8550
|
clearTimeout(timeout);
|
|
8105
8551
|
controller.abort();
|
|
@@ -8108,15 +8554,15 @@ async function handleTechStackDependencyResearch(res, deps2, dependencyId) {
|
|
|
8108
8554
|
function handleTechStackJobStatus(res, deps2, jobId) {
|
|
8109
8555
|
const job = deps2.store.getJob(jobId);
|
|
8110
8556
|
if (!job) {
|
|
8111
|
-
|
|
8557
|
+
sendJson3(res, 404, { error: "Job not found" });
|
|
8112
8558
|
return;
|
|
8113
8559
|
}
|
|
8114
|
-
|
|
8560
|
+
sendJson3(res, 200, { job });
|
|
8115
8561
|
}
|
|
8116
8562
|
function handleTechStackReport(res, deps2, reportId, format) {
|
|
8117
8563
|
const snapshot = deps2.store.getSnapshotById(reportId);
|
|
8118
8564
|
if (!snapshot) {
|
|
8119
|
-
|
|
8565
|
+
sendJson3(res, 404, { error: "Report not found" });
|
|
8120
8566
|
return;
|
|
8121
8567
|
}
|
|
8122
8568
|
if (deps2.engine) {
|
|
@@ -8127,30 +8573,30 @@ function handleTechStackReport(res, deps2, reportId, format) {
|
|
|
8127
8573
|
});
|
|
8128
8574
|
res.end(report);
|
|
8129
8575
|
} else {
|
|
8130
|
-
|
|
8576
|
+
sendJson3(res, 200, snapshot);
|
|
8131
8577
|
}
|
|
8132
8578
|
}
|
|
8133
8579
|
async function handleTechStackTrends(res, deps2) {
|
|
8134
8580
|
try {
|
|
8135
8581
|
const { TrendStore } = await import("@wrongstack/techstack");
|
|
8136
|
-
|
|
8582
|
+
sendJson3(res, 200, { trend: new TrendStore(deps2.store).analyze(deps2.projectId) });
|
|
8137
8583
|
} catch (error2) {
|
|
8138
|
-
|
|
8584
|
+
sendJson3(res, 500, { error: "Trend analysis failed", detail: errorMessage(error2) });
|
|
8139
8585
|
}
|
|
8140
8586
|
}
|
|
8141
8587
|
async function handleTechStackRemediationPlan(res, deps2) {
|
|
8142
8588
|
const snapshot = deps2.store.getSnapshot(deps2.projectId);
|
|
8143
8589
|
if (!snapshot) {
|
|
8144
|
-
|
|
8590
|
+
sendJson3(res, 404, { error: "No TechStack snapshot is available" });
|
|
8145
8591
|
return;
|
|
8146
8592
|
}
|
|
8147
8593
|
const { applyPlan, generateUpgradePlan } = await import("@wrongstack/techstack");
|
|
8148
8594
|
const plan = generateUpgradePlan(snapshot);
|
|
8149
|
-
|
|
8595
|
+
sendJson3(res, 200, { plan, preview: await applyPlan(plan) });
|
|
8150
8596
|
}
|
|
8151
8597
|
async function handleTechStackRemediationApply(req, res, deps2) {
|
|
8152
8598
|
if (!deps2.executePackageOperation) {
|
|
8153
|
-
|
|
8599
|
+
sendJson3(res, 503, { error: "Permission-governed package execution is unavailable" });
|
|
8154
8600
|
return;
|
|
8155
8601
|
}
|
|
8156
8602
|
let approvedItems;
|
|
@@ -8163,16 +8609,16 @@ async function handleTechStackRemediationApply(req, res, deps2) {
|
|
|
8163
8609
|
const body = JSON.parse(raw || "{}");
|
|
8164
8610
|
approvedItems = Array.isArray(body.approvedItems) ? body.approvedItems.filter((value) => typeof value === "string") : [];
|
|
8165
8611
|
} catch (error2) {
|
|
8166
|
-
|
|
8612
|
+
sendJson3(res, 400, { error: "Invalid request body", detail: errorMessage(error2) });
|
|
8167
8613
|
return;
|
|
8168
8614
|
}
|
|
8169
8615
|
if (approvedItems.length === 0) {
|
|
8170
|
-
|
|
8616
|
+
sendJson3(res, 400, { error: "approvedItems must explicitly identify at least one plan item" });
|
|
8171
8617
|
return;
|
|
8172
8618
|
}
|
|
8173
8619
|
const snapshot = deps2.store.getSnapshot(deps2.projectId);
|
|
8174
8620
|
if (!snapshot) {
|
|
8175
|
-
|
|
8621
|
+
sendJson3(res, 404, { error: "No TechStack snapshot is available" });
|
|
8176
8622
|
return;
|
|
8177
8623
|
}
|
|
8178
8624
|
const approved = new Set(approvedItems);
|
|
@@ -8187,14 +8633,17 @@ async function handleTechStackRemediationApply(req, res, deps2) {
|
|
|
8187
8633
|
return executePackageOperation(operation, workspace?.relativeRoot);
|
|
8188
8634
|
}
|
|
8189
8635
|
});
|
|
8190
|
-
|
|
8636
|
+
sendJson3(res, 200, { plan, result });
|
|
8191
8637
|
}
|
|
8192
8638
|
|
|
8193
8639
|
// src/server/ws-auth.ts
|
|
8194
8640
|
import { Buffer as Buffer2 } from "node:buffer";
|
|
8195
8641
|
import { timingSafeEqual } from "node:crypto";
|
|
8642
|
+
import { isLoopbackHost as isLoopbackHostCore } from "@wrongstack/core/hq";
|
|
8196
8643
|
function isLoopbackHostname(hostname) {
|
|
8197
|
-
|
|
8644
|
+
const normalized = hostname.toLowerCase();
|
|
8645
|
+
if (normalized === "localhost") return true;
|
|
8646
|
+
return isLoopbackHostCore(normalized);
|
|
8198
8647
|
}
|
|
8199
8648
|
function effectivePort(url) {
|
|
8200
8649
|
if (url.port) return url.port;
|
|
@@ -8215,7 +8664,7 @@ function isTrustedLoopbackOrigin(origin, hostHeader) {
|
|
|
8215
8664
|
}
|
|
8216
8665
|
}
|
|
8217
8666
|
function isLoopbackBind(wsHost) {
|
|
8218
|
-
return wsHost
|
|
8667
|
+
return isLoopbackHostCore(wsHost) || wsHost === "localhost";
|
|
8219
8668
|
}
|
|
8220
8669
|
function isWildcardBind(wsHost) {
|
|
8221
8670
|
return wsHost === "0.0.0.0" || wsHost === "::" || wsHost === "[::]";
|
|
@@ -8242,19 +8691,22 @@ function extractToken(url) {
|
|
|
8242
8691
|
function extractTokenFromCookie(cookieHeader) {
|
|
8243
8692
|
if (!cookieHeader) return void 0;
|
|
8244
8693
|
const raw = Array.isArray(cookieHeader) ? cookieHeader.join("; ") : cookieHeader;
|
|
8694
|
+
let plain;
|
|
8245
8695
|
for (const part of raw.split(";")) {
|
|
8246
8696
|
const eq = part.indexOf("=");
|
|
8247
8697
|
if (eq < 0) continue;
|
|
8248
8698
|
const name2 = part.slice(0, eq).trim();
|
|
8249
|
-
if (name2
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
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();
|
|
8255
8705
|
}
|
|
8706
|
+
if (name2 === "__Host-ws_token") return value;
|
|
8707
|
+
plain ??= value;
|
|
8256
8708
|
}
|
|
8257
|
-
return
|
|
8709
|
+
return plain;
|
|
8258
8710
|
}
|
|
8259
8711
|
function hostHeaderOk(input) {
|
|
8260
8712
|
if (!isLoopbackBind(input.wsHost)) return true;
|
|
@@ -8296,7 +8748,8 @@ function verifyClient(input) {
|
|
|
8296
8748
|
expectedToken,
|
|
8297
8749
|
requireToken,
|
|
8298
8750
|
allowedHostnames,
|
|
8299
|
-
allowBrowserUrlToken
|
|
8751
|
+
allowBrowserUrlToken,
|
|
8752
|
+
allowCrossPortLoopbackCookie
|
|
8300
8753
|
} = input;
|
|
8301
8754
|
const urlTokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
|
|
8302
8755
|
const cookieTokenOk = tokenMatches(extractTokenFromCookie(cookieHeader), expectedToken);
|
|
@@ -8311,7 +8764,10 @@ function verifyClient(input) {
|
|
|
8311
8764
|
const { hostname: originHostname } = new URL(origin);
|
|
8312
8765
|
if (isLoopbackHostname(originHostname)) {
|
|
8313
8766
|
if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
|
|
8314
|
-
|
|
8767
|
+
if (!isTrustedLoopbackOrigin(origin, hostHeader)) {
|
|
8768
|
+
return Boolean(allowCrossPortLoopbackCookie) && cookieTokenOk;
|
|
8769
|
+
}
|
|
8770
|
+
return true;
|
|
8315
8771
|
}
|
|
8316
8772
|
return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
|
|
8317
8773
|
} catch {
|
|
@@ -8346,11 +8802,22 @@ ${out}`;
|
|
|
8346
8802
|
function firstHeader(value) {
|
|
8347
8803
|
return Array.isArray(value) ? value[0] : value;
|
|
8348
8804
|
}
|
|
8349
|
-
|
|
8350
|
-
|
|
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("; ");
|
|
8351
8818
|
}
|
|
8352
|
-
function setAuthCookieHeaders(res, token) {
|
|
8353
|
-
res.setHeader("Set-Cookie", wsTokenCookie(token));
|
|
8819
|
+
function setAuthCookieHeaders(res, token, secure) {
|
|
8820
|
+
res.setHeader("Set-Cookie", wsTokenCookie(token, secure));
|
|
8354
8821
|
res.setHeader("Cache-Control", "no-store");
|
|
8355
8822
|
}
|
|
8356
8823
|
function setStaticSecurityHeaders(res) {
|
|
@@ -8358,8 +8825,16 @@ function setStaticSecurityHeaders(res) {
|
|
|
8358
8825
|
res.setHeader("X-Frame-Options", "DENY");
|
|
8359
8826
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
8360
8827
|
}
|
|
8361
|
-
function requestToken(req, url) {
|
|
8362
|
-
|
|
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);
|
|
8363
8838
|
}
|
|
8364
8839
|
function formatCspHostname(hostname) {
|
|
8365
8840
|
return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
@@ -8414,7 +8889,8 @@ function strictDecodeParam(segment, res) {
|
|
|
8414
8889
|
function createHttpServer(opts) {
|
|
8415
8890
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
8416
8891
|
const distDir = path13.resolve(opts.distDir);
|
|
8417
|
-
const requireAccessToken =
|
|
8892
|
+
const requireAccessToken = true;
|
|
8893
|
+
const secureCookies = opts.secureCookies ?? (opts.publicWsUrl?.trim().toLowerCase().startsWith("wss:") ?? false);
|
|
8418
8894
|
const trustedHostnames = (() => {
|
|
8419
8895
|
const names = [...opts.allowedHostnames ?? []];
|
|
8420
8896
|
if (opts.publicWsUrl) {
|
|
@@ -8453,13 +8929,13 @@ function createHttpServer(opts) {
|
|
|
8453
8929
|
const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
|
|
8454
8930
|
const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
|
|
8455
8931
|
if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
|
|
8456
|
-
const provided = requestToken(req, url);
|
|
8932
|
+
const provided = requestToken(req, url, { allowQuery: true });
|
|
8457
8933
|
if (!provided || !opts.apiToken || !tokenMatches(provided, opts.apiToken)) {
|
|
8458
8934
|
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
8459
8935
|
res.end("Unauthorized");
|
|
8460
8936
|
return;
|
|
8461
8937
|
}
|
|
8462
|
-
setAuthCookieHeaders(res, opts.apiToken);
|
|
8938
|
+
setAuthCookieHeaders(res, opts.apiToken, secureCookies);
|
|
8463
8939
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
8464
8940
|
res.end("ok");
|
|
8465
8941
|
return;
|
|
@@ -8473,7 +8949,7 @@ function createHttpServer(opts) {
|
|
|
8473
8949
|
return;
|
|
8474
8950
|
}
|
|
8475
8951
|
if (shouldSetAuthCookie && opts.apiToken) {
|
|
8476
|
-
setAuthCookieHeaders(res, opts.apiToken);
|
|
8952
|
+
setAuthCookieHeaders(res, opts.apiToken, secureCookies);
|
|
8477
8953
|
}
|
|
8478
8954
|
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
8479
8955
|
if (requireAccessToken && !accessTokenOk) {
|
|
@@ -8591,6 +9067,88 @@ function createHttpServer(opts) {
|
|
|
8591
9067
|
await handleApiAnalyticsSummary(res);
|
|
8592
9068
|
return;
|
|
8593
9069
|
}
|
|
9070
|
+
if (url.pathname === "/api/requirement-intakes" && req.method === "GET") {
|
|
9071
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
9072
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
9073
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
9074
|
+
return;
|
|
9075
|
+
}
|
|
9076
|
+
await handleRequirementIntakeListForServer(res, opts.intakeService, opts.projectRoot);
|
|
9077
|
+
return;
|
|
9078
|
+
}
|
|
9079
|
+
const intakeProjectMatch = url.pathname.match(
|
|
9080
|
+
/^\/api\/projects\/([^/]+)\/requirement-intakes$/
|
|
9081
|
+
);
|
|
9082
|
+
if (intakeProjectMatch && req.method === "POST") {
|
|
9083
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
9084
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
9085
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
9086
|
+
return;
|
|
9087
|
+
}
|
|
9088
|
+
await handleRequirementIntakeCreate(
|
|
9089
|
+
res,
|
|
9090
|
+
req,
|
|
9091
|
+
opts.intakeService,
|
|
9092
|
+
decodeURIComponent(intakeProjectMatch[1])
|
|
9093
|
+
);
|
|
9094
|
+
return;
|
|
9095
|
+
}
|
|
9096
|
+
if (intakeProjectMatch && req.method === "GET") {
|
|
9097
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
9098
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
9099
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
9100
|
+
return;
|
|
9101
|
+
}
|
|
9102
|
+
await handleRequirementIntakeList(
|
|
9103
|
+
res,
|
|
9104
|
+
opts.intakeService,
|
|
9105
|
+
decodeURIComponent(intakeProjectMatch[1])
|
|
9106
|
+
);
|
|
9107
|
+
return;
|
|
9108
|
+
}
|
|
9109
|
+
const intakeIdMatch = url.pathname.match(/^\/api\/requirement-intakes\/([^/]+)$/);
|
|
9110
|
+
if (intakeIdMatch && req.method === "GET") {
|
|
9111
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
9112
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
9113
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
9114
|
+
return;
|
|
9115
|
+
}
|
|
9116
|
+
await handleRequirementIntakeGet(res, opts.intakeService, intakeIdMatch[1]);
|
|
9117
|
+
return;
|
|
9118
|
+
}
|
|
9119
|
+
if (intakeIdMatch && req.method === "PATCH") {
|
|
9120
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
9121
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
9122
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
9123
|
+
return;
|
|
9124
|
+
}
|
|
9125
|
+
await handleRequirementIntakeUpdate(res, req, opts.intakeService, intakeIdMatch[1]);
|
|
9126
|
+
return;
|
|
9127
|
+
}
|
|
9128
|
+
const intakeActionMatch = url.pathname.match(
|
|
9129
|
+
/^\/api\/requirement-intakes\/([^/]+)\/(answers|suggestions|submit|cancel|archive)$/
|
|
9130
|
+
);
|
|
9131
|
+
if (intakeActionMatch && req.method === "POST") {
|
|
9132
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
9133
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
9134
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
9135
|
+
return;
|
|
9136
|
+
}
|
|
9137
|
+
const intakeId = intakeActionMatch[1];
|
|
9138
|
+
const action = intakeActionMatch[2];
|
|
9139
|
+
if (action === "answers") {
|
|
9140
|
+
await handleRequirementIntakeAnswers(res, req, opts.intakeService, intakeId);
|
|
9141
|
+
} else if (action === "suggestions") {
|
|
9142
|
+
await handleRequirementIntakeSuggestions(res, req, opts.intakeService, intakeId);
|
|
9143
|
+
} else if (action === "submit") {
|
|
9144
|
+
await handleRequirementIntakeSubmit(res, opts.intakeService, intakeId);
|
|
9145
|
+
} else if (action === "cancel") {
|
|
9146
|
+
await handleRequirementIntakeCancel(res, req, opts.intakeService, intakeId);
|
|
9147
|
+
} else {
|
|
9148
|
+
await handleRequirementIntakeArchive(res, opts.intakeService, intakeId);
|
|
9149
|
+
}
|
|
9150
|
+
return;
|
|
9151
|
+
}
|
|
8594
9152
|
if (url.pathname === "/api/codemap/packages" && req.method === "GET") {
|
|
8595
9153
|
if (requireAccessToken && !accessTokenOk) {
|
|
8596
9154
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
@@ -8786,17 +9344,25 @@ function createHttpServer(opts) {
|
|
|
8786
9344
|
res.end(JSON.stringify({ error: "Project root not configured" }));
|
|
8787
9345
|
return;
|
|
8788
9346
|
}
|
|
8789
|
-
await handleDeadCodeScan(
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
9347
|
+
await handleDeadCodeScan(
|
|
9348
|
+
res,
|
|
9349
|
+
{
|
|
9350
|
+
projectRoot: opts.projectRoot,
|
|
9351
|
+
indexDir: opts.indexDir
|
|
9352
|
+
},
|
|
9353
|
+
req
|
|
9354
|
+
);
|
|
8793
9355
|
return;
|
|
8794
9356
|
}
|
|
8795
9357
|
if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
|
|
8796
|
-
await handleDeadCodeActionPlan(
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
9358
|
+
await handleDeadCodeActionPlan(
|
|
9359
|
+
res,
|
|
9360
|
+
{
|
|
9361
|
+
projectRoot: opts.projectRoot ?? "",
|
|
9362
|
+
indexDir: opts.indexDir
|
|
9363
|
+
},
|
|
9364
|
+
req
|
|
9365
|
+
);
|
|
8800
9366
|
return;
|
|
8801
9367
|
}
|
|
8802
9368
|
if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
|
|
@@ -11026,6 +11592,55 @@ import {
|
|
|
11026
11592
|
restartMcp,
|
|
11027
11593
|
updateMcp
|
|
11028
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
|
+
}
|
|
11029
11644
|
function mapStatus(raw) {
|
|
11030
11645
|
switch (raw) {
|
|
11031
11646
|
case "connected":
|
|
@@ -11100,7 +11715,7 @@ async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
|
|
|
11100
11715
|
payload: { servers: servers.map((server) => toView(server, health.get(server.name))) }
|
|
11101
11716
|
});
|
|
11102
11717
|
}
|
|
11103
|
-
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
11718
|
+
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry, trustBoundary) {
|
|
11104
11719
|
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
11105
11720
|
if (!d) return;
|
|
11106
11721
|
const validated = validateMcpServerPayload(msg.payload, "mcp.add");
|
|
@@ -11111,6 +11726,7 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
11111
11726
|
});
|
|
11112
11727
|
return;
|
|
11113
11728
|
}
|
|
11729
|
+
if (!await authorizeMcpMutation(ws, "mcp.add", name(msg), trustBoundary)) return;
|
|
11114
11730
|
const result = await addMcp(validated.value, d);
|
|
11115
11731
|
if (result.ok && result.server) {
|
|
11116
11732
|
send(ws, { type: "mcp.server.added", payload: { server: toView(result.server) } });
|
|
@@ -11128,7 +11744,7 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
11128
11744
|
payload: { success: result.ok, message: result.message }
|
|
11129
11745
|
});
|
|
11130
11746
|
}
|
|
11131
|
-
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
11747
|
+
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry, trustBoundary) {
|
|
11132
11748
|
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
11133
11749
|
if (!d) return;
|
|
11134
11750
|
const validated = validateMcpServerPayload(msg.payload, "mcp.update");
|
|
@@ -11139,6 +11755,7 @@ async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
11139
11755
|
});
|
|
11140
11756
|
return;
|
|
11141
11757
|
}
|
|
11758
|
+
if (!await authorizeMcpMutation(ws, "mcp.update", name(msg), trustBoundary)) return;
|
|
11142
11759
|
const result = await updateMcp(validated.value, d);
|
|
11143
11760
|
if (result.ok && result.server) {
|
|
11144
11761
|
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
@@ -12239,11 +12856,11 @@ function createModelOperations(context) {
|
|
|
12239
12856
|
}
|
|
12240
12857
|
|
|
12241
12858
|
// src/server/port-utils.ts
|
|
12242
|
-
import * as
|
|
12859
|
+
import * as net from "node:net";
|
|
12243
12860
|
import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
12244
12861
|
function isPortFree(host, port) {
|
|
12245
12862
|
return new Promise((resolve15) => {
|
|
12246
|
-
const srv =
|
|
12863
|
+
const srv = net.createServer();
|
|
12247
12864
|
srv.once("error", () => resolve15(false));
|
|
12248
12865
|
srv.once("listening", () => {
|
|
12249
12866
|
srv.close(() => resolve15(true));
|
|
@@ -12272,6 +12889,25 @@ async function findFreePort(host, startPort, opts = {}) {
|
|
|
12272
12889
|
});
|
|
12273
12890
|
}
|
|
12274
12891
|
|
|
12892
|
+
// src/server/intake-service.ts
|
|
12893
|
+
import { resolveWstackPaths as resolveWstackPaths4 } from "@wrongstack/core/utils";
|
|
12894
|
+
import {
|
|
12895
|
+
AllowAllIntakeAuthorizer,
|
|
12896
|
+
RequirementIntakeService,
|
|
12897
|
+
RequirementIntakeStore
|
|
12898
|
+
} from "@wrongstack/requirement-intake";
|
|
12899
|
+
function createProjectIntakeService(opts) {
|
|
12900
|
+
return new RequirementIntakeService({
|
|
12901
|
+
store: new RequirementIntakeStore({
|
|
12902
|
+
baseDir: resolveWstackPaths4({
|
|
12903
|
+
projectRoot: opts.projectRoot,
|
|
12904
|
+
globalRoot: opts.globalRoot
|
|
12905
|
+
}).projectRequirementIntakes
|
|
12906
|
+
}),
|
|
12907
|
+
authorizer: new AllowAllIntakeAuthorizer()
|
|
12908
|
+
});
|
|
12909
|
+
}
|
|
12910
|
+
|
|
12275
12911
|
// src/server/network-info.ts
|
|
12276
12912
|
import * as os2 from "node:os";
|
|
12277
12913
|
function isTailscaleIPv4(addr) {
|
|
@@ -12327,7 +12963,21 @@ function formatExternalAccessUrls(opts) {
|
|
|
12327
12963
|
}
|
|
12328
12964
|
|
|
12329
12965
|
// src/server/brain-handlers.ts
|
|
12966
|
+
import { BUILTIN_COUNCIL_PERSONAS } from "@wrongstack/core/execution";
|
|
12330
12967
|
import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
|
|
12968
|
+
var COUNCIL_PERSONA_CATALOG = Object.freeze(
|
|
12969
|
+
BUILTIN_COUNCIL_PERSONAS.map(
|
|
12970
|
+
(persona) => Object.freeze({
|
|
12971
|
+
id: persona.id,
|
|
12972
|
+
name: persona.name,
|
|
12973
|
+
description: persona.description,
|
|
12974
|
+
...persona.defaultVeto !== void 0 ? { defaultVeto: persona.defaultVeto } : {}
|
|
12975
|
+
})
|
|
12976
|
+
)
|
|
12977
|
+
);
|
|
12978
|
+
function brainConfigPayload(runtime) {
|
|
12979
|
+
return { ...runtime.getSnapshot(), personaCatalog: COUNCIL_PERSONA_CATALOG };
|
|
12980
|
+
}
|
|
12331
12981
|
function sendResult6(ctx, ws, success, message) {
|
|
12332
12982
|
ctx.send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
12333
12983
|
}
|
|
@@ -12367,7 +13017,7 @@ function handleBrainConfigGet(ctx, ws) {
|
|
|
12367
13017
|
}
|
|
12368
13018
|
ctx.send(ws, {
|
|
12369
13019
|
type: "brain.config",
|
|
12370
|
-
payload: { config: ctx.brainRuntime
|
|
13020
|
+
payload: { config: brainConfigPayload(ctx.brainRuntime), persisted: true }
|
|
12371
13021
|
});
|
|
12372
13022
|
}
|
|
12373
13023
|
async function handleBrainConfigSet(ctx, ws, payload) {
|
|
@@ -12391,7 +13041,7 @@ async function handleBrainConfigSet(ctx, ws, payload) {
|
|
|
12391
13041
|
ctx.send(ws, {
|
|
12392
13042
|
type: "brain.config",
|
|
12393
13043
|
payload: {
|
|
12394
|
-
config: ctx.brainRuntime
|
|
13044
|
+
config: brainConfigPayload(ctx.brainRuntime),
|
|
12395
13045
|
persisted: result.ok,
|
|
12396
13046
|
...result.ok ? {} : { error: result.error ?? "Persist failed." }
|
|
12397
13047
|
}
|
|
@@ -12401,7 +13051,7 @@ async function handleBrainConfigSet(ctx, ws, payload) {
|
|
|
12401
13051
|
ctx.send(ws, {
|
|
12402
13052
|
type: "brain.config",
|
|
12403
13053
|
payload: {
|
|
12404
|
-
config: ctx.brainRuntime
|
|
13054
|
+
config: brainConfigPayload(ctx.brainRuntime),
|
|
12405
13055
|
persisted: false,
|
|
12406
13056
|
error: `Invalid Brain setting: ${toErrorMessage5(err)}`
|
|
12407
13057
|
}
|
|
@@ -12455,6 +13105,8 @@ async function handleBrainAsk(ctx, ws, question) {
|
|
|
12455
13105
|
|
|
12456
13106
|
// src/server/context-meta.ts
|
|
12457
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";
|
|
12458
13110
|
function seedContextMeta(config, context) {
|
|
12459
13111
|
const meta = context.meta;
|
|
12460
13112
|
const autonomyCfg = config.autonomy ?? {};
|
|
@@ -12531,6 +13183,21 @@ function seedContextMeta(config, context) {
|
|
|
12531
13183
|
meta["tgDelegate"] = tgExt?.["notifyOnDelegate"] !== false;
|
|
12532
13184
|
const tgMs = tgExt?.["longToolThresholdMs"];
|
|
12533
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
|
+
}
|
|
12534
13201
|
const chimeraExt = config.extensions?.["wstack-chimera"];
|
|
12535
13202
|
meta["chimeraEnabled"] = chimeraExt?.["enabled"] === true;
|
|
12536
13203
|
meta["chimeraProvider"] = chimeraExt?.["provider"] ?? "";
|
|
@@ -12568,8 +13235,9 @@ function seedContextMeta(config, context) {
|
|
|
12568
13235
|
// src/server/pref-helpers.ts
|
|
12569
13236
|
import * as fs13 from "node:fs/promises";
|
|
12570
13237
|
import * as path15 from "node:path";
|
|
13238
|
+
import { pluginEntryMatchesName } from "@wrongstack/core/plugin";
|
|
12571
13239
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
12572
|
-
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";
|
|
12573
13241
|
var PREF_KEYS = [
|
|
12574
13242
|
"autonomy",
|
|
12575
13243
|
"autonomyDelayMs",
|
|
@@ -12650,6 +13318,8 @@ var PREF_KEYS = [
|
|
|
12650
13318
|
// Display-only toggles (purely visual WebUI prefs, not persisted to config).
|
|
12651
13319
|
"groupToolCalls",
|
|
12652
13320
|
"showThinkingLogs",
|
|
13321
|
+
// v15: chat-input auto-collapse (opt-in display toggle, default off).
|
|
13322
|
+
"autoCollapseInput",
|
|
12653
13323
|
// Per-plugin enable/disable map (parity with the embedded server).
|
|
12654
13324
|
"pluginsEnabled",
|
|
12655
13325
|
// Fleet chat verbosity: off | full (migrated from streamFleet boolean).
|
|
@@ -12704,6 +13374,8 @@ async function updateGlobalConfig(deps2, holder, mutate, errorLabel) {
|
|
|
12704
13374
|
var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set([
|
|
12705
13375
|
"groupToolCalls",
|
|
12706
13376
|
"showThinkingLogs",
|
|
13377
|
+
// v15: chat-input auto-collapse (opt-in display toggle, default off).
|
|
13378
|
+
"autoCollapseInput",
|
|
12707
13379
|
"autoReviewFallbackModels",
|
|
12708
13380
|
// v11 Display parity: agent-swarm panel + inverse fsAccess flag.
|
|
12709
13381
|
// The TUI settings picker mirrors these so the browser can keep the
|
|
@@ -12914,15 +13586,29 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
12914
13586
|
decrypted.debugStream = payload["debugStream"];
|
|
12915
13587
|
if (typeof payload["pluginsEnabled"] === "object" && payload["pluginsEnabled"] !== null) {
|
|
12916
13588
|
const ext = decrypted.extensions ?? {};
|
|
13589
|
+
const toggled = [];
|
|
12917
13590
|
for (const [pluginName, enabled] of Object.entries(
|
|
12918
13591
|
payload["pluginsEnabled"]
|
|
12919
13592
|
)) {
|
|
12920
|
-
if (
|
|
13593
|
+
if (FORBIDDEN_PROTO_KEYS3.has(pluginName)) continue;
|
|
13594
|
+
if (typeof enabled !== "boolean") continue;
|
|
12921
13595
|
const pExt = ext[pluginName] ?? {};
|
|
12922
13596
|
pExt["enabled"] = enabled;
|
|
12923
13597
|
ext[pluginName] = pExt;
|
|
13598
|
+
toggled.push([pluginName, enabled]);
|
|
12924
13599
|
}
|
|
12925
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
|
+
}
|
|
12926
13612
|
}
|
|
12927
13613
|
const chimeraTouched = typeof payload["chimeraEnabled"] === "boolean" || typeof payload["chimeraProvider"] === "string" || typeof payload["chimeraModel"] === "string" || typeof payload["chimeraMaxFiles"] === "number" || typeof payload["chimeraAutoFix"] === "string";
|
|
12928
13614
|
if (chimeraTouched) {
|
|
@@ -13031,39 +13717,6 @@ async function handlePrefsRoute(ws, msg, handlers) {
|
|
|
13031
13717
|
// src/server/process-handlers.ts
|
|
13032
13718
|
import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
|
|
13033
13719
|
import { getProcessRegistry as getProcessRegistry2 } from "@wrongstack/tools";
|
|
13034
|
-
|
|
13035
|
-
// src/server/privileged-actions.ts
|
|
13036
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
13037
|
-
import {
|
|
13038
|
-
isTrustDecisionAllowed
|
|
13039
|
-
} from "@wrongstack/core/security";
|
|
13040
|
-
async function authorizeWebUIAction(boundary, action, logger) {
|
|
13041
|
-
const request = {
|
|
13042
|
-
version: 1,
|
|
13043
|
-
requestId: randomUUID3(),
|
|
13044
|
-
actor: {
|
|
13045
|
-
kind: "remote-client",
|
|
13046
|
-
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
13047
|
-
},
|
|
13048
|
-
surface: "webui",
|
|
13049
|
-
capability: action.capability,
|
|
13050
|
-
subject: action.subject,
|
|
13051
|
-
risk: action.risk,
|
|
13052
|
-
scope: {
|
|
13053
|
-
...action.cwd ? { cwd: action.cwd } : {},
|
|
13054
|
-
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
13055
|
-
},
|
|
13056
|
-
authContext: { method: "session" },
|
|
13057
|
-
...action.metadata ? { metadata: action.metadata } : {}
|
|
13058
|
-
};
|
|
13059
|
-
const decision = await boundary.evaluate(request);
|
|
13060
|
-
logger?.debug?.(
|
|
13061
|
-
`[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
|
|
13062
|
-
);
|
|
13063
|
-
return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
|
|
13064
|
-
}
|
|
13065
|
-
|
|
13066
|
-
// src/server/process-handlers.ts
|
|
13067
13720
|
function handleProcessList(ws) {
|
|
13068
13721
|
try {
|
|
13069
13722
|
const procs = getProcessRegistry2().list();
|
|
@@ -13166,7 +13819,7 @@ async function handleProcessRoute(ws, msg, handlers) {
|
|
|
13166
13819
|
import * as fs14 from "node:fs/promises";
|
|
13167
13820
|
import * as path16 from "node:path";
|
|
13168
13821
|
import { DefaultSessionStore } from "@wrongstack/core/storage";
|
|
13169
|
-
import { resolveWstackPaths as
|
|
13822
|
+
import { resolveWstackPaths as resolveWstackPaths5 } from "@wrongstack/core/utils";
|
|
13170
13823
|
function createProjectHandlers(ctx) {
|
|
13171
13824
|
const sendTo = (ws, message) => {
|
|
13172
13825
|
if (ctx.sendMessage) ctx.sendMessage(ws, message);
|
|
@@ -13284,7 +13937,7 @@ function createProjectHandlers(ctx) {
|
|
|
13284
13937
|
});
|
|
13285
13938
|
return;
|
|
13286
13939
|
}
|
|
13287
|
-
const paths =
|
|
13940
|
+
const paths = resolveWstackPaths5({
|
|
13288
13941
|
projectRoot: resolved,
|
|
13289
13942
|
globalRoot: ctx.wpaths.globalRoot
|
|
13290
13943
|
});
|
|
@@ -13295,7 +13948,7 @@ function createProjectHandlers(ctx) {
|
|
|
13295
13948
|
const previous = ctx.getSession();
|
|
13296
13949
|
const previousId = previous.id;
|
|
13297
13950
|
const previousProjectRoot = ctx.getProjectRoot();
|
|
13298
|
-
const previousPaths =
|
|
13951
|
+
const previousPaths = resolveWstackPaths5({
|
|
13299
13952
|
projectRoot: previousProjectRoot,
|
|
13300
13953
|
globalRoot: ctx.wpaths.globalRoot
|
|
13301
13954
|
});
|
|
@@ -13879,8 +14532,10 @@ function createProviderOperations(deps2) {
|
|
|
13879
14532
|
if (result.ok) {
|
|
13880
14533
|
deps2.log?.(`[WebUI] Provider "${payload.id}" added via provider.add`);
|
|
13881
14534
|
}
|
|
14535
|
+
return result.ok;
|
|
13882
14536
|
} catch (err) {
|
|
13883
14537
|
sendOperationResult(ws, false, errMessage(err));
|
|
14538
|
+
return false;
|
|
13884
14539
|
}
|
|
13885
14540
|
}
|
|
13886
14541
|
async function handleProviderRemove(ws, providerId) {
|
|
@@ -14201,6 +14856,7 @@ var CLIENT_CONVERSATION_MESSAGE_TYPES = [
|
|
|
14201
14856
|
"completion.request",
|
|
14202
14857
|
"model.switch",
|
|
14203
14858
|
"model.refine",
|
|
14859
|
+
"model.fallback_choice",
|
|
14204
14860
|
"autonomy.switch",
|
|
14205
14861
|
"context.clear",
|
|
14206
14862
|
"context.compact",
|
|
@@ -14218,6 +14874,7 @@ var CLIENT_CONVERSATION_MESSAGE_TYPES = [
|
|
|
14218
14874
|
"modes.list",
|
|
14219
14875
|
"session.checkpoints",
|
|
14220
14876
|
"session.delete",
|
|
14877
|
+
"session.inspect",
|
|
14221
14878
|
"session.new",
|
|
14222
14879
|
"session.rename",
|
|
14223
14880
|
"session.resume",
|
|
@@ -14471,6 +15128,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
|
|
|
14471
15128
|
"provider.active_blocked",
|
|
14472
15129
|
"provider.error",
|
|
14473
15130
|
"provider.fallback",
|
|
15131
|
+
"provider.fallback_pending",
|
|
14474
15132
|
"provider.response",
|
|
14475
15133
|
"provider.retry",
|
|
14476
15134
|
"provider.status_changed",
|
|
@@ -14481,6 +15139,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
|
|
|
14481
15139
|
"session.checkpoints",
|
|
14482
15140
|
"session.damaged",
|
|
14483
15141
|
"session.end",
|
|
15142
|
+
"session.inspect",
|
|
14484
15143
|
"session.rewound",
|
|
14485
15144
|
"session.start",
|
|
14486
15145
|
"session.stats",
|
|
@@ -14511,6 +15170,7 @@ var SERVER_COLLABORATION_MESSAGE_TYPES = [
|
|
|
14511
15170
|
"collab.state",
|
|
14512
15171
|
"mailbox.action_result",
|
|
14513
15172
|
"mailbox.agent_registered",
|
|
15173
|
+
"mailbox.agent_deregistered",
|
|
14514
15174
|
"mailbox.agents",
|
|
14515
15175
|
"mailbox.cleared",
|
|
14516
15176
|
"mailbox.compacted",
|
|
@@ -14839,6 +15499,271 @@ function protocolAdvertisement() {
|
|
|
14839
15499
|
}
|
|
14840
15500
|
|
|
14841
15501
|
// src/server/session-history.ts
|
|
15502
|
+
function blockText(block) {
|
|
15503
|
+
if (typeof block === "string") return block;
|
|
15504
|
+
switch (block.type) {
|
|
15505
|
+
case "text":
|
|
15506
|
+
return block.text;
|
|
15507
|
+
case "thinking":
|
|
15508
|
+
return block.thinking;
|
|
15509
|
+
case "tool_use":
|
|
15510
|
+
return `[tool:${block.name}]`;
|
|
15511
|
+
case "tool_result":
|
|
15512
|
+
return block.content;
|
|
15513
|
+
case "image":
|
|
15514
|
+
return "[image]";
|
|
15515
|
+
default: {
|
|
15516
|
+
const _exhaustive = block;
|
|
15517
|
+
void _exhaustive;
|
|
15518
|
+
return "[block]";
|
|
15519
|
+
}
|
|
15520
|
+
}
|
|
15521
|
+
}
|
|
15522
|
+
function labelForEvent(e) {
|
|
15523
|
+
switch (e.type) {
|
|
15524
|
+
case "session_start":
|
|
15525
|
+
return "Session started";
|
|
15526
|
+
case "session_resumed":
|
|
15527
|
+
return "Session resumed";
|
|
15528
|
+
case "session_forked":
|
|
15529
|
+
return `Forked from ${e.parentSessionId}`;
|
|
15530
|
+
case "user_input": {
|
|
15531
|
+
const content = typeof e.content === "string" ? e.content : Array.isArray(e.content) ? e.content.map(blockText).join("") : "[input]";
|
|
15532
|
+
return `User: ${content.length > 100 ? content.slice(0, 100) + "\u2026" : content}`;
|
|
15533
|
+
}
|
|
15534
|
+
case "llm_request":
|
|
15535
|
+
return "LLM request";
|
|
15536
|
+
case "llm_response":
|
|
15537
|
+
return "LLM response";
|
|
15538
|
+
case "tool_use":
|
|
15539
|
+
return `Tool: ${e.name}`;
|
|
15540
|
+
case "tool_call_start":
|
|
15541
|
+
return `Tool start: ${e.name}`;
|
|
15542
|
+
case "tool_call_end":
|
|
15543
|
+
return `Tool done: ${e.name} (${e.durationMs}ms, ${e.ok === false ? "error" : "ok"})`;
|
|
15544
|
+
case "tool_result":
|
|
15545
|
+
return `Tool result: ${e.id}${e.isError ? " (error)" : ""}`;
|
|
15546
|
+
case "tool_progress":
|
|
15547
|
+
return `Progress: ${e.name} \u2014 ${e.event.type}`;
|
|
15548
|
+
case "compaction":
|
|
15549
|
+
return `Compaction: ${e.before} \u2192 ${e.after} tokens`;
|
|
15550
|
+
case "context_snapshot":
|
|
15551
|
+
return "Context snapshot";
|
|
15552
|
+
case "error":
|
|
15553
|
+
return `Error: ${e.message.length > 100 ? e.message.slice(0, 100) + "\u2026" : e.message}`;
|
|
15554
|
+
case "session_end":
|
|
15555
|
+
return "Session ended";
|
|
15556
|
+
case "message_appended":
|
|
15557
|
+
return `Message appended: ${e.message.role}`;
|
|
15558
|
+
case "message_updated":
|
|
15559
|
+
return "Message updated";
|
|
15560
|
+
case "messages_replaced": {
|
|
15561
|
+
const count = e.messagesOmitted ?? e.messages.length;
|
|
15562
|
+
return `Messages replaced (${e.messagesOmitted ? "~" : ""}${count} msgs)`;
|
|
15563
|
+
}
|
|
15564
|
+
case "message_truncated":
|
|
15565
|
+
return `Message truncated: ${e.before} \u2192 ${e.after}`;
|
|
15566
|
+
case "file_event":
|
|
15567
|
+
return `File ${e.operation}: ${e.filePath} (${e.toolName})`;
|
|
15568
|
+
case "file_snapshot":
|
|
15569
|
+
return `File snapshot at prompt #${e.promptIndex}`;
|
|
15570
|
+
case "file_observation":
|
|
15571
|
+
return `File observation: ${e.path}`;
|
|
15572
|
+
case "rewound":
|
|
15573
|
+
return `Rewound to prompt #${e.toPromptIndex} (${e.revertedFiles.length} files)`;
|
|
15574
|
+
case "mode_changed":
|
|
15575
|
+
return `Mode: ${e.from} \u2192 ${e.to}`;
|
|
15576
|
+
case "task_created":
|
|
15577
|
+
return `Task created: ${e.title}`;
|
|
15578
|
+
case "task_updated":
|
|
15579
|
+
return `Task ${e.taskId}: ${e.status}`;
|
|
15580
|
+
case "task_completed":
|
|
15581
|
+
return `Task done: ${e.title}`;
|
|
15582
|
+
case "task_failed":
|
|
15583
|
+
return `Task failed: ${e.title} \u2014 ${e.error.length > 80 ? e.error.slice(0, 80) + "\u2026" : e.error}`;
|
|
15584
|
+
case "agent_spawned":
|
|
15585
|
+
return `Agent spawned: ${e.role}`;
|
|
15586
|
+
case "agent_stopped":
|
|
15587
|
+
return "Agent stopped";
|
|
15588
|
+
case "agent_error":
|
|
15589
|
+
return `Agent error: ${e.error.length > 80 ? e.error.slice(0, 80) + "\u2026" : e.error}`;
|
|
15590
|
+
case "provider_retry":
|
|
15591
|
+
return `Retry: ${e.description} (attempt ${e.attempt})`;
|
|
15592
|
+
case "provider_error":
|
|
15593
|
+
return `Provider error: ${e.description}`;
|
|
15594
|
+
case "checkpoint":
|
|
15595
|
+
return `Checkpoint at prompt #${e.promptIndex}`;
|
|
15596
|
+
case "in_flight_start":
|
|
15597
|
+
return `In-flight started: ${e.context}`;
|
|
15598
|
+
case "in_flight_end":
|
|
15599
|
+
return `In-flight ended: ${e.reason}`;
|
|
15600
|
+
case "side_effect":
|
|
15601
|
+
return `Side effect: ${e.toolName} (${e.risk})`;
|
|
15602
|
+
case "spec_parsed":
|
|
15603
|
+
return `Spec parsed: ${e.title}`;
|
|
15604
|
+
case "spec_analyzed":
|
|
15605
|
+
return `Spec analyzed: ${e.specId}`;
|
|
15606
|
+
case "skill_activated":
|
|
15607
|
+
return `Skill: ${e.skillName}`;
|
|
15608
|
+
case "skill_deactivated":
|
|
15609
|
+
return `Skill done: ${e.skillName}`;
|
|
15610
|
+
default: {
|
|
15611
|
+
const _exhaustive = e;
|
|
15612
|
+
void _exhaustive;
|
|
15613
|
+
return String(_exhaustive);
|
|
15614
|
+
}
|
|
15615
|
+
}
|
|
15616
|
+
}
|
|
15617
|
+
function detailForEvent(e) {
|
|
15618
|
+
switch (e.type) {
|
|
15619
|
+
case "session_start":
|
|
15620
|
+
return `${e.model} @ ${e.provider}`;
|
|
15621
|
+
case "session_resumed":
|
|
15622
|
+
return `${e.model} @ ${e.provider}`;
|
|
15623
|
+
case "session_forked":
|
|
15624
|
+
return `parent checkpoint: ${e.parentCheckpointHash.slice(0, 12)}\u2026`;
|
|
15625
|
+
case "llm_request":
|
|
15626
|
+
return `${e.model} \xB7 ${e.messageCount} msgs \xB7 ${e.toolCount ?? "?"} tools`;
|
|
15627
|
+
case "llm_response":
|
|
15628
|
+
return `${e.stopReason} \xB7 ${e.usage.input ?? 0}+${e.usage.output ?? 0} tokens`;
|
|
15629
|
+
case "tool_use":
|
|
15630
|
+
return `id: ${e.id}`;
|
|
15631
|
+
case "tool_call_start":
|
|
15632
|
+
return `id: ${e.id}`;
|
|
15633
|
+
case "tool_call_end":
|
|
15634
|
+
return `${(e.outputBytes ?? e.outputSize ?? 0).toLocaleString()} B \xB7 ${e.outputLines ?? 0} lines`;
|
|
15635
|
+
case "tool_progress":
|
|
15636
|
+
return `${e.event.type}${e.event.text ? `: ${e.event.text}` : ""}`;
|
|
15637
|
+
case "compaction":
|
|
15638
|
+
return `saved ~${Math.max(0, e.before - e.after)} tokens`;
|
|
15639
|
+
case "context_snapshot":
|
|
15640
|
+
return `${e.messages.length} msgs${e.messagesOmitted ? ` (${e.messagesOmitted} omitted)` : ""}`;
|
|
15641
|
+
case "error":
|
|
15642
|
+
return `phase: ${e.phase}`;
|
|
15643
|
+
case "session_end":
|
|
15644
|
+
return `${e.usage.input ?? 0}+${e.usage.output ?? 0} total tokens`;
|
|
15645
|
+
case "message_appended":
|
|
15646
|
+
return `appended ${e.message.role}`;
|
|
15647
|
+
case "message_updated":
|
|
15648
|
+
return `at index ${e.index}`;
|
|
15649
|
+
case "messages_replaced":
|
|
15650
|
+
return `${e.messagesOmitted ?? e.messages.length} total`;
|
|
15651
|
+
case "message_truncated":
|
|
15652
|
+
return `truncated to ${e.after} tokens`;
|
|
15653
|
+
case "mode_changed":
|
|
15654
|
+
return `${e.from} \u2192 ${e.to}`;
|
|
15655
|
+
case "task_created":
|
|
15656
|
+
return `id: ${e.taskId}`;
|
|
15657
|
+
case "task_updated":
|
|
15658
|
+
return `${e.taskId}: ${e.status}`;
|
|
15659
|
+
case "task_completed":
|
|
15660
|
+
return `${e.taskId}`;
|
|
15661
|
+
case "task_failed":
|
|
15662
|
+
return `${e.taskId}: ${e.error}`;
|
|
15663
|
+
case "agent_spawned":
|
|
15664
|
+
return `id: ${e.agentId} (${e.role})`;
|
|
15665
|
+
case "agent_stopped":
|
|
15666
|
+
return `id: ${e.agentId}`;
|
|
15667
|
+
case "agent_error":
|
|
15668
|
+
return `id: ${e.agentId}: ${e.error}`;
|
|
15669
|
+
case "file_event":
|
|
15670
|
+
return `${e.filePath} (${e.toolName})`;
|
|
15671
|
+
case "file_snapshot":
|
|
15672
|
+
return `${e.files.length} files at prompt #${e.promptIndex}`;
|
|
15673
|
+
case "file_observation":
|
|
15674
|
+
return `${e.source === "user" ? "user-saved" : "tool-written"} at ${e.path}`;
|
|
15675
|
+
case "rewound":
|
|
15676
|
+
return `${e.revertedFiles.length} files reverted`;
|
|
15677
|
+
case "in_flight_start":
|
|
15678
|
+
return e.context;
|
|
15679
|
+
case "in_flight_end":
|
|
15680
|
+
return `${e.reason}`;
|
|
15681
|
+
case "side_effect":
|
|
15682
|
+
return `${e.toolName} (${e.risk})${e.outcome ? `: ${e.outcome}` : ""}`;
|
|
15683
|
+
case "provider_retry":
|
|
15684
|
+
return `delay ${e.delayMs}ms${e.status ? ` \xB7 HTTP ${e.status}` : ""}`;
|
|
15685
|
+
case "provider_error":
|
|
15686
|
+
return `${e.retryable ? "retryable" : "fatal"}${e.status ? ` \xB7 HTTP ${e.status}` : ""}`;
|
|
15687
|
+
case "user_input":
|
|
15688
|
+
return typeof e.content === "string" ? `${e.content.length} chars` : `${e.content.length} blocks`;
|
|
15689
|
+
case "tool_result":
|
|
15690
|
+
return `${e.isError ? "error" : "ok"}`;
|
|
15691
|
+
case "checkpoint":
|
|
15692
|
+
return `prompt #${e.promptIndex}`;
|
|
15693
|
+
case "spec_parsed":
|
|
15694
|
+
return `${e.title} (${e.completeness}% complete)`;
|
|
15695
|
+
case "spec_analyzed":
|
|
15696
|
+
return `${e.gaps.length} gaps identified`;
|
|
15697
|
+
case "skill_activated":
|
|
15698
|
+
return `at ${e.skillName}`;
|
|
15699
|
+
case "skill_deactivated":
|
|
15700
|
+
return `at ${e.skillName}`;
|
|
15701
|
+
default: {
|
|
15702
|
+
const _exhaustive = e;
|
|
15703
|
+
void _exhaustive;
|
|
15704
|
+
return "";
|
|
15705
|
+
}
|
|
15706
|
+
}
|
|
15707
|
+
}
|
|
15708
|
+
function buildInspectPayload(summary, events, fallback) {
|
|
15709
|
+
const inspectEvents = events.map((e) => ({
|
|
15710
|
+
ts: e.ts,
|
|
15711
|
+
type: e.type,
|
|
15712
|
+
label: labelForEvent(e),
|
|
15713
|
+
detail: detailForEvent(e)
|
|
15714
|
+
}));
|
|
15715
|
+
const fileEvents = [];
|
|
15716
|
+
let computedToolCallCount = 0;
|
|
15717
|
+
let computedToolErrorCount = 0;
|
|
15718
|
+
let computedFileChangeCount = 0;
|
|
15719
|
+
let computedCompactionCount = 0;
|
|
15720
|
+
let computedMessageCount = 0;
|
|
15721
|
+
let computedIterationCount = 0;
|
|
15722
|
+
const computedToolBreakdown = {};
|
|
15723
|
+
for (const e of events) {
|
|
15724
|
+
if (e.type === "file_event") {
|
|
15725
|
+
fileEvents.push({
|
|
15726
|
+
operation: e.operation,
|
|
15727
|
+
filePath: e.filePath,
|
|
15728
|
+
toolName: e.toolName,
|
|
15729
|
+
ts: e.ts
|
|
15730
|
+
});
|
|
15731
|
+
computedFileChangeCount++;
|
|
15732
|
+
} else if (e.type === "tool_call_end") {
|
|
15733
|
+
computedToolCallCount++;
|
|
15734
|
+
if (e.ok === false) computedToolErrorCount++;
|
|
15735
|
+
computedToolBreakdown[e.name] = (computedToolBreakdown[e.name] ?? 0) + 1;
|
|
15736
|
+
} else if (e.type === "compaction") {
|
|
15737
|
+
computedCompactionCount++;
|
|
15738
|
+
} else if (e.type === "user_input") {
|
|
15739
|
+
computedMessageCount++;
|
|
15740
|
+
} else if (e.type === "llm_response") {
|
|
15741
|
+
computedIterationCount++;
|
|
15742
|
+
}
|
|
15743
|
+
}
|
|
15744
|
+
const s = summary;
|
|
15745
|
+
return {
|
|
15746
|
+
id: s?.id ?? fallback.id,
|
|
15747
|
+
title: s?.title ?? fallback.title,
|
|
15748
|
+
...s?.name !== void 0 ? { name: s.name } : {},
|
|
15749
|
+
model: s?.model ?? fallback.model,
|
|
15750
|
+
provider: s?.provider ?? fallback.provider,
|
|
15751
|
+
startedAt: s?.startedAt ?? fallback.startedAt,
|
|
15752
|
+
...(s?.endedAt ?? fallback.endedAt) !== void 0 ? { endedAt: s?.endedAt ?? fallback.endedAt } : {},
|
|
15753
|
+
tokenTotal: s?.tokenTotal ?? 0,
|
|
15754
|
+
...s?.outcome !== void 0 ? { outcome: s.outcome } : {},
|
|
15755
|
+
messageCount: s?.messageCount ?? computedMessageCount,
|
|
15756
|
+
iterationCount: s?.iterationCount ?? computedIterationCount,
|
|
15757
|
+
toolCallCount: s?.toolCallCount ?? computedToolCallCount,
|
|
15758
|
+
toolErrorCount: s?.toolErrorCount ?? computedToolErrorCount,
|
|
15759
|
+
fileChangeCount: s?.fileChangeCount ?? computedFileChangeCount,
|
|
15760
|
+
compactionCount: s?.compactionCount ?? computedCompactionCount,
|
|
15761
|
+
toolBreakdown: s?.toolBreakdown ?? computedToolBreakdown,
|
|
15762
|
+
events: inspectEvents,
|
|
15763
|
+
fileEvents,
|
|
15764
|
+
...s?.lastUserMessage !== void 0 ? { lastUserMessage: s.lastUserMessage } : {}
|
|
15765
|
+
};
|
|
15766
|
+
}
|
|
14842
15767
|
function toSessionHistoryEntry(summary, currentSessionId2) {
|
|
14843
15768
|
return {
|
|
14844
15769
|
id: summary.id,
|
|
@@ -15113,6 +16038,7 @@ function createSessionHandlers(ctx) {
|
|
|
15113
16038
|
tools: ctx.listTools?.() ?? ctx.toolRegistry?.list(),
|
|
15114
16039
|
baseRevision: typeof payload["baseRevision"] === "string" ? payload["baseRevision"] : "",
|
|
15115
16040
|
messages: payload["messages"],
|
|
16041
|
+
removals: payload["removals"],
|
|
15116
16042
|
allowRepair: payload["allowRepair"] === true,
|
|
15117
16043
|
runActive: ctx.isRunActive?.() === true
|
|
15118
16044
|
});
|
|
@@ -15129,6 +16055,7 @@ function createSessionHandlers(ctx) {
|
|
|
15129
16055
|
tools: ctx.listTools?.() ?? ctx.toolRegistry?.list(),
|
|
15130
16056
|
baseRevision: typeof payload["baseRevision"] === "string" ? payload["baseRevision"] : "",
|
|
15131
16057
|
messages: payload["messages"],
|
|
16058
|
+
removals: payload["removals"],
|
|
15132
16059
|
allowRepair: payload["allowRepair"] === true,
|
|
15133
16060
|
runActive: ctx.isRunActive?.() === true
|
|
15134
16061
|
});
|
|
@@ -15385,6 +16312,47 @@ function createSessionHandlers(ctx) {
|
|
|
15385
16312
|
if (!ensureCurrentSession(ws, msg, "session.save")) return;
|
|
15386
16313
|
result(ws, true, `Session ${ctx.getSession().id} is auto-saved`);
|
|
15387
16314
|
},
|
|
16315
|
+
inspectSession: async (ws, msg) => {
|
|
16316
|
+
const { id } = msg.payload;
|
|
16317
|
+
if (!id) {
|
|
16318
|
+
sendTo(ws, {
|
|
16319
|
+
type: "session.inspect",
|
|
16320
|
+
payload: { id: "", error: "Session id is required" }
|
|
16321
|
+
});
|
|
16322
|
+
return;
|
|
16323
|
+
}
|
|
16324
|
+
try {
|
|
16325
|
+
const store = ctx.getSessionStore();
|
|
16326
|
+
const data = await store.load(id);
|
|
16327
|
+
let summary;
|
|
16328
|
+
try {
|
|
16329
|
+
const summaries = await store.list(200);
|
|
16330
|
+
summary = summaries.find((s) => s.id === id);
|
|
16331
|
+
} catch {
|
|
16332
|
+
summary = void 0;
|
|
16333
|
+
}
|
|
16334
|
+
const payload = buildInspectPayload(summary, data.events, {
|
|
16335
|
+
id: data.metadata.id,
|
|
16336
|
+
title: data.metadata.title ?? "",
|
|
16337
|
+
model: data.metadata.model ?? "",
|
|
16338
|
+
provider: data.metadata.provider ?? "",
|
|
16339
|
+
startedAt: data.metadata.startedAt,
|
|
16340
|
+
endedAt: data.metadata.endedAt
|
|
16341
|
+
});
|
|
16342
|
+
sendTo(ws, {
|
|
16343
|
+
type: "session.inspect",
|
|
16344
|
+
payload
|
|
16345
|
+
});
|
|
16346
|
+
} catch (err) {
|
|
16347
|
+
sendTo(ws, {
|
|
16348
|
+
type: "session.inspect",
|
|
16349
|
+
payload: {
|
|
16350
|
+
id,
|
|
16351
|
+
error: err instanceof Error ? err.message : String(err)
|
|
16352
|
+
}
|
|
16353
|
+
});
|
|
16354
|
+
}
|
|
16355
|
+
},
|
|
15388
16356
|
listCheckpoints: async (ws, msg) => {
|
|
15389
16357
|
if (!ensureCurrentSession(ws, msg, "session.checkpoints")) return;
|
|
15390
16358
|
try {
|
|
@@ -15907,6 +16875,19 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
|
|
|
15907
16875
|
return true;
|
|
15908
16876
|
}
|
|
15909
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
|
+
|
|
15910
16891
|
// src/server/agent-roster-routes.ts
|
|
15911
16892
|
async function handleAgentRosterRoute(ws, msg, handlers) {
|
|
15912
16893
|
if (!msg.type.startsWith("agent-roster.")) return false;
|
|
@@ -16071,13 +17052,17 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16071
17052
|
case "model.refine":
|
|
16072
17053
|
await routes.refineModel(ws, msg);
|
|
16073
17054
|
return true;
|
|
17055
|
+
case "model.fallback_choice":
|
|
17056
|
+
await routes.fallbackChoice(ws, msg);
|
|
17057
|
+
return true;
|
|
16074
17058
|
case "key.add":
|
|
16075
17059
|
case "key.update": {
|
|
16076
17060
|
const payload = asPayloadRecord(msg);
|
|
16077
17061
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16078
17062
|
const label = payload ? requiredString(payload, "label") : null;
|
|
16079
17063
|
const apiKey = payload ? requiredString(payload, "apiKey") : null;
|
|
16080
|
-
if (!providerId || !label || !apiKey)
|
|
17064
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label || !apiKey)
|
|
17065
|
+
return invalidPayload(ws, msg.type);
|
|
16081
17066
|
await routes.providerHandlers.handleKeyUpsert(ws, providerId, label, apiKey);
|
|
16082
17067
|
return true;
|
|
16083
17068
|
}
|
|
@@ -16085,7 +17070,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16085
17070
|
const payload = asPayloadRecord(msg);
|
|
16086
17071
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16087
17072
|
const label = payload ? requiredString(payload, "label") : null;
|
|
16088
|
-
if (!providerId || !
|
|
17073
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label)
|
|
17074
|
+
return invalidPayload(ws, msg.type);
|
|
16089
17075
|
await routes.providerHandlers.handleKeyDelete(ws, providerId, label);
|
|
16090
17076
|
return true;
|
|
16091
17077
|
}
|
|
@@ -16093,7 +17079,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16093
17079
|
const payload = asPayloadRecord(msg);
|
|
16094
17080
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16095
17081
|
const label = payload ? requiredString(payload, "label") : null;
|
|
16096
|
-
if (!providerId || !
|
|
17082
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label)
|
|
17083
|
+
return invalidPayload(ws, msg.type);
|
|
16097
17084
|
await routes.providerHandlers.handleKeySetActive(ws, providerId, label);
|
|
16098
17085
|
return true;
|
|
16099
17086
|
}
|
|
@@ -16105,11 +17092,11 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16105
17092
|
const apiKey = payload?.["apiKey"];
|
|
16106
17093
|
const models = payload ? optionalStringArray(payload, "models") : null;
|
|
16107
17094
|
const customModels = payload ? optionalCustomModels(payload) : null;
|
|
16108
|
-
if (!id || !family) return invalidPayload(ws, msg.type);
|
|
17095
|
+
if (!id || !SAFE_CONFIG_KEY.test(id) || !family) return invalidPayload(ws, msg.type);
|
|
16109
17096
|
if (baseUrl !== void 0 && typeof baseUrl !== "string") return invalidPayload(ws, msg.type);
|
|
16110
17097
|
if (apiKey !== void 0 && typeof apiKey !== "string") return invalidPayload(ws, msg.type);
|
|
16111
17098
|
if (models === null || customModels === null) return invalidPayload(ws, msg.type);
|
|
16112
|
-
await routes.providerHandlers.handleProviderAdd(ws, {
|
|
17099
|
+
const added = await routes.providerHandlers.handleProviderAdd(ws, {
|
|
16113
17100
|
id,
|
|
16114
17101
|
family,
|
|
16115
17102
|
baseUrl,
|
|
@@ -16117,20 +17104,22 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16117
17104
|
models,
|
|
16118
17105
|
customModels
|
|
16119
17106
|
});
|
|
16120
|
-
|
|
17107
|
+
if (added) {
|
|
17108
|
+
void routes.adoptDefaultProviderIfUnset(id).catch(() => void 0);
|
|
17109
|
+
}
|
|
16121
17110
|
return true;
|
|
16122
17111
|
}
|
|
16123
17112
|
case "provider.remove": {
|
|
16124
17113
|
const payload = asPayloadRecord(msg);
|
|
16125
17114
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16126
|
-
if (!providerId) return invalidPayload(ws, msg.type);
|
|
17115
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId)) return invalidPayload(ws, msg.type);
|
|
16127
17116
|
await routes.providerHandlers.handleProviderRemove(ws, providerId);
|
|
16128
17117
|
return true;
|
|
16129
17118
|
}
|
|
16130
17119
|
case "provider.clear_models": {
|
|
16131
17120
|
const payload = asPayloadRecord(msg);
|
|
16132
17121
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16133
|
-
if (!providerId) return invalidPayload(ws, msg.type);
|
|
17122
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId)) return invalidPayload(ws, msg.type);
|
|
16134
17123
|
await routes.providerHandlers.handleProviderClearModels(ws, providerId);
|
|
16135
17124
|
return true;
|
|
16136
17125
|
}
|
|
@@ -16144,7 +17133,7 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16144
17133
|
const customModelRaw = payload["customModel"];
|
|
16145
17134
|
if (!isRecord4(customModelRaw)) return invalidPayload(ws, msg.type);
|
|
16146
17135
|
const cm = optionalCustomModels({ customModels: { [modelId]: customModelRaw } });
|
|
16147
|
-
if (!cm
|
|
17136
|
+
if (!cm?.[modelId]) return invalidPayload(ws, msg.type);
|
|
16148
17137
|
await routes.providerHandlers.handleCustomModelSet(ws, providerId, modelId, cm[modelId]);
|
|
16149
17138
|
return true;
|
|
16150
17139
|
}
|
|
@@ -16162,7 +17151,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16162
17151
|
const payload = asPayloadRecord(msg);
|
|
16163
17152
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16164
17153
|
const previousModels = payload ? optionalStringArray(payload, "previousModels") : null;
|
|
16165
|
-
if (!providerId || !
|
|
17154
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !previousModels)
|
|
17155
|
+
return invalidPayload(ws, msg.type);
|
|
16166
17156
|
await routes.providerHandlers.handleProviderUndoClear(ws, providerId, previousModels);
|
|
16167
17157
|
return true;
|
|
16168
17158
|
}
|
|
@@ -16172,7 +17162,7 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16172
17162
|
const envVars = payload ? optionalStringArray(payload, "envVars") : null;
|
|
16173
17163
|
const models = payload ? optionalStringArray(payload, "models") : null;
|
|
16174
17164
|
const customModels = payload ? optionalCustomModels(payload) : null;
|
|
16175
|
-
if (!payload || !id || envVars === null || models === null || customModels === null)
|
|
17165
|
+
if (!payload || !id || !SAFE_CONFIG_KEY.test(id) || envVars === null || models === null || customModels === null)
|
|
16176
17166
|
return invalidPayload(ws, msg.type);
|
|
16177
17167
|
for (const key of ["family", "baseUrl"]) {
|
|
16178
17168
|
if (payload[key] !== void 0 && typeof payload[key] !== "string")
|
|
@@ -16192,7 +17182,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16192
17182
|
const payload = asPayloadRecord(msg);
|
|
16193
17183
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16194
17184
|
const timeoutMs = payload ? optionalNumber(payload, "timeoutMs") : null;
|
|
16195
|
-
if (!providerId || timeoutMs === null)
|
|
17185
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || timeoutMs === null)
|
|
17186
|
+
return invalidPayload(ws, msg.type);
|
|
16196
17187
|
await routes.providerHandlers.handleProviderProbe(ws, providerId, timeoutMs);
|
|
16197
17188
|
return true;
|
|
16198
17189
|
}
|
|
@@ -16201,7 +17192,7 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16201
17192
|
const kind = oauthKind(payload);
|
|
16202
17193
|
const providerId = payload?.["providerId"];
|
|
16203
17194
|
if (!kind) return invalidPayload(ws, msg.type);
|
|
16204
|
-
if (providerId !== void 0 && typeof providerId !== "string") {
|
|
17195
|
+
if (providerId !== void 0 && (typeof providerId !== "string" || !SAFE_CONFIG_KEY.test(providerId))) {
|
|
16205
17196
|
return invalidPayload(ws, msg.type);
|
|
16206
17197
|
}
|
|
16207
17198
|
await routes.providerHandlers.handleOAuthStart(ws, kind, providerId);
|
|
@@ -16240,7 +17231,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16240
17231
|
const payload = asPayloadRecord(msg);
|
|
16241
17232
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16242
17233
|
const model = payload ? requiredString(payload, "model") : null;
|
|
16243
|
-
if (!providerId || !
|
|
17234
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !model)
|
|
17235
|
+
return invalidPayload(ws, msg.type);
|
|
16244
17236
|
const released = routes.statusTracker.retryNow(providerId, model);
|
|
16245
17237
|
sendResult2(
|
|
16246
17238
|
ws,
|
|
@@ -16257,7 +17249,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
16257
17249
|
const payload = asPayloadRecord(msg);
|
|
16258
17250
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
16259
17251
|
const model = payload ? requiredString(payload, "model") : null;
|
|
16260
|
-
if (!providerId || !
|
|
17252
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !model)
|
|
17253
|
+
return invalidPayload(ws, msg.type);
|
|
16261
17254
|
routes.statusTracker.clear(providerId, model);
|
|
16262
17255
|
sendResult2(ws, true, `Cleared tracking for ${providerId}/${model}.`);
|
|
16263
17256
|
return true;
|
|
@@ -16338,6 +17331,9 @@ async function handleSessionRoute(ws, msg, handlers) {
|
|
|
16338
17331
|
case "session.save":
|
|
16339
17332
|
await handlers.saveSession(ws, msg);
|
|
16340
17333
|
return true;
|
|
17334
|
+
case "session.inspect":
|
|
17335
|
+
await handlers.inspectSession(ws, msg);
|
|
17336
|
+
return true;
|
|
16341
17337
|
case "session.checkpoints":
|
|
16342
17338
|
await handlers.listCheckpoints(ws, msg);
|
|
16343
17339
|
return true;
|
|
@@ -18210,7 +19206,8 @@ function setupEvents(deps2) {
|
|
|
18210
19206
|
attempt: e.attempt,
|
|
18211
19207
|
delayMs: e.delayMs,
|
|
18212
19208
|
status: e.status,
|
|
18213
|
-
description: e.description
|
|
19209
|
+
description: e.description,
|
|
19210
|
+
...e.errorBody ? { errorBody: e.errorBody } : {}
|
|
18214
19211
|
})
|
|
18215
19212
|
});
|
|
18216
19213
|
appendForCurrentSession(e.sessionId, {
|
|
@@ -18220,7 +19217,8 @@ function setupEvents(deps2) {
|
|
|
18220
19217
|
attempt: e.attempt,
|
|
18221
19218
|
delayMs: e.delayMs,
|
|
18222
19219
|
status: e.status,
|
|
18223
|
-
description: e.description
|
|
19220
|
+
description: e.description,
|
|
19221
|
+
...e.errorBody ? { errorBody: e.errorBody } : {}
|
|
18224
19222
|
});
|
|
18225
19223
|
});
|
|
18226
19224
|
on("provider.status_changed", (e) => {
|
|
@@ -18260,7 +19258,8 @@ function setupEvents(deps2) {
|
|
|
18260
19258
|
providerId: e.providerId,
|
|
18261
19259
|
status: e.status,
|
|
18262
19260
|
description: e.description,
|
|
18263
|
-
retryable: e.retryable
|
|
19261
|
+
retryable: e.retryable,
|
|
19262
|
+
...e.errorBody ? { errorBody: e.errorBody } : {}
|
|
18264
19263
|
})
|
|
18265
19264
|
});
|
|
18266
19265
|
appendForCurrentSession(e.sessionId, {
|
|
@@ -18269,7 +19268,8 @@ function setupEvents(deps2) {
|
|
|
18269
19268
|
providerId: e.providerId,
|
|
18270
19269
|
status: e.status,
|
|
18271
19270
|
description: e.description,
|
|
18272
|
-
retryable: e.retryable
|
|
19271
|
+
retryable: e.retryable,
|
|
19272
|
+
...e.errorBody ? { errorBody: e.errorBody } : {}
|
|
18273
19273
|
});
|
|
18274
19274
|
});
|
|
18275
19275
|
on("provider.fallback", (e) => {
|
|
@@ -18280,7 +19280,22 @@ function setupEvents(deps2) {
|
|
|
18280
19280
|
from: e.from,
|
|
18281
19281
|
to: e.to,
|
|
18282
19282
|
status: e.status,
|
|
18283
|
-
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
|
|
18284
19299
|
})
|
|
18285
19300
|
});
|
|
18286
19301
|
});
|
|
@@ -18365,6 +19380,15 @@ function setupEvents(deps2) {
|
|
|
18365
19380
|
type: "mailbox.agent_registered",
|
|
18366
19381
|
payload
|
|
18367
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
|
+
});
|
|
18368
19392
|
})
|
|
18369
19393
|
);
|
|
18370
19394
|
const forwardSubagent = (kind, payload) => broadcast2(clients, { type: "subagent.event", payload: sessionPayload2({ kind, ...payload }) });
|
|
@@ -20649,29 +21673,10 @@ import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/too
|
|
|
20649
21673
|
// src/server/model-auto-discovery.ts
|
|
20650
21674
|
import * as fs19 from "node:fs/promises";
|
|
20651
21675
|
import * as path24 from "node:path";
|
|
20652
|
-
import {
|
|
21676
|
+
import { discoverOpenAICompatibleModels, resolveDiscoveryTargets } from "@wrongstack/providers";
|
|
20653
21677
|
function isOverlayRegistry(value) {
|
|
20654
21678
|
return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
|
|
20655
21679
|
}
|
|
20656
|
-
function resolveKey(cfg) {
|
|
20657
|
-
if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
|
|
20658
|
-
const active = cfg.activeKey ? cfg.apiKeys.find((key) => key.label === cfg.activeKey) : void 0;
|
|
20659
|
-
return (active ?? cfg.apiKeys[0])?.apiKey;
|
|
20660
|
-
}
|
|
20661
|
-
return cfg.apiKey && cfg.apiKey.length > 0 ? cfg.apiKey : void 0;
|
|
20662
|
-
}
|
|
20663
|
-
function eligibleProviders(config) {
|
|
20664
|
-
const out = [];
|
|
20665
|
-
for (const [id, cfg] of Object.entries(config.providers ?? {})) {
|
|
20666
|
-
const preset = COMPATIBLE_PRESETS[id];
|
|
20667
|
-
const enabled = cfg.autoDiscoverModels ?? preset?.autoDiscover ?? false;
|
|
20668
|
-
if (!enabled) continue;
|
|
20669
|
-
const baseUrl = cfg.baseUrl ?? preset?.defaultBaseUrl;
|
|
20670
|
-
if (!baseUrl) continue;
|
|
20671
|
-
out.push({ id, cfg, baseUrl, apiKey: resolveKey(cfg) });
|
|
20672
|
-
}
|
|
20673
|
-
return out;
|
|
20674
|
-
}
|
|
20675
21680
|
async function readCache(file) {
|
|
20676
21681
|
try {
|
|
20677
21682
|
return JSON.parse(await fs19.readFile(file, "utf8"));
|
|
@@ -20682,14 +21687,13 @@ async function readCache(file) {
|
|
|
20682
21687
|
async function discoverAndMergeWebuiProviders(opts) {
|
|
20683
21688
|
const registry = opts.registry;
|
|
20684
21689
|
if (!isOverlayRegistry(registry)) return;
|
|
20685
|
-
const targets =
|
|
21690
|
+
const targets = resolveDiscoveryTargets(opts.config);
|
|
20686
21691
|
if (targets.length === 0) return;
|
|
20687
21692
|
const cacheFile = path24.join(opts.cacheDir, "discovered-models-cache.json");
|
|
20688
21693
|
const cache2 = await readCache(cacheFile);
|
|
20689
21694
|
let cacheDirty = false;
|
|
20690
21695
|
await Promise.all(
|
|
20691
|
-
targets.map(async ({ id, cfg, baseUrl, apiKey }) => {
|
|
20692
|
-
const cacheKey = `${id}\0${baseUrl}`;
|
|
21696
|
+
targets.map(async ({ id, cfg, baseUrl, apiKey, cacheKey }) => {
|
|
20693
21697
|
const provider = await discoverOpenAICompatibleModels(id, {
|
|
20694
21698
|
baseUrl,
|
|
20695
21699
|
apiKey,
|
|
@@ -21055,15 +22059,6 @@ async function createPreContextServices(input) {
|
|
|
21055
22059
|
logger.warn(`models.dev refresh failed (${toErrorMessage11(err)}); using cached catalog`);
|
|
21056
22060
|
}
|
|
21057
22061
|
}
|
|
21058
|
-
try {
|
|
21059
|
-
await installCatalogModelOutputLimits({
|
|
21060
|
-
registry: modelsRegistry,
|
|
21061
|
-
getConfig: () => config,
|
|
21062
|
-
log: (message) => logger.debug(message)
|
|
21063
|
-
});
|
|
21064
|
-
} catch (err) {
|
|
21065
|
-
logger.debug(`model output-limit index skipped: ${toErrorMessage11(err)}`);
|
|
21066
|
-
}
|
|
21067
22062
|
try {
|
|
21068
22063
|
await discoverAndMergeWebuiProviders({
|
|
21069
22064
|
config,
|
|
@@ -21074,6 +22069,15 @@ async function createPreContextServices(input) {
|
|
|
21074
22069
|
} catch (err) {
|
|
21075
22070
|
logger.debug(`provider auto-discovery skipped: ${toErrorMessage11(err)}`);
|
|
21076
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
|
+
}
|
|
21077
22081
|
const events = opts.services?.events ?? new EventBus();
|
|
21078
22082
|
events.setLogger(logger);
|
|
21079
22083
|
const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry, events });
|
|
@@ -21346,7 +22350,7 @@ import { makeProviderFromConfig as makeProviderFromConfig2, withCatalogCapabilit
|
|
|
21346
22350
|
|
|
21347
22351
|
// src/server/mode-handlers.ts
|
|
21348
22352
|
import { DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2 } from "@wrongstack/core/agent";
|
|
21349
|
-
import { resolveWstackPaths as
|
|
22353
|
+
import { resolveWstackPaths as resolveWstackPaths6 } from "@wrongstack/core/utils";
|
|
21350
22354
|
function createModeHandlers(context) {
|
|
21351
22355
|
return createModeRouteHandlers({
|
|
21352
22356
|
modeStore: context.modeStore,
|
|
@@ -21355,7 +22359,7 @@ function createModeHandlers(context) {
|
|
|
21355
22359
|
send,
|
|
21356
22360
|
afterSwitch: async (id) => {
|
|
21357
22361
|
const modePrompt = id === "default" ? "" : (await context.modeStore.getMode(id))?.prompt ?? "";
|
|
21358
|
-
const paths =
|
|
22362
|
+
const paths = resolveWstackPaths6({
|
|
21359
22363
|
projectRoot: context.projectRoot,
|
|
21360
22364
|
globalRoot: context.globalRoot
|
|
21361
22365
|
});
|
|
@@ -21460,7 +22464,16 @@ function buildRoutes(state, deps2, cb) {
|
|
|
21460
22464
|
refineModel: (ws, msg) => modelOperations.refineModel(
|
|
21461
22465
|
ws,
|
|
21462
22466
|
msg.payload
|
|
21463
|
-
)
|
|
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
|
+
}
|
|
21464
22477
|
};
|
|
21465
22478
|
const sessionRoutes = createSessionHandlers({
|
|
21466
22479
|
config: state.getConfig(),
|
|
@@ -21638,8 +22651,10 @@ function buildRoutes(state, deps2, cb) {
|
|
|
21638
22651
|
});
|
|
21639
22652
|
const mcpRoutes = {
|
|
21640
22653
|
list: (ws, msg) => handleMcpList(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
21641
|
-
add
|
|
21642
|
-
|
|
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),
|
|
21643
22658
|
remove: (ws, msg) => handleMcpRemove(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
21644
22659
|
enable: (ws, msg) => handleMcpEnable(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
21645
22660
|
disable: (ws, msg) => handleMcpDisable(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
@@ -21715,7 +22730,10 @@ async function resolvePorts(opts) {
|
|
|
21715
22730
|
const surface = opts.surface ?? "webui";
|
|
21716
22731
|
const surfaceDefaults = surface === "simpleui" ? { http: 3466 } : { http: 3456 };
|
|
21717
22732
|
const wsHost = opts.wsHost ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
|
|
21718
|
-
const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(
|
|
22733
|
+
const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(
|
|
22734
|
+
process.env["WEBUI_PORT"] ?? process.env["PORT"] ?? String(surfaceDefaults.http),
|
|
22735
|
+
10
|
|
22736
|
+
);
|
|
21719
22737
|
const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
|
|
21720
22738
|
const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
|
|
21721
22739
|
const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
|
|
@@ -21724,7 +22742,16 @@ async function resolvePorts(opts) {
|
|
|
21724
22742
|
if (!strictPort) {
|
|
21725
22743
|
httpPort = await findFreePort(wsHost, requestedHttpPort);
|
|
21726
22744
|
if (httpPort !== requestedHttpPort) {
|
|
21727
|
-
console.warn(
|
|
22745
|
+
console.warn(
|
|
22746
|
+
JSON.stringify({
|
|
22747
|
+
level: "warn",
|
|
22748
|
+
event: "webui.port_reassigned",
|
|
22749
|
+
protocol: "HTTP",
|
|
22750
|
+
requested: requestedHttpPort,
|
|
22751
|
+
assigned: httpPort,
|
|
22752
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
22753
|
+
})
|
|
22754
|
+
);
|
|
21728
22755
|
}
|
|
21729
22756
|
}
|
|
21730
22757
|
return { wsHost, httpPort, publicUrl, publicWsUrl, requireToken };
|
|
@@ -21799,7 +22826,10 @@ function createWsServers(httpServer, ports, accessToken) {
|
|
|
21799
22826
|
expectedToken: wsToken,
|
|
21800
22827
|
requireToken: ports.requireToken,
|
|
21801
22828
|
allowedHostnames: publicHostnames,
|
|
21802
|
-
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"
|
|
21803
22833
|
});
|
|
21804
22834
|
const WS_MAX_PAYLOAD = 20 * 1024 * 1024;
|
|
21805
22835
|
const wssPrimary = new WebSocketServer({
|
|
@@ -21830,21 +22860,49 @@ function armEvents(wssPrimary, wssSecondary, wsHost, httpPort, setupInput, watch
|
|
|
21830
22860
|
if (eventsArmed) return;
|
|
21831
22861
|
eventsArmed = true;
|
|
21832
22862
|
console.log(`[WebUI] Backend ready (${label})`);
|
|
21833
|
-
disposeEvents = setupEvents({
|
|
21834
|
-
|
|
21835
|
-
|
|
22863
|
+
disposeEvents = setupEvents({
|
|
22864
|
+
...setupInput,
|
|
22865
|
+
watcherMetrics,
|
|
22866
|
+
onFleetBroadcaster: (fn) => {
|
|
22867
|
+
fleetBroadcast = fn;
|
|
22868
|
+
}
|
|
22869
|
+
});
|
|
21836
22870
|
};
|
|
21837
22871
|
wssPrimary.on("listening", () => arm(`${wsHost}:${httpPort}`));
|
|
21838
22872
|
wssPrimary.on("error", (err) => {
|
|
21839
|
-
console.error(
|
|
22873
|
+
console.error(
|
|
22874
|
+
JSON.stringify({
|
|
22875
|
+
level: "error",
|
|
22876
|
+
event: "webui.ws_server_error",
|
|
22877
|
+
host: wsHost,
|
|
22878
|
+
message: toErrorMessage12(err),
|
|
22879
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
22880
|
+
})
|
|
22881
|
+
);
|
|
21840
22882
|
});
|
|
21841
22883
|
if (wssSecondary) {
|
|
21842
22884
|
wssSecondary.on("listening", () => arm(`::1:${httpPort}`));
|
|
21843
22885
|
wssSecondary.on("error", (err) => {
|
|
21844
22886
|
if (err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL") {
|
|
21845
|
-
console.warn(
|
|
22887
|
+
console.warn(
|
|
22888
|
+
JSON.stringify({
|
|
22889
|
+
level: "warn",
|
|
22890
|
+
event: "webui.ipv6_unavailable",
|
|
22891
|
+
code: err.code,
|
|
22892
|
+
message: err.message,
|
|
22893
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
22894
|
+
})
|
|
22895
|
+
);
|
|
21846
22896
|
} else {
|
|
21847
|
-
console.error(
|
|
22897
|
+
console.error(
|
|
22898
|
+
JSON.stringify({
|
|
22899
|
+
level: "error",
|
|
22900
|
+
event: "webui.ws_server_error",
|
|
22901
|
+
host: "::1",
|
|
22902
|
+
message: err.message,
|
|
22903
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
22904
|
+
})
|
|
22905
|
+
);
|
|
21848
22906
|
}
|
|
21849
22907
|
});
|
|
21850
22908
|
}
|
|
@@ -21865,6 +22923,7 @@ function resolveWebuiDistDir(fromUrl, explicitDistDir) {
|
|
|
21865
22923
|
}
|
|
21866
22924
|
}
|
|
21867
22925
|
function startHttpServer(opts) {
|
|
22926
|
+
const intakeService = opts.intakeService ?? createProjectIntakeService({ projectRoot: opts.projectRoot, globalRoot: opts.globalRoot });
|
|
21868
22927
|
const httpServer = createHttpServer({
|
|
21869
22928
|
host: opts.wsHost,
|
|
21870
22929
|
port: opts.httpPort,
|
|
@@ -21878,7 +22937,8 @@ function startHttpServer(opts) {
|
|
|
21878
22937
|
onTechStackEvent: opts.onTechStackEvent,
|
|
21879
22938
|
getLlm: opts.getLlm,
|
|
21880
22939
|
executePackageOperation: opts.executePackageOperation,
|
|
21881
|
-
projectRoot: opts.projectRoot
|
|
22940
|
+
projectRoot: opts.projectRoot,
|
|
22941
|
+
intakeService
|
|
21882
22942
|
});
|
|
21883
22943
|
return httpServer;
|
|
21884
22944
|
}
|