@wrongstack/webui-server 0.299.0 → 0.301.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +854 -268
- package/dist/protocol/client-conversation.d.ts +1 -1
- package/dist/protocol/index.js +23 -1
- package/dist/protocol/projections.d.ts +8 -0
- package/dist/protocol/registry.d.ts +2 -2
- package/dist/protocol/server-conversation.d.ts +2 -2
- package/dist/protocol/version.d.ts +2 -0
- package/dist/server/connections-health-route.d.ts +10 -0
- package/dist/server/context-editor.d.ts +9 -0
- package/dist/server/conversation-routes.d.ts +1 -0
- package/dist/server/discover-mailbox-bridge.d.ts +5 -0
- package/dist/server/embedded-host-adapters.d.ts +1 -1
- package/dist/server/embedded-lifecycle.d.ts +23 -5
- package/dist/server/entry.js +1296 -262
- package/dist/server/fallback-choice.d.ts +19 -0
- package/dist/server/goal-ws-handler.d.ts +12 -0
- package/dist/server/http-server.d.ts +49 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/instance-registry.d.ts +21 -0
- package/dist/server/kanban-broadcast.d.ts +37 -0
- package/dist/server/lifecycle.d.ts +6 -0
- package/dist/server/mcp-handlers.d.ts +3 -2
- package/dist/server/pref-helpers.d.ts +1 -1
- package/dist/server/provider-handlers.d.ts +2 -2
- package/dist/server/provider-routes.d.ts +3 -1
- package/dist/server/ws-auth.d.ts +18 -0
- package/dist/server/ws-payload-validation.d.ts +7 -0
- package/package.json +11 -11
package/dist/index.js
CHANGED
|
@@ -48,6 +48,7 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
|
48
48
|
"chime",
|
|
49
49
|
"confirmExit",
|
|
50
50
|
"nextPrediction",
|
|
51
|
+
"nextStepsTool",
|
|
51
52
|
"titleAnimation",
|
|
52
53
|
"enhanceEnabled",
|
|
53
54
|
"featureMcp",
|
|
@@ -73,6 +74,10 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
|
73
74
|
// Display-only toggles (purely visual, persisted in localStorage via Zustand).
|
|
74
75
|
"groupToolCalls",
|
|
75
76
|
"showThinkingLogs",
|
|
77
|
+
// v15: auto-collapse of the chat input under the history (opt-in display
|
|
78
|
+
// toggle, default off). Whitelisted so the key survives `prefs.update`
|
|
79
|
+
// round-trips without tripping the "unknown preference key" rejection.
|
|
80
|
+
"autoCollapseInput",
|
|
76
81
|
// v11 Display parity: inverse fsAccess flag.
|
|
77
82
|
"allowOutsideProjectRoot",
|
|
78
83
|
// v13 Display parity (TUI SettingsPicker fields 42 & 43): the read tool
|
|
@@ -167,6 +172,7 @@ var ENUM_PREF_KEYS = {
|
|
|
167
172
|
fsAccess: /* @__PURE__ */ new Set(["unrestricted", "project"]),
|
|
168
173
|
// Chimera autoFix + auto-review cascade threshold
|
|
169
174
|
chimeraAutoFix: /* @__PURE__ */ new Set(["off", "ask", "auto"]),
|
|
175
|
+
autoReviewModelSelection: /* @__PURE__ */ new Set(["round-robin", "random"]),
|
|
170
176
|
autoReviewCascadeOn: /* @__PURE__ */ new Set(["off", "critical", "high"]),
|
|
171
177
|
fleetChatVerbosity: /* @__PURE__ */ new Set(["off", "full"]),
|
|
172
178
|
showAgentSwarmPanel: /* @__PURE__ */ new Set(["bottom", "sidebar", "off"])
|
|
@@ -397,6 +403,51 @@ function validateModelSwitchPayload(payload) {
|
|
|
397
403
|
}
|
|
398
404
|
};
|
|
399
405
|
}
|
|
406
|
+
function validateModelFallbackChoicePayload(payload) {
|
|
407
|
+
if (!isRecord2(payload)) {
|
|
408
|
+
return {
|
|
409
|
+
ok: false,
|
|
410
|
+
message: "model.fallback_choice payload must be an object"
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const requestId = payload["requestId"];
|
|
414
|
+
if (typeof requestId !== "string" || requestId.trim().length === 0) {
|
|
415
|
+
return {
|
|
416
|
+
ok: false,
|
|
417
|
+
message: "model.fallback_choice payload.requestId must be a non-empty string"
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const providerId = payload["providerId"];
|
|
421
|
+
const model = payload["model"];
|
|
422
|
+
const autoSwitch = payload["autoSwitch"];
|
|
423
|
+
if (providerId !== void 0 && typeof providerId !== "string") {
|
|
424
|
+
return {
|
|
425
|
+
ok: false,
|
|
426
|
+
message: "model.fallback_choice payload.providerId must be a string when provided"
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
if (model !== void 0 && typeof model !== "string") {
|
|
430
|
+
return {
|
|
431
|
+
ok: false,
|
|
432
|
+
message: "model.fallback_choice payload.model must be a string when provided"
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (autoSwitch !== void 0 && typeof autoSwitch !== "boolean") {
|
|
436
|
+
return {
|
|
437
|
+
ok: false,
|
|
438
|
+
message: "model.fallback_choice payload.autoSwitch must be a boolean when provided"
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
ok: true,
|
|
443
|
+
value: {
|
|
444
|
+
requestId: requestId.trim(),
|
|
445
|
+
...typeof providerId === "string" ? { providerId } : {},
|
|
446
|
+
...typeof model === "string" ? { model } : {},
|
|
447
|
+
...typeof autoSwitch === "boolean" ? { autoSwitch } : {}
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
400
451
|
var AUTONOMY_VALUES2 = /* @__PURE__ */ new Set(["off", "suggest", "auto", "eternal", "eternal-parallel"]);
|
|
401
452
|
function validateMailboxMessagesPayload(payload) {
|
|
402
453
|
if (payload === void 0) return { ok: true, value: void 0 };
|
|
@@ -4267,6 +4318,9 @@ async function handleConversationRoute(ws, msg, handlers) {
|
|
|
4267
4318
|
case "user_message":
|
|
4268
4319
|
await handlers.userMessage(ws, msg);
|
|
4269
4320
|
return true;
|
|
4321
|
+
case "topic.advice":
|
|
4322
|
+
await handlers.topicAdvice(ws, msg);
|
|
4323
|
+
return true;
|
|
4270
4324
|
case "abort":
|
|
4271
4325
|
await handlers.abort(ws, msg);
|
|
4272
4326
|
return true;
|
|
@@ -4282,6 +4336,7 @@ async function handleConversationRoute(ws, msg, handlers) {
|
|
|
4282
4336
|
}
|
|
4283
4337
|
|
|
4284
4338
|
// src/server/conversation-operations.ts
|
|
4339
|
+
import { startFreshTopicContext, TopicShiftAdvisor } from "@wrongstack/core/execution";
|
|
4285
4340
|
import {
|
|
4286
4341
|
buildUserContentBlocks,
|
|
4287
4342
|
IncomingImageError,
|
|
@@ -4298,6 +4353,7 @@ function requestedSessionId(msg) {
|
|
|
4298
4353
|
return payload && typeof payload === "object" && typeof payload.sessionId === "string" ? payload.sessionId : void 0;
|
|
4299
4354
|
}
|
|
4300
4355
|
function createConversationOperations(ctx) {
|
|
4356
|
+
const topicShiftAdvisor = new TopicShiftAdvisor();
|
|
4301
4357
|
const sessionPayload2 = (payload) => {
|
|
4302
4358
|
const provided = payload["sessionId"];
|
|
4303
4359
|
const sessionId = typeof provided === "string" && provided.length > 0 ? provided : ctx.getSessionId();
|
|
@@ -4318,6 +4374,38 @@ function createConversationOperations(ctx) {
|
|
|
4318
4374
|
return false;
|
|
4319
4375
|
};
|
|
4320
4376
|
return {
|
|
4377
|
+
topicAdvice: async (ws, msg) => {
|
|
4378
|
+
if (!ensureCurrentSession(ws, msg, "topic.advice")) return;
|
|
4379
|
+
const payload = msg.payload ?? {};
|
|
4380
|
+
if (typeof payload.requestId !== "string" || typeof payload.prompt !== "string") {
|
|
4381
|
+
ctx.send(ws, {
|
|
4382
|
+
type: "topic.advice_result",
|
|
4383
|
+
payload: sessionPayload2({
|
|
4384
|
+
requestId: typeof payload.requestId === "string" ? payload.requestId : "",
|
|
4385
|
+
suggestNewContext: false,
|
|
4386
|
+
confidence: 0,
|
|
4387
|
+
reason: "Invalid topic advice request.",
|
|
4388
|
+
source: "local"
|
|
4389
|
+
})
|
|
4390
|
+
});
|
|
4391
|
+
return;
|
|
4392
|
+
}
|
|
4393
|
+
const agent = ctx.getAgent();
|
|
4394
|
+
const configuredMax = agent.ctx.meta["effectiveMaxContext"];
|
|
4395
|
+
const maxContext = typeof configuredMax === "number" ? configuredMax : agent.ctx.provider.capabilities.maxContext;
|
|
4396
|
+
const advice = await topicShiftAdvisor.advise({
|
|
4397
|
+
prompt: payload.prompt,
|
|
4398
|
+
messages: agent.ctx.messages,
|
|
4399
|
+
provider: agent.ctx.provider,
|
|
4400
|
+
model: agent.ctx.model,
|
|
4401
|
+
contextTokens: agent.ctx.lastRequestTokens,
|
|
4402
|
+
maxContext
|
|
4403
|
+
});
|
|
4404
|
+
ctx.send(ws, {
|
|
4405
|
+
type: "topic.advice_result",
|
|
4406
|
+
payload: sessionPayload2({ requestId: payload.requestId, ...advice })
|
|
4407
|
+
});
|
|
4408
|
+
},
|
|
4321
4409
|
userMessage: async (ws, msg) => {
|
|
4322
4410
|
if (!ensureCurrentSession(ws, msg, "user_message")) return;
|
|
4323
4411
|
const payload = msg.payload ?? {};
|
|
@@ -4335,6 +4423,7 @@ function createConversationOperations(ctx) {
|
|
|
4335
4423
|
const originSessionId = ctx.getSessionId();
|
|
4336
4424
|
try {
|
|
4337
4425
|
const agent = ctx.getAgent();
|
|
4426
|
+
if (payload.freshContext === true) await startFreshTopicContext(agent.ctx);
|
|
4338
4427
|
const content = typeof payload.content === "string" ? payload.content : "";
|
|
4339
4428
|
let input = content;
|
|
4340
4429
|
const imageBlocks = parseIncomingImages(payload.images, payload.imageBase64);
|
|
@@ -4417,13 +4506,10 @@ function createConversationOperations(ctx) {
|
|
|
4417
4506
|
|
|
4418
4507
|
// src/server/context-editor.ts
|
|
4419
4508
|
import { createHash } from "node:crypto";
|
|
4420
|
-
import net from "node:net";
|
|
4421
4509
|
import {
|
|
4422
4510
|
ALLOWED_IMAGE_MEDIA_TYPES,
|
|
4423
4511
|
base64DecodedBytes,
|
|
4424
4512
|
isAllowedImageMediaType,
|
|
4425
|
-
isPrivateIPv4,
|
|
4426
|
-
isPrivateIPv6,
|
|
4427
4513
|
isValidImageBase64,
|
|
4428
4514
|
MAX_INCOMING_IMAGE_BYTES,
|
|
4429
4515
|
repairToolUseAdjacency
|
|
@@ -4491,40 +4577,7 @@ var REVISION_PREFIX = "wrongstack-context-editor-v1\0";
|
|
|
4491
4577
|
var MAX_MESSAGE_COUNT_GROWTH = 10;
|
|
4492
4578
|
var MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
|
|
4493
4579
|
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
|
-
}
|
|
4580
|
+
var MAX_REMOVAL_COUNT = 4096;
|
|
4528
4581
|
function isRecord3(value) {
|
|
4529
4582
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4530
4583
|
}
|
|
@@ -4533,7 +4586,7 @@ function canonicalize(value) {
|
|
|
4533
4586
|
if (isRecord3(value)) {
|
|
4534
4587
|
const sorted = {};
|
|
4535
4588
|
for (const key of Object.keys(value).sort()) {
|
|
4536
|
-
if (key === "_estTokens") continue;
|
|
4589
|
+
if (key === "_estTokens" || key === "_toolErrorInfo") continue;
|
|
4537
4590
|
const item = value[key];
|
|
4538
4591
|
if (item === void 0) continue;
|
|
4539
4592
|
sorted[key] = canonicalize(item);
|
|
@@ -4564,6 +4617,12 @@ function isMessageRole(value) {
|
|
|
4564
4617
|
function isPlainJsonObject(value) {
|
|
4565
4618
|
return isRecord3(value);
|
|
4566
4619
|
}
|
|
4620
|
+
function splitsSurrogatePair(text2, offset) {
|
|
4621
|
+
if (offset <= 0 || offset >= text2.length) return false;
|
|
4622
|
+
const previous = text2.charCodeAt(offset - 1);
|
|
4623
|
+
const next = text2.charCodeAt(offset);
|
|
4624
|
+
return previous >= 55296 && previous <= 56319 && next >= 56320 && next <= 57343;
|
|
4625
|
+
}
|
|
4567
4626
|
function validateCacheControl(value, path35, errors) {
|
|
4568
4627
|
if (value === void 0) return void 0;
|
|
4569
4628
|
if (!isRecord3(value) || value["type"] !== "ephemeral") {
|
|
@@ -4774,19 +4833,13 @@ function validateBlock(value, path35, errors) {
|
|
|
4774
4833
|
);
|
|
4775
4834
|
return void 0;
|
|
4776
4835
|
}
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
source: {
|
|
4785
|
-
type: "url",
|
|
4786
|
-
...typeof mediaType === "string" ? { media_type: mediaType } : {},
|
|
4787
|
-
url
|
|
4788
|
-
}
|
|
4789
|
-
};
|
|
4836
|
+
error(
|
|
4837
|
+
errors,
|
|
4838
|
+
`${path35}/source/url`,
|
|
4839
|
+
"UNSAFE_IMAGE_URL",
|
|
4840
|
+
"URL image sources are not allowed in context editor proposals; use an ingested base64 image."
|
|
4841
|
+
);
|
|
4842
|
+
return void 0;
|
|
4790
4843
|
}
|
|
4791
4844
|
case "thinking": {
|
|
4792
4845
|
const thinking = value["thinking"];
|
|
@@ -4943,6 +4996,14 @@ function warningsForMessage(message, index) {
|
|
|
4943
4996
|
message: "This block contains provider replay metadata and should only be removed with the whole turn if no longer needed."
|
|
4944
4997
|
});
|
|
4945
4998
|
}
|
|
4999
|
+
if (block.type === "image" && block.source.type === "url") {
|
|
5000
|
+
warnings.push({
|
|
5001
|
+
path: `/messages/${index}/content/${blockIndex}/source/url`,
|
|
5002
|
+
code: "UNSAFE_IMAGE_URL",
|
|
5003
|
+
severity: "danger",
|
|
5004
|
+
message: "URL image sources cannot be retained in context editor proposals; remove the whole message before applying other edits."
|
|
5005
|
+
});
|
|
5006
|
+
}
|
|
4946
5007
|
if (block.type === "tool_result" && block.content.length > 2e4) {
|
|
4947
5008
|
warnings.push({
|
|
4948
5009
|
path: `/messages/${index}/content/${blockIndex}`,
|
|
@@ -4970,6 +5031,21 @@ function metricFor(ctx, messages, tools) {
|
|
|
4970
5031
|
fullRequestTokens: breakdown.total
|
|
4971
5032
|
};
|
|
4972
5033
|
}
|
|
5034
|
+
function isToolResultMessage(message) {
|
|
5035
|
+
return Boolean(
|
|
5036
|
+
message?.role === "user" && Array.isArray(message.content) && message.content.length > 0 && message.content.every((block) => block.type === "tool_result")
|
|
5037
|
+
);
|
|
5038
|
+
}
|
|
5039
|
+
function pairedAssistantIndices(messages, userIndex) {
|
|
5040
|
+
if (messages[userIndex]?.role !== "user" || isToolResultMessage(messages[userIndex])) return [];
|
|
5041
|
+
const paired = [];
|
|
5042
|
+
for (let index = userIndex + 1; index < messages.length; index += 1) {
|
|
5043
|
+
const message = messages[index];
|
|
5044
|
+
if (message?.role === "user" && !isToolResultMessage(message)) break;
|
|
5045
|
+
if (message?.role === "assistant") paired.push(index);
|
|
5046
|
+
}
|
|
5047
|
+
return paired;
|
|
5048
|
+
}
|
|
4973
5049
|
function buildContextEditorSnapshot(ctx, tools) {
|
|
4974
5050
|
const messages = ctx.messages.map(
|
|
4975
5051
|
(message) => ({
|
|
@@ -4999,11 +5075,165 @@ function buildContextEditorSnapshot(ctx, tools) {
|
|
|
4999
5075
|
tokens: messageTokens(message.content),
|
|
5000
5076
|
preview: breakdown.messages.breakdown[index]?.preview ?? "",
|
|
5001
5077
|
blockCount: Array.isArray(message.content) ? message.content.length : null,
|
|
5002
|
-
warnings: warningsForMessage(message, index)
|
|
5078
|
+
warnings: warningsForMessage(message, index),
|
|
5079
|
+
pairedAssistantIndices: pairedAssistantIndices(ctx.messages, index)
|
|
5003
5080
|
})),
|
|
5004
5081
|
diagnostics: toolDiagnostics(ctx.messages)
|
|
5005
5082
|
};
|
|
5006
5083
|
}
|
|
5084
|
+
function validateRemovalPlan(value, originalMessages, proposedMessages) {
|
|
5085
|
+
const errors = [];
|
|
5086
|
+
if (value === void 0) {
|
|
5087
|
+
error(
|
|
5088
|
+
errors,
|
|
5089
|
+
"/removals",
|
|
5090
|
+
"REMOVAL_PLAN_REQUIRED",
|
|
5091
|
+
"A removal plan is required for every context editor proposal."
|
|
5092
|
+
);
|
|
5093
|
+
return { errors };
|
|
5094
|
+
}
|
|
5095
|
+
if (!Array.isArray(value)) {
|
|
5096
|
+
error(errors, "/removals", "INVALID_REMOVALS", "removals must be an array.");
|
|
5097
|
+
return { errors };
|
|
5098
|
+
}
|
|
5099
|
+
if (value.length > MAX_REMOVAL_COUNT) {
|
|
5100
|
+
error(
|
|
5101
|
+
errors,
|
|
5102
|
+
"/removals",
|
|
5103
|
+
"TOO_MANY_REMOVALS",
|
|
5104
|
+
`removals must contain at most ${MAX_REMOVAL_COUNT} entries.`
|
|
5105
|
+
);
|
|
5106
|
+
return { errors };
|
|
5107
|
+
}
|
|
5108
|
+
const wholeMessages = /* @__PURE__ */ new Set();
|
|
5109
|
+
const touchedUsers = /* @__PURE__ */ new Set();
|
|
5110
|
+
const ranges = [];
|
|
5111
|
+
for (const [removalIndex, raw] of value.entries()) {
|
|
5112
|
+
const path35 = `/removals/${removalIndex}`;
|
|
5113
|
+
if (!isRecord3(raw) || !Number.isInteger(raw["messageIndex"])) {
|
|
5114
|
+
error(errors, path35, "INVALID_REMOVAL", "Removal must include an integer messageIndex.");
|
|
5115
|
+
continue;
|
|
5116
|
+
}
|
|
5117
|
+
const messageIndex = raw["messageIndex"];
|
|
5118
|
+
const original = originalMessages[messageIndex];
|
|
5119
|
+
if (!original) {
|
|
5120
|
+
error(errors, `${path35}/messageIndex`, "INVALID_MESSAGE_INDEX", "Removal messageIndex is out of range.");
|
|
5121
|
+
continue;
|
|
5122
|
+
}
|
|
5123
|
+
const start = raw["start"];
|
|
5124
|
+
const end = raw["end"];
|
|
5125
|
+
const blockIndex = raw["blockIndex"];
|
|
5126
|
+
if (start === void 0 && end === void 0 && blockIndex === void 0) {
|
|
5127
|
+
wholeMessages.add(messageIndex);
|
|
5128
|
+
if (original.role === "user") touchedUsers.add(messageIndex);
|
|
5129
|
+
continue;
|
|
5130
|
+
}
|
|
5131
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start) {
|
|
5132
|
+
error(errors, path35, "INVALID_RANGE", "Range removal requires integer start/end with 0 <= start < end.");
|
|
5133
|
+
continue;
|
|
5134
|
+
}
|
|
5135
|
+
let text2;
|
|
5136
|
+
if (blockIndex === void 0 && typeof original.content === "string") text2 = original.content;
|
|
5137
|
+
if (Number.isInteger(blockIndex) && Array.isArray(original.content)) {
|
|
5138
|
+
const block = original.content[blockIndex];
|
|
5139
|
+
if (block?.type === "text") text2 = block.text;
|
|
5140
|
+
}
|
|
5141
|
+
if (text2 === void 0 || end > text2.length) {
|
|
5142
|
+
error(errors, path35, "INVALID_RANGE_TARGET", "Range must target existing string or text-block content.");
|
|
5143
|
+
continue;
|
|
5144
|
+
}
|
|
5145
|
+
if (splitsSurrogatePair(text2, start) || splitsSurrogatePair(text2, end)) {
|
|
5146
|
+
error(
|
|
5147
|
+
errors,
|
|
5148
|
+
path35,
|
|
5149
|
+
"INVALID_UNICODE_RANGE",
|
|
5150
|
+
"Range boundaries must not split a Unicode surrogate pair."
|
|
5151
|
+
);
|
|
5152
|
+
continue;
|
|
5153
|
+
}
|
|
5154
|
+
ranges.push({
|
|
5155
|
+
messageIndex,
|
|
5156
|
+
...blockIndex === void 0 ? {} : { blockIndex },
|
|
5157
|
+
start,
|
|
5158
|
+
end
|
|
5159
|
+
});
|
|
5160
|
+
if (original.role === "user") touchedUsers.add(messageIndex);
|
|
5161
|
+
}
|
|
5162
|
+
const rangesByTarget = /* @__PURE__ */ new Map();
|
|
5163
|
+
for (const range of ranges) {
|
|
5164
|
+
const key = `${range.messageIndex}:${range.blockIndex ?? "string"}`;
|
|
5165
|
+
const targetRanges = rangesByTarget.get(key) ?? [];
|
|
5166
|
+
targetRanges.push(range);
|
|
5167
|
+
rangesByTarget.set(key, targetRanges);
|
|
5168
|
+
}
|
|
5169
|
+
for (const targetRanges of rangesByTarget.values()) {
|
|
5170
|
+
targetRanges.sort((left, right) => (left.start ?? 0) - (right.start ?? 0));
|
|
5171
|
+
for (let index = 1; index < targetRanges.length; index += 1) {
|
|
5172
|
+
const previous = targetRanges[index - 1];
|
|
5173
|
+
const current2 = targetRanges[index];
|
|
5174
|
+
if (previous?.end !== void 0 && current2?.start !== void 0 && current2.start < previous.end) {
|
|
5175
|
+
error(
|
|
5176
|
+
errors,
|
|
5177
|
+
"/removals",
|
|
5178
|
+
"OVERLAPPING_RANGES",
|
|
5179
|
+
"Removal ranges targeting the same text must not overlap."
|
|
5180
|
+
);
|
|
5181
|
+
break;
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
for (const userIndex of touchedUsers) {
|
|
5186
|
+
for (const assistantIndex of pairedAssistantIndices(originalMessages, userIndex)) {
|
|
5187
|
+
if (wholeMessages.has(assistantIndex)) continue;
|
|
5188
|
+
error(
|
|
5189
|
+
errors,
|
|
5190
|
+
"/removals",
|
|
5191
|
+
"MISSING_ASSISTANT_PAIR",
|
|
5192
|
+
`Editing user message ${userIndex} must also remove assistant message ${assistantIndex}.`
|
|
5193
|
+
);
|
|
5194
|
+
}
|
|
5195
|
+
}
|
|
5196
|
+
const expectedMessages = structuredClone(originalMessages);
|
|
5197
|
+
for (const targetRanges of rangesByTarget.values()) {
|
|
5198
|
+
const first = targetRanges[0];
|
|
5199
|
+
if (!first) continue;
|
|
5200
|
+
const message = expectedMessages[first.messageIndex];
|
|
5201
|
+
if (!message) continue;
|
|
5202
|
+
let text2;
|
|
5203
|
+
if (first.blockIndex === void 0 && typeof message.content === "string") {
|
|
5204
|
+
text2 = message.content;
|
|
5205
|
+
} else if (first.blockIndex !== void 0 && Array.isArray(message.content)) {
|
|
5206
|
+
const block = message.content[first.blockIndex];
|
|
5207
|
+
if (block?.type === "text") text2 = block.text;
|
|
5208
|
+
}
|
|
5209
|
+
if (text2 === void 0) continue;
|
|
5210
|
+
const pieces = [];
|
|
5211
|
+
let cursor = 0;
|
|
5212
|
+
for (const range of targetRanges) {
|
|
5213
|
+
if (range.start === void 0 || range.end === void 0) continue;
|
|
5214
|
+
pieces.push(text2.slice(cursor, range.start));
|
|
5215
|
+
cursor = range.end;
|
|
5216
|
+
}
|
|
5217
|
+
pieces.push(text2.slice(cursor));
|
|
5218
|
+
const nextText = pieces.join("");
|
|
5219
|
+
if (first.blockIndex === void 0 && typeof message.content === "string") {
|
|
5220
|
+
message.content = nextText;
|
|
5221
|
+
} else if (first.blockIndex !== void 0 && Array.isArray(message.content)) {
|
|
5222
|
+
const block = message.content[first.blockIndex];
|
|
5223
|
+
if (block?.type === "text") block.text = nextText;
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
5226
|
+
const expectedProposal = expectedMessages.filter((_, index) => !wholeMessages.has(index));
|
|
5227
|
+
if (JSON.stringify(canonicalize(expectedProposal)) !== JSON.stringify(canonicalize(proposedMessages))) {
|
|
5228
|
+
error(
|
|
5229
|
+
errors,
|
|
5230
|
+
"/messages",
|
|
5231
|
+
"REMOVAL_PLAN_MISMATCH",
|
|
5232
|
+
"Submitted messages do not exactly match the declared removal plan."
|
|
5233
|
+
);
|
|
5234
|
+
}
|
|
5235
|
+
return errors.length > 0 ? { errors } : { errors, messages: expectedProposal };
|
|
5236
|
+
}
|
|
5007
5237
|
function validateContextEditorProposal(input) {
|
|
5008
5238
|
const currentRevision = contextEditorRevision(input.ctx.messages);
|
|
5009
5239
|
const before = metricFor(input.ctx, input.ctx.messages, input.tools);
|
|
@@ -5055,7 +5285,23 @@ function validateContextEditorProposal(input) {
|
|
|
5055
5285
|
repair: emptyRepair
|
|
5056
5286
|
};
|
|
5057
5287
|
}
|
|
5058
|
-
const
|
|
5288
|
+
const removalPlan = validateRemovalPlan(
|
|
5289
|
+
input.removals,
|
|
5290
|
+
input.ctx.messages,
|
|
5291
|
+
parsed.messages
|
|
5292
|
+
);
|
|
5293
|
+
if (removalPlan.errors.length > 0 || !removalPlan.messages) {
|
|
5294
|
+
return {
|
|
5295
|
+
ok: false,
|
|
5296
|
+
baseRevision: input.baseRevision,
|
|
5297
|
+
currentRevision,
|
|
5298
|
+
before,
|
|
5299
|
+
validationErrors: removalPlan.errors,
|
|
5300
|
+
warnings: [],
|
|
5301
|
+
repair: emptyRepair
|
|
5302
|
+
};
|
|
5303
|
+
}
|
|
5304
|
+
const repaired = repairToolUseAdjacency(removalPlan.messages);
|
|
5059
5305
|
const repair = {
|
|
5060
5306
|
changed: repaired.report.changed,
|
|
5061
5307
|
removedToolUses: repaired.report.removedToolUses,
|
|
@@ -5305,7 +5551,40 @@ import {
|
|
|
5305
5551
|
getKanbanServerConnection,
|
|
5306
5552
|
isKanbanServerAvailable
|
|
5307
5553
|
} from "@wrongstack/kanban";
|
|
5308
|
-
import * as
|
|
5554
|
+
import * as net from "node:net";
|
|
5555
|
+
|
|
5556
|
+
// src/server/privileged-actions.ts
|
|
5557
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
5558
|
+
import {
|
|
5559
|
+
isTrustDecisionAllowed
|
|
5560
|
+
} from "@wrongstack/core/security";
|
|
5561
|
+
async function authorizeWebUIAction(boundary, action, logger) {
|
|
5562
|
+
const request = {
|
|
5563
|
+
version: 1,
|
|
5564
|
+
requestId: randomUUID2(),
|
|
5565
|
+
actor: {
|
|
5566
|
+
kind: "remote-client",
|
|
5567
|
+
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
5568
|
+
},
|
|
5569
|
+
surface: "webui",
|
|
5570
|
+
capability: action.capability,
|
|
5571
|
+
subject: action.subject,
|
|
5572
|
+
risk: action.risk,
|
|
5573
|
+
scope: {
|
|
5574
|
+
...action.cwd ? { cwd: action.cwd } : {},
|
|
5575
|
+
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
5576
|
+
},
|
|
5577
|
+
authContext: { method: "session" },
|
|
5578
|
+
...action.metadata ? { metadata: action.metadata } : {}
|
|
5579
|
+
};
|
|
5580
|
+
const decision = await boundary.evaluate(request);
|
|
5581
|
+
logger?.debug?.(
|
|
5582
|
+
`[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
|
|
5583
|
+
);
|
|
5584
|
+
return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
|
|
5585
|
+
}
|
|
5586
|
+
|
|
5587
|
+
// src/server/connections-health-route.ts
|
|
5309
5588
|
import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
|
|
5310
5589
|
import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
|
|
5311
5590
|
import {
|
|
@@ -5711,6 +5990,42 @@ async function handleConnectionsServiceAction(ws, message, context) {
|
|
|
5711
5990
|
return true;
|
|
5712
5991
|
}
|
|
5713
5992
|
const action = rawAction;
|
|
5993
|
+
if (!context.trustBoundary) {
|
|
5994
|
+
context.send(ws, {
|
|
5995
|
+
type: "connections.service_action_result",
|
|
5996
|
+
payload: {
|
|
5997
|
+
serviceId,
|
|
5998
|
+
action,
|
|
5999
|
+
success: false,
|
|
6000
|
+
message: "Service control is unavailable: no policy authority is configured."
|
|
6001
|
+
}
|
|
6002
|
+
});
|
|
6003
|
+
return true;
|
|
6004
|
+
}
|
|
6005
|
+
const projectRootForAuth = context.getProjectRoot();
|
|
6006
|
+
const authorization = await authorizeWebUIAction(
|
|
6007
|
+
context.trustBoundary,
|
|
6008
|
+
{
|
|
6009
|
+
capability: `connections.service.${action}`,
|
|
6010
|
+
subject: { kind: "process", id: `${serviceId}@${projectRootForAuth}` },
|
|
6011
|
+
risk: "elevated",
|
|
6012
|
+
cwd: projectRootForAuth,
|
|
6013
|
+
metadata: { transport: "websocket", serviceId, action }
|
|
6014
|
+
},
|
|
6015
|
+
context.logger
|
|
6016
|
+
);
|
|
6017
|
+
if (!authorization.allowed) {
|
|
6018
|
+
context.send(ws, {
|
|
6019
|
+
type: "connections.service_action_result",
|
|
6020
|
+
payload: {
|
|
6021
|
+
serviceId,
|
|
6022
|
+
action,
|
|
6023
|
+
success: false,
|
|
6024
|
+
message: authorization.reason ?? "Refused by policy."
|
|
6025
|
+
}
|
|
6026
|
+
});
|
|
6027
|
+
return true;
|
|
6028
|
+
}
|
|
5714
6029
|
if (serviceId === "webui") {
|
|
5715
6030
|
context.send(ws, {
|
|
5716
6031
|
type: "connections.service_action_result",
|
|
@@ -6149,7 +6464,7 @@ var RESTART_POLL_INTERVAL_MS = 250;
|
|
|
6149
6464
|
var RESTART_DEADLINE_MS = 3e3;
|
|
6150
6465
|
function isEndpointAlive(endpoint) {
|
|
6151
6466
|
return new Promise((resolve16) => {
|
|
6152
|
-
const sock =
|
|
6467
|
+
const sock = net.createConnection(endpoint);
|
|
6153
6468
|
const timer = setTimeout(() => {
|
|
6154
6469
|
sock.destroy();
|
|
6155
6470
|
resolve16(false);
|
|
@@ -6891,12 +7206,12 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
|
|
|
6891
7206
|
...maybeVerify,
|
|
6892
7207
|
onPhaseComplete: (phase) => {
|
|
6893
7208
|
this.logger.info(`[Goal] Phase completed: ${phase.name}`);
|
|
6894
|
-
|
|
7209
|
+
this.persistDetached(graph);
|
|
6895
7210
|
this.broadcastState();
|
|
6896
7211
|
},
|
|
6897
7212
|
onPhaseFail: (phase, error2) => {
|
|
6898
7213
|
this.logger.error(`[Goal] Phase failed: ${phase.name} \u2014 ${error2.message}`);
|
|
6899
|
-
|
|
7214
|
+
this.persistDetached(graph);
|
|
6900
7215
|
this.broadcastState();
|
|
6901
7216
|
}
|
|
6902
7217
|
},
|
|
@@ -6913,7 +7228,7 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
|
|
|
6913
7228
|
this.broadcastState();
|
|
6914
7229
|
void this.orchestrator.start().then(() => {
|
|
6915
7230
|
this.orchestrator?.stop();
|
|
6916
|
-
|
|
7231
|
+
this.persistDetached(graph);
|
|
6917
7232
|
this.stopBroadcast();
|
|
6918
7233
|
const failed = graph.failedPhaseIds.length > 0;
|
|
6919
7234
|
this.broadcast(
|
|
@@ -7111,9 +7426,27 @@ ${result_.finalText.slice(0, 2e3)}`
|
|
|
7111
7426
|
this.logger.warn(`[Goal] Chimera review failed for "${task.title}": ${toErrorMessage2(err)}`);
|
|
7112
7427
|
}
|
|
7113
7428
|
}
|
|
7429
|
+
/**
|
|
7430
|
+
* Fire-and-forget persist.
|
|
7431
|
+
*
|
|
7432
|
+
* Every detached `store.save()` used to be a bare `void`, so a rejection
|
|
7433
|
+
* became an unhandled rejection and — under Node 22's default
|
|
7434
|
+
* `--unhandled-rejections=throw` — killed the process mid-run. On Windows an
|
|
7435
|
+
* AV scanner or indexer holding the `.wrongstack/phases/<id>.json` rename
|
|
7436
|
+
* target for a few hundred ms is enough (EPERM from `atomicWrite`), and in
|
|
7437
|
+
* `--webui` mode that takes the CLI session down with it. `handleStop` at
|
|
7438
|
+
* `:549` already had the `.catch`; these call sites did not.
|
|
7439
|
+
*/
|
|
7440
|
+
persistDetached(graph) {
|
|
7441
|
+
void this.store.save(graph).catch((err) => {
|
|
7442
|
+
this.logger.warn(
|
|
7443
|
+
`[Goal] Failed to persist phase graph: ${err instanceof Error ? err.message : String(err)}`
|
|
7444
|
+
);
|
|
7445
|
+
});
|
|
7446
|
+
}
|
|
7114
7447
|
/** Persist + broadcast after an interactive board mutation. */
|
|
7115
7448
|
afterBoardMutation() {
|
|
7116
|
-
if (this.graph)
|
|
7449
|
+
if (this.graph) this.persistDetached(this.graph);
|
|
7117
7450
|
this.broadcastState();
|
|
7118
7451
|
}
|
|
7119
7452
|
async handleTaskStatusChange(taskId, status) {
|
|
@@ -8105,12 +8438,21 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
8105
8438
|
const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
|
|
8106
8439
|
const store = new DefaultSessionStore4({ dir: paths.projectSessions });
|
|
8107
8440
|
const reader = new DefaultSessionReader2({ store });
|
|
8108
|
-
const
|
|
8441
|
+
const RING = Math.max(limit * 4, 2e3);
|
|
8442
|
+
const ring = [];
|
|
8443
|
+
let totalRaw = 0;
|
|
8444
|
+
let dropped = false;
|
|
8109
8445
|
for await (const ev of reader.replay(sessionId)) {
|
|
8110
8446
|
const mapped = mapWatchEntry(ev);
|
|
8111
|
-
if (mapped)
|
|
8447
|
+
if (!mapped) continue;
|
|
8448
|
+
totalRaw += 1;
|
|
8449
|
+
ring.push(mapped);
|
|
8450
|
+
if (ring.length > RING) {
|
|
8451
|
+
ring.shift();
|
|
8452
|
+
dropped = true;
|
|
8453
|
+
}
|
|
8112
8454
|
}
|
|
8113
|
-
const all = correlateToolEvents(
|
|
8455
|
+
const all = correlateToolEvents(ring);
|
|
8114
8456
|
const tail2 = all.slice(-limit);
|
|
8115
8457
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
8116
8458
|
res.end(
|
|
@@ -8119,7 +8461,12 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
8119
8461
|
status: entry.status,
|
|
8120
8462
|
clientType: entry.clientType,
|
|
8121
8463
|
projectName: entry.projectName,
|
|
8122
|
-
|
|
8464
|
+
// Exact when the whole session fit in the ring (the previous
|
|
8465
|
+
// behaviour). Past that, correlation never ran over the dropped
|
|
8466
|
+
// prefix, so report the raw event count — an upper bound — and say so
|
|
8467
|
+
// rather than silently understating the session's size.
|
|
8468
|
+
total: dropped ? totalRaw : all.length,
|
|
8469
|
+
...dropped ? { truncated: true } : {},
|
|
8123
8470
|
entries: tail2
|
|
8124
8471
|
})
|
|
8125
8472
|
);
|
|
@@ -8809,7 +9156,7 @@ async function touchProjectInManifest(options, globalConfigPath) {
|
|
|
8809
9156
|
}
|
|
8810
9157
|
|
|
8811
9158
|
// src/server/techstack-handlers.ts
|
|
8812
|
-
import { randomUUID as
|
|
9159
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
8813
9160
|
var DEEP_DIVE_TIMEOUT_MS = 6e4;
|
|
8814
9161
|
function sendJson3(res, status, data) {
|
|
8815
9162
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -8850,7 +9197,7 @@ function requireJobDeps(res, deps2) {
|
|
|
8850
9197
|
}
|
|
8851
9198
|
function startJob(res, deps2, kind) {
|
|
8852
9199
|
if (!requireJobDeps(res, deps2)) return;
|
|
8853
|
-
const jobId =
|
|
9200
|
+
const jobId = randomUUID3();
|
|
8854
9201
|
const controller = new AbortController();
|
|
8855
9202
|
deps2.runningJobs?.set(jobId, controller);
|
|
8856
9203
|
deps2.emit?.({ type: "techstack.job.started", payload: { jobId, kind } });
|
|
@@ -9082,19 +9429,22 @@ function extractToken(url) {
|
|
|
9082
9429
|
function extractTokenFromCookie(cookieHeader) {
|
|
9083
9430
|
if (!cookieHeader) return void 0;
|
|
9084
9431
|
const raw = Array.isArray(cookieHeader) ? cookieHeader.join("; ") : cookieHeader;
|
|
9432
|
+
let plain;
|
|
9085
9433
|
for (const part of raw.split(";")) {
|
|
9086
9434
|
const eq = part.indexOf("=");
|
|
9087
9435
|
if (eq < 0) continue;
|
|
9088
9436
|
const name2 = part.slice(0, eq).trim();
|
|
9089
|
-
if (name2
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9437
|
+
if (name2 !== "ws_token" && name2 !== "__Host-ws_token") continue;
|
|
9438
|
+
let value;
|
|
9439
|
+
try {
|
|
9440
|
+
value = decodeURIComponent(part.slice(eq + 1).trim());
|
|
9441
|
+
} catch {
|
|
9442
|
+
value = part.slice(eq + 1).trim();
|
|
9095
9443
|
}
|
|
9444
|
+
if (name2 === "__Host-ws_token") return value;
|
|
9445
|
+
plain ??= value;
|
|
9096
9446
|
}
|
|
9097
|
-
return
|
|
9447
|
+
return plain;
|
|
9098
9448
|
}
|
|
9099
9449
|
function hostHeaderOk(input) {
|
|
9100
9450
|
if (!isLoopbackBind(input.wsHost)) return true;
|
|
@@ -9136,7 +9486,8 @@ function verifyClient(input) {
|
|
|
9136
9486
|
expectedToken,
|
|
9137
9487
|
requireToken,
|
|
9138
9488
|
allowedHostnames,
|
|
9139
|
-
allowBrowserUrlToken
|
|
9489
|
+
allowBrowserUrlToken,
|
|
9490
|
+
allowCrossPortLoopbackCookie
|
|
9140
9491
|
} = input;
|
|
9141
9492
|
const urlTokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
|
|
9142
9493
|
const cookieTokenOk = tokenMatches(extractTokenFromCookie(cookieHeader), expectedToken);
|
|
@@ -9151,7 +9502,10 @@ function verifyClient(input) {
|
|
|
9151
9502
|
const { hostname: originHostname } = new URL(origin);
|
|
9152
9503
|
if (isLoopbackHostname(originHostname)) {
|
|
9153
9504
|
if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
|
|
9154
|
-
|
|
9505
|
+
if (!isTrustedLoopbackOrigin(origin, hostHeader)) {
|
|
9506
|
+
return Boolean(allowCrossPortLoopbackCookie) && cookieTokenOk;
|
|
9507
|
+
}
|
|
9508
|
+
return true;
|
|
9155
9509
|
}
|
|
9156
9510
|
return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
|
|
9157
9511
|
} catch {
|
|
@@ -9186,11 +9540,22 @@ ${out}`;
|
|
|
9186
9540
|
function firstHeader(value) {
|
|
9187
9541
|
return Array.isArray(value) ? value[0] : value;
|
|
9188
9542
|
}
|
|
9189
|
-
|
|
9190
|
-
|
|
9543
|
+
var WS_TOKEN_COOKIE = "ws_token";
|
|
9544
|
+
var WS_TOKEN_COOKIE_SECURE = "__Host-ws_token";
|
|
9545
|
+
function wsTokenCookie(token, secure) {
|
|
9546
|
+
const name2 = secure ? WS_TOKEN_COOKIE_SECURE : WS_TOKEN_COOKIE;
|
|
9547
|
+
const parts = [
|
|
9548
|
+
`${name2}=${encodeURIComponent(token)}`,
|
|
9549
|
+
"HttpOnly",
|
|
9550
|
+
"SameSite=Strict",
|
|
9551
|
+
"Path=/",
|
|
9552
|
+
"Max-Age=3600"
|
|
9553
|
+
];
|
|
9554
|
+
if (secure) parts.push("Secure");
|
|
9555
|
+
return parts.join("; ");
|
|
9191
9556
|
}
|
|
9192
|
-
function setAuthCookieHeaders(res, token) {
|
|
9193
|
-
res.setHeader("Set-Cookie", wsTokenCookie(token));
|
|
9557
|
+
function setAuthCookieHeaders(res, token, secure) {
|
|
9558
|
+
res.setHeader("Set-Cookie", wsTokenCookie(token, secure));
|
|
9194
9559
|
res.setHeader("Cache-Control", "no-store");
|
|
9195
9560
|
}
|
|
9196
9561
|
function setStaticSecurityHeaders(res) {
|
|
@@ -9198,8 +9563,16 @@ function setStaticSecurityHeaders(res) {
|
|
|
9198
9563
|
res.setHeader("X-Frame-Options", "DENY");
|
|
9199
9564
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
9200
9565
|
}
|
|
9201
|
-
function requestToken(req, url) {
|
|
9202
|
-
|
|
9566
|
+
function requestToken(req, url, opts = {}) {
|
|
9567
|
+
const queryToken = url.searchParams.get("token") ?? void 0;
|
|
9568
|
+
if (queryToken !== void 0 && (opts.allowQuery === true || isLoopbackPeer(req))) {
|
|
9569
|
+
return queryToken;
|
|
9570
|
+
}
|
|
9571
|
+
return firstHeader(req.headers["x-ws-token"]) ?? extractTokenFromCookie(req.headers.cookie);
|
|
9572
|
+
}
|
|
9573
|
+
function isLoopbackPeer(req) {
|
|
9574
|
+
const address = req.socket.remoteAddress?.replace(/^::ffff:/i, "");
|
|
9575
|
+
return address !== void 0 && isLoopbackHostname(address);
|
|
9203
9576
|
}
|
|
9204
9577
|
function formatCspHostname(hostname) {
|
|
9205
9578
|
return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
@@ -9254,7 +9627,8 @@ function strictDecodeParam(segment, res) {
|
|
|
9254
9627
|
function createHttpServer(opts) {
|
|
9255
9628
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
9256
9629
|
const distDir = path13.resolve(opts.distDir);
|
|
9257
|
-
const requireAccessToken =
|
|
9630
|
+
const requireAccessToken = true;
|
|
9631
|
+
const secureCookies = opts.secureCookies ?? (opts.publicWsUrl?.trim().toLowerCase().startsWith("wss:") ?? false);
|
|
9258
9632
|
const trustedHostnames = (() => {
|
|
9259
9633
|
const names = [...opts.allowedHostnames ?? []];
|
|
9260
9634
|
if (opts.publicWsUrl) {
|
|
@@ -9289,17 +9663,17 @@ function createHttpServer(opts) {
|
|
|
9289
9663
|
res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
|
|
9290
9664
|
return;
|
|
9291
9665
|
}
|
|
9292
|
-
const providedAccessToken = requestToken(req, url);
|
|
9666
|
+
const providedAccessToken = requestToken(req, url, { allowQuery: true });
|
|
9293
9667
|
const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
|
|
9294
9668
|
const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
|
|
9295
9669
|
if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
|
|
9296
|
-
const provided = requestToken(req, url);
|
|
9670
|
+
const provided = requestToken(req, url, { allowQuery: true });
|
|
9297
9671
|
if (!provided || !opts.apiToken || !tokenMatches(provided, opts.apiToken)) {
|
|
9298
9672
|
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
9299
9673
|
res.end("Unauthorized");
|
|
9300
9674
|
return;
|
|
9301
9675
|
}
|
|
9302
|
-
setAuthCookieHeaders(res, opts.apiToken);
|
|
9676
|
+
setAuthCookieHeaders(res, opts.apiToken, secureCookies);
|
|
9303
9677
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
9304
9678
|
res.end("ok");
|
|
9305
9679
|
return;
|
|
@@ -9313,7 +9687,7 @@ function createHttpServer(opts) {
|
|
|
9313
9687
|
return;
|
|
9314
9688
|
}
|
|
9315
9689
|
if (shouldSetAuthCookie && opts.apiToken) {
|
|
9316
|
-
setAuthCookieHeaders(res, opts.apiToken);
|
|
9690
|
+
setAuthCookieHeaders(res, opts.apiToken, secureCookies);
|
|
9317
9691
|
}
|
|
9318
9692
|
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
9319
9693
|
if (requireAccessToken && !accessTokenOk) {
|
|
@@ -10150,6 +10524,27 @@ import {
|
|
|
10150
10524
|
getServerKanbanStore
|
|
10151
10525
|
} from "@wrongstack/kanban";
|
|
10152
10526
|
import { recordKanbanVerificationEvidence } from "@wrongstack/tools";
|
|
10527
|
+
|
|
10528
|
+
// src/server/kanban-broadcast.ts
|
|
10529
|
+
function kanbanBoardMessage(board) {
|
|
10530
|
+
return { type: "kanban.get", payload: { success: true, data: { board } } };
|
|
10531
|
+
}
|
|
10532
|
+
function kanbanListMessage(boards) {
|
|
10533
|
+
return { type: "kanban.list", payload: { success: true, data: boards } };
|
|
10534
|
+
}
|
|
10535
|
+
function kanbanDeletedMessage(boardId) {
|
|
10536
|
+
return { type: "kanban.delete", payload: { success: true, data: { removed: true, boardId } } };
|
|
10537
|
+
}
|
|
10538
|
+
async function publishKanbanBoard(broadcast2, board, listBoards6) {
|
|
10539
|
+
broadcast2(kanbanBoardMessage(board));
|
|
10540
|
+
if (!listBoards6) return;
|
|
10541
|
+
try {
|
|
10542
|
+
broadcast2(kanbanListMessage(await listBoards6()));
|
|
10543
|
+
} catch {
|
|
10544
|
+
}
|
|
10545
|
+
}
|
|
10546
|
+
|
|
10547
|
+
// src/server/kanban-dispatch.ts
|
|
10153
10548
|
function parseResolvedDispatchRoute(summary) {
|
|
10154
10549
|
const tags = summary.match(/Spawned subagent\s+\S+\s+\((.*?)\)\s+for task/i)?.[1];
|
|
10155
10550
|
if (!tags) return {};
|
|
@@ -10280,10 +10675,7 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
|
|
|
10280
10675
|
payload: { success: true, data: { boardId: board.id, task: completedTask } }
|
|
10281
10676
|
});
|
|
10282
10677
|
if (completedBoard) {
|
|
10283
|
-
ctx.broadcast?.(
|
|
10284
|
-
type: "kanban.get",
|
|
10285
|
-
payload: { success: true, data: { board: completedBoard } }
|
|
10286
|
-
});
|
|
10678
|
+
ctx.broadcast?.(kanbanBoardMessage(completedBoard));
|
|
10287
10679
|
}
|
|
10288
10680
|
ctx.broadcast?.({
|
|
10289
10681
|
type: "kanban.list",
|
|
@@ -10307,7 +10699,7 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
|
|
|
10307
10699
|
payload: { success: true, data: { boardId: board.id, task: runningTask } }
|
|
10308
10700
|
});
|
|
10309
10701
|
if (started?.board) {
|
|
10310
|
-
ctx.broadcast?.(
|
|
10702
|
+
ctx.broadcast?.(kanbanBoardMessage(started.board));
|
|
10311
10703
|
}
|
|
10312
10704
|
reply(ws, "kanban.task.dispatch", true, { boardId: board.id, task: runningTask, summary });
|
|
10313
10705
|
} catch (error2) {
|
|
@@ -10550,14 +10942,11 @@ async function handleDecompositionResolution(ws, type, payload, ctx) {
|
|
|
10550
10942
|
type: "kanban.decomposition.applied",
|
|
10551
10943
|
payload: { success: true, data: { board: resolved.board } }
|
|
10552
10944
|
});
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
type: "kanban.list",
|
|
10559
|
-
payload: { success: true, data: await listBoards(ctx.projectRoot) }
|
|
10560
|
-
});
|
|
10945
|
+
await publishKanbanBoard(
|
|
10946
|
+
(message) => ctx.broadcast?.(message),
|
|
10947
|
+
resolved.board,
|
|
10948
|
+
() => listBoards(ctx.projectRoot)
|
|
10949
|
+
);
|
|
10561
10950
|
} else {
|
|
10562
10951
|
ctx.broadcast?.({
|
|
10563
10952
|
type: "kanban.decomposition.resolved",
|
|
@@ -10590,10 +10979,7 @@ async function handleTaskVerification(ws, type, payload, ctx) {
|
|
|
10590
10979
|
payload: { success: true, data: { boardId, task: freshTask } }
|
|
10591
10980
|
});
|
|
10592
10981
|
if (persisted) {
|
|
10593
|
-
ctx.broadcast?.(
|
|
10594
|
-
type: "kanban.get",
|
|
10595
|
-
payload: { success: true, data: { board: persisted } }
|
|
10596
|
-
});
|
|
10982
|
+
ctx.broadcast?.(kanbanBoardMessage(persisted));
|
|
10597
10983
|
}
|
|
10598
10984
|
} catch (err) {
|
|
10599
10985
|
ctx.broadcast?.({
|
|
@@ -11516,10 +11902,7 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
11516
11902
|
let connectionCount = 0;
|
|
11517
11903
|
const broadcastDeleted = (boardId) => {
|
|
11518
11904
|
knownRevisions.delete(boardId);
|
|
11519
|
-
broadcastMessage(
|
|
11520
|
-
type: "kanban.delete",
|
|
11521
|
-
payload: { success: true, data: { removed: true, boardId } }
|
|
11522
|
-
});
|
|
11905
|
+
broadcastMessage(kanbanDeletedMessage(boardId));
|
|
11523
11906
|
};
|
|
11524
11907
|
const broadcastBoard = async (boardId) => {
|
|
11525
11908
|
const board = await store.getBoard(boardId);
|
|
@@ -11528,10 +11911,7 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
11528
11911
|
return;
|
|
11529
11912
|
}
|
|
11530
11913
|
knownRevisions.set(boardId, board.updatedAt);
|
|
11531
|
-
broadcastMessage(
|
|
11532
|
-
type: "kanban.get",
|
|
11533
|
-
payload: { success: true, data: { board } }
|
|
11534
|
-
});
|
|
11914
|
+
broadcastMessage(kanbanBoardMessage(board));
|
|
11535
11915
|
};
|
|
11536
11916
|
const reconcileAfterConnect = async () => {
|
|
11537
11917
|
const summaries = await store.listBoards();
|
|
@@ -11550,20 +11930,36 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
11550
11930
|
}
|
|
11551
11931
|
}
|
|
11552
11932
|
};
|
|
11553
|
-
|
|
11933
|
+
const COALESCE_MS = 300;
|
|
11934
|
+
const pendingBroadcasts = /* @__PURE__ */ new Map();
|
|
11935
|
+
const scheduleBroadcast = (boardId) => {
|
|
11936
|
+
if (pendingBroadcasts.has(boardId)) return;
|
|
11937
|
+
const timer = setTimeout(() => {
|
|
11938
|
+
pendingBroadcasts.delete(boardId);
|
|
11939
|
+
void broadcastBoard(boardId).catch(() => {
|
|
11940
|
+
});
|
|
11941
|
+
}, COALESCE_MS);
|
|
11942
|
+
timer.unref?.();
|
|
11943
|
+
pendingBroadcasts.set(boardId, timer);
|
|
11944
|
+
};
|
|
11945
|
+
const unsubscribe = bridgeKanbanSupervisor(
|
|
11554
11946
|
projectRoot,
|
|
11555
11947
|
async (event) => {
|
|
11948
|
+
const family = event.event?.split(".")[0];
|
|
11949
|
+
if (family !== "board" && family !== "task" && family !== "column") return;
|
|
11556
11950
|
const evData = event.data;
|
|
11557
11951
|
const boardId = evData?.boardId;
|
|
11558
11952
|
if (!boardId) return;
|
|
11559
|
-
|
|
11560
|
-
|
|
11561
|
-
|
|
11562
|
-
|
|
11953
|
+
if (event.event === "board.deleted") {
|
|
11954
|
+
const timer = pendingBroadcasts.get(boardId);
|
|
11955
|
+
if (timer) {
|
|
11956
|
+
clearTimeout(timer);
|
|
11957
|
+
pendingBroadcasts.delete(boardId);
|
|
11563
11958
|
}
|
|
11564
|
-
|
|
11565
|
-
|
|
11959
|
+
broadcastDeleted(boardId);
|
|
11960
|
+
return;
|
|
11566
11961
|
}
|
|
11962
|
+
scheduleBroadcast(boardId);
|
|
11567
11963
|
},
|
|
11568
11964
|
{
|
|
11569
11965
|
autoReconnect: true,
|
|
@@ -11571,6 +11967,11 @@ function subscribeKanbanDaemonEvents(projectRoot, broadcastMessage) {
|
|
|
11571
11967
|
onConnected: reconcileAfterConnect
|
|
11572
11968
|
}
|
|
11573
11969
|
);
|
|
11970
|
+
return () => {
|
|
11971
|
+
for (const timer of pendingBroadcasts.values()) clearTimeout(timer);
|
|
11972
|
+
pendingBroadcasts.clear();
|
|
11973
|
+
unsubscribe();
|
|
11974
|
+
};
|
|
11574
11975
|
}
|
|
11575
11976
|
|
|
11576
11977
|
// src/server/kanban-board-watcher.ts
|
|
@@ -11592,7 +11993,13 @@ function createShutdown(res) {
|
|
|
11592
11993
|
} catch (e) {
|
|
11593
11994
|
log(`[WebUI] Error closing session: ${e instanceof Error ? e.message : String(e)}`);
|
|
11594
11995
|
}
|
|
11595
|
-
for (const ws of res.clients())
|
|
11996
|
+
for (const ws of res.clients()) {
|
|
11997
|
+
try {
|
|
11998
|
+
ws.close();
|
|
11999
|
+
ws.terminate?.();
|
|
12000
|
+
} catch {
|
|
12001
|
+
}
|
|
12002
|
+
}
|
|
11596
12003
|
for (const server of res.servers) server?.close();
|
|
11597
12004
|
if (res.onShutdown) {
|
|
11598
12005
|
try {
|
|
@@ -12029,6 +12436,22 @@ import {
|
|
|
12029
12436
|
restartMcp,
|
|
12030
12437
|
updateMcp
|
|
12031
12438
|
} from "@wrongstack/mcp";
|
|
12439
|
+
async function authorizeMcpMutation(ws, operation, serverName, trustBoundary) {
|
|
12440
|
+
if (!trustBoundary) return true;
|
|
12441
|
+
const authorization = await authorizeWebUIAction(trustBoundary, {
|
|
12442
|
+
capability: "mcp.server.configure",
|
|
12443
|
+
subject: { kind: "process", id: serverName },
|
|
12444
|
+
risk: "elevated",
|
|
12445
|
+
metadata: { transport: "websocket", operation }
|
|
12446
|
+
});
|
|
12447
|
+
if (!authorization.allowed) {
|
|
12448
|
+
send(ws, {
|
|
12449
|
+
type: "mcp.operation_result",
|
|
12450
|
+
payload: { success: false, message: `${operation} denied: ${authorization.reason}` }
|
|
12451
|
+
});
|
|
12452
|
+
}
|
|
12453
|
+
return authorization.allowed;
|
|
12454
|
+
}
|
|
12032
12455
|
function mapStatus(raw) {
|
|
12033
12456
|
switch (raw) {
|
|
12034
12457
|
case "connected":
|
|
@@ -12103,7 +12526,7 @@ async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
|
|
|
12103
12526
|
payload: { servers: servers.map((server) => toView(server, health.get(server.name))) }
|
|
12104
12527
|
});
|
|
12105
12528
|
}
|
|
12106
|
-
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
12529
|
+
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry, trustBoundary) {
|
|
12107
12530
|
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
12108
12531
|
if (!d) return;
|
|
12109
12532
|
const validated = validateMcpServerPayload(msg.payload, "mcp.add");
|
|
@@ -12114,6 +12537,7 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
12114
12537
|
});
|
|
12115
12538
|
return;
|
|
12116
12539
|
}
|
|
12540
|
+
if (!await authorizeMcpMutation(ws, "mcp.add", name(msg), trustBoundary)) return;
|
|
12117
12541
|
const result = await addMcp(validated.value, d);
|
|
12118
12542
|
if (result.ok && result.server) {
|
|
12119
12543
|
send(ws, { type: "mcp.server.added", payload: { server: toView(result.server) } });
|
|
@@ -12131,7 +12555,7 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
12131
12555
|
payload: { success: result.ok, message: result.message }
|
|
12132
12556
|
});
|
|
12133
12557
|
}
|
|
12134
|
-
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
12558
|
+
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry, trustBoundary) {
|
|
12135
12559
|
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
12136
12560
|
if (!d) return;
|
|
12137
12561
|
const validated = validateMcpServerPayload(msg.payload, "mcp.update");
|
|
@@ -12142,6 +12566,7 @@ async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
12142
12566
|
});
|
|
12143
12567
|
return;
|
|
12144
12568
|
}
|
|
12569
|
+
if (!await authorizeMcpMutation(ws, "mcp.update", name(msg), trustBoundary)) return;
|
|
12145
12570
|
const result = await updateMcp(validated.value, d);
|
|
12146
12571
|
if (result.ok && result.server) {
|
|
12147
12572
|
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
@@ -13285,7 +13710,7 @@ function openBrowser(url, platform = process.platform) {
|
|
|
13285
13710
|
}
|
|
13286
13711
|
|
|
13287
13712
|
// src/server/port-utils.ts
|
|
13288
|
-
import * as
|
|
13713
|
+
import * as net2 from "node:net";
|
|
13289
13714
|
import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
13290
13715
|
var SURFACE_DEFAULT_PORTS = {
|
|
13291
13716
|
webui: { http: 3456 },
|
|
@@ -13299,7 +13724,7 @@ function getSurfaceDefaultPorts(surface) {
|
|
|
13299
13724
|
}
|
|
13300
13725
|
function isPortFree(host, port) {
|
|
13301
13726
|
return new Promise((resolve16) => {
|
|
13302
|
-
const srv =
|
|
13727
|
+
const srv = net2.createServer();
|
|
13303
13728
|
srv.once("error", () => resolve16(false));
|
|
13304
13729
|
srv.once("listening", () => {
|
|
13305
13730
|
srv.close(() => resolve16(true));
|
|
@@ -13560,7 +13985,8 @@ function registerWebuiInstance(p, deps2 = {}) {
|
|
|
13560
13985
|
host: p.host,
|
|
13561
13986
|
port: p.httpPort,
|
|
13562
13987
|
publicUrl: p.publicUrl
|
|
13563
|
-
})
|
|
13988
|
+
}),
|
|
13989
|
+
...p.authToken ? { authToken: p.authToken } : {}
|
|
13564
13990
|
},
|
|
13565
13991
|
p.registryBaseDir
|
|
13566
13992
|
).catch(() => {
|
|
@@ -13594,10 +14020,31 @@ ${extraBlock}`
|
|
|
13594
14020
|
if (p.open) launch(openUrl);
|
|
13595
14021
|
});
|
|
13596
14022
|
}
|
|
14023
|
+
var DEFAULT_CHILD_CLEANUP_TIMEOUT_MS = 1e4;
|
|
14024
|
+
async function runBounded(work, timeoutMs, label, debug) {
|
|
14025
|
+
let timer;
|
|
14026
|
+
try {
|
|
14027
|
+
await Promise.race([
|
|
14028
|
+
Promise.resolve().then(() => work()).catch((err) => {
|
|
14029
|
+
debug(`[webui-server] ${label} failed: ${err}`);
|
|
14030
|
+
}),
|
|
14031
|
+
new Promise((resolve16) => {
|
|
14032
|
+
timer = setTimeout(() => {
|
|
14033
|
+
debug(`[webui-server] ${label} timed out after ${timeoutMs}ms`);
|
|
14034
|
+
resolve16();
|
|
14035
|
+
}, timeoutMs);
|
|
14036
|
+
timer.unref?.();
|
|
14037
|
+
})
|
|
14038
|
+
]);
|
|
14039
|
+
} finally {
|
|
14040
|
+
if (timer) clearTimeout(timer);
|
|
14041
|
+
}
|
|
14042
|
+
}
|
|
13597
14043
|
function createWebuiShutdown(res) {
|
|
13598
14044
|
const log = res.log ?? ((m) => console.log(m));
|
|
13599
14045
|
const debug = res.debug ?? ((m) => console.debug(m));
|
|
13600
14046
|
const unregister = res.unregisterFn ?? unregisterInstance;
|
|
14047
|
+
const childTimeout = Math.max(1, res.childCleanupTimeoutMs ?? DEFAULT_CHILD_CLEANUP_TIMEOUT_MS);
|
|
13601
14048
|
let started = false;
|
|
13602
14049
|
return () => {
|
|
13603
14050
|
if (started) return;
|
|
@@ -13605,17 +14052,30 @@ function createWebuiShutdown(res) {
|
|
|
13605
14052
|
log("[WebUI] Shutting down...");
|
|
13606
14053
|
res.abortInFlight();
|
|
13607
14054
|
res.unsubscribeEvents();
|
|
13608
|
-
|
|
13609
|
-
|
|
13610
|
-
|
|
13611
|
-
|
|
13612
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
14055
|
+
void (async () => {
|
|
14056
|
+
if (res.stopOwnedChildren) {
|
|
14057
|
+
await runBounded(res.stopOwnedChildren, childTimeout, "stopOwnedChildren", debug);
|
|
14058
|
+
}
|
|
14059
|
+
if (res.disposeResources) {
|
|
14060
|
+
await runBounded(res.disposeResources, Math.min(childTimeout, 5e3), "disposeResources", debug);
|
|
14061
|
+
}
|
|
14062
|
+
res.closeClients();
|
|
14063
|
+
res.closeHttpServer();
|
|
14064
|
+
const unregistered = unregister(res.pid, res.registryBaseDir).catch(
|
|
14065
|
+
(err) => debug(`[webui-server] unregister failed: ${err}`)
|
|
14066
|
+
);
|
|
14067
|
+
await new Promise((resolve16) => {
|
|
14068
|
+
res.wss.close(() => resolve16());
|
|
13618
14069
|
});
|
|
14070
|
+
await unregistered;
|
|
14071
|
+
log("[WebUI] Server stopped");
|
|
14072
|
+
res.onStopped();
|
|
14073
|
+
})().catch((err) => {
|
|
14074
|
+
debug(`[webui-server] shutdown sequence failed: ${err}`);
|
|
14075
|
+
try {
|
|
14076
|
+
res.onStopped();
|
|
14077
|
+
} catch {
|
|
14078
|
+
}
|
|
13619
14079
|
});
|
|
13620
14080
|
};
|
|
13621
14081
|
}
|
|
@@ -14096,12 +14556,9 @@ function createKanbanRunMirror(deps2) {
|
|
|
14096
14556
|
}
|
|
14097
14557
|
async function publish(board) {
|
|
14098
14558
|
if (board) {
|
|
14099
|
-
broadcast2(
|
|
14559
|
+
broadcast2(kanbanBoardMessage(board));
|
|
14100
14560
|
}
|
|
14101
|
-
broadcast2(
|
|
14102
|
-
type: "kanban.list",
|
|
14103
|
-
payload: { success: true, data: await listBoards3(projectRoot) }
|
|
14104
|
-
});
|
|
14561
|
+
broadcast2(kanbanListMessage(await listBoards3(projectRoot)));
|
|
14105
14562
|
}
|
|
14106
14563
|
async function projectSdd(runId, snapshot) {
|
|
14107
14564
|
const k = mapKey("sdd", runId);
|
|
@@ -14504,14 +14961,11 @@ function createKanbanSupervisor(deps2) {
|
|
|
14504
14961
|
publish(snapshot);
|
|
14505
14962
|
const changedBoard = recovered?.board ?? gateSwept ?? reconciled?.board;
|
|
14506
14963
|
if (changedBoard) {
|
|
14507
|
-
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
type: "kanban.list",
|
|
14513
|
-
payload: { success: true, data: await listBoards4(deps2.projectRoot) }
|
|
14514
|
-
});
|
|
14964
|
+
await publishKanbanBoard(
|
|
14965
|
+
deps2.broadcast,
|
|
14966
|
+
changedBoard,
|
|
14967
|
+
() => listBoards4(deps2.projectRoot)
|
|
14968
|
+
);
|
|
14515
14969
|
}
|
|
14516
14970
|
if (config.mode === "agentic" && anomalyCount > 0) {
|
|
14517
14971
|
await maybeRunAgent(board, config, health, snapshot);
|
|
@@ -14723,6 +15177,8 @@ function buildAuditPrompt(board, health) {
|
|
|
14723
15177
|
|
|
14724
15178
|
// src/server/context-meta.ts
|
|
14725
15179
|
import { FallbackProfileManager } from "@wrongstack/core/agent";
|
|
15180
|
+
import { resolvePluginEnablement } from "@wrongstack/core/plugin";
|
|
15181
|
+
import { FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
|
|
14726
15182
|
function seedContextMeta(config, context) {
|
|
14727
15183
|
const meta = context.meta;
|
|
14728
15184
|
const autonomyCfg = config.autonomy ?? {};
|
|
@@ -14738,6 +15194,7 @@ function seedContextMeta(config, context) {
|
|
|
14738
15194
|
meta["enhanceDelayMs"] = autonomyCfg["enhanceDelayMs"] ?? 6e4;
|
|
14739
15195
|
meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
|
|
14740
15196
|
meta["nextPrediction"] = config.nextPrediction ?? false;
|
|
15197
|
+
meta["nextStepsTool"] = config.tools?.nextsteps?.enabled === true;
|
|
14741
15198
|
meta["fallbackModels"] = config.fallbackModels ?? [];
|
|
14742
15199
|
meta["fallbackBridge"] = config.fallbackBridge ?? "";
|
|
14743
15200
|
meta["fallbackProfiles"] = config.fallbackProfiles ?? {};
|
|
@@ -14799,6 +15256,21 @@ function seedContextMeta(config, context) {
|
|
|
14799
15256
|
meta["tgDelegate"] = tgExt?.["notifyOnDelegate"] !== false;
|
|
14800
15257
|
const tgMs = tgExt?.["longToolThresholdMs"];
|
|
14801
15258
|
meta["tgLongToolMs"] = typeof tgMs === "number" ? tgMs : 3e4;
|
|
15259
|
+
{
|
|
15260
|
+
const pluginsEnabled = {};
|
|
15261
|
+
const record2 = (name2) => {
|
|
15262
|
+
if (FORBIDDEN_PROTO_KEYS2.has(name2) || name2 in pluginsEnabled) return;
|
|
15263
|
+
pluginsEnabled[name2] = resolvePluginEnablement({ name: name2, config }).enabled;
|
|
15264
|
+
};
|
|
15265
|
+
for (const entry of config.plugins ?? []) {
|
|
15266
|
+
const name2 = typeof entry === "string" ? entry : entry?.name;
|
|
15267
|
+
if (typeof name2 === "string") record2(name2);
|
|
15268
|
+
}
|
|
15269
|
+
for (const [name2, options] of Object.entries(config.extensions ?? {})) {
|
|
15270
|
+
if (typeof options?.["enabled"] === "boolean") record2(name2);
|
|
15271
|
+
}
|
|
15272
|
+
if (Object.keys(pluginsEnabled).length > 0) meta["pluginsEnabled"] = pluginsEnabled;
|
|
15273
|
+
}
|
|
14802
15274
|
const chimeraExt = config.extensions?.["wstack-chimera"];
|
|
14803
15275
|
meta["chimeraEnabled"] = chimeraExt?.["enabled"] === true;
|
|
14804
15276
|
meta["chimeraProvider"] = chimeraExt?.["provider"] ?? "";
|
|
@@ -14811,6 +15283,7 @@ function seedContextMeta(config, context) {
|
|
|
14811
15283
|
meta["autoReviewProvider"] = autoReviewExt?.["provider"] ?? "";
|
|
14812
15284
|
meta["autoReviewModel"] = autoReviewExt?.["model"] ?? "";
|
|
14813
15285
|
meta["autoReviewFallbackProfile"] = autoReviewExt?.["fallbackProfile"] ?? "";
|
|
15286
|
+
meta["autoReviewModelSelection"] = autoReviewExt?.["modelSelection"] === "random" ? "random" : "round-robin";
|
|
14814
15287
|
meta["autoReviewFallbackModels"] = Array.isArray(autoReviewExt?.["fallbackModels"]) ? autoReviewExt?.["fallbackModels"] : [];
|
|
14815
15288
|
meta["autoReviewDebounceMs"] = typeof autoReviewExt?.["debounceMs"] === "number" && autoReviewExt["debounceMs"] >= 0 ? autoReviewExt["debounceMs"] : 15e3;
|
|
14816
15289
|
meta["autoReviewMaxFilesPerBatch"] = typeof autoReviewExt?.["maxFilesPerBatch"] === "number" && autoReviewExt["maxFilesPerBatch"] >= 1 ? autoReviewExt["maxFilesPerBatch"] : 15;
|
|
@@ -14836,8 +15309,9 @@ function seedContextMeta(config, context) {
|
|
|
14836
15309
|
// src/server/pref-helpers.ts
|
|
14837
15310
|
import * as fs13 from "node:fs/promises";
|
|
14838
15311
|
import * as path18 from "node:path";
|
|
15312
|
+
import { pluginEntryMatchesName } from "@wrongstack/core/plugin";
|
|
14839
15313
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
14840
|
-
import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as
|
|
15314
|
+
import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS3 } from "@wrongstack/core/utils";
|
|
14841
15315
|
var PREF_KEYS = [
|
|
14842
15316
|
"autonomy",
|
|
14843
15317
|
"autonomyDelayMs",
|
|
@@ -14847,6 +15321,7 @@ var PREF_KEYS = [
|
|
|
14847
15321
|
"chime",
|
|
14848
15322
|
"confirmExit",
|
|
14849
15323
|
"nextPrediction",
|
|
15324
|
+
"nextStepsTool",
|
|
14850
15325
|
"enhanceEnabled",
|
|
14851
15326
|
"enhanceDelayMs",
|
|
14852
15327
|
"enhanceLanguage",
|
|
@@ -14910,6 +15385,7 @@ var PREF_KEYS = [
|
|
|
14910
15385
|
"autoReviewProvider",
|
|
14911
15386
|
"autoReviewModel",
|
|
14912
15387
|
"autoReviewFallbackProfile",
|
|
15388
|
+
"autoReviewModelSelection",
|
|
14913
15389
|
"autoReviewFallbackModels",
|
|
14914
15390
|
"autoReviewDebounceMs",
|
|
14915
15391
|
"autoReviewMaxFilesPerBatch",
|
|
@@ -14918,6 +15394,8 @@ var PREF_KEYS = [
|
|
|
14918
15394
|
// Display-only toggles (purely visual WebUI prefs, not persisted to config).
|
|
14919
15395
|
"groupToolCalls",
|
|
14920
15396
|
"showThinkingLogs",
|
|
15397
|
+
// v15: chat-input auto-collapse (opt-in display toggle, default off).
|
|
15398
|
+
"autoCollapseInput",
|
|
14921
15399
|
// Per-plugin enable/disable map (parity with the embedded server).
|
|
14922
15400
|
"pluginsEnabled",
|
|
14923
15401
|
// Fleet chat verbosity: off | full (migrated from streamFleet boolean).
|
|
@@ -14972,6 +15450,8 @@ async function updateGlobalConfig(deps2, holder, mutate, errorLabel) {
|
|
|
14972
15450
|
var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set([
|
|
14973
15451
|
"groupToolCalls",
|
|
14974
15452
|
"showThinkingLogs",
|
|
15453
|
+
// v15: chat-input auto-collapse (opt-in display toggle, default off).
|
|
15454
|
+
"autoCollapseInput",
|
|
14975
15455
|
"autoReviewFallbackModels",
|
|
14976
15456
|
// v11 Display parity: agent-swarm panel + inverse fsAccess flag.
|
|
14977
15457
|
// The TUI settings picker mirrors these so the browser can keep the
|
|
@@ -15119,6 +15599,11 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
15119
15599
|
toolsCfg.maxIterations = payload["maxIterations"];
|
|
15120
15600
|
decrypted.tools = toolsCfg;
|
|
15121
15601
|
}
|
|
15602
|
+
if (typeof payload["nextStepsTool"] === "boolean") {
|
|
15603
|
+
const toolsCfg = decrypted.tools ?? {};
|
|
15604
|
+
toolsCfg.nextsteps = { enabled: payload["nextStepsTool"] };
|
|
15605
|
+
decrypted.tools = toolsCfg;
|
|
15606
|
+
}
|
|
15122
15607
|
const hqTouched = typeof payload["hqEnabled"] === "boolean" || typeof payload["hqUrl"] === "string" || typeof payload["hqToken"] === "string" || typeof payload["hqRawContent"] === "boolean";
|
|
15123
15608
|
if (hqTouched) {
|
|
15124
15609
|
const hqCfg = decrypted.hq ?? {};
|
|
@@ -15182,15 +15667,29 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
15182
15667
|
decrypted.debugStream = payload["debugStream"];
|
|
15183
15668
|
if (typeof payload["pluginsEnabled"] === "object" && payload["pluginsEnabled"] !== null) {
|
|
15184
15669
|
const ext = decrypted.extensions ?? {};
|
|
15670
|
+
const toggled = [];
|
|
15185
15671
|
for (const [pluginName, enabled] of Object.entries(
|
|
15186
15672
|
payload["pluginsEnabled"]
|
|
15187
15673
|
)) {
|
|
15188
|
-
if (
|
|
15674
|
+
if (FORBIDDEN_PROTO_KEYS3.has(pluginName)) continue;
|
|
15675
|
+
if (typeof enabled !== "boolean") continue;
|
|
15189
15676
|
const pExt = ext[pluginName] ?? {};
|
|
15190
15677
|
pExt["enabled"] = enabled;
|
|
15191
15678
|
ext[pluginName] = pExt;
|
|
15679
|
+
toggled.push([pluginName, enabled]);
|
|
15192
15680
|
}
|
|
15193
15681
|
decrypted.extensions = ext;
|
|
15682
|
+
if (Array.isArray(decrypted.plugins) && toggled.length > 0) {
|
|
15683
|
+
decrypted.plugins = decrypted.plugins.map((entry) => {
|
|
15684
|
+
const entryName = typeof entry === "string" ? entry : entry?.name;
|
|
15685
|
+
if (typeof entryName !== "string") return entry;
|
|
15686
|
+
const hit = toggled.find(([name2]) => pluginEntryMatchesName(entryName, name2));
|
|
15687
|
+
if (!hit) return entry;
|
|
15688
|
+
const [, enabled] = hit;
|
|
15689
|
+
if (typeof entry === "string") return enabled ? entry : { name: entry, enabled: false };
|
|
15690
|
+
return { ...entry, enabled };
|
|
15691
|
+
});
|
|
15692
|
+
}
|
|
15194
15693
|
}
|
|
15195
15694
|
const chimeraTouched = typeof payload["chimeraEnabled"] === "boolean" || typeof payload["chimeraProvider"] === "string" || typeof payload["chimeraModel"] === "string" || typeof payload["chimeraMaxFiles"] === "number" || typeof payload["chimeraAutoFix"] === "string";
|
|
15196
15695
|
if (chimeraTouched) {
|
|
@@ -15212,7 +15711,7 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
15212
15711
|
ext["wstack-chimera"] = chimera;
|
|
15213
15712
|
decrypted.extensions = ext;
|
|
15214
15713
|
}
|
|
15215
|
-
const autoReviewTouched = typeof payload["autoReviewEnabled"] === "boolean" || typeof payload["autoReviewProvider"] === "string" || typeof payload["autoReviewModel"] === "string" || typeof payload["autoReviewFallbackProfile"] === "string" || Array.isArray(payload["autoReviewFallbackModels"]) || typeof payload["autoReviewDebounceMs"] === "number" || typeof payload["autoReviewMaxFilesPerBatch"] === "number" || typeof payload["autoReviewMaxConcurrentReviews"] === "number" || typeof payload["autoReviewCascadeOn"] === "string";
|
|
15714
|
+
const autoReviewTouched = typeof payload["autoReviewEnabled"] === "boolean" || typeof payload["autoReviewProvider"] === "string" || typeof payload["autoReviewModel"] === "string" || typeof payload["autoReviewFallbackProfile"] === "string" || typeof payload["autoReviewModelSelection"] === "string" || Array.isArray(payload["autoReviewFallbackModels"]) || typeof payload["autoReviewDebounceMs"] === "number" || typeof payload["autoReviewMaxFilesPerBatch"] === "number" || typeof payload["autoReviewMaxConcurrentReviews"] === "number" || typeof payload["autoReviewCascadeOn"] === "string";
|
|
15216
15715
|
if (autoReviewTouched) {
|
|
15217
15716
|
const ext = decrypted.extensions ?? {};
|
|
15218
15717
|
const ar = ext["wstack-auto-review"] ?? {};
|
|
@@ -15229,6 +15728,9 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
15229
15728
|
ar["fallbackProfile"] = payload["autoReviewFallbackProfile"];
|
|
15230
15729
|
}
|
|
15231
15730
|
}
|
|
15731
|
+
if (payload["autoReviewModelSelection"] === "round-robin" || payload["autoReviewModelSelection"] === "random") {
|
|
15732
|
+
ar["modelSelection"] = payload["autoReviewModelSelection"];
|
|
15733
|
+
}
|
|
15232
15734
|
if (typeof payload["autoReviewDebounceMs"] === "number" && payload["autoReviewDebounceMs"] >= 0) {
|
|
15233
15735
|
ar["debounceMs"] = payload["autoReviewDebounceMs"];
|
|
15234
15736
|
}
|
|
@@ -15299,39 +15801,6 @@ async function handlePrefsRoute(ws, msg, handlers) {
|
|
|
15299
15801
|
// src/server/process-handlers.ts
|
|
15300
15802
|
import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
|
|
15301
15803
|
import { getProcessRegistry as getProcessRegistry2 } from "@wrongstack/tools";
|
|
15302
|
-
|
|
15303
|
-
// src/server/privileged-actions.ts
|
|
15304
|
-
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
15305
|
-
import {
|
|
15306
|
-
isTrustDecisionAllowed
|
|
15307
|
-
} from "@wrongstack/core/security";
|
|
15308
|
-
async function authorizeWebUIAction(boundary, action, logger) {
|
|
15309
|
-
const request = {
|
|
15310
|
-
version: 1,
|
|
15311
|
-
requestId: randomUUID4(),
|
|
15312
|
-
actor: {
|
|
15313
|
-
kind: "remote-client",
|
|
15314
|
-
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
15315
|
-
},
|
|
15316
|
-
surface: "webui",
|
|
15317
|
-
capability: action.capability,
|
|
15318
|
-
subject: action.subject,
|
|
15319
|
-
risk: action.risk,
|
|
15320
|
-
scope: {
|
|
15321
|
-
...action.cwd ? { cwd: action.cwd } : {},
|
|
15322
|
-
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
15323
|
-
},
|
|
15324
|
-
authContext: { method: "session" },
|
|
15325
|
-
...action.metadata ? { metadata: action.metadata } : {}
|
|
15326
|
-
};
|
|
15327
|
-
const decision = await boundary.evaluate(request);
|
|
15328
|
-
logger?.debug?.(
|
|
15329
|
-
`[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
|
|
15330
|
-
);
|
|
15331
|
-
return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
|
|
15332
|
-
}
|
|
15333
|
-
|
|
15334
|
-
// src/server/process-handlers.ts
|
|
15335
15804
|
function handleProcessList(ws) {
|
|
15336
15805
|
try {
|
|
15337
15806
|
const procs = getProcessRegistry2().list();
|
|
@@ -15630,6 +16099,7 @@ function createProjectHandlers(ctx) {
|
|
|
15630
16099
|
ctx.context.session = next;
|
|
15631
16100
|
ctx.context.state.replaceMessages([]);
|
|
15632
16101
|
ctx.context.state.replaceTodos([]);
|
|
16102
|
+
ctx.context.clearMemoryEvidence?.();
|
|
15633
16103
|
ctx.context.readFiles.clear();
|
|
15634
16104
|
ctx.context.fileMtimes.clear();
|
|
15635
16105
|
ctx.tokenCounter.reset();
|
|
@@ -15674,7 +16144,7 @@ function createProjectHandlers(ctx) {
|
|
|
15674
16144
|
}
|
|
15675
16145
|
|
|
15676
16146
|
// src/server/provider-handlers.ts
|
|
15677
|
-
import { resolveProviderModelList } from "@wrongstack/core/models";
|
|
16147
|
+
import { hasProviderCredential, resolveProviderModelList } from "@wrongstack/core/models";
|
|
15678
16148
|
import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
|
|
15679
16149
|
import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
|
|
15680
16150
|
import {
|
|
@@ -15981,7 +16451,7 @@ function createProviderOperations(deps2) {
|
|
|
15981
16451
|
}
|
|
15982
16452
|
try {
|
|
15983
16453
|
const providers = await deps2.modelsRegistry.listProviders();
|
|
15984
|
-
const
|
|
16454
|
+
const savedProviders = await loadConfigProviders();
|
|
15985
16455
|
sendMessage(ws, {
|
|
15986
16456
|
type: "provider.catalog",
|
|
15987
16457
|
payload: {
|
|
@@ -15992,7 +16462,7 @@ function createProviderOperations(deps2) {
|
|
|
15992
16462
|
apiBase: provider.apiBase,
|
|
15993
16463
|
envVars: provider.envVars,
|
|
15994
16464
|
modelCount: provider.models.length,
|
|
15995
|
-
hasApiKey:
|
|
16465
|
+
hasApiKey: hasProviderCredential(provider, { providers: savedProviders })
|
|
15996
16466
|
}))
|
|
15997
16467
|
}
|
|
15998
16468
|
});
|
|
@@ -16155,8 +16625,10 @@ function createProviderOperations(deps2) {
|
|
|
16155
16625
|
if (result.ok) {
|
|
16156
16626
|
deps2.log?.(`[WebUI] Provider "${payload.id}" added via provider.add`);
|
|
16157
16627
|
}
|
|
16628
|
+
return result.ok;
|
|
16158
16629
|
} catch (err) {
|
|
16159
16630
|
sendOperationResult(ws, false, errMessage(err));
|
|
16631
|
+
return false;
|
|
16160
16632
|
}
|
|
16161
16633
|
}
|
|
16162
16634
|
async function handleProviderRemove(ws, providerId) {
|
|
@@ -16537,9 +17009,11 @@ var CLIENT_CONVERSATION_MESSAGE_TYPES = [
|
|
|
16537
17009
|
"ping",
|
|
16538
17010
|
"user_message",
|
|
16539
17011
|
"tool.confirm_result",
|
|
17012
|
+
"topic.advice",
|
|
16540
17013
|
"completion.request",
|
|
16541
17014
|
"model.switch",
|
|
16542
17015
|
"model.refine",
|
|
17016
|
+
"model.fallback_choice",
|
|
16543
17017
|
"autonomy.switch",
|
|
16544
17018
|
"context.clear",
|
|
16545
17019
|
"context.compact",
|
|
@@ -16811,6 +17285,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
|
|
|
16811
17285
|
"provider.active_blocked",
|
|
16812
17286
|
"provider.error",
|
|
16813
17287
|
"provider.fallback",
|
|
17288
|
+
"provider.fallback_pending",
|
|
16814
17289
|
"provider.response",
|
|
16815
17290
|
"provider.retry",
|
|
16816
17291
|
"provider.status_changed",
|
|
@@ -16837,6 +17312,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
|
|
|
16837
17312
|
"tool.loop_detected",
|
|
16838
17313
|
"tool.progress",
|
|
16839
17314
|
"tool.started",
|
|
17315
|
+
"topic.advice_result",
|
|
16840
17316
|
"tools.list",
|
|
16841
17317
|
"trust.persisted"
|
|
16842
17318
|
];
|
|
@@ -16852,6 +17328,7 @@ var SERVER_COLLABORATION_MESSAGE_TYPES = [
|
|
|
16852
17328
|
"collab.state",
|
|
16853
17329
|
"mailbox.action_result",
|
|
16854
17330
|
"mailbox.agent_registered",
|
|
17331
|
+
"mailbox.agent_deregistered",
|
|
16855
17332
|
"mailbox.agents",
|
|
16856
17333
|
"mailbox.cleared",
|
|
16857
17334
|
"mailbox.compacted",
|
|
@@ -17231,6 +17708,13 @@ function projectToolMessage(message) {
|
|
|
17231
17708
|
}
|
|
17232
17709
|
return null;
|
|
17233
17710
|
}
|
|
17711
|
+
function optionalFinite(value) {
|
|
17712
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
17713
|
+
return value;
|
|
17714
|
+
}
|
|
17715
|
+
function optionalString2(value) {
|
|
17716
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
17717
|
+
}
|
|
17234
17718
|
function projectFleetMessage(message) {
|
|
17235
17719
|
const payload = record(message.payload);
|
|
17236
17720
|
if (!payload) return null;
|
|
@@ -17239,7 +17723,15 @@ function projectFleetMessage(message) {
|
|
|
17239
17723
|
return {
|
|
17240
17724
|
kind: "concurrency",
|
|
17241
17725
|
active: finite(payload["fleetConcurrency"]),
|
|
17242
|
-
maximum: finite(payload["fleetConcurrencyMax"])
|
|
17726
|
+
maximum: finite(payload["fleetConcurrencyMax"]),
|
|
17727
|
+
maxSpawns: optionalFinite(payload["maxSpawns"]),
|
|
17728
|
+
usedSpawns: optionalFinite(payload["usedSpawns"]),
|
|
17729
|
+
remainingSpawns: optionalFinite(payload["remainingSpawns"]),
|
|
17730
|
+
maxSpawnsSource: optionalString2(payload["maxSpawnsSource"]),
|
|
17731
|
+
maxConcurrentSource: optionalString2(payload["maxConcurrentSource"]),
|
|
17732
|
+
effectiveSource: optionalString2(payload["effectiveSource"]),
|
|
17733
|
+
checkpointMaxSpawns: optionalFinite(payload["checkpointMaxSpawns"]),
|
|
17734
|
+
ceilingMismatch: payload["ceilingMismatch"] === true ? true : void 0
|
|
17243
17735
|
};
|
|
17244
17736
|
case "client.status_update":
|
|
17245
17737
|
return { kind: "client-status", status: payload };
|
|
@@ -17330,6 +17822,8 @@ var SURFACE_PROTOCOL_CAPABILITIES = [
|
|
|
17330
17822
|
"chronicle.metrics",
|
|
17331
17823
|
"chronicle.status",
|
|
17332
17824
|
"connections.health",
|
|
17825
|
+
/** Bounded topic-shift advice plus same-session provider-context boundaries. */
|
|
17826
|
+
"context.topic-boundary",
|
|
17333
17827
|
/** Interview resume/discard + lastAgentText/lastRunId continuity. */
|
|
17334
17828
|
"sdd.interview.continuity",
|
|
17335
17829
|
/** Launch multi-agent runs from a graph id or resolved spec id. */
|
|
@@ -17734,6 +18228,7 @@ function createSessionHandlers(ctx) {
|
|
|
17734
18228
|
await ctx.onBeforeSessionTodosReplaced?.(next.id, sessionsDirectory());
|
|
17735
18229
|
ctx.context.state.replaceTodos(todos);
|
|
17736
18230
|
resetContextAccounting();
|
|
18231
|
+
ctx.context.clearMemoryEvidence?.();
|
|
17737
18232
|
ctx.context.readFiles.clear();
|
|
17738
18233
|
ctx.context.fileMtimes.clear();
|
|
17739
18234
|
ctx.context.state.setMeta?.(
|
|
@@ -17781,6 +18276,7 @@ function createSessionHandlers(ctx) {
|
|
|
17781
18276
|
ctx.context.state.replaceMessages([]);
|
|
17782
18277
|
ctx.context.state.replaceTodos([]);
|
|
17783
18278
|
resetContextAccounting();
|
|
18279
|
+
ctx.context.clearMemoryEvidence?.();
|
|
17784
18280
|
ctx.context.readFiles.clear();
|
|
17785
18281
|
ctx.context.fileMtimes.clear();
|
|
17786
18282
|
ctx.tokenCounter.reset?.();
|
|
@@ -17795,6 +18291,7 @@ function createSessionHandlers(ctx) {
|
|
|
17795
18291
|
ctx.context.state.replaceMessages([]);
|
|
17796
18292
|
ctx.context.state.replaceTodos([]);
|
|
17797
18293
|
resetContextAccounting();
|
|
18294
|
+
ctx.context.clearMemoryEvidence?.();
|
|
17798
18295
|
ctx.context.readFiles.clear();
|
|
17799
18296
|
ctx.context.fileMtimes.clear();
|
|
17800
18297
|
ctx.tokenCounter.reset?.();
|
|
@@ -17895,6 +18392,7 @@ function createSessionHandlers(ctx) {
|
|
|
17895
18392
|
tools: ctx.listTools?.() ?? ctx.toolRegistry?.list(),
|
|
17896
18393
|
baseRevision: typeof payload["baseRevision"] === "string" ? payload["baseRevision"] : "",
|
|
17897
18394
|
messages: payload["messages"],
|
|
18395
|
+
removals: payload["removals"],
|
|
17898
18396
|
allowRepair: payload["allowRepair"] === true,
|
|
17899
18397
|
runActive: ctx.isRunActive?.() === true
|
|
17900
18398
|
});
|
|
@@ -17911,6 +18409,7 @@ function createSessionHandlers(ctx) {
|
|
|
17911
18409
|
tools: ctx.listTools?.() ?? ctx.toolRegistry?.list(),
|
|
17912
18410
|
baseRevision: typeof payload["baseRevision"] === "string" ? payload["baseRevision"] : "",
|
|
17913
18411
|
messages: payload["messages"],
|
|
18412
|
+
removals: payload["removals"],
|
|
17914
18413
|
allowRepair: payload["allowRepair"] === true,
|
|
17915
18414
|
runActive: ctx.isRunActive?.() === true
|
|
17916
18415
|
});
|
|
@@ -18892,6 +19391,19 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
|
|
|
18892
19391
|
return true;
|
|
18893
19392
|
}
|
|
18894
19393
|
|
|
19394
|
+
// src/server/fallback-choice.ts
|
|
19395
|
+
function emitFallbackChoice(events, msg) {
|
|
19396
|
+
const parsed = validateModelFallbackChoicePayload(msg.payload);
|
|
19397
|
+
if (!parsed.ok) return parsed;
|
|
19398
|
+
events?.emit("provider.fallback_choice", {
|
|
19399
|
+
requestId: parsed.value.requestId,
|
|
19400
|
+
...parsed.value.providerId ? { providerId: parsed.value.providerId } : {},
|
|
19401
|
+
...parsed.value.model ? { model: parsed.value.model } : {},
|
|
19402
|
+
...parsed.value.autoSwitch ? { autoSwitch: true } : {}
|
|
19403
|
+
});
|
|
19404
|
+
return { ok: true };
|
|
19405
|
+
}
|
|
19406
|
+
|
|
18895
19407
|
// src/server/agent-roster-routes.ts
|
|
18896
19408
|
async function handleAgentRosterRoute(ws, msg, handlers) {
|
|
18897
19409
|
if (!msg.type.startsWith("agent-roster.")) return false;
|
|
@@ -19056,13 +19568,17 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19056
19568
|
case "model.refine":
|
|
19057
19569
|
await routes.refineModel(ws, msg);
|
|
19058
19570
|
return true;
|
|
19571
|
+
case "model.fallback_choice":
|
|
19572
|
+
await routes.fallbackChoice(ws, msg);
|
|
19573
|
+
return true;
|
|
19059
19574
|
case "key.add":
|
|
19060
19575
|
case "key.update": {
|
|
19061
19576
|
const payload = asPayloadRecord(msg);
|
|
19062
19577
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19063
19578
|
const label = payload ? requiredString(payload, "label") : null;
|
|
19064
19579
|
const apiKey = payload ? requiredString(payload, "apiKey") : null;
|
|
19065
|
-
if (!providerId || !label || !apiKey)
|
|
19580
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label || !apiKey)
|
|
19581
|
+
return invalidPayload(ws, msg.type);
|
|
19066
19582
|
await routes.providerHandlers.handleKeyUpsert(ws, providerId, label, apiKey);
|
|
19067
19583
|
return true;
|
|
19068
19584
|
}
|
|
@@ -19070,7 +19586,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19070
19586
|
const payload = asPayloadRecord(msg);
|
|
19071
19587
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19072
19588
|
const label = payload ? requiredString(payload, "label") : null;
|
|
19073
|
-
if (!providerId || !
|
|
19589
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label)
|
|
19590
|
+
return invalidPayload(ws, msg.type);
|
|
19074
19591
|
await routes.providerHandlers.handleKeyDelete(ws, providerId, label);
|
|
19075
19592
|
return true;
|
|
19076
19593
|
}
|
|
@@ -19078,7 +19595,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19078
19595
|
const payload = asPayloadRecord(msg);
|
|
19079
19596
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19080
19597
|
const label = payload ? requiredString(payload, "label") : null;
|
|
19081
|
-
if (!providerId || !
|
|
19598
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label)
|
|
19599
|
+
return invalidPayload(ws, msg.type);
|
|
19082
19600
|
await routes.providerHandlers.handleKeySetActive(ws, providerId, label);
|
|
19083
19601
|
return true;
|
|
19084
19602
|
}
|
|
@@ -19090,11 +19608,11 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19090
19608
|
const apiKey = payload?.["apiKey"];
|
|
19091
19609
|
const models = payload ? optionalStringArray(payload, "models") : null;
|
|
19092
19610
|
const customModels = payload ? optionalCustomModels(payload) : null;
|
|
19093
|
-
if (!id || !family) return invalidPayload(ws, msg.type);
|
|
19611
|
+
if (!id || !SAFE_CONFIG_KEY.test(id) || !family) return invalidPayload(ws, msg.type);
|
|
19094
19612
|
if (baseUrl !== void 0 && typeof baseUrl !== "string") return invalidPayload(ws, msg.type);
|
|
19095
19613
|
if (apiKey !== void 0 && typeof apiKey !== "string") return invalidPayload(ws, msg.type);
|
|
19096
19614
|
if (models === null || customModels === null) return invalidPayload(ws, msg.type);
|
|
19097
|
-
await routes.providerHandlers.handleProviderAdd(ws, {
|
|
19615
|
+
const added = await routes.providerHandlers.handleProviderAdd(ws, {
|
|
19098
19616
|
id,
|
|
19099
19617
|
family,
|
|
19100
19618
|
baseUrl,
|
|
@@ -19102,20 +19620,22 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19102
19620
|
models,
|
|
19103
19621
|
customModels
|
|
19104
19622
|
});
|
|
19105
|
-
|
|
19623
|
+
if (added) {
|
|
19624
|
+
void routes.adoptDefaultProviderIfUnset(id).catch(() => void 0);
|
|
19625
|
+
}
|
|
19106
19626
|
return true;
|
|
19107
19627
|
}
|
|
19108
19628
|
case "provider.remove": {
|
|
19109
19629
|
const payload = asPayloadRecord(msg);
|
|
19110
19630
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19111
|
-
if (!providerId) return invalidPayload(ws, msg.type);
|
|
19631
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId)) return invalidPayload(ws, msg.type);
|
|
19112
19632
|
await routes.providerHandlers.handleProviderRemove(ws, providerId);
|
|
19113
19633
|
return true;
|
|
19114
19634
|
}
|
|
19115
19635
|
case "provider.clear_models": {
|
|
19116
19636
|
const payload = asPayloadRecord(msg);
|
|
19117
19637
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19118
|
-
if (!providerId) return invalidPayload(ws, msg.type);
|
|
19638
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId)) return invalidPayload(ws, msg.type);
|
|
19119
19639
|
await routes.providerHandlers.handleProviderClearModels(ws, providerId);
|
|
19120
19640
|
return true;
|
|
19121
19641
|
}
|
|
@@ -19147,7 +19667,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19147
19667
|
const payload = asPayloadRecord(msg);
|
|
19148
19668
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19149
19669
|
const previousModels = payload ? optionalStringArray(payload, "previousModels") : null;
|
|
19150
|
-
if (!providerId || !
|
|
19670
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !previousModels)
|
|
19671
|
+
return invalidPayload(ws, msg.type);
|
|
19151
19672
|
await routes.providerHandlers.handleProviderUndoClear(ws, providerId, previousModels);
|
|
19152
19673
|
return true;
|
|
19153
19674
|
}
|
|
@@ -19157,7 +19678,7 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19157
19678
|
const envVars = payload ? optionalStringArray(payload, "envVars") : null;
|
|
19158
19679
|
const models = payload ? optionalStringArray(payload, "models") : null;
|
|
19159
19680
|
const customModels = payload ? optionalCustomModels(payload) : null;
|
|
19160
|
-
if (!payload || !id || envVars === null || models === null || customModels === null)
|
|
19681
|
+
if (!payload || !id || !SAFE_CONFIG_KEY.test(id) || envVars === null || models === null || customModels === null)
|
|
19161
19682
|
return invalidPayload(ws, msg.type);
|
|
19162
19683
|
for (const key of ["family", "baseUrl"]) {
|
|
19163
19684
|
if (payload[key] !== void 0 && typeof payload[key] !== "string")
|
|
@@ -19177,7 +19698,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19177
19698
|
const payload = asPayloadRecord(msg);
|
|
19178
19699
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19179
19700
|
const timeoutMs = payload ? optionalNumber(payload, "timeoutMs") : null;
|
|
19180
|
-
if (!providerId || timeoutMs === null)
|
|
19701
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || timeoutMs === null)
|
|
19702
|
+
return invalidPayload(ws, msg.type);
|
|
19181
19703
|
await routes.providerHandlers.handleProviderProbe(ws, providerId, timeoutMs);
|
|
19182
19704
|
return true;
|
|
19183
19705
|
}
|
|
@@ -19186,7 +19708,7 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19186
19708
|
const kind = oauthKind(payload);
|
|
19187
19709
|
const providerId = payload?.["providerId"];
|
|
19188
19710
|
if (!kind) return invalidPayload(ws, msg.type);
|
|
19189
|
-
if (providerId !== void 0 && typeof providerId !== "string") {
|
|
19711
|
+
if (providerId !== void 0 && (typeof providerId !== "string" || !SAFE_CONFIG_KEY.test(providerId))) {
|
|
19190
19712
|
return invalidPayload(ws, msg.type);
|
|
19191
19713
|
}
|
|
19192
19714
|
await routes.providerHandlers.handleOAuthStart(ws, kind, providerId);
|
|
@@ -19225,7 +19747,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19225
19747
|
const payload = asPayloadRecord(msg);
|
|
19226
19748
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19227
19749
|
const model = payload ? requiredString(payload, "model") : null;
|
|
19228
|
-
if (!providerId || !
|
|
19750
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !model)
|
|
19751
|
+
return invalidPayload(ws, msg.type);
|
|
19229
19752
|
const released = routes.statusTracker.retryNow(providerId, model);
|
|
19230
19753
|
sendResult2(
|
|
19231
19754
|
ws,
|
|
@@ -19242,7 +19765,8 @@ async function handleProviderRoute(ws, msg, routes) {
|
|
|
19242
19765
|
const payload = asPayloadRecord(msg);
|
|
19243
19766
|
const providerId = payload ? requiredString(payload, "providerId") : null;
|
|
19244
19767
|
const model = payload ? requiredString(payload, "model") : null;
|
|
19245
|
-
if (!providerId || !
|
|
19768
|
+
if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !model)
|
|
19769
|
+
return invalidPayload(ws, msg.type);
|
|
19246
19770
|
routes.statusTracker.clear(providerId, model);
|
|
19247
19771
|
sendResult2(ws, true, `Cleared tracking for ${providerId}/${model}.`);
|
|
19248
19772
|
return true;
|
|
@@ -19553,6 +20077,7 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
19553
20077
|
};
|
|
19554
20078
|
const guardedTypes = /* @__PURE__ */ new Set([
|
|
19555
20079
|
"user_message",
|
|
20080
|
+
"topic.advice",
|
|
19556
20081
|
"abort",
|
|
19557
20082
|
"tool.confirm_result",
|
|
19558
20083
|
"session.new",
|
|
@@ -19600,8 +20125,10 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
19600
20125
|
};
|
|
19601
20126
|
const mcp = {
|
|
19602
20127
|
list: (ws, msg) => handleMcpList(ws, msg, opts.profileConfigPath, opts.mcpRegistry),
|
|
19603
|
-
add
|
|
19604
|
-
|
|
20128
|
+
// add/update are the spawn-capable pair — they take a `command`/`args`
|
|
20129
|
+
// from the wire and start it. They go past the trust boundary (M1).
|
|
20130
|
+
add: (ws, msg) => handleMcpAdd(ws, msg, opts.profileConfigPath, opts.mcpRegistry, deps2.trustBoundary),
|
|
20131
|
+
update: (ws, msg) => handleMcpUpdate(ws, msg, opts.profileConfigPath, opts.mcpRegistry, deps2.trustBoundary),
|
|
19605
20132
|
remove: (ws, msg) => handleMcpRemove(ws, msg, opts.profileConfigPath, opts.mcpRegistry),
|
|
19606
20133
|
enable: (ws, msg) => handleMcpEnable(ws, msg, opts.profileConfigPath, opts.mcpRegistry),
|
|
19607
20134
|
disable: (ws, msg) => handleMcpDisable(ws, msg, opts.profileConfigPath, opts.mcpRegistry),
|
|
@@ -19688,6 +20215,15 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
19688
20215
|
searchProviderModels: (ws, query, limit) => providerOperations.handleProviderModelsSearch(ws, query, limit),
|
|
19689
20216
|
switchModel: (ws, msg) => modelOperations.switchModel(ws, msg.payload),
|
|
19690
20217
|
refineModel: (ws, msg) => modelOperations.refineModel(ws, msg.payload),
|
|
20218
|
+
fallbackChoice: async (ws, msg) => {
|
|
20219
|
+
const result = emitFallbackChoice(deps2.sessionCtx.opts.events, msg);
|
|
20220
|
+
if (!result.ok) {
|
|
20221
|
+
send2(ws, {
|
|
20222
|
+
type: "error",
|
|
20223
|
+
payload: { phase: "invalid_request", message: result.message }
|
|
20224
|
+
});
|
|
20225
|
+
}
|
|
20226
|
+
},
|
|
19691
20227
|
adoptDefaultProviderIfUnset: providerOperations.adoptDefaultProviderIfUnset,
|
|
19692
20228
|
providerHandlers: providerOperations,
|
|
19693
20229
|
statusTracker: deps2.statusTracker
|
|
@@ -19869,6 +20405,8 @@ function createEmbeddedMessageRouter(deps2) {
|
|
|
19869
20405
|
))
|
|
19870
20406
|
return;
|
|
19871
20407
|
if (await handleConnectionsServiceAction(ws, message, {
|
|
20408
|
+
trustBoundary: deps2.trustBoundary,
|
|
20409
|
+
logger: deps2.logger,
|
|
19872
20410
|
getProjectRoot: projectRoot,
|
|
19873
20411
|
getIndexDir: () => typeof opts.agent.ctx.meta["codebaseIndexDir"] === "string" ? opts.agent.ctx.meta["codebaseIndexDir"] : void 0,
|
|
19874
20412
|
send: send2,
|
|
@@ -19981,17 +20519,12 @@ function createProviderStore(deps2) {
|
|
|
19981
20519
|
cfg.activeKey = active.label;
|
|
19982
20520
|
}
|
|
19983
20521
|
}
|
|
19984
|
-
function maskedKey2(key) {
|
|
19985
|
-
if (!key) return "\u2014";
|
|
19986
|
-
if (key.length <= 8) return "\u2022".repeat(key.length);
|
|
19987
|
-
return `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
|
|
19988
|
-
}
|
|
19989
20522
|
return {
|
|
19990
20523
|
load: loadSavedProviders2,
|
|
19991
20524
|
save: saveProviders2,
|
|
19992
20525
|
normalizeKeys: normalizeKeys2,
|
|
19993
20526
|
writeKeysBack: writeKeysBack2,
|
|
19994
|
-
maskedKey
|
|
20527
|
+
maskedKey
|
|
19995
20528
|
};
|
|
19996
20529
|
}
|
|
19997
20530
|
|
|
@@ -20860,7 +21393,7 @@ function registerSetupEventsClientStatusWriter(deps2) {
|
|
|
20860
21393
|
const on = (event, listener) => events.on(event, listener);
|
|
20861
21394
|
return on("client.status", async (e) => {
|
|
20862
21395
|
broadcast2(clients, { type: "client.status_update", payload: e });
|
|
20863
|
-
if (wpaths?.projectStatus) {
|
|
21396
|
+
if (wpaths?.projectStatus && e.projectHash !== "unknown") {
|
|
20864
21397
|
try {
|
|
20865
21398
|
const statusFile = wpaths.projectStatus(e.projectHash);
|
|
20866
21399
|
const dir = path24.dirname(statusFile);
|
|
@@ -21035,7 +21568,10 @@ function registerSetupEventsProviderHandlers({
|
|
|
21035
21568
|
sessionId: e.sessionId,
|
|
21036
21569
|
providerId: e.providerId,
|
|
21037
21570
|
modelId: e.modelId,
|
|
21038
|
-
maxContext: e.maxContext
|
|
21571
|
+
maxContext: e.maxContext,
|
|
21572
|
+
...e.previousMaxContext !== void 0 ? { previousMaxContext: e.previousMaxContext } : {},
|
|
21573
|
+
...e.source !== void 0 ? { source: e.source } : {},
|
|
21574
|
+
...e.decreased !== void 0 ? { decreased: e.decreased } : {}
|
|
21039
21575
|
})
|
|
21040
21576
|
});
|
|
21041
21577
|
});
|
|
@@ -21732,7 +22268,22 @@ function setupEvents(deps2) {
|
|
|
21732
22268
|
from: e.from,
|
|
21733
22269
|
to: e.to,
|
|
21734
22270
|
status: e.status,
|
|
21735
|
-
providerSwitched: e.providerSwitched
|
|
22271
|
+
providerSwitched: e.providerSwitched,
|
|
22272
|
+
...e.requestId ? { requestId: e.requestId } : {}
|
|
22273
|
+
})
|
|
22274
|
+
});
|
|
22275
|
+
});
|
|
22276
|
+
on("provider.fallback_pending", (e) => {
|
|
22277
|
+
broadcast2(clients, {
|
|
22278
|
+
type: "provider.fallback_pending",
|
|
22279
|
+
payload: sessionPayload2({
|
|
22280
|
+
sessionId: e.sessionId,
|
|
22281
|
+
from: e.from,
|
|
22282
|
+
status: e.status,
|
|
22283
|
+
candidates: e.candidates,
|
|
22284
|
+
autoSwitchSeconds: e.autoSwitchSeconds,
|
|
22285
|
+
requestId: e.requestId,
|
|
22286
|
+
timestamp: e.timestamp
|
|
21736
22287
|
})
|
|
21737
22288
|
});
|
|
21738
22289
|
});
|
|
@@ -21817,6 +22368,15 @@ function setupEvents(deps2) {
|
|
|
21817
22368
|
type: "mailbox.agent_registered",
|
|
21818
22369
|
payload
|
|
21819
22370
|
});
|
|
22371
|
+
}),
|
|
22372
|
+
// Deregistration (subagent retirement) must reach the browser too —
|
|
22373
|
+
// otherwise dead agents linger in the client roster until an unrelated
|
|
22374
|
+
// refresh. Emitted by sqlite-mailbox.deregisterAgent with { agentId }.
|
|
22375
|
+
events.onPattern("mailbox.agent_deregistered", (_e, payload) => {
|
|
22376
|
+
broadcast2(clients, {
|
|
22377
|
+
type: "mailbox.agent_deregistered",
|
|
22378
|
+
payload
|
|
22379
|
+
});
|
|
21820
22380
|
})
|
|
21821
22381
|
);
|
|
21822
22382
|
const forwardSubagent = (kind, payload) => broadcast2(clients, { type: "subagent.event", payload: sessionPayload2({ kind, ...payload }) });
|
|
@@ -22121,7 +22681,15 @@ var SpecsWebSocketHandler = class {
|
|
|
22121
22681
|
this.clients.add(client);
|
|
22122
22682
|
ws.on("close", () => this.clients.delete(client));
|
|
22123
22683
|
ws.on("error", () => this.clients.delete(client));
|
|
22124
|
-
void this.sendList(client)
|
|
22684
|
+
void this.sendList(client).catch((err) => {
|
|
22685
|
+
console.warn(
|
|
22686
|
+
JSON.stringify({
|
|
22687
|
+
level: "warn",
|
|
22688
|
+
event: "specs.initial_send_failed",
|
|
22689
|
+
message: err instanceof Error ? err.message : String(err)
|
|
22690
|
+
})
|
|
22691
|
+
);
|
|
22692
|
+
});
|
|
22125
22693
|
}
|
|
22126
22694
|
dispose() {
|
|
22127
22695
|
this.clients.clear();
|
|
@@ -22335,15 +22903,17 @@ import {
|
|
|
22335
22903
|
|
|
22336
22904
|
// src/server/discover-mailbox-bridge.ts
|
|
22337
22905
|
import { spawn as spawn4 } from "node:child_process";
|
|
22338
|
-
import { createRequire } from "node:module";
|
|
22339
22906
|
import { existsSync as existsSync2 } from "node:fs";
|
|
22907
|
+
import { createRequire } from "node:module";
|
|
22340
22908
|
import { dirname as dirname10, join as join13 } from "node:path";
|
|
22341
|
-
import {
|
|
22909
|
+
import {
|
|
22910
|
+
readLiveLock,
|
|
22911
|
+
resolveProjectDir as resolveProjectDir3
|
|
22912
|
+
} from "@wrongstack/core/coordination";
|
|
22342
22913
|
import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
|
|
22343
|
-
import { readLiveLock } from "@wrongstack/core/coordination";
|
|
22344
22914
|
var MAILBOX_BRIDGE_BOOT_TIMEOUT_MS = 5e3;
|
|
22345
22915
|
async function discoverMailboxBridgeForWebui(params) {
|
|
22346
|
-
const mode = params.config?.features?.mailboxBridge ?? "
|
|
22916
|
+
const mode = params.config?.features?.mailboxBridge ?? "off";
|
|
22347
22917
|
if (mode === "off") return;
|
|
22348
22918
|
const projectDir = resolveProjectDir3(params.projectRoot, wstackGlobalRoot3());
|
|
22349
22919
|
let result = await readLiveLock(projectDir);
|
|
@@ -22605,6 +23175,13 @@ var TerminalWebSocketHandler = class {
|
|
|
22605
23175
|
this.send(ws, { type: "terminal.exit", payload: { id: payload.id, exitCode: -1 } });
|
|
22606
23176
|
return;
|
|
22607
23177
|
}
|
|
23178
|
+
if (this.sessions.get(ws) !== map) {
|
|
23179
|
+
this.logger.info?.(
|
|
23180
|
+
`terminal.create raced a disconnect (id=${payload.id}) \u2014 killing the orphan`
|
|
23181
|
+
);
|
|
23182
|
+
this.killPty(pty, "terminal create after disconnect");
|
|
23183
|
+
return;
|
|
23184
|
+
}
|
|
22608
23185
|
map.set(payload.id, pty);
|
|
22609
23186
|
this.logger.info?.(`terminal.create spawned (id=${payload.id}, pid=${pty.pid ?? "?"}) in ${cwd}`);
|
|
22610
23187
|
pty.onData((data) => {
|
|
@@ -24030,6 +24607,15 @@ function createMessageDispatcher(opts) {
|
|
|
24030
24607
|
msg
|
|
24031
24608
|
))
|
|
24032
24609
|
return;
|
|
24610
|
+
if (await handleConnectionsServiceAction(ws, msg, {
|
|
24611
|
+
trustBoundary: deps2.trustBoundary,
|
|
24612
|
+
logger: deps2.logger,
|
|
24613
|
+
getProjectRoot: state.getProjectRoot,
|
|
24614
|
+
getIndexDir: () => typeof deps2.context.meta["codebaseIndexDir"] === "string" ? deps2.context.meta["codebaseIndexDir"] : void 0,
|
|
24615
|
+
send,
|
|
24616
|
+
backend: "standalone"
|
|
24617
|
+
}))
|
|
24618
|
+
return;
|
|
24033
24619
|
if (await handleCodebaseIndexServerControl(ws, msg, {
|
|
24034
24620
|
trustBoundary: deps2.trustBoundary,
|
|
24035
24621
|
logger: deps2.logger,
|
|
@@ -24101,29 +24687,10 @@ import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/too
|
|
|
24101
24687
|
// src/server/model-auto-discovery.ts
|
|
24102
24688
|
import * as fs21 from "node:fs/promises";
|
|
24103
24689
|
import * as path29 from "node:path";
|
|
24104
|
-
import {
|
|
24690
|
+
import { discoverOpenAICompatibleModels, resolveDiscoveryTargets } from "@wrongstack/providers";
|
|
24105
24691
|
function isOverlayRegistry(value) {
|
|
24106
24692
|
return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
|
|
24107
24693
|
}
|
|
24108
|
-
function resolveKey(cfg) {
|
|
24109
|
-
if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
|
|
24110
|
-
const active = cfg.activeKey ? cfg.apiKeys.find((key) => key.label === cfg.activeKey) : void 0;
|
|
24111
|
-
return (active ?? cfg.apiKeys[0])?.apiKey;
|
|
24112
|
-
}
|
|
24113
|
-
return cfg.apiKey && cfg.apiKey.length > 0 ? cfg.apiKey : void 0;
|
|
24114
|
-
}
|
|
24115
|
-
function eligibleProviders(config) {
|
|
24116
|
-
const out = [];
|
|
24117
|
-
for (const [id, cfg] of Object.entries(config.providers ?? {})) {
|
|
24118
|
-
const preset = COMPATIBLE_PRESETS[id];
|
|
24119
|
-
const enabled = cfg.autoDiscoverModels ?? preset?.autoDiscover ?? false;
|
|
24120
|
-
if (!enabled) continue;
|
|
24121
|
-
const baseUrl = cfg.baseUrl ?? preset?.defaultBaseUrl;
|
|
24122
|
-
if (!baseUrl) continue;
|
|
24123
|
-
out.push({ id, cfg, baseUrl, apiKey: resolveKey(cfg) });
|
|
24124
|
-
}
|
|
24125
|
-
return out;
|
|
24126
|
-
}
|
|
24127
24694
|
async function readCache(file) {
|
|
24128
24695
|
try {
|
|
24129
24696
|
return JSON.parse(await fs21.readFile(file, "utf8"));
|
|
@@ -24134,14 +24701,13 @@ async function readCache(file) {
|
|
|
24134
24701
|
async function discoverAndMergeWebuiProviders(opts) {
|
|
24135
24702
|
const registry = opts.registry;
|
|
24136
24703
|
if (!isOverlayRegistry(registry)) return;
|
|
24137
|
-
const targets =
|
|
24704
|
+
const targets = resolveDiscoveryTargets(opts.config);
|
|
24138
24705
|
if (targets.length === 0) return;
|
|
24139
24706
|
const cacheFile = path29.join(opts.cacheDir, "discovered-models-cache.json");
|
|
24140
24707
|
const cache2 = await readCache(cacheFile);
|
|
24141
24708
|
let cacheDirty = false;
|
|
24142
24709
|
await Promise.all(
|
|
24143
|
-
targets.map(async ({ id, cfg, baseUrl, apiKey }) => {
|
|
24144
|
-
const cacheKey = `${id}\0${baseUrl}`;
|
|
24710
|
+
targets.map(async ({ id, cfg, baseUrl, apiKey, cacheKey }) => {
|
|
24145
24711
|
const provider = await discoverOpenAICompatibleModels(id, {
|
|
24146
24712
|
baseUrl,
|
|
24147
24713
|
apiKey,
|
|
@@ -24507,15 +25073,6 @@ async function createPreContextServices(input) {
|
|
|
24507
25073
|
logger.warn(`models.dev refresh failed (${toErrorMessage12(err)}); using cached catalog`);
|
|
24508
25074
|
}
|
|
24509
25075
|
}
|
|
24510
|
-
try {
|
|
24511
|
-
await installCatalogModelOutputLimits({
|
|
24512
|
-
registry: modelsRegistry,
|
|
24513
|
-
getConfig: () => config,
|
|
24514
|
-
log: (message) => logger.debug(message)
|
|
24515
|
-
});
|
|
24516
|
-
} catch (err) {
|
|
24517
|
-
logger.debug(`model output-limit index skipped: ${toErrorMessage12(err)}`);
|
|
24518
|
-
}
|
|
24519
25076
|
try {
|
|
24520
25077
|
await discoverAndMergeWebuiProviders({
|
|
24521
25078
|
config,
|
|
@@ -24526,6 +25083,15 @@ async function createPreContextServices(input) {
|
|
|
24526
25083
|
} catch (err) {
|
|
24527
25084
|
logger.debug(`provider auto-discovery skipped: ${toErrorMessage12(err)}`);
|
|
24528
25085
|
}
|
|
25086
|
+
try {
|
|
25087
|
+
await installCatalogModelOutputLimits({
|
|
25088
|
+
registry: modelsRegistry,
|
|
25089
|
+
getConfig: () => config,
|
|
25090
|
+
log: (message) => logger.debug(message)
|
|
25091
|
+
});
|
|
25092
|
+
} catch (err) {
|
|
25093
|
+
logger.debug(`model output-limit index skipped: ${toErrorMessage12(err)}`);
|
|
25094
|
+
}
|
|
24529
25095
|
const events = opts.services?.events ?? new EventBus();
|
|
24530
25096
|
events.setLogger(logger);
|
|
24531
25097
|
const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry, events });
|
|
@@ -24556,6 +25122,7 @@ async function createPreContextServices(input) {
|
|
|
24556
25122
|
registry: toolRegistry,
|
|
24557
25123
|
tier: normalizeTokenSavingTier(config.features.tokenSavingMode),
|
|
24558
25124
|
memory: { enabled: config.features.memory, store: memoryStore },
|
|
25125
|
+
nextSteps: { enabled: config.tools?.nextsteps?.enabled === true },
|
|
24559
25126
|
coordinationTools: [
|
|
24560
25127
|
makeMailboxTool({ projectDir: wpaths.projectDir, events }),
|
|
24561
25128
|
makeMailSendTool({ projectDir: wpaths.projectDir, events }),
|
|
@@ -24912,7 +25479,16 @@ function buildRoutes(state, deps2, cb) {
|
|
|
24912
25479
|
refineModel: (ws, msg) => modelOperations.refineModel(
|
|
24913
25480
|
ws,
|
|
24914
25481
|
msg.payload
|
|
24915
|
-
)
|
|
25482
|
+
),
|
|
25483
|
+
fallbackChoice: async (ws, msg) => {
|
|
25484
|
+
const result = emitFallbackChoice(deps2.events, msg);
|
|
25485
|
+
if (!result.ok) {
|
|
25486
|
+
send(ws, {
|
|
25487
|
+
type: "error",
|
|
25488
|
+
payload: { phase: "invalid_request", message: result.message }
|
|
25489
|
+
});
|
|
25490
|
+
}
|
|
25491
|
+
}
|
|
24916
25492
|
};
|
|
24917
25493
|
const sessionRoutes = createSessionHandlers({
|
|
24918
25494
|
config: state.getConfig(),
|
|
@@ -25090,8 +25666,10 @@ function buildRoutes(state, deps2, cb) {
|
|
|
25090
25666
|
});
|
|
25091
25667
|
const mcpRoutes = {
|
|
25092
25668
|
list: (ws, msg) => handleMcpList(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
25093
|
-
add
|
|
25094
|
-
|
|
25669
|
+
// add/update are the spawn-capable pair — they take a `command`/`args`
|
|
25670
|
+
// from the wire and start it. They go past the trust boundary (M1).
|
|
25671
|
+
add: (ws, msg) => handleMcpAdd(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry, deps2.trustBoundary),
|
|
25672
|
+
update: (ws, msg) => handleMcpUpdate(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry, deps2.trustBoundary),
|
|
25095
25673
|
remove: (ws, msg) => handleMcpRemove(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
25096
25674
|
enable: (ws, msg) => handleMcpEnable(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
25097
25675
|
disable: (ws, msg) => handleMcpDisable(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
|
|
@@ -25263,7 +25841,10 @@ function createWsServers(httpServer, ports, accessToken) {
|
|
|
25263
25841
|
expectedToken: wsToken,
|
|
25264
25842
|
requireToken: ports.requireToken,
|
|
25265
25843
|
allowedHostnames: publicHostnames,
|
|
25266
|
-
allowBrowserUrlToken: Boolean(ports.publicWsUrl)
|
|
25844
|
+
allowBrowserUrlToken: Boolean(ports.publicWsUrl),
|
|
25845
|
+
// WS-003 opt-out for the Vite dev loop only (app and WS server cannot
|
|
25846
|
+
// share a port). Off unless explicitly requested — see ws-auth.ts.
|
|
25847
|
+
allowCrossPortLoopbackCookie: process.env["WRONGSTACK_WEBUI_DEV_CROSS_PORT_WS"] === "1"
|
|
25267
25848
|
});
|
|
25268
25849
|
const WS_MAX_PAYLOAD = 20 * 1024 * 1024;
|
|
25269
25850
|
const wssPrimary = new WebSocketServer({
|
|
@@ -25729,7 +26310,8 @@ async function startWebUI(opts = {}) {
|
|
|
25729
26310
|
watcherMetricsRef
|
|
25730
26311
|
);
|
|
25731
26312
|
httpServer.listen(httpPort, wsHost, () => {
|
|
25732
|
-
|
|
26313
|
+
const tokenQuery = accessToken ? `/?token=${encodeURIComponent(accessToken)}` : "";
|
|
26314
|
+
console.log(`[WebUI] HTTP server running on http://${wsHost}:${httpPort}${tokenQuery}`);
|
|
25733
26315
|
const extraUrls = formatExternalAccessUrls({
|
|
25734
26316
|
bindHost: wsHost,
|
|
25735
26317
|
port: httpPort,
|
|
@@ -25751,8 +26333,11 @@ async function startWebUI(opts = {}) {
|
|
|
25751
26333
|
(req, socket, head2) => httpServer.emit("upgrade", req, socket, head2)
|
|
25752
26334
|
);
|
|
25753
26335
|
companionServer.on("error", (err) => {
|
|
25754
|
-
|
|
25755
|
-
|
|
26336
|
+
const expected = err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL" || err.code === "EADDRINUSE";
|
|
26337
|
+
if (!expected) {
|
|
26338
|
+
console.warn(
|
|
26339
|
+
`[WebUI] companion listener on ${companionLabel} failed (${err.code ?? "unknown"}): ${err.message}. The primary address is unaffected.`
|
|
26340
|
+
);
|
|
25756
26341
|
}
|
|
25757
26342
|
});
|
|
25758
26343
|
companionServer.listen(httpPort, companion, () => {
|
|
@@ -25994,24 +26579,23 @@ async function startWebUI(opts = {}) {
|
|
|
25994
26579
|
clients,
|
|
25995
26580
|
pendingConfirms,
|
|
25996
26581
|
onSecurityRejection: (ev) => {
|
|
25997
|
-
|
|
25998
|
-
|
|
25999
|
-
|
|
26000
|
-
|
|
26001
|
-
|
|
26002
|
-
|
|
26003
|
-
|
|
26004
|
-
body: `Decoder tripwire ${ev.issueCode}: ${ev.issueMessage}
|
|
26582
|
+
void mailbox.send({
|
|
26583
|
+
from: context.agentId,
|
|
26584
|
+
to: "*",
|
|
26585
|
+
type: "note",
|
|
26586
|
+
audience: "leaders",
|
|
26587
|
+
subject: `Security rejection: ${ev.issueCode}`,
|
|
26588
|
+
body: `Decoder tripwire ${ev.issueCode}: ${ev.issueMessage}
|
|
26005
26589
|
|
|
26006
26590
|
connectionId: ${ev.connectionId ?? "?"}
|
|
26007
26591
|
sessionId: ${ev.sessionId ?? "?"}
|
|
26008
26592
|
agentId: ${ev.agentId ?? "?"}
|
|
26009
26593
|
projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
26010
|
-
|
|
26011
|
-
|
|
26012
|
-
|
|
26013
|
-
|
|
26014
|
-
}
|
|
26594
|
+
priority: "high",
|
|
26595
|
+
senderSessionId: session.id
|
|
26596
|
+
}).catch((err) => {
|
|
26597
|
+
console.warn(`[WebUI] security-rejection mailbox note failed: ${String(err)}`);
|
|
26598
|
+
});
|
|
26015
26599
|
},
|
|
26016
26600
|
goalHandler,
|
|
26017
26601
|
specsHandler,
|
|
@@ -26096,6 +26680,7 @@ export {
|
|
|
26096
26680
|
SddWizardWebSocketHandler,
|
|
26097
26681
|
SpecsWebSocketHandler,
|
|
26098
26682
|
TerminalWebSocketHandler,
|
|
26683
|
+
WEBUI_WS_MAX_BUFFERED_BYTES,
|
|
26099
26684
|
WorktreeWebSocketHandler,
|
|
26100
26685
|
addProvider,
|
|
26101
26686
|
announceWebuiReady,
|
|
@@ -26350,6 +26935,7 @@ export {
|
|
|
26350
26935
|
seedContextMeta,
|
|
26351
26936
|
send,
|
|
26352
26937
|
sendResult2 as sendResult,
|
|
26938
|
+
sendSerialized,
|
|
26353
26939
|
setActiveKey,
|
|
26354
26940
|
setupEvents,
|
|
26355
26941
|
setupWebUICodebaseIndexing,
|