@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/index.js
CHANGED
|
@@ -73,6 +73,10 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
|
73
73
|
// Display-only toggles (purely visual, persisted in localStorage via Zustand).
|
|
74
74
|
"groupToolCalls",
|
|
75
75
|
"showThinkingLogs",
|
|
76
|
+
// v15: auto-collapse of the chat input under the history (opt-in display
|
|
77
|
+
// toggle, default off). Whitelisted so the key survives `prefs.update`
|
|
78
|
+
// round-trips without tripping the "unknown preference key" rejection.
|
|
79
|
+
"autoCollapseInput",
|
|
76
80
|
// v11 Display parity: inverse fsAccess flag.
|
|
77
81
|
"allowOutsideProjectRoot",
|
|
78
82
|
// v13 Display parity (TUI SettingsPicker fields 42 & 43): the read tool
|
|
@@ -397,6 +401,51 @@ function validateModelSwitchPayload(payload) {
|
|
|
397
401
|
}
|
|
398
402
|
};
|
|
399
403
|
}
|
|
404
|
+
function validateModelFallbackChoicePayload(payload) {
|
|
405
|
+
if (!isRecord2(payload)) {
|
|
406
|
+
return {
|
|
407
|
+
ok: false,
|
|
408
|
+
message: "model.fallback_choice payload must be an object"
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const requestId = payload["requestId"];
|
|
412
|
+
if (typeof requestId !== "string" || requestId.trim().length === 0) {
|
|
413
|
+
return {
|
|
414
|
+
ok: false,
|
|
415
|
+
message: "model.fallback_choice payload.requestId must be a non-empty string"
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const providerId = payload["providerId"];
|
|
419
|
+
const model = payload["model"];
|
|
420
|
+
const autoSwitch = payload["autoSwitch"];
|
|
421
|
+
if (providerId !== void 0 && typeof providerId !== "string") {
|
|
422
|
+
return {
|
|
423
|
+
ok: false,
|
|
424
|
+
message: "model.fallback_choice payload.providerId must be a string when provided"
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (model !== void 0 && typeof model !== "string") {
|
|
428
|
+
return {
|
|
429
|
+
ok: false,
|
|
430
|
+
message: "model.fallback_choice payload.model must be a string when provided"
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (autoSwitch !== void 0 && typeof autoSwitch !== "boolean") {
|
|
434
|
+
return {
|
|
435
|
+
ok: false,
|
|
436
|
+
message: "model.fallback_choice payload.autoSwitch must be a boolean when provided"
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
ok: true,
|
|
441
|
+
value: {
|
|
442
|
+
requestId: requestId.trim(),
|
|
443
|
+
...typeof providerId === "string" ? { providerId } : {},
|
|
444
|
+
...typeof model === "string" ? { model } : {},
|
|
445
|
+
...typeof autoSwitch === "boolean" ? { autoSwitch } : {}
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
400
449
|
var AUTONOMY_VALUES2 = /* @__PURE__ */ new Set(["off", "suggest", "auto", "eternal", "eternal-parallel"]);
|
|
401
450
|
function validateMailboxMessagesPayload(payload) {
|
|
402
451
|
if (payload === void 0) return { ok: true, value: void 0 };
|
|
@@ -4417,13 +4466,10 @@ function createConversationOperations(ctx) {
|
|
|
4417
4466
|
|
|
4418
4467
|
// src/server/context-editor.ts
|
|
4419
4468
|
import { createHash } from "node:crypto";
|
|
4420
|
-
import net from "node:net";
|
|
4421
4469
|
import {
|
|
4422
4470
|
ALLOWED_IMAGE_MEDIA_TYPES,
|
|
4423
4471
|
base64DecodedBytes,
|
|
4424
4472
|
isAllowedImageMediaType,
|
|
4425
|
-
isPrivateIPv4,
|
|
4426
|
-
isPrivateIPv6,
|
|
4427
4473
|
isValidImageBase64,
|
|
4428
4474
|
MAX_INCOMING_IMAGE_BYTES,
|
|
4429
4475
|
repairToolUseAdjacency
|
|
@@ -4491,40 +4537,7 @@ var REVISION_PREFIX = "wrongstack-context-editor-v1\0";
|
|
|
4491
4537
|
var MAX_MESSAGE_COUNT_GROWTH = 10;
|
|
4492
4538
|
var MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
|
|
4493
4539
|
var MAX_STRING_LENGTH = 8 * 1024 * 1024;
|
|
4494
|
-
var
|
|
4495
|
-
function imageUrlRejectionReason(url) {
|
|
4496
|
-
if (url.length > MAX_IMAGE_URL_LENGTH) {
|
|
4497
|
-
return `image.source.url exceeds ${MAX_IMAGE_URL_LENGTH} characters.`;
|
|
4498
|
-
}
|
|
4499
|
-
let parsed;
|
|
4500
|
-
try {
|
|
4501
|
-
parsed = new URL(url);
|
|
4502
|
-
} catch {
|
|
4503
|
-
return "image.source.url must be an absolute URL.";
|
|
4504
|
-
}
|
|
4505
|
-
if (parsed.protocol !== "https:") {
|
|
4506
|
-
return `image.source.url must use https (got "${parsed.protocol}").`;
|
|
4507
|
-
}
|
|
4508
|
-
if (parsed.username !== "" || parsed.password !== "") {
|
|
4509
|
-
return "image.source.url must not embed credentials.";
|
|
4510
|
-
}
|
|
4511
|
-
const host = parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]") ? parsed.hostname.slice(1, -1) : parsed.hostname;
|
|
4512
|
-
const bareHost = host.endsWith(".") ? host.slice(0, -1) : host;
|
|
4513
|
-
if (bareHost === "") {
|
|
4514
|
-
return "image.source.url must include a hostname.";
|
|
4515
|
-
}
|
|
4516
|
-
if (bareHost === "localhost" || bareHost.endsWith(".localhost")) {
|
|
4517
|
-
return "image.source.url must not target localhost.";
|
|
4518
|
-
}
|
|
4519
|
-
const family = net.isIP(bareHost);
|
|
4520
|
-
if (family === 4 && isPrivateIPv4(bareHost)) {
|
|
4521
|
-
return `image.source.url must not target a private or loopback address ("${bareHost}").`;
|
|
4522
|
-
}
|
|
4523
|
-
if (family === 6 && isPrivateIPv6(bareHost)) {
|
|
4524
|
-
return `image.source.url must not target a private or loopback address ("${bareHost}").`;
|
|
4525
|
-
}
|
|
4526
|
-
return void 0;
|
|
4527
|
-
}
|
|
4540
|
+
var MAX_REMOVAL_COUNT = 4096;
|
|
4528
4541
|
function isRecord3(value) {
|
|
4529
4542
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4530
4543
|
}
|
|
@@ -4533,7 +4546,7 @@ function canonicalize(value) {
|
|
|
4533
4546
|
if (isRecord3(value)) {
|
|
4534
4547
|
const sorted = {};
|
|
4535
4548
|
for (const key of Object.keys(value).sort()) {
|
|
4536
|
-
if (key === "_estTokens") continue;
|
|
4549
|
+
if (key === "_estTokens" || key === "_toolErrorInfo") continue;
|
|
4537
4550
|
const item = value[key];
|
|
4538
4551
|
if (item === void 0) continue;
|
|
4539
4552
|
sorted[key] = canonicalize(item);
|
|
@@ -4564,6 +4577,12 @@ function isMessageRole(value) {
|
|
|
4564
4577
|
function isPlainJsonObject(value) {
|
|
4565
4578
|
return isRecord3(value);
|
|
4566
4579
|
}
|
|
4580
|
+
function splitsSurrogatePair(text2, offset) {
|
|
4581
|
+
if (offset <= 0 || offset >= text2.length) return false;
|
|
4582
|
+
const previous = text2.charCodeAt(offset - 1);
|
|
4583
|
+
const next = text2.charCodeAt(offset);
|
|
4584
|
+
return previous >= 55296 && previous <= 56319 && next >= 56320 && next <= 57343;
|
|
4585
|
+
}
|
|
4567
4586
|
function validateCacheControl(value, path35, errors) {
|
|
4568
4587
|
if (value === void 0) return void 0;
|
|
4569
4588
|
if (!isRecord3(value) || value["type"] !== "ephemeral") {
|
|
@@ -4774,19 +4793,13 @@ function validateBlock(value, path35, errors) {
|
|
|
4774
4793
|
);
|
|
4775
4794
|
return void 0;
|
|
4776
4795
|
}
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
source: {
|
|
4785
|
-
type: "url",
|
|
4786
|
-
...typeof mediaType === "string" ? { media_type: mediaType } : {},
|
|
4787
|
-
url
|
|
4788
|
-
}
|
|
4789
|
-
};
|
|
4796
|
+
error(
|
|
4797
|
+
errors,
|
|
4798
|
+
`${path35}/source/url`,
|
|
4799
|
+
"UNSAFE_IMAGE_URL",
|
|
4800
|
+
"URL image sources are not allowed in context editor proposals; use an ingested base64 image."
|
|
4801
|
+
);
|
|
4802
|
+
return void 0;
|
|
4790
4803
|
}
|
|
4791
4804
|
case "thinking": {
|
|
4792
4805
|
const thinking = value["thinking"];
|
|
@@ -4943,6 +4956,14 @@ function warningsForMessage(message, index) {
|
|
|
4943
4956
|
message: "This block contains provider replay metadata and should only be removed with the whole turn if no longer needed."
|
|
4944
4957
|
});
|
|
4945
4958
|
}
|
|
4959
|
+
if (block.type === "image" && block.source.type === "url") {
|
|
4960
|
+
warnings.push({
|
|
4961
|
+
path: `/messages/${index}/content/${blockIndex}/source/url`,
|
|
4962
|
+
code: "UNSAFE_IMAGE_URL",
|
|
4963
|
+
severity: "danger",
|
|
4964
|
+
message: "URL image sources cannot be retained in context editor proposals; remove the whole message before applying other edits."
|
|
4965
|
+
});
|
|
4966
|
+
}
|
|
4946
4967
|
if (block.type === "tool_result" && block.content.length > 2e4) {
|
|
4947
4968
|
warnings.push({
|
|
4948
4969
|
path: `/messages/${index}/content/${blockIndex}`,
|
|
@@ -4970,6 +4991,21 @@ function metricFor(ctx, messages, tools) {
|
|
|
4970
4991
|
fullRequestTokens: breakdown.total
|
|
4971
4992
|
};
|
|
4972
4993
|
}
|
|
4994
|
+
function isToolResultMessage(message) {
|
|
4995
|
+
return Boolean(
|
|
4996
|
+
message?.role === "user" && Array.isArray(message.content) && message.content.length > 0 && message.content.every((block) => block.type === "tool_result")
|
|
4997
|
+
);
|
|
4998
|
+
}
|
|
4999
|
+
function pairedAssistantIndices(messages, userIndex) {
|
|
5000
|
+
if (messages[userIndex]?.role !== "user" || isToolResultMessage(messages[userIndex])) return [];
|
|
5001
|
+
const paired = [];
|
|
5002
|
+
for (let index = userIndex + 1; index < messages.length; index += 1) {
|
|
5003
|
+
const message = messages[index];
|
|
5004
|
+
if (message?.role === "user" && !isToolResultMessage(message)) break;
|
|
5005
|
+
if (message?.role === "assistant") paired.push(index);
|
|
5006
|
+
}
|
|
5007
|
+
return paired;
|
|
5008
|
+
}
|
|
4973
5009
|
function buildContextEditorSnapshot(ctx, tools) {
|
|
4974
5010
|
const messages = ctx.messages.map(
|
|
4975
5011
|
(message) => ({
|
|
@@ -4999,11 +5035,165 @@ function buildContextEditorSnapshot(ctx, tools) {
|
|
|
4999
5035
|
tokens: messageTokens(message.content),
|
|
5000
5036
|
preview: breakdown.messages.breakdown[index]?.preview ?? "",
|
|
5001
5037
|
blockCount: Array.isArray(message.content) ? message.content.length : null,
|
|
5002
|
-
warnings: warningsForMessage(message, index)
|
|
5038
|
+
warnings: warningsForMessage(message, index),
|
|
5039
|
+
pairedAssistantIndices: pairedAssistantIndices(ctx.messages, index)
|
|
5003
5040
|
})),
|
|
5004
5041
|
diagnostics: toolDiagnostics(ctx.messages)
|
|
5005
5042
|
};
|
|
5006
5043
|
}
|
|
5044
|
+
function validateRemovalPlan(value, originalMessages, proposedMessages) {
|
|
5045
|
+
const errors = [];
|
|
5046
|
+
if (value === void 0) {
|
|
5047
|
+
error(
|
|
5048
|
+
errors,
|
|
5049
|
+
"/removals",
|
|
5050
|
+
"REMOVAL_PLAN_REQUIRED",
|
|
5051
|
+
"A removal plan is required for every context editor proposal."
|
|
5052
|
+
);
|
|
5053
|
+
return { errors };
|
|
5054
|
+
}
|
|
5055
|
+
if (!Array.isArray(value)) {
|
|
5056
|
+
error(errors, "/removals", "INVALID_REMOVALS", "removals must be an array.");
|
|
5057
|
+
return { errors };
|
|
5058
|
+
}
|
|
5059
|
+
if (value.length > MAX_REMOVAL_COUNT) {
|
|
5060
|
+
error(
|
|
5061
|
+
errors,
|
|
5062
|
+
"/removals",
|
|
5063
|
+
"TOO_MANY_REMOVALS",
|
|
5064
|
+
`removals must contain at most ${MAX_REMOVAL_COUNT} entries.`
|
|
5065
|
+
);
|
|
5066
|
+
return { errors };
|
|
5067
|
+
}
|
|
5068
|
+
const wholeMessages = /* @__PURE__ */ new Set();
|
|
5069
|
+
const touchedUsers = /* @__PURE__ */ new Set();
|
|
5070
|
+
const ranges = [];
|
|
5071
|
+
for (const [removalIndex, raw] of value.entries()) {
|
|
5072
|
+
const path35 = `/removals/${removalIndex}`;
|
|
5073
|
+
if (!isRecord3(raw) || !Number.isInteger(raw["messageIndex"])) {
|
|
5074
|
+
error(errors, path35, "INVALID_REMOVAL", "Removal must include an integer messageIndex.");
|
|
5075
|
+
continue;
|
|
5076
|
+
}
|
|
5077
|
+
const messageIndex = raw["messageIndex"];
|
|
5078
|
+
const original = originalMessages[messageIndex];
|
|
5079
|
+
if (!original) {
|
|
5080
|
+
error(errors, `${path35}/messageIndex`, "INVALID_MESSAGE_INDEX", "Removal messageIndex is out of range.");
|
|
5081
|
+
continue;
|
|
5082
|
+
}
|
|
5083
|
+
const start = raw["start"];
|
|
5084
|
+
const end = raw["end"];
|
|
5085
|
+
const blockIndex = raw["blockIndex"];
|
|
5086
|
+
if (start === void 0 && end === void 0 && blockIndex === void 0) {
|
|
5087
|
+
wholeMessages.add(messageIndex);
|
|
5088
|
+
if (original.role === "user") touchedUsers.add(messageIndex);
|
|
5089
|
+
continue;
|
|
5090
|
+
}
|
|
5091
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start) {
|
|
5092
|
+
error(errors, path35, "INVALID_RANGE", "Range removal requires integer start/end with 0 <= start < end.");
|
|
5093
|
+
continue;
|
|
5094
|
+
}
|
|
5095
|
+
let text2;
|
|
5096
|
+
if (blockIndex === void 0 && typeof original.content === "string") text2 = original.content;
|
|
5097
|
+
if (Number.isInteger(blockIndex) && Array.isArray(original.content)) {
|
|
5098
|
+
const block = original.content[blockIndex];
|
|
5099
|
+
if (block?.type === "text") text2 = block.text;
|
|
5100
|
+
}
|
|
5101
|
+
if (text2 === void 0 || end > text2.length) {
|
|
5102
|
+
error(errors, path35, "INVALID_RANGE_TARGET", "Range must target existing string or text-block content.");
|
|
5103
|
+
continue;
|
|
5104
|
+
}
|
|
5105
|
+
if (splitsSurrogatePair(text2, start) || splitsSurrogatePair(text2, end)) {
|
|
5106
|
+
error(
|
|
5107
|
+
errors,
|
|
5108
|
+
path35,
|
|
5109
|
+
"INVALID_UNICODE_RANGE",
|
|
5110
|
+
"Range boundaries must not split a Unicode surrogate pair."
|
|
5111
|
+
);
|
|
5112
|
+
continue;
|
|
5113
|
+
}
|
|
5114
|
+
ranges.push({
|
|
5115
|
+
messageIndex,
|
|
5116
|
+
...blockIndex === void 0 ? {} : { blockIndex },
|
|
5117
|
+
start,
|
|
5118
|
+
end
|
|
5119
|
+
});
|
|
5120
|
+
if (original.role === "user") touchedUsers.add(messageIndex);
|
|
5121
|
+
}
|
|
5122
|
+
const rangesByTarget = /* @__PURE__ */ new Map();
|
|
5123
|
+
for (const range of ranges) {
|
|
5124
|
+
const key = `${range.messageIndex}:${range.blockIndex ?? "string"}`;
|
|
5125
|
+
const targetRanges = rangesByTarget.get(key) ?? [];
|
|
5126
|
+
targetRanges.push(range);
|
|
5127
|
+
rangesByTarget.set(key, targetRanges);
|
|
5128
|
+
}
|
|
5129
|
+
for (const targetRanges of rangesByTarget.values()) {
|
|
5130
|
+
targetRanges.sort((left, right) => (left.start ?? 0) - (right.start ?? 0));
|
|
5131
|
+
for (let index = 1; index < targetRanges.length; index += 1) {
|
|
5132
|
+
const previous = targetRanges[index - 1];
|
|
5133
|
+
const current2 = targetRanges[index];
|
|
5134
|
+
if (previous?.end !== void 0 && current2?.start !== void 0 && current2.start < previous.end) {
|
|
5135
|
+
error(
|
|
5136
|
+
errors,
|
|
5137
|
+
"/removals",
|
|
5138
|
+
"OVERLAPPING_RANGES",
|
|
5139
|
+
"Removal ranges targeting the same text must not overlap."
|
|
5140
|
+
);
|
|
5141
|
+
break;
|
|
5142
|
+
}
|
|
5143
|
+
}
|
|
5144
|
+
}
|
|
5145
|
+
for (const userIndex of touchedUsers) {
|
|
5146
|
+
for (const assistantIndex of pairedAssistantIndices(originalMessages, userIndex)) {
|
|
5147
|
+
if (wholeMessages.has(assistantIndex)) continue;
|
|
5148
|
+
error(
|
|
5149
|
+
errors,
|
|
5150
|
+
"/removals",
|
|
5151
|
+
"MISSING_ASSISTANT_PAIR",
|
|
5152
|
+
`Editing user message ${userIndex} must also remove assistant message ${assistantIndex}.`
|
|
5153
|
+
);
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
const expectedMessages = structuredClone(originalMessages);
|
|
5157
|
+
for (const targetRanges of rangesByTarget.values()) {
|
|
5158
|
+
const first = targetRanges[0];
|
|
5159
|
+
if (!first) continue;
|
|
5160
|
+
const message = expectedMessages[first.messageIndex];
|
|
5161
|
+
if (!message) continue;
|
|
5162
|
+
let text2;
|
|
5163
|
+
if (first.blockIndex === void 0 && typeof message.content === "string") {
|
|
5164
|
+
text2 = message.content;
|
|
5165
|
+
} else if (first.blockIndex !== void 0 && Array.isArray(message.content)) {
|
|
5166
|
+
const block = message.content[first.blockIndex];
|
|
5167
|
+
if (block?.type === "text") text2 = block.text;
|
|
5168
|
+
}
|
|
5169
|
+
if (text2 === void 0) continue;
|
|
5170
|
+
const pieces = [];
|
|
5171
|
+
let cursor = 0;
|
|
5172
|
+
for (const range of targetRanges) {
|
|
5173
|
+
if (range.start === void 0 || range.end === void 0) continue;
|
|
5174
|
+
pieces.push(text2.slice(cursor, range.start));
|
|
5175
|
+
cursor = range.end;
|
|
5176
|
+
}
|
|
5177
|
+
pieces.push(text2.slice(cursor));
|
|
5178
|
+
const nextText = pieces.join("");
|
|
5179
|
+
if (first.blockIndex === void 0 && typeof message.content === "string") {
|
|
5180
|
+
message.content = nextText;
|
|
5181
|
+
} else if (first.blockIndex !== void 0 && Array.isArray(message.content)) {
|
|
5182
|
+
const block = message.content[first.blockIndex];
|
|
5183
|
+
if (block?.type === "text") block.text = nextText;
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
const expectedProposal = expectedMessages.filter((_, index) => !wholeMessages.has(index));
|
|
5187
|
+
if (JSON.stringify(canonicalize(expectedProposal)) !== JSON.stringify(canonicalize(proposedMessages))) {
|
|
5188
|
+
error(
|
|
5189
|
+
errors,
|
|
5190
|
+
"/messages",
|
|
5191
|
+
"REMOVAL_PLAN_MISMATCH",
|
|
5192
|
+
"Submitted messages do not exactly match the declared removal plan."
|
|
5193
|
+
);
|
|
5194
|
+
}
|
|
5195
|
+
return errors.length > 0 ? { errors } : { errors, messages: expectedProposal };
|
|
5196
|
+
}
|
|
5007
5197
|
function validateContextEditorProposal(input) {
|
|
5008
5198
|
const currentRevision = contextEditorRevision(input.ctx.messages);
|
|
5009
5199
|
const before = metricFor(input.ctx, input.ctx.messages, input.tools);
|
|
@@ -5055,7 +5245,23 @@ function validateContextEditorProposal(input) {
|
|
|
5055
5245
|
repair: emptyRepair
|
|
5056
5246
|
};
|
|
5057
5247
|
}
|
|
5058
|
-
const
|
|
5248
|
+
const removalPlan = validateRemovalPlan(
|
|
5249
|
+
input.removals,
|
|
5250
|
+
input.ctx.messages,
|
|
5251
|
+
parsed.messages
|
|
5252
|
+
);
|
|
5253
|
+
if (removalPlan.errors.length > 0 || !removalPlan.messages) {
|
|
5254
|
+
return {
|
|
5255
|
+
ok: false,
|
|
5256
|
+
baseRevision: input.baseRevision,
|
|
5257
|
+
currentRevision,
|
|
5258
|
+
before,
|
|
5259
|
+
validationErrors: removalPlan.errors,
|
|
5260
|
+
warnings: [],
|
|
5261
|
+
repair: emptyRepair
|
|
5262
|
+
};
|
|
5263
|
+
}
|
|
5264
|
+
const repaired = repairToolUseAdjacency(removalPlan.messages);
|
|
5059
5265
|
const repair = {
|
|
5060
5266
|
changed: repaired.report.changed,
|
|
5061
5267
|
removedToolUses: repaired.report.removedToolUses,
|
|
@@ -5305,7 +5511,7 @@ import {
|
|
|
5305
5511
|
getKanbanServerConnection,
|
|
5306
5512
|
isKanbanServerAvailable
|
|
5307
5513
|
} from "@wrongstack/kanban";
|
|
5308
|
-
import * as
|
|
5514
|
+
import * as net from "node:net";
|
|
5309
5515
|
import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
|
|
5310
5516
|
import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
|
|
5311
5517
|
import {
|
|
@@ -6149,7 +6355,7 @@ var RESTART_POLL_INTERVAL_MS = 250;
|
|
|
6149
6355
|
var RESTART_DEADLINE_MS = 3e3;
|
|
6150
6356
|
function isEndpointAlive(endpoint) {
|
|
6151
6357
|
return new Promise((resolve16) => {
|
|
6152
|
-
const sock =
|
|
6358
|
+
const sock = net.createConnection(endpoint);
|
|
6153
6359
|
const timer = setTimeout(() => {
|
|
6154
6360
|
sock.destroy();
|
|
6155
6361
|
resolve16(false);
|
|
@@ -8094,7 +8300,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
8094
8300
|
}
|
|
8095
8301
|
try {
|
|
8096
8302
|
const { SessionRegistry, DefaultSessionStore: DefaultSessionStore4, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core/storage");
|
|
8097
|
-
const { resolveWstackPaths:
|
|
8303
|
+
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8098
8304
|
const registry = new SessionRegistry(globalRoot);
|
|
8099
8305
|
const entry = await registry.get(sessionId);
|
|
8100
8306
|
if (!entry) {
|
|
@@ -8102,7 +8308,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
8102
8308
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
8103
8309
|
return;
|
|
8104
8310
|
}
|
|
8105
|
-
const paths =
|
|
8311
|
+
const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
|
|
8106
8312
|
const store = new DefaultSessionStore4({ dir: paths.projectSessions });
|
|
8107
8313
|
const reader = new DefaultSessionReader2({ store });
|
|
8108
8314
|
const rawEntries = [];
|
|
@@ -8183,7 +8389,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
|
8183
8389
|
try {
|
|
8184
8390
|
const { SessionRegistry } = await import("@wrongstack/core/storage");
|
|
8185
8391
|
const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8186
|
-
const { resolveWstackPaths:
|
|
8392
|
+
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8187
8393
|
const registry = new SessionRegistry(globalRoot);
|
|
8188
8394
|
const entry = await registry.get(sessionId);
|
|
8189
8395
|
if (!entry) {
|
|
@@ -8191,7 +8397,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
|
8191
8397
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
8192
8398
|
return;
|
|
8193
8399
|
}
|
|
8194
|
-
const paths =
|
|
8400
|
+
const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
|
|
8195
8401
|
const mailbox = getSharedProjectMailbox6(paths.projectDir);
|
|
8196
8402
|
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
8197
8403
|
const sent = await mailbox.send({ from, to, type, subject, body: text2, priority });
|
|
@@ -8211,7 +8417,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
|
8211
8417
|
try {
|
|
8212
8418
|
const { SessionRegistry } = await import("@wrongstack/core/storage");
|
|
8213
8419
|
const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8214
|
-
const { resolveWstackPaths:
|
|
8420
|
+
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8215
8421
|
const registry = new SessionRegistry(globalRoot);
|
|
8216
8422
|
const entry = await registry.get(sessionId);
|
|
8217
8423
|
if (!entry) {
|
|
@@ -8219,7 +8425,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
|
8219
8425
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
8220
8426
|
return;
|
|
8221
8427
|
}
|
|
8222
|
-
const paths =
|
|
8428
|
+
const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
|
|
8223
8429
|
const mailbox = getSharedProjectMailbox6(paths.projectDir);
|
|
8224
8430
|
const leaderAddr = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
8225
8431
|
const [inbound, outbound] = await Promise.all([
|
|
@@ -8271,7 +8477,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
|
8271
8477
|
try {
|
|
8272
8478
|
const { SessionRegistry } = await import("@wrongstack/core/storage");
|
|
8273
8479
|
const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8274
|
-
const { resolveWstackPaths:
|
|
8480
|
+
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8275
8481
|
const registry = new SessionRegistry(globalRoot);
|
|
8276
8482
|
const entry = await registry.get(sessionId);
|
|
8277
8483
|
if (!entry) {
|
|
@@ -8279,7 +8485,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
|
8279
8485
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
8280
8486
|
return;
|
|
8281
8487
|
}
|
|
8282
|
-
const paths =
|
|
8488
|
+
const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
|
|
8283
8489
|
const mailbox = getSharedProjectMailbox6(paths.projectDir);
|
|
8284
8490
|
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
8285
8491
|
const sent = await mailbox.sendRuntimeControl({
|
|
@@ -8320,7 +8526,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
|
8320
8526
|
try {
|
|
8321
8527
|
const { SessionRegistry } = await import("@wrongstack/core/storage");
|
|
8322
8528
|
const { getSharedProjectMailbox: getSharedProjectMailbox6, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
|
|
8323
|
-
const { resolveWstackPaths:
|
|
8529
|
+
const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
|
|
8324
8530
|
const registry = new SessionRegistry(globalRoot);
|
|
8325
8531
|
const all = await registry.list();
|
|
8326
8532
|
const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
|
|
@@ -8332,7 +8538,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
|
8332
8538
|
}
|
|
8333
8539
|
const mbByDir = /* @__PURE__ */ new Map();
|
|
8334
8540
|
const mailboxFor = (projectRoot) => {
|
|
8335
|
-
const dir =
|
|
8541
|
+
const dir = resolveWstackPaths7({ projectRoot, globalRoot }).projectDir;
|
|
8336
8542
|
let mb = mbByDir.get(dir);
|
|
8337
8543
|
if (!mb) {
|
|
8338
8544
|
mb = getSharedProjectMailbox6(dir);
|
|
@@ -8366,6 +8572,246 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
|
8366
8572
|
}
|
|
8367
8573
|
}
|
|
8368
8574
|
|
|
8575
|
+
// src/server/requirement-intake-handlers.ts
|
|
8576
|
+
import { readProjectIdentity } from "@wrongstack/core/utils";
|
|
8577
|
+
import {
|
|
8578
|
+
IntakeError,
|
|
8579
|
+
IntakeValidationError
|
|
8580
|
+
} from "@wrongstack/requirement-intake";
|
|
8581
|
+
var SERVER_ACTOR = { id: "webui-server", type: "agent" };
|
|
8582
|
+
var MAX_INTAKE_BODY_BYTES = 512e3;
|
|
8583
|
+
function intakeContext(projectId) {
|
|
8584
|
+
return { ...SERVER_ACTOR, projectId };
|
|
8585
|
+
}
|
|
8586
|
+
function sendJson2(res, status, body) {
|
|
8587
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8588
|
+
res.end(JSON.stringify(body));
|
|
8589
|
+
}
|
|
8590
|
+
function sendNotFound(res, intakeId) {
|
|
8591
|
+
sendJson2(res, 404, {
|
|
8592
|
+
error: {
|
|
8593
|
+
code: "INTAKE_NOT_FOUND",
|
|
8594
|
+
message: `Requirement intake record not found: ${intakeId}`
|
|
8595
|
+
}
|
|
8596
|
+
});
|
|
8597
|
+
}
|
|
8598
|
+
function sendIntakeError(res, error2) {
|
|
8599
|
+
if (error2 instanceof IntakeValidationError) {
|
|
8600
|
+
sendJson2(res, 400, {
|
|
8601
|
+
error: { code: error2.code, message: error2.message, issues: error2.issues }
|
|
8602
|
+
});
|
|
8603
|
+
return;
|
|
8604
|
+
}
|
|
8605
|
+
if (error2 instanceof IntakeError) {
|
|
8606
|
+
const status = error2.code === "INTAKE_UNAUTHORIZED" ? 403 : error2.code === "INTAKE_NOT_FOUND" ? 404 : error2.code === "INTAKE_SUGGESTION_ERROR" ? 502 : 409;
|
|
8607
|
+
sendJson2(res, status, { error: { code: error2.code, message: error2.message } });
|
|
8608
|
+
return;
|
|
8609
|
+
}
|
|
8610
|
+
sendJson2(res, 500, { error: { code: "INTERNAL_ERROR", message: "Internal server error" } });
|
|
8611
|
+
}
|
|
8612
|
+
function serviceOr503(res, service) {
|
|
8613
|
+
if (service) return true;
|
|
8614
|
+
sendJson2(res, 503, {
|
|
8615
|
+
error: { code: "INTAKE_UNAVAILABLE", message: "Requirement intake service not configured" }
|
|
8616
|
+
});
|
|
8617
|
+
return false;
|
|
8618
|
+
}
|
|
8619
|
+
async function readJsonBody3(res, req) {
|
|
8620
|
+
const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
|
|
8621
|
+
if (contentType !== "application/json") {
|
|
8622
|
+
sendJson2(res, 400, {
|
|
8623
|
+
error: {
|
|
8624
|
+
code: "INVALID_CONTENT_TYPE",
|
|
8625
|
+
message: `Unsupported Content-Type: ${contentType || "(absent)"}`
|
|
8626
|
+
}
|
|
8627
|
+
});
|
|
8628
|
+
return null;
|
|
8629
|
+
}
|
|
8630
|
+
return new Promise((resolve16) => {
|
|
8631
|
+
let data = "";
|
|
8632
|
+
let failed = false;
|
|
8633
|
+
const fail2 = (message) => {
|
|
8634
|
+
if (failed) return;
|
|
8635
|
+
failed = true;
|
|
8636
|
+
sendJson2(res, 400, { error: { code: "INVALID_BODY", message } });
|
|
8637
|
+
resolve16(null);
|
|
8638
|
+
};
|
|
8639
|
+
req.on("data", (chunk) => {
|
|
8640
|
+
if (failed) return;
|
|
8641
|
+
data += chunk.toString("utf8");
|
|
8642
|
+
if (data.length > MAX_INTAKE_BODY_BYTES) {
|
|
8643
|
+
req.destroy();
|
|
8644
|
+
fail2("Request body too large");
|
|
8645
|
+
}
|
|
8646
|
+
});
|
|
8647
|
+
req.on("end", () => {
|
|
8648
|
+
if (failed) return;
|
|
8649
|
+
try {
|
|
8650
|
+
resolve16(data.trim().length === 0 ? {} : JSON.parse(data));
|
|
8651
|
+
} catch {
|
|
8652
|
+
fail2("Request body is not valid JSON");
|
|
8653
|
+
}
|
|
8654
|
+
});
|
|
8655
|
+
req.on("error", () => fail2("Failed to read request body"));
|
|
8656
|
+
});
|
|
8657
|
+
}
|
|
8658
|
+
async function discoverIntake(res, service, intakeId) {
|
|
8659
|
+
const record2 = await service.getIntake(intakeId, intakeContext(""));
|
|
8660
|
+
if (!record2) {
|
|
8661
|
+
sendNotFound(res, intakeId);
|
|
8662
|
+
return null;
|
|
8663
|
+
}
|
|
8664
|
+
return record2;
|
|
8665
|
+
}
|
|
8666
|
+
async function handleRequirementIntakeListForServer(res, service, projectRoot) {
|
|
8667
|
+
if (!serviceOr503(res, service)) return;
|
|
8668
|
+
if (!projectRoot) {
|
|
8669
|
+
sendJson2(res, 503, {
|
|
8670
|
+
error: { code: "PROJECT_ROOT_NOT_CONFIGURED", message: "Project root not configured" }
|
|
8671
|
+
});
|
|
8672
|
+
return;
|
|
8673
|
+
}
|
|
8674
|
+
try {
|
|
8675
|
+
const identity = await readProjectIdentity(projectRoot);
|
|
8676
|
+
if (!identity) {
|
|
8677
|
+
sendJson2(res, 404, {
|
|
8678
|
+
error: {
|
|
8679
|
+
code: "PROJECT_IDENTITY_NOT_FOUND",
|
|
8680
|
+
message: "No project identity found \u2014 run `wstack init` first"
|
|
8681
|
+
}
|
|
8682
|
+
});
|
|
8683
|
+
return;
|
|
8684
|
+
}
|
|
8685
|
+
const projectId = identity.projectId;
|
|
8686
|
+
const records = await service.listIntakes(projectId, intakeContext(projectId));
|
|
8687
|
+
sendJson2(res, 200, { projectId, intakes: records });
|
|
8688
|
+
} catch (error2) {
|
|
8689
|
+
sendIntakeError(res, error2);
|
|
8690
|
+
}
|
|
8691
|
+
}
|
|
8692
|
+
async function handleRequirementIntakeCreate(res, req, service, projectId) {
|
|
8693
|
+
if (!serviceOr503(res, service)) return;
|
|
8694
|
+
const body = await readJsonBody3(res, req);
|
|
8695
|
+
if (body === null) return;
|
|
8696
|
+
try {
|
|
8697
|
+
const result = await service.createIntake(body, intakeContext(projectId));
|
|
8698
|
+
sendJson2(res, result.idempotent ? 200 : 201, result);
|
|
8699
|
+
} catch (error2) {
|
|
8700
|
+
sendIntakeError(res, error2);
|
|
8701
|
+
}
|
|
8702
|
+
}
|
|
8703
|
+
async function handleRequirementIntakeList(res, service, projectId) {
|
|
8704
|
+
if (!serviceOr503(res, service)) return;
|
|
8705
|
+
try {
|
|
8706
|
+
const records = await service.listIntakes(projectId, intakeContext(projectId));
|
|
8707
|
+
sendJson2(res, 200, records);
|
|
8708
|
+
} catch (error2) {
|
|
8709
|
+
sendIntakeError(res, error2);
|
|
8710
|
+
}
|
|
8711
|
+
}
|
|
8712
|
+
async function handleRequirementIntakeGet(res, service, intakeId) {
|
|
8713
|
+
if (!serviceOr503(res, service)) return;
|
|
8714
|
+
try {
|
|
8715
|
+
const record2 = await discoverIntake(res, service, intakeId);
|
|
8716
|
+
if (!record2) return;
|
|
8717
|
+
sendJson2(res, 200, record2);
|
|
8718
|
+
} catch (error2) {
|
|
8719
|
+
sendIntakeError(res, error2);
|
|
8720
|
+
}
|
|
8721
|
+
}
|
|
8722
|
+
async function handleRequirementIntakeUpdate(res, req, service, intakeId) {
|
|
8723
|
+
if (!serviceOr503(res, service)) return;
|
|
8724
|
+
const body = await readJsonBody3(res, req);
|
|
8725
|
+
if (body === null) return;
|
|
8726
|
+
try {
|
|
8727
|
+
const record2 = await discoverIntake(res, service, intakeId);
|
|
8728
|
+
if (!record2) return;
|
|
8729
|
+
const expectedVersion = typeof body["expectedVersion"] === "number" ? body["expectedVersion"] : void 0;
|
|
8730
|
+
const { expectedVersion: _ignored, ...patch } = body;
|
|
8731
|
+
void _ignored;
|
|
8732
|
+
const updated = await service.updateIntake(
|
|
8733
|
+
intakeId,
|
|
8734
|
+
patch,
|
|
8735
|
+
intakeContext(record2.projectId),
|
|
8736
|
+
expectedVersion
|
|
8737
|
+
);
|
|
8738
|
+
sendJson2(res, 200, updated);
|
|
8739
|
+
} catch (error2) {
|
|
8740
|
+
sendIntakeError(res, error2);
|
|
8741
|
+
}
|
|
8742
|
+
}
|
|
8743
|
+
async function handleRequirementIntakeAnswers(res, req, service, intakeId) {
|
|
8744
|
+
if (!serviceOr503(res, service)) return;
|
|
8745
|
+
const body = await readJsonBody3(res, req);
|
|
8746
|
+
if (body === null) return;
|
|
8747
|
+
try {
|
|
8748
|
+
const record2 = await discoverIntake(res, service, intakeId);
|
|
8749
|
+
if (!record2) return;
|
|
8750
|
+
const updated = await service.addAnswer(
|
|
8751
|
+
intakeId,
|
|
8752
|
+
body,
|
|
8753
|
+
intakeContext(record2.projectId)
|
|
8754
|
+
);
|
|
8755
|
+
sendJson2(res, 200, updated);
|
|
8756
|
+
} catch (error2) {
|
|
8757
|
+
sendIntakeError(res, error2);
|
|
8758
|
+
}
|
|
8759
|
+
}
|
|
8760
|
+
async function handleRequirementIntakeSuggestions(res, req, service, intakeId) {
|
|
8761
|
+
if (!serviceOr503(res, service)) return;
|
|
8762
|
+
const body = await readJsonBody3(res, req);
|
|
8763
|
+
if (body === null) return;
|
|
8764
|
+
try {
|
|
8765
|
+
const record2 = await discoverIntake(res, service, intakeId);
|
|
8766
|
+
if (!record2) return;
|
|
8767
|
+
const focus = Array.isArray(body["focus"]) ? body["focus"] : void 0;
|
|
8768
|
+
const suggestions = await service.generateSuggestions(
|
|
8769
|
+
intakeId,
|
|
8770
|
+
intakeContext(record2.projectId),
|
|
8771
|
+
focus
|
|
8772
|
+
);
|
|
8773
|
+
sendJson2(res, 200, { suggestions });
|
|
8774
|
+
} catch (error2) {
|
|
8775
|
+
sendIntakeError(res, error2);
|
|
8776
|
+
}
|
|
8777
|
+
}
|
|
8778
|
+
async function handleRequirementIntakeSubmit(res, service, intakeId) {
|
|
8779
|
+
if (!serviceOr503(res, service)) return;
|
|
8780
|
+
try {
|
|
8781
|
+
const record2 = await discoverIntake(res, service, intakeId);
|
|
8782
|
+
if (!record2) return;
|
|
8783
|
+
const result = await service.submitIntake(intakeId, intakeContext(record2.projectId));
|
|
8784
|
+
sendJson2(res, 200, result);
|
|
8785
|
+
} catch (error2) {
|
|
8786
|
+
sendIntakeError(res, error2);
|
|
8787
|
+
}
|
|
8788
|
+
}
|
|
8789
|
+
async function handleRequirementIntakeCancel(res, req, service, intakeId) {
|
|
8790
|
+
if (!serviceOr503(res, service)) return;
|
|
8791
|
+
const body = await readJsonBody3(res, req);
|
|
8792
|
+
if (body === null) return;
|
|
8793
|
+
try {
|
|
8794
|
+
const record2 = await discoverIntake(res, service, intakeId);
|
|
8795
|
+
if (!record2) return;
|
|
8796
|
+
const reason = typeof body["reason"] === "string" ? body["reason"] : void 0;
|
|
8797
|
+
const updated = await service.cancelIntake(intakeId, intakeContext(record2.projectId), reason);
|
|
8798
|
+
sendJson2(res, 200, updated);
|
|
8799
|
+
} catch (error2) {
|
|
8800
|
+
sendIntakeError(res, error2);
|
|
8801
|
+
}
|
|
8802
|
+
}
|
|
8803
|
+
async function handleRequirementIntakeArchive(res, service, intakeId) {
|
|
8804
|
+
if (!serviceOr503(res, service)) return;
|
|
8805
|
+
try {
|
|
8806
|
+
const record2 = await discoverIntake(res, service, intakeId);
|
|
8807
|
+
if (!record2) return;
|
|
8808
|
+
const updated = await service.archiveIntake(intakeId, intakeContext(record2.projectId));
|
|
8809
|
+
sendJson2(res, 200, updated);
|
|
8810
|
+
} catch (error2) {
|
|
8811
|
+
sendIntakeError(res, error2);
|
|
8812
|
+
}
|
|
8813
|
+
}
|
|
8814
|
+
|
|
8369
8815
|
// src/server/memory-diagnostics.ts
|
|
8370
8816
|
import * as fs8 from "node:fs/promises";
|
|
8371
8817
|
import * as path11 from "node:path";
|
|
@@ -8571,7 +9017,7 @@ async function touchProjectInManifest(options, globalConfigPath) {
|
|
|
8571
9017
|
// src/server/techstack-handlers.ts
|
|
8572
9018
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
8573
9019
|
var DEEP_DIVE_TIMEOUT_MS = 6e4;
|
|
8574
|
-
function
|
|
9020
|
+
function sendJson3(res, status, data) {
|
|
8575
9021
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8576
9022
|
res.end(JSON.stringify(data));
|
|
8577
9023
|
}
|
|
@@ -8586,13 +9032,13 @@ function handleTechStackSnapshot(res, deps2) {
|
|
|
8586
9032
|
try {
|
|
8587
9033
|
const snapshot = deps2.store.getSnapshot(deps2.projectId);
|
|
8588
9034
|
if (!snapshot) {
|
|
8589
|
-
|
|
9035
|
+
sendJson3(res, 404, { snapshot: null, stale: false });
|
|
8590
9036
|
return;
|
|
8591
9037
|
}
|
|
8592
9038
|
const ageMs = Date.now() - new Date(snapshot.createdAt).getTime();
|
|
8593
|
-
|
|
9039
|
+
sendJson3(res, 200, { snapshot, stale: ageMs > 24 * 60 * 60 * 1e3 });
|
|
8594
9040
|
} catch (error2) {
|
|
8595
|
-
|
|
9041
|
+
sendJson3(res, 500, {
|
|
8596
9042
|
error: "TechStack store unavailable",
|
|
8597
9043
|
detail: errorMessage(error2)
|
|
8598
9044
|
});
|
|
@@ -8603,7 +9049,7 @@ function errorMessage(error2) {
|
|
|
8603
9049
|
}
|
|
8604
9050
|
function requireJobDeps(res, deps2) {
|
|
8605
9051
|
if (!deps2.projectRoot || !deps2.engine) {
|
|
8606
|
-
|
|
9052
|
+
sendJson3(res, 503, { error: "TechStack engine unavailable" });
|
|
8607
9053
|
return false;
|
|
8608
9054
|
}
|
|
8609
9055
|
return true;
|
|
@@ -8614,7 +9060,7 @@ function startJob(res, deps2, kind) {
|
|
|
8614
9060
|
const controller = new AbortController();
|
|
8615
9061
|
deps2.runningJobs?.set(jobId, controller);
|
|
8616
9062
|
deps2.emit?.({ type: "techstack.job.started", payload: { jobId, kind } });
|
|
8617
|
-
|
|
9063
|
+
sendJson3(res, 202, { jobId, kind, status: "queued" });
|
|
8618
9064
|
void buildResearcher(deps2, kind).catch(() => void 0).then(
|
|
8619
9065
|
(researcher) => deps2.engine.analyze(deps2.projectId, {
|
|
8620
9066
|
targetRoot: deps2.projectRoot,
|
|
@@ -8660,24 +9106,24 @@ function handleTechStackCancel(res, deps2, jobId) {
|
|
|
8660
9106
|
if (controller && !controller.signal.aborted) controller.abort();
|
|
8661
9107
|
deps2.store.updateJobStatus(jobId, "cancelled");
|
|
8662
9108
|
deps2.emit?.({ type: "techstack.job.cancelled", payload: { jobId } });
|
|
8663
|
-
|
|
9109
|
+
sendJson3(res, 200, { jobId, status: "cancelled" });
|
|
8664
9110
|
}
|
|
8665
9111
|
async function handleTechStackDependencyResearch(res, deps2, dependencyId) {
|
|
8666
9112
|
const snapshot = deps2.store.getSnapshot(deps2.projectId);
|
|
8667
9113
|
const dependency = snapshot?.dependencies.find((dep) => dep.id === dependencyId);
|
|
8668
9114
|
if (!dependency) {
|
|
8669
|
-
|
|
9115
|
+
sendJson3(res, 404, { error: "Dependency not found in the current snapshot" });
|
|
8670
9116
|
return;
|
|
8671
9117
|
}
|
|
8672
9118
|
let researcher;
|
|
8673
9119
|
try {
|
|
8674
9120
|
researcher = await buildResearcher(deps2, "analyze");
|
|
8675
9121
|
} catch (error2) {
|
|
8676
|
-
|
|
9122
|
+
sendJson3(res, 503, { error: "Research unavailable", detail: errorMessage(error2) });
|
|
8677
9123
|
return;
|
|
8678
9124
|
}
|
|
8679
9125
|
if (!researcher) {
|
|
8680
|
-
|
|
9126
|
+
sendJson3(res, 503, {
|
|
8681
9127
|
error: "No model configured \u2014 connect a provider to run LLM analysis."
|
|
8682
9128
|
});
|
|
8683
9129
|
return;
|
|
@@ -8694,9 +9140,9 @@ async function handleTechStackDependencyResearch(res, deps2, dependencyId) {
|
|
|
8694
9140
|
[triaged ?? { dependency, cluster: "breaking_change", priority: 0 }],
|
|
8695
9141
|
{ signal: controller.signal }
|
|
8696
9142
|
);
|
|
8697
|
-
|
|
9143
|
+
sendJson3(res, 200, { dependencyId, findings });
|
|
8698
9144
|
} catch (error2) {
|
|
8699
|
-
|
|
9145
|
+
sendJson3(res, 500, { error: "Research failed", detail: errorMessage(error2) });
|
|
8700
9146
|
} finally {
|
|
8701
9147
|
clearTimeout(timeout);
|
|
8702
9148
|
controller.abort();
|
|
@@ -8705,15 +9151,15 @@ async function handleTechStackDependencyResearch(res, deps2, dependencyId) {
|
|
|
8705
9151
|
function handleTechStackJobStatus(res, deps2, jobId) {
|
|
8706
9152
|
const job = deps2.store.getJob(jobId);
|
|
8707
9153
|
if (!job) {
|
|
8708
|
-
|
|
9154
|
+
sendJson3(res, 404, { error: "Job not found" });
|
|
8709
9155
|
return;
|
|
8710
9156
|
}
|
|
8711
|
-
|
|
9157
|
+
sendJson3(res, 200, { job });
|
|
8712
9158
|
}
|
|
8713
9159
|
function handleTechStackReport(res, deps2, reportId, format) {
|
|
8714
9160
|
const snapshot = deps2.store.getSnapshotById(reportId);
|
|
8715
9161
|
if (!snapshot) {
|
|
8716
|
-
|
|
9162
|
+
sendJson3(res, 404, { error: "Report not found" });
|
|
8717
9163
|
return;
|
|
8718
9164
|
}
|
|
8719
9165
|
if (deps2.engine) {
|
|
@@ -8724,30 +9170,30 @@ function handleTechStackReport(res, deps2, reportId, format) {
|
|
|
8724
9170
|
});
|
|
8725
9171
|
res.end(report);
|
|
8726
9172
|
} else {
|
|
8727
|
-
|
|
9173
|
+
sendJson3(res, 200, snapshot);
|
|
8728
9174
|
}
|
|
8729
9175
|
}
|
|
8730
9176
|
async function handleTechStackTrends(res, deps2) {
|
|
8731
9177
|
try {
|
|
8732
9178
|
const { TrendStore } = await import("@wrongstack/techstack");
|
|
8733
|
-
|
|
9179
|
+
sendJson3(res, 200, { trend: new TrendStore(deps2.store).analyze(deps2.projectId) });
|
|
8734
9180
|
} catch (error2) {
|
|
8735
|
-
|
|
9181
|
+
sendJson3(res, 500, { error: "Trend analysis failed", detail: errorMessage(error2) });
|
|
8736
9182
|
}
|
|
8737
9183
|
}
|
|
8738
9184
|
async function handleTechStackRemediationPlan(res, deps2) {
|
|
8739
9185
|
const snapshot = deps2.store.getSnapshot(deps2.projectId);
|
|
8740
9186
|
if (!snapshot) {
|
|
8741
|
-
|
|
9187
|
+
sendJson3(res, 404, { error: "No TechStack snapshot is available" });
|
|
8742
9188
|
return;
|
|
8743
9189
|
}
|
|
8744
9190
|
const { applyPlan, generateUpgradePlan } = await import("@wrongstack/techstack");
|
|
8745
9191
|
const plan = generateUpgradePlan(snapshot);
|
|
8746
|
-
|
|
9192
|
+
sendJson3(res, 200, { plan, preview: await applyPlan(plan) });
|
|
8747
9193
|
}
|
|
8748
9194
|
async function handleTechStackRemediationApply(req, res, deps2) {
|
|
8749
9195
|
if (!deps2.executePackageOperation) {
|
|
8750
|
-
|
|
9196
|
+
sendJson3(res, 503, { error: "Permission-governed package execution is unavailable" });
|
|
8751
9197
|
return;
|
|
8752
9198
|
}
|
|
8753
9199
|
let approvedItems;
|
|
@@ -8760,16 +9206,16 @@ async function handleTechStackRemediationApply(req, res, deps2) {
|
|
|
8760
9206
|
const body = JSON.parse(raw || "{}");
|
|
8761
9207
|
approvedItems = Array.isArray(body.approvedItems) ? body.approvedItems.filter((value) => typeof value === "string") : [];
|
|
8762
9208
|
} catch (error2) {
|
|
8763
|
-
|
|
9209
|
+
sendJson3(res, 400, { error: "Invalid request body", detail: errorMessage(error2) });
|
|
8764
9210
|
return;
|
|
8765
9211
|
}
|
|
8766
9212
|
if (approvedItems.length === 0) {
|
|
8767
|
-
|
|
9213
|
+
sendJson3(res, 400, { error: "approvedItems must explicitly identify at least one plan item" });
|
|
8768
9214
|
return;
|
|
8769
9215
|
}
|
|
8770
9216
|
const snapshot = deps2.store.getSnapshot(deps2.projectId);
|
|
8771
9217
|
if (!snapshot) {
|
|
8772
|
-
|
|
9218
|
+
sendJson3(res, 404, { error: "No TechStack snapshot is available" });
|
|
8773
9219
|
return;
|
|
8774
9220
|
}
|
|
8775
9221
|
const approved = new Set(approvedItems);
|
|
@@ -8784,14 +9230,17 @@ async function handleTechStackRemediationApply(req, res, deps2) {
|
|
|
8784
9230
|
return executePackageOperation(operation, workspace?.relativeRoot);
|
|
8785
9231
|
}
|
|
8786
9232
|
});
|
|
8787
|
-
|
|
9233
|
+
sendJson3(res, 200, { plan, result });
|
|
8788
9234
|
}
|
|
8789
9235
|
|
|
8790
9236
|
// src/server/ws-auth.ts
|
|
8791
9237
|
import { Buffer as Buffer2 } from "node:buffer";
|
|
8792
9238
|
import { timingSafeEqual } from "node:crypto";
|
|
9239
|
+
import { isLoopbackHost as isLoopbackHostCore } from "@wrongstack/core/hq";
|
|
8793
9240
|
function isLoopbackHostname(hostname) {
|
|
8794
|
-
|
|
9241
|
+
const normalized = hostname.toLowerCase();
|
|
9242
|
+
if (normalized === "localhost") return true;
|
|
9243
|
+
return isLoopbackHostCore(normalized);
|
|
8795
9244
|
}
|
|
8796
9245
|
function effectivePort(url) {
|
|
8797
9246
|
if (url.port) return url.port;
|
|
@@ -8812,7 +9261,7 @@ function isTrustedLoopbackOrigin(origin, hostHeader) {
|
|
|
8812
9261
|
}
|
|
8813
9262
|
}
|
|
8814
9263
|
function isLoopbackBind(wsHost) {
|
|
8815
|
-
return wsHost
|
|
9264
|
+
return isLoopbackHostCore(wsHost) || wsHost === "localhost";
|
|
8816
9265
|
}
|
|
8817
9266
|
function isWildcardBind(wsHost) {
|
|
8818
9267
|
return wsHost === "0.0.0.0" || wsHost === "::" || wsHost === "[::]";
|
|
@@ -8839,19 +9288,22 @@ function extractToken(url) {
|
|
|
8839
9288
|
function extractTokenFromCookie(cookieHeader) {
|
|
8840
9289
|
if (!cookieHeader) return void 0;
|
|
8841
9290
|
const raw = Array.isArray(cookieHeader) ? cookieHeader.join("; ") : cookieHeader;
|
|
9291
|
+
let plain;
|
|
8842
9292
|
for (const part of raw.split(";")) {
|
|
8843
9293
|
const eq = part.indexOf("=");
|
|
8844
9294
|
if (eq < 0) continue;
|
|
8845
9295
|
const name2 = part.slice(0, eq).trim();
|
|
8846
|
-
if (name2
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
9296
|
+
if (name2 !== "ws_token" && name2 !== "__Host-ws_token") continue;
|
|
9297
|
+
let value;
|
|
9298
|
+
try {
|
|
9299
|
+
value = decodeURIComponent(part.slice(eq + 1).trim());
|
|
9300
|
+
} catch {
|
|
9301
|
+
value = part.slice(eq + 1).trim();
|
|
8852
9302
|
}
|
|
9303
|
+
if (name2 === "__Host-ws_token") return value;
|
|
9304
|
+
plain ??= value;
|
|
8853
9305
|
}
|
|
8854
|
-
return
|
|
9306
|
+
return plain;
|
|
8855
9307
|
}
|
|
8856
9308
|
function hostHeaderOk(input) {
|
|
8857
9309
|
if (!isLoopbackBind(input.wsHost)) return true;
|
|
@@ -8893,7 +9345,8 @@ function verifyClient(input) {
|
|
|
8893
9345
|
expectedToken,
|
|
8894
9346
|
requireToken,
|
|
8895
9347
|
allowedHostnames,
|
|
8896
|
-
allowBrowserUrlToken
|
|
9348
|
+
allowBrowserUrlToken,
|
|
9349
|
+
allowCrossPortLoopbackCookie
|
|
8897
9350
|
} = input;
|
|
8898
9351
|
const urlTokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
|
|
8899
9352
|
const cookieTokenOk = tokenMatches(extractTokenFromCookie(cookieHeader), expectedToken);
|
|
@@ -8908,7 +9361,10 @@ function verifyClient(input) {
|
|
|
8908
9361
|
const { hostname: originHostname } = new URL(origin);
|
|
8909
9362
|
if (isLoopbackHostname(originHostname)) {
|
|
8910
9363
|
if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
|
|
8911
|
-
|
|
9364
|
+
if (!isTrustedLoopbackOrigin(origin, hostHeader)) {
|
|
9365
|
+
return Boolean(allowCrossPortLoopbackCookie) && cookieTokenOk;
|
|
9366
|
+
}
|
|
9367
|
+
return true;
|
|
8912
9368
|
}
|
|
8913
9369
|
return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
|
|
8914
9370
|
} catch {
|
|
@@ -8943,11 +9399,22 @@ ${out}`;
|
|
|
8943
9399
|
function firstHeader(value) {
|
|
8944
9400
|
return Array.isArray(value) ? value[0] : value;
|
|
8945
9401
|
}
|
|
8946
|
-
|
|
8947
|
-
|
|
9402
|
+
var WS_TOKEN_COOKIE = "ws_token";
|
|
9403
|
+
var WS_TOKEN_COOKIE_SECURE = "__Host-ws_token";
|
|
9404
|
+
function wsTokenCookie(token, secure) {
|
|
9405
|
+
const name2 = secure ? WS_TOKEN_COOKIE_SECURE : WS_TOKEN_COOKIE;
|
|
9406
|
+
const parts = [
|
|
9407
|
+
`${name2}=${encodeURIComponent(token)}`,
|
|
9408
|
+
"HttpOnly",
|
|
9409
|
+
"SameSite=Strict",
|
|
9410
|
+
"Path=/",
|
|
9411
|
+
"Max-Age=3600"
|
|
9412
|
+
];
|
|
9413
|
+
if (secure) parts.push("Secure");
|
|
9414
|
+
return parts.join("; ");
|
|
8948
9415
|
}
|
|
8949
|
-
function setAuthCookieHeaders(res, token) {
|
|
8950
|
-
res.setHeader("Set-Cookie", wsTokenCookie(token));
|
|
9416
|
+
function setAuthCookieHeaders(res, token, secure) {
|
|
9417
|
+
res.setHeader("Set-Cookie", wsTokenCookie(token, secure));
|
|
8951
9418
|
res.setHeader("Cache-Control", "no-store");
|
|
8952
9419
|
}
|
|
8953
9420
|
function setStaticSecurityHeaders(res) {
|
|
@@ -8955,8 +9422,16 @@ function setStaticSecurityHeaders(res) {
|
|
|
8955
9422
|
res.setHeader("X-Frame-Options", "DENY");
|
|
8956
9423
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
8957
9424
|
}
|
|
8958
|
-
function requestToken(req, url) {
|
|
8959
|
-
|
|
9425
|
+
function requestToken(req, url, opts = {}) {
|
|
9426
|
+
const queryToken = url.searchParams.get("token") ?? void 0;
|
|
9427
|
+
if (queryToken !== void 0 && (opts.allowQuery === true || isLoopbackPeer(req))) {
|
|
9428
|
+
return queryToken;
|
|
9429
|
+
}
|
|
9430
|
+
return firstHeader(req.headers["x-ws-token"]) ?? extractTokenFromCookie(req.headers.cookie);
|
|
9431
|
+
}
|
|
9432
|
+
function isLoopbackPeer(req) {
|
|
9433
|
+
const address = req.socket.remoteAddress?.replace(/^::ffff:/i, "");
|
|
9434
|
+
return address !== void 0 && isLoopbackHostname(address);
|
|
8960
9435
|
}
|
|
8961
9436
|
function formatCspHostname(hostname) {
|
|
8962
9437
|
return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
@@ -9011,7 +9486,8 @@ function strictDecodeParam(segment, res) {
|
|
|
9011
9486
|
function createHttpServer(opts) {
|
|
9012
9487
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
9013
9488
|
const distDir = path13.resolve(opts.distDir);
|
|
9014
|
-
const requireAccessToken =
|
|
9489
|
+
const requireAccessToken = true;
|
|
9490
|
+
const secureCookies = opts.secureCookies ?? (opts.publicWsUrl?.trim().toLowerCase().startsWith("wss:") ?? false);
|
|
9015
9491
|
const trustedHostnames = (() => {
|
|
9016
9492
|
const names = [...opts.allowedHostnames ?? []];
|
|
9017
9493
|
if (opts.publicWsUrl) {
|
|
@@ -9050,13 +9526,13 @@ function createHttpServer(opts) {
|
|
|
9050
9526
|
const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
|
|
9051
9527
|
const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
|
|
9052
9528
|
if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
|
|
9053
|
-
const provided = requestToken(req, url);
|
|
9529
|
+
const provided = requestToken(req, url, { allowQuery: true });
|
|
9054
9530
|
if (!provided || !opts.apiToken || !tokenMatches(provided, opts.apiToken)) {
|
|
9055
9531
|
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
9056
9532
|
res.end("Unauthorized");
|
|
9057
9533
|
return;
|
|
9058
9534
|
}
|
|
9059
|
-
setAuthCookieHeaders(res, opts.apiToken);
|
|
9535
|
+
setAuthCookieHeaders(res, opts.apiToken, secureCookies);
|
|
9060
9536
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
9061
9537
|
res.end("ok");
|
|
9062
9538
|
return;
|
|
@@ -9070,7 +9546,7 @@ function createHttpServer(opts) {
|
|
|
9070
9546
|
return;
|
|
9071
9547
|
}
|
|
9072
9548
|
if (shouldSetAuthCookie && opts.apiToken) {
|
|
9073
|
-
setAuthCookieHeaders(res, opts.apiToken);
|
|
9549
|
+
setAuthCookieHeaders(res, opts.apiToken, secureCookies);
|
|
9074
9550
|
}
|
|
9075
9551
|
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
9076
9552
|
if (requireAccessToken && !accessTokenOk) {
|
|
@@ -9188,6 +9664,88 @@ function createHttpServer(opts) {
|
|
|
9188
9664
|
await handleApiAnalyticsSummary(res);
|
|
9189
9665
|
return;
|
|
9190
9666
|
}
|
|
9667
|
+
if (url.pathname === "/api/requirement-intakes" && req.method === "GET") {
|
|
9668
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
9669
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
9670
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
9671
|
+
return;
|
|
9672
|
+
}
|
|
9673
|
+
await handleRequirementIntakeListForServer(res, opts.intakeService, opts.projectRoot);
|
|
9674
|
+
return;
|
|
9675
|
+
}
|
|
9676
|
+
const intakeProjectMatch = url.pathname.match(
|
|
9677
|
+
/^\/api\/projects\/([^/]+)\/requirement-intakes$/
|
|
9678
|
+
);
|
|
9679
|
+
if (intakeProjectMatch && req.method === "POST") {
|
|
9680
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
9681
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
9682
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
9683
|
+
return;
|
|
9684
|
+
}
|
|
9685
|
+
await handleRequirementIntakeCreate(
|
|
9686
|
+
res,
|
|
9687
|
+
req,
|
|
9688
|
+
opts.intakeService,
|
|
9689
|
+
decodeURIComponent(intakeProjectMatch[1])
|
|
9690
|
+
);
|
|
9691
|
+
return;
|
|
9692
|
+
}
|
|
9693
|
+
if (intakeProjectMatch && req.method === "GET") {
|
|
9694
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
9695
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
9696
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
9697
|
+
return;
|
|
9698
|
+
}
|
|
9699
|
+
await handleRequirementIntakeList(
|
|
9700
|
+
res,
|
|
9701
|
+
opts.intakeService,
|
|
9702
|
+
decodeURIComponent(intakeProjectMatch[1])
|
|
9703
|
+
);
|
|
9704
|
+
return;
|
|
9705
|
+
}
|
|
9706
|
+
const intakeIdMatch = url.pathname.match(/^\/api\/requirement-intakes\/([^/]+)$/);
|
|
9707
|
+
if (intakeIdMatch && req.method === "GET") {
|
|
9708
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
9709
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
9710
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
9711
|
+
return;
|
|
9712
|
+
}
|
|
9713
|
+
await handleRequirementIntakeGet(res, opts.intakeService, intakeIdMatch[1]);
|
|
9714
|
+
return;
|
|
9715
|
+
}
|
|
9716
|
+
if (intakeIdMatch && req.method === "PATCH") {
|
|
9717
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
9718
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
9719
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
9720
|
+
return;
|
|
9721
|
+
}
|
|
9722
|
+
await handleRequirementIntakeUpdate(res, req, opts.intakeService, intakeIdMatch[1]);
|
|
9723
|
+
return;
|
|
9724
|
+
}
|
|
9725
|
+
const intakeActionMatch = url.pathname.match(
|
|
9726
|
+
/^\/api\/requirement-intakes\/([^/]+)\/(answers|suggestions|submit|cancel|archive)$/
|
|
9727
|
+
);
|
|
9728
|
+
if (intakeActionMatch && req.method === "POST") {
|
|
9729
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
9730
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
9731
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
9732
|
+
return;
|
|
9733
|
+
}
|
|
9734
|
+
const intakeId = intakeActionMatch[1];
|
|
9735
|
+
const action = intakeActionMatch[2];
|
|
9736
|
+
if (action === "answers") {
|
|
9737
|
+
await handleRequirementIntakeAnswers(res, req, opts.intakeService, intakeId);
|
|
9738
|
+
} else if (action === "suggestions") {
|
|
9739
|
+
await handleRequirementIntakeSuggestions(res, req, opts.intakeService, intakeId);
|
|
9740
|
+
} else if (action === "submit") {
|
|
9741
|
+
await handleRequirementIntakeSubmit(res, opts.intakeService, intakeId);
|
|
9742
|
+
} else if (action === "cancel") {
|
|
9743
|
+
await handleRequirementIntakeCancel(res, req, opts.intakeService, intakeId);
|
|
9744
|
+
} else {
|
|
9745
|
+
await handleRequirementIntakeArchive(res, opts.intakeService, intakeId);
|
|
9746
|
+
}
|
|
9747
|
+
return;
|
|
9748
|
+
}
|
|
9191
9749
|
if (url.pathname === "/api/codemap/packages" && req.method === "GET") {
|
|
9192
9750
|
if (requireAccessToken && !accessTokenOk) {
|
|
9193
9751
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
@@ -9383,17 +9941,25 @@ function createHttpServer(opts) {
|
|
|
9383
9941
|
res.end(JSON.stringify({ error: "Project root not configured" }));
|
|
9384
9942
|
return;
|
|
9385
9943
|
}
|
|
9386
|
-
await handleDeadCodeScan(
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9944
|
+
await handleDeadCodeScan(
|
|
9945
|
+
res,
|
|
9946
|
+
{
|
|
9947
|
+
projectRoot: opts.projectRoot,
|
|
9948
|
+
indexDir: opts.indexDir
|
|
9949
|
+
},
|
|
9950
|
+
req
|
|
9951
|
+
);
|
|
9390
9952
|
return;
|
|
9391
9953
|
}
|
|
9392
9954
|
if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
|
|
9393
|
-
await handleDeadCodeActionPlan(
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9955
|
+
await handleDeadCodeActionPlan(
|
|
9956
|
+
res,
|
|
9957
|
+
{
|
|
9958
|
+
projectRoot: opts.projectRoot ?? "",
|
|
9959
|
+
indexDir: opts.indexDir
|
|
9960
|
+
},
|
|
9961
|
+
req
|
|
9962
|
+
);
|
|
9397
9963
|
return;
|
|
9398
9964
|
}
|
|
9399
9965
|
if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
|
|
@@ -11696,6 +12262,55 @@ import {
|
|
|
11696
12262
|
restartMcp,
|
|
11697
12263
|
updateMcp
|
|
11698
12264
|
} from "@wrongstack/mcp";
|
|
12265
|
+
|
|
12266
|
+
// src/server/privileged-actions.ts
|
|
12267
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
12268
|
+
import {
|
|
12269
|
+
isTrustDecisionAllowed
|
|
12270
|
+
} from "@wrongstack/core/security";
|
|
12271
|
+
async function authorizeWebUIAction(boundary, action, logger) {
|
|
12272
|
+
const request = {
|
|
12273
|
+
version: 1,
|
|
12274
|
+
requestId: randomUUID3(),
|
|
12275
|
+
actor: {
|
|
12276
|
+
kind: "remote-client",
|
|
12277
|
+
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
12278
|
+
},
|
|
12279
|
+
surface: "webui",
|
|
12280
|
+
capability: action.capability,
|
|
12281
|
+
subject: action.subject,
|
|
12282
|
+
risk: action.risk,
|
|
12283
|
+
scope: {
|
|
12284
|
+
...action.cwd ? { cwd: action.cwd } : {},
|
|
12285
|
+
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
12286
|
+
},
|
|
12287
|
+
authContext: { method: "session" },
|
|
12288
|
+
...action.metadata ? { metadata: action.metadata } : {}
|
|
12289
|
+
};
|
|
12290
|
+
const decision = await boundary.evaluate(request);
|
|
12291
|
+
logger?.debug?.(
|
|
12292
|
+
`[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
|
|
12293
|
+
);
|
|
12294
|
+
return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
|
|
12295
|
+
}
|
|
12296
|
+
|
|
12297
|
+
// src/server/mcp-handlers.ts
|
|
12298
|
+
async function authorizeMcpMutation(ws, operation, serverName, trustBoundary) {
|
|
12299
|
+
if (!trustBoundary) return true;
|
|
12300
|
+
const authorization = await authorizeWebUIAction(trustBoundary, {
|
|
12301
|
+
capability: "mcp.server.configure",
|
|
12302
|
+
subject: { kind: "process", id: serverName },
|
|
12303
|
+
risk: "elevated",
|
|
12304
|
+
metadata: { transport: "websocket", operation }
|
|
12305
|
+
});
|
|
12306
|
+
if (!authorization.allowed) {
|
|
12307
|
+
send(ws, {
|
|
12308
|
+
type: "mcp.operation_result",
|
|
12309
|
+
payload: { success: false, message: `${operation} denied: ${authorization.reason}` }
|
|
12310
|
+
});
|
|
12311
|
+
}
|
|
12312
|
+
return authorization.allowed;
|
|
12313
|
+
}
|
|
11699
12314
|
function mapStatus(raw) {
|
|
11700
12315
|
switch (raw) {
|
|
11701
12316
|
case "connected":
|
|
@@ -11770,7 +12385,7 @@ async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
|
|
|
11770
12385
|
payload: { servers: servers.map((server) => toView(server, health.get(server.name))) }
|
|
11771
12386
|
});
|
|
11772
12387
|
}
|
|
11773
|
-
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
12388
|
+
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry, trustBoundary) {
|
|
11774
12389
|
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
11775
12390
|
if (!d) return;
|
|
11776
12391
|
const validated = validateMcpServerPayload(msg.payload, "mcp.add");
|
|
@@ -11781,6 +12396,7 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
11781
12396
|
});
|
|
11782
12397
|
return;
|
|
11783
12398
|
}
|
|
12399
|
+
if (!await authorizeMcpMutation(ws, "mcp.add", name(msg), trustBoundary)) return;
|
|
11784
12400
|
const result = await addMcp(validated.value, d);
|
|
11785
12401
|
if (result.ok && result.server) {
|
|
11786
12402
|
send(ws, { type: "mcp.server.added", payload: { server: toView(result.server) } });
|
|
@@ -11798,7 +12414,7 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
11798
12414
|
payload: { success: result.ok, message: result.message }
|
|
11799
12415
|
});
|
|
11800
12416
|
}
|
|
11801
|
-
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
12417
|
+
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry, trustBoundary) {
|
|
11802
12418
|
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
11803
12419
|
if (!d) return;
|
|
11804
12420
|
const validated = validateMcpServerPayload(msg.payload, "mcp.update");
|
|
@@ -11809,6 +12425,7 @@ async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
11809
12425
|
});
|
|
11810
12426
|
return;
|
|
11811
12427
|
}
|
|
12428
|
+
if (!await authorizeMcpMutation(ws, "mcp.update", name(msg), trustBoundary)) return;
|
|
11812
12429
|
const result = await updateMcp(validated.value, d);
|
|
11813
12430
|
if (result.ok && result.server) {
|
|
11814
12431
|
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
@@ -12952,7 +13569,7 @@ function openBrowser(url, platform = process.platform) {
|
|
|
12952
13569
|
}
|
|
12953
13570
|
|
|
12954
13571
|
// src/server/port-utils.ts
|
|
12955
|
-
import * as
|
|
13572
|
+
import * as net2 from "node:net";
|
|
12956
13573
|
import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
12957
13574
|
var SURFACE_DEFAULT_PORTS = {
|
|
12958
13575
|
webui: { http: 3456 },
|
|
@@ -12966,7 +13583,7 @@ function getSurfaceDefaultPorts(surface) {
|
|
|
12966
13583
|
}
|
|
12967
13584
|
function isPortFree(host, port) {
|
|
12968
13585
|
return new Promise((resolve16) => {
|
|
12969
|
-
const srv =
|
|
13586
|
+
const srv = net2.createServer();
|
|
12970
13587
|
srv.once("error", () => resolve16(false));
|
|
12971
13588
|
srv.once("listening", () => {
|
|
12972
13589
|
srv.close(() => resolve16(true));
|
|
@@ -13000,6 +13617,27 @@ import { spawn as spawn2 } from "node:child_process";
|
|
|
13000
13617
|
import { existsSync } from "node:fs";
|
|
13001
13618
|
import { findPackageJSON } from "node:module";
|
|
13002
13619
|
import * as path15 from "node:path";
|
|
13620
|
+
|
|
13621
|
+
// src/server/intake-service.ts
|
|
13622
|
+
import { resolveWstackPaths as resolveWstackPaths4 } from "@wrongstack/core/utils";
|
|
13623
|
+
import {
|
|
13624
|
+
AllowAllIntakeAuthorizer,
|
|
13625
|
+
RequirementIntakeService,
|
|
13626
|
+
RequirementIntakeStore
|
|
13627
|
+
} from "@wrongstack/requirement-intake";
|
|
13628
|
+
function createProjectIntakeService(opts) {
|
|
13629
|
+
return new RequirementIntakeService({
|
|
13630
|
+
store: new RequirementIntakeStore({
|
|
13631
|
+
baseDir: resolveWstackPaths4({
|
|
13632
|
+
projectRoot: opts.projectRoot,
|
|
13633
|
+
globalRoot: opts.globalRoot
|
|
13634
|
+
}).projectRequirementIntakes
|
|
13635
|
+
}),
|
|
13636
|
+
authorizer: new AllowAllIntakeAuthorizer()
|
|
13637
|
+
});
|
|
13638
|
+
}
|
|
13639
|
+
|
|
13640
|
+
// src/server/frontend-static-serve.ts
|
|
13003
13641
|
function resolveDistDir(input) {
|
|
13004
13642
|
const options = typeof input === "string" ? { explicitDistDir: input } : input ?? {};
|
|
13005
13643
|
if (options.explicitDistDir) return path15.resolve(options.explicitDistDir);
|
|
@@ -13084,6 +13722,10 @@ async function startStaticServe(opts, deps2 = {}) {
|
|
|
13084
13722
|
const create = deps2.createServer ?? createHttpServer;
|
|
13085
13723
|
const distDir = deps2.resolveDist ? deps2.resolveDist(opts.distDir) : await ensureDist(opts.distDir);
|
|
13086
13724
|
if (distDir === null) return null;
|
|
13725
|
+
const intakeService = opts.intakeService ?? (opts.projectRoot ? createProjectIntakeService({
|
|
13726
|
+
projectRoot: opts.projectRoot,
|
|
13727
|
+
globalRoot: opts.globalRoot
|
|
13728
|
+
}) : void 0);
|
|
13087
13729
|
const server = create({
|
|
13088
13730
|
host: opts.host,
|
|
13089
13731
|
port: opts.httpPort,
|
|
@@ -13096,7 +13738,8 @@ async function startStaticServe(opts, deps2 = {}) {
|
|
|
13096
13738
|
publicWsUrl: opts.publicWsUrl,
|
|
13097
13739
|
apiToken: opts.apiToken,
|
|
13098
13740
|
requireToken: opts.requireToken,
|
|
13099
|
-
allowedHostnames: opts.allowedHostnames
|
|
13741
|
+
allowedHostnames: opts.allowedHostnames,
|
|
13742
|
+
intakeService
|
|
13100
13743
|
});
|
|
13101
13744
|
if (!opts.deferListen) {
|
|
13102
13745
|
server.listen(opts.httpPort, opts.host);
|
|
@@ -13201,7 +13844,8 @@ function registerWebuiInstance(p, deps2 = {}) {
|
|
|
13201
13844
|
host: p.host,
|
|
13202
13845
|
port: p.httpPort,
|
|
13203
13846
|
publicUrl: p.publicUrl
|
|
13204
|
-
})
|
|
13847
|
+
}),
|
|
13848
|
+
...p.authToken ? { authToken: p.authToken } : {}
|
|
13205
13849
|
},
|
|
13206
13850
|
p.registryBaseDir
|
|
13207
13851
|
).catch(() => {
|
|
@@ -13235,10 +13879,31 @@ ${extraBlock}`
|
|
|
13235
13879
|
if (p.open) launch(openUrl);
|
|
13236
13880
|
});
|
|
13237
13881
|
}
|
|
13882
|
+
var DEFAULT_CHILD_CLEANUP_TIMEOUT_MS = 1e4;
|
|
13883
|
+
async function runBounded(work, timeoutMs, label, debug) {
|
|
13884
|
+
let timer;
|
|
13885
|
+
try {
|
|
13886
|
+
await Promise.race([
|
|
13887
|
+
Promise.resolve().then(() => work()).catch((err) => {
|
|
13888
|
+
debug(`[webui-server] ${label} failed: ${err}`);
|
|
13889
|
+
}),
|
|
13890
|
+
new Promise((resolve16) => {
|
|
13891
|
+
timer = setTimeout(() => {
|
|
13892
|
+
debug(`[webui-server] ${label} timed out after ${timeoutMs}ms`);
|
|
13893
|
+
resolve16();
|
|
13894
|
+
}, timeoutMs);
|
|
13895
|
+
timer.unref?.();
|
|
13896
|
+
})
|
|
13897
|
+
]);
|
|
13898
|
+
} finally {
|
|
13899
|
+
if (timer) clearTimeout(timer);
|
|
13900
|
+
}
|
|
13901
|
+
}
|
|
13238
13902
|
function createWebuiShutdown(res) {
|
|
13239
13903
|
const log = res.log ?? ((m) => console.log(m));
|
|
13240
13904
|
const debug = res.debug ?? ((m) => console.debug(m));
|
|
13241
13905
|
const unregister = res.unregisterFn ?? unregisterInstance;
|
|
13906
|
+
const childTimeout = Math.max(1, res.childCleanupTimeoutMs ?? DEFAULT_CHILD_CLEANUP_TIMEOUT_MS);
|
|
13242
13907
|
let started = false;
|
|
13243
13908
|
return () => {
|
|
13244
13909
|
if (started) return;
|
|
@@ -13246,17 +13911,30 @@ function createWebuiShutdown(res) {
|
|
|
13246
13911
|
log("[WebUI] Shutting down...");
|
|
13247
13912
|
res.abortInFlight();
|
|
13248
13913
|
res.unsubscribeEvents();
|
|
13249
|
-
|
|
13250
|
-
|
|
13251
|
-
|
|
13252
|
-
|
|
13253
|
-
|
|
13254
|
-
|
|
13255
|
-
|
|
13256
|
-
|
|
13257
|
-
|
|
13258
|
-
|
|
13914
|
+
void (async () => {
|
|
13915
|
+
if (res.stopOwnedChildren) {
|
|
13916
|
+
await runBounded(res.stopOwnedChildren, childTimeout, "stopOwnedChildren", debug);
|
|
13917
|
+
}
|
|
13918
|
+
if (res.disposeResources) {
|
|
13919
|
+
await runBounded(res.disposeResources, Math.min(childTimeout, 5e3), "disposeResources", debug);
|
|
13920
|
+
}
|
|
13921
|
+
res.closeClients();
|
|
13922
|
+
res.closeHttpServer();
|
|
13923
|
+
const unregistered = unregister(res.pid, res.registryBaseDir).catch(
|
|
13924
|
+
(err) => debug(`[webui-server] unregister failed: ${err}`)
|
|
13925
|
+
);
|
|
13926
|
+
await new Promise((resolve16) => {
|
|
13927
|
+
res.wss.close(() => resolve16());
|
|
13259
13928
|
});
|
|
13929
|
+
await unregistered;
|
|
13930
|
+
log("[WebUI] Server stopped");
|
|
13931
|
+
res.onStopped();
|
|
13932
|
+
})().catch((err) => {
|
|
13933
|
+
debug(`[webui-server] shutdown sequence failed: ${err}`);
|
|
13934
|
+
try {
|
|
13935
|
+
res.onStopped();
|
|
13936
|
+
} catch {
|
|
13937
|
+
}
|
|
13260
13938
|
});
|
|
13261
13939
|
};
|
|
13262
13940
|
}
|
|
@@ -13511,7 +14189,21 @@ function createWebuiClientPresence(deps2) {
|
|
|
13511
14189
|
}
|
|
13512
14190
|
|
|
13513
14191
|
// src/server/brain-handlers.ts
|
|
14192
|
+
import { BUILTIN_COUNCIL_PERSONAS } from "@wrongstack/core/execution";
|
|
13514
14193
|
import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
|
|
14194
|
+
var COUNCIL_PERSONA_CATALOG = Object.freeze(
|
|
14195
|
+
BUILTIN_COUNCIL_PERSONAS.map(
|
|
14196
|
+
(persona) => Object.freeze({
|
|
14197
|
+
id: persona.id,
|
|
14198
|
+
name: persona.name,
|
|
14199
|
+
description: persona.description,
|
|
14200
|
+
...persona.defaultVeto !== void 0 ? { defaultVeto: persona.defaultVeto } : {}
|
|
14201
|
+
})
|
|
14202
|
+
)
|
|
14203
|
+
);
|
|
14204
|
+
function brainConfigPayload(runtime) {
|
|
14205
|
+
return { ...runtime.getSnapshot(), personaCatalog: COUNCIL_PERSONA_CATALOG };
|
|
14206
|
+
}
|
|
13515
14207
|
function sendResult6(ctx, ws, success, message) {
|
|
13516
14208
|
ctx.send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
13517
14209
|
}
|
|
@@ -13551,7 +14243,7 @@ function handleBrainConfigGet(ctx, ws) {
|
|
|
13551
14243
|
}
|
|
13552
14244
|
ctx.send(ws, {
|
|
13553
14245
|
type: "brain.config",
|
|
13554
|
-
payload: { config: ctx.brainRuntime
|
|
14246
|
+
payload: { config: brainConfigPayload(ctx.brainRuntime), persisted: true }
|
|
13555
14247
|
});
|
|
13556
14248
|
}
|
|
13557
14249
|
async function handleBrainConfigSet(ctx, ws, payload) {
|
|
@@ -13575,7 +14267,7 @@ async function handleBrainConfigSet(ctx, ws, payload) {
|
|
|
13575
14267
|
ctx.send(ws, {
|
|
13576
14268
|
type: "brain.config",
|
|
13577
14269
|
payload: {
|
|
13578
|
-
config: ctx.brainRuntime
|
|
14270
|
+
config: brainConfigPayload(ctx.brainRuntime),
|
|
13579
14271
|
persisted: result.ok,
|
|
13580
14272
|
...result.ok ? {} : { error: result.error ?? "Persist failed." }
|
|
13581
14273
|
}
|
|
@@ -13585,7 +14277,7 @@ async function handleBrainConfigSet(ctx, ws, payload) {
|
|
|
13585
14277
|
ctx.send(ws, {
|
|
13586
14278
|
type: "brain.config",
|
|
13587
14279
|
payload: {
|
|
13588
|
-
config: ctx.brainRuntime
|
|
14280
|
+
config: brainConfigPayload(ctx.brainRuntime),
|
|
13589
14281
|
persisted: false,
|
|
13590
14282
|
error: `Invalid Brain setting: ${toErrorMessage5(err)}`
|
|
13591
14283
|
}
|
|
@@ -14350,6 +15042,8 @@ function buildAuditPrompt(board, health) {
|
|
|
14350
15042
|
|
|
14351
15043
|
// src/server/context-meta.ts
|
|
14352
15044
|
import { FallbackProfileManager } from "@wrongstack/core/agent";
|
|
15045
|
+
import { resolvePluginEnablement } from "@wrongstack/core/plugin";
|
|
15046
|
+
import { FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
|
|
14353
15047
|
function seedContextMeta(config, context) {
|
|
14354
15048
|
const meta = context.meta;
|
|
14355
15049
|
const autonomyCfg = config.autonomy ?? {};
|
|
@@ -14426,6 +15120,21 @@ function seedContextMeta(config, context) {
|
|
|
14426
15120
|
meta["tgDelegate"] = tgExt?.["notifyOnDelegate"] !== false;
|
|
14427
15121
|
const tgMs = tgExt?.["longToolThresholdMs"];
|
|
14428
15122
|
meta["tgLongToolMs"] = typeof tgMs === "number" ? tgMs : 3e4;
|
|
15123
|
+
{
|
|
15124
|
+
const pluginsEnabled = {};
|
|
15125
|
+
const record2 = (name2) => {
|
|
15126
|
+
if (FORBIDDEN_PROTO_KEYS2.has(name2) || name2 in pluginsEnabled) return;
|
|
15127
|
+
pluginsEnabled[name2] = resolvePluginEnablement({ name: name2, config }).enabled;
|
|
15128
|
+
};
|
|
15129
|
+
for (const entry of config.plugins ?? []) {
|
|
15130
|
+
const name2 = typeof entry === "string" ? entry : entry?.name;
|
|
15131
|
+
if (typeof name2 === "string") record2(name2);
|
|
15132
|
+
}
|
|
15133
|
+
for (const [name2, options] of Object.entries(config.extensions ?? {})) {
|
|
15134
|
+
if (typeof options?.["enabled"] === "boolean") record2(name2);
|
|
15135
|
+
}
|
|
15136
|
+
if (Object.keys(pluginsEnabled).length > 0) meta["pluginsEnabled"] = pluginsEnabled;
|
|
15137
|
+
}
|
|
14429
15138
|
const chimeraExt = config.extensions?.["wstack-chimera"];
|
|
14430
15139
|
meta["chimeraEnabled"] = chimeraExt?.["enabled"] === true;
|
|
14431
15140
|
meta["chimeraProvider"] = chimeraExt?.["provider"] ?? "";
|
|
@@ -14463,8 +15172,9 @@ function seedContextMeta(config, context) {
|
|
|
14463
15172
|
// src/server/pref-helpers.ts
|
|
14464
15173
|
import * as fs13 from "node:fs/promises";
|
|
14465
15174
|
import * as path18 from "node:path";
|
|
15175
|
+
import { pluginEntryMatchesName } from "@wrongstack/core/plugin";
|
|
14466
15176
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
14467
|
-
import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as
|
|
15177
|
+
import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS3 } from "@wrongstack/core/utils";
|
|
14468
15178
|
var PREF_KEYS = [
|
|
14469
15179
|
"autonomy",
|
|
14470
15180
|
"autonomyDelayMs",
|
|
@@ -14545,6 +15255,8 @@ var PREF_KEYS = [
|
|
|
14545
15255
|
// Display-only toggles (purely visual WebUI prefs, not persisted to config).
|
|
14546
15256
|
"groupToolCalls",
|
|
14547
15257
|
"showThinkingLogs",
|
|
15258
|
+
// v15: chat-input auto-collapse (opt-in display toggle, default off).
|
|
15259
|
+
"autoCollapseInput",
|
|
14548
15260
|
// Per-plugin enable/disable map (parity with the embedded server).
|
|
14549
15261
|
"pluginsEnabled",
|
|
14550
15262
|
// Fleet chat verbosity: off | full (migrated from streamFleet boolean).
|
|
@@ -14599,6 +15311,8 @@ async function updateGlobalConfig(deps2, holder, mutate, errorLabel) {
|
|
|
14599
15311
|
var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set([
|
|
14600
15312
|
"groupToolCalls",
|
|
14601
15313
|
"showThinkingLogs",
|
|
15314
|
+
// v15: chat-input auto-collapse (opt-in display toggle, default off).
|
|
15315
|
+
"autoCollapseInput",
|
|
14602
15316
|
"autoReviewFallbackModels",
|
|
14603
15317
|
// v11 Display parity: agent-swarm panel + inverse fsAccess flag.
|
|
14604
15318
|
// The TUI settings picker mirrors these so the browser can keep the
|
|
@@ -14809,15 +15523,29 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
14809
15523
|
decrypted.debugStream = payload["debugStream"];
|
|
14810
15524
|
if (typeof payload["pluginsEnabled"] === "object" && payload["pluginsEnabled"] !== null) {
|
|
14811
15525
|
const ext = decrypted.extensions ?? {};
|
|
15526
|
+
const toggled = [];
|
|
14812
15527
|
for (const [pluginName, enabled] of Object.entries(
|
|
14813
15528
|
payload["pluginsEnabled"]
|
|
14814
15529
|
)) {
|
|
14815
|
-
if (
|
|
15530
|
+
if (FORBIDDEN_PROTO_KEYS3.has(pluginName)) continue;
|
|
15531
|
+
if (typeof enabled !== "boolean") continue;
|
|
14816
15532
|
const pExt = ext[pluginName] ?? {};
|
|
14817
15533
|
pExt["enabled"] = enabled;
|
|
14818
15534
|
ext[pluginName] = pExt;
|
|
15535
|
+
toggled.push([pluginName, enabled]);
|
|
14819
15536
|
}
|
|
14820
15537
|
decrypted.extensions = ext;
|
|
15538
|
+
if (Array.isArray(decrypted.plugins) && toggled.length > 0) {
|
|
15539
|
+
decrypted.plugins = decrypted.plugins.map((entry) => {
|
|
15540
|
+
const entryName = typeof entry === "string" ? entry : entry?.name;
|
|
15541
|
+
if (typeof entryName !== "string") return entry;
|
|
15542
|
+
const hit = toggled.find(([name2]) => pluginEntryMatchesName(entryName, name2));
|
|
15543
|
+
if (!hit) return entry;
|
|
15544
|
+
const [, enabled] = hit;
|
|
15545
|
+
if (typeof entry === "string") return enabled ? entry : { name: entry, enabled: false };
|
|
15546
|
+
return { ...entry, enabled };
|
|
15547
|
+
});
|
|
15548
|
+
}
|
|
14821
15549
|
}
|
|
14822
15550
|
const chimeraTouched = typeof payload["chimeraEnabled"] === "boolean" || typeof payload["chimeraProvider"] === "string" || typeof payload["chimeraModel"] === "string" || typeof payload["chimeraMaxFiles"] === "number" || typeof payload["chimeraAutoFix"] === "string";
|
|
14823
15551
|
if (chimeraTouched) {
|
|
@@ -14926,39 +15654,6 @@ async function handlePrefsRoute(ws, msg, handlers) {
|
|
|
14926
15654
|
// src/server/process-handlers.ts
|
|
14927
15655
|
import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
|
|
14928
15656
|
import { getProcessRegistry as getProcessRegistry2 } from "@wrongstack/tools";
|
|
14929
|
-
|
|
14930
|
-
// src/server/privileged-actions.ts
|
|
14931
|
-
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
14932
|
-
import {
|
|
14933
|
-
isTrustDecisionAllowed
|
|
14934
|
-
} from "@wrongstack/core/security";
|
|
14935
|
-
async function authorizeWebUIAction(boundary, action, logger) {
|
|
14936
|
-
const request = {
|
|
14937
|
-
version: 1,
|
|
14938
|
-
requestId: randomUUID4(),
|
|
14939
|
-
actor: {
|
|
14940
|
-
kind: "remote-client",
|
|
14941
|
-
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
14942
|
-
},
|
|
14943
|
-
surface: "webui",
|
|
14944
|
-
capability: action.capability,
|
|
14945
|
-
subject: action.subject,
|
|
14946
|
-
risk: action.risk,
|
|
14947
|
-
scope: {
|
|
14948
|
-
...action.cwd ? { cwd: action.cwd } : {},
|
|
14949
|
-
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
14950
|
-
},
|
|
14951
|
-
authContext: { method: "session" },
|
|
14952
|
-
...action.metadata ? { metadata: action.metadata } : {}
|
|
14953
|
-
};
|
|
14954
|
-
const decision = await boundary.evaluate(request);
|
|
14955
|
-
logger?.debug?.(
|
|
14956
|
-
`[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
|
|
14957
|
-
);
|
|
14958
|
-
return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
|
|
14959
|
-
}
|
|
14960
|
-
|
|
14961
|
-
// src/server/process-handlers.ts
|
|
14962
15657
|
function handleProcessList(ws) {
|
|
14963
15658
|
try {
|
|
14964
15659
|
const procs = getProcessRegistry2().list();
|
|
@@ -15069,7 +15764,7 @@ import { makeProviderFromConfig } from "@wrongstack/providers";
|
|
|
15069
15764
|
import * as fs14 from "node:fs/promises";
|
|
15070
15765
|
import * as path19 from "node:path";
|
|
15071
15766
|
import { DefaultSessionStore } from "@wrongstack/core/storage";
|
|
15072
|
-
import { resolveWstackPaths as
|
|
15767
|
+
import { resolveWstackPaths as resolveWstackPaths5 } from "@wrongstack/core/utils";
|
|
15073
15768
|
function createProjectHandlers(ctx) {
|
|
15074
15769
|
const sendTo = (ws, message) => {
|
|
15075
15770
|
if (ctx.sendMessage) ctx.sendMessage(ws, message);
|
|
@@ -15187,7 +15882,7 @@ function createProjectHandlers(ctx) {
|
|
|
15187
15882
|
});
|
|
15188
15883
|
return;
|
|
15189
15884
|
}
|
|
15190
|
-
const paths =
|
|
15885
|
+
const paths = resolveWstackPaths5({
|
|
15191
15886
|
projectRoot: resolved,
|
|
15192
15887
|
globalRoot: ctx.wpaths.globalRoot
|
|
15193
15888
|
});
|
|
@@ -15198,7 +15893,7 @@ function createProjectHandlers(ctx) {
|
|
|
15198
15893
|
const previous = ctx.getSession();
|
|
15199
15894
|
const previousId = previous.id;
|
|
15200
15895
|
const previousProjectRoot = ctx.getProjectRoot();
|
|
15201
|
-
const previousPaths =
|
|
15896
|
+
const previousPaths = resolveWstackPaths5({
|
|
15202
15897
|
projectRoot: previousProjectRoot,
|
|
15203
15898
|
globalRoot: ctx.wpaths.globalRoot
|
|
15204
15899
|
});
|
|
@@ -15782,8 +16477,10 @@ function createProviderOperations(deps2) {
|
|
|
15782
16477
|
if (result.ok) {
|
|
15783
16478
|
deps2.log?.(`[WebUI] Provider "${payload.id}" added via provider.add`);
|
|
15784
16479
|
}
|
|
16480
|
+
return result.ok;
|
|
15785
16481
|
} catch (err) {
|
|
15786
16482
|
sendOperationResult(ws, false, errMessage(err));
|
|
16483
|
+
return false;
|
|
15787
16484
|
}
|
|
15788
16485
|
}
|
|
15789
16486
|
async function handleProviderRemove(ws, providerId) {
|
|
@@ -16167,6 +16864,7 @@ var CLIENT_CONVERSATION_MESSAGE_TYPES = [
|
|
|
16167
16864
|
"completion.request",
|
|
16168
16865
|
"model.switch",
|
|
16169
16866
|
"model.refine",
|
|
16867
|
+
"model.fallback_choice",
|
|
16170
16868
|
"autonomy.switch",
|
|
16171
16869
|
"context.clear",
|
|
16172
16870
|
"context.compact",
|
|
@@ -16184,6 +16882,7 @@ var CLIENT_CONVERSATION_MESSAGE_TYPES = [
|
|
|
16184
16882
|
"modes.list",
|
|
16185
16883
|
"session.checkpoints",
|
|
16186
16884
|
"session.delete",
|
|
16885
|
+
"session.inspect",
|
|
16187
16886
|
"session.new",
|
|
16188
16887
|
"session.rename",
|
|
16189
16888
|
"session.resume",
|
|
@@ -16437,6 +17136,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
|
|
|
16437
17136
|
"provider.active_blocked",
|
|
16438
17137
|
"provider.error",
|
|
16439
17138
|
"provider.fallback",
|
|
17139
|
+
"provider.fallback_pending",
|
|
16440
17140
|
"provider.response",
|
|
16441
17141
|
"provider.retry",
|
|
16442
17142
|
"provider.status_changed",
|
|
@@ -16447,6 +17147,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
|
|
|
16447
17147
|
"session.checkpoints",
|
|
16448
17148
|
"session.damaged",
|
|
16449
17149
|
"session.end",
|
|
17150
|
+
"session.inspect",
|
|
16450
17151
|
"session.rewound",
|
|
16451
17152
|
"session.start",
|
|
16452
17153
|
"session.stats",
|
|
@@ -16477,6 +17178,7 @@ var SERVER_COLLABORATION_MESSAGE_TYPES = [
|
|
|
16477
17178
|
"collab.state",
|
|
16478
17179
|
"mailbox.action_result",
|
|
16479
17180
|
"mailbox.agent_registered",
|
|
17181
|
+
"mailbox.agent_deregistered",
|
|
16480
17182
|
"mailbox.agents",
|
|
16481
17183
|
"mailbox.cleared",
|
|
16482
17184
|
"mailbox.compacted",
|
|
@@ -16856,6 +17558,13 @@ function projectToolMessage(message) {
|
|
|
16856
17558
|
}
|
|
16857
17559
|
return null;
|
|
16858
17560
|
}
|
|
17561
|
+
function optionalFinite(value) {
|
|
17562
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
17563
|
+
return value;
|
|
17564
|
+
}
|
|
17565
|
+
function optionalString2(value) {
|
|
17566
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
17567
|
+
}
|
|
16859
17568
|
function projectFleetMessage(message) {
|
|
16860
17569
|
const payload = record(message.payload);
|
|
16861
17570
|
if (!payload) return null;
|
|
@@ -16864,7 +17573,15 @@ function projectFleetMessage(message) {
|
|
|
16864
17573
|
return {
|
|
16865
17574
|
kind: "concurrency",
|
|
16866
17575
|
active: finite(payload["fleetConcurrency"]),
|
|
16867
|
-
maximum: finite(payload["fleetConcurrencyMax"])
|
|
17576
|
+
maximum: finite(payload["fleetConcurrencyMax"]),
|
|
17577
|
+
maxSpawns: optionalFinite(payload["maxSpawns"]),
|
|
17578
|
+
usedSpawns: optionalFinite(payload["usedSpawns"]),
|
|
17579
|
+
remainingSpawns: optionalFinite(payload["remainingSpawns"]),
|
|
17580
|
+
maxSpawnsSource: optionalString2(payload["maxSpawnsSource"]),
|
|
17581
|
+
maxConcurrentSource: optionalString2(payload["maxConcurrentSource"]),
|
|
17582
|
+
effectiveSource: optionalString2(payload["effectiveSource"]),
|
|
17583
|
+
checkpointMaxSpawns: optionalFinite(payload["checkpointMaxSpawns"]),
|
|
17584
|
+
ceilingMismatch: payload["ceilingMismatch"] === true ? true : void 0
|
|
16868
17585
|
};
|
|
16869
17586
|
case "client.status_update":
|
|
16870
17587
|
return { kind: "client-status", status: payload };
|
|
@@ -16981,6 +17698,271 @@ function negotiateProtocol(peer) {
|
|
|
16981
17698
|
}
|
|
16982
17699
|
|
|
16983
17700
|
// src/server/session-history.ts
|
|
17701
|
+
function blockText(block) {
|
|
17702
|
+
if (typeof block === "string") return block;
|
|
17703
|
+
switch (block.type) {
|
|
17704
|
+
case "text":
|
|
17705
|
+
return block.text;
|
|
17706
|
+
case "thinking":
|
|
17707
|
+
return block.thinking;
|
|
17708
|
+
case "tool_use":
|
|
17709
|
+
return `[tool:${block.name}]`;
|
|
17710
|
+
case "tool_result":
|
|
17711
|
+
return block.content;
|
|
17712
|
+
case "image":
|
|
17713
|
+
return "[image]";
|
|
17714
|
+
default: {
|
|
17715
|
+
const _exhaustive = block;
|
|
17716
|
+
void _exhaustive;
|
|
17717
|
+
return "[block]";
|
|
17718
|
+
}
|
|
17719
|
+
}
|
|
17720
|
+
}
|
|
17721
|
+
function labelForEvent(e) {
|
|
17722
|
+
switch (e.type) {
|
|
17723
|
+
case "session_start":
|
|
17724
|
+
return "Session started";
|
|
17725
|
+
case "session_resumed":
|
|
17726
|
+
return "Session resumed";
|
|
17727
|
+
case "session_forked":
|
|
17728
|
+
return `Forked from ${e.parentSessionId}`;
|
|
17729
|
+
case "user_input": {
|
|
17730
|
+
const content = typeof e.content === "string" ? e.content : Array.isArray(e.content) ? e.content.map(blockText).join("") : "[input]";
|
|
17731
|
+
return `User: ${content.length > 100 ? content.slice(0, 100) + "\u2026" : content}`;
|
|
17732
|
+
}
|
|
17733
|
+
case "llm_request":
|
|
17734
|
+
return "LLM request";
|
|
17735
|
+
case "llm_response":
|
|
17736
|
+
return "LLM response";
|
|
17737
|
+
case "tool_use":
|
|
17738
|
+
return `Tool: ${e.name}`;
|
|
17739
|
+
case "tool_call_start":
|
|
17740
|
+
return `Tool start: ${e.name}`;
|
|
17741
|
+
case "tool_call_end":
|
|
17742
|
+
return `Tool done: ${e.name} (${e.durationMs}ms, ${e.ok === false ? "error" : "ok"})`;
|
|
17743
|
+
case "tool_result":
|
|
17744
|
+
return `Tool result: ${e.id}${e.isError ? " (error)" : ""}`;
|
|
17745
|
+
case "tool_progress":
|
|
17746
|
+
return `Progress: ${e.name} \u2014 ${e.event.type}`;
|
|
17747
|
+
case "compaction":
|
|
17748
|
+
return `Compaction: ${e.before} \u2192 ${e.after} tokens`;
|
|
17749
|
+
case "context_snapshot":
|
|
17750
|
+
return "Context snapshot";
|
|
17751
|
+
case "error":
|
|
17752
|
+
return `Error: ${e.message.length > 100 ? e.message.slice(0, 100) + "\u2026" : e.message}`;
|
|
17753
|
+
case "session_end":
|
|
17754
|
+
return "Session ended";
|
|
17755
|
+
case "message_appended":
|
|
17756
|
+
return `Message appended: ${e.message.role}`;
|
|
17757
|
+
case "message_updated":
|
|
17758
|
+
return "Message updated";
|
|
17759
|
+
case "messages_replaced": {
|
|
17760
|
+
const count = e.messagesOmitted ?? e.messages.length;
|
|
17761
|
+
return `Messages replaced (${e.messagesOmitted ? "~" : ""}${count} msgs)`;
|
|
17762
|
+
}
|
|
17763
|
+
case "message_truncated":
|
|
17764
|
+
return `Message truncated: ${e.before} \u2192 ${e.after}`;
|
|
17765
|
+
case "file_event":
|
|
17766
|
+
return `File ${e.operation}: ${e.filePath} (${e.toolName})`;
|
|
17767
|
+
case "file_snapshot":
|
|
17768
|
+
return `File snapshot at prompt #${e.promptIndex}`;
|
|
17769
|
+
case "file_observation":
|
|
17770
|
+
return `File observation: ${e.path}`;
|
|
17771
|
+
case "rewound":
|
|
17772
|
+
return `Rewound to prompt #${e.toPromptIndex} (${e.revertedFiles.length} files)`;
|
|
17773
|
+
case "mode_changed":
|
|
17774
|
+
return `Mode: ${e.from} \u2192 ${e.to}`;
|
|
17775
|
+
case "task_created":
|
|
17776
|
+
return `Task created: ${e.title}`;
|
|
17777
|
+
case "task_updated":
|
|
17778
|
+
return `Task ${e.taskId}: ${e.status}`;
|
|
17779
|
+
case "task_completed":
|
|
17780
|
+
return `Task done: ${e.title}`;
|
|
17781
|
+
case "task_failed":
|
|
17782
|
+
return `Task failed: ${e.title} \u2014 ${e.error.length > 80 ? e.error.slice(0, 80) + "\u2026" : e.error}`;
|
|
17783
|
+
case "agent_spawned":
|
|
17784
|
+
return `Agent spawned: ${e.role}`;
|
|
17785
|
+
case "agent_stopped":
|
|
17786
|
+
return "Agent stopped";
|
|
17787
|
+
case "agent_error":
|
|
17788
|
+
return `Agent error: ${e.error.length > 80 ? e.error.slice(0, 80) + "\u2026" : e.error}`;
|
|
17789
|
+
case "provider_retry":
|
|
17790
|
+
return `Retry: ${e.description} (attempt ${e.attempt})`;
|
|
17791
|
+
case "provider_error":
|
|
17792
|
+
return `Provider error: ${e.description}`;
|
|
17793
|
+
case "checkpoint":
|
|
17794
|
+
return `Checkpoint at prompt #${e.promptIndex}`;
|
|
17795
|
+
case "in_flight_start":
|
|
17796
|
+
return `In-flight started: ${e.context}`;
|
|
17797
|
+
case "in_flight_end":
|
|
17798
|
+
return `In-flight ended: ${e.reason}`;
|
|
17799
|
+
case "side_effect":
|
|
17800
|
+
return `Side effect: ${e.toolName} (${e.risk})`;
|
|
17801
|
+
case "spec_parsed":
|
|
17802
|
+
return `Spec parsed: ${e.title}`;
|
|
17803
|
+
case "spec_analyzed":
|
|
17804
|
+
return `Spec analyzed: ${e.specId}`;
|
|
17805
|
+
case "skill_activated":
|
|
17806
|
+
return `Skill: ${e.skillName}`;
|
|
17807
|
+
case "skill_deactivated":
|
|
17808
|
+
return `Skill done: ${e.skillName}`;
|
|
17809
|
+
default: {
|
|
17810
|
+
const _exhaustive = e;
|
|
17811
|
+
void _exhaustive;
|
|
17812
|
+
return String(_exhaustive);
|
|
17813
|
+
}
|
|
17814
|
+
}
|
|
17815
|
+
}
|
|
17816
|
+
function detailForEvent(e) {
|
|
17817
|
+
switch (e.type) {
|
|
17818
|
+
case "session_start":
|
|
17819
|
+
return `${e.model} @ ${e.provider}`;
|
|
17820
|
+
case "session_resumed":
|
|
17821
|
+
return `${e.model} @ ${e.provider}`;
|
|
17822
|
+
case "session_forked":
|
|
17823
|
+
return `parent checkpoint: ${e.parentCheckpointHash.slice(0, 12)}\u2026`;
|
|
17824
|
+
case "llm_request":
|
|
17825
|
+
return `${e.model} \xB7 ${e.messageCount} msgs \xB7 ${e.toolCount ?? "?"} tools`;
|
|
17826
|
+
case "llm_response":
|
|
17827
|
+
return `${e.stopReason} \xB7 ${e.usage.input ?? 0}+${e.usage.output ?? 0} tokens`;
|
|
17828
|
+
case "tool_use":
|
|
17829
|
+
return `id: ${e.id}`;
|
|
17830
|
+
case "tool_call_start":
|
|
17831
|
+
return `id: ${e.id}`;
|
|
17832
|
+
case "tool_call_end":
|
|
17833
|
+
return `${(e.outputBytes ?? e.outputSize ?? 0).toLocaleString()} B \xB7 ${e.outputLines ?? 0} lines`;
|
|
17834
|
+
case "tool_progress":
|
|
17835
|
+
return `${e.event.type}${e.event.text ? `: ${e.event.text}` : ""}`;
|
|
17836
|
+
case "compaction":
|
|
17837
|
+
return `saved ~${Math.max(0, e.before - e.after)} tokens`;
|
|
17838
|
+
case "context_snapshot":
|
|
17839
|
+
return `${e.messages.length} msgs${e.messagesOmitted ? ` (${e.messagesOmitted} omitted)` : ""}`;
|
|
17840
|
+
case "error":
|
|
17841
|
+
return `phase: ${e.phase}`;
|
|
17842
|
+
case "session_end":
|
|
17843
|
+
return `${e.usage.input ?? 0}+${e.usage.output ?? 0} total tokens`;
|
|
17844
|
+
case "message_appended":
|
|
17845
|
+
return `appended ${e.message.role}`;
|
|
17846
|
+
case "message_updated":
|
|
17847
|
+
return `at index ${e.index}`;
|
|
17848
|
+
case "messages_replaced":
|
|
17849
|
+
return `${e.messagesOmitted ?? e.messages.length} total`;
|
|
17850
|
+
case "message_truncated":
|
|
17851
|
+
return `truncated to ${e.after} tokens`;
|
|
17852
|
+
case "mode_changed":
|
|
17853
|
+
return `${e.from} \u2192 ${e.to}`;
|
|
17854
|
+
case "task_created":
|
|
17855
|
+
return `id: ${e.taskId}`;
|
|
17856
|
+
case "task_updated":
|
|
17857
|
+
return `${e.taskId}: ${e.status}`;
|
|
17858
|
+
case "task_completed":
|
|
17859
|
+
return `${e.taskId}`;
|
|
17860
|
+
case "task_failed":
|
|
17861
|
+
return `${e.taskId}: ${e.error}`;
|
|
17862
|
+
case "agent_spawned":
|
|
17863
|
+
return `id: ${e.agentId} (${e.role})`;
|
|
17864
|
+
case "agent_stopped":
|
|
17865
|
+
return `id: ${e.agentId}`;
|
|
17866
|
+
case "agent_error":
|
|
17867
|
+
return `id: ${e.agentId}: ${e.error}`;
|
|
17868
|
+
case "file_event":
|
|
17869
|
+
return `${e.filePath} (${e.toolName})`;
|
|
17870
|
+
case "file_snapshot":
|
|
17871
|
+
return `${e.files.length} files at prompt #${e.promptIndex}`;
|
|
17872
|
+
case "file_observation":
|
|
17873
|
+
return `${e.source === "user" ? "user-saved" : "tool-written"} at ${e.path}`;
|
|
17874
|
+
case "rewound":
|
|
17875
|
+
return `${e.revertedFiles.length} files reverted`;
|
|
17876
|
+
case "in_flight_start":
|
|
17877
|
+
return e.context;
|
|
17878
|
+
case "in_flight_end":
|
|
17879
|
+
return `${e.reason}`;
|
|
17880
|
+
case "side_effect":
|
|
17881
|
+
return `${e.toolName} (${e.risk})${e.outcome ? `: ${e.outcome}` : ""}`;
|
|
17882
|
+
case "provider_retry":
|
|
17883
|
+
return `delay ${e.delayMs}ms${e.status ? ` \xB7 HTTP ${e.status}` : ""}`;
|
|
17884
|
+
case "provider_error":
|
|
17885
|
+
return `${e.retryable ? "retryable" : "fatal"}${e.status ? ` \xB7 HTTP ${e.status}` : ""}`;
|
|
17886
|
+
case "user_input":
|
|
17887
|
+
return typeof e.content === "string" ? `${e.content.length} chars` : `${e.content.length} blocks`;
|
|
17888
|
+
case "tool_result":
|
|
17889
|
+
return `${e.isError ? "error" : "ok"}`;
|
|
17890
|
+
case "checkpoint":
|
|
17891
|
+
return `prompt #${e.promptIndex}`;
|
|
17892
|
+
case "spec_parsed":
|
|
17893
|
+
return `${e.title} (${e.completeness}% complete)`;
|
|
17894
|
+
case "spec_analyzed":
|
|
17895
|
+
return `${e.gaps.length} gaps identified`;
|
|
17896
|
+
case "skill_activated":
|
|
17897
|
+
return `at ${e.skillName}`;
|
|
17898
|
+
case "skill_deactivated":
|
|
17899
|
+
return `at ${e.skillName}`;
|
|
17900
|
+
default: {
|
|
17901
|
+
const _exhaustive = e;
|
|
17902
|
+
void _exhaustive;
|
|
17903
|
+
return "";
|
|
17904
|
+
}
|
|
17905
|
+
}
|
|
17906
|
+
}
|
|
17907
|
+
function buildInspectPayload(summary, events, fallback) {
|
|
17908
|
+
const inspectEvents = events.map((e) => ({
|
|
17909
|
+
ts: e.ts,
|
|
17910
|
+
type: e.type,
|
|
17911
|
+
label: labelForEvent(e),
|
|
17912
|
+
detail: detailForEvent(e)
|
|
17913
|
+
}));
|
|
17914
|
+
const fileEvents = [];
|
|
17915
|
+
let computedToolCallCount = 0;
|
|
17916
|
+
let computedToolErrorCount = 0;
|
|
17917
|
+
let computedFileChangeCount = 0;
|
|
17918
|
+
let computedCompactionCount = 0;
|
|
17919
|
+
let computedMessageCount = 0;
|
|
17920
|
+
let computedIterationCount = 0;
|
|
17921
|
+
const computedToolBreakdown = {};
|
|
17922
|
+
for (const e of events) {
|
|
17923
|
+
if (e.type === "file_event") {
|
|
17924
|
+
fileEvents.push({
|
|
17925
|
+
operation: e.operation,
|
|
17926
|
+
filePath: e.filePath,
|
|
17927
|
+
toolName: e.toolName,
|
|
17928
|
+
ts: e.ts
|
|
17929
|
+
});
|
|
17930
|
+
computedFileChangeCount++;
|
|
17931
|
+
} else if (e.type === "tool_call_end") {
|
|
17932
|
+
computedToolCallCount++;
|
|
17933
|
+
if (e.ok === false) computedToolErrorCount++;
|
|
17934
|
+
computedToolBreakdown[e.name] = (computedToolBreakdown[e.name] ?? 0) + 1;
|
|
17935
|
+
} else if (e.type === "compaction") {
|
|
17936
|
+
computedCompactionCount++;
|
|
17937
|
+
} else if (e.type === "user_input") {
|
|
17938
|
+
computedMessageCount++;
|
|
17939
|
+
} else if (e.type === "llm_response") {
|
|
17940
|
+
computedIterationCount++;
|
|
17941
|
+
}
|
|
17942
|
+
}
|
|
17943
|
+
const s = summary;
|
|
17944
|
+
return {
|
|
17945
|
+
id: s?.id ?? fallback.id,
|
|
17946
|
+
title: s?.title ?? fallback.title,
|
|
17947
|
+
...s?.name !== void 0 ? { name: s.name } : {},
|
|
17948
|
+
model: s?.model ?? fallback.model,
|
|
17949
|
+
provider: s?.provider ?? fallback.provider,
|
|
17950
|
+
startedAt: s?.startedAt ?? fallback.startedAt,
|
|
17951
|
+
...(s?.endedAt ?? fallback.endedAt) !== void 0 ? { endedAt: s?.endedAt ?? fallback.endedAt } : {},
|
|
17952
|
+
tokenTotal: s?.tokenTotal ?? 0,
|
|
17953
|
+
...s?.outcome !== void 0 ? { outcome: s.outcome } : {},
|
|
17954
|
+
messageCount: s?.messageCount ?? computedMessageCount,
|
|
17955
|
+
iterationCount: s?.iterationCount ?? computedIterationCount,
|
|
17956
|
+
toolCallCount: s?.toolCallCount ?? computedToolCallCount,
|
|
17957
|
+
toolErrorCount: s?.toolErrorCount ?? computedToolErrorCount,
|
|
17958
|
+
fileChangeCount: s?.fileChangeCount ?? computedFileChangeCount,
|
|
17959
|
+
compactionCount: s?.compactionCount ?? computedCompactionCount,
|
|
17960
|
+
toolBreakdown: s?.toolBreakdown ?? computedToolBreakdown,
|
|
17961
|
+
events: inspectEvents,
|
|
17962
|
+
fileEvents,
|
|
17963
|
+
...s?.lastUserMessage !== void 0 ? { lastUserMessage: s.lastUserMessage } : {}
|
|
17964
|
+
};
|
|
17965
|
+
}
|
|
16984
17966
|
function toSessionHistoryEntry(summary, currentSessionId2) {
|
|
16985
17967
|
return {
|
|
16986
17968
|
id: summary.id,
|
|
@@ -17255,6 +18237,7 @@ function createSessionHandlers(ctx) {
|
|
|
17255
18237
|
tools: ctx.listTools?.() ?? ctx.toolRegistry?.list(),
|
|
17256
18238
|
baseRevision: typeof payload["baseRevision"] === "string" ? payload["baseRevision"] : "",
|
|
17257
18239
|
messages: payload["messages"],
|
|
18240
|
+
removals: payload["removals"],
|
|
17258
18241
|
allowRepair: payload["allowRepair"] === true,
|
|
17259
18242
|
runActive: ctx.isRunActive?.() === true
|
|
17260
18243
|
});
|
|
@@ -17271,6 +18254,7 @@ function createSessionHandlers(ctx) {
|
|
|
17271
18254
|
tools: ctx.listTools?.() ?? ctx.toolRegistry?.list(),
|
|
17272
18255
|
baseRevision: typeof payload["baseRevision"] === "string" ? payload["baseRevision"] : "",
|
|
17273
18256
|
messages: payload["messages"],
|
|
18257
|
+
removals: payload["removals"],
|
|
17274
18258
|
allowRepair: payload["allowRepair"] === true,
|
|
17275
18259
|
runActive: ctx.isRunActive?.() === true
|
|
17276
18260
|
});
|
|
@@ -17527,6 +18511,47 @@ function createSessionHandlers(ctx) {
|
|
|
17527
18511
|
if (!ensureCurrentSession(ws, msg, "session.save")) return;
|
|
17528
18512
|
result(ws, true, `Session ${ctx.getSession().id} is auto-saved`);
|
|
17529
18513
|
},
|
|
18514
|
+
inspectSession: async (ws, msg) => {
|
|
18515
|
+
const { id } = msg.payload;
|
|
18516
|
+
if (!id) {
|
|
18517
|
+
sendTo(ws, {
|
|
18518
|
+
type: "session.inspect",
|
|
18519
|
+
payload: { id: "", error: "Session id is required" }
|
|
18520
|
+
});
|
|
18521
|
+
return;
|
|
18522
|
+
}
|
|
18523
|
+
try {
|
|
18524
|
+
const store = ctx.getSessionStore();
|
|
18525
|
+
const data = await store.load(id);
|
|
18526
|
+
let summary;
|
|
18527
|
+
try {
|
|
18528
|
+
const summaries = await store.list(200);
|
|
18529
|
+
summary = summaries.find((s) => s.id === id);
|
|
18530
|
+
} catch {
|
|
18531
|
+
summary = void 0;
|
|
18532
|
+
}
|
|
18533
|
+
const payload = buildInspectPayload(summary, data.events, {
|
|
18534
|
+
id: data.metadata.id,
|
|
18535
|
+
title: data.metadata.title ?? "",
|
|
18536
|
+
model: data.metadata.model ?? "",
|
|
18537
|
+
provider: data.metadata.provider ?? "",
|
|
18538
|
+
startedAt: data.metadata.startedAt,
|
|
18539
|
+
endedAt: data.metadata.endedAt
|
|
18540
|
+
});
|
|
18541
|
+
sendTo(ws, {
|
|
18542
|
+
type: "session.inspect",
|
|
18543
|
+
payload
|
|
18544
|
+
});
|
|
18545
|
+
} catch (err) {
|
|
18546
|
+
sendTo(ws, {
|
|
18547
|
+
type: "session.inspect",
|
|
18548
|
+
payload: {
|
|
18549
|
+
id,
|
|
18550
|
+
error: err instanceof Error ? err.message : String(err)
|
|
18551
|
+
}
|
|
18552
|
+
});
|
|
18553
|
+
}
|
|
18554
|
+
},
|
|
17530
18555
|
listCheckpoints: async (ws, msg) => {
|
|
17531
18556
|
if (!ensureCurrentSession(ws, msg, "session.checkpoints")) return;
|
|
17532
18557
|
try {
|
|
@@ -18211,6 +19236,19 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
|
|
|
18211
19236
|
return true;
|
|
18212
19237
|
}
|
|
18213
19238
|
|
|
19239
|
+
// src/server/fallback-choice.ts
|
|
19240
|
+
function emitFallbackChoice(events, msg) {
|
|
19241
|
+
const parsed = validateModelFallbackChoicePayload(msg.payload);
|
|
19242
|
+
if (!parsed.ok) return parsed;
|
|
19243
|
+
events?.emit("provider.fallback_choice", {
|
|
19244
|
+
requestId: parsed.value.requestId,
|
|
19245
|
+
...parsed.value.providerId ? { providerId: parsed.value.providerId } : {},
|
|
19246
|
+
...parsed.value.model ? { model: parsed.value.model } : {},
|
|
19247
|
+
...parsed.value.autoSwitch ? { autoSwitch: true } : {}
|
|
19248
|
+
});
|
|
19249
|
+
return { ok: true };
|
|
19250
|
+
}
|
|
19251
|
+
|
|
18214
19252
|
// src/server/agent-roster-routes.ts
|
|
18215
19253
|
async function handleAgentRosterRoute(ws, msg, handlers) {
|
|
18216
19254
|
if (!msg.type.startsWith("agent-roster.")) return false;
|
|
@@ -18375,13 +19413,17 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
18375
19413
|
case "model.refine":
|
|
18376
19414
|
await routes.refineModel(ws, msg);
|
|
18377
19415
|
return true;
|
|
19416
|
+
case "model.fallback_choice":
|
|
19417
|
+
await routes.fallbackChoice(ws, msg);
|
|
19418
|
+
return true;
|
|
18378
19419
|
case "key.add":
|
|
18379
19420
|
case "key.update": {
|
|
18380
19421
|
const payload = asPayloadRecord(msg);
|
|
18381
19422
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
18382
19423
|
const label = payload ? requiredString(payload, "label") : null;
|
|
18383
19424
|
const apiKey = payload ? requiredString(payload, "apiKey") : null;
|
|
18384
|
-
if (!providerId || !label || !apiKey)
|
|
19425
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label || !apiKey)
|
|
19426
|
+
return invalidPayload(ws, msg.type);
|
|
18385
19427
|
await routes.providerHandlers.handleKeyUpsert(ws, providerId, label, apiKey);
|
|
18386
19428
|
return true;
|
|
18387
19429
|
}
|
|
@@ -18389,7 +19431,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
18389
19431
|
const payload = asPayloadRecord(msg);
|
|
18390
19432
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
18391
19433
|
const label = payload ? requiredString(payload, "label") : null;
|
|
18392
|
-
if (!providerId || !
|
|
19434
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label)
|
|
19435
|
+
return invalidPayload(ws, msg.type);
|
|
18393
19436
|
await routes.providerHandlers.handleKeyDelete(ws, providerId, label);
|
|
18394
19437
|
return true;
|
|
18395
19438
|
}
|
|
@@ -18397,7 +19440,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
18397
19440
|
const payload = asPayloadRecord(msg);
|
|
18398
19441
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
18399
19442
|
const label = payload ? requiredString(payload, "label") : null;
|
|
18400
|
-
if (!providerId || !
|
|
19443
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label)
|
|
19444
|
+
return invalidPayload(ws, msg.type);
|
|
18401
19445
|
await routes.providerHandlers.handleKeySetActive(ws, providerId, label);
|
|
18402
19446
|
return true;
|
|
18403
19447
|
}
|
|
@@ -18409,11 +19453,11 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
18409
19453
|
const apiKey = payload?.["apiKey"];
|
|
18410
19454
|
const models = payload ? optionalStringArray(payload, "models") : null;
|
|
18411
19455
|
const customModels = payload ? optionalCustomModels(payload) : null;
|
|
18412
|
-
if (!id || !family) return invalidPayload(ws, msg.type);
|
|
19456
|
+
if (!id || !SAFE_CONFIG_KEY.test(id) || !family) return invalidPayload(ws, msg.type);
|
|
18413
19457
|
if (baseUrl !== void 0 && typeof baseUrl !== "string") return invalidPayload(ws, msg.type);
|
|
18414
19458
|
if (apiKey !== void 0 && typeof apiKey !== "string") return invalidPayload(ws, msg.type);
|
|
18415
19459
|
if (models === null || customModels === null) return invalidPayload(ws, msg.type);
|
|
18416
|
-
await routes.providerHandlers.handleProviderAdd(ws, {
|
|
19460
|
+
const added = await routes.providerHandlers.handleProviderAdd(ws, {
|
|
18417
19461
|
id,
|
|
18418
19462
|
family,
|
|
18419
19463
|
baseUrl,
|
|
@@ -18421,20 +19465,22 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
18421
19465
|
models,
|
|
18422
19466
|
customModels
|
|
18423
19467
|
});
|
|
18424
|
-
|
|
19468
|
+
if (added) {
|
|
19469
|
+
void routes.adoptDefaultProviderIfUnset(id).catch(() => void 0);
|
|
19470
|
+
}
|
|
18425
19471
|
return true;
|
|
18426
19472
|
}
|
|
18427
19473
|
case "provider.remove": {
|
|
18428
19474
|
const payload = asPayloadRecord(msg);
|
|
18429
19475
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
18430
|
-
if (!providerId) return invalidPayload(ws, msg.type);
|
|
19476
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId)) return invalidPayload(ws, msg.type);
|
|
18431
19477
|
await routes.providerHandlers.handleProviderRemove(ws, providerId);
|
|
18432
19478
|
return true;
|
|
18433
19479
|
}
|
|
18434
19480
|
case "provider.clear_models": {
|
|
18435
19481
|
const payload = asPayloadRecord(msg);
|
|
18436
19482
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
18437
|
-
if (!providerId) return invalidPayload(ws, msg.type);
|
|
19483
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId)) return invalidPayload(ws, msg.type);
|
|
18438
19484
|
await routes.providerHandlers.handleProviderClearModels(ws, providerId);
|
|
18439
19485
|
return true;
|
|
18440
19486
|
}
|
|
@@ -18448,7 +19494,7 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
18448
19494
|
const customModelRaw = payload["customModel"];
|
|
18449
19495
|
if (!isRecord4(customModelRaw)) return invalidPayload(ws, msg.type);
|
|
18450
19496
|
const cm = optionalCustomModels({ customModels: { [modelId]: customModelRaw } });
|
|
18451
|
-
if (!cm
|
|
19497
|
+
if (!cm?.[modelId]) return invalidPayload(ws, msg.type);
|
|
18452
19498
|
await routes.providerHandlers.handleCustomModelSet(ws, providerId, modelId, cm[modelId]);
|
|
18453
19499
|
return true;
|
|
18454
19500
|
}
|
|
@@ -18466,7 +19512,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
18466
19512
|
const payload = asPayloadRecord(msg);
|
|
18467
19513
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
18468
19514
|
const previousModels = payload ? optionalStringArray(payload, "previousModels") : null;
|
|
18469
|
-
if (!providerId || !
|
|
19515
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !previousModels)
|
|
19516
|
+
return invalidPayload(ws, msg.type);
|
|
18470
19517
|
await routes.providerHandlers.handleProviderUndoClear(ws, providerId, previousModels);
|
|
18471
19518
|
return true;
|
|
18472
19519
|
}
|
|
@@ -18476,7 +19523,7 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
18476
19523
|
const envVars = payload ? optionalStringArray(payload, "envVars") : null;
|
|
18477
19524
|
const models = payload ? optionalStringArray(payload, "models") : null;
|
|
18478
19525
|
const customModels = payload ? optionalCustomModels(payload) : null;
|
|
18479
|
-
if (!payload || !id || envVars === null || models === null || customModels === null)
|
|
19526
|
+
if (!payload || !id || !SAFE_CONFIG_KEY.test(id) || envVars === null || models === null || customModels === null)
|
|
18480
19527
|
return invalidPayload(ws, msg.type);
|
|
18481
19528
|
for (const key of ["family", "baseUrl"]) {
|
|
18482
19529
|
if (payload[key] !== void 0 && typeof payload[key] !== "string")
|
|
@@ -18496,7 +19543,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
18496
19543
|
const payload = asPayloadRecord(msg);
|
|
18497
19544
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
18498
19545
|
const timeoutMs = payload ? optionalNumber(payload, "timeoutMs") : null;
|
|
18499
|
-
if (!providerId || timeoutMs === null)
|
|
19546
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || timeoutMs === null)
|
|
19547
|
+
return invalidPayload(ws, msg.type);
|
|
18500
19548
|
await routes.providerHandlers.handleProviderProbe(ws, providerId, timeoutMs);
|
|
18501
19549
|
return true;
|
|
18502
19550
|
}
|
|
@@ -18505,7 +19553,7 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
18505
19553
|
const kind = oauthKind(payload);
|
|
18506
19554
|
const providerId = payload?.["providerId"];
|
|
18507
19555
|
if (!kind) return invalidPayload(ws, msg.type);
|
|
18508
|
-
if (providerId !== void 0 && typeof providerId !== "string") {
|
|
19556
|
+
if (providerId !== void 0 && (typeof providerId !== "string" || !SAFE_CONFIG_KEY.test(providerId))) {
|
|
18509
19557
|
return invalidPayload(ws, msg.type);
|
|
18510
19558
|
}
|
|
18511
19559
|
await routes.providerHandlers.handleOAuthStart(ws, kind, providerId);
|
|
@@ -18544,7 +19592,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
18544
19592
|
const payload = asPayloadRecord(msg);
|
|
18545
19593
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
18546
19594
|
const model = payload ? requiredString(payload, "model") : null;
|
|
18547
|
-
if (!providerId || !
|
|
19595
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !model)
|
|
19596
|
+
return invalidPayload(ws, msg.type);
|
|
18548
19597
|
const released = routes.statusTracker.retryNow(providerId, model);
|
|
18549
19598
|
sendResult2(
|
|
18550
19599
|
ws,
|
|
@@ -18561,7 +19610,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
18561
19610
|
const payload = asPayloadRecord(msg);
|
|
18562
19611
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
18563
19612
|
const model = payload ? requiredString(payload, "model") : null;
|
|
18564
|
-
if (!providerId || !
|
|
19613
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !model)
|
|
19614
|
+
return invalidPayload(ws, msg.type);
|
|
18565
19615
|
routes.statusTracker.clear(providerId, model);
|
|
18566
19616
|
sendResult2(ws, true, `Cleared tracking for ${providerId}/${model}.`);
|
|
18567
19617
|
return true;
|
|
@@ -18642,6 +19692,9 @@ async function handleSessionRoute(ws, msg, handlers) {
|
|
|
18642
19692
|
case "session.save":
|
|
18643
19693
|
await handlers.saveSession(ws, msg);
|
|
18644
19694
|
return true;
|
|
19695
|
+
case "session.inspect":
|
|
19696
|
+
await handlers.inspectSession(ws, msg);
|
|
19697
|
+
return true;
|
|
18645
19698
|
case "session.checkpoints":
|
|
18646
19699
|
await handlers.listCheckpoints(ws, msg);
|
|
18647
19700
|
return true;
|
|
@@ -18916,8 +19969,10 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
18916
19969
|
};
|
|
18917
19970
|
const mcp = {
|
|
18918
19971
|
list: (ws, msg) => handleMcpList(ws, msg, opts.profileConfigPath, opts.mcpRegistry),
|
|
18919
|
-
add
|
|
18920
|
-
|
|
19972
|
+
// add/update are the spawn-capable pair — they take a `command`/`args`
|
|
19973
|
+
// from the wire and start it. They go past the trust boundary (M1).
|
|
19974
|
+
add: (ws, msg) => handleMcpAdd(ws, msg, opts.profileConfigPath, opts.mcpRegistry, deps2.trustBoundary),
|
|
19975
|
+
update: (ws, msg) => handleMcpUpdate(ws, msg, opts.profileConfigPath, opts.mcpRegistry, deps2.trustBoundary),
|
|
18921
19976
|
remove: (ws, msg) => handleMcpRemove(ws, msg, opts.profileConfigPath, opts.mcpRegistry),
|
|
18922
19977
|
enable: (ws, msg) => handleMcpEnable(ws, msg, opts.profileConfigPath, opts.mcpRegistry),
|
|
18923
19978
|
disable: (ws, msg) => handleMcpDisable(ws, msg, opts.profileConfigPath, opts.mcpRegistry),
|
|
@@ -19004,6 +20059,15 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
19004
20059
|
searchProviderModels: (ws, query, limit) => providerOperations.handleProviderModelsSearch(ws, query, limit),
|
|
19005
20060
|
switchModel: (ws, msg) => modelOperations.switchModel(ws, msg.payload),
|
|
19006
20061
|
refineModel: (ws, msg) => modelOperations.refineModel(ws, msg.payload),
|
|
20062
|
+
fallbackChoice: async (ws, msg) => {
|
|
20063
|
+
const result = emitFallbackChoice(deps2.sessionCtx.opts.events, msg);
|
|
20064
|
+
if (!result.ok) {
|
|
20065
|
+
send2(ws, {
|
|
20066
|
+
type: "error",
|
|
20067
|
+
payload: { phase: "invalid_request", message: result.message }
|
|
20068
|
+
});
|
|
20069
|
+
}
|
|
20070
|
+
},
|
|
19007
20071
|
adoptDefaultProviderIfUnset: providerOperations.adoptDefaultProviderIfUnset,
|
|
19008
20072
|
providerHandlers: providerOperations,
|
|
19009
20073
|
statusTracker: deps2.statusTracker
|
|
@@ -19297,17 +20361,12 @@ function createProviderStore(deps2) {
|
|
|
19297
20361
|
cfg.activeKey = active.label;
|
|
19298
20362
|
}
|
|
19299
20363
|
}
|
|
19300
|
-
function maskedKey2(key) {
|
|
19301
|
-
if (!key) return "\u2014";
|
|
19302
|
-
if (key.length <= 8) return "\u2022".repeat(key.length);
|
|
19303
|
-
return `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
|
|
19304
|
-
}
|
|
19305
20364
|
return {
|
|
19306
20365
|
load: loadSavedProviders2,
|
|
19307
20366
|
save: saveProviders2,
|
|
19308
20367
|
normalizeKeys: normalizeKeys2,
|
|
19309
20368
|
writeKeysBack: writeKeysBack2,
|
|
19310
|
-
maskedKey
|
|
20369
|
+
maskedKey
|
|
19311
20370
|
};
|
|
19312
20371
|
}
|
|
19313
20372
|
|
|
@@ -20974,7 +22033,8 @@ function setupEvents(deps2) {
|
|
|
20974
22033
|
attempt: e.attempt,
|
|
20975
22034
|
delayMs: e.delayMs,
|
|
20976
22035
|
status: e.status,
|
|
20977
|
-
description: e.description
|
|
22036
|
+
description: e.description,
|
|
22037
|
+
...e.errorBody ? { errorBody: e.errorBody } : {}
|
|
20978
22038
|
})
|
|
20979
22039
|
});
|
|
20980
22040
|
appendForCurrentSession(e.sessionId, {
|
|
@@ -20984,7 +22044,8 @@ function setupEvents(deps2) {
|
|
|
20984
22044
|
attempt: e.attempt,
|
|
20985
22045
|
delayMs: e.delayMs,
|
|
20986
22046
|
status: e.status,
|
|
20987
|
-
description: e.description
|
|
22047
|
+
description: e.description,
|
|
22048
|
+
...e.errorBody ? { errorBody: e.errorBody } : {}
|
|
20988
22049
|
});
|
|
20989
22050
|
});
|
|
20990
22051
|
on("provider.status_changed", (e) => {
|
|
@@ -21024,7 +22085,8 @@ function setupEvents(deps2) {
|
|
|
21024
22085
|
providerId: e.providerId,
|
|
21025
22086
|
status: e.status,
|
|
21026
22087
|
description: e.description,
|
|
21027
|
-
retryable: e.retryable
|
|
22088
|
+
retryable: e.retryable,
|
|
22089
|
+
...e.errorBody ? { errorBody: e.errorBody } : {}
|
|
21028
22090
|
})
|
|
21029
22091
|
});
|
|
21030
22092
|
appendForCurrentSession(e.sessionId, {
|
|
@@ -21033,7 +22095,8 @@ function setupEvents(deps2) {
|
|
|
21033
22095
|
providerId: e.providerId,
|
|
21034
22096
|
status: e.status,
|
|
21035
22097
|
description: e.description,
|
|
21036
|
-
retryable: e.retryable
|
|
22098
|
+
retryable: e.retryable,
|
|
22099
|
+
...e.errorBody ? { errorBody: e.errorBody } : {}
|
|
21037
22100
|
});
|
|
21038
22101
|
});
|
|
21039
22102
|
on("provider.fallback", (e) => {
|
|
@@ -21044,7 +22107,22 @@ function setupEvents(deps2) {
|
|
|
21044
22107
|
from: e.from,
|
|
21045
22108
|
to: e.to,
|
|
21046
22109
|
status: e.status,
|
|
21047
|
-
providerSwitched: e.providerSwitched
|
|
22110
|
+
providerSwitched: e.providerSwitched,
|
|
22111
|
+
...e.requestId ? { requestId: e.requestId } : {}
|
|
22112
|
+
})
|
|
22113
|
+
});
|
|
22114
|
+
});
|
|
22115
|
+
on("provider.fallback_pending", (e) => {
|
|
22116
|
+
broadcast2(clients, {
|
|
22117
|
+
type: "provider.fallback_pending",
|
|
22118
|
+
payload: sessionPayload2({
|
|
22119
|
+
sessionId: e.sessionId,
|
|
22120
|
+
from: e.from,
|
|
22121
|
+
status: e.status,
|
|
22122
|
+
candidates: e.candidates,
|
|
22123
|
+
autoSwitchSeconds: e.autoSwitchSeconds,
|
|
22124
|
+
requestId: e.requestId,
|
|
22125
|
+
timestamp: e.timestamp
|
|
21048
22126
|
})
|
|
21049
22127
|
});
|
|
21050
22128
|
});
|
|
@@ -21129,6 +22207,15 @@ function setupEvents(deps2) {
|
|
|
21129
22207
|
type: "mailbox.agent_registered",
|
|
21130
22208
|
payload
|
|
21131
22209
|
});
|
|
22210
|
+
}),
|
|
22211
|
+
// Deregistration (subagent retirement) must reach the browser too —
|
|
22212
|
+
// otherwise dead agents linger in the client roster until an unrelated
|
|
22213
|
+
// refresh. Emitted by sqlite-mailbox.deregisterAgent with { agentId }.
|
|
22214
|
+
events.onPattern("mailbox.agent_deregistered", (_e, payload) => {
|
|
22215
|
+
broadcast2(clients, {
|
|
22216
|
+
type: "mailbox.agent_deregistered",
|
|
22217
|
+
payload
|
|
22218
|
+
});
|
|
21132
22219
|
})
|
|
21133
22220
|
);
|
|
21134
22221
|
const forwardSubagent = (kind, payload) => broadcast2(clients, { type: "subagent.event", payload: sessionPayload2({ kind, ...payload }) });
|
|
@@ -23413,29 +24500,10 @@ import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/too
|
|
|
23413
24500
|
// src/server/model-auto-discovery.ts
|
|
23414
24501
|
import * as fs21 from "node:fs/promises";
|
|
23415
24502
|
import * as path29 from "node:path";
|
|
23416
|
-
import {
|
|
24503
|
+
import { discoverOpenAICompatibleModels, resolveDiscoveryTargets } from "@wrongstack/providers";
|
|
23417
24504
|
function isOverlayRegistry(value) {
|
|
23418
24505
|
return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
|
|
23419
24506
|
}
|
|
23420
|
-
function resolveKey(cfg) {
|
|
23421
|
-
if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
|
|
23422
|
-
const active = cfg.activeKey ? cfg.apiKeys.find((key) => key.label === cfg.activeKey) : void 0;
|
|
23423
|
-
return (active ?? cfg.apiKeys[0])?.apiKey;
|
|
23424
|
-
}
|
|
23425
|
-
return cfg.apiKey && cfg.apiKey.length > 0 ? cfg.apiKey : void 0;
|
|
23426
|
-
}
|
|
23427
|
-
function eligibleProviders(config) {
|
|
23428
|
-
const out = [];
|
|
23429
|
-
for (const [id, cfg] of Object.entries(config.providers ?? {})) {
|
|
23430
|
-
const preset = COMPATIBLE_PRESETS[id];
|
|
23431
|
-
const enabled = cfg.autoDiscoverModels ?? preset?.autoDiscover ?? false;
|
|
23432
|
-
if (!enabled) continue;
|
|
23433
|
-
const baseUrl = cfg.baseUrl ?? preset?.defaultBaseUrl;
|
|
23434
|
-
if (!baseUrl) continue;
|
|
23435
|
-
out.push({ id, cfg, baseUrl, apiKey: resolveKey(cfg) });
|
|
23436
|
-
}
|
|
23437
|
-
return out;
|
|
23438
|
-
}
|
|
23439
24507
|
async function readCache(file) {
|
|
23440
24508
|
try {
|
|
23441
24509
|
return JSON.parse(await fs21.readFile(file, "utf8"));
|
|
@@ -23446,14 +24514,13 @@ async function readCache(file) {
|
|
|
23446
24514
|
async function discoverAndMergeWebuiProviders(opts) {
|
|
23447
24515
|
const registry = opts.registry;
|
|
23448
24516
|
if (!isOverlayRegistry(registry)) return;
|
|
23449
|
-
const targets =
|
|
24517
|
+
const targets = resolveDiscoveryTargets(opts.config);
|
|
23450
24518
|
if (targets.length === 0) return;
|
|
23451
24519
|
const cacheFile = path29.join(opts.cacheDir, "discovered-models-cache.json");
|
|
23452
24520
|
const cache2 = await readCache(cacheFile);
|
|
23453
24521
|
let cacheDirty = false;
|
|
23454
24522
|
await Promise.all(
|
|
23455
|
-
targets.map(async ({ id, cfg, baseUrl, apiKey }) => {
|
|
23456
|
-
const cacheKey = `${id}\0${baseUrl}`;
|
|
24523
|
+
targets.map(async ({ id, cfg, baseUrl, apiKey, cacheKey }) => {
|
|
23457
24524
|
const provider = await discoverOpenAICompatibleModels(id, {
|
|
23458
24525
|
baseUrl,
|
|
23459
24526
|
apiKey,
|
|
@@ -23819,15 +24886,6 @@ async function createPreContextServices(input) {
|
|
|
23819
24886
|
logger.warn(`models.dev refresh failed (${toErrorMessage12(err)}); using cached catalog`);
|
|
23820
24887
|
}
|
|
23821
24888
|
}
|
|
23822
|
-
try {
|
|
23823
|
-
await installCatalogModelOutputLimits({
|
|
23824
|
-
registry: modelsRegistry,
|
|
23825
|
-
getConfig: () => config,
|
|
23826
|
-
log: (message) => logger.debug(message)
|
|
23827
|
-
});
|
|
23828
|
-
} catch (err) {
|
|
23829
|
-
logger.debug(`model output-limit index skipped: ${toErrorMessage12(err)}`);
|
|
23830
|
-
}
|
|
23831
24889
|
try {
|
|
23832
24890
|
await discoverAndMergeWebuiProviders({
|
|
23833
24891
|
config,
|
|
@@ -23838,6 +24896,15 @@ async function createPreContextServices(input) {
|
|
|
23838
24896
|
} catch (err) {
|
|
23839
24897
|
logger.debug(`provider auto-discovery skipped: ${toErrorMessage12(err)}`);
|
|
23840
24898
|
}
|
|
24899
|
+
try {
|
|
24900
|
+
await installCatalogModelOutputLimits({
|
|
24901
|
+
registry: modelsRegistry,
|
|
24902
|
+
getConfig: () => config,
|
|
24903
|
+
log: (message) => logger.debug(message)
|
|
24904
|
+
});
|
|
24905
|
+
} catch (err) {
|
|
24906
|
+
logger.debug(`model output-limit index skipped: ${toErrorMessage12(err)}`);
|
|
24907
|
+
}
|
|
23841
24908
|
const events = opts.services?.events ?? new EventBus();
|
|
23842
24909
|
events.setLogger(logger);
|
|
23843
24910
|
const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry, events });
|
|
@@ -24110,7 +25177,7 @@ import { makeProviderFromConfig as makeProviderFromConfig4, withCatalogCapabilit
|
|
|
24110
25177
|
|
|
24111
25178
|
// src/server/mode-handlers.ts
|
|
24112
25179
|
import { DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2 } from "@wrongstack/core/agent";
|
|
24113
|
-
import { resolveWstackPaths as
|
|
25180
|
+
import { resolveWstackPaths as resolveWstackPaths6 } from "@wrongstack/core/utils";
|
|
24114
25181
|
function createModeHandlers(context) {
|
|
24115
25182
|
return createModeRouteHandlers({
|
|
24116
25183
|
modeStore: context.modeStore,
|
|
@@ -24119,7 +25186,7 @@ function createModeHandlers(context) {
|
|
|
24119
25186
|
send,
|
|
24120
25187
|
afterSwitch: async (id) => {
|
|
24121
25188
|
const modePrompt = id === "default" ? "" : (await context.modeStore.getMode(id))?.prompt ?? "";
|
|
24122
|
-
const paths =
|
|
25189
|
+
const paths = resolveWstackPaths6({
|
|
24123
25190
|
projectRoot: context.projectRoot,
|
|
24124
25191
|
globalRoot: context.globalRoot
|
|
24125
25192
|
});
|
|
@@ -24224,7 +25291,16 @@ function buildRoutes(state, deps2, cb) {
|
|
|
24224
25291
|
refineModel: (ws, msg) => modelOperations.refineModel(
|
|
24225
25292
|
ws,
|
|
24226
25293
|
msg.payload
|
|
24227
|
-
)
|
|
25294
|
+
),
|
|
25295
|
+
fallbackChoice: async (ws, msg) => {
|
|
25296
|
+
const result = emitFallbackChoice(deps2.events, msg);
|
|
25297
|
+
if (!result.ok) {
|
|
25298
|
+
send(ws, {
|
|
25299
|
+
type: "error",
|
|
25300
|
+
payload: { phase: "invalid_request", message: result.message }
|
|
25301
|
+
});
|
|
25302
|
+
}
|
|
25303
|
+
}
|
|
24228
25304
|
};
|
|
24229
25305
|
const sessionRoutes = createSessionHandlers({
|
|
24230
25306
|
config: state.getConfig(),
|
|
@@ -24402,8 +25478,10 @@ function buildRoutes(state, deps2, cb) {
|
|
|
24402
25478
|
});
|
|
24403
25479
|
const mcpRoutes = {
|
|
24404
25480
|
list: (ws, msg) => handleMcpList(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
24405
|
-
add
|
|
24406
|
-
|
|
25481
|
+
// add/update are the spawn-capable pair — they take a `command`/`args`
|
|
25482
|
+
// from the wire and start it. They go past the trust boundary (M1).
|
|
25483
|
+
add: (ws, msg) => handleMcpAdd(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry, deps2.trustBoundary),
|
|
25484
|
+
update: (ws, msg) => handleMcpUpdate(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry, deps2.trustBoundary),
|
|
24407
25485
|
remove: (ws, msg) => handleMcpRemove(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
24408
25486
|
enable: (ws, msg) => handleMcpEnable(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
24409
25487
|
disable: (ws, msg) => handleMcpDisable(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
@@ -24479,7 +25557,10 @@ async function resolvePorts(opts) {
|
|
|
24479
25557
|
const surface = opts.surface ?? "webui";
|
|
24480
25558
|
const surfaceDefaults = surface === "simpleui" ? { http: 3466 } : { http: 3456 };
|
|
24481
25559
|
const wsHost = opts.wsHost ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
|
|
24482
|
-
const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(
|
|
25560
|
+
const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(
|
|
25561
|
+
process.env["WEBUI_PORT"] ?? process.env["PORT"] ?? String(surfaceDefaults.http),
|
|
25562
|
+
10
|
|
25563
|
+
);
|
|
24483
25564
|
const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
|
|
24484
25565
|
const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
|
|
24485
25566
|
const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
|
|
@@ -24488,7 +25569,16 @@ async function resolvePorts(opts) {
|
|
|
24488
25569
|
if (!strictPort) {
|
|
24489
25570
|
httpPort = await findFreePort(wsHost, requestedHttpPort);
|
|
24490
25571
|
if (httpPort !== requestedHttpPort) {
|
|
24491
|
-
console.warn(
|
|
25572
|
+
console.warn(
|
|
25573
|
+
JSON.stringify({
|
|
25574
|
+
level: "warn",
|
|
25575
|
+
event: "webui.port_reassigned",
|
|
25576
|
+
protocol: "HTTP",
|
|
25577
|
+
requested: requestedHttpPort,
|
|
25578
|
+
assigned: httpPort,
|
|
25579
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
25580
|
+
})
|
|
25581
|
+
);
|
|
24492
25582
|
}
|
|
24493
25583
|
}
|
|
24494
25584
|
return { wsHost, httpPort, publicUrl, publicWsUrl, requireToken };
|
|
@@ -24563,7 +25653,10 @@ function createWsServers(httpServer, ports, accessToken) {
|
|
|
24563
25653
|
expectedToken: wsToken,
|
|
24564
25654
|
requireToken: ports.requireToken,
|
|
24565
25655
|
allowedHostnames: publicHostnames,
|
|
24566
|
-
allowBrowserUrlToken: Boolean(ports.publicWsUrl)
|
|
25656
|
+
allowBrowserUrlToken: Boolean(ports.publicWsUrl),
|
|
25657
|
+
// WS-003 opt-out for the Vite dev loop only (app and WS server cannot
|
|
25658
|
+
// share a port). Off unless explicitly requested — see ws-auth.ts.
|
|
25659
|
+
allowCrossPortLoopbackCookie: process.env["WRONGSTACK_WEBUI_DEV_CROSS_PORT_WS"] === "1"
|
|
24567
25660
|
});
|
|
24568
25661
|
const WS_MAX_PAYLOAD = 20 * 1024 * 1024;
|
|
24569
25662
|
const wssPrimary = new WebSocketServer({
|
|
@@ -24594,21 +25687,49 @@ function armEvents(wssPrimary, wssSecondary, wsHost, httpPort, setupInput, watch
|
|
|
24594
25687
|
if (eventsArmed) return;
|
|
24595
25688
|
eventsArmed = true;
|
|
24596
25689
|
console.log(`[WebUI] Backend ready (${label})`);
|
|
24597
|
-
disposeEvents = setupEvents({
|
|
24598
|
-
|
|
24599
|
-
|
|
25690
|
+
disposeEvents = setupEvents({
|
|
25691
|
+
...setupInput,
|
|
25692
|
+
watcherMetrics,
|
|
25693
|
+
onFleetBroadcaster: (fn) => {
|
|
25694
|
+
fleetBroadcast = fn;
|
|
25695
|
+
}
|
|
25696
|
+
});
|
|
24600
25697
|
};
|
|
24601
25698
|
wssPrimary.on("listening", () => arm(`${wsHost}:${httpPort}`));
|
|
24602
25699
|
wssPrimary.on("error", (err) => {
|
|
24603
|
-
console.error(
|
|
25700
|
+
console.error(
|
|
25701
|
+
JSON.stringify({
|
|
25702
|
+
level: "error",
|
|
25703
|
+
event: "webui.ws_server_error",
|
|
25704
|
+
host: wsHost,
|
|
25705
|
+
message: toErrorMessage13(err),
|
|
25706
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
25707
|
+
})
|
|
25708
|
+
);
|
|
24604
25709
|
});
|
|
24605
25710
|
if (wssSecondary) {
|
|
24606
25711
|
wssSecondary.on("listening", () => arm(`::1:${httpPort}`));
|
|
24607
25712
|
wssSecondary.on("error", (err) => {
|
|
24608
25713
|
if (err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL") {
|
|
24609
|
-
console.warn(
|
|
25714
|
+
console.warn(
|
|
25715
|
+
JSON.stringify({
|
|
25716
|
+
level: "warn",
|
|
25717
|
+
event: "webui.ipv6_unavailable",
|
|
25718
|
+
code: err.code,
|
|
25719
|
+
message: err.message,
|
|
25720
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
25721
|
+
})
|
|
25722
|
+
);
|
|
24610
25723
|
} else {
|
|
24611
|
-
console.error(
|
|
25724
|
+
console.error(
|
|
25725
|
+
JSON.stringify({
|
|
25726
|
+
level: "error",
|
|
25727
|
+
event: "webui.ws_server_error",
|
|
25728
|
+
host: "::1",
|
|
25729
|
+
message: err.message,
|
|
25730
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
25731
|
+
})
|
|
25732
|
+
);
|
|
24612
25733
|
}
|
|
24613
25734
|
});
|
|
24614
25735
|
}
|
|
@@ -24629,6 +25750,7 @@ function resolveWebuiDistDir(fromUrl, explicitDistDir) {
|
|
|
24629
25750
|
}
|
|
24630
25751
|
}
|
|
24631
25752
|
function startHttpServer(opts) {
|
|
25753
|
+
const intakeService = opts.intakeService ?? createProjectIntakeService({ projectRoot: opts.projectRoot, globalRoot: opts.globalRoot });
|
|
24632
25754
|
const httpServer = createHttpServer({
|
|
24633
25755
|
host: opts.wsHost,
|
|
24634
25756
|
port: opts.httpPort,
|
|
@@ -24642,7 +25764,8 @@ function startHttpServer(opts) {
|
|
|
24642
25764
|
onTechStackEvent: opts.onTechStackEvent,
|
|
24643
25765
|
getLlm: opts.getLlm,
|
|
24644
25766
|
executePackageOperation: opts.executePackageOperation,
|
|
24645
|
-
projectRoot: opts.projectRoot
|
|
25767
|
+
projectRoot: opts.projectRoot,
|
|
25768
|
+
intakeService
|
|
24646
25769
|
});
|
|
24647
25770
|
return httpServer;
|
|
24648
25771
|
}
|