@wrongstack/webui-server 0.310.0 → 0.313.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -97,7 +97,12 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
97
97
  // `prefs.update` exactly so the panel does not error with
98
98
  // "unknown preference key".
99
99
  "readSymbols",
100
- "showSageMemoryInject"
100
+ "showSageMemoryInject",
101
+ // WrongProxy / WrongTrace master switch. When on, the CLI rewrites every
102
+ // provider's base URL through the configured proxy URL (default
103
+ // http://localhost:3444 → http://localhost:3444/proxy/<host><path>).
104
+ // Excluded providers (openai-codex) flow through unchanged.
105
+ "wrongProxyEnabled"
101
106
  ]);
102
107
  var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set([
103
108
  "fallbackModels",
@@ -164,7 +169,11 @@ var STRING_PREF_KEYS = /* @__PURE__ */ new Set([
164
169
  "chimeraModel",
165
170
  "autoReviewProvider",
166
171
  "autoReviewModel",
167
- "autoReviewFallbackProfile"
172
+ "autoReviewFallbackProfile",
173
+ // WrongProxy / WrongTrace URL. Empty = unset. Default lives in
174
+ // `LocalPrefs.DEFAULTS.wrongProxyUrl` ('http://localhost:3444'); users
175
+ // can override here for non-default daemon ports / paths.
176
+ "wrongProxyUrl"
168
177
  ]);
169
178
  var ENUM_PREF_KEYS = {
170
179
  autonomy: AUTONOMY_VALUES,
@@ -178,7 +187,7 @@ var ENUM_PREF_KEYS = {
178
187
  reasoningEffort: REASONING_EFFORT_VALUES,
179
188
  cacheTtl: CACHE_TTL_VALUES,
180
189
  statuslineMode: /* @__PURE__ */ new Set(["minimum", "detailed", "no-color"]),
181
- animationStyle: /* @__PURE__ */ new Set(["rainbow", "wave", "pulse", "dots", "breathe", "cycle"]),
190
+ animationStyle: /* @__PURE__ */ new Set(["rainbow", "wave", "pulse", "dots", "breathe", "static", "cycle"]),
182
191
  fsAccess: /* @__PURE__ */ new Set(["unrestricted", "project"]),
183
192
  // Chimera autoFix + auto-review cascade threshold
184
193
  chimeraAutoFix: /* @__PURE__ */ new Set(["off", "ask", "auto"]),
@@ -1054,7 +1063,7 @@ function routingPatch(payload) {
1054
1063
  function handlePrefsGet(ctx, ws) {
1055
1064
  ctx.send(ws, { type: "prefs.updated", payload: ctx.snapshot() });
1056
1065
  }
1057
- function handlePrefsUpdate(ctx, ws, input) {
1066
+ async function handlePrefsUpdate(ctx, ws, input) {
1058
1067
  const parsed = validatePrefsUpdatePayload(input);
1059
1068
  if (!parsed.ok) {
1060
1069
  sendResult(ctx, ws, false, parsed.message);
@@ -1087,6 +1096,9 @@ function handlePrefsUpdate(ctx, ws, input) {
1087
1096
  ({ setDebugStreamEnabled }) => setDebugStreamEnabled(payload["debugStream"])
1088
1097
  );
1089
1098
  }
1099
+ if (typeof payload["wrongProxyEnabled"] === "boolean" || typeof payload["wrongProxyUrl"] === "string") {
1100
+ await ctx.applyWrongProxyPrefs?.(payload);
1101
+ }
1090
1102
  if (typeof payload["logLevel"] === "string" && ["debug", "info", "warn", "error"].includes(payload["logLevel"])) {
1091
1103
  ctx.setLogLevel?.(payload["logLevel"]);
1092
1104
  }
@@ -6364,7 +6376,8 @@ function estimateTokens(s) {
6364
6376
  function stringifyContent(c) {
6365
6377
  if (typeof c === "string") return c;
6366
6378
  try {
6367
- return JSON.stringify(c);
6379
+ const serialized = JSON.stringify(c);
6380
+ return serialized === void 0 ? String(c) : serialized;
6368
6381
  } catch {
6369
6382
  return String(c);
6370
6383
  }
@@ -7290,6 +7303,8 @@ function seedContextMeta(config, context) {
7290
7303
  meta["tgDelegate"] = tgExt?.["notifyOnDelegate"] !== false;
7291
7304
  const tgMs = tgExt?.["longToolThresholdMs"];
7292
7305
  meta["tgLongToolMs"] = typeof tgMs === "number" ? tgMs : 3e4;
7306
+ meta["wrongProxyEnabled"] = config.tools?.wrongProxy?.enabled === true;
7307
+ meta["wrongProxyUrl"] = config.tools?.wrongProxy?.url ?? "";
7293
7308
  {
7294
7309
  const pluginsEnabled = {};
7295
7310
  const record = (name2) => {
@@ -7662,6 +7677,87 @@ import * as path13 from "node:path";
7662
7677
  import { TOKENS } from "@wrongstack/core/kernel";
7663
7678
  import { DefaultSessionStore as DefaultSessionStore2 } from "@wrongstack/core/storage";
7664
7679
  import { toErrorMessage as toErrorMessage7, wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
7680
+
7681
+ // src/server/proxy-runtime.ts
7682
+ import {
7683
+ applyProxyConfig,
7684
+ getProxyConfig,
7685
+ rewriteBaseUrl,
7686
+ shouldRewriteFor
7687
+ } from "@wrongstack/core/wiring/proxy-rewrite";
7688
+ var HEALTH_PATH = "/api/health";
7689
+ var PROBE_TIMEOUT_MS = 2e3;
7690
+ function seedWrongProxyFromConfig(config) {
7691
+ const wp = config.tools?.wrongProxy;
7692
+ if (!wp) {
7693
+ return;
7694
+ }
7695
+ applyProxyConfig({
7696
+ enabled: wp.enabled === true,
7697
+ url: typeof wp.url === "string" ? wp.url : ""
7698
+ });
7699
+ }
7700
+ async function probeWrongProxyActive() {
7701
+ const cfg = getProxyConfig();
7702
+ if (!cfg.enabled || !cfg.url) {
7703
+ applyProxyConfig({ active: false });
7704
+ return false;
7705
+ }
7706
+ const abort = new AbortController();
7707
+ const timeout = setTimeout(() => abort.abort(), PROBE_TIMEOUT_MS);
7708
+ if (typeof timeout.unref === "function") {
7709
+ timeout.unref();
7710
+ }
7711
+ try {
7712
+ const healthUrl = `${cfg.url.replace(/\/+$/, "")}${HEALTH_PATH}`;
7713
+ const res = await fetch(healthUrl, {
7714
+ method: "GET",
7715
+ signal: abort.signal,
7716
+ headers: { accept: "application/json" }
7717
+ });
7718
+ const ok2 = res.ok && res.status >= 200 && res.status < 300;
7719
+ applyProxyConfig({ active: ok2 });
7720
+ return ok2;
7721
+ } catch {
7722
+ applyProxyConfig({ active: false });
7723
+ return false;
7724
+ } finally {
7725
+ clearTimeout(timeout);
7726
+ }
7727
+ }
7728
+ async function applyWrongProxyPrefs(payload) {
7729
+ const patch = {};
7730
+ if (typeof payload["wrongProxyEnabled"] === "boolean") {
7731
+ patch.enabled = payload["wrongProxyEnabled"];
7732
+ }
7733
+ if (typeof payload["wrongProxyUrl"] === "string") {
7734
+ patch.url = payload["wrongProxyUrl"];
7735
+ }
7736
+ if (Object.keys(patch).length === 0) return;
7737
+ applyProxyConfig(patch);
7738
+ await probeWrongProxyActive();
7739
+ }
7740
+ async function bootstrapWrongProxyFromConfig(config) {
7741
+ seedWrongProxyFromConfig(config);
7742
+ await probeWrongProxyActive();
7743
+ }
7744
+ function routeProviderCfgThroughProxy(cfg, fallbackBaseUrl, providerId) {
7745
+ const raw = cfg;
7746
+ const cfgType = typeof raw.type === "string" ? raw.type : void 0;
7747
+ const cfgBaseUrl = typeof raw.baseUrl === "string" ? raw.baseUrl : void 0;
7748
+ const factoryType = cfgType ?? providerId;
7749
+ const rawBaseUrl = cfgBaseUrl ?? fallbackBaseUrl;
7750
+ if (!rawBaseUrl || !shouldRewriteFor(factoryType)) {
7751
+ return { ...cfg };
7752
+ }
7753
+ const baseUrl = rewriteBaseUrl(rawBaseUrl, getProxyConfig().url);
7754
+ return {
7755
+ ...cfg,
7756
+ ...baseUrl !== cfgBaseUrl ? { baseUrl } : {}
7757
+ };
7758
+ }
7759
+
7760
+ // src/server/embedded-host-adapters.ts
7665
7761
  import { makeProviderFromConfig } from "@wrongstack/providers";
7666
7762
 
7667
7763
  // src/server/project-handlers.ts
@@ -9884,7 +9980,12 @@ async function applyEmbeddedModelSwitch(ctx, providerId, modelId) {
9884
9980
  await agentContext.runModelTransition(async () => {
9885
9981
  const saved = await ctx.loadSavedProviders();
9886
9982
  const providerConfig = saved[providerId] ?? { type: providerId };
9887
- const nextProvider = makeProviderFromConfig(providerId, providerConfig);
9983
+ const routedConfig = routeProviderCfgThroughProxy(
9984
+ providerConfig,
9985
+ ctx.getConfig?.()?.baseUrl,
9986
+ providerId
9987
+ );
9988
+ const nextProvider = makeProviderFromConfig(providerId, routedConfig);
9888
9989
  await ctx.modelsRegistry?.refresh().catch((error2) => {
9889
9990
  ctx.log(
9890
9991
  JSON.stringify({
@@ -16267,7 +16368,15 @@ function createEmbeddedMessageRouter(deps2) {
16267
16368
  getLiveProviderId: () => deps2.agentConfigCtx.agent.ctx.provider.id,
16268
16369
  buildProvider: async (providerId) => {
16269
16370
  const saved = await deps2.agentConfigCtx.loadSavedProviders();
16270
- return makeProviderFromConfig2(providerId, saved[providerId] ?? { type: providerId });
16371
+ const providerCfg = saved[providerId] ?? { type: providerId };
16372
+ return makeProviderFromConfig2(
16373
+ providerId,
16374
+ routeProviderCfgThroughProxy(
16375
+ providerCfg,
16376
+ deps2.agentConfigCtx.getConfig?.()?.baseUrl,
16377
+ providerId
16378
+ )
16379
+ );
16271
16380
  },
16272
16381
  applyModelSwitch: (providerId, modelId) => applyEmbeddedModelSwitch(deps2.agentConfigCtx, providerId, modelId),
16273
16382
  isRunActive: () => deps2.conversationCtx.abortControllers.size > 0,
@@ -21101,7 +21210,13 @@ var PREF_KEYS = [
21101
21210
  // Per-plugin enable/disable map (parity with the embedded server).
21102
21211
  "pluginsEnabled",
21103
21212
  // Fleet chat verbosity: off | full (migrated from streamFleet boolean).
21104
- "fleetChatVerbosity"
21213
+ "fleetChatVerbosity",
21214
+ // WrongProxy / WrongTrace: master switch + configurable URL (default
21215
+ // http://localhost:3444). When `wrongProxyEnabled` is true and the daemon
21216
+ // is reachable, every provider's base URL flows through
21217
+ // `${wrongProxyUrl}/proxy/<host><path>`. openai-codex is excluded by spec.
21218
+ "wrongProxyEnabled",
21219
+ "wrongProxyUrl"
21105
21220
  ];
21106
21221
  function prefSnapshot(contextMeta) {
21107
21222
  const snapshot = {};
@@ -21367,6 +21482,15 @@ async function persistPrefsToConfig(deps2, holder, payload) {
21367
21482
  }
21368
21483
  if (typeof payload["debugStream"] === "boolean")
21369
21484
  decrypted.debugStream = payload["debugStream"];
21485
+ if (typeof payload["wrongProxyEnabled"] === "boolean" || typeof payload["wrongProxyUrl"] === "string") {
21486
+ const toolsCfg = decrypted.tools ?? {};
21487
+ const wp = toolsCfg["wrongProxy"] ?? {};
21488
+ if (typeof payload["wrongProxyEnabled"] === "boolean")
21489
+ wp["enabled"] = payload["wrongProxyEnabled"];
21490
+ if (typeof payload["wrongProxyUrl"] === "string") wp["url"] = payload["wrongProxyUrl"];
21491
+ toolsCfg["wrongProxy"] = wp;
21492
+ decrypted.tools = toolsCfg;
21493
+ }
21370
21494
  if (typeof payload["pluginsEnabled"] === "object" && payload["pluginsEnabled"] !== null) {
21371
21495
  const ext = decrypted.extensions ?? {};
21372
21496
  const toggled = [];
@@ -24134,6 +24258,8 @@ import {
24134
24258
  // src/server/backend-services.ts
24135
24259
  import { join as join15 } from "node:path";
24136
24260
  import { Agent } from "@wrongstack/core/agent";
24261
+ import { HookRegistry, HookRunner } from "@wrongstack/core/hooks";
24262
+ import { createWrongTraceHookPair, recordGateDecision, snapshotGateDecisions, persistWrongTraceGateCounters } from "@wrongstack/wrongtrace";
24137
24263
  import {
24138
24264
  BrainDecisionLedger,
24139
24265
  BrainMonitor,
@@ -25175,6 +25301,33 @@ async function createAgentServices(input) {
25175
25301
  const secretScrubber = container.resolve(TOKENS2.SecretScrubber);
25176
25302
  const renderer = container.has(TOKENS2.Renderer) ? container.resolve(TOKENS2.Renderer) : void 0;
25177
25303
  const permissionPolicy = container.resolve(TOKENS2.PermissionPolicy);
25304
+ const wrongTraceHookRegistry = new HookRegistry();
25305
+ const wrongTraceHooks = createWrongTraceHookPair(() => context.session.id, {
25306
+ emit: (event) => {
25307
+ events.emit("wrongtrace.gate.decision", event);
25308
+ recordGateDecision(event);
25309
+ void persistWrongTraceGateCounters(projectRoot, snapshotGateDecisions());
25310
+ }
25311
+ });
25312
+ wrongTraceHookRegistry.registerInProcess(
25313
+ "PreToolUse",
25314
+ "edit|write|replace|patch|codebase-ast-replace",
25315
+ wrongTraceHooks.preToolUse,
25316
+ "wrongtrace-gate"
25317
+ );
25318
+ wrongTraceHookRegistry.registerInProcess(
25319
+ "PostToolUse",
25320
+ "edit|write|replace|patch|codebase-ast-replace",
25321
+ wrongTraceHooks.postToolUse,
25322
+ "wrongtrace-gate"
25323
+ );
25324
+ const wrongTraceHookRunner = new HookRunner({
25325
+ registry: wrongTraceHookRegistry,
25326
+ sessionId: () => context.session.id,
25327
+ // Coordination, not enforcement: these hooks are fail-open by
25328
+ // construction and must run regardless of shell-hook gating.
25329
+ allowNonPolicy: true
25330
+ });
25178
25331
  const toolExecutor = new ToolExecutor(toolRegistry, {
25179
25332
  permissionPolicy,
25180
25333
  secretScrubber,
@@ -25184,6 +25337,7 @@ async function createAgentServices(input) {
25184
25337
  iterationTimeoutMs: config.tools?.iterationTimeoutMs ?? DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
25185
25338
  perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
25186
25339
  tracer: void 0,
25340
+ hookRunner: wrongTraceHookRunner,
25187
25341
  // Off unless the operator opts in. The WebUI drives the same agent as the
25188
25342
  // CLI, so it must resolve this identically — a surface-dependent gate
25189
25343
  // would mean the same repo governs under `wstack` and not in the browser.
@@ -25405,6 +25559,12 @@ async function createAgentServices(input) {
25405
25559
  // round-robin keeps reassigning the doomed model. Mirrors the CLI
25406
25560
  // factory wiring at host-subagent-factory.ts:337.
25407
25561
  statusTracker: container.safeResolve(TOKENS2.ProviderModelStatusTracker),
25562
+ // WrongTrace lock gate for SDD-wizard workers: the standalone server
25563
+ // already built a dedicated WrongTrace-only HookRunner for its own
25564
+ // executor above; handing the same runner to the runtime factory
25565
+ // makes every worker edit honor peer locks with one process-wide
25566
+ // owner identity (context.session.id).
25567
+ hookRunner: wrongTraceHookRunner,
25408
25568
  ...input.installToolBoundary ? { installToolBoundary: input.installToolBoundary } : {}
25409
25569
  }),
25410
25570
  paths: {
@@ -26133,7 +26293,12 @@ function resolveSetupProvider(opts) {
26133
26293
  baseUrl: config.baseUrl
26134
26294
  };
26135
26295
  try {
26136
- const cfgWithType = { ...providerConfig, type: config.provider };
26296
+ const routedConfig = routeProviderCfgThroughProxy(
26297
+ providerConfig,
26298
+ config.baseUrl,
26299
+ config.provider
26300
+ );
26301
+ const cfgWithType = { ...routedConfig, type: config.provider };
26137
26302
  const provider2 = config.features.modelsRegistry && providerRegistry.has(config.provider) ? providerRegistry.create(cfgWithType) : makeProviderFromConfig3(config.provider, cfgWithType);
26138
26303
  return { provider: provider2, needsSetup: false };
26139
26304
  } catch (err) {
@@ -26146,8 +26311,13 @@ function resolveSetupProvider(opts) {
26146
26311
  if (firstKey) {
26147
26312
  const firstProvider = expectDefined3(savedProviders[firstKey]);
26148
26313
  try {
26314
+ const routedConfig = routeProviderCfgThroughProxy(
26315
+ firstProvider,
26316
+ config.baseUrl,
26317
+ firstKey
26318
+ );
26149
26319
  const provider2 = makeProviderFromConfig3(firstKey, {
26150
- ...firstProvider,
26320
+ ...routedConfig,
26151
26321
  type: firstKey,
26152
26322
  family: firstProvider.family,
26153
26323
  apiKey: firstProvider.apiKey
@@ -26793,9 +26963,14 @@ function buildRoutes(state, deps2, cb) {
26793
26963
  const cur = state.getConfig();
26794
26964
  const newCfg = patchConfig(cur, { provider: newProvider, model: newModel });
26795
26965
  const providerCfg = newCfg.providers?.[newProvider] ?? { type: newProvider };
26796
- const built = deps2.providerRegistry.has(newProvider) ? deps2.providerRegistry.create({ ...providerCfg, type: newProvider }) : makeProviderFromConfig4(newProvider, providerCfg);
26966
+ const routedCfg = routeProviderCfgThroughProxy(
26967
+ providerCfg,
26968
+ newCfg.baseUrl,
26969
+ newProvider
26970
+ );
26971
+ const built = deps2.providerRegistry.has(newProvider) ? deps2.providerRegistry.create({ ...routedCfg, type: newProvider }) : makeProviderFromConfig4(newProvider, routedCfg);
26797
26972
  const newProv = deps2.modelsRegistry ? await withCatalogCapabilities(deps2.modelsRegistry, newProvider, built, {
26798
- ...providerCfg,
26973
+ ...routedCfg,
26799
26974
  type: newProvider,
26800
26975
  model: newModel
26801
26976
  }) : built;
@@ -26807,7 +26982,7 @@ function buildRoutes(state, deps2, cb) {
26807
26982
  deps2.configStore.update({ provider: newProvider, model: newModel });
26808
26983
  deps2.context.model = newModel;
26809
26984
  deps2.context.provider = newProv;
26810
- await cb.updateAutoCompactionMaxContext(newProv, newProvider, providerCfg).catch((error2) => {
26985
+ await cb.updateAutoCompactionMaxContext(newProv, newProvider, routedCfg).catch((error2) => {
26811
26986
  deps2.logger.warn(`model.switch capability refresh failed: ${String(error2)}`);
26812
26987
  });
26813
26988
  broadcast(state.getClients(), {
@@ -26948,6 +27123,13 @@ function buildRoutes(state, deps2, cb) {
26948
27123
  if (typeof payload["fallbackAuto"] === "boolean")
26949
27124
  config.fallbackAuto = payload["fallbackAuto"];
26950
27125
  },
27126
+ // WrongProxy / WrongTrace: reflect the standalone toggle/URL into the
27127
+ // shared `ProxyConfig` singleton immediately and await the re-probe so
27128
+ // `active` is fresh before a subsequent model.switch reads it. In the
27129
+ // CLI-hosted path this same key is the CLI's `applyWrongProxyPrefs`; when
27130
+ // running as its own process there is no CLI to inject it, so route it to
27131
+ // the server-local runtime module.
27132
+ applyWrongProxyPrefs: (payload) => applyWrongProxyPrefs(payload),
26951
27133
  setAutoCompact: (enabled) => {
26952
27134
  deps2.pipelines.contextWindow.remove("AutoCompaction", { optional: true });
26953
27135
  if (enabled && deps2.autoCompactor) {
@@ -27432,7 +27614,12 @@ function setupWebuiCredentialWatcher(options) {
27432
27614
  ...snapshot.apiKey !== void 0 ? { apiKey: snapshot.apiKey } : {},
27433
27615
  ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {}
27434
27616
  };
27435
- const newProv = deps2.providerRegistry.has(activeId) ? deps2.providerRegistry.create({ ...providerCfg, type: activeId }) : makeProviderFromConfig5(activeId, { ...providerCfg, type: activeId });
27617
+ const routedCfg = routeProviderCfgThroughProxy(
27618
+ providerCfg,
27619
+ state.getConfig().baseUrl,
27620
+ activeId
27621
+ );
27622
+ const newProv = deps2.providerRegistry.has(activeId) ? deps2.providerRegistry.create({ ...routedCfg, type: activeId }) : makeProviderFromConfig5(activeId, { ...routedCfg, type: activeId });
27436
27623
  deps2.context.provider = newProv;
27437
27624
  void updateAutoCompactionMaxContext(newProv).catch(() => void 0);
27438
27625
  console.log(`[WebUI] Provider credentials reloaded from config.json (${activeId})`);
@@ -27447,6 +27634,40 @@ function setupWebuiCredentialWatcher(options) {
27447
27634
  return credentialWatcher.close;
27448
27635
  }
27449
27636
 
27637
+ // src/server/start-webui-proxy-apply.ts
27638
+ import { createProxyInstantApply } from "@wrongstack/core/wiring/proxy-rewrite";
27639
+ import { makeProviderFromConfig as makeProviderFromConfig6, withCatalogCapabilities as withCatalogCapabilities2 } from "@wrongstack/providers";
27640
+ function setupWebuiProxyInstantApply(options) {
27641
+ const { state, deps: deps2, updateAutoCompactionMaxContext } = options;
27642
+ const instantApply = createProxyInstantApply({
27643
+ // Prefer the LIVE provider id over the config's — mirrors the CLI
27644
+ // wiring; the context is the source of truth once the session boots.
27645
+ getActiveProviderId: () => deps2.context.provider?.id ?? state.getConfig().provider ?? "",
27646
+ // Same raw-read rule as every other WebUI provider-build site:
27647
+ // savedCfg.baseUrl ?? top-level config.baseUrl.
27648
+ getRawBaseUrl: (providerId) => state.getConfig().providers?.[providerId]?.baseUrl ?? state.getConfig().baseUrl,
27649
+ rebuildProvider: async (providerId) => {
27650
+ await deps2.context.runModelTransition(async () => {
27651
+ if (deps2.context.provider?.id !== providerId) return;
27652
+ const cur = state.getConfig();
27653
+ const providerCfg = cur.providers?.[providerId] ?? { type: providerId };
27654
+ const routedCfg = routeProviderCfgThroughProxy(providerCfg, cur.baseUrl, providerId);
27655
+ const built = deps2.providerRegistry.has(providerId) ? deps2.providerRegistry.create({ ...routedCfg, type: providerId }) : makeProviderFromConfig6(providerId, routedCfg);
27656
+ const model = deps2.context.model ?? cur.model ?? "";
27657
+ const newProv = deps2.modelsRegistry ? await withCatalogCapabilities2(deps2.modelsRegistry, providerId, built, {
27658
+ ...routedCfg,
27659
+ type: providerId,
27660
+ model
27661
+ }) : built;
27662
+ deps2.context.provider = newProv;
27663
+ void updateAutoCompactionMaxContext(newProv, providerId).catch(() => void 0);
27664
+ });
27665
+ },
27666
+ logger: deps2.logger
27667
+ });
27668
+ return () => instantApply.dispose();
27669
+ }
27670
+
27450
27671
  // src/server/start-webui-remediation.ts
27451
27672
  import { randomUUID as randomUUID5 } from "node:crypto";
27452
27673
  import { toLanguagePackageInput } from "@wrongstack/techstack";
@@ -27536,6 +27757,7 @@ function setupWebuiShutdown(options) {
27536
27757
  await options.todosCheckpoint.detach();
27537
27758
  await options.stopHeapWatchdog();
27538
27759
  options.getCredentialWatcherClose()?.();
27760
+ options.getProxyInstantApplyDispose()();
27539
27761
  options.disposeRealtimeHandlers();
27540
27762
  const governanceCleanup = await options.governanceHandle?.close();
27541
27763
  if (governanceCleanup && !governanceCleanup.ok) {
@@ -27634,6 +27856,7 @@ async function startWebUI(opts = {}) {
27634
27856
  console.log("[WebUI] Starting backend services...");
27635
27857
  const boot = await bootConfig();
27636
27858
  const { config: baseConfig, globalConfigPath, wpaths, logger } = boot;
27859
+ await bootstrapWrongProxyFromConfig(baseConfig);
27637
27860
  const vault = opts.services?.vault ?? boot.vault;
27638
27861
  let config = baseConfig;
27639
27862
  let projectRoot = boot.projectRoot;
@@ -28081,6 +28304,11 @@ async function startWebUI(opts = {}) {
28081
28304
  clients,
28082
28305
  updateAutoCompactionMaxContext
28083
28306
  });
28307
+ const proxyInstantApplyDispose = setupWebuiProxyInstantApply({
28308
+ state,
28309
+ deps: deps2,
28310
+ updateAutoCompactionMaxContext
28311
+ });
28084
28312
  const stopHeapWatchdog = startSharedHeapWatchdog({
28085
28313
  collectStats: () => ({
28086
28314
  surface: opts.surface ?? "webui",
@@ -28188,6 +28416,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
28188
28416
  todosCheckpoint,
28189
28417
  stopHeapWatchdog,
28190
28418
  getCredentialWatcherClose: () => credentialWatcherClose,
28419
+ getProxyInstantApplyDispose: () => proxyInstantApplyDispose,
28191
28420
  disposeRealtimeHandlers,
28192
28421
  governanceHandle,
28193
28422
  logger,
@@ -98,7 +98,12 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
98
98
  // `prefs.update` exactly so the panel does not error with
99
99
  // "unknown preference key".
100
100
  "readSymbols",
101
- "showSageMemoryInject"
101
+ "showSageMemoryInject",
102
+ // WrongProxy / WrongTrace master switch. When on, the CLI rewrites every
103
+ // provider's base URL through the configured proxy URL (default
104
+ // http://localhost:3444 → http://localhost:3444/proxy/<host><path>).
105
+ // Excluded providers (openai-codex) flow through unchanged.
106
+ "wrongProxyEnabled"
102
107
  ]);
103
108
  var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set([
104
109
  "fallbackModels",
@@ -165,7 +170,11 @@ var STRING_PREF_KEYS = /* @__PURE__ */ new Set([
165
170
  "chimeraModel",
166
171
  "autoReviewProvider",
167
172
  "autoReviewModel",
168
- "autoReviewFallbackProfile"
173
+ "autoReviewFallbackProfile",
174
+ // WrongProxy / WrongTrace URL. Empty = unset. Default lives in
175
+ // `LocalPrefs.DEFAULTS.wrongProxyUrl` ('http://localhost:3444'); users
176
+ // can override here for non-default daemon ports / paths.
177
+ "wrongProxyUrl"
169
178
  ]);
170
179
  var ENUM_PREF_KEYS = {
171
180
  autonomy: AUTONOMY_VALUES,
@@ -179,7 +188,7 @@ var ENUM_PREF_KEYS = {
179
188
  reasoningEffort: REASONING_EFFORT_VALUES,
180
189
  cacheTtl: CACHE_TTL_VALUES,
181
190
  statuslineMode: /* @__PURE__ */ new Set(["minimum", "detailed", "no-color"]),
182
- animationStyle: /* @__PURE__ */ new Set(["rainbow", "wave", "pulse", "dots", "breathe", "cycle"]),
191
+ animationStyle: /* @__PURE__ */ new Set(["rainbow", "wave", "pulse", "dots", "breathe", "static", "cycle"]),
183
192
  fsAccess: /* @__PURE__ */ new Set(["unrestricted", "project"]),
184
193
  // Chimera autoFix + auto-review cascade threshold
185
194
  chimeraAutoFix: /* @__PURE__ */ new Set(["off", "ask", "auto"]),
@@ -1031,7 +1040,7 @@ function routingPatch(payload) {
1031
1040
  function handlePrefsGet(ctx, ws) {
1032
1041
  ctx.send(ws, { type: "prefs.updated", payload: ctx.snapshot() });
1033
1042
  }
1034
- function handlePrefsUpdate(ctx, ws, input) {
1043
+ async function handlePrefsUpdate(ctx, ws, input) {
1035
1044
  const parsed = validatePrefsUpdatePayload(input);
1036
1045
  if (!parsed.ok) {
1037
1046
  sendResult(ctx, ws, false, parsed.message);
@@ -1064,6 +1073,9 @@ function handlePrefsUpdate(ctx, ws, input) {
1064
1073
  ({ setDebugStreamEnabled }) => setDebugStreamEnabled(payload["debugStream"])
1065
1074
  );
1066
1075
  }
1076
+ if (typeof payload["wrongProxyEnabled"] === "boolean" || typeof payload["wrongProxyUrl"] === "string") {
1077
+ await ctx.applyWrongProxyPrefs?.(payload);
1078
+ }
1067
1079
  if (typeof payload["logLevel"] === "string" && ["debug", "info", "warn", "error"].includes(payload["logLevel"])) {
1068
1080
  ctx.setLogLevel?.(payload["logLevel"]);
1069
1081
  }
@@ -6179,7 +6191,8 @@ function estimateTokens(s) {
6179
6191
  function stringifyContent(c) {
6180
6192
  if (typeof c === "string") return c;
6181
6193
  try {
6182
- return JSON.stringify(c);
6194
+ const serialized = JSON.stringify(c);
6195
+ return serialized === void 0 ? String(c) : serialized;
6183
6196
  } catch {
6184
6197
  return String(c);
6185
6198
  }
@@ -7105,6 +7118,8 @@ function seedContextMeta(config, context) {
7105
7118
  meta["tgDelegate"] = tgExt?.["notifyOnDelegate"] !== false;
7106
7119
  const tgMs = tgExt?.["longToolThresholdMs"];
7107
7120
  meta["tgLongToolMs"] = typeof tgMs === "number" ? tgMs : 3e4;
7121
+ meta["wrongProxyEnabled"] = config.tools?.wrongProxy?.enabled === true;
7122
+ meta["wrongProxyUrl"] = config.tools?.wrongProxy?.url ?? "";
7108
7123
  {
7109
7124
  const pluginsEnabled = {};
7110
7125
  const record = (name2) => {
@@ -7471,6 +7486,85 @@ function createCustomModeStore(wrongstackDir) {
7471
7486
  return { modes, load: load2, save: save2, create, update, remove, list };
7472
7487
  }
7473
7488
 
7489
+ // src/server/proxy-runtime.ts
7490
+ import {
7491
+ applyProxyConfig,
7492
+ getProxyConfig,
7493
+ rewriteBaseUrl,
7494
+ shouldRewriteFor
7495
+ } from "@wrongstack/core/wiring/proxy-rewrite";
7496
+ var HEALTH_PATH = "/api/health";
7497
+ var PROBE_TIMEOUT_MS = 2e3;
7498
+ function seedWrongProxyFromConfig(config) {
7499
+ const wp = config.tools?.wrongProxy;
7500
+ if (!wp) {
7501
+ return;
7502
+ }
7503
+ applyProxyConfig({
7504
+ enabled: wp.enabled === true,
7505
+ url: typeof wp.url === "string" ? wp.url : ""
7506
+ });
7507
+ }
7508
+ async function probeWrongProxyActive() {
7509
+ const cfg = getProxyConfig();
7510
+ if (!cfg.enabled || !cfg.url) {
7511
+ applyProxyConfig({ active: false });
7512
+ return false;
7513
+ }
7514
+ const abort = new AbortController();
7515
+ const timeout = setTimeout(() => abort.abort(), PROBE_TIMEOUT_MS);
7516
+ if (typeof timeout.unref === "function") {
7517
+ timeout.unref();
7518
+ }
7519
+ try {
7520
+ const healthUrl = `${cfg.url.replace(/\/+$/, "")}${HEALTH_PATH}`;
7521
+ const res = await fetch(healthUrl, {
7522
+ method: "GET",
7523
+ signal: abort.signal,
7524
+ headers: { accept: "application/json" }
7525
+ });
7526
+ const ok2 = res.ok && res.status >= 200 && res.status < 300;
7527
+ applyProxyConfig({ active: ok2 });
7528
+ return ok2;
7529
+ } catch {
7530
+ applyProxyConfig({ active: false });
7531
+ return false;
7532
+ } finally {
7533
+ clearTimeout(timeout);
7534
+ }
7535
+ }
7536
+ async function applyWrongProxyPrefs(payload) {
7537
+ const patch = {};
7538
+ if (typeof payload["wrongProxyEnabled"] === "boolean") {
7539
+ patch.enabled = payload["wrongProxyEnabled"];
7540
+ }
7541
+ if (typeof payload["wrongProxyUrl"] === "string") {
7542
+ patch.url = payload["wrongProxyUrl"];
7543
+ }
7544
+ if (Object.keys(patch).length === 0) return;
7545
+ applyProxyConfig(patch);
7546
+ await probeWrongProxyActive();
7547
+ }
7548
+ async function bootstrapWrongProxyFromConfig(config) {
7549
+ seedWrongProxyFromConfig(config);
7550
+ await probeWrongProxyActive();
7551
+ }
7552
+ function routeProviderCfgThroughProxy(cfg, fallbackBaseUrl, providerId) {
7553
+ const raw = cfg;
7554
+ const cfgType = typeof raw.type === "string" ? raw.type : void 0;
7555
+ const cfgBaseUrl = typeof raw.baseUrl === "string" ? raw.baseUrl : void 0;
7556
+ const factoryType = cfgType ?? providerId;
7557
+ const rawBaseUrl = cfgBaseUrl ?? fallbackBaseUrl;
7558
+ if (!rawBaseUrl || !shouldRewriteFor(factoryType)) {
7559
+ return { ...cfg };
7560
+ }
7561
+ const baseUrl = rewriteBaseUrl(rawBaseUrl, getProxyConfig().url);
7562
+ return {
7563
+ ...cfg,
7564
+ ...baseUrl !== cfgBaseUrl ? { baseUrl } : {}
7565
+ };
7566
+ }
7567
+
7474
7568
  // src/server/project-handlers.ts
7475
7569
  import * as fs10 from "node:fs/promises";
7476
7570
  import * as path11 from "node:path";
@@ -19402,7 +19496,13 @@ var PREF_KEYS = [
19402
19496
  // Per-plugin enable/disable map (parity with the embedded server).
19403
19497
  "pluginsEnabled",
19404
19498
  // Fleet chat verbosity: off | full (migrated from streamFleet boolean).
19405
- "fleetChatVerbosity"
19499
+ "fleetChatVerbosity",
19500
+ // WrongProxy / WrongTrace: master switch + configurable URL (default
19501
+ // http://localhost:3444). When `wrongProxyEnabled` is true and the daemon
19502
+ // is reachable, every provider's base URL flows through
19503
+ // `${wrongProxyUrl}/proxy/<host><path>`. openai-codex is excluded by spec.
19504
+ "wrongProxyEnabled",
19505
+ "wrongProxyUrl"
19406
19506
  ];
19407
19507
  function prefSnapshot(contextMeta) {
19408
19508
  const snapshot = {};
@@ -19668,6 +19768,15 @@ async function persistPrefsToConfig(deps2, holder, payload) {
19668
19768
  }
19669
19769
  if (typeof payload["debugStream"] === "boolean")
19670
19770
  decrypted.debugStream = payload["debugStream"];
19771
+ if (typeof payload["wrongProxyEnabled"] === "boolean" || typeof payload["wrongProxyUrl"] === "string") {
19772
+ const toolsCfg = decrypted.tools ?? {};
19773
+ const wp = toolsCfg["wrongProxy"] ?? {};
19774
+ if (typeof payload["wrongProxyEnabled"] === "boolean")
19775
+ wp["enabled"] = payload["wrongProxyEnabled"];
19776
+ if (typeof payload["wrongProxyUrl"] === "string") wp["url"] = payload["wrongProxyUrl"];
19777
+ toolsCfg["wrongProxy"] = wp;
19778
+ decrypted.tools = toolsCfg;
19779
+ }
19671
19780
  if (typeof payload["pluginsEnabled"] === "object" && payload["pluginsEnabled"] !== null) {
19672
19781
  const ext = decrypted.extensions ?? {};
19673
19782
  const toggled = [];
@@ -22333,6 +22442,8 @@ import {
22333
22442
  // src/server/backend-services.ts
22334
22443
  import { join as join12 } from "node:path";
22335
22444
  import { Agent } from "@wrongstack/core/agent";
22445
+ import { HookRegistry, HookRunner } from "@wrongstack/core/hooks";
22446
+ import { createWrongTraceHookPair, recordGateDecision, snapshotGateDecisions, persistWrongTraceGateCounters } from "@wrongstack/wrongtrace";
22336
22447
  import {
22337
22448
  BrainDecisionLedger,
22338
22449
  BrainMonitor,
@@ -23374,6 +23485,33 @@ async function createAgentServices(input) {
23374
23485
  const secretScrubber = container.resolve(TOKENS.SecretScrubber);
23375
23486
  const renderer = container.has(TOKENS.Renderer) ? container.resolve(TOKENS.Renderer) : void 0;
23376
23487
  const permissionPolicy = container.resolve(TOKENS.PermissionPolicy);
23488
+ const wrongTraceHookRegistry = new HookRegistry();
23489
+ const wrongTraceHooks = createWrongTraceHookPair(() => context.session.id, {
23490
+ emit: (event) => {
23491
+ events.emit("wrongtrace.gate.decision", event);
23492
+ recordGateDecision(event);
23493
+ void persistWrongTraceGateCounters(projectRoot, snapshotGateDecisions());
23494
+ }
23495
+ });
23496
+ wrongTraceHookRegistry.registerInProcess(
23497
+ "PreToolUse",
23498
+ "edit|write|replace|patch|codebase-ast-replace",
23499
+ wrongTraceHooks.preToolUse,
23500
+ "wrongtrace-gate"
23501
+ );
23502
+ wrongTraceHookRegistry.registerInProcess(
23503
+ "PostToolUse",
23504
+ "edit|write|replace|patch|codebase-ast-replace",
23505
+ wrongTraceHooks.postToolUse,
23506
+ "wrongtrace-gate"
23507
+ );
23508
+ const wrongTraceHookRunner = new HookRunner({
23509
+ registry: wrongTraceHookRegistry,
23510
+ sessionId: () => context.session.id,
23511
+ // Coordination, not enforcement: these hooks are fail-open by
23512
+ // construction and must run regardless of shell-hook gating.
23513
+ allowNonPolicy: true
23514
+ });
23377
23515
  const toolExecutor = new ToolExecutor(toolRegistry, {
23378
23516
  permissionPolicy,
23379
23517
  secretScrubber,
@@ -23383,6 +23521,7 @@ async function createAgentServices(input) {
23383
23521
  iterationTimeoutMs: config.tools?.iterationTimeoutMs ?? DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
23384
23522
  perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
23385
23523
  tracer: void 0,
23524
+ hookRunner: wrongTraceHookRunner,
23386
23525
  // Off unless the operator opts in. The WebUI drives the same agent as the
23387
23526
  // CLI, so it must resolve this identically — a surface-dependent gate
23388
23527
  // would mean the same repo governs under `wstack` and not in the browser.
@@ -23604,6 +23743,12 @@ async function createAgentServices(input) {
23604
23743
  // round-robin keeps reassigning the doomed model. Mirrors the CLI
23605
23744
  // factory wiring at host-subagent-factory.ts:337.
23606
23745
  statusTracker: container.safeResolve(TOKENS.ProviderModelStatusTracker),
23746
+ // WrongTrace lock gate for SDD-wizard workers: the standalone server
23747
+ // already built a dedicated WrongTrace-only HookRunner for its own
23748
+ // executor above; handing the same runner to the runtime factory
23749
+ // makes every worker edit honor peer locks with one process-wide
23750
+ // owner identity (context.session.id).
23751
+ hookRunner: wrongTraceHookRunner,
23607
23752
  ...input.installToolBoundary ? { installToolBoundary: input.installToolBoundary } : {}
23608
23753
  }),
23609
23754
  paths: {
@@ -24332,7 +24477,12 @@ function resolveSetupProvider(opts) {
24332
24477
  baseUrl: config.baseUrl
24333
24478
  };
24334
24479
  try {
24335
- const cfgWithType = { ...providerConfig, type: config.provider };
24480
+ const routedConfig = routeProviderCfgThroughProxy(
24481
+ providerConfig,
24482
+ config.baseUrl,
24483
+ config.provider
24484
+ );
24485
+ const cfgWithType = { ...routedConfig, type: config.provider };
24336
24486
  const provider2 = config.features.modelsRegistry && providerRegistry.has(config.provider) ? providerRegistry.create(cfgWithType) : makeProviderFromConfig(config.provider, cfgWithType);
24337
24487
  return { provider: provider2, needsSetup: false };
24338
24488
  } catch (err) {
@@ -24345,8 +24495,13 @@ function resolveSetupProvider(opts) {
24345
24495
  if (firstKey) {
24346
24496
  const firstProvider = expectDefined2(savedProviders[firstKey]);
24347
24497
  try {
24498
+ const routedConfig = routeProviderCfgThroughProxy(
24499
+ firstProvider,
24500
+ config.baseUrl,
24501
+ firstKey
24502
+ );
24348
24503
  const provider2 = makeProviderFromConfig(firstKey, {
24349
- ...firstProvider,
24504
+ ...routedConfig,
24350
24505
  type: firstKey,
24351
24506
  family: firstProvider.family,
24352
24507
  apiKey: firstProvider.apiKey
@@ -24992,9 +25147,14 @@ function buildRoutes(state, deps2, cb) {
24992
25147
  const cur = state.getConfig();
24993
25148
  const newCfg = patchConfig(cur, { provider: newProvider, model: newModel });
24994
25149
  const providerCfg = newCfg.providers?.[newProvider] ?? { type: newProvider };
24995
- const built = deps2.providerRegistry.has(newProvider) ? deps2.providerRegistry.create({ ...providerCfg, type: newProvider }) : makeProviderFromConfig2(newProvider, providerCfg);
25150
+ const routedCfg = routeProviderCfgThroughProxy(
25151
+ providerCfg,
25152
+ newCfg.baseUrl,
25153
+ newProvider
25154
+ );
25155
+ const built = deps2.providerRegistry.has(newProvider) ? deps2.providerRegistry.create({ ...routedCfg, type: newProvider }) : makeProviderFromConfig2(newProvider, routedCfg);
24996
25156
  const newProv = deps2.modelsRegistry ? await withCatalogCapabilities(deps2.modelsRegistry, newProvider, built, {
24997
- ...providerCfg,
25157
+ ...routedCfg,
24998
25158
  type: newProvider,
24999
25159
  model: newModel
25000
25160
  }) : built;
@@ -25006,7 +25166,7 @@ function buildRoutes(state, deps2, cb) {
25006
25166
  deps2.configStore.update({ provider: newProvider, model: newModel });
25007
25167
  deps2.context.model = newModel;
25008
25168
  deps2.context.provider = newProv;
25009
- await cb.updateAutoCompactionMaxContext(newProv, newProvider, providerCfg).catch((error2) => {
25169
+ await cb.updateAutoCompactionMaxContext(newProv, newProvider, routedCfg).catch((error2) => {
25010
25170
  deps2.logger.warn(`model.switch capability refresh failed: ${String(error2)}`);
25011
25171
  });
25012
25172
  broadcast(state.getClients(), {
@@ -25147,6 +25307,13 @@ function buildRoutes(state, deps2, cb) {
25147
25307
  if (typeof payload["fallbackAuto"] === "boolean")
25148
25308
  config.fallbackAuto = payload["fallbackAuto"];
25149
25309
  },
25310
+ // WrongProxy / WrongTrace: reflect the standalone toggle/URL into the
25311
+ // shared `ProxyConfig` singleton immediately and await the re-probe so
25312
+ // `active` is fresh before a subsequent model.switch reads it. In the
25313
+ // CLI-hosted path this same key is the CLI's `applyWrongProxyPrefs`; when
25314
+ // running as its own process there is no CLI to inject it, so route it to
25315
+ // the server-local runtime module.
25316
+ applyWrongProxyPrefs: (payload) => applyWrongProxyPrefs(payload),
25150
25317
  setAutoCompact: (enabled) => {
25151
25318
  deps2.pipelines.contextWindow.remove("AutoCompaction", { optional: true });
25152
25319
  if (enabled && deps2.autoCompactor) {
@@ -25631,7 +25798,12 @@ function setupWebuiCredentialWatcher(options) {
25631
25798
  ...snapshot.apiKey !== void 0 ? { apiKey: snapshot.apiKey } : {},
25632
25799
  ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {}
25633
25800
  };
25634
- const newProv = deps2.providerRegistry.has(activeId) ? deps2.providerRegistry.create({ ...providerCfg, type: activeId }) : makeProviderFromConfig3(activeId, { ...providerCfg, type: activeId });
25801
+ const routedCfg = routeProviderCfgThroughProxy(
25802
+ providerCfg,
25803
+ state.getConfig().baseUrl,
25804
+ activeId
25805
+ );
25806
+ const newProv = deps2.providerRegistry.has(activeId) ? deps2.providerRegistry.create({ ...routedCfg, type: activeId }) : makeProviderFromConfig3(activeId, { ...routedCfg, type: activeId });
25635
25807
  deps2.context.provider = newProv;
25636
25808
  void updateAutoCompactionMaxContext(newProv).catch(() => void 0);
25637
25809
  console.log(`[WebUI] Provider credentials reloaded from config.json (${activeId})`);
@@ -25646,6 +25818,40 @@ function setupWebuiCredentialWatcher(options) {
25646
25818
  return credentialWatcher.close;
25647
25819
  }
25648
25820
 
25821
+ // src/server/start-webui-proxy-apply.ts
25822
+ import { createProxyInstantApply } from "@wrongstack/core/wiring/proxy-rewrite";
25823
+ import { makeProviderFromConfig as makeProviderFromConfig4, withCatalogCapabilities as withCatalogCapabilities2 } from "@wrongstack/providers";
25824
+ function setupWebuiProxyInstantApply(options) {
25825
+ const { state, deps: deps2, updateAutoCompactionMaxContext } = options;
25826
+ const instantApply = createProxyInstantApply({
25827
+ // Prefer the LIVE provider id over the config's — mirrors the CLI
25828
+ // wiring; the context is the source of truth once the session boots.
25829
+ getActiveProviderId: () => deps2.context.provider?.id ?? state.getConfig().provider ?? "",
25830
+ // Same raw-read rule as every other WebUI provider-build site:
25831
+ // savedCfg.baseUrl ?? top-level config.baseUrl.
25832
+ getRawBaseUrl: (providerId) => state.getConfig().providers?.[providerId]?.baseUrl ?? state.getConfig().baseUrl,
25833
+ rebuildProvider: async (providerId) => {
25834
+ await deps2.context.runModelTransition(async () => {
25835
+ if (deps2.context.provider?.id !== providerId) return;
25836
+ const cur = state.getConfig();
25837
+ const providerCfg = cur.providers?.[providerId] ?? { type: providerId };
25838
+ const routedCfg = routeProviderCfgThroughProxy(providerCfg, cur.baseUrl, providerId);
25839
+ const built = deps2.providerRegistry.has(providerId) ? deps2.providerRegistry.create({ ...routedCfg, type: providerId }) : makeProviderFromConfig4(providerId, routedCfg);
25840
+ const model = deps2.context.model ?? cur.model ?? "";
25841
+ const newProv = deps2.modelsRegistry ? await withCatalogCapabilities2(deps2.modelsRegistry, providerId, built, {
25842
+ ...routedCfg,
25843
+ type: providerId,
25844
+ model
25845
+ }) : built;
25846
+ deps2.context.provider = newProv;
25847
+ void updateAutoCompactionMaxContext(newProv, providerId).catch(() => void 0);
25848
+ });
25849
+ },
25850
+ logger: deps2.logger
25851
+ });
25852
+ return () => instantApply.dispose();
25853
+ }
25854
+
25649
25855
  // src/server/start-webui-remediation.ts
25650
25856
  import { randomUUID as randomUUID4 } from "node:crypto";
25651
25857
  import { toLanguagePackageInput } from "@wrongstack/techstack";
@@ -25735,6 +25941,7 @@ function setupWebuiShutdown(options) {
25735
25941
  await options.todosCheckpoint.detach();
25736
25942
  await options.stopHeapWatchdog();
25737
25943
  options.getCredentialWatcherClose()?.();
25944
+ options.getProxyInstantApplyDispose()();
25738
25945
  options.disposeRealtimeHandlers();
25739
25946
  const governanceCleanup = await options.governanceHandle?.close();
25740
25947
  if (governanceCleanup && !governanceCleanup.ok) {
@@ -25833,6 +26040,7 @@ async function startWebUI(opts = {}) {
25833
26040
  console.log("[WebUI] Starting backend services...");
25834
26041
  const boot = await bootConfig();
25835
26042
  const { config: baseConfig, globalConfigPath, wpaths, logger } = boot;
26043
+ await bootstrapWrongProxyFromConfig(baseConfig);
25836
26044
  const vault = opts.services?.vault ?? boot.vault;
25837
26045
  let config = baseConfig;
25838
26046
  let projectRoot = boot.projectRoot;
@@ -26280,6 +26488,11 @@ async function startWebUI(opts = {}) {
26280
26488
  clients,
26281
26489
  updateAutoCompactionMaxContext
26282
26490
  });
26491
+ const proxyInstantApplyDispose = setupWebuiProxyInstantApply({
26492
+ state,
26493
+ deps: deps2,
26494
+ updateAutoCompactionMaxContext
26495
+ });
26283
26496
  const stopHeapWatchdog = startSharedHeapWatchdog({
26284
26497
  collectStats: () => ({
26285
26498
  surface: opts.surface ?? "webui",
@@ -26387,6 +26600,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
26387
26600
  todosCheckpoint,
26388
26601
  stopHeapWatchdog,
26389
26602
  getCredentialWatcherClose: () => credentialWatcherClose,
26603
+ getProxyInstantApplyDispose: () => proxyInstantApplyDispose,
26390
26604
  disposeRealtimeHandlers,
26391
26605
  governanceHandle,
26392
26606
  logger,
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import type { SecretVault } from '@wrongstack/core/types';
19
19
  /** Pref keys exposed to the settings panel via prefs.get / prefs.updated. */
20
- export declare const PREF_KEYS: readonly ['autonomy', 'autonomyDelayMs', 'autoProceedMaxIterations', 'yolo', 'maxIterations', 'chime', 'confirmExit', 'nextPrediction', 'nextStepsTool', 'enhanceEnabled', 'enhanceDelayMs', 'enhanceLanguage', 'featureMcp', 'featurePlugins', 'featureMemory', 'featureSkills', 'featureModelsRegistry', 'indexOnStart', 'contextAutoCompact', 'contextStrategy', 'contextMode', 'tokenSavingTier', 'maxConcurrent', 'titleAnimation', 'uiLocale', 'logLevel', 'auditLevel', 'hqEnabled', 'hqUrl', 'hqRawContent', 'tgConfigured', 'tgSessionEnd', 'tgDelegate', 'tgLongToolMs', 'reasoningMode', 'reasoningEffort', 'reasoningPreserve', 'cacheTtl', 'fallbackModels', 'fallbackProfiles', 'favoriteModels', 'favoriteModelsOnly', 'modelAvailabilitySchedule', 'modelMatrix', 'fallbackAuto', 'refinerProvider', 'refinerModel', 'refinerFallbackProfile', 'thinkingWord', 'statuslineMode', 'animationStyle', 'showModelReasoning', 'breakerEnabled', 'breakerAutoKillResetMs', 'fsAccess', 'debugStream', 'chimeraEnabled', 'chimeraProvider', 'chimeraModel', 'chimeraMaxFiles', 'chimeraAutoFix', 'autoReviewEnabled', 'autoReviewProvider', 'autoReviewModel', 'autoReviewFallbackProfile', 'autoReviewModelSelection', 'autoReviewFallbackModels', 'autoReviewDebounceMs', 'autoReviewMaxFilesPerBatch', 'autoReviewMaxConcurrentReviews', 'autoReviewCascadeOn', 'groupToolCalls', 'showThinkingLogs', 'autoCollapseInput', 'pluginsEnabled', 'fleetChatVerbosity'];
20
+ export declare const PREF_KEYS: readonly ['autonomy', 'autonomyDelayMs', 'autoProceedMaxIterations', 'yolo', 'maxIterations', 'chime', 'confirmExit', 'nextPrediction', 'nextStepsTool', 'enhanceEnabled', 'enhanceDelayMs', 'enhanceLanguage', 'featureMcp', 'featurePlugins', 'featureMemory', 'featureSkills', 'featureModelsRegistry', 'indexOnStart', 'contextAutoCompact', 'contextStrategy', 'contextMode', 'tokenSavingTier', 'maxConcurrent', 'titleAnimation', 'uiLocale', 'logLevel', 'auditLevel', 'hqEnabled', 'hqUrl', 'hqRawContent', 'tgConfigured', 'tgSessionEnd', 'tgDelegate', 'tgLongToolMs', 'reasoningMode', 'reasoningEffort', 'reasoningPreserve', 'cacheTtl', 'fallbackModels', 'fallbackProfiles', 'favoriteModels', 'favoriteModelsOnly', 'modelAvailabilitySchedule', 'modelMatrix', 'fallbackAuto', 'refinerProvider', 'refinerModel', 'refinerFallbackProfile', 'thinkingWord', 'statuslineMode', 'animationStyle', 'showModelReasoning', 'breakerEnabled', 'breakerAutoKillResetMs', 'fsAccess', 'debugStream', 'chimeraEnabled', 'chimeraProvider', 'chimeraModel', 'chimeraMaxFiles', 'chimeraAutoFix', 'autoReviewEnabled', 'autoReviewProvider', 'autoReviewModel', 'autoReviewFallbackProfile', 'autoReviewModelSelection', 'autoReviewFallbackModels', 'autoReviewDebounceMs', 'autoReviewMaxFilesPerBatch', 'autoReviewMaxConcurrentReviews', 'autoReviewCascadeOn', 'groupToolCalls', 'showThinkingLogs', 'autoCollapseInput', 'pluginsEnabled', 'fleetChatVerbosity', 'wrongProxyEnabled', 'wrongProxyUrl'];
21
21
  export interface PrefHelperDeps {
22
22
  /** Path to the active profile config; the sole settings mutation target. */
23
23
  profileConfigPath: string;
@@ -13,10 +13,20 @@ export interface PrefsHandlerContext {
13
13
  applyConfigPrefs?: ((payload: Record<string, unknown>) => void) | undefined;
14
14
  setAutoCompact?: ((enabled: boolean) => void) | undefined;
15
15
  setLogLevel?: ((level: 'debug' | 'info' | 'warn' | 'error') => void) | undefined;
16
+ /**
17
+ * WrongProxy / WrongTrace: applies the toggle + URL to the runtime
18
+ * config and kicks the periodic probe. Injected by the CLI's
19
+ * `createPrefsSeeding` so the WS server stays package-agnostic — the
20
+ * server doesn't pull `@wrongstack/cli`'s import graph or its
21
+ * `setInterval`-owning probe module. May return a promise so the
22
+ * standalone server's re-probe can be awaited before the next
23
+ * prefs-dependent request (e.g. an immediate model.switch).
24
+ */
25
+ applyWrongProxyPrefs?: ((payload: Record<string, unknown>) => void | Promise<void>) | undefined;
16
26
  send: (ws: WebSocket, message: WSServerMessage) => void;
17
27
  broadcast: (message: WSServerMessage) => void;
18
28
  }
19
29
  export declare function handlePrefsGet(ctx: PrefsHandlerContext, ws: WebSocket): void;
20
- export declare function handlePrefsUpdate(ctx: PrefsHandlerContext, ws: WebSocket, input: Record<string, unknown>): void;
30
+ export declare function handlePrefsUpdate(ctx: PrefsHandlerContext, ws: WebSocket, input: Record<string, unknown>): Promise<void>;
21
31
  export declare function handleAutonomySwitch(ctx: PrefsHandlerContext, ws: WebSocket, mode: string): void;
22
32
  //# sourceMappingURL=prefs-handlers.d.ts.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * WrongProxy / WrongTrace runtime seeding for the standalone WebUI server.
3
+ *
4
+ * The CLI hosts WebUI in-process via `dispatch-webui.ts`, which injects
5
+ * `applyWrongProxyPrefs` (from `@wrongstack/cli/wiring/proxy-wiring`) into
6
+ * the WS prefs handler and shares the CLI's proxy singleton. But when the
7
+ * WebUI server runs as its OWN process (`startWebUI`), no CLI is present to
8
+ * seed the `ProxyConfig` singleton in `@wrongstack/core/wiring/proxy-rewrite`
9
+ * — so all of the server's provider-build paths would construct providers
10
+ * with the raw base URL and never traverse the proxy.
11
+ *
12
+ * webui ⇏ cli, so this module cannot import `@wrongstack/cli`'s probe. It
13
+ * owns the same two concerns the CLI's `proxy-wiring` does, but dependency-
14
+ * light and self-contained:
15
+ *
16
+ * 1. Seeding the shared `ProxyConfig` singleton (`enabled` / `url`) from
17
+ * `config.tools.wrongProxy` at boot.
18
+ * 2. A one-shot `/api/health` reachability probe that flips `active` to
19
+ * true so `shouldRewriteFor()` returns true for subsequent provider
20
+ * builds. Unlike the CLI's periodic probe this runs once at seed time
21
+ * (the standalone server re-probes when the operator toggles the field
22
+ * via `applyWrongProxyPrefs`); a long-lived daemon is assumed stable.
23
+ *
24
+ * The rewrite itself (`shouldRewriteFor` / `rewriteBaseUrl`) is pure logic
25
+ * in `@wrongstack/core` and is applied at each provider-build site in this
26
+ * server (setup-screen, routes applyModelSwitchCore, embedded-message-router,
27
+ * embedded-host-adapters, start-webui-credential-watcher).
28
+ */
29
+ import type { Config } from '@wrongstack/core/types';
30
+ /**
31
+ * Seed the shared `ProxyConfig` singleton from the persisted
32
+ * `config.tools.wrongProxy.{enabled,url}` block. Idempotent — safe to call
33
+ * at every boot. A provider that is eligible and reachable with a
34
+ * configured URL will then have its base URL rewritten through the proxy.
35
+ */
36
+ export declare function seedWrongProxyFromConfig(config: Config): void;
37
+ /**
38
+ * Probe the configured proxy daemon once and flip `active` to true when it
39
+ * answers a 2xx on `/api/health`. Mirrors the CLI's probe contract but
40
+ * without owning a `setInterval` loop. Resolves with the new `active` value.
41
+ * A non-2xx / timeout / ECONNREFUSED leaves `active` false so the rewriter
42
+ * keeps base URLs unchanged.
43
+ */
44
+ export declare function probeWrongProxyActive(): Promise<boolean>;
45
+ /**
46
+ * Apply the `wrongProxyEnabled` + `wrongProxyUrl` portion of a WS prefs
47
+ * payload, then re-probe so the rewrite reflects the new state immediately
48
+ * (the next provider build reads the freshly-probed `active`). Skips when the
49
+ * payload carries neither key. Mirrors the CLI's `applyWrongProxyPrefs`.
50
+ */
51
+ export declare function applyWrongProxyPrefs(payload: Record<string, unknown>): Promise<void>;
52
+ /**
53
+ * Seed the singleton from the persisted config and await the first probe so
54
+ * `active` is correct BEFORE the server's provider is constructed. Without
55
+ * the await, `setup-screen`'s `resolveSetupProvider` reads the singleton
56
+ * (synchronously) while `active` is still false and bakes the raw base URL
57
+ * into the initial provider. Mirrors the CLI's
58
+ * `bootstrapWrongProxy` + `awaitFirstWrongProxyProbe` pair.
59
+ */
60
+ export declare function bootstrapWrongProxyFromConfig(config: Config): Promise<void>;
61
+ /**
62
+ * Apply the WrongProxy / WrongTrace base-URL rewrite to ONE provider config
63
+ * before it is handed to a provider factory. This is the single shared seam
64
+ * for every WebUI-server provider-build path so the eligibility + compose
65
+ * rules cannot drift between call sites:
66
+ *
67
+ * - `routes.ts` `applyModelSwitchCore` (model.switch rebuild)
68
+ * - `setup-screen.ts` `resolveSetupProvider` (boot branches 1 + 2)
69
+ * - `embedded-message-router.ts` `buildProvider`
70
+ * - `embedded-host-adapters.ts` `applyEmbeddedModelSwitch`
71
+ * - `start-webui-credential-watcher.ts` (credential hot-reload)
72
+ *
73
+ * Returns a copy of `cfg` with `baseUrl` overridden to
74
+ * `${proxyUrl}/proxy/<host><path>` when the toggle is on, the daemon is
75
+ * reachable (`active`) and the provider is eligible (openai-codex excluded
76
+ * by the rewriter). `fallbackBaseUrl` is used when `cfg` carries no explicit
77
+ * baseUrl (the call site's top-level config baseUrl). When the proxy is off
78
+ * or unreachable, the cfg is returned unchanged so factory construction
79
+ * behaves exactly as before the proxy feature existed.
80
+ */
81
+ export declare function routeProviderCfgThroughProxy<T extends object>(cfg: Readonly<T>, fallbackBaseUrl: string | undefined, providerId: string): T;
82
+ //# sourceMappingURL=proxy-runtime.d.ts.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * WrongProxy instant-apply for the standalone WebUI server.
3
+ *
4
+ * Mirrors the CLI-side wiring in `setupProviderRuntime` (packages/cli/src/
5
+ * wiring/provider-runtime-setup.ts): subscribe to material proxy-config
6
+ * changes via `createProxyInstantApply` and rebuild the LIVE provider when
7
+ * the routing verdict for the active provider flips, so a Settings toggle
8
+ * or probe deactivation takes effect immediately instead of on the next
9
+ * incidental provider build.
10
+ *
11
+ * The rebuild sequence mirrors `applyModelSwitchCore` (routes.ts) — route
12
+ * the cfg through `routeProviderCfgThroughProxy`, build via registry or
13
+ * `makeProviderFromConfig`, overlay the live model's catalog capabilities
14
+ * — minus the parts that don't apply when provider+model are unchanged
15
+ * (no config persist, no session.start spam). The swap itself runs inside
16
+ * `runModelTransition` with a superseded-check so a queued rebuild can
17
+ * never overwrite a newer /model switch.
18
+ *
19
+ * In the CLI-hosted path this server module is NOT wired (the CLI's own
20
+ * `setupProviderRuntime` instant-apply covers the shared agent context),
21
+ * so there is exactly one rebuilder per process.
22
+ */
23
+ import type { Provider, ProviderConfig } from '@wrongstack/core/types';
24
+ import type { WebuiDeps, WebuiMutableState } from './routes.js';
25
+ export interface WebuiProxyApplyOptions {
26
+ state: WebuiMutableState;
27
+ deps: WebuiDeps;
28
+ /** Mirrors `cb.updateAutoCompactionMaxContext` in start-webui.ts. */
29
+ updateAutoCompactionMaxContext: (provider: Provider, providerId?: string, providerCfg?: ProviderConfig | undefined) => Promise<void>;
30
+ }
31
+ /**
32
+ * Wire the standalone server's WrongProxy instant-apply. Returns the
33
+ * dispose function for shutdown wiring. The helper itself lives in
34
+ * `@wrongstack/core` so both hosts share the identical change-detection
35
+ * semantics (effective-URL comparison, serialized rebuilds).
36
+ */
37
+ export declare function setupWebuiProxyInstantApply(options: WebuiProxyApplyOptions): () => void;
38
+ //# sourceMappingURL=start-webui-proxy-apply.d.ts.map
@@ -18,6 +18,7 @@ export declare function setupWebuiShutdown(options: {
18
18
  };
19
19
  stopHeapWatchdog: () => Promise<void>;
20
20
  getCredentialWatcherClose: () => (() => void) | undefined;
21
+ getProxyInstantApplyDispose: () => () => void;
21
22
  disposeRealtimeHandlers: () => void;
22
23
  governanceHandle?: {
23
24
  close: () => Promise<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/webui-server",
3
- "version": "0.310.0",
3
+ "version": "0.313.0",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack WebUI HTTP/WebSocket server module — extracted from @wrongstack/webui in PR #243/244 to remove the CLI -> @wrongstack/webui/server cross-package edge (audit §3.1.1). Pure backend: HTTP routes, WebSocket handlers, MCP tool wrappers, HTML serving. The web frontend lives in @wrongstack/webui; this package is the standalone server it can run on.",
6
6
  "keywords": [
@@ -36,18 +36,19 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "ws": "^8.21.3",
39
- "@wrongstack/kanban": "0.310.0",
40
- "@wrongstack/requirement-intake": "0.310.0",
41
- "@wrongstack/providers": "0.310.0",
42
- "@wrongstack/runtime": "0.310.0",
43
- "@wrongstack/sdd": "0.310.0",
44
- "@wrongstack/core": "0.310.0",
45
- "@wrongstack/sage": "0.310.0",
46
- "@wrongstack/techstack": "0.310.0",
47
- "@wrongstack/mcp": "0.310.0",
48
- "@wrongstack/vector-memory": "0.310.0",
49
- "@wrongstack/tools": "0.310.0",
50
- "@wrongstack/webui-protocol": "0.310.0"
39
+ "@wrongstack/core": "0.313.0",
40
+ "@wrongstack/mcp": "0.313.0",
41
+ "@wrongstack/kanban": "0.313.0",
42
+ "@wrongstack/sage": "0.313.0",
43
+ "@wrongstack/runtime": "0.313.0",
44
+ "@wrongstack/providers": "0.313.0",
45
+ "@wrongstack/sdd": "0.313.0",
46
+ "@wrongstack/requirement-intake": "0.313.0",
47
+ "@wrongstack/techstack": "0.313.0",
48
+ "@wrongstack/tools": "0.313.0",
49
+ "@wrongstack/vector-memory": "0.313.0",
50
+ "@wrongstack/wrongtrace": "0.313.0",
51
+ "@wrongstack/webui-protocol": "0.313.0"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@types/node": "^26.2.0",