@wrongstack/webui-server 0.298.3 → 0.300.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -77,6 +77,10 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
77
77
  // Display-only toggles (purely visual, persisted in localStorage via Zustand).
78
78
  "groupToolCalls",
79
79
  "showThinkingLogs",
80
+ // v15: auto-collapse of the chat input under the history (opt-in display
81
+ // toggle, default off). Whitelisted so the key survives `prefs.update`
82
+ // round-trips without tripping the "unknown preference key" rejection.
83
+ "autoCollapseInput",
80
84
  // v11 Display parity: inverse fsAccess flag.
81
85
  "allowOutsideProjectRoot",
82
86
  // v13 Display parity (TUI SettingsPicker fields 42 & 43): the read tool
@@ -401,6 +405,51 @@ function validateModelSwitchPayload(payload) {
401
405
  }
402
406
  };
403
407
  }
408
+ function validateModelFallbackChoicePayload(payload) {
409
+ if (!isRecord2(payload)) {
410
+ return {
411
+ ok: false,
412
+ message: "model.fallback_choice payload must be an object"
413
+ };
414
+ }
415
+ const requestId = payload["requestId"];
416
+ if (typeof requestId !== "string" || requestId.trim().length === 0) {
417
+ return {
418
+ ok: false,
419
+ message: "model.fallback_choice payload.requestId must be a non-empty string"
420
+ };
421
+ }
422
+ const providerId = payload["providerId"];
423
+ const model = payload["model"];
424
+ const autoSwitch = payload["autoSwitch"];
425
+ if (providerId !== void 0 && typeof providerId !== "string") {
426
+ return {
427
+ ok: false,
428
+ message: "model.fallback_choice payload.providerId must be a string when provided"
429
+ };
430
+ }
431
+ if (model !== void 0 && typeof model !== "string") {
432
+ return {
433
+ ok: false,
434
+ message: "model.fallback_choice payload.model must be a string when provided"
435
+ };
436
+ }
437
+ if (autoSwitch !== void 0 && typeof autoSwitch !== "boolean") {
438
+ return {
439
+ ok: false,
440
+ message: "model.fallback_choice payload.autoSwitch must be a boolean when provided"
441
+ };
442
+ }
443
+ return {
444
+ ok: true,
445
+ value: {
446
+ requestId: requestId.trim(),
447
+ ...typeof providerId === "string" ? { providerId } : {},
448
+ ...typeof model === "string" ? { model } : {},
449
+ ...typeof autoSwitch === "boolean" ? { autoSwitch } : {}
450
+ }
451
+ };
452
+ }
404
453
  var AUTONOMY_VALUES2 = /* @__PURE__ */ new Set(["off", "suggest", "auto", "eternal", "eternal-parallel"]);
405
454
  function validateMailboxMessagesPayload(payload) {
406
455
  if (payload === void 0) return { ok: true, value: void 0 };
@@ -4331,13 +4380,10 @@ function createConversationOperations(ctx) {
4331
4380
 
4332
4381
  // src/server/context-editor.ts
4333
4382
  import { createHash } from "node:crypto";
4334
- import net from "node:net";
4335
4383
  import {
4336
4384
  ALLOWED_IMAGE_MEDIA_TYPES,
4337
4385
  base64DecodedBytes,
4338
4386
  isAllowedImageMediaType,
4339
- isPrivateIPv4,
4340
- isPrivateIPv6,
4341
4387
  isValidImageBase64,
4342
4388
  MAX_INCOMING_IMAGE_BYTES,
4343
4389
  repairToolUseAdjacency
@@ -4405,40 +4451,7 @@ var REVISION_PREFIX = "wrongstack-context-editor-v1\0";
4405
4451
  var MAX_MESSAGE_COUNT_GROWTH = 10;
4406
4452
  var MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
4407
4453
  var MAX_STRING_LENGTH = 8 * 1024 * 1024;
4408
- var MAX_IMAGE_URL_LENGTH = 2048;
4409
- function imageUrlRejectionReason(url) {
4410
- if (url.length > MAX_IMAGE_URL_LENGTH) {
4411
- return `image.source.url exceeds ${MAX_IMAGE_URL_LENGTH} characters.`;
4412
- }
4413
- let parsed;
4414
- try {
4415
- parsed = new URL(url);
4416
- } catch {
4417
- return "image.source.url must be an absolute URL.";
4418
- }
4419
- if (parsed.protocol !== "https:") {
4420
- return `image.source.url must use https (got "${parsed.protocol}").`;
4421
- }
4422
- if (parsed.username !== "" || parsed.password !== "") {
4423
- return "image.source.url must not embed credentials.";
4424
- }
4425
- const host = parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]") ? parsed.hostname.slice(1, -1) : parsed.hostname;
4426
- const bareHost = host.endsWith(".") ? host.slice(0, -1) : host;
4427
- if (bareHost === "") {
4428
- return "image.source.url must include a hostname.";
4429
- }
4430
- if (bareHost === "localhost" || bareHost.endsWith(".localhost")) {
4431
- return "image.source.url must not target localhost.";
4432
- }
4433
- const family = net.isIP(bareHost);
4434
- if (family === 4 && isPrivateIPv4(bareHost)) {
4435
- return `image.source.url must not target a private or loopback address ("${bareHost}").`;
4436
- }
4437
- if (family === 6 && isPrivateIPv6(bareHost)) {
4438
- return `image.source.url must not target a private or loopback address ("${bareHost}").`;
4439
- }
4440
- return void 0;
4441
- }
4454
+ var MAX_REMOVAL_COUNT = 4096;
4442
4455
  function isRecord3(value) {
4443
4456
  return value !== null && typeof value === "object" && !Array.isArray(value);
4444
4457
  }
@@ -4447,7 +4460,7 @@ function canonicalize(value) {
4447
4460
  if (isRecord3(value)) {
4448
4461
  const sorted = {};
4449
4462
  for (const key of Object.keys(value).sort()) {
4450
- if (key === "_estTokens") continue;
4463
+ if (key === "_estTokens" || key === "_toolErrorInfo") continue;
4451
4464
  const item = value[key];
4452
4465
  if (item === void 0) continue;
4453
4466
  sorted[key] = canonicalize(item);
@@ -4478,6 +4491,12 @@ function isMessageRole(value) {
4478
4491
  function isPlainJsonObject(value) {
4479
4492
  return isRecord3(value);
4480
4493
  }
4494
+ function splitsSurrogatePair(text, offset) {
4495
+ if (offset <= 0 || offset >= text.length) return false;
4496
+ const previous = text.charCodeAt(offset - 1);
4497
+ const next = text.charCodeAt(offset);
4498
+ return previous >= 55296 && previous <= 56319 && next >= 56320 && next <= 57343;
4499
+ }
4481
4500
  function validateCacheControl(value, path30, errors) {
4482
4501
  if (value === void 0) return void 0;
4483
4502
  if (!isRecord3(value) || value["type"] !== "ephemeral") {
@@ -4688,19 +4707,13 @@ function validateBlock(value, path30, errors) {
4688
4707
  );
4689
4708
  return void 0;
4690
4709
  }
4691
- const urlError = imageUrlRejectionReason(url);
4692
- if (urlError !== void 0) {
4693
- error(errors, `${path30}/source/url`, "UNSAFE_IMAGE_URL", urlError);
4694
- return void 0;
4695
- }
4696
- return {
4697
- type: "image",
4698
- source: {
4699
- type: "url",
4700
- ...typeof mediaType === "string" ? { media_type: mediaType } : {},
4701
- url
4702
- }
4703
- };
4710
+ error(
4711
+ errors,
4712
+ `${path30}/source/url`,
4713
+ "UNSAFE_IMAGE_URL",
4714
+ "URL image sources are not allowed in context editor proposals; use an ingested base64 image."
4715
+ );
4716
+ return void 0;
4704
4717
  }
4705
4718
  case "thinking": {
4706
4719
  const thinking = value["thinking"];
@@ -4857,6 +4870,14 @@ function warningsForMessage(message, index) {
4857
4870
  message: "This block contains provider replay metadata and should only be removed with the whole turn if no longer needed."
4858
4871
  });
4859
4872
  }
4873
+ if (block.type === "image" && block.source.type === "url") {
4874
+ warnings.push({
4875
+ path: `/messages/${index}/content/${blockIndex}/source/url`,
4876
+ code: "UNSAFE_IMAGE_URL",
4877
+ severity: "danger",
4878
+ message: "URL image sources cannot be retained in context editor proposals; remove the whole message before applying other edits."
4879
+ });
4880
+ }
4860
4881
  if (block.type === "tool_result" && block.content.length > 2e4) {
4861
4882
  warnings.push({
4862
4883
  path: `/messages/${index}/content/${blockIndex}`,
@@ -4884,6 +4905,21 @@ function metricFor(ctx, messages, tools) {
4884
4905
  fullRequestTokens: breakdown.total
4885
4906
  };
4886
4907
  }
4908
+ function isToolResultMessage(message) {
4909
+ return Boolean(
4910
+ message?.role === "user" && Array.isArray(message.content) && message.content.length > 0 && message.content.every((block) => block.type === "tool_result")
4911
+ );
4912
+ }
4913
+ function pairedAssistantIndices(messages, userIndex) {
4914
+ if (messages[userIndex]?.role !== "user" || isToolResultMessage(messages[userIndex])) return [];
4915
+ const paired = [];
4916
+ for (let index = userIndex + 1; index < messages.length; index += 1) {
4917
+ const message = messages[index];
4918
+ if (message?.role === "user" && !isToolResultMessage(message)) break;
4919
+ if (message?.role === "assistant") paired.push(index);
4920
+ }
4921
+ return paired;
4922
+ }
4887
4923
  function buildContextEditorSnapshot(ctx, tools) {
4888
4924
  const messages = ctx.messages.map(
4889
4925
  (message) => ({
@@ -4913,11 +4949,165 @@ function buildContextEditorSnapshot(ctx, tools) {
4913
4949
  tokens: messageTokens(message.content),
4914
4950
  preview: breakdown.messages.breakdown[index]?.preview ?? "",
4915
4951
  blockCount: Array.isArray(message.content) ? message.content.length : null,
4916
- warnings: warningsForMessage(message, index)
4952
+ warnings: warningsForMessage(message, index),
4953
+ pairedAssistantIndices: pairedAssistantIndices(ctx.messages, index)
4917
4954
  })),
4918
4955
  diagnostics: toolDiagnostics(ctx.messages)
4919
4956
  };
4920
4957
  }
4958
+ function validateRemovalPlan(value, originalMessages, proposedMessages) {
4959
+ const errors = [];
4960
+ if (value === void 0) {
4961
+ error(
4962
+ errors,
4963
+ "/removals",
4964
+ "REMOVAL_PLAN_REQUIRED",
4965
+ "A removal plan is required for every context editor proposal."
4966
+ );
4967
+ return { errors };
4968
+ }
4969
+ if (!Array.isArray(value)) {
4970
+ error(errors, "/removals", "INVALID_REMOVALS", "removals must be an array.");
4971
+ return { errors };
4972
+ }
4973
+ if (value.length > MAX_REMOVAL_COUNT) {
4974
+ error(
4975
+ errors,
4976
+ "/removals",
4977
+ "TOO_MANY_REMOVALS",
4978
+ `removals must contain at most ${MAX_REMOVAL_COUNT} entries.`
4979
+ );
4980
+ return { errors };
4981
+ }
4982
+ const wholeMessages = /* @__PURE__ */ new Set();
4983
+ const touchedUsers = /* @__PURE__ */ new Set();
4984
+ const ranges = [];
4985
+ for (const [removalIndex, raw] of value.entries()) {
4986
+ const path30 = `/removals/${removalIndex}`;
4987
+ if (!isRecord3(raw) || !Number.isInteger(raw["messageIndex"])) {
4988
+ error(errors, path30, "INVALID_REMOVAL", "Removal must include an integer messageIndex.");
4989
+ continue;
4990
+ }
4991
+ const messageIndex = raw["messageIndex"];
4992
+ const original = originalMessages[messageIndex];
4993
+ if (!original) {
4994
+ error(errors, `${path30}/messageIndex`, "INVALID_MESSAGE_INDEX", "Removal messageIndex is out of range.");
4995
+ continue;
4996
+ }
4997
+ const start = raw["start"];
4998
+ const end = raw["end"];
4999
+ const blockIndex = raw["blockIndex"];
5000
+ if (start === void 0 && end === void 0 && blockIndex === void 0) {
5001
+ wholeMessages.add(messageIndex);
5002
+ if (original.role === "user") touchedUsers.add(messageIndex);
5003
+ continue;
5004
+ }
5005
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start) {
5006
+ error(errors, path30, "INVALID_RANGE", "Range removal requires integer start/end with 0 <= start < end.");
5007
+ continue;
5008
+ }
5009
+ let text;
5010
+ if (blockIndex === void 0 && typeof original.content === "string") text = original.content;
5011
+ if (Number.isInteger(blockIndex) && Array.isArray(original.content)) {
5012
+ const block = original.content[blockIndex];
5013
+ if (block?.type === "text") text = block.text;
5014
+ }
5015
+ if (text === void 0 || end > text.length) {
5016
+ error(errors, path30, "INVALID_RANGE_TARGET", "Range must target existing string or text-block content.");
5017
+ continue;
5018
+ }
5019
+ if (splitsSurrogatePair(text, start) || splitsSurrogatePair(text, end)) {
5020
+ error(
5021
+ errors,
5022
+ path30,
5023
+ "INVALID_UNICODE_RANGE",
5024
+ "Range boundaries must not split a Unicode surrogate pair."
5025
+ );
5026
+ continue;
5027
+ }
5028
+ ranges.push({
5029
+ messageIndex,
5030
+ ...blockIndex === void 0 ? {} : { blockIndex },
5031
+ start,
5032
+ end
5033
+ });
5034
+ if (original.role === "user") touchedUsers.add(messageIndex);
5035
+ }
5036
+ const rangesByTarget = /* @__PURE__ */ new Map();
5037
+ for (const range of ranges) {
5038
+ const key = `${range.messageIndex}:${range.blockIndex ?? "string"}`;
5039
+ const targetRanges = rangesByTarget.get(key) ?? [];
5040
+ targetRanges.push(range);
5041
+ rangesByTarget.set(key, targetRanges);
5042
+ }
5043
+ for (const targetRanges of rangesByTarget.values()) {
5044
+ targetRanges.sort((left, right) => (left.start ?? 0) - (right.start ?? 0));
5045
+ for (let index = 1; index < targetRanges.length; index += 1) {
5046
+ const previous = targetRanges[index - 1];
5047
+ const current2 = targetRanges[index];
5048
+ if (previous?.end !== void 0 && current2?.start !== void 0 && current2.start < previous.end) {
5049
+ error(
5050
+ errors,
5051
+ "/removals",
5052
+ "OVERLAPPING_RANGES",
5053
+ "Removal ranges targeting the same text must not overlap."
5054
+ );
5055
+ break;
5056
+ }
5057
+ }
5058
+ }
5059
+ for (const userIndex of touchedUsers) {
5060
+ for (const assistantIndex of pairedAssistantIndices(originalMessages, userIndex)) {
5061
+ if (wholeMessages.has(assistantIndex)) continue;
5062
+ error(
5063
+ errors,
5064
+ "/removals",
5065
+ "MISSING_ASSISTANT_PAIR",
5066
+ `Editing user message ${userIndex} must also remove assistant message ${assistantIndex}.`
5067
+ );
5068
+ }
5069
+ }
5070
+ const expectedMessages = structuredClone(originalMessages);
5071
+ for (const targetRanges of rangesByTarget.values()) {
5072
+ const first = targetRanges[0];
5073
+ if (!first) continue;
5074
+ const message = expectedMessages[first.messageIndex];
5075
+ if (!message) continue;
5076
+ let text;
5077
+ if (first.blockIndex === void 0 && typeof message.content === "string") {
5078
+ text = message.content;
5079
+ } else if (first.blockIndex !== void 0 && Array.isArray(message.content)) {
5080
+ const block = message.content[first.blockIndex];
5081
+ if (block?.type === "text") text = block.text;
5082
+ }
5083
+ if (text === void 0) continue;
5084
+ const pieces = [];
5085
+ let cursor = 0;
5086
+ for (const range of targetRanges) {
5087
+ if (range.start === void 0 || range.end === void 0) continue;
5088
+ pieces.push(text.slice(cursor, range.start));
5089
+ cursor = range.end;
5090
+ }
5091
+ pieces.push(text.slice(cursor));
5092
+ const nextText = pieces.join("");
5093
+ if (first.blockIndex === void 0 && typeof message.content === "string") {
5094
+ message.content = nextText;
5095
+ } else if (first.blockIndex !== void 0 && Array.isArray(message.content)) {
5096
+ const block = message.content[first.blockIndex];
5097
+ if (block?.type === "text") block.text = nextText;
5098
+ }
5099
+ }
5100
+ const expectedProposal = expectedMessages.filter((_, index) => !wholeMessages.has(index));
5101
+ if (JSON.stringify(canonicalize(expectedProposal)) !== JSON.stringify(canonicalize(proposedMessages))) {
5102
+ error(
5103
+ errors,
5104
+ "/messages",
5105
+ "REMOVAL_PLAN_MISMATCH",
5106
+ "Submitted messages do not exactly match the declared removal plan."
5107
+ );
5108
+ }
5109
+ return errors.length > 0 ? { errors } : { errors, messages: expectedProposal };
5110
+ }
4921
5111
  function validateContextEditorProposal(input) {
4922
5112
  const currentRevision = contextEditorRevision(input.ctx.messages);
4923
5113
  const before = metricFor(input.ctx, input.ctx.messages, input.tools);
@@ -4969,7 +5159,23 @@ function validateContextEditorProposal(input) {
4969
5159
  repair: emptyRepair
4970
5160
  };
4971
5161
  }
4972
- const repaired = repairToolUseAdjacency(parsed.messages);
5162
+ const removalPlan = validateRemovalPlan(
5163
+ input.removals,
5164
+ input.ctx.messages,
5165
+ parsed.messages
5166
+ );
5167
+ if (removalPlan.errors.length > 0 || !removalPlan.messages) {
5168
+ return {
5169
+ ok: false,
5170
+ baseRevision: input.baseRevision,
5171
+ currentRevision,
5172
+ before,
5173
+ validationErrors: removalPlan.errors,
5174
+ warnings: [],
5175
+ repair: emptyRepair
5176
+ };
5177
+ }
5178
+ const repaired = repairToolUseAdjacency(removalPlan.messages);
4973
5179
  const repair = {
4974
5180
  changed: repaired.report.changed,
4975
5181
  removedToolUses: repaired.report.removedToolUses,
@@ -7497,7 +7703,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
7497
7703
  }
7498
7704
  try {
7499
7705
  const { SessionRegistry, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core/storage");
7500
- const { resolveWstackPaths: resolveWstackPaths6 } = await import("@wrongstack/core/utils");
7706
+ const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
7501
7707
  const registry = new SessionRegistry(globalRoot);
7502
7708
  const entry = await registry.get(sessionId);
7503
7709
  if (!entry) {
@@ -7505,7 +7711,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
7505
7711
  res.end(JSON.stringify({ error: "Session not found" }));
7506
7712
  return;
7507
7713
  }
7508
- const paths = resolveWstackPaths6({ projectRoot: entry.projectRoot, globalRoot });
7714
+ const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
7509
7715
  const store = new DefaultSessionStore3({ dir: paths.projectSessions });
7510
7716
  const reader = new DefaultSessionReader2({ store });
7511
7717
  const rawEntries = [];
@@ -7586,7 +7792,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
7586
7792
  try {
7587
7793
  const { SessionRegistry } = await import("@wrongstack/core/storage");
7588
7794
  const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
7589
- const { resolveWstackPaths: resolveWstackPaths6 } = await import("@wrongstack/core/utils");
7795
+ const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
7590
7796
  const registry = new SessionRegistry(globalRoot);
7591
7797
  const entry = await registry.get(sessionId);
7592
7798
  if (!entry) {
@@ -7594,7 +7800,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
7594
7800
  res.end(JSON.stringify({ error: "Session not found" }));
7595
7801
  return;
7596
7802
  }
7597
- const paths = resolveWstackPaths6({ projectRoot: entry.projectRoot, globalRoot });
7803
+ const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
7598
7804
  const mailbox = getSharedProjectMailbox5(paths.projectDir);
7599
7805
  const to = `leader@${mailboxSessionTag2(sessionId)}`;
7600
7806
  const sent = await mailbox.send({ from, to, type, subject, body: text, priority });
@@ -7614,7 +7820,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
7614
7820
  try {
7615
7821
  const { SessionRegistry } = await import("@wrongstack/core/storage");
7616
7822
  const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
7617
- const { resolveWstackPaths: resolveWstackPaths6 } = await import("@wrongstack/core/utils");
7823
+ const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
7618
7824
  const registry = new SessionRegistry(globalRoot);
7619
7825
  const entry = await registry.get(sessionId);
7620
7826
  if (!entry) {
@@ -7622,7 +7828,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
7622
7828
  res.end(JSON.stringify({ error: "Session not found" }));
7623
7829
  return;
7624
7830
  }
7625
- const paths = resolveWstackPaths6({ projectRoot: entry.projectRoot, globalRoot });
7831
+ const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
7626
7832
  const mailbox = getSharedProjectMailbox5(paths.projectDir);
7627
7833
  const leaderAddr = `leader@${mailboxSessionTag2(sessionId)}`;
7628
7834
  const [inbound, outbound] = await Promise.all([
@@ -7674,7 +7880,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
7674
7880
  try {
7675
7881
  const { SessionRegistry } = await import("@wrongstack/core/storage");
7676
7882
  const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
7677
- const { resolveWstackPaths: resolveWstackPaths6 } = await import("@wrongstack/core/utils");
7883
+ const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
7678
7884
  const registry = new SessionRegistry(globalRoot);
7679
7885
  const entry = await registry.get(sessionId);
7680
7886
  if (!entry) {
@@ -7682,7 +7888,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
7682
7888
  res.end(JSON.stringify({ error: "Session not found" }));
7683
7889
  return;
7684
7890
  }
7685
- const paths = resolveWstackPaths6({ projectRoot: entry.projectRoot, globalRoot });
7891
+ const paths = resolveWstackPaths7({ projectRoot: entry.projectRoot, globalRoot });
7686
7892
  const mailbox = getSharedProjectMailbox5(paths.projectDir);
7687
7893
  const to = `leader@${mailboxSessionTag2(sessionId)}`;
7688
7894
  const sent = await mailbox.sendRuntimeControl({
@@ -7723,7 +7929,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7723
7929
  try {
7724
7930
  const { SessionRegistry } = await import("@wrongstack/core/storage");
7725
7931
  const { getSharedProjectMailbox: getSharedProjectMailbox5, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core/coordination");
7726
- const { resolveWstackPaths: resolveWstackPaths6 } = await import("@wrongstack/core/utils");
7932
+ const { resolveWstackPaths: resolveWstackPaths7 } = await import("@wrongstack/core/utils");
7727
7933
  const registry = new SessionRegistry(globalRoot);
7728
7934
  const all = await registry.list();
7729
7935
  const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
@@ -7735,7 +7941,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7735
7941
  }
7736
7942
  const mbByDir = /* @__PURE__ */ new Map();
7737
7943
  const mailboxFor = (projectRoot) => {
7738
- const dir = resolveWstackPaths6({ projectRoot, globalRoot }).projectDir;
7944
+ const dir = resolveWstackPaths7({ projectRoot, globalRoot }).projectDir;
7739
7945
  let mb = mbByDir.get(dir);
7740
7946
  if (!mb) {
7741
7947
  mb = getSharedProjectMailbox5(dir);
@@ -7769,6 +7975,246 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7769
7975
  }
7770
7976
  }
7771
7977
 
7978
+ // src/server/requirement-intake-handlers.ts
7979
+ import { readProjectIdentity } from "@wrongstack/core/utils";
7980
+ import {
7981
+ IntakeError,
7982
+ IntakeValidationError
7983
+ } from "@wrongstack/requirement-intake";
7984
+ var SERVER_ACTOR = { id: "webui-server", type: "agent" };
7985
+ var MAX_INTAKE_BODY_BYTES = 512e3;
7986
+ function intakeContext(projectId) {
7987
+ return { ...SERVER_ACTOR, projectId };
7988
+ }
7989
+ function sendJson2(res, status, body) {
7990
+ res.writeHead(status, { "Content-Type": "application/json" });
7991
+ res.end(JSON.stringify(body));
7992
+ }
7993
+ function sendNotFound(res, intakeId) {
7994
+ sendJson2(res, 404, {
7995
+ error: {
7996
+ code: "INTAKE_NOT_FOUND",
7997
+ message: `Requirement intake record not found: ${intakeId}`
7998
+ }
7999
+ });
8000
+ }
8001
+ function sendIntakeError(res, error2) {
8002
+ if (error2 instanceof IntakeValidationError) {
8003
+ sendJson2(res, 400, {
8004
+ error: { code: error2.code, message: error2.message, issues: error2.issues }
8005
+ });
8006
+ return;
8007
+ }
8008
+ if (error2 instanceof IntakeError) {
8009
+ const status = error2.code === "INTAKE_UNAUTHORIZED" ? 403 : error2.code === "INTAKE_NOT_FOUND" ? 404 : error2.code === "INTAKE_SUGGESTION_ERROR" ? 502 : 409;
8010
+ sendJson2(res, status, { error: { code: error2.code, message: error2.message } });
8011
+ return;
8012
+ }
8013
+ sendJson2(res, 500, { error: { code: "INTERNAL_ERROR", message: "Internal server error" } });
8014
+ }
8015
+ function serviceOr503(res, service) {
8016
+ if (service) return true;
8017
+ sendJson2(res, 503, {
8018
+ error: { code: "INTAKE_UNAVAILABLE", message: "Requirement intake service not configured" }
8019
+ });
8020
+ return false;
8021
+ }
8022
+ async function readJsonBody3(res, req) {
8023
+ const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
8024
+ if (contentType !== "application/json") {
8025
+ sendJson2(res, 400, {
8026
+ error: {
8027
+ code: "INVALID_CONTENT_TYPE",
8028
+ message: `Unsupported Content-Type: ${contentType || "(absent)"}`
8029
+ }
8030
+ });
8031
+ return null;
8032
+ }
8033
+ return new Promise((resolve15) => {
8034
+ let data = "";
8035
+ let failed = false;
8036
+ const fail2 = (message) => {
8037
+ if (failed) return;
8038
+ failed = true;
8039
+ sendJson2(res, 400, { error: { code: "INVALID_BODY", message } });
8040
+ resolve15(null);
8041
+ };
8042
+ req.on("data", (chunk) => {
8043
+ if (failed) return;
8044
+ data += chunk.toString("utf8");
8045
+ if (data.length > MAX_INTAKE_BODY_BYTES) {
8046
+ req.destroy();
8047
+ fail2("Request body too large");
8048
+ }
8049
+ });
8050
+ req.on("end", () => {
8051
+ if (failed) return;
8052
+ try {
8053
+ resolve15(data.trim().length === 0 ? {} : JSON.parse(data));
8054
+ } catch {
8055
+ fail2("Request body is not valid JSON");
8056
+ }
8057
+ });
8058
+ req.on("error", () => fail2("Failed to read request body"));
8059
+ });
8060
+ }
8061
+ async function discoverIntake(res, service, intakeId) {
8062
+ const record = await service.getIntake(intakeId, intakeContext(""));
8063
+ if (!record) {
8064
+ sendNotFound(res, intakeId);
8065
+ return null;
8066
+ }
8067
+ return record;
8068
+ }
8069
+ async function handleRequirementIntakeListForServer(res, service, projectRoot) {
8070
+ if (!serviceOr503(res, service)) return;
8071
+ if (!projectRoot) {
8072
+ sendJson2(res, 503, {
8073
+ error: { code: "PROJECT_ROOT_NOT_CONFIGURED", message: "Project root not configured" }
8074
+ });
8075
+ return;
8076
+ }
8077
+ try {
8078
+ const identity = await readProjectIdentity(projectRoot);
8079
+ if (!identity) {
8080
+ sendJson2(res, 404, {
8081
+ error: {
8082
+ code: "PROJECT_IDENTITY_NOT_FOUND",
8083
+ message: "No project identity found \u2014 run `wstack init` first"
8084
+ }
8085
+ });
8086
+ return;
8087
+ }
8088
+ const projectId = identity.projectId;
8089
+ const records = await service.listIntakes(projectId, intakeContext(projectId));
8090
+ sendJson2(res, 200, { projectId, intakes: records });
8091
+ } catch (error2) {
8092
+ sendIntakeError(res, error2);
8093
+ }
8094
+ }
8095
+ async function handleRequirementIntakeCreate(res, req, service, projectId) {
8096
+ if (!serviceOr503(res, service)) return;
8097
+ const body = await readJsonBody3(res, req);
8098
+ if (body === null) return;
8099
+ try {
8100
+ const result = await service.createIntake(body, intakeContext(projectId));
8101
+ sendJson2(res, result.idempotent ? 200 : 201, result);
8102
+ } catch (error2) {
8103
+ sendIntakeError(res, error2);
8104
+ }
8105
+ }
8106
+ async function handleRequirementIntakeList(res, service, projectId) {
8107
+ if (!serviceOr503(res, service)) return;
8108
+ try {
8109
+ const records = await service.listIntakes(projectId, intakeContext(projectId));
8110
+ sendJson2(res, 200, records);
8111
+ } catch (error2) {
8112
+ sendIntakeError(res, error2);
8113
+ }
8114
+ }
8115
+ async function handleRequirementIntakeGet(res, service, intakeId) {
8116
+ if (!serviceOr503(res, service)) return;
8117
+ try {
8118
+ const record = await discoverIntake(res, service, intakeId);
8119
+ if (!record) return;
8120
+ sendJson2(res, 200, record);
8121
+ } catch (error2) {
8122
+ sendIntakeError(res, error2);
8123
+ }
8124
+ }
8125
+ async function handleRequirementIntakeUpdate(res, req, service, intakeId) {
8126
+ if (!serviceOr503(res, service)) return;
8127
+ const body = await readJsonBody3(res, req);
8128
+ if (body === null) return;
8129
+ try {
8130
+ const record = await discoverIntake(res, service, intakeId);
8131
+ if (!record) return;
8132
+ const expectedVersion = typeof body["expectedVersion"] === "number" ? body["expectedVersion"] : void 0;
8133
+ const { expectedVersion: _ignored, ...patch } = body;
8134
+ void _ignored;
8135
+ const updated = await service.updateIntake(
8136
+ intakeId,
8137
+ patch,
8138
+ intakeContext(record.projectId),
8139
+ expectedVersion
8140
+ );
8141
+ sendJson2(res, 200, updated);
8142
+ } catch (error2) {
8143
+ sendIntakeError(res, error2);
8144
+ }
8145
+ }
8146
+ async function handleRequirementIntakeAnswers(res, req, service, intakeId) {
8147
+ if (!serviceOr503(res, service)) return;
8148
+ const body = await readJsonBody3(res, req);
8149
+ if (body === null) return;
8150
+ try {
8151
+ const record = await discoverIntake(res, service, intakeId);
8152
+ if (!record) return;
8153
+ const updated = await service.addAnswer(
8154
+ intakeId,
8155
+ body,
8156
+ intakeContext(record.projectId)
8157
+ );
8158
+ sendJson2(res, 200, updated);
8159
+ } catch (error2) {
8160
+ sendIntakeError(res, error2);
8161
+ }
8162
+ }
8163
+ async function handleRequirementIntakeSuggestions(res, req, service, intakeId) {
8164
+ if (!serviceOr503(res, service)) return;
8165
+ const body = await readJsonBody3(res, req);
8166
+ if (body === null) return;
8167
+ try {
8168
+ const record = await discoverIntake(res, service, intakeId);
8169
+ if (!record) return;
8170
+ const focus = Array.isArray(body["focus"]) ? body["focus"] : void 0;
8171
+ const suggestions = await service.generateSuggestions(
8172
+ intakeId,
8173
+ intakeContext(record.projectId),
8174
+ focus
8175
+ );
8176
+ sendJson2(res, 200, { suggestions });
8177
+ } catch (error2) {
8178
+ sendIntakeError(res, error2);
8179
+ }
8180
+ }
8181
+ async function handleRequirementIntakeSubmit(res, service, intakeId) {
8182
+ if (!serviceOr503(res, service)) return;
8183
+ try {
8184
+ const record = await discoverIntake(res, service, intakeId);
8185
+ if (!record) return;
8186
+ const result = await service.submitIntake(intakeId, intakeContext(record.projectId));
8187
+ sendJson2(res, 200, result);
8188
+ } catch (error2) {
8189
+ sendIntakeError(res, error2);
8190
+ }
8191
+ }
8192
+ async function handleRequirementIntakeCancel(res, req, service, intakeId) {
8193
+ if (!serviceOr503(res, service)) return;
8194
+ const body = await readJsonBody3(res, req);
8195
+ if (body === null) return;
8196
+ try {
8197
+ const record = await discoverIntake(res, service, intakeId);
8198
+ if (!record) return;
8199
+ const reason = typeof body["reason"] === "string" ? body["reason"] : void 0;
8200
+ const updated = await service.cancelIntake(intakeId, intakeContext(record.projectId), reason);
8201
+ sendJson2(res, 200, updated);
8202
+ } catch (error2) {
8203
+ sendIntakeError(res, error2);
8204
+ }
8205
+ }
8206
+ async function handleRequirementIntakeArchive(res, service, intakeId) {
8207
+ if (!serviceOr503(res, service)) return;
8208
+ try {
8209
+ const record = await discoverIntake(res, service, intakeId);
8210
+ if (!record) return;
8211
+ const updated = await service.archiveIntake(intakeId, intakeContext(record.projectId));
8212
+ sendJson2(res, 200, updated);
8213
+ } catch (error2) {
8214
+ sendIntakeError(res, error2);
8215
+ }
8216
+ }
8217
+
7772
8218
  // src/server/memory-diagnostics.ts
7773
8219
  import * as fs8 from "node:fs/promises";
7774
8220
  import * as path11 from "node:path";
@@ -7974,7 +8420,7 @@ async function touchProjectInManifest(options, globalConfigPath) {
7974
8420
  // src/server/techstack-handlers.ts
7975
8421
  import { randomUUID as randomUUID2 } from "node:crypto";
7976
8422
  var DEEP_DIVE_TIMEOUT_MS = 6e4;
7977
- function sendJson2(res, status, data) {
8423
+ function sendJson3(res, status, data) {
7978
8424
  res.writeHead(status, { "Content-Type": "application/json" });
7979
8425
  res.end(JSON.stringify(data));
7980
8426
  }
@@ -7989,13 +8435,13 @@ function handleTechStackSnapshot(res, deps2) {
7989
8435
  try {
7990
8436
  const snapshot = deps2.store.getSnapshot(deps2.projectId);
7991
8437
  if (!snapshot) {
7992
- sendJson2(res, 404, { snapshot: null, stale: false });
8438
+ sendJson3(res, 404, { snapshot: null, stale: false });
7993
8439
  return;
7994
8440
  }
7995
8441
  const ageMs = Date.now() - new Date(snapshot.createdAt).getTime();
7996
- sendJson2(res, 200, { snapshot, stale: ageMs > 24 * 60 * 60 * 1e3 });
8442
+ sendJson3(res, 200, { snapshot, stale: ageMs > 24 * 60 * 60 * 1e3 });
7997
8443
  } catch (error2) {
7998
- sendJson2(res, 500, {
8444
+ sendJson3(res, 500, {
7999
8445
  error: "TechStack store unavailable",
8000
8446
  detail: errorMessage(error2)
8001
8447
  });
@@ -8006,7 +8452,7 @@ function errorMessage(error2) {
8006
8452
  }
8007
8453
  function requireJobDeps(res, deps2) {
8008
8454
  if (!deps2.projectRoot || !deps2.engine) {
8009
- sendJson2(res, 503, { error: "TechStack engine unavailable" });
8455
+ sendJson3(res, 503, { error: "TechStack engine unavailable" });
8010
8456
  return false;
8011
8457
  }
8012
8458
  return true;
@@ -8017,7 +8463,7 @@ function startJob(res, deps2, kind) {
8017
8463
  const controller = new AbortController();
8018
8464
  deps2.runningJobs?.set(jobId, controller);
8019
8465
  deps2.emit?.({ type: "techstack.job.started", payload: { jobId, kind } });
8020
- sendJson2(res, 202, { jobId, kind, status: "queued" });
8466
+ sendJson3(res, 202, { jobId, kind, status: "queued" });
8021
8467
  void buildResearcher(deps2, kind).catch(() => void 0).then(
8022
8468
  (researcher) => deps2.engine.analyze(deps2.projectId, {
8023
8469
  targetRoot: deps2.projectRoot,
@@ -8063,24 +8509,24 @@ function handleTechStackCancel(res, deps2, jobId) {
8063
8509
  if (controller && !controller.signal.aborted) controller.abort();
8064
8510
  deps2.store.updateJobStatus(jobId, "cancelled");
8065
8511
  deps2.emit?.({ type: "techstack.job.cancelled", payload: { jobId } });
8066
- sendJson2(res, 200, { jobId, status: "cancelled" });
8512
+ sendJson3(res, 200, { jobId, status: "cancelled" });
8067
8513
  }
8068
8514
  async function handleTechStackDependencyResearch(res, deps2, dependencyId) {
8069
8515
  const snapshot = deps2.store.getSnapshot(deps2.projectId);
8070
8516
  const dependency = snapshot?.dependencies.find((dep) => dep.id === dependencyId);
8071
8517
  if (!dependency) {
8072
- sendJson2(res, 404, { error: "Dependency not found in the current snapshot" });
8518
+ sendJson3(res, 404, { error: "Dependency not found in the current snapshot" });
8073
8519
  return;
8074
8520
  }
8075
8521
  let researcher;
8076
8522
  try {
8077
8523
  researcher = await buildResearcher(deps2, "analyze");
8078
8524
  } catch (error2) {
8079
- sendJson2(res, 503, { error: "Research unavailable", detail: errorMessage(error2) });
8525
+ sendJson3(res, 503, { error: "Research unavailable", detail: errorMessage(error2) });
8080
8526
  return;
8081
8527
  }
8082
8528
  if (!researcher) {
8083
- sendJson2(res, 503, {
8529
+ sendJson3(res, 503, {
8084
8530
  error: "No model configured \u2014 connect a provider to run LLM analysis."
8085
8531
  });
8086
8532
  return;
@@ -8097,9 +8543,9 @@ async function handleTechStackDependencyResearch(res, deps2, dependencyId) {
8097
8543
  [triaged ?? { dependency, cluster: "breaking_change", priority: 0 }],
8098
8544
  { signal: controller.signal }
8099
8545
  );
8100
- sendJson2(res, 200, { dependencyId, findings });
8546
+ sendJson3(res, 200, { dependencyId, findings });
8101
8547
  } catch (error2) {
8102
- sendJson2(res, 500, { error: "Research failed", detail: errorMessage(error2) });
8548
+ sendJson3(res, 500, { error: "Research failed", detail: errorMessage(error2) });
8103
8549
  } finally {
8104
8550
  clearTimeout(timeout);
8105
8551
  controller.abort();
@@ -8108,15 +8554,15 @@ async function handleTechStackDependencyResearch(res, deps2, dependencyId) {
8108
8554
  function handleTechStackJobStatus(res, deps2, jobId) {
8109
8555
  const job = deps2.store.getJob(jobId);
8110
8556
  if (!job) {
8111
- sendJson2(res, 404, { error: "Job not found" });
8557
+ sendJson3(res, 404, { error: "Job not found" });
8112
8558
  return;
8113
8559
  }
8114
- sendJson2(res, 200, { job });
8560
+ sendJson3(res, 200, { job });
8115
8561
  }
8116
8562
  function handleTechStackReport(res, deps2, reportId, format) {
8117
8563
  const snapshot = deps2.store.getSnapshotById(reportId);
8118
8564
  if (!snapshot) {
8119
- sendJson2(res, 404, { error: "Report not found" });
8565
+ sendJson3(res, 404, { error: "Report not found" });
8120
8566
  return;
8121
8567
  }
8122
8568
  if (deps2.engine) {
@@ -8127,30 +8573,30 @@ function handleTechStackReport(res, deps2, reportId, format) {
8127
8573
  });
8128
8574
  res.end(report);
8129
8575
  } else {
8130
- sendJson2(res, 200, snapshot);
8576
+ sendJson3(res, 200, snapshot);
8131
8577
  }
8132
8578
  }
8133
8579
  async function handleTechStackTrends(res, deps2) {
8134
8580
  try {
8135
8581
  const { TrendStore } = await import("@wrongstack/techstack");
8136
- sendJson2(res, 200, { trend: new TrendStore(deps2.store).analyze(deps2.projectId) });
8582
+ sendJson3(res, 200, { trend: new TrendStore(deps2.store).analyze(deps2.projectId) });
8137
8583
  } catch (error2) {
8138
- sendJson2(res, 500, { error: "Trend analysis failed", detail: errorMessage(error2) });
8584
+ sendJson3(res, 500, { error: "Trend analysis failed", detail: errorMessage(error2) });
8139
8585
  }
8140
8586
  }
8141
8587
  async function handleTechStackRemediationPlan(res, deps2) {
8142
8588
  const snapshot = deps2.store.getSnapshot(deps2.projectId);
8143
8589
  if (!snapshot) {
8144
- sendJson2(res, 404, { error: "No TechStack snapshot is available" });
8590
+ sendJson3(res, 404, { error: "No TechStack snapshot is available" });
8145
8591
  return;
8146
8592
  }
8147
8593
  const { applyPlan, generateUpgradePlan } = await import("@wrongstack/techstack");
8148
8594
  const plan = generateUpgradePlan(snapshot);
8149
- sendJson2(res, 200, { plan, preview: await applyPlan(plan) });
8595
+ sendJson3(res, 200, { plan, preview: await applyPlan(plan) });
8150
8596
  }
8151
8597
  async function handleTechStackRemediationApply(req, res, deps2) {
8152
8598
  if (!deps2.executePackageOperation) {
8153
- sendJson2(res, 503, { error: "Permission-governed package execution is unavailable" });
8599
+ sendJson3(res, 503, { error: "Permission-governed package execution is unavailable" });
8154
8600
  return;
8155
8601
  }
8156
8602
  let approvedItems;
@@ -8163,16 +8609,16 @@ async function handleTechStackRemediationApply(req, res, deps2) {
8163
8609
  const body = JSON.parse(raw || "{}");
8164
8610
  approvedItems = Array.isArray(body.approvedItems) ? body.approvedItems.filter((value) => typeof value === "string") : [];
8165
8611
  } catch (error2) {
8166
- sendJson2(res, 400, { error: "Invalid request body", detail: errorMessage(error2) });
8612
+ sendJson3(res, 400, { error: "Invalid request body", detail: errorMessage(error2) });
8167
8613
  return;
8168
8614
  }
8169
8615
  if (approvedItems.length === 0) {
8170
- sendJson2(res, 400, { error: "approvedItems must explicitly identify at least one plan item" });
8616
+ sendJson3(res, 400, { error: "approvedItems must explicitly identify at least one plan item" });
8171
8617
  return;
8172
8618
  }
8173
8619
  const snapshot = deps2.store.getSnapshot(deps2.projectId);
8174
8620
  if (!snapshot) {
8175
- sendJson2(res, 404, { error: "No TechStack snapshot is available" });
8621
+ sendJson3(res, 404, { error: "No TechStack snapshot is available" });
8176
8622
  return;
8177
8623
  }
8178
8624
  const approved = new Set(approvedItems);
@@ -8187,14 +8633,17 @@ async function handleTechStackRemediationApply(req, res, deps2) {
8187
8633
  return executePackageOperation(operation, workspace?.relativeRoot);
8188
8634
  }
8189
8635
  });
8190
- sendJson2(res, 200, { plan, result });
8636
+ sendJson3(res, 200, { plan, result });
8191
8637
  }
8192
8638
 
8193
8639
  // src/server/ws-auth.ts
8194
8640
  import { Buffer as Buffer2 } from "node:buffer";
8195
8641
  import { timingSafeEqual } from "node:crypto";
8642
+ import { isLoopbackHost as isLoopbackHostCore } from "@wrongstack/core/hq";
8196
8643
  function isLoopbackHostname(hostname) {
8197
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
8644
+ const normalized = hostname.toLowerCase();
8645
+ if (normalized === "localhost") return true;
8646
+ return isLoopbackHostCore(normalized);
8198
8647
  }
8199
8648
  function effectivePort(url) {
8200
8649
  if (url.port) return url.port;
@@ -8215,7 +8664,7 @@ function isTrustedLoopbackOrigin(origin, hostHeader) {
8215
8664
  }
8216
8665
  }
8217
8666
  function isLoopbackBind(wsHost) {
8218
- return wsHost === "127.0.0.1" || wsHost === "::1" || wsHost === "localhost";
8667
+ return isLoopbackHostCore(wsHost) || wsHost === "localhost";
8219
8668
  }
8220
8669
  function isWildcardBind(wsHost) {
8221
8670
  return wsHost === "0.0.0.0" || wsHost === "::" || wsHost === "[::]";
@@ -8242,19 +8691,22 @@ function extractToken(url) {
8242
8691
  function extractTokenFromCookie(cookieHeader) {
8243
8692
  if (!cookieHeader) return void 0;
8244
8693
  const raw = Array.isArray(cookieHeader) ? cookieHeader.join("; ") : cookieHeader;
8694
+ let plain;
8245
8695
  for (const part of raw.split(";")) {
8246
8696
  const eq = part.indexOf("=");
8247
8697
  if (eq < 0) continue;
8248
8698
  const name2 = part.slice(0, eq).trim();
8249
- if (name2 === "ws_token") {
8250
- try {
8251
- return decodeURIComponent(part.slice(eq + 1).trim());
8252
- } catch {
8253
- return part.slice(eq + 1).trim();
8254
- }
8699
+ if (name2 !== "ws_token" && name2 !== "__Host-ws_token") continue;
8700
+ let value;
8701
+ try {
8702
+ value = decodeURIComponent(part.slice(eq + 1).trim());
8703
+ } catch {
8704
+ value = part.slice(eq + 1).trim();
8255
8705
  }
8706
+ if (name2 === "__Host-ws_token") return value;
8707
+ plain ??= value;
8256
8708
  }
8257
- return void 0;
8709
+ return plain;
8258
8710
  }
8259
8711
  function hostHeaderOk(input) {
8260
8712
  if (!isLoopbackBind(input.wsHost)) return true;
@@ -8296,7 +8748,8 @@ function verifyClient(input) {
8296
8748
  expectedToken,
8297
8749
  requireToken,
8298
8750
  allowedHostnames,
8299
- allowBrowserUrlToken
8751
+ allowBrowserUrlToken,
8752
+ allowCrossPortLoopbackCookie
8300
8753
  } = input;
8301
8754
  const urlTokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
8302
8755
  const cookieTokenOk = tokenMatches(extractTokenFromCookie(cookieHeader), expectedToken);
@@ -8311,7 +8764,10 @@ function verifyClient(input) {
8311
8764
  const { hostname: originHostname } = new URL(origin);
8312
8765
  if (isLoopbackHostname(originHostname)) {
8313
8766
  if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
8314
- return cookieTokenOk || isTrustedLoopbackOrigin(origin, hostHeader);
8767
+ if (!isTrustedLoopbackOrigin(origin, hostHeader)) {
8768
+ return Boolean(allowCrossPortLoopbackCookie) && cookieTokenOk;
8769
+ }
8770
+ return true;
8315
8771
  }
8316
8772
  return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
8317
8773
  } catch {
@@ -8346,11 +8802,22 @@ ${out}`;
8346
8802
  function firstHeader(value) {
8347
8803
  return Array.isArray(value) ? value[0] : value;
8348
8804
  }
8349
- function wsTokenCookie(token) {
8350
- return `ws_token=${encodeURIComponent(token)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=3600`;
8805
+ var WS_TOKEN_COOKIE = "ws_token";
8806
+ var WS_TOKEN_COOKIE_SECURE = "__Host-ws_token";
8807
+ function wsTokenCookie(token, secure) {
8808
+ const name2 = secure ? WS_TOKEN_COOKIE_SECURE : WS_TOKEN_COOKIE;
8809
+ const parts = [
8810
+ `${name2}=${encodeURIComponent(token)}`,
8811
+ "HttpOnly",
8812
+ "SameSite=Strict",
8813
+ "Path=/",
8814
+ "Max-Age=3600"
8815
+ ];
8816
+ if (secure) parts.push("Secure");
8817
+ return parts.join("; ");
8351
8818
  }
8352
- function setAuthCookieHeaders(res, token) {
8353
- res.setHeader("Set-Cookie", wsTokenCookie(token));
8819
+ function setAuthCookieHeaders(res, token, secure) {
8820
+ res.setHeader("Set-Cookie", wsTokenCookie(token, secure));
8354
8821
  res.setHeader("Cache-Control", "no-store");
8355
8822
  }
8356
8823
  function setStaticSecurityHeaders(res) {
@@ -8358,8 +8825,16 @@ function setStaticSecurityHeaders(res) {
8358
8825
  res.setHeader("X-Frame-Options", "DENY");
8359
8826
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
8360
8827
  }
8361
- function requestToken(req, url) {
8362
- return url.searchParams.get("token") ?? firstHeader(req.headers["x-ws-token"]) ?? extractTokenFromCookie(req.headers.cookie);
8828
+ function requestToken(req, url, opts = {}) {
8829
+ const queryToken = url.searchParams.get("token") ?? void 0;
8830
+ if (queryToken !== void 0 && (opts.allowQuery === true || isLoopbackPeer(req))) {
8831
+ return queryToken;
8832
+ }
8833
+ return firstHeader(req.headers["x-ws-token"]) ?? extractTokenFromCookie(req.headers.cookie);
8834
+ }
8835
+ function isLoopbackPeer(req) {
8836
+ const address = req.socket.remoteAddress?.replace(/^::ffff:/i, "");
8837
+ return address !== void 0 && isLoopbackHostname(address);
8363
8838
  }
8364
8839
  function formatCspHostname(hostname) {
8365
8840
  return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
@@ -8414,7 +8889,8 @@ function strictDecodeParam(segment, res) {
8414
8889
  function createHttpServer(opts) {
8415
8890
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
8416
8891
  const distDir = path13.resolve(opts.distDir);
8417
- const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
8892
+ const requireAccessToken = true;
8893
+ const secureCookies = opts.secureCookies ?? (opts.publicWsUrl?.trim().toLowerCase().startsWith("wss:") ?? false);
8418
8894
  const trustedHostnames = (() => {
8419
8895
  const names = [...opts.allowedHostnames ?? []];
8420
8896
  if (opts.publicWsUrl) {
@@ -8453,13 +8929,13 @@ function createHttpServer(opts) {
8453
8929
  const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
8454
8930
  const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
8455
8931
  if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
8456
- const provided = requestToken(req, url);
8932
+ const provided = requestToken(req, url, { allowQuery: true });
8457
8933
  if (!provided || !opts.apiToken || !tokenMatches(provided, opts.apiToken)) {
8458
8934
  res.writeHead(401, { "Content-Type": "text/plain" });
8459
8935
  res.end("Unauthorized");
8460
8936
  return;
8461
8937
  }
8462
- setAuthCookieHeaders(res, opts.apiToken);
8938
+ setAuthCookieHeaders(res, opts.apiToken, secureCookies);
8463
8939
  res.writeHead(200, { "Content-Type": "text/plain" });
8464
8940
  res.end("ok");
8465
8941
  return;
@@ -8473,7 +8949,7 @@ function createHttpServer(opts) {
8473
8949
  return;
8474
8950
  }
8475
8951
  if (shouldSetAuthCookie && opts.apiToken) {
8476
- setAuthCookieHeaders(res, opts.apiToken);
8952
+ setAuthCookieHeaders(res, opts.apiToken, secureCookies);
8477
8953
  }
8478
8954
  if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
8479
8955
  if (requireAccessToken && !accessTokenOk) {
@@ -8591,6 +9067,88 @@ function createHttpServer(opts) {
8591
9067
  await handleApiAnalyticsSummary(res);
8592
9068
  return;
8593
9069
  }
9070
+ if (url.pathname === "/api/requirement-intakes" && req.method === "GET") {
9071
+ if (requireAccessToken && !accessTokenOk) {
9072
+ res.writeHead(401, { "Content-Type": "application/json" });
9073
+ res.end(JSON.stringify({ error: "Unauthorized" }));
9074
+ return;
9075
+ }
9076
+ await handleRequirementIntakeListForServer(res, opts.intakeService, opts.projectRoot);
9077
+ return;
9078
+ }
9079
+ const intakeProjectMatch = url.pathname.match(
9080
+ /^\/api\/projects\/([^/]+)\/requirement-intakes$/
9081
+ );
9082
+ if (intakeProjectMatch && req.method === "POST") {
9083
+ if (requireAccessToken && !accessTokenOk) {
9084
+ res.writeHead(401, { "Content-Type": "application/json" });
9085
+ res.end(JSON.stringify({ error: "Unauthorized" }));
9086
+ return;
9087
+ }
9088
+ await handleRequirementIntakeCreate(
9089
+ res,
9090
+ req,
9091
+ opts.intakeService,
9092
+ decodeURIComponent(intakeProjectMatch[1])
9093
+ );
9094
+ return;
9095
+ }
9096
+ if (intakeProjectMatch && req.method === "GET") {
9097
+ if (requireAccessToken && !accessTokenOk) {
9098
+ res.writeHead(401, { "Content-Type": "application/json" });
9099
+ res.end(JSON.stringify({ error: "Unauthorized" }));
9100
+ return;
9101
+ }
9102
+ await handleRequirementIntakeList(
9103
+ res,
9104
+ opts.intakeService,
9105
+ decodeURIComponent(intakeProjectMatch[1])
9106
+ );
9107
+ return;
9108
+ }
9109
+ const intakeIdMatch = url.pathname.match(/^\/api\/requirement-intakes\/([^/]+)$/);
9110
+ if (intakeIdMatch && req.method === "GET") {
9111
+ if (requireAccessToken && !accessTokenOk) {
9112
+ res.writeHead(401, { "Content-Type": "application/json" });
9113
+ res.end(JSON.stringify({ error: "Unauthorized" }));
9114
+ return;
9115
+ }
9116
+ await handleRequirementIntakeGet(res, opts.intakeService, intakeIdMatch[1]);
9117
+ return;
9118
+ }
9119
+ if (intakeIdMatch && req.method === "PATCH") {
9120
+ if (requireAccessToken && !accessTokenOk) {
9121
+ res.writeHead(401, { "Content-Type": "application/json" });
9122
+ res.end(JSON.stringify({ error: "Unauthorized" }));
9123
+ return;
9124
+ }
9125
+ await handleRequirementIntakeUpdate(res, req, opts.intakeService, intakeIdMatch[1]);
9126
+ return;
9127
+ }
9128
+ const intakeActionMatch = url.pathname.match(
9129
+ /^\/api\/requirement-intakes\/([^/]+)\/(answers|suggestions|submit|cancel|archive)$/
9130
+ );
9131
+ if (intakeActionMatch && req.method === "POST") {
9132
+ if (requireAccessToken && !accessTokenOk) {
9133
+ res.writeHead(401, { "Content-Type": "application/json" });
9134
+ res.end(JSON.stringify({ error: "Unauthorized" }));
9135
+ return;
9136
+ }
9137
+ const intakeId = intakeActionMatch[1];
9138
+ const action = intakeActionMatch[2];
9139
+ if (action === "answers") {
9140
+ await handleRequirementIntakeAnswers(res, req, opts.intakeService, intakeId);
9141
+ } else if (action === "suggestions") {
9142
+ await handleRequirementIntakeSuggestions(res, req, opts.intakeService, intakeId);
9143
+ } else if (action === "submit") {
9144
+ await handleRequirementIntakeSubmit(res, opts.intakeService, intakeId);
9145
+ } else if (action === "cancel") {
9146
+ await handleRequirementIntakeCancel(res, req, opts.intakeService, intakeId);
9147
+ } else {
9148
+ await handleRequirementIntakeArchive(res, opts.intakeService, intakeId);
9149
+ }
9150
+ return;
9151
+ }
8594
9152
  if (url.pathname === "/api/codemap/packages" && req.method === "GET") {
8595
9153
  if (requireAccessToken && !accessTokenOk) {
8596
9154
  res.writeHead(401, { "Content-Type": "application/json" });
@@ -8786,17 +9344,25 @@ function createHttpServer(opts) {
8786
9344
  res.end(JSON.stringify({ error: "Project root not configured" }));
8787
9345
  return;
8788
9346
  }
8789
- await handleDeadCodeScan(res, {
8790
- projectRoot: opts.projectRoot,
8791
- indexDir: opts.indexDir
8792
- }, req);
9347
+ await handleDeadCodeScan(
9348
+ res,
9349
+ {
9350
+ projectRoot: opts.projectRoot,
9351
+ indexDir: opts.indexDir
9352
+ },
9353
+ req
9354
+ );
8793
9355
  return;
8794
9356
  }
8795
9357
  if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
8796
- await handleDeadCodeActionPlan(res, {
8797
- projectRoot: opts.projectRoot ?? "",
8798
- indexDir: opts.indexDir
8799
- }, req);
9358
+ await handleDeadCodeActionPlan(
9359
+ res,
9360
+ {
9361
+ projectRoot: opts.projectRoot ?? "",
9362
+ indexDir: opts.indexDir
9363
+ },
9364
+ req
9365
+ );
8800
9366
  return;
8801
9367
  }
8802
9368
  if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
@@ -11026,6 +11592,55 @@ import {
11026
11592
  restartMcp,
11027
11593
  updateMcp
11028
11594
  } from "@wrongstack/mcp";
11595
+
11596
+ // src/server/privileged-actions.ts
11597
+ import { randomUUID as randomUUID3 } from "node:crypto";
11598
+ import {
11599
+ isTrustDecisionAllowed
11600
+ } from "@wrongstack/core/security";
11601
+ async function authorizeWebUIAction(boundary, action, logger) {
11602
+ const request = {
11603
+ version: 1,
11604
+ requestId: randomUUID3(),
11605
+ actor: {
11606
+ kind: "remote-client",
11607
+ ...action.sessionId ? { sessionId: action.sessionId } : {}
11608
+ },
11609
+ surface: "webui",
11610
+ capability: action.capability,
11611
+ subject: action.subject,
11612
+ risk: action.risk,
11613
+ scope: {
11614
+ ...action.cwd ? { cwd: action.cwd } : {},
11615
+ ...action.sessionId ? { sessionId: action.sessionId } : {}
11616
+ },
11617
+ authContext: { method: "session" },
11618
+ ...action.metadata ? { metadata: action.metadata } : {}
11619
+ };
11620
+ const decision = await boundary.evaluate(request);
11621
+ logger?.debug?.(
11622
+ `[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
11623
+ );
11624
+ return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
11625
+ }
11626
+
11627
+ // src/server/mcp-handlers.ts
11628
+ async function authorizeMcpMutation(ws, operation, serverName, trustBoundary) {
11629
+ if (!trustBoundary) return true;
11630
+ const authorization = await authorizeWebUIAction(trustBoundary, {
11631
+ capability: "mcp.server.configure",
11632
+ subject: { kind: "process", id: serverName },
11633
+ risk: "elevated",
11634
+ metadata: { transport: "websocket", operation }
11635
+ });
11636
+ if (!authorization.allowed) {
11637
+ send(ws, {
11638
+ type: "mcp.operation_result",
11639
+ payload: { success: false, message: `${operation} denied: ${authorization.reason}` }
11640
+ });
11641
+ }
11642
+ return authorization.allowed;
11643
+ }
11029
11644
  function mapStatus(raw) {
11030
11645
  switch (raw) {
11031
11646
  case "connected":
@@ -11100,7 +11715,7 @@ async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
11100
11715
  payload: { servers: servers.map((server) => toView(server, health.get(server.name))) }
11101
11716
  });
11102
11717
  }
11103
- async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
11718
+ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry, trustBoundary) {
11104
11719
  const d = deps(ws, globalConfigPath, mcpRegistry);
11105
11720
  if (!d) return;
11106
11721
  const validated = validateMcpServerPayload(msg.payload, "mcp.add");
@@ -11111,6 +11726,7 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
11111
11726
  });
11112
11727
  return;
11113
11728
  }
11729
+ if (!await authorizeMcpMutation(ws, "mcp.add", name(msg), trustBoundary)) return;
11114
11730
  const result = await addMcp(validated.value, d);
11115
11731
  if (result.ok && result.server) {
11116
11732
  send(ws, { type: "mcp.server.added", payload: { server: toView(result.server) } });
@@ -11128,7 +11744,7 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
11128
11744
  payload: { success: result.ok, message: result.message }
11129
11745
  });
11130
11746
  }
11131
- async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
11747
+ async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry, trustBoundary) {
11132
11748
  const d = deps(ws, globalConfigPath, mcpRegistry);
11133
11749
  if (!d) return;
11134
11750
  const validated = validateMcpServerPayload(msg.payload, "mcp.update");
@@ -11139,6 +11755,7 @@ async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
11139
11755
  });
11140
11756
  return;
11141
11757
  }
11758
+ if (!await authorizeMcpMutation(ws, "mcp.update", name(msg), trustBoundary)) return;
11142
11759
  const result = await updateMcp(validated.value, d);
11143
11760
  if (result.ok && result.server) {
11144
11761
  send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
@@ -12239,11 +12856,11 @@ function createModelOperations(context) {
12239
12856
  }
12240
12857
 
12241
12858
  // src/server/port-utils.ts
12242
- import * as net2 from "node:net";
12859
+ import * as net from "node:net";
12243
12860
  import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
12244
12861
  function isPortFree(host, port) {
12245
12862
  return new Promise((resolve15) => {
12246
- const srv = net2.createServer();
12863
+ const srv = net.createServer();
12247
12864
  srv.once("error", () => resolve15(false));
12248
12865
  srv.once("listening", () => {
12249
12866
  srv.close(() => resolve15(true));
@@ -12272,6 +12889,25 @@ async function findFreePort(host, startPort, opts = {}) {
12272
12889
  });
12273
12890
  }
12274
12891
 
12892
+ // src/server/intake-service.ts
12893
+ import { resolveWstackPaths as resolveWstackPaths4 } from "@wrongstack/core/utils";
12894
+ import {
12895
+ AllowAllIntakeAuthorizer,
12896
+ RequirementIntakeService,
12897
+ RequirementIntakeStore
12898
+ } from "@wrongstack/requirement-intake";
12899
+ function createProjectIntakeService(opts) {
12900
+ return new RequirementIntakeService({
12901
+ store: new RequirementIntakeStore({
12902
+ baseDir: resolveWstackPaths4({
12903
+ projectRoot: opts.projectRoot,
12904
+ globalRoot: opts.globalRoot
12905
+ }).projectRequirementIntakes
12906
+ }),
12907
+ authorizer: new AllowAllIntakeAuthorizer()
12908
+ });
12909
+ }
12910
+
12275
12911
  // src/server/network-info.ts
12276
12912
  import * as os2 from "node:os";
12277
12913
  function isTailscaleIPv4(addr) {
@@ -12327,7 +12963,21 @@ function formatExternalAccessUrls(opts) {
12327
12963
  }
12328
12964
 
12329
12965
  // src/server/brain-handlers.ts
12966
+ import { BUILTIN_COUNCIL_PERSONAS } from "@wrongstack/core/execution";
12330
12967
  import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
12968
+ var COUNCIL_PERSONA_CATALOG = Object.freeze(
12969
+ BUILTIN_COUNCIL_PERSONAS.map(
12970
+ (persona) => Object.freeze({
12971
+ id: persona.id,
12972
+ name: persona.name,
12973
+ description: persona.description,
12974
+ ...persona.defaultVeto !== void 0 ? { defaultVeto: persona.defaultVeto } : {}
12975
+ })
12976
+ )
12977
+ );
12978
+ function brainConfigPayload(runtime) {
12979
+ return { ...runtime.getSnapshot(), personaCatalog: COUNCIL_PERSONA_CATALOG };
12980
+ }
12331
12981
  function sendResult6(ctx, ws, success, message) {
12332
12982
  ctx.send(ws, { type: "key.operation_result", payload: { success, message } });
12333
12983
  }
@@ -12367,7 +13017,7 @@ function handleBrainConfigGet(ctx, ws) {
12367
13017
  }
12368
13018
  ctx.send(ws, {
12369
13019
  type: "brain.config",
12370
- payload: { config: ctx.brainRuntime.getSnapshot(), persisted: true }
13020
+ payload: { config: brainConfigPayload(ctx.brainRuntime), persisted: true }
12371
13021
  });
12372
13022
  }
12373
13023
  async function handleBrainConfigSet(ctx, ws, payload) {
@@ -12391,7 +13041,7 @@ async function handleBrainConfigSet(ctx, ws, payload) {
12391
13041
  ctx.send(ws, {
12392
13042
  type: "brain.config",
12393
13043
  payload: {
12394
- config: ctx.brainRuntime.getSnapshot(),
13044
+ config: brainConfigPayload(ctx.brainRuntime),
12395
13045
  persisted: result.ok,
12396
13046
  ...result.ok ? {} : { error: result.error ?? "Persist failed." }
12397
13047
  }
@@ -12401,7 +13051,7 @@ async function handleBrainConfigSet(ctx, ws, payload) {
12401
13051
  ctx.send(ws, {
12402
13052
  type: "brain.config",
12403
13053
  payload: {
12404
- config: ctx.brainRuntime.getSnapshot(),
13054
+ config: brainConfigPayload(ctx.brainRuntime),
12405
13055
  persisted: false,
12406
13056
  error: `Invalid Brain setting: ${toErrorMessage5(err)}`
12407
13057
  }
@@ -12455,6 +13105,8 @@ async function handleBrainAsk(ctx, ws, question) {
12455
13105
 
12456
13106
  // src/server/context-meta.ts
12457
13107
  import { FallbackProfileManager } from "@wrongstack/core/agent";
13108
+ import { resolvePluginEnablement } from "@wrongstack/core/plugin";
13109
+ import { FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
12458
13110
  function seedContextMeta(config, context) {
12459
13111
  const meta = context.meta;
12460
13112
  const autonomyCfg = config.autonomy ?? {};
@@ -12531,6 +13183,21 @@ function seedContextMeta(config, context) {
12531
13183
  meta["tgDelegate"] = tgExt?.["notifyOnDelegate"] !== false;
12532
13184
  const tgMs = tgExt?.["longToolThresholdMs"];
12533
13185
  meta["tgLongToolMs"] = typeof tgMs === "number" ? tgMs : 3e4;
13186
+ {
13187
+ const pluginsEnabled = {};
13188
+ const record = (name2) => {
13189
+ if (FORBIDDEN_PROTO_KEYS2.has(name2) || name2 in pluginsEnabled) return;
13190
+ pluginsEnabled[name2] = resolvePluginEnablement({ name: name2, config }).enabled;
13191
+ };
13192
+ for (const entry of config.plugins ?? []) {
13193
+ const name2 = typeof entry === "string" ? entry : entry?.name;
13194
+ if (typeof name2 === "string") record(name2);
13195
+ }
13196
+ for (const [name2, options] of Object.entries(config.extensions ?? {})) {
13197
+ if (typeof options?.["enabled"] === "boolean") record(name2);
13198
+ }
13199
+ if (Object.keys(pluginsEnabled).length > 0) meta["pluginsEnabled"] = pluginsEnabled;
13200
+ }
12534
13201
  const chimeraExt = config.extensions?.["wstack-chimera"];
12535
13202
  meta["chimeraEnabled"] = chimeraExt?.["enabled"] === true;
12536
13203
  meta["chimeraProvider"] = chimeraExt?.["provider"] ?? "";
@@ -12568,8 +13235,9 @@ function seedContextMeta(config, context) {
12568
13235
  // src/server/pref-helpers.ts
12569
13236
  import * as fs13 from "node:fs/promises";
12570
13237
  import * as path15 from "node:path";
13238
+ import { pluginEntryMatchesName } from "@wrongstack/core/plugin";
12571
13239
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
12572
- import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
13240
+ import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS3 } from "@wrongstack/core/utils";
12573
13241
  var PREF_KEYS = [
12574
13242
  "autonomy",
12575
13243
  "autonomyDelayMs",
@@ -12650,6 +13318,8 @@ var PREF_KEYS = [
12650
13318
  // Display-only toggles (purely visual WebUI prefs, not persisted to config).
12651
13319
  "groupToolCalls",
12652
13320
  "showThinkingLogs",
13321
+ // v15: chat-input auto-collapse (opt-in display toggle, default off).
13322
+ "autoCollapseInput",
12653
13323
  // Per-plugin enable/disable map (parity with the embedded server).
12654
13324
  "pluginsEnabled",
12655
13325
  // Fleet chat verbosity: off | full (migrated from streamFleet boolean).
@@ -12704,6 +13374,8 @@ async function updateGlobalConfig(deps2, holder, mutate, errorLabel) {
12704
13374
  var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set([
12705
13375
  "groupToolCalls",
12706
13376
  "showThinkingLogs",
13377
+ // v15: chat-input auto-collapse (opt-in display toggle, default off).
13378
+ "autoCollapseInput",
12707
13379
  "autoReviewFallbackModels",
12708
13380
  // v11 Display parity: agent-swarm panel + inverse fsAccess flag.
12709
13381
  // The TUI settings picker mirrors these so the browser can keep the
@@ -12914,15 +13586,29 @@ async function persistPrefsToConfig(deps2, holder, payload) {
12914
13586
  decrypted.debugStream = payload["debugStream"];
12915
13587
  if (typeof payload["pluginsEnabled"] === "object" && payload["pluginsEnabled"] !== null) {
12916
13588
  const ext = decrypted.extensions ?? {};
13589
+ const toggled = [];
12917
13590
  for (const [pluginName, enabled] of Object.entries(
12918
13591
  payload["pluginsEnabled"]
12919
13592
  )) {
12920
- if (FORBIDDEN_PROTO_KEYS2.has(pluginName)) continue;
13593
+ if (FORBIDDEN_PROTO_KEYS3.has(pluginName)) continue;
13594
+ if (typeof enabled !== "boolean") continue;
12921
13595
  const pExt = ext[pluginName] ?? {};
12922
13596
  pExt["enabled"] = enabled;
12923
13597
  ext[pluginName] = pExt;
13598
+ toggled.push([pluginName, enabled]);
12924
13599
  }
12925
13600
  decrypted.extensions = ext;
13601
+ if (Array.isArray(decrypted.plugins) && toggled.length > 0) {
13602
+ decrypted.plugins = decrypted.plugins.map((entry) => {
13603
+ const entryName = typeof entry === "string" ? entry : entry?.name;
13604
+ if (typeof entryName !== "string") return entry;
13605
+ const hit = toggled.find(([name2]) => pluginEntryMatchesName(entryName, name2));
13606
+ if (!hit) return entry;
13607
+ const [, enabled] = hit;
13608
+ if (typeof entry === "string") return enabled ? entry : { name: entry, enabled: false };
13609
+ return { ...entry, enabled };
13610
+ });
13611
+ }
12926
13612
  }
12927
13613
  const chimeraTouched = typeof payload["chimeraEnabled"] === "boolean" || typeof payload["chimeraProvider"] === "string" || typeof payload["chimeraModel"] === "string" || typeof payload["chimeraMaxFiles"] === "number" || typeof payload["chimeraAutoFix"] === "string";
12928
13614
  if (chimeraTouched) {
@@ -13031,39 +13717,6 @@ async function handlePrefsRoute(ws, msg, handlers) {
13031
13717
  // src/server/process-handlers.ts
13032
13718
  import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
13033
13719
  import { getProcessRegistry as getProcessRegistry2 } from "@wrongstack/tools";
13034
-
13035
- // src/server/privileged-actions.ts
13036
- import { randomUUID as randomUUID3 } from "node:crypto";
13037
- import {
13038
- isTrustDecisionAllowed
13039
- } from "@wrongstack/core/security";
13040
- async function authorizeWebUIAction(boundary, action, logger) {
13041
- const request = {
13042
- version: 1,
13043
- requestId: randomUUID3(),
13044
- actor: {
13045
- kind: "remote-client",
13046
- ...action.sessionId ? { sessionId: action.sessionId } : {}
13047
- },
13048
- surface: "webui",
13049
- capability: action.capability,
13050
- subject: action.subject,
13051
- risk: action.risk,
13052
- scope: {
13053
- ...action.cwd ? { cwd: action.cwd } : {},
13054
- ...action.sessionId ? { sessionId: action.sessionId } : {}
13055
- },
13056
- authContext: { method: "session" },
13057
- ...action.metadata ? { metadata: action.metadata } : {}
13058
- };
13059
- const decision = await boundary.evaluate(request);
13060
- logger?.debug?.(
13061
- `[trust-boundary] ${request.capability} ${decision.kind} request=${request.requestId}`
13062
- );
13063
- return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason, request };
13064
- }
13065
-
13066
- // src/server/process-handlers.ts
13067
13720
  function handleProcessList(ws) {
13068
13721
  try {
13069
13722
  const procs = getProcessRegistry2().list();
@@ -13166,7 +13819,7 @@ async function handleProcessRoute(ws, msg, handlers) {
13166
13819
  import * as fs14 from "node:fs/promises";
13167
13820
  import * as path16 from "node:path";
13168
13821
  import { DefaultSessionStore } from "@wrongstack/core/storage";
13169
- import { resolveWstackPaths as resolveWstackPaths4 } from "@wrongstack/core/utils";
13822
+ import { resolveWstackPaths as resolveWstackPaths5 } from "@wrongstack/core/utils";
13170
13823
  function createProjectHandlers(ctx) {
13171
13824
  const sendTo = (ws, message) => {
13172
13825
  if (ctx.sendMessage) ctx.sendMessage(ws, message);
@@ -13284,7 +13937,7 @@ function createProjectHandlers(ctx) {
13284
13937
  });
13285
13938
  return;
13286
13939
  }
13287
- const paths = resolveWstackPaths4({
13940
+ const paths = resolveWstackPaths5({
13288
13941
  projectRoot: resolved,
13289
13942
  globalRoot: ctx.wpaths.globalRoot
13290
13943
  });
@@ -13295,7 +13948,7 @@ function createProjectHandlers(ctx) {
13295
13948
  const previous = ctx.getSession();
13296
13949
  const previousId = previous.id;
13297
13950
  const previousProjectRoot = ctx.getProjectRoot();
13298
- const previousPaths = resolveWstackPaths4({
13951
+ const previousPaths = resolveWstackPaths5({
13299
13952
  projectRoot: previousProjectRoot,
13300
13953
  globalRoot: ctx.wpaths.globalRoot
13301
13954
  });
@@ -13879,8 +14532,10 @@ function createProviderOperations(deps2) {
13879
14532
  if (result.ok) {
13880
14533
  deps2.log?.(`[WebUI] Provider "${payload.id}" added via provider.add`);
13881
14534
  }
14535
+ return result.ok;
13882
14536
  } catch (err) {
13883
14537
  sendOperationResult(ws, false, errMessage(err));
14538
+ return false;
13884
14539
  }
13885
14540
  }
13886
14541
  async function handleProviderRemove(ws, providerId) {
@@ -14201,6 +14856,7 @@ var CLIENT_CONVERSATION_MESSAGE_TYPES = [
14201
14856
  "completion.request",
14202
14857
  "model.switch",
14203
14858
  "model.refine",
14859
+ "model.fallback_choice",
14204
14860
  "autonomy.switch",
14205
14861
  "context.clear",
14206
14862
  "context.compact",
@@ -14218,6 +14874,7 @@ var CLIENT_CONVERSATION_MESSAGE_TYPES = [
14218
14874
  "modes.list",
14219
14875
  "session.checkpoints",
14220
14876
  "session.delete",
14877
+ "session.inspect",
14221
14878
  "session.new",
14222
14879
  "session.rename",
14223
14880
  "session.resume",
@@ -14471,6 +15128,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
14471
15128
  "provider.active_blocked",
14472
15129
  "provider.error",
14473
15130
  "provider.fallback",
15131
+ "provider.fallback_pending",
14474
15132
  "provider.response",
14475
15133
  "provider.retry",
14476
15134
  "provider.status_changed",
@@ -14481,6 +15139,7 @@ var SERVER_CONVERSATION_MESSAGE_TYPES = [
14481
15139
  "session.checkpoints",
14482
15140
  "session.damaged",
14483
15141
  "session.end",
15142
+ "session.inspect",
14484
15143
  "session.rewound",
14485
15144
  "session.start",
14486
15145
  "session.stats",
@@ -14511,6 +15170,7 @@ var SERVER_COLLABORATION_MESSAGE_TYPES = [
14511
15170
  "collab.state",
14512
15171
  "mailbox.action_result",
14513
15172
  "mailbox.agent_registered",
15173
+ "mailbox.agent_deregistered",
14514
15174
  "mailbox.agents",
14515
15175
  "mailbox.cleared",
14516
15176
  "mailbox.compacted",
@@ -14839,6 +15499,271 @@ function protocolAdvertisement() {
14839
15499
  }
14840
15500
 
14841
15501
  // src/server/session-history.ts
15502
+ function blockText(block) {
15503
+ if (typeof block === "string") return block;
15504
+ switch (block.type) {
15505
+ case "text":
15506
+ return block.text;
15507
+ case "thinking":
15508
+ return block.thinking;
15509
+ case "tool_use":
15510
+ return `[tool:${block.name}]`;
15511
+ case "tool_result":
15512
+ return block.content;
15513
+ case "image":
15514
+ return "[image]";
15515
+ default: {
15516
+ const _exhaustive = block;
15517
+ void _exhaustive;
15518
+ return "[block]";
15519
+ }
15520
+ }
15521
+ }
15522
+ function labelForEvent(e) {
15523
+ switch (e.type) {
15524
+ case "session_start":
15525
+ return "Session started";
15526
+ case "session_resumed":
15527
+ return "Session resumed";
15528
+ case "session_forked":
15529
+ return `Forked from ${e.parentSessionId}`;
15530
+ case "user_input": {
15531
+ const content = typeof e.content === "string" ? e.content : Array.isArray(e.content) ? e.content.map(blockText).join("") : "[input]";
15532
+ return `User: ${content.length > 100 ? content.slice(0, 100) + "\u2026" : content}`;
15533
+ }
15534
+ case "llm_request":
15535
+ return "LLM request";
15536
+ case "llm_response":
15537
+ return "LLM response";
15538
+ case "tool_use":
15539
+ return `Tool: ${e.name}`;
15540
+ case "tool_call_start":
15541
+ return `Tool start: ${e.name}`;
15542
+ case "tool_call_end":
15543
+ return `Tool done: ${e.name} (${e.durationMs}ms, ${e.ok === false ? "error" : "ok"})`;
15544
+ case "tool_result":
15545
+ return `Tool result: ${e.id}${e.isError ? " (error)" : ""}`;
15546
+ case "tool_progress":
15547
+ return `Progress: ${e.name} \u2014 ${e.event.type}`;
15548
+ case "compaction":
15549
+ return `Compaction: ${e.before} \u2192 ${e.after} tokens`;
15550
+ case "context_snapshot":
15551
+ return "Context snapshot";
15552
+ case "error":
15553
+ return `Error: ${e.message.length > 100 ? e.message.slice(0, 100) + "\u2026" : e.message}`;
15554
+ case "session_end":
15555
+ return "Session ended";
15556
+ case "message_appended":
15557
+ return `Message appended: ${e.message.role}`;
15558
+ case "message_updated":
15559
+ return "Message updated";
15560
+ case "messages_replaced": {
15561
+ const count = e.messagesOmitted ?? e.messages.length;
15562
+ return `Messages replaced (${e.messagesOmitted ? "~" : ""}${count} msgs)`;
15563
+ }
15564
+ case "message_truncated":
15565
+ return `Message truncated: ${e.before} \u2192 ${e.after}`;
15566
+ case "file_event":
15567
+ return `File ${e.operation}: ${e.filePath} (${e.toolName})`;
15568
+ case "file_snapshot":
15569
+ return `File snapshot at prompt #${e.promptIndex}`;
15570
+ case "file_observation":
15571
+ return `File observation: ${e.path}`;
15572
+ case "rewound":
15573
+ return `Rewound to prompt #${e.toPromptIndex} (${e.revertedFiles.length} files)`;
15574
+ case "mode_changed":
15575
+ return `Mode: ${e.from} \u2192 ${e.to}`;
15576
+ case "task_created":
15577
+ return `Task created: ${e.title}`;
15578
+ case "task_updated":
15579
+ return `Task ${e.taskId}: ${e.status}`;
15580
+ case "task_completed":
15581
+ return `Task done: ${e.title}`;
15582
+ case "task_failed":
15583
+ return `Task failed: ${e.title} \u2014 ${e.error.length > 80 ? e.error.slice(0, 80) + "\u2026" : e.error}`;
15584
+ case "agent_spawned":
15585
+ return `Agent spawned: ${e.role}`;
15586
+ case "agent_stopped":
15587
+ return "Agent stopped";
15588
+ case "agent_error":
15589
+ return `Agent error: ${e.error.length > 80 ? e.error.slice(0, 80) + "\u2026" : e.error}`;
15590
+ case "provider_retry":
15591
+ return `Retry: ${e.description} (attempt ${e.attempt})`;
15592
+ case "provider_error":
15593
+ return `Provider error: ${e.description}`;
15594
+ case "checkpoint":
15595
+ return `Checkpoint at prompt #${e.promptIndex}`;
15596
+ case "in_flight_start":
15597
+ return `In-flight started: ${e.context}`;
15598
+ case "in_flight_end":
15599
+ return `In-flight ended: ${e.reason}`;
15600
+ case "side_effect":
15601
+ return `Side effect: ${e.toolName} (${e.risk})`;
15602
+ case "spec_parsed":
15603
+ return `Spec parsed: ${e.title}`;
15604
+ case "spec_analyzed":
15605
+ return `Spec analyzed: ${e.specId}`;
15606
+ case "skill_activated":
15607
+ return `Skill: ${e.skillName}`;
15608
+ case "skill_deactivated":
15609
+ return `Skill done: ${e.skillName}`;
15610
+ default: {
15611
+ const _exhaustive = e;
15612
+ void _exhaustive;
15613
+ return String(_exhaustive);
15614
+ }
15615
+ }
15616
+ }
15617
+ function detailForEvent(e) {
15618
+ switch (e.type) {
15619
+ case "session_start":
15620
+ return `${e.model} @ ${e.provider}`;
15621
+ case "session_resumed":
15622
+ return `${e.model} @ ${e.provider}`;
15623
+ case "session_forked":
15624
+ return `parent checkpoint: ${e.parentCheckpointHash.slice(0, 12)}\u2026`;
15625
+ case "llm_request":
15626
+ return `${e.model} \xB7 ${e.messageCount} msgs \xB7 ${e.toolCount ?? "?"} tools`;
15627
+ case "llm_response":
15628
+ return `${e.stopReason} \xB7 ${e.usage.input ?? 0}+${e.usage.output ?? 0} tokens`;
15629
+ case "tool_use":
15630
+ return `id: ${e.id}`;
15631
+ case "tool_call_start":
15632
+ return `id: ${e.id}`;
15633
+ case "tool_call_end":
15634
+ return `${(e.outputBytes ?? e.outputSize ?? 0).toLocaleString()} B \xB7 ${e.outputLines ?? 0} lines`;
15635
+ case "tool_progress":
15636
+ return `${e.event.type}${e.event.text ? `: ${e.event.text}` : ""}`;
15637
+ case "compaction":
15638
+ return `saved ~${Math.max(0, e.before - e.after)} tokens`;
15639
+ case "context_snapshot":
15640
+ return `${e.messages.length} msgs${e.messagesOmitted ? ` (${e.messagesOmitted} omitted)` : ""}`;
15641
+ case "error":
15642
+ return `phase: ${e.phase}`;
15643
+ case "session_end":
15644
+ return `${e.usage.input ?? 0}+${e.usage.output ?? 0} total tokens`;
15645
+ case "message_appended":
15646
+ return `appended ${e.message.role}`;
15647
+ case "message_updated":
15648
+ return `at index ${e.index}`;
15649
+ case "messages_replaced":
15650
+ return `${e.messagesOmitted ?? e.messages.length} total`;
15651
+ case "message_truncated":
15652
+ return `truncated to ${e.after} tokens`;
15653
+ case "mode_changed":
15654
+ return `${e.from} \u2192 ${e.to}`;
15655
+ case "task_created":
15656
+ return `id: ${e.taskId}`;
15657
+ case "task_updated":
15658
+ return `${e.taskId}: ${e.status}`;
15659
+ case "task_completed":
15660
+ return `${e.taskId}`;
15661
+ case "task_failed":
15662
+ return `${e.taskId}: ${e.error}`;
15663
+ case "agent_spawned":
15664
+ return `id: ${e.agentId} (${e.role})`;
15665
+ case "agent_stopped":
15666
+ return `id: ${e.agentId}`;
15667
+ case "agent_error":
15668
+ return `id: ${e.agentId}: ${e.error}`;
15669
+ case "file_event":
15670
+ return `${e.filePath} (${e.toolName})`;
15671
+ case "file_snapshot":
15672
+ return `${e.files.length} files at prompt #${e.promptIndex}`;
15673
+ case "file_observation":
15674
+ return `${e.source === "user" ? "user-saved" : "tool-written"} at ${e.path}`;
15675
+ case "rewound":
15676
+ return `${e.revertedFiles.length} files reverted`;
15677
+ case "in_flight_start":
15678
+ return e.context;
15679
+ case "in_flight_end":
15680
+ return `${e.reason}`;
15681
+ case "side_effect":
15682
+ return `${e.toolName} (${e.risk})${e.outcome ? `: ${e.outcome}` : ""}`;
15683
+ case "provider_retry":
15684
+ return `delay ${e.delayMs}ms${e.status ? ` \xB7 HTTP ${e.status}` : ""}`;
15685
+ case "provider_error":
15686
+ return `${e.retryable ? "retryable" : "fatal"}${e.status ? ` \xB7 HTTP ${e.status}` : ""}`;
15687
+ case "user_input":
15688
+ return typeof e.content === "string" ? `${e.content.length} chars` : `${e.content.length} blocks`;
15689
+ case "tool_result":
15690
+ return `${e.isError ? "error" : "ok"}`;
15691
+ case "checkpoint":
15692
+ return `prompt #${e.promptIndex}`;
15693
+ case "spec_parsed":
15694
+ return `${e.title} (${e.completeness}% complete)`;
15695
+ case "spec_analyzed":
15696
+ return `${e.gaps.length} gaps identified`;
15697
+ case "skill_activated":
15698
+ return `at ${e.skillName}`;
15699
+ case "skill_deactivated":
15700
+ return `at ${e.skillName}`;
15701
+ default: {
15702
+ const _exhaustive = e;
15703
+ void _exhaustive;
15704
+ return "";
15705
+ }
15706
+ }
15707
+ }
15708
+ function buildInspectPayload(summary, events, fallback) {
15709
+ const inspectEvents = events.map((e) => ({
15710
+ ts: e.ts,
15711
+ type: e.type,
15712
+ label: labelForEvent(e),
15713
+ detail: detailForEvent(e)
15714
+ }));
15715
+ const fileEvents = [];
15716
+ let computedToolCallCount = 0;
15717
+ let computedToolErrorCount = 0;
15718
+ let computedFileChangeCount = 0;
15719
+ let computedCompactionCount = 0;
15720
+ let computedMessageCount = 0;
15721
+ let computedIterationCount = 0;
15722
+ const computedToolBreakdown = {};
15723
+ for (const e of events) {
15724
+ if (e.type === "file_event") {
15725
+ fileEvents.push({
15726
+ operation: e.operation,
15727
+ filePath: e.filePath,
15728
+ toolName: e.toolName,
15729
+ ts: e.ts
15730
+ });
15731
+ computedFileChangeCount++;
15732
+ } else if (e.type === "tool_call_end") {
15733
+ computedToolCallCount++;
15734
+ if (e.ok === false) computedToolErrorCount++;
15735
+ computedToolBreakdown[e.name] = (computedToolBreakdown[e.name] ?? 0) + 1;
15736
+ } else if (e.type === "compaction") {
15737
+ computedCompactionCount++;
15738
+ } else if (e.type === "user_input") {
15739
+ computedMessageCount++;
15740
+ } else if (e.type === "llm_response") {
15741
+ computedIterationCount++;
15742
+ }
15743
+ }
15744
+ const s = summary;
15745
+ return {
15746
+ id: s?.id ?? fallback.id,
15747
+ title: s?.title ?? fallback.title,
15748
+ ...s?.name !== void 0 ? { name: s.name } : {},
15749
+ model: s?.model ?? fallback.model,
15750
+ provider: s?.provider ?? fallback.provider,
15751
+ startedAt: s?.startedAt ?? fallback.startedAt,
15752
+ ...(s?.endedAt ?? fallback.endedAt) !== void 0 ? { endedAt: s?.endedAt ?? fallback.endedAt } : {},
15753
+ tokenTotal: s?.tokenTotal ?? 0,
15754
+ ...s?.outcome !== void 0 ? { outcome: s.outcome } : {},
15755
+ messageCount: s?.messageCount ?? computedMessageCount,
15756
+ iterationCount: s?.iterationCount ?? computedIterationCount,
15757
+ toolCallCount: s?.toolCallCount ?? computedToolCallCount,
15758
+ toolErrorCount: s?.toolErrorCount ?? computedToolErrorCount,
15759
+ fileChangeCount: s?.fileChangeCount ?? computedFileChangeCount,
15760
+ compactionCount: s?.compactionCount ?? computedCompactionCount,
15761
+ toolBreakdown: s?.toolBreakdown ?? computedToolBreakdown,
15762
+ events: inspectEvents,
15763
+ fileEvents,
15764
+ ...s?.lastUserMessage !== void 0 ? { lastUserMessage: s.lastUserMessage } : {}
15765
+ };
15766
+ }
14842
15767
  function toSessionHistoryEntry(summary, currentSessionId2) {
14843
15768
  return {
14844
15769
  id: summary.id,
@@ -15113,6 +16038,7 @@ function createSessionHandlers(ctx) {
15113
16038
  tools: ctx.listTools?.() ?? ctx.toolRegistry?.list(),
15114
16039
  baseRevision: typeof payload["baseRevision"] === "string" ? payload["baseRevision"] : "",
15115
16040
  messages: payload["messages"],
16041
+ removals: payload["removals"],
15116
16042
  allowRepair: payload["allowRepair"] === true,
15117
16043
  runActive: ctx.isRunActive?.() === true
15118
16044
  });
@@ -15129,6 +16055,7 @@ function createSessionHandlers(ctx) {
15129
16055
  tools: ctx.listTools?.() ?? ctx.toolRegistry?.list(),
15130
16056
  baseRevision: typeof payload["baseRevision"] === "string" ? payload["baseRevision"] : "",
15131
16057
  messages: payload["messages"],
16058
+ removals: payload["removals"],
15132
16059
  allowRepair: payload["allowRepair"] === true,
15133
16060
  runActive: ctx.isRunActive?.() === true
15134
16061
  });
@@ -15385,6 +16312,47 @@ function createSessionHandlers(ctx) {
15385
16312
  if (!ensureCurrentSession(ws, msg, "session.save")) return;
15386
16313
  result(ws, true, `Session ${ctx.getSession().id} is auto-saved`);
15387
16314
  },
16315
+ inspectSession: async (ws, msg) => {
16316
+ const { id } = msg.payload;
16317
+ if (!id) {
16318
+ sendTo(ws, {
16319
+ type: "session.inspect",
16320
+ payload: { id: "", error: "Session id is required" }
16321
+ });
16322
+ return;
16323
+ }
16324
+ try {
16325
+ const store = ctx.getSessionStore();
16326
+ const data = await store.load(id);
16327
+ let summary;
16328
+ try {
16329
+ const summaries = await store.list(200);
16330
+ summary = summaries.find((s) => s.id === id);
16331
+ } catch {
16332
+ summary = void 0;
16333
+ }
16334
+ const payload = buildInspectPayload(summary, data.events, {
16335
+ id: data.metadata.id,
16336
+ title: data.metadata.title ?? "",
16337
+ model: data.metadata.model ?? "",
16338
+ provider: data.metadata.provider ?? "",
16339
+ startedAt: data.metadata.startedAt,
16340
+ endedAt: data.metadata.endedAt
16341
+ });
16342
+ sendTo(ws, {
16343
+ type: "session.inspect",
16344
+ payload
16345
+ });
16346
+ } catch (err) {
16347
+ sendTo(ws, {
16348
+ type: "session.inspect",
16349
+ payload: {
16350
+ id,
16351
+ error: err instanceof Error ? err.message : String(err)
16352
+ }
16353
+ });
16354
+ }
16355
+ },
15388
16356
  listCheckpoints: async (ws, msg) => {
15389
16357
  if (!ensureCurrentSession(ws, msg, "session.checkpoints")) return;
15390
16358
  try {
@@ -15907,6 +16875,19 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
15907
16875
  return true;
15908
16876
  }
15909
16877
 
16878
+ // src/server/fallback-choice.ts
16879
+ function emitFallbackChoice(events, msg) {
16880
+ const parsed = validateModelFallbackChoicePayload(msg.payload);
16881
+ if (!parsed.ok) return parsed;
16882
+ events?.emit("provider.fallback_choice", {
16883
+ requestId: parsed.value.requestId,
16884
+ ...parsed.value.providerId ? { providerId: parsed.value.providerId } : {},
16885
+ ...parsed.value.model ? { model: parsed.value.model } : {},
16886
+ ...parsed.value.autoSwitch ? { autoSwitch: true } : {}
16887
+ });
16888
+ return { ok: true };
16889
+ }
16890
+
15910
16891
  // src/server/agent-roster-routes.ts
15911
16892
  async function handleAgentRosterRoute(ws, msg, handlers) {
15912
16893
  if (!msg.type.startsWith("agent-roster.")) return false;
@@ -16071,13 +17052,17 @@ async function handleProviderRoute(ws, msg, routes) {
16071
17052
  case "model.refine":
16072
17053
  await routes.refineModel(ws, msg);
16073
17054
  return true;
17055
+ case "model.fallback_choice":
17056
+ await routes.fallbackChoice(ws, msg);
17057
+ return true;
16074
17058
  case "key.add":
16075
17059
  case "key.update": {
16076
17060
  const payload = asPayloadRecord(msg);
16077
17061
  const providerId = payload ? requiredString(payload, "providerId") : null;
16078
17062
  const label = payload ? requiredString(payload, "label") : null;
16079
17063
  const apiKey = payload ? requiredString(payload, "apiKey") : null;
16080
- if (!providerId || !label || !apiKey) return invalidPayload(ws, msg.type);
17064
+ if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label || !apiKey)
17065
+ return invalidPayload(ws, msg.type);
16081
17066
  await routes.providerHandlers.handleKeyUpsert(ws, providerId, label, apiKey);
16082
17067
  return true;
16083
17068
  }
@@ -16085,7 +17070,8 @@ async function handleProviderRoute(ws, msg, routes) {
16085
17070
  const payload = asPayloadRecord(msg);
16086
17071
  const providerId = payload ? requiredString(payload, "providerId") : null;
16087
17072
  const label = payload ? requiredString(payload, "label") : null;
16088
- if (!providerId || !label) return invalidPayload(ws, msg.type);
17073
+ if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label)
17074
+ return invalidPayload(ws, msg.type);
16089
17075
  await routes.providerHandlers.handleKeyDelete(ws, providerId, label);
16090
17076
  return true;
16091
17077
  }
@@ -16093,7 +17079,8 @@ async function handleProviderRoute(ws, msg, routes) {
16093
17079
  const payload = asPayloadRecord(msg);
16094
17080
  const providerId = payload ? requiredString(payload, "providerId") : null;
16095
17081
  const label = payload ? requiredString(payload, "label") : null;
16096
- if (!providerId || !label) return invalidPayload(ws, msg.type);
17082
+ if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !label)
17083
+ return invalidPayload(ws, msg.type);
16097
17084
  await routes.providerHandlers.handleKeySetActive(ws, providerId, label);
16098
17085
  return true;
16099
17086
  }
@@ -16105,11 +17092,11 @@ async function handleProviderRoute(ws, msg, routes) {
16105
17092
  const apiKey = payload?.["apiKey"];
16106
17093
  const models = payload ? optionalStringArray(payload, "models") : null;
16107
17094
  const customModels = payload ? optionalCustomModels(payload) : null;
16108
- if (!id || !family) return invalidPayload(ws, msg.type);
17095
+ if (!id || !SAFE_CONFIG_KEY.test(id) || !family) return invalidPayload(ws, msg.type);
16109
17096
  if (baseUrl !== void 0 && typeof baseUrl !== "string") return invalidPayload(ws, msg.type);
16110
17097
  if (apiKey !== void 0 && typeof apiKey !== "string") return invalidPayload(ws, msg.type);
16111
17098
  if (models === null || customModels === null) return invalidPayload(ws, msg.type);
16112
- await routes.providerHandlers.handleProviderAdd(ws, {
17099
+ const added = await routes.providerHandlers.handleProviderAdd(ws, {
16113
17100
  id,
16114
17101
  family,
16115
17102
  baseUrl,
@@ -16117,20 +17104,22 @@ async function handleProviderRoute(ws, msg, routes) {
16117
17104
  models,
16118
17105
  customModels
16119
17106
  });
16120
- await routes.adoptDefaultProviderIfUnset(id);
17107
+ if (added) {
17108
+ void routes.adoptDefaultProviderIfUnset(id).catch(() => void 0);
17109
+ }
16121
17110
  return true;
16122
17111
  }
16123
17112
  case "provider.remove": {
16124
17113
  const payload = asPayloadRecord(msg);
16125
17114
  const providerId = payload ? requiredString(payload, "providerId") : null;
16126
- if (!providerId) return invalidPayload(ws, msg.type);
17115
+ if (!providerId || !SAFE_CONFIG_KEY.test(providerId)) return invalidPayload(ws, msg.type);
16127
17116
  await routes.providerHandlers.handleProviderRemove(ws, providerId);
16128
17117
  return true;
16129
17118
  }
16130
17119
  case "provider.clear_models": {
16131
17120
  const payload = asPayloadRecord(msg);
16132
17121
  const providerId = payload ? requiredString(payload, "providerId") : null;
16133
- if (!providerId) return invalidPayload(ws, msg.type);
17122
+ if (!providerId || !SAFE_CONFIG_KEY.test(providerId)) return invalidPayload(ws, msg.type);
16134
17123
  await routes.providerHandlers.handleProviderClearModels(ws, providerId);
16135
17124
  return true;
16136
17125
  }
@@ -16144,7 +17133,7 @@ async function handleProviderRoute(ws, msg, routes) {
16144
17133
  const customModelRaw = payload["customModel"];
16145
17134
  if (!isRecord4(customModelRaw)) return invalidPayload(ws, msg.type);
16146
17135
  const cm = optionalCustomModels({ customModels: { [modelId]: customModelRaw } });
16147
- if (!cm || !cm[modelId]) return invalidPayload(ws, msg.type);
17136
+ if (!cm?.[modelId]) return invalidPayload(ws, msg.type);
16148
17137
  await routes.providerHandlers.handleCustomModelSet(ws, providerId, modelId, cm[modelId]);
16149
17138
  return true;
16150
17139
  }
@@ -16162,7 +17151,8 @@ async function handleProviderRoute(ws, msg, routes) {
16162
17151
  const payload = asPayloadRecord(msg);
16163
17152
  const providerId = payload ? requiredString(payload, "providerId") : null;
16164
17153
  const previousModels = payload ? optionalStringArray(payload, "previousModels") : null;
16165
- if (!providerId || !previousModels) return invalidPayload(ws, msg.type);
17154
+ if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !previousModels)
17155
+ return invalidPayload(ws, msg.type);
16166
17156
  await routes.providerHandlers.handleProviderUndoClear(ws, providerId, previousModels);
16167
17157
  return true;
16168
17158
  }
@@ -16172,7 +17162,7 @@ async function handleProviderRoute(ws, msg, routes) {
16172
17162
  const envVars = payload ? optionalStringArray(payload, "envVars") : null;
16173
17163
  const models = payload ? optionalStringArray(payload, "models") : null;
16174
17164
  const customModels = payload ? optionalCustomModels(payload) : null;
16175
- if (!payload || !id || envVars === null || models === null || customModels === null)
17165
+ if (!payload || !id || !SAFE_CONFIG_KEY.test(id) || envVars === null || models === null || customModels === null)
16176
17166
  return invalidPayload(ws, msg.type);
16177
17167
  for (const key of ["family", "baseUrl"]) {
16178
17168
  if (payload[key] !== void 0 && typeof payload[key] !== "string")
@@ -16192,7 +17182,8 @@ async function handleProviderRoute(ws, msg, routes) {
16192
17182
  const payload = asPayloadRecord(msg);
16193
17183
  const providerId = payload ? requiredString(payload, "providerId") : null;
16194
17184
  const timeoutMs = payload ? optionalNumber(payload, "timeoutMs") : null;
16195
- if (!providerId || timeoutMs === null) return invalidPayload(ws, msg.type);
17185
+ if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || timeoutMs === null)
17186
+ return invalidPayload(ws, msg.type);
16196
17187
  await routes.providerHandlers.handleProviderProbe(ws, providerId, timeoutMs);
16197
17188
  return true;
16198
17189
  }
@@ -16201,7 +17192,7 @@ async function handleProviderRoute(ws, msg, routes) {
16201
17192
  const kind = oauthKind(payload);
16202
17193
  const providerId = payload?.["providerId"];
16203
17194
  if (!kind) return invalidPayload(ws, msg.type);
16204
- if (providerId !== void 0 && typeof providerId !== "string") {
17195
+ if (providerId !== void 0 && (typeof providerId !== "string" || !SAFE_CONFIG_KEY.test(providerId))) {
16205
17196
  return invalidPayload(ws, msg.type);
16206
17197
  }
16207
17198
  await routes.providerHandlers.handleOAuthStart(ws, kind, providerId);
@@ -16240,7 +17231,8 @@ async function handleProviderRoute(ws, msg, routes) {
16240
17231
  const payload = asPayloadRecord(msg);
16241
17232
  const providerId = payload ? requiredString(payload, "providerId") : null;
16242
17233
  const model = payload ? requiredString(payload, "model") : null;
16243
- if (!providerId || !model) return invalidPayload(ws, msg.type);
17234
+ if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !model)
17235
+ return invalidPayload(ws, msg.type);
16244
17236
  const released = routes.statusTracker.retryNow(providerId, model);
16245
17237
  sendResult2(
16246
17238
  ws,
@@ -16257,7 +17249,8 @@ async function handleProviderRoute(ws, msg, routes) {
16257
17249
  const payload = asPayloadRecord(msg);
16258
17250
  const providerId = payload ? requiredString(payload, "providerId") : null;
16259
17251
  const model = payload ? requiredString(payload, "model") : null;
16260
- if (!providerId || !model) return invalidPayload(ws, msg.type);
17252
+ if (!providerId || !SAFE_CONFIG_KEY.test(providerId) || !model)
17253
+ return invalidPayload(ws, msg.type);
16261
17254
  routes.statusTracker.clear(providerId, model);
16262
17255
  sendResult2(ws, true, `Cleared tracking for ${providerId}/${model}.`);
16263
17256
  return true;
@@ -16338,6 +17331,9 @@ async function handleSessionRoute(ws, msg, handlers) {
16338
17331
  case "session.save":
16339
17332
  await handlers.saveSession(ws, msg);
16340
17333
  return true;
17334
+ case "session.inspect":
17335
+ await handlers.inspectSession(ws, msg);
17336
+ return true;
16341
17337
  case "session.checkpoints":
16342
17338
  await handlers.listCheckpoints(ws, msg);
16343
17339
  return true;
@@ -18210,7 +19206,8 @@ function setupEvents(deps2) {
18210
19206
  attempt: e.attempt,
18211
19207
  delayMs: e.delayMs,
18212
19208
  status: e.status,
18213
- description: e.description
19209
+ description: e.description,
19210
+ ...e.errorBody ? { errorBody: e.errorBody } : {}
18214
19211
  })
18215
19212
  });
18216
19213
  appendForCurrentSession(e.sessionId, {
@@ -18220,7 +19217,8 @@ function setupEvents(deps2) {
18220
19217
  attempt: e.attempt,
18221
19218
  delayMs: e.delayMs,
18222
19219
  status: e.status,
18223
- description: e.description
19220
+ description: e.description,
19221
+ ...e.errorBody ? { errorBody: e.errorBody } : {}
18224
19222
  });
18225
19223
  });
18226
19224
  on("provider.status_changed", (e) => {
@@ -18260,7 +19258,8 @@ function setupEvents(deps2) {
18260
19258
  providerId: e.providerId,
18261
19259
  status: e.status,
18262
19260
  description: e.description,
18263
- retryable: e.retryable
19261
+ retryable: e.retryable,
19262
+ ...e.errorBody ? { errorBody: e.errorBody } : {}
18264
19263
  })
18265
19264
  });
18266
19265
  appendForCurrentSession(e.sessionId, {
@@ -18269,7 +19268,8 @@ function setupEvents(deps2) {
18269
19268
  providerId: e.providerId,
18270
19269
  status: e.status,
18271
19270
  description: e.description,
18272
- retryable: e.retryable
19271
+ retryable: e.retryable,
19272
+ ...e.errorBody ? { errorBody: e.errorBody } : {}
18273
19273
  });
18274
19274
  });
18275
19275
  on("provider.fallback", (e) => {
@@ -18280,7 +19280,22 @@ function setupEvents(deps2) {
18280
19280
  from: e.from,
18281
19281
  to: e.to,
18282
19282
  status: e.status,
18283
- providerSwitched: e.providerSwitched
19283
+ providerSwitched: e.providerSwitched,
19284
+ ...e.requestId ? { requestId: e.requestId } : {}
19285
+ })
19286
+ });
19287
+ });
19288
+ on("provider.fallback_pending", (e) => {
19289
+ broadcast2(clients, {
19290
+ type: "provider.fallback_pending",
19291
+ payload: sessionPayload2({
19292
+ sessionId: e.sessionId,
19293
+ from: e.from,
19294
+ status: e.status,
19295
+ candidates: e.candidates,
19296
+ autoSwitchSeconds: e.autoSwitchSeconds,
19297
+ requestId: e.requestId,
19298
+ timestamp: e.timestamp
18284
19299
  })
18285
19300
  });
18286
19301
  });
@@ -18365,6 +19380,15 @@ function setupEvents(deps2) {
18365
19380
  type: "mailbox.agent_registered",
18366
19381
  payload
18367
19382
  });
19383
+ }),
19384
+ // Deregistration (subagent retirement) must reach the browser too —
19385
+ // otherwise dead agents linger in the client roster until an unrelated
19386
+ // refresh. Emitted by sqlite-mailbox.deregisterAgent with { agentId }.
19387
+ events.onPattern("mailbox.agent_deregistered", (_e, payload) => {
19388
+ broadcast2(clients, {
19389
+ type: "mailbox.agent_deregistered",
19390
+ payload
19391
+ });
18368
19392
  })
18369
19393
  );
18370
19394
  const forwardSubagent = (kind, payload) => broadcast2(clients, { type: "subagent.event", payload: sessionPayload2({ kind, ...payload }) });
@@ -20649,29 +21673,10 @@ import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/too
20649
21673
  // src/server/model-auto-discovery.ts
20650
21674
  import * as fs19 from "node:fs/promises";
20651
21675
  import * as path24 from "node:path";
20652
- import { COMPATIBLE_PRESETS, discoverOpenAICompatibleModels } from "@wrongstack/providers";
21676
+ import { discoverOpenAICompatibleModels, resolveDiscoveryTargets } from "@wrongstack/providers";
20653
21677
  function isOverlayRegistry(value) {
20654
21678
  return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
20655
21679
  }
20656
- function resolveKey(cfg) {
20657
- if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
20658
- const active = cfg.activeKey ? cfg.apiKeys.find((key) => key.label === cfg.activeKey) : void 0;
20659
- return (active ?? cfg.apiKeys[0])?.apiKey;
20660
- }
20661
- return cfg.apiKey && cfg.apiKey.length > 0 ? cfg.apiKey : void 0;
20662
- }
20663
- function eligibleProviders(config) {
20664
- const out = [];
20665
- for (const [id, cfg] of Object.entries(config.providers ?? {})) {
20666
- const preset = COMPATIBLE_PRESETS[id];
20667
- const enabled = cfg.autoDiscoverModels ?? preset?.autoDiscover ?? false;
20668
- if (!enabled) continue;
20669
- const baseUrl = cfg.baseUrl ?? preset?.defaultBaseUrl;
20670
- if (!baseUrl) continue;
20671
- out.push({ id, cfg, baseUrl, apiKey: resolveKey(cfg) });
20672
- }
20673
- return out;
20674
- }
20675
21680
  async function readCache(file) {
20676
21681
  try {
20677
21682
  return JSON.parse(await fs19.readFile(file, "utf8"));
@@ -20682,14 +21687,13 @@ async function readCache(file) {
20682
21687
  async function discoverAndMergeWebuiProviders(opts) {
20683
21688
  const registry = opts.registry;
20684
21689
  if (!isOverlayRegistry(registry)) return;
20685
- const targets = eligibleProviders(opts.config);
21690
+ const targets = resolveDiscoveryTargets(opts.config);
20686
21691
  if (targets.length === 0) return;
20687
21692
  const cacheFile = path24.join(opts.cacheDir, "discovered-models-cache.json");
20688
21693
  const cache2 = await readCache(cacheFile);
20689
21694
  let cacheDirty = false;
20690
21695
  await Promise.all(
20691
- targets.map(async ({ id, cfg, baseUrl, apiKey }) => {
20692
- const cacheKey = `${id}\0${baseUrl}`;
21696
+ targets.map(async ({ id, cfg, baseUrl, apiKey, cacheKey }) => {
20693
21697
  const provider = await discoverOpenAICompatibleModels(id, {
20694
21698
  baseUrl,
20695
21699
  apiKey,
@@ -21055,15 +22059,6 @@ async function createPreContextServices(input) {
21055
22059
  logger.warn(`models.dev refresh failed (${toErrorMessage11(err)}); using cached catalog`);
21056
22060
  }
21057
22061
  }
21058
- try {
21059
- await installCatalogModelOutputLimits({
21060
- registry: modelsRegistry,
21061
- getConfig: () => config,
21062
- log: (message) => logger.debug(message)
21063
- });
21064
- } catch (err) {
21065
- logger.debug(`model output-limit index skipped: ${toErrorMessage11(err)}`);
21066
- }
21067
22062
  try {
21068
22063
  await discoverAndMergeWebuiProviders({
21069
22064
  config,
@@ -21074,6 +22069,15 @@ async function createPreContextServices(input) {
21074
22069
  } catch (err) {
21075
22070
  logger.debug(`provider auto-discovery skipped: ${toErrorMessage11(err)}`);
21076
22071
  }
22072
+ try {
22073
+ await installCatalogModelOutputLimits({
22074
+ registry: modelsRegistry,
22075
+ getConfig: () => config,
22076
+ log: (message) => logger.debug(message)
22077
+ });
22078
+ } catch (err) {
22079
+ logger.debug(`model output-limit index skipped: ${toErrorMessage11(err)}`);
22080
+ }
21077
22081
  const events = opts.services?.events ?? new EventBus();
21078
22082
  events.setLogger(logger);
21079
22083
  const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry, events });
@@ -21346,7 +22350,7 @@ import { makeProviderFromConfig as makeProviderFromConfig2, withCatalogCapabilit
21346
22350
 
21347
22351
  // src/server/mode-handlers.ts
21348
22352
  import { DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2 } from "@wrongstack/core/agent";
21349
- import { resolveWstackPaths as resolveWstackPaths5 } from "@wrongstack/core/utils";
22353
+ import { resolveWstackPaths as resolveWstackPaths6 } from "@wrongstack/core/utils";
21350
22354
  function createModeHandlers(context) {
21351
22355
  return createModeRouteHandlers({
21352
22356
  modeStore: context.modeStore,
@@ -21355,7 +22359,7 @@ function createModeHandlers(context) {
21355
22359
  send,
21356
22360
  afterSwitch: async (id) => {
21357
22361
  const modePrompt = id === "default" ? "" : (await context.modeStore.getMode(id))?.prompt ?? "";
21358
- const paths = resolveWstackPaths5({
22362
+ const paths = resolveWstackPaths6({
21359
22363
  projectRoot: context.projectRoot,
21360
22364
  globalRoot: context.globalRoot
21361
22365
  });
@@ -21460,7 +22464,16 @@ function buildRoutes(state, deps2, cb) {
21460
22464
  refineModel: (ws, msg) => modelOperations.refineModel(
21461
22465
  ws,
21462
22466
  msg.payload
21463
- )
22467
+ ),
22468
+ fallbackChoice: async (ws, msg) => {
22469
+ const result = emitFallbackChoice(deps2.events, msg);
22470
+ if (!result.ok) {
22471
+ send(ws, {
22472
+ type: "error",
22473
+ payload: { phase: "invalid_request", message: result.message }
22474
+ });
22475
+ }
22476
+ }
21464
22477
  };
21465
22478
  const sessionRoutes = createSessionHandlers({
21466
22479
  config: state.getConfig(),
@@ -21638,8 +22651,10 @@ function buildRoutes(state, deps2, cb) {
21638
22651
  });
21639
22652
  const mcpRoutes = {
21640
22653
  list: (ws, msg) => handleMcpList(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
21641
- add: (ws, msg) => handleMcpAdd(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
21642
- update: (ws, msg) => handleMcpUpdate(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
22654
+ // add/update are the spawn-capable pair they take a `command`/`args`
22655
+ // from the wire and start it. They go past the trust boundary (M1).
22656
+ add: (ws, msg) => handleMcpAdd(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry, deps2.trustBoundary),
22657
+ update: (ws, msg) => handleMcpUpdate(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry, deps2.trustBoundary),
21643
22658
  remove: (ws, msg) => handleMcpRemove(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
21644
22659
  enable: (ws, msg) => handleMcpEnable(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
21645
22660
  disable: (ws, msg) => handleMcpDisable(ws, msg, deps2.profileConfigPath, deps2.mcpRegistry),
@@ -21715,7 +22730,10 @@ async function resolvePorts(opts) {
21715
22730
  const surface = opts.surface ?? "webui";
21716
22731
  const surfaceDefaults = surface === "simpleui" ? { http: 3466 } : { http: 3456 };
21717
22732
  const wsHost = opts.wsHost ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
21718
- const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(process.env["WEBUI_PORT"] ?? process.env["PORT"] ?? String(surfaceDefaults.http), 10);
22733
+ const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(
22734
+ process.env["WEBUI_PORT"] ?? process.env["PORT"] ?? String(surfaceDefaults.http),
22735
+ 10
22736
+ );
21719
22737
  const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
21720
22738
  const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
21721
22739
  const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
@@ -21724,7 +22742,16 @@ async function resolvePorts(opts) {
21724
22742
  if (!strictPort) {
21725
22743
  httpPort = await findFreePort(wsHost, requestedHttpPort);
21726
22744
  if (httpPort !== requestedHttpPort) {
21727
- console.warn(JSON.stringify({ level: "warn", event: "webui.port_reassigned", protocol: "HTTP", requested: requestedHttpPort, assigned: httpPort, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
22745
+ console.warn(
22746
+ JSON.stringify({
22747
+ level: "warn",
22748
+ event: "webui.port_reassigned",
22749
+ protocol: "HTTP",
22750
+ requested: requestedHttpPort,
22751
+ assigned: httpPort,
22752
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
22753
+ })
22754
+ );
21728
22755
  }
21729
22756
  }
21730
22757
  return { wsHost, httpPort, publicUrl, publicWsUrl, requireToken };
@@ -21799,7 +22826,10 @@ function createWsServers(httpServer, ports, accessToken) {
21799
22826
  expectedToken: wsToken,
21800
22827
  requireToken: ports.requireToken,
21801
22828
  allowedHostnames: publicHostnames,
21802
- allowBrowserUrlToken: Boolean(ports.publicWsUrl)
22829
+ allowBrowserUrlToken: Boolean(ports.publicWsUrl),
22830
+ // WS-003 opt-out for the Vite dev loop only (app and WS server cannot
22831
+ // share a port). Off unless explicitly requested — see ws-auth.ts.
22832
+ allowCrossPortLoopbackCookie: process.env["WRONGSTACK_WEBUI_DEV_CROSS_PORT_WS"] === "1"
21803
22833
  });
21804
22834
  const WS_MAX_PAYLOAD = 20 * 1024 * 1024;
21805
22835
  const wssPrimary = new WebSocketServer({
@@ -21830,21 +22860,49 @@ function armEvents(wssPrimary, wssSecondary, wsHost, httpPort, setupInput, watch
21830
22860
  if (eventsArmed) return;
21831
22861
  eventsArmed = true;
21832
22862
  console.log(`[WebUI] Backend ready (${label})`);
21833
- disposeEvents = setupEvents({ ...setupInput, watcherMetrics, onFleetBroadcaster: (fn) => {
21834
- fleetBroadcast = fn;
21835
- } });
22863
+ disposeEvents = setupEvents({
22864
+ ...setupInput,
22865
+ watcherMetrics,
22866
+ onFleetBroadcaster: (fn) => {
22867
+ fleetBroadcast = fn;
22868
+ }
22869
+ });
21836
22870
  };
21837
22871
  wssPrimary.on("listening", () => arm(`${wsHost}:${httpPort}`));
21838
22872
  wssPrimary.on("error", (err) => {
21839
- console.error(JSON.stringify({ level: "error", event: "webui.ws_server_error", host: wsHost, message: toErrorMessage12(err), timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
22873
+ console.error(
22874
+ JSON.stringify({
22875
+ level: "error",
22876
+ event: "webui.ws_server_error",
22877
+ host: wsHost,
22878
+ message: toErrorMessage12(err),
22879
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
22880
+ })
22881
+ );
21840
22882
  });
21841
22883
  if (wssSecondary) {
21842
22884
  wssSecondary.on("listening", () => arm(`::1:${httpPort}`));
21843
22885
  wssSecondary.on("error", (err) => {
21844
22886
  if (err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL") {
21845
- console.warn(JSON.stringify({ level: "warn", event: "webui.ipv6_unavailable", code: err.code, message: err.message, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
22887
+ console.warn(
22888
+ JSON.stringify({
22889
+ level: "warn",
22890
+ event: "webui.ipv6_unavailable",
22891
+ code: err.code,
22892
+ message: err.message,
22893
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
22894
+ })
22895
+ );
21846
22896
  } else {
21847
- console.error(JSON.stringify({ level: "error", event: "webui.ws_server_error", host: "::1", message: err.message, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
22897
+ console.error(
22898
+ JSON.stringify({
22899
+ level: "error",
22900
+ event: "webui.ws_server_error",
22901
+ host: "::1",
22902
+ message: err.message,
22903
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
22904
+ })
22905
+ );
21848
22906
  }
21849
22907
  });
21850
22908
  }
@@ -21865,6 +22923,7 @@ function resolveWebuiDistDir(fromUrl, explicitDistDir) {
21865
22923
  }
21866
22924
  }
21867
22925
  function startHttpServer(opts) {
22926
+ const intakeService = opts.intakeService ?? createProjectIntakeService({ projectRoot: opts.projectRoot, globalRoot: opts.globalRoot });
21868
22927
  const httpServer = createHttpServer({
21869
22928
  host: opts.wsHost,
21870
22929
  port: opts.httpPort,
@@ -21878,7 +22937,8 @@ function startHttpServer(opts) {
21878
22937
  onTechStackEvent: opts.onTechStackEvent,
21879
22938
  getLlm: opts.getLlm,
21880
22939
  executePackageOperation: opts.executePackageOperation,
21881
- projectRoot: opts.projectRoot
22940
+ projectRoot: opts.projectRoot,
22941
+ intakeService
21882
22942
  });
21883
22943
  return httpServer;
21884
22944
  }