@vostride/agent-qa-dashboard 0.2.2 → 0.2.4

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.
@@ -1,9 +1,10 @@
1
1
  // src/server/server.ts
2
2
  import { createServer } from "node:http";
3
3
  import { randomBytes } from "node:crypto";
4
- import { readFile as readFile8, stat as stat7 } from "node:fs/promises";
5
- import { join as join5, extname, resolve as resolve4 } from "node:path";
4
+ import { readFile as readFile8, stat as stat6 } from "node:fs/promises";
5
+ import { join as join4, extname, resolve as resolve4 } from "node:path";
6
6
  import { cpus as cpus2 } from "node:os";
7
+ import { format as format3 } from "node:util";
7
8
 
8
9
  // src/db/run-owner-host.ts
9
10
  import { spawnSync } from "node:child_process";
@@ -104,8 +105,8 @@ var ownedElsewhere = (pid) => `This run belongs to another agent-qa process (pid
104
105
 
105
106
  // src/server/routes.ts
106
107
  import { randomUUID as randomUUID6 } from "node:crypto";
107
- import { readFile as readFile6, writeFile as writeFile4, stat as stat5, readdir, rm as rm5 } from "node:fs/promises";
108
- import { join as join4, isAbsolute as isAbsolute2, basename as basename5, resolve as resolve3, dirname as dirname7 } from "node:path";
108
+ import { readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
109
+ import { isAbsolute as isAbsolute2, basename as basename5, resolve as resolve3, dirname as dirname7 } from "node:path";
109
110
 
110
111
  // src/db/run-status-class.ts
111
112
  var CLASS_OF = {
@@ -302,7 +303,7 @@ var TestFileManager = class {
302
303
  constructor(workspace) {
303
304
  this.workspace = workspace;
304
305
  }
305
- // 2026-10-18 (444-33, D-65; 444-35, D-66): each list call reads the workspace itself; each file is written whole and one writer at a time.
306
+ // Each list call reads the workspace itself; each file is written whole, one writer at a time.
306
307
  underLock = createPathLock();
307
308
  async list() {
308
309
  const records = await discoverWorkspaceFiles({ workspace: this.workspace, kind: "test" });
@@ -479,8 +480,7 @@ var ConfigManager = class _ConfigManager {
479
480
  this.configPath = configPath;
480
481
  this.mask = mask;
481
482
  }
482
- // 441 GAP3-D5: a whole document may keep a marker the file already holds, as often, but never
483
- // add one. A missing file holds none.
483
+ // A whole document may keep a marker the file already holds, as often, but never add one.
484
484
  static addsShownMarker(current, next) {
485
485
  const held = shownMarkerCounts(current);
486
486
  for (const [marker, count] of shownMarkerCounts(next)) {
@@ -488,13 +488,13 @@ var ConfigManager = class _ConfigManager {
488
488
  }
489
489
  return false;
490
490
  }
491
- // 429-05 Pitfall 3: a half-edited file is named, never served as the partial config yaml recovers.
491
+ // A half-edited file is named, never served as the partial config yaml recovers.
492
492
  async readWithRevision() {
493
493
  const read = await this.readParsed();
494
494
  if (read.kind !== "ok") return read;
495
495
  return { kind: "ok", config: read.config, raw: read.raw, revision: read.revision };
496
496
  }
497
- // 441-21 D-29: the payload and the file view pass through the declared mask and nothing else.
497
+ // The payload and the file view pass through the declared mask and nothing else.
498
498
  async readMasked() {
499
499
  const read = await this.readParsed();
500
500
  if (read.kind === "absent") {
@@ -538,8 +538,7 @@ var ConfigManager = class _ConfigManager {
538
538
  revision
539
539
  };
540
540
  }
541
- // 429-01 D-19/D-22: one transaction per request, serialised so overlapping autosaves never
542
- // read the same bytes and drop each other's field.
541
+ // One transaction per request, serialised so overlapping autosaves never drop each other's field.
543
542
  async applyChanges(changes, options = {}) {
544
543
  const marked = changeWithShownMarker(changes);
545
544
  if (marked !== void 0) {
@@ -554,13 +553,11 @@ var ConfigManager = class _ConfigManager {
554
553
  }
555
554
  return this.serialized(() => this.applyChangesOnce(changes, options.capturePriors === true));
556
555
  }
557
- // 429-11 D-24: an Undo is one more transaction on the same mutex, newest change first. A field
558
- // that no longer holds what the change left there was written since, and is never overwritten.
556
+ // An Undo restores newest first; a field written after the change is never overwritten.
559
557
  async restorePriors(priors) {
560
558
  return this.serialized(() => this.restorePriorsOnce(priors));
561
559
  }
562
- // 432-07 D-05: a generated whole-file change (a scaffold, a hooks file) publishes only over the
563
- // bytes it was built from, read again in this path's turn. Anything else is somebody's edit.
560
+ // A generated whole-file change publishes only over the bytes it is built from, re-read in turn.
564
561
  async replaceDocument(expectedRevision, content) {
565
562
  return this.serialized(async () => {
566
563
  let current;
@@ -687,8 +684,7 @@ var ConfigManager = class _ConfigManager {
687
684
  await this.writeAtomic(next);
688
685
  return capturePriors ? { kind: "ok", revision: revisionOf(next), baseRevision, priors } : { kind: "ok", revision: revisionOf(next), baseRevision };
689
686
  }
690
- // 429-05 Pitfall 8 / Pattern 1 step 6: the temp file sits beside the real file, keeps its mode
691
- // (chmod, because writeFile's mode is filtered by the umask), and replaces it in one rename.
687
+ // The temp file sits beside the real one and is chmodded, as the umask filters `writeFile` modes.
692
688
  async writeAtomic(content) {
693
689
  const target = await this.writeTarget();
694
690
  const directory = dirname3(target);
@@ -1892,6 +1888,12 @@ var HookRegistryManager = class {
1892
1888
 
1893
1889
  // src/server/body-parser.ts
1894
1890
  var DEFAULT_MAX_BYTES = 1024 * 1024;
1891
+ function carriesNonJsonBody(req) {
1892
+ const hasBody = req.headers["transfer-encoding"] !== void 0 || Number(req.headers["content-length"] ?? "0") > 0;
1893
+ if (!hasBody) return false;
1894
+ const type = req.headers["content-type"];
1895
+ return type === void 0 || type.split(";", 1)[0].trim().toLowerCase() !== "application/json";
1896
+ }
1895
1897
  function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
1896
1898
  return new Promise((resolve5, reject) => {
1897
1899
  const chunks = [];
@@ -1926,12 +1928,16 @@ function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
1926
1928
  // src/live-editor/live-session.ts
1927
1929
  import { randomUUID as randomUUID5 } from "node:crypto";
1928
1930
  import { dirname as dirname6, resolve as resolvePath2 } from "node:path";
1931
+ import { format } from "node:util";
1929
1932
  import {
1930
1933
  AUTH_STATE_SCHEMA_VERSION,
1931
1934
  MobileSetupError as MobileSetupError2,
1932
1935
  PlannerConfigSchema,
1933
1936
  SuiteDefinitionSchema,
1934
1937
  TestDefinitionSchema,
1938
+ TimeoutConfigSchema,
1939
+ getAgentQaVersion,
1940
+ printRunText,
1935
1941
  readAuthStateMetadata,
1936
1942
  resolveAuthStatePaths,
1937
1943
  writeAuthStateFiles
@@ -1960,7 +1966,7 @@ import {
1960
1966
  resolveVisionGate,
1961
1967
  visionFromConfig
1962
1968
  } from "@vostride/agent-qa-model";
1963
- import { buildProductionRefusal, distinctInFirstOccurrenceOrder, resolvePlannerLimits } from "@vostride/agent-qa-schema";
1969
+ import { resolvePlannerLimits } from "@vostride/agent-qa-schema";
1964
1970
  import { parse as parseYaml5 } from "yaml";
1965
1971
 
1966
1972
  // src/llm-model-mapping.ts
@@ -2039,6 +2045,7 @@ import { dirname as dirname5, join as join3 } from "node:path";
2039
2045
  import { parse as parseYaml4 } from "yaml";
2040
2046
  import {
2041
2047
  MobileSetupError,
2048
+ refuseRetiredLocalBindings,
2042
2049
  resolveMobileRunConfig
2043
2050
  } from "@vostride/agent-qa-core";
2044
2051
  async function loadLocalBindings(configPath) {
@@ -2046,12 +2053,10 @@ async function loadLocalBindings(configPath) {
2046
2053
  try {
2047
2054
  const content = await readFile5(filePath, "utf-8");
2048
2055
  const parsed = parseYaml4(content);
2049
- return parsed && typeof parsed === "object" ? {
2050
- devices: parsed.devices,
2051
- providers: parsed.providers,
2052
- apps: parsed.apps,
2053
- filePath
2054
- } : { filePath };
2056
+ refuseRetiredLocalBindings(parsed, filePath);
2057
+ if (!parsed || typeof parsed !== "object") return { filePath };
2058
+ const bindings = parsed;
2059
+ return { devices: bindings.devices, apps: bindings.apps, filePath };
2055
2060
  } catch (err) {
2056
2061
  if (err.code === "ENOENT") return null;
2057
2062
  throw err;
@@ -2141,44 +2146,15 @@ async function prepareMobileLiveSession(input) {
2141
2146
  appActivity: resolved.app.appActivity,
2142
2147
  deepLinkAppId: resolved.app.deepLinkAppId,
2143
2148
  appPath: resolved.app.install?.path,
2144
- browserstackApp: resolved.app.install?.browserstack,
2145
2149
  appState: resolved.appState
2146
2150
  }
2147
2151
  };
2148
2152
  }
2149
2153
 
2150
2154
  // src/live-editor/live-session.ts
2151
- var LiveProductionRefusedError = class extends Error {
2152
- productionTargetNames;
2153
- constructor(productionTargetNames) {
2154
- super(buildProductionRefusal(productionTargetNames));
2155
- this.name = "LiveProductionRefusedError";
2156
- this.productionTargetNames = distinctInFirstOccurrenceOrder(productionTargetNames);
2157
- }
2158
- };
2159
2155
  function isConfigRecord(value) {
2160
2156
  return value !== null && typeof value === "object" && !Array.isArray(value);
2161
2157
  }
2162
- function liveTargetName(targetName) {
2163
- if (typeof targetName !== "string") return void 0;
2164
- const trimmed = targetName.trim();
2165
- return trimmed.length > 0 ? trimmed : void 0;
2166
- }
2167
- async function resolveLiveTargetProduction(configManager, targetName) {
2168
- const name = liveTargetName(targetName);
2169
- if (name === void 0 || configManager === void 0) {
2170
- return { productionTargetNames: [], allowProduction: false };
2171
- }
2172
- const config = await readConfigValues(configManager);
2173
- const registry = isConfigRecord(config.registry) ? config.registry : void 0;
2174
- const targets = registry !== void 0 && isConfigRecord(registry.targets) ? registry.targets : void 0;
2175
- const declared = targets !== void 0 && Object.hasOwn(targets, name) ? targets[name] : void 0;
2176
- const production = isConfigRecord(declared) && declared.environment === "production";
2177
- return {
2178
- productionTargetNames: production ? [name] : [],
2179
- allowProduction: config.allowProduction === true
2180
- };
2181
- }
2182
2158
  async function readEntityUse(entity, readers) {
2183
2159
  if (entity === void 0) return void 0;
2184
2160
  const find = entity.type === "suite" ? readers.findSuite : readers.findTest;
@@ -2198,6 +2174,12 @@ var LivePlannerLimitsRefusedError = class extends Error {
2198
2174
  this.name = "LivePlannerLimitsRefusedError";
2199
2175
  }
2200
2176
  };
2177
+ var LiveNavigationTimeoutRefusedError = class extends Error {
2178
+ constructor(message) {
2179
+ super(message);
2180
+ this.name = "LiveNavigationTimeoutRefusedError";
2181
+ }
2182
+ };
2201
2183
  var LIVE_LIMIT_LEAVES = ["maxActions", "maxPlanningTurns"];
2202
2184
  function shownLimitValue(value) {
2203
2185
  return typeof value === "number" ? String(value) : JSON.stringify(value) ?? String(value);
@@ -2246,9 +2228,30 @@ async function resolveLivePlannerLimits(configManager, entities, readers) {
2246
2228
  }
2247
2229
  return resolvePlannerLimits(config.use, ...uses);
2248
2230
  }
2249
- function liveProductionRefusal(found) {
2250
- if (found.productionTargetNames.length === 0 || found.allowProduction) return void 0;
2251
- return new LiveProductionRefusedError(found.productionTargetNames);
2231
+ async function resolveLiveNavigationTimeoutMs(configManager, entities, readers) {
2232
+ const config = configManager === void 0 ? {} : await readConfigValues(configManager);
2233
+ const layers = [
2234
+ { layer: "project config", use: config.use }
2235
+ ];
2236
+ for (const entity of [].concat(entities ?? [])) {
2237
+ const use = await readEntityUse(entity, readers);
2238
+ layers.push({ layer: `${entity.type} file ${entity.id}`, use });
2239
+ }
2240
+ let navigation;
2241
+ for (const { layer, use } of layers) {
2242
+ if (!isConfigRecord(use) || !isConfigRecord(use.timeout)) continue;
2243
+ if (!Object.hasOwn(use.timeout, "navigation")) continue;
2244
+ const value = use.timeout.navigation;
2245
+ const parsed = TimeoutConfigSchema.shape.navigation.safeParse(value);
2246
+ if (!parsed.success) {
2247
+ const issue = parsed.error.issues[0]?.message ?? "invalid value";
2248
+ throw new LiveNavigationTimeoutRefusedError(
2249
+ `${layer} sets use.timeout.navigation to ${shownLimitValue(value)}: ${issue}. The CLI refuses this file too.`
2250
+ );
2251
+ }
2252
+ navigation = parsed.data;
2253
+ }
2254
+ return navigation;
2252
2255
  }
2253
2256
  var IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
2254
2257
  var LIVE_HUB_CLOSE_LIMIT_MS = 2e3;
@@ -2384,8 +2387,7 @@ var LiveSession = class {
2384
2387
  planner = null;
2385
2388
  verifier = null;
2386
2389
  modelProfiles = null;
2387
- // D-14 (Phase 360): the run-scoped storage-state store, constructed lazily on first use and
2388
- // rooted at the session's `.agent-qa` runtime dir (the FsStorageStateStore rooting idiom).
2390
+ // Built lazily on first use, rooted at the session's `.agent-qa` runtime dir.
2389
2391
  storageStateStore = null;
2390
2392
  model = null;
2391
2393
  variableStore = null;
@@ -2413,15 +2415,14 @@ var LiveSession = class {
2413
2415
  mobileAppiumLease = null;
2414
2416
  secretStore;
2415
2417
  secretRedactor;
2416
- // 428 D-07: one hub per session, handle 0, created once the adapter exists. A session whose
2417
- // setup hooks failed has no adapter and therefore no hub; its subject declares `none`.
2418
+ // One hub per session at handle 0, created once the adapter exists; no adapter means no hub.
2418
2419
  liveHub = null;
2419
2420
  attachment = null;
2420
2421
  resolveEnded = () => {
2421
2422
  };
2422
2423
  ended;
2423
2424
  cleanupRun = null;
2424
- // RESEARCH P4: which control socket is the current one. Only its close may detach the session.
2425
+ // Which control socket is the current one; only its close may detach the session.
2425
2426
  attachmentGeneration = 0;
2426
2427
  releaseAttachment = null;
2427
2428
  get modelName() {
@@ -2443,11 +2444,7 @@ var LiveSession = class {
2443
2444
  hub.register(0, registration?.capability, registration?.fallback);
2444
2445
  this.liveHub = hub;
2445
2446
  }
2446
- /**
2447
- * Make a control socket the current one. `release` is how the session lets go of it later: the
2448
- * previous attachment's is called with `superseded` here (428 WR-01), so one socket drives the
2449
- * session at a time.
2450
- */
2447
+ // The previous attachment's `release` gets `superseded`, so one socket drives the session.
2451
2448
  beginAttachment(release) {
2452
2449
  const previous = this.releaseAttachment;
2453
2450
  this.attachmentGeneration += 1;
@@ -2466,11 +2463,7 @@ var LiveSession = class {
2466
2463
  }
2467
2464
  this.pendingMessages = [];
2468
2465
  }
2469
- /**
2470
- * A command's reply, for whichever control socket is attached now (428 CR-01). A command can
2471
- * outlive the socket that issued it, and its outcome belongs to the page, not to that socket:
2472
- * with none attached it is held and flushed to the next one.
2473
- */
2466
+ // A reply can outlive its socket, so it goes to the current socket or waits for the next one.
2474
2467
  deliver(message) {
2475
2468
  if (this.messageSink) {
2476
2469
  this.messageSink(message);
@@ -2581,7 +2574,7 @@ var LiveSession = class {
2581
2574
  continue;
2582
2575
  }
2583
2576
  const result = await runHooks([hookDef], {
2584
- executor: new DockerHookExecutor(),
2577
+ executor: new DockerHookExecutor({ version: getAgentQaVersion() }),
2585
2578
  env: this.getHookEnvVars(),
2586
2579
  secretStore: this.secretStore,
2587
2580
  secretRedactor: this.secretRedactor
@@ -2655,7 +2648,6 @@ var LiveSession = class {
2655
2648
  device: config.device ? {
2656
2649
  name: config.device.name,
2657
2650
  platform,
2658
- transport: "local",
2659
2651
  match: config.device
2660
2652
  } : void 0
2661
2653
  };
@@ -2720,15 +2712,6 @@ var LiveSession = class {
2720
2712
  }
2721
2713
  }
2722
2714
  async initialize(config) {
2723
- const found = await resolveLiveTargetProduction(this.deps.configManager, config.targetName);
2724
- const refusal = liveProductionRefusal({
2725
- productionTargetNames: distinctInFirstOccurrenceOrder([
2726
- ...config.production?.productionTargetNames ?? [],
2727
- ...found.productionTargetNames
2728
- ]),
2729
- allowProduction: found.allowProduction
2730
- });
2731
- if (refusal !== void 0) throw refusal;
2732
2715
  this.platform = config.platform;
2733
2716
  this.targetName = typeof config.targetName === "string" && config.targetName.trim().length > 0 ? config.targetName.trim() : null;
2734
2717
  this._modelName = config.llmConfig.model ?? "unknown";
@@ -2763,7 +2746,11 @@ var LiveSession = class {
2763
2746
  } else {
2764
2747
  const { WebPlatformAdapter } = await import("@vostride/agent-qa-web");
2765
2748
  const adapter = new WebPlatformAdapter();
2766
- await adapter.setup({ platform: "web", browser: { name: "chromium", headless: config.headless ?? true } });
2749
+ await adapter.setup({
2750
+ platform: "web",
2751
+ browser: { name: "chromium", headless: config.headless ?? true },
2752
+ ...config.navigationTimeoutMs === void 0 ? {} : { timeouts: { navigation: config.navigationTimeoutMs } }
2753
+ });
2767
2754
  this.adapter = adapter;
2768
2755
  if (config.url) {
2769
2756
  const resolvedUrl = this.resolveSecrets(config.url);
@@ -2825,7 +2812,7 @@ var LiveSession = class {
2825
2812
  }
2826
2813
  const startedAt = performance.now();
2827
2814
  try {
2828
- const result = await new DockerHookExecutor().execute(hookDef, {
2815
+ const result = await new DockerHookExecutor({ version: getAgentQaVersion() }).execute(hookDef, {
2829
2816
  env: this.getHookEnvVars(),
2830
2817
  secretEnv: resolveSecretEnv(this.secretStore),
2831
2818
  secretRedactor: this.secretRedactor
@@ -2992,12 +2979,7 @@ var LiveSession = class {
2992
2979
  };
2993
2980
  return this.redact(result);
2994
2981
  }
2995
- // Inner step execution — no gating. Callers must own the executing/status
2996
- // lifecycle (executeStepCommand for single steps, executeTestCommand for
2997
- // the per-test loop that runs many steps under one outer gate).
2998
- // D-14 (Phase 360): lazily construct the run-scoped storage-state store, rooted at the session's
2999
- // `.agent-qa` runtime dir. All fs writes live in runner-local (validate:arch storageNeutral);
3000
- // the loop records only { name, fingerprint }. Absent configPath falls back to the cwd root.
2982
+ // Lazily built and rooted at the session's `.agent-qa` dir; fs writes stay in runner-local.
3001
2983
  async getStorageStateStore() {
3002
2984
  let store = this.storageStateStore;
3003
2985
  if (!store) {
@@ -3136,7 +3118,9 @@ ${errorLines.join("\n")}`;
3136
3118
  testLimits = this.plannerLimitsForTest !== void 0 && testDraft.testId.length > 0 ? await this.plannerLimitsForTest(testDraft.testId) : this.plannerLimits ?? resolvePlannerLimits();
3137
3119
  } catch (error) {
3138
3120
  if (!(error instanceof LivePlannerLimitsRefusedError)) throw error;
3139
- console.warn(`[live-session] executeTestCommand: ${error.message}`, { testName: testDraft.testName });
3121
+ const line = `[live-session] executeTestCommand: ${error.message}`;
3122
+ const detail = { testName: testDraft.testName };
3123
+ printRunText(format(line, detail), () => console.warn(line, detail));
3140
3124
  return this.redact({
3141
3125
  status: "failed",
3142
3126
  duration: performance.now() - start,
@@ -3156,11 +3140,13 @@ ${errorLines.join("\n")}`;
3156
3140
  if (!setupResult.allPassed) {
3157
3141
  const failedHook = setupHookPayloads.find((h) => h.status === "failed");
3158
3142
  const detail = failedHook ? `Setup hook "${failedHook.hookName}" failed${failedHook.error ? `: ${failedHook.error}` : ""}${failedHook.stderr ? ` \u2014 ${failedHook.stderr.split("\n")[0].slice(0, 200)}` : ""}` : "Per-test setup hook failed";
3159
- console.warn(`[live-session] executeTestCommand: ${detail}`, {
3143
+ const line = `[live-session] executeTestCommand: ${detail}`;
3144
+ const context = {
3160
3145
  testName: testDraft.testName,
3161
3146
  setupHooks: testDraft.setup,
3162
3147
  failedHook: failedHook ? { name: failedHook.hookName, stderr: failedHook.stderr, error: failedHook.error } : null
3163
- });
3148
+ };
3149
+ printRunText(format(line, context), () => console.warn(line, context));
3164
3150
  return {
3165
3151
  status: "failed",
3166
3152
  duration: performance.now() - start,
@@ -3238,7 +3224,9 @@ ${errorLines.join("\n")}`;
3238
3224
  topLevelError = failedHook ? `Teardown hook "${failedHook.hookName}" failed${failedHook.error ? `: ${failedHook.error}` : ""}` : "Teardown hook failed";
3239
3225
  }
3240
3226
  if (topLevelError) {
3241
- console.warn(`[live-session] executeTestCommand: ${topLevelError}`, { testName: testDraft.testName });
3227
+ const line = `[live-session] executeTestCommand: ${topLevelError}`;
3228
+ const detail = { testName: testDraft.testName };
3229
+ printRunText(format(line, detail), () => console.warn(line, detail));
3242
3230
  }
3243
3231
  return this.redact({
3244
3232
  status: finalStatus,
@@ -3254,10 +3242,7 @@ ${errorLines.join("\n")}`;
3254
3242
  this.status = "idle";
3255
3243
  }
3256
3244
  }
3257
- // Thin wrapper around runLifecycleHooks that also captures the dispatched
3258
- // hook payloads (hook-complete) for inclusion in the LiveTestResultPayload.
3259
- // The capture sits beside the message sink rather than wrapping it, so a
3260
- // control socket that reattaches mid-hook keeps its sink (428 CR-01).
3245
+ // Captures dispatched hook payloads beside the message sink, so a reattached socket keeps it.
3261
3246
  async runLifecycleHooksCapturing(phase, hookIds, sink, owner = { scope: "suite" }) {
3262
3247
  this.hookCaptures.add(sink);
3263
3248
  try {
@@ -3501,6 +3486,21 @@ function refuseUnparseableRequest(res) {
3501
3486
  });
3502
3487
  res.end(body);
3503
3488
  }
3489
+ function refuseForeignHostPage(res, port) {
3490
+ if (res.headersSent) {
3491
+ res.destroy();
3492
+ return;
3493
+ }
3494
+ const p = String(port);
3495
+ const body = `The agent-qa dashboard answers only at localhost, 127.0.0.1 or [::1].
3496
+ Open http://localhost:${p}/ on the machine it runs on, or forward the port with ssh -L ${p}:localhost:${p} <host>.
3497
+ `;
3498
+ res.writeHead(403, {
3499
+ "Content-Type": "text/plain; charset=utf-8",
3500
+ "Content-Length": Buffer.byteLength(body)
3501
+ });
3502
+ res.end(body);
3503
+ }
3504
3504
  function backstopServerError(res, req, error) {
3505
3505
  const kind = error instanceof Error ? error.constructor.name : typeof error;
3506
3506
  console.error(
@@ -3606,10 +3606,6 @@ function respondJson(res, status, data) {
3606
3606
  res.end(body);
3607
3607
  }
3608
3608
  function serveRunEvidenceRead(req, res, deps) {
3609
- if (deps.boundary === "human" && refuseCrossSiteRequest(req)) {
3610
- respondJson(res, 403, { error: "cross_site_refused" });
3611
- return;
3612
- }
3613
3609
  const query = new URL(req.url ?? "/", "http://localhost").searchParams;
3614
3610
  const entries = [["runId", deps.runId]];
3615
3611
  const seen = /* @__PURE__ */ new Set(["runId"]);
@@ -3651,10 +3647,6 @@ function respondFeedbackRead(res, deps, functionId, input, parse) {
3651
3647
  });
3652
3648
  }
3653
3649
  function serveRunMarksHistoryRead(req, res, deps) {
3654
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
3655
- respondJson(res, 403, { error: "cross_site_refused" });
3656
- return;
3657
- }
3658
3650
  if (new URL(req.url ?? "/", "http://localhost").search !== "") {
3659
3651
  respondJson(res, 400, { error: "invalid_marks_input" });
3660
3652
  return;
@@ -3673,10 +3665,6 @@ function serveRunMarksHistoryRead(req, res, deps) {
3673
3665
  );
3674
3666
  }
3675
3667
  function serveRunMarkPreviewRead(req, res, deps) {
3676
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
3677
- respondJson(res, 403, { error: "cross_site_refused" });
3678
- return;
3679
- }
3680
3668
  void readJsonBody(req).then((body) => {
3681
3669
  const parsed = RunsMarkInputSchema.safeParse(body);
3682
3670
  if (!parsed.success) {
@@ -3699,10 +3687,6 @@ function serveRunMarkPreviewRead(req, res, deps) {
3699
3687
  });
3700
3688
  }
3701
3689
  function serveRunDetailRead(req, res, deps) {
3702
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
3703
- respondJson(res, 403, { error: "cross_site_refused" });
3704
- return;
3705
- }
3706
3690
  void readRunDetail(deps.host, deps.runId).then(
3707
3691
  (outcome) => {
3708
3692
  if (outcome.kind === "refused") {
@@ -3854,10 +3838,6 @@ async function readRunAnalysis(host, runId) {
3854
3838
  }
3855
3839
  }
3856
3840
  function serveRunAnalysisRead(req, res, deps) {
3857
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
3858
- respondJson(res, 403, { error: "cross_site_refused" });
3859
- return;
3860
- }
3861
3841
  void readRunAnalysis(deps.host, deps.runId).then(
3862
3842
  (outcome) => {
3863
3843
  if (outcome.kind === "refused") {
@@ -4081,10 +4061,7 @@ function serveRunEventStream(req, res, deps) {
4081
4061
  else pending.push(frame);
4082
4062
  };
4083
4063
  const frameOf = (event) => (
4084
- // D-13, all three parts: the id IS the seq, the event name IS the envelope type, and the
4085
- // data IS the envelope's own JSON. No projection, no rename, no second vocabulary. The filter
4086
- // DROPS envelopes; it never edits one, so a filtered stream's frames are byte-identical to the
4087
- // firehose's — with gaps in the ids, which is what seq-as-id means once a filter exists.
4064
+ // The filter drops envelopes and never edits one, so filtered frames match the firehose bytes.
4088
4065
  `id: ${String(event.seq)}
4089
4066
  event: ${event.type}
4090
4067
  data: ${JSON.stringify(event)}
@@ -4146,12 +4123,6 @@ data: ${JSON.stringify(event)}
4146
4123
  deps.openStreams?.add(teardown);
4147
4124
  req.on("close", teardown);
4148
4125
  }
4149
- function runDenialOf(error) {
4150
- if (typeof error !== "object" || error === null) return void 0;
4151
- const neutralReason = error.neutralReason;
4152
- if (typeof neutralReason !== "string" || neutralReason.trim().length === 0) return void 0;
4153
- return { error: "run_denied", neutralReason };
4154
- }
4155
4126
  function createRunServiceRoutes(ports) {
4156
4127
  const openStreams = /* @__PURE__ */ new Set();
4157
4128
  function handleStart(req, res) {
@@ -4182,12 +4153,7 @@ function createRunServiceRoutes(ports) {
4182
4153
  ack = await ports.invokeRunStart(
4183
4154
  parsed.data.patterns === void 0 ? {} : { patterns: parsed.data.patterns }
4184
4155
  );
4185
- } catch (error) {
4186
- const denial = runDenialOf(error);
4187
- if (denial !== void 0) {
4188
- respondJson2(res, 403, denial);
4189
- return;
4190
- }
4156
+ } catch {
4191
4157
  refuse(res, 500, "run_start_failed");
4192
4158
  return;
4193
4159
  }
@@ -4267,6 +4233,10 @@ function createRunServiceRoutes(ports) {
4267
4233
  refuse(res, 403, "cross_site_refused");
4268
4234
  return;
4269
4235
  }
4236
+ if (carriesNonJsonBody(req)) {
4237
+ refuse(res, 415, "unsupported_media_type");
4238
+ return;
4239
+ }
4270
4240
  const matched = matchRunServiceRoute(req.method, path);
4271
4241
  if (matched === void 0) {
4272
4242
  refuse(res, 404, "unknown_route");
@@ -4534,8 +4504,7 @@ data: ${JSON.stringify(again.body)}
4534
4504
  // A failed re-read is not a reason to close a live channel: the next envelope will trigger
4535
4505
  // another, and the page's own poll is the floor underneath both.
4536
4506
  () => void 0
4537
- // …but a throw from the arm ABOVE is not a failed re-read, it is a dead socket, and `.then`'s
4538
- // failure arm never sees its own fulfilment arm's throw (UF-01, on the nested chain).
4507
+ // A throw from the arm above is a dead socket, and `.then`'s failure arm never sees it.
4539
4508
  ).catch(failStream);
4540
4509
  });
4541
4510
  heartbeat = setInterval(() => {
@@ -4574,10 +4543,6 @@ async function readRunKnowledge(host, runId) {
4574
4543
  }
4575
4544
  }
4576
4545
  function serveRunKnowledgeRead(req, res, deps) {
4577
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
4578
- respondJson(res, 403, { error: "cross_site_refused" });
4579
- return;
4580
- }
4581
4546
  void readRunKnowledge(deps.host, deps.runId).then(
4582
4547
  (outcome) => {
4583
4548
  if (outcome.kind === "refused") {
@@ -4620,10 +4585,6 @@ async function markRun(host, runId, body) {
4620
4585
  }
4621
4586
  }
4622
4587
  function registerRunMarkRoute(req, res, deps) {
4623
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
4624
- respondJson(res, 403, { error: "cross_site_refused" });
4625
- return;
4626
- }
4627
4588
  void readJsonBody(req).then(async (body) => {
4628
4589
  const outcome = await markRun(deps.host, deps.runId, body);
4629
4590
  if (outcome.kind === "refused") {
@@ -4646,10 +4607,6 @@ import {
4646
4607
  isPageWritableConfigPath
4647
4608
  } from "@vostride/agent-qa-schema";
4648
4609
  function registerConfigWriteRoute(req, res, deps) {
4649
- if (refuseCrossSiteRequest(req) !== void 0) {
4650
- respondJson(res, 403, { error: "cross_site_refused" });
4651
- return;
4652
- }
4653
4610
  void readJsonBody(req).then(async (body) => {
4654
4611
  const parsed = ConfigWriteRequestSchema.safeParse(body);
4655
4612
  if (!parsed.success) {
@@ -4707,10 +4664,6 @@ function registerConfigUndoRoute(req, res, deps) {
4707
4664
  respondJson(res, 405, { error: "method_not_allowed" });
4708
4665
  return;
4709
4666
  }
4710
- if (refuseCrossSiteRequest(req) !== void 0) {
4711
- respondJson(res, 403, { error: "cross_site_refused" });
4712
- return;
4713
- }
4714
4667
  void readJsonBody(req).then(async (body) => {
4715
4668
  const parsed = ConfigUndoRequestSchema.safeParse(body);
4716
4669
  if (!parsed.success) {
@@ -4953,10 +4906,6 @@ async function connectName(request2, deps) {
4953
4906
  });
4954
4907
  }
4955
4908
  function registerLlmConnectRoute(req, res, deps) {
4956
- if (refuseCrossSiteRequest(req) !== void 0) {
4957
- respondJson(res, 403, { error: "cross_site_refused" });
4958
- return;
4959
- }
4960
4909
  void readJsonBody(req).then(async (body) => {
4961
4910
  const answer = await connectLlm(body, deps);
4962
4911
  respondJson(res, answer.status, answer.body);
@@ -4968,9 +4917,9 @@ function registerLlmConnectRoute(req, res, deps) {
4968
4917
  }
4969
4918
 
4970
4919
  // src/server/routes.ts
4971
- import { AuthStateNameSchema, buildInternalRunAttributes, mergeRunAttributes, readAuth as readAuth2, writeAuth as writeAuth2, removeAuth as removeAuth2, getAgentQaVersion, getAgentQaUpdateStatus, getLLMAuthProviderPlugin as getLLMAuthProviderPlugin2, listAuthStateMetadata, listLLMAuthProviderPlugins, ModelConfigSchema as ModelConfigSchema2, NameSlugSchema as NameSlugSchema2, TimeoutConfigSchema, AgentQaConfigSchema as AgentQaConfigSchema3, TestDefinitionSchema as TestDefinitionSchema2, SuiteDefinitionSchema as SuiteDefinitionSchema2, parseEnvFile, serializeEnvFile, RUNTIME_IMAGE_MAP, validateUserRunAttributes, discoverWorkspaceFiles as discoverWorkspaceFiles3, isWorkspacePathMatch, resolveWorkspaceFileTarget as resolveWorkspaceFileTarget2, SizeString } from "@vostride/agent-qa-core";
4920
+ import { AuthStateNameSchema, buildInternalRunAttributes, DEFAULT_AGENT_QA_CACHE_DIR, mergeRunAttributes, readAuth as readAuth2, writeAuth as writeAuth2, removeAuth as removeAuth2, getAgentQaVersion as getAgentQaVersion2, getAgentQaUpdateStatus, getLLMAuthProviderPlugin as getLLMAuthProviderPlugin2, listAuthStateMetadata, listLLMAuthProviderPlugins, ModelConfigSchema as ModelConfigSchema2, NameSlugSchema as NameSlugSchema2, TimeoutConfigSchema as TimeoutConfigSchema2, AgentQaConfigSchema as AgentQaConfigSchema3, TestDefinitionSchema as TestDefinitionSchema2, SuiteDefinitionSchema as SuiteDefinitionSchema2, parseEnvFile, serializeEnvFile, hookRunnerImage, validateUserRunAttributes, discoverWorkspaceFiles as discoverWorkspaceFiles3, isWorkspacePathMatch, resolveWorkspaceFileTarget as resolveWorkspaceFileTarget2, SizeString } from "@vostride/agent-qa-core";
4972
4921
  import { SecretStore, SecretRedactor } from "@vostride/agent-qa-runner";
4973
- import { DockerHookExecutor as DockerHookExecutor2 } from "@vostride/agent-qa-runner-local";
4922
+ import { DockerHookExecutor as DockerHookExecutor2, purgeActionCache } from "@vostride/agent-qa-runner-local";
4974
4923
  import { createModel as createModel3, fetchLiveModels, getModelCatalog, listProviders, mergeCatalog, resolveVisionConfig } from "@vostride/agent-qa-model";
4975
4924
  import { parse as parseYaml6 } from "yaml";
4976
4925
  var LLM_PROVIDER_MODES = /* @__PURE__ */ new Set([
@@ -5278,25 +5227,10 @@ function llmConnectionTestTimeoutMs(config) {
5278
5227
  return REMOTE_LLM_CONNECTION_TEST_TIMEOUT_MS;
5279
5228
  }
5280
5229
  function json(res, data, status = 200) {
5281
- const body = JSON.stringify(data);
5282
- res.writeHead(status, {
5283
- "Content-Type": "application/json",
5284
- "Access-Control-Allow-Origin": "*",
5285
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
5286
- "Access-Control-Allow-Headers": "Content-Type"
5287
- });
5288
- res.end(body);
5289
- }
5290
- function jsonNoCors(res, data, status = 200) {
5291
5230
  const body = JSON.stringify(data);
5292
5231
  res.writeHead(status, { "Content-Type": "application/json" });
5293
5232
  res.end(body);
5294
5233
  }
5295
- function refuseCrossSiteDisplayRead(req, res) {
5296
- if (refuseCrossSiteRequest(req) === void 0) return false;
5297
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
5298
- return true;
5299
- }
5300
5234
  var INSIGHTS_KNOWLEDGE_RUN_CAP = 200;
5301
5235
  var INSIGHTS_KNOWLEDGE_CONCURRENCY = 4;
5302
5236
  async function isPostProcessingSkipped(host, runId) {
@@ -5360,14 +5294,6 @@ async function readInsightsKnowledgeChanges(host, db, window) {
5360
5294
  function notFound(res, message = "Not found") {
5361
5295
  json(res, { error: message }, 404);
5362
5296
  }
5363
- function cors(res) {
5364
- res.writeHead(204, {
5365
- "Access-Control-Allow-Origin": "*",
5366
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
5367
- "Access-Control-Allow-Headers": "Content-Type"
5368
- });
5369
- res.end();
5370
- }
5371
5297
  function isPlainRecord(value) {
5372
5298
  return value !== null && typeof value === "object" && !Array.isArray(value);
5373
5299
  }
@@ -5420,7 +5346,7 @@ function normalizeRunFilterValue(value) {
5420
5346
  function buildRunRequestAttributes(input) {
5421
5347
  const internal = buildInternalRunAttributes({
5422
5348
  trigger: input.trigger,
5423
- runner: input.runner ?? "local"
5349
+ runner: "local"
5424
5350
  });
5425
5351
  const user = validateUserRunAttributes(input.userAttributes, "run attributes");
5426
5352
  return mergeRunAttributes(internal, user);
@@ -5555,7 +5481,7 @@ function parseTimeoutConfigTestTimeoutMs(timeoutConfig) {
5555
5481
  return transformedTimeout;
5556
5482
  }
5557
5483
  }
5558
- const parsed = TimeoutConfigSchema.partial().safeParse(timeoutConfig);
5484
+ const parsed = TimeoutConfigSchema2.partial().safeParse(timeoutConfig);
5559
5485
  if (!parsed.success) return void 0;
5560
5486
  const testTimeout = parsed.data.test;
5561
5487
  return typeof testTimeout === "number" && Number.isFinite(testTimeout) && testTimeout > 0 ? testTimeout : void 0;
@@ -5865,9 +5791,7 @@ var RESPONSE_SAFE_RUN_METADATA_KEYS = /* @__PURE__ */ new Set([
5865
5791
  "timeoutSource",
5866
5792
  "timeoutBaseMs",
5867
5793
  "timeoutBufferMs",
5868
- "suiteIndex",
5869
- "farmSessionUrl",
5870
- "runDevice"
5794
+ "suiteIndex"
5871
5795
  ]);
5872
5796
  function sanitizeRunMetadataForResponse(metadata) {
5873
5797
  if (metadata === null || metadata === void 0) return metadata ?? null;
@@ -5897,17 +5821,7 @@ var RESPONSE_SAFE_RUN_READ_METHODS = [
5897
5821
  "getRunsByTest"
5898
5822
  ];
5899
5823
  var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
5900
- // 2026-08-27 (405-01, D-01/D-03): the v2 projection's six members.
5901
- //
5902
- // The five WRITES return nothing; they are here for the same reason `insertRun` is.
5903
- //
5904
- // `readRunDetailRows` IS A READ AND IS STILL HERE, and that is the judgement this set exists to
5905
- // make explicit rather than to hide. It returns members, steps and sub-actions — the bus
5906
- // payloads as the subscriber MASKED them (T-363-05) — and NO `RunRow` and no
5907
- // `RunArtifactRow`. It therefore carries none of the run metadata the 401-23 scrub law exists
5908
- // to strip, so wrapping it would be wrapping a shape that has nothing to strip. This router
5909
- // does not call it either: the detail arm dispatches through the injected host, which is the
5910
- // whole of D-03.
5824
+ // `readRunDetailRows` returns masked bus payloads and no run row, so it has no metadata to strip.
5911
5825
  "deleteMemberProjection",
5912
5826
  "incrementRunMemberAttempts",
5913
5827
  "insertRunStep",
@@ -5918,23 +5832,23 @@ var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
5918
5832
  "finalizeRunArtifact",
5919
5833
  "getRunsBySuiteIdCount",
5920
5834
  "getRunsByTestCount",
5921
- // 433-30: run ids and a count, no row; each id is dispatched through the agent host.
5835
+ // Run ids and a count, no row; each id is dispatched through the agent host.
5922
5836
  "listInsightsWindowRunIds",
5923
5837
  "insertPendingRun",
5924
5838
  "insertRun",
5925
5839
  "insertRunArtifact",
5926
5840
  "listRunAttributeKeys",
5927
5841
  "listRunAttributeValues",
5928
- // 2026-10-16 (442-01, D-01): an owner (pid, incarnation, host), no run metadata.
5842
+ // An owner (pid, incarnation, host), no run metadata.
5929
5843
  "readRunOwner",
5930
- // 2026-10-16 (442-01, D-04): ids and owners only, no run metadata.
5844
+ // Ids and owners only, no run metadata.
5931
5845
  "listRunningRunOwners",
5932
- // 2026-10-16 (442-01, D-04): compare-and-set writes that return a boolean.
5846
+ // Compare-and-set writes that return a boolean.
5933
5847
  "finalizeOrphanRun",
5934
5848
  "finalizeOwnRun",
5935
- // 2026-10-17 (443-13, GAP-D1): a compare-and-set write that returns a boolean, no run metadata.
5849
+ // A compare-and-set write that returns a boolean, no run metadata.
5936
5850
  "closeOwnRun",
5937
- // 2026-10-17 (443-02, D-07): a compare-and-set write that returns a boolean, no run metadata.
5851
+ // A compare-and-set write that returns a boolean, no run metadata.
5938
5852
  "claimRunningRow",
5939
5853
  "stageRunArtifact",
5940
5854
  "updateRun",
@@ -6005,10 +5919,7 @@ function withResponseSafeRunReads(db) {
6005
5919
  // `sanitizeArtifactForResponse`; a caller reaching for the bare row has no such step, so the
6006
5920
  // projection is its only choke point. Same allowlist, so the two cannot drift.
6007
5921
  getRunArtifact: { value: (runId) => scrubArtifact(db.getRunArtifact(runId)) },
6008
- // 405-07 (D-02): the bundle is ONE artifact now. It used to carry the child runs of a suite
6009
- // parent, whose `run` rows this override scrubbed; nothing has written a child row since
6010
- // 402-09, and the shape went with the column. `getRunArtifact` above remains the choke point
6011
- // for the payload itself, and this wrapper delegates to it.
5922
+ // One artifact; `getRunArtifact` is the choke point for its payload, and this delegates to it.
6012
5923
  getRunArtifactBundle: {
6013
5924
  value: (runId) => ({ artifact: scrubArtifact(db.getRunArtifactBundle(runId).artifact) })
6014
5925
  }
@@ -6156,10 +6067,18 @@ function createRouter(dbOrDeps) {
6156
6067
  json(res, { error: "Bad Request" }, 400);
6157
6068
  return;
6158
6069
  }
6070
+ if (refuseCrossSiteRequest(req) !== void 0) {
6071
+ json(res, { error: "cross_site_refused" }, 403);
6072
+ return;
6073
+ }
6074
+ if (carriesNonJsonBody(req)) {
6075
+ json(res, { error: "unsupported_media_type" }, 415);
6076
+ return;
6077
+ }
6159
6078
  const path = url.pathname;
6160
6079
  if (path === "/api/config" && req.method === "PATCH") {
6161
6080
  if (!configManager) {
6162
- jsonNoCors(res, { error: "config_unavailable" }, 503);
6081
+ json(res, { error: "config_unavailable" }, 503);
6163
6082
  return;
6164
6083
  }
6165
6084
  registerConfigWriteRoute(req, res, { configManager, undoStore });
@@ -6179,7 +6098,7 @@ function createRouter(dbOrDeps) {
6179
6098
  }
6180
6099
  if (path === "/api/config/revision" && req.method === "GET") {
6181
6100
  if (!configManager) {
6182
- jsonNoCors(res, { error: "config_unavailable" }, 503);
6101
+ json(res, { error: "config_unavailable" }, 503);
6183
6102
  return;
6184
6103
  }
6185
6104
  registerConfigRevisionRoute(req, res, { configManager });
@@ -6187,7 +6106,7 @@ function createRouter(dbOrDeps) {
6187
6106
  }
6188
6107
  if (path === "/api/llm/connect" && req.method === "POST") {
6189
6108
  if (!configManager) {
6190
- jsonNoCors(res, { error: "config_unavailable" }, 503);
6109
+ json(res, { error: "config_unavailable" }, 503);
6191
6110
  return;
6192
6111
  }
6193
6112
  registerLlmConnectRoute(req, res, {
@@ -6199,16 +6118,6 @@ function createRouter(dbOrDeps) {
6199
6118
  });
6200
6119
  return;
6201
6120
  }
6202
- if (path === "/api/runs/trigger" || path === "/api/queue/enqueue") {
6203
- if (refuseCrossSiteRequest(req) !== void 0) {
6204
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
6205
- return;
6206
- }
6207
- }
6208
- if (req.method === "OPTIONS") {
6209
- cors(res);
6210
- return;
6211
- }
6212
6121
  if (path === "/api/auth-states" && req.method === "GET") {
6213
6122
  if (!configManager || !deps.configPath) {
6214
6123
  json(res, { error: "Auth-state metadata not available" }, 503);
@@ -6232,11 +6141,10 @@ function createRouter(dbOrDeps) {
6232
6141
  return;
6233
6142
  }
6234
6143
  if (path === "/api/runs" && req.method === "GET") {
6235
- if (refuseCrossSiteDisplayRead(req, res)) return;
6236
6144
  const statuses = url.searchParams.getAll("status");
6237
6145
  const unknownStatus = statuses.find((word) => !isRunStatusWord(word));
6238
6146
  if (unknownStatus !== void 0) {
6239
- jsonNoCors(res, { error: `Unknown run status: ${unknownStatus}.` }, 400);
6147
+ json(res, { error: `Unknown run status: ${unknownStatus}.` }, 400);
6240
6148
  return;
6241
6149
  }
6242
6150
  const name = url.searchParams.get("name") ?? void 0;
@@ -6268,12 +6176,12 @@ function createRouter(dbOrDeps) {
6268
6176
  )).sort((a, b) => a.localeCompare(b));
6269
6177
  const filtered = platformFilteredRuns.filter((run) => matchesTargetFilter(run, target));
6270
6178
  const runs = filtered.slice(offset, offset + limit);
6271
- jsonNoCors(res, {
6179
+ json(res, {
6272
6180
  runs,
6273
6181
  total: filtered.length,
6274
6182
  targets: targetOptions
6275
6183
  });
6276
- }).catch(() => jsonNoCors(res, { error: "Failed to list runs" }, 500));
6184
+ }).catch(() => json(res, { error: "Failed to list runs" }, 500));
6277
6185
  return;
6278
6186
  }
6279
6187
  if (path === "/api/runs/attributes/keys" && req.method === "GET") {
@@ -6311,7 +6219,6 @@ function createRouter(dbOrDeps) {
6311
6219
  }
6312
6220
  const runArtifactMatch = path.match(/^\/api\/runs\/([^/]+)\/artifact$/);
6313
6221
  if (runArtifactMatch && req.method === "GET") {
6314
- if (refuseCrossSiteDisplayRead(req, res)) return;
6315
6222
  const runId = decodeURIComponent(runArtifactMatch[1]);
6316
6223
  const run = db.getRun(runId);
6317
6224
  if (!run) {
@@ -6319,7 +6226,7 @@ function createRouter(dbOrDeps) {
6319
6226
  return;
6320
6227
  }
6321
6228
  const bundle = db.getRunArtifactBundle(runId);
6322
- jsonNoCors(res, {
6229
+ json(res, {
6323
6230
  run,
6324
6231
  artifact: sanitizeArtifactForResponse(bundle.artifact),
6325
6232
  missingSections: getArtifactMissingSections(bundle.artifact)
@@ -6327,7 +6234,6 @@ function createRouter(dbOrDeps) {
6327
6234
  return;
6328
6235
  }
6329
6236
  if (path.startsWith("/api/runs/") && path.endsWith("/execution-logs") && req.method === "GET") {
6330
- if (refuseCrossSiteDisplayRead(req, res)) return;
6331
6237
  const segments = path.split("/");
6332
6238
  const id = segments[3];
6333
6239
  const run = db.getRun(id);
@@ -6338,37 +6244,37 @@ function createRouter(dbOrDeps) {
6338
6244
  const stepId = url.searchParams.get("stepId") ?? void 0;
6339
6245
  const type = url.searchParams.get("type") ?? void 0;
6340
6246
  const logs = db.getExecutionLogs({ runId: id, stepId, type });
6341
- jsonNoCors(res, { logs });
6247
+ json(res, { logs });
6342
6248
  return;
6343
6249
  }
6344
6250
  if (path.startsWith("/api/runs/") && path.endsWith("/mark") && req.method === "POST") {
6345
6251
  if (deps.agentHost === void 0) {
6346
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
6252
+ json(res, { error: "agent_host_unavailable" }, 503);
6347
6253
  return;
6348
6254
  }
6349
6255
  let runId;
6350
6256
  try {
6351
6257
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark".length));
6352
6258
  } catch {
6353
- jsonNoCors(res, { error: "invalid_mark_input" }, 400);
6259
+ json(res, { error: "invalid_mark_input" }, 400);
6354
6260
  return;
6355
6261
  }
6356
- registerRunMarkRoute(req, res, { host: deps.agentHost, runId, boundary: "human" });
6262
+ registerRunMarkRoute(req, res, { host: deps.agentHost, runId });
6357
6263
  return;
6358
6264
  }
6359
6265
  if (req.method === "POST" && /^\/api\/runs\/[^/]+\/mark-preview$/.test(path)) {
6360
6266
  if (deps.agentHost === void 0) {
6361
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
6267
+ json(res, { error: "agent_host_unavailable" }, 503);
6362
6268
  return;
6363
6269
  }
6364
6270
  let runId;
6365
6271
  try {
6366
6272
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark-preview".length));
6367
6273
  } catch {
6368
- jsonNoCors(res, { error: "invalid_run_id" }, 400);
6274
+ json(res, { error: "invalid_run_id" }, 400);
6369
6275
  return;
6370
6276
  }
6371
- serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost, boundary: "human" });
6277
+ serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost });
6372
6278
  return;
6373
6279
  }
6374
6280
  if (path.startsWith("/api/runs/") && path.endsWith("/cancel") && req.method === "POST") {
@@ -6447,44 +6353,39 @@ function createRouter(dbOrDeps) {
6447
6353
  return;
6448
6354
  }
6449
6355
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/live-url$/.test(path)) {
6450
- const crossSite = refuseCrossSiteRequest(req);
6451
- if (crossSite !== void 0) {
6452
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
6453
- return;
6454
- }
6455
6356
  if (!deps.mintLiveUrl) {
6456
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
6357
+ json(res, { error: "live_view_unavailable" }, 503);
6457
6358
  return;
6458
6359
  }
6459
6360
  let liveUrlRunId;
6460
6361
  try {
6461
6362
  liveUrlRunId = decodeURIComponent(path.slice("/api/runs/".length, -"/live-url".length));
6462
6363
  } catch {
6463
- jsonNoCors(res, { error: "unknown_run" }, 404);
6364
+ json(res, { error: "unknown_run" }, 404);
6464
6365
  return;
6465
6366
  }
6466
6367
  const rawHandle = handleParamOf(req);
6467
6368
  const parsedHandle = parseHandleParam2(rawHandle);
6468
6369
  if (parsedHandle.kind === "invalid") {
6469
- jsonNoCors(res, { error: "invalid_handle" }, 400);
6370
+ json(res, { error: "invalid_handle" }, 400);
6470
6371
  return;
6471
6372
  }
6472
6373
  const handle = parsedHandle.kind === "absent" ? 0 : parsedHandle.index;
6473
6374
  void deps.mintLiveUrl(liveUrlRunId, handle).then(
6474
6375
  (result) => {
6475
- jsonNoCors(res, result);
6376
+ json(res, result);
6476
6377
  },
6477
6378
  (error) => {
6478
6379
  const name = error instanceof Error ? error.name : "";
6479
6380
  if (name === "UnknownRunError") {
6480
- jsonNoCors(res, { error: "unknown_run" }, 404);
6381
+ json(res, { error: "unknown_run" }, 404);
6481
6382
  return;
6482
6383
  }
6483
6384
  if (name === "LiveViewUnavailableError") {
6484
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
6385
+ json(res, { error: "live_view_unavailable" }, 503);
6485
6386
  return;
6486
6387
  }
6487
- jsonNoCors(res, { error: "live_url_mint_failed" }, 500);
6388
+ json(res, { error: "live_url_mint_failed" }, 500);
6488
6389
  }
6489
6390
  );
6490
6391
  return;
@@ -6513,32 +6414,32 @@ function createRouter(dbOrDeps) {
6513
6414
  }
6514
6415
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/evidence$/.test(path)) {
6515
6416
  if (!deps.agentHost) {
6516
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
6417
+ json(res, { error: "agent_host_unavailable" }, 503);
6517
6418
  return;
6518
6419
  }
6519
6420
  let runId;
6520
6421
  try {
6521
6422
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/evidence".length));
6522
6423
  } catch {
6523
- jsonNoCors(res, { error: "invalid_run_id" }, 400);
6424
+ json(res, { error: "invalid_run_id" }, 400);
6524
6425
  return;
6525
6426
  }
6526
- serveRunEvidenceRead(req, res, { runId, host: deps.agentHost, boundary: "human" });
6427
+ serveRunEvidenceRead(req, res, { runId, host: deps.agentHost });
6527
6428
  return;
6528
6429
  }
6529
6430
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/marks$/.test(path)) {
6530
6431
  if (!deps.agentHost) {
6531
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
6432
+ json(res, { error: "agent_host_unavailable" }, 503);
6532
6433
  return;
6533
6434
  }
6534
6435
  let runId;
6535
6436
  try {
6536
6437
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/marks".length));
6537
6438
  } catch {
6538
- jsonNoCors(res, { error: "invalid_run_id" }, 400);
6439
+ json(res, { error: "invalid_run_id" }, 400);
6539
6440
  return;
6540
6441
  }
6541
- serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost, boundary: "human" });
6442
+ serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost });
6542
6443
  return;
6543
6444
  }
6544
6445
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/detail$/.test(path)) {
@@ -6553,7 +6454,7 @@ function createRouter(dbOrDeps) {
6553
6454
  json(res, { error: "invalid_run_id" }, 400);
6554
6455
  return;
6555
6456
  }
6556
- serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost, boundary: "human" });
6457
+ serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost });
6557
6458
  return;
6558
6459
  }
6559
6460
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/analysis$/.test(path)) {
@@ -6568,7 +6469,7 @@ function createRouter(dbOrDeps) {
6568
6469
  json(res, { error: "invalid_run_id" }, 400);
6569
6470
  return;
6570
6471
  }
6571
- serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost, boundary: "human" });
6472
+ serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost });
6572
6473
  return;
6573
6474
  }
6574
6475
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/knowledge$/.test(path)) {
@@ -6585,8 +6486,7 @@ function createRouter(dbOrDeps) {
6585
6486
  }
6586
6487
  serveRunKnowledgeRead(req, res, {
6587
6488
  runId: knowledgeRunId,
6588
- host: deps.agentHost,
6589
- boundary: "human"
6489
+ host: deps.agentHost
6590
6490
  });
6591
6491
  return;
6592
6492
  }
@@ -6636,14 +6536,13 @@ function createRouter(dbOrDeps) {
6636
6536
  notFound(res);
6637
6537
  return;
6638
6538
  }
6639
- if (refuseCrossSiteDisplayRead(req, res)) return;
6640
6539
  const id = segments[3];
6641
6540
  const run = db.getRun(id);
6642
6541
  if (!run) {
6643
6542
  notFound(res, "Run not found");
6644
6543
  return;
6645
6544
  }
6646
- jsonNoCors(res, { run });
6545
+ json(res, { run });
6647
6546
  return;
6648
6547
  }
6649
6548
  if (path === "/api/stats/costs" && req.method === "GET") {
@@ -6654,19 +6553,18 @@ function createRouter(dbOrDeps) {
6654
6553
  return;
6655
6554
  }
6656
6555
  if (path === "/api/insights/knowledge-changes" && req.method === "GET") {
6657
- if (refuseCrossSiteDisplayRead(req, res)) return;
6658
6556
  if (!deps.agentHost) {
6659
- jsonNoCors(res, { status: "unavailable", reason: "agent_host_unavailable" }, 503);
6557
+ json(res, { status: "unavailable", reason: "agent_host_unavailable" }, 503);
6660
6558
  return;
6661
6559
  }
6662
6560
  const from = url.searchParams.get("from") ?? void 0;
6663
6561
  const to = url.searchParams.get("to") ?? void 0;
6664
6562
  void readInsightsKnowledgeChanges(deps.agentHost, db, { from, to }).then(
6665
6563
  (body) => {
6666
- jsonNoCors(res, body);
6564
+ json(res, body);
6667
6565
  },
6668
6566
  () => {
6669
- jsonNoCors(res, { status: "unavailable", reason: "read_failed" }, 500);
6567
+ json(res, { status: "unavailable", reason: "read_failed" }, 500);
6670
6568
  }
6671
6569
  );
6672
6570
  return;
@@ -6732,10 +6630,7 @@ function createRouter(dbOrDeps) {
6732
6630
  serveRunEventStream(req, res, {
6733
6631
  runId,
6734
6632
  handleParam: handleParamOf(req),
6735
- // 403-03 (D-13 · INVK-05): the resume point, read from the SAME pair the authed door reads
6736
- // it from. `url.searchParams.get('lastEventId')` would resolve a repeated parameter to its
6737
- // first value — the one answer `lastEventIdParamOf` exists to refuse — and a second
6738
- // header/query precedence rule here would be a second contract for where a client resumes.
6633
+ // Read from the same pair as the authed door; `searchParams.get` takes a repeated first.
6739
6634
  resume: resumePointOf(req.headers["last-event-id"], lastEventIdParamOf(req)),
6740
6635
  attach: deps.runServiceAttach
6741
6636
  });
@@ -6777,7 +6672,6 @@ function createRouter(dbOrDeps) {
6777
6672
  try {
6778
6673
  attributes = buildRunRequestAttributes({
6779
6674
  trigger: "api",
6780
- runner: "local",
6781
6675
  userAttributes: body.attributes
6782
6676
  });
6783
6677
  } catch (err) {
@@ -6785,25 +6679,10 @@ function createRouter(dbOrDeps) {
6785
6679
  return;
6786
6680
  }
6787
6681
  const args = normalizedFileTarget ? [normalizedFileTarget.executionPath] : [];
6788
- const source = isSuiteFile ? "suite" : "dashboard";
6789
- try {
6790
- await executionBackend?.preflight?.({ args, source });
6791
- } catch (error) {
6792
- const denial = runDenialOf(error);
6793
- if (denial !== void 0) {
6794
- json(res, denial, 403);
6795
- return;
6796
- }
6797
- json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
6798
- return;
6799
- }
6800
6682
  const runId = jobQueue.enqueue({
6801
6683
  name: body.name,
6802
6684
  filePath: normalizedFileTarget?.storagePath,
6803
- // 2026-08-24 (402-09, D-11): NO PARENT KIND IS MINTED. A suite run is ONE run row
6804
- // with member handles, so a parent artifact kind would claim parenthood over rows
6805
- // nothing creates. `isSuiteFile` still travels in `metadata.isSuite` below, and the run's
6806
- // own `suite_id` plus the artifact's `source.kind` snapshot still say it is a suite.
6685
+ // No parent kind is minted; `metadata.isSuite` and `suite_id` say it is a suite.
6807
6686
  kind: "test",
6808
6687
  attributes,
6809
6688
  priority: body.priority,
@@ -6886,17 +6765,6 @@ function createRouter(dbOrDeps) {
6886
6765
  args.push("--disable-cache");
6887
6766
  }
6888
6767
  const isSuite = Boolean(isSuiteFile);
6889
- try {
6890
- await executionBackend?.preflight?.({ args, source: isSuite ? "suite" : "dashboard" });
6891
- } catch (error) {
6892
- const denial = runDenialOf(error);
6893
- if (denial !== void 0) {
6894
- json(res, denial, 403);
6895
- return;
6896
- }
6897
- json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
6898
- return;
6899
- }
6900
6768
  let testName = body.file ? basename5(normalizedFileTarget?.storagePath ?? body.file, ".yaml") : "unknown";
6901
6769
  let fileParallel;
6902
6770
  let platform;
@@ -6939,11 +6807,10 @@ function createRouter(dbOrDeps) {
6939
6807
  const runId = jobQueue.enqueue({
6940
6808
  name: testName,
6941
6809
  filePath: normalizedFileTarget?.storagePath ?? body.file,
6942
- // 2026-08-24 (402-09, D-11): no parent kind minted — see the enqueue route above.
6810
+ // No parent kind is minted; see the enqueue route above.
6943
6811
  kind: "test",
6944
6812
  attributes: buildRunRequestAttributes({
6945
- trigger: triggerSource.trigger,
6946
- runner: body.local === false ? "browserstack" : "local"
6813
+ trigger: triggerSource.trigger
6947
6814
  }),
6948
6815
  platform,
6949
6816
  parallel: effectiveParallel,
@@ -7452,7 +7319,7 @@ function createRouter(dbOrDeps) {
7452
7319
  return;
7453
7320
  }
7454
7321
  if (path === "/api/app-metadata" && req.method === "GET") {
7455
- const version = getAgentQaVersion().trim() || "0.0.0";
7322
+ const version = getAgentQaVersion2().trim() || "0.0.0";
7456
7323
  (async () => {
7457
7324
  try {
7458
7325
  const status = await getAgentQaUpdateStatus();
@@ -7496,9 +7363,8 @@ function createRouter(dbOrDeps) {
7496
7363
  return;
7497
7364
  }
7498
7365
  if (path === "/api/config" && req.method === "GET") {
7499
- if (refuseCrossSiteDisplayRead(req, res)) return;
7500
7366
  if (!configManager) {
7501
- jsonNoCors(res, { error: "Config management not available" }, 501);
7367
+ json(res, { error: "Config management not available" }, 501);
7502
7368
  return;
7503
7369
  }
7504
7370
  ;
@@ -7506,7 +7372,7 @@ function createRouter(dbOrDeps) {
7506
7372
  try {
7507
7373
  const read = await configManager.readMasked();
7508
7374
  if (read.kind === "file_invalid") {
7509
- jsonNoCors(res, { error: "file_invalid", message: read.message }, 409);
7375
+ json(res, { error: "file_invalid", message: read.message }, 409);
7510
7376
  return;
7511
7377
  }
7512
7378
  const { config, revision, lines, fileIssues } = read;
@@ -7516,7 +7382,7 @@ function createRouter(dbOrDeps) {
7516
7382
  const activeLlm = use?.llm ?? config.defaultLLM;
7517
7383
  const defaultCfg = Array.isArray(llms) ? llms.find((c) => c?.name === activeLlm) : void 0;
7518
7384
  const provider = defaultCfg?.provider ?? null;
7519
- jsonNoCors(res, {
7385
+ json(res, {
7520
7386
  config,
7521
7387
  provider,
7522
7388
  revision,
@@ -7524,45 +7390,17 @@ function createRouter(dbOrDeps) {
7524
7390
  ...fileIssues.length > 0 ? { fileIssues } : {}
7525
7391
  });
7526
7392
  } catch (err) {
7527
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to read config" }, 500);
7393
+ json(res, { error: err instanceof Error ? err.message : "Failed to read config" }, 500);
7528
7394
  }
7529
7395
  })();
7530
7396
  return;
7531
7397
  }
7532
- if (path === "/api/config/farm/test-connection" && req.method === "POST") {
7533
- readJsonBody(req).then(async (body) => {
7534
- if (body.provider !== "browserstack") {
7535
- json(res, { error: "Only BrowserStack is currently supported" }, 400);
7536
- return;
7537
- }
7538
- try {
7539
- const auth = Buffer.from(`${body.username}:${body.accessKey}`).toString("base64");
7540
- const response = await fetch("https://api-cloud.browserstack.com/app-automate/devices.json", {
7541
- headers: { Authorization: `Basic ${auth}` }
7542
- });
7543
- if (response.ok) {
7544
- json(res, { success: true });
7545
- } else if (response.status === 401) {
7546
- json(res, { success: false, error: "Invalid credentials" });
7547
- } else {
7548
- json(res, { success: false, error: `BrowserStack API returned ${response.status}` });
7549
- }
7550
- } catch (err) {
7551
- json(res, { success: false, error: err instanceof Error ? err.message : "Connection failed" });
7552
- }
7553
- }).catch(() => json(res, { error: "Invalid request body" }, 400));
7554
- return;
7555
- }
7556
7398
  if (path.startsWith("/api/auth/") && req.method === "DELETE") {
7557
7399
  const segments = path.split("/");
7558
7400
  if (segments.length === 4 && segments[2] === "auth") {
7559
7401
  const providerName = decodeURIComponent(segments[3]);
7560
7402
  if (providerName === "status" || providerName === "credential" || providerName === "oauth") {
7561
7403
  } else {
7562
- if (refuseCrossSiteRequest(req) !== void 0) {
7563
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
7564
- return;
7565
- }
7566
7404
  ;
7567
7405
  (async () => {
7568
7406
  try {
@@ -7573,13 +7411,13 @@ function createRouter(dbOrDeps) {
7573
7411
  return credential;
7574
7412
  });
7575
7413
  if (removed === void 0) {
7576
- jsonNoCors(res, { deleted: false });
7414
+ json(res, { deleted: false });
7577
7415
  return;
7578
7416
  }
7579
7417
  const undo = undoStore.put({ kind: "credential", name: providerName, credential: removed }, Date.now());
7580
- jsonNoCors(res, { deleted: true, undo });
7418
+ json(res, { deleted: true, undo });
7581
7419
  } catch (err) {
7582
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to delete credential" }, 500);
7420
+ json(res, { error: err instanceof Error ? err.message : "Failed to delete credential" }, 500);
7583
7421
  }
7584
7422
  })();
7585
7423
  return;
@@ -7689,27 +7527,21 @@ function createRouter(dbOrDeps) {
7689
7527
  const segments = path.split("/");
7690
7528
  const provider = segments.length >= 6 ? decodeURIComponent(segments[4] ?? "") : "";
7691
7529
  const action = segments.length >= 6 ? segments[5] : "";
7692
- const authDoor = req.method === "POST" && (action === "start" || action === "exchange");
7693
- if (authDoor && refuseCrossSiteRequest(req) !== void 0) {
7694
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
7695
- return;
7696
- }
7697
7530
  const plugin = provider ? getLLMAuthProviderPlugin2(provider) : void 0;
7698
7531
  if (!plugin) {
7699
- ;
7700
- (authDoor ? jsonNoCors : json)(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
7532
+ json(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
7701
7533
  return;
7702
7534
  }
7703
7535
  if (action === "start" && req.method === "POST") {
7704
7536
  readJsonBody(req).then(async (body) => {
7705
7537
  if (!plugin.startAuth) {
7706
- jsonNoCors(res, { error: `Provider "${provider}" does not support dashboard auth start` }, 400);
7538
+ json(res, { error: `Provider "${provider}" does not support dashboard auth start` }, 400);
7707
7539
  return;
7708
7540
  }
7709
7541
  try {
7710
7542
  const target = await requirePluginAuthConfig(configManager, body.configName, plugin);
7711
7543
  if (!target.ok) {
7712
- jsonNoCors(res, credentialRefusalBody(target), target.status);
7544
+ json(res, credentialRefusalBody(target), target.status);
7713
7545
  return;
7714
7546
  }
7715
7547
  const started = await plugin.startAuth({
@@ -7745,16 +7577,16 @@ function createRouter(dbOrDeps) {
7745
7577
  session.error = err instanceof Error ? err.message : "Authentication failed";
7746
7578
  }).finally(() => cleanupPluginOAuthSession(session));
7747
7579
  }
7748
- jsonNoCors(res, {
7580
+ json(res, {
7749
7581
  authorizeUrl: started.authorizeUrl,
7750
7582
  sessionId,
7751
7583
  mode: plugin.dashboardAuth.mode,
7752
7584
  created: target.created
7753
7585
  });
7754
7586
  } catch (err) {
7755
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to start auth plugin flow" }, 500);
7587
+ json(res, { error: err instanceof Error ? err.message : "Failed to start auth plugin flow" }, 500);
7756
7588
  }
7757
- }).catch(() => jsonNoCors(res, { error: "Invalid request body" }, 400));
7589
+ }).catch(() => json(res, { error: "Invalid request body" }, 400));
7758
7590
  return;
7759
7591
  }
7760
7592
  if (action === "result" && req.method === "GET") {
@@ -7782,22 +7614,22 @@ function createRouter(dbOrDeps) {
7782
7614
  if (action === "exchange" && req.method === "POST") {
7783
7615
  readJsonBody(req).then(async (body) => {
7784
7616
  if (!plugin.exchangeCode) {
7785
- jsonNoCors(res, { error: `Provider "${provider}" does not support code exchange` }, 400);
7617
+ json(res, { error: `Provider "${provider}" does not support code exchange` }, 400);
7786
7618
  return;
7787
7619
  }
7788
7620
  const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
7789
7621
  const code = typeof body.code === "string" ? body.code.trim() : "";
7790
7622
  if (!sessionId || !code) {
7791
- jsonNoCors(res, { error: "sessionId and code are required" }, 400);
7623
+ json(res, { error: "sessionId and code are required" }, 400);
7792
7624
  return;
7793
7625
  }
7794
7626
  const session = pluginOAuthSessions.get(sessionId);
7795
7627
  if (!session || session.providerId !== provider) {
7796
- jsonNoCors(res, { error: "Auth session not found" }, 404);
7628
+ json(res, { error: "Auth session not found" }, 404);
7797
7629
  return;
7798
7630
  }
7799
7631
  if (session.status !== "pending") {
7800
- jsonNoCors(res, { error: "Auth session is no longer pending" }, 409);
7632
+ json(res, { error: "Auth session is no longer pending" }, 409);
7801
7633
  return;
7802
7634
  }
7803
7635
  try {
@@ -7819,61 +7651,57 @@ function createRouter(dbOrDeps) {
7819
7651
  session.error = CREDENTIAL_PROVIDER_CONFLICT.message;
7820
7652
  cleanupPluginOAuthSession(session);
7821
7653
  pluginOAuthSessions.delete(sessionId);
7822
- jsonNoCors(res, credentialRefusalBody(CREDENTIAL_PROVIDER_CONFLICT), CREDENTIAL_PROVIDER_CONFLICT.status);
7654
+ json(res, credentialRefusalBody(CREDENTIAL_PROVIDER_CONFLICT), CREDENTIAL_PROVIDER_CONFLICT.status);
7823
7655
  return;
7824
7656
  }
7825
7657
  session.status = "completed";
7826
7658
  cleanupPluginOAuthSession(session);
7827
7659
  pluginOAuthSessions.delete(sessionId);
7828
- jsonNoCors(res, { status: "completed", saved: true });
7660
+ json(res, { status: "completed", saved: true });
7829
7661
  } catch (err) {
7830
7662
  session.status = "error";
7831
7663
  session.error = err instanceof Error ? err.message : "Token exchange failed";
7832
7664
  cleanupPluginOAuthSession(session);
7833
7665
  pluginOAuthSessions.delete(sessionId);
7834
- jsonNoCors(res, { error: session.error }, 500);
7666
+ json(res, { error: session.error }, 500);
7835
7667
  }
7836
- }).catch(() => jsonNoCors(res, { error: "Invalid request body" }, 400));
7668
+ }).catch(() => json(res, { error: "Invalid request body" }, 400));
7837
7669
  return;
7838
7670
  }
7839
7671
  json(res, { error: "Auth plugin route not found" }, 404);
7840
7672
  return;
7841
7673
  }
7842
7674
  if (path === "/api/auth/credential" && req.method === "POST") {
7843
- if (refuseCrossSiteRequest(req) !== void 0) {
7844
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
7845
- return;
7846
- }
7847
7675
  readJsonBody(req).then(async (body) => {
7848
7676
  const provider = typeof body.provider === "string" ? body.provider.trim() : "";
7849
7677
  const credentialType = typeof body.type === "string" ? body.type.trim() : "";
7850
7678
  const secret = typeof body.secret === "string" ? body.secret.trim() : "";
7851
7679
  if (typeof body.configName !== "string" || body.configName.trim() === "") {
7852
- jsonNoCors(res, { error: "configName is required" }, 400);
7680
+ json(res, { error: "configName is required" }, 400);
7853
7681
  return;
7854
7682
  }
7855
7683
  if (!provider || !isKnownLLMProvider(provider)) {
7856
- jsonNoCors(res, { error: "valid provider is required" }, 400);
7684
+ json(res, { error: "valid provider is required" }, 400);
7857
7685
  return;
7858
7686
  }
7859
7687
  if (credentialType !== "api-key" && credentialType !== "bearer-token") {
7860
- jsonNoCors(res, { error: "type must be api-key or bearer-token" }, 400);
7688
+ json(res, { error: "type must be api-key or bearer-token" }, 400);
7861
7689
  return;
7862
7690
  }
7863
7691
  if (!secret) {
7864
- jsonNoCors(res, { error: "secret is required" }, 400);
7692
+ json(res, { error: "secret is required" }, 400);
7865
7693
  return;
7866
7694
  }
7867
7695
  if (getLLMAuthProviderPlugin2(provider)) {
7868
- jsonNoCors(res, { error: "Subscription providers use OAuth login" }, 400);
7696
+ json(res, { error: "Subscription providers use OAuth login" }, 400);
7869
7697
  return;
7870
7698
  }
7871
7699
  if (credentialType === "bearer-token" && provider !== "anthropic-compatible") {
7872
- jsonNoCors(res, { error: "bearer-token credentials are only supported for anthropic-compatible configs" }, 400);
7700
+ json(res, { error: "bearer-token credentials are only supported for anthropic-compatible configs" }, 400);
7873
7701
  return;
7874
7702
  }
7875
7703
  if (credentialType === "api-key" && !API_KEY_CREDENTIAL_PROVIDERS.has(provider)) {
7876
- jsonNoCors(res, { error: "api-key credentials are not supported for this provider" }, 400);
7704
+ json(res, { error: "api-key credentials are not supported for this provider" }, 400);
7877
7705
  return;
7878
7706
  }
7879
7707
  try {
@@ -7888,35 +7716,31 @@ function createRouter(dbOrDeps) {
7888
7716
  return checked;
7889
7717
  });
7890
7718
  if (!target.ok) {
7891
- jsonNoCors(res, credentialRefusalBody(target), target.status);
7719
+ json(res, credentialRefusalBody(target), target.status);
7892
7720
  return;
7893
7721
  }
7894
- jsonNoCors(res, { saved: true, created: target.created });
7722
+ json(res, { saved: true, created: target.created });
7895
7723
  } catch (err) {
7896
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to save credential" }, 500);
7724
+ json(res, { error: err instanceof Error ? err.message : "Failed to save credential" }, 500);
7897
7725
  }
7898
- }).catch(() => jsonNoCors(res, { error: "Invalid request body" }, 400));
7726
+ }).catch(() => json(res, { error: "Invalid request body" }, 400));
7899
7727
  return;
7900
7728
  }
7901
7729
  if (path === "/api/auth/credential/move" && req.method === "POST") {
7902
- if (refuseCrossSiteRequest(req) !== void 0) {
7903
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
7904
- return;
7905
- }
7906
7730
  readJsonBody(req).then(async (raw) => {
7907
7731
  const parsed = LlmCredentialMoveRequestSchema.safeParse(raw);
7908
7732
  if (!parsed.success) {
7909
- jsonNoCors(res, { error: "invalid_setup_input" }, 400);
7733
+ json(res, { error: "invalid_setup_input" }, 400);
7910
7734
  return;
7911
7735
  }
7912
7736
  const from = checkCredentialName(parsed.data.from);
7913
7737
  const to = checkCredentialName(parsed.data.to);
7914
7738
  if (!from.ok || !to.ok) {
7915
- jsonNoCors(res, { error: "name_invalid" }, 400);
7739
+ json(res, { error: "name_invalid" }, 400);
7916
7740
  return;
7917
7741
  }
7918
7742
  if (!configManager) {
7919
- jsonNoCors(res, { error: "Config manager is required to move credentials" }, 400);
7743
+ json(res, { error: "Config manager is required to move credentials" }, 400);
7920
7744
  return;
7921
7745
  }
7922
7746
  try {
@@ -7934,25 +7758,21 @@ function createRouter(dbOrDeps) {
7934
7758
  return void 0;
7935
7759
  });
7936
7760
  if (refusal) {
7937
- jsonNoCors(res, credentialRefusalBody(refusal), refusal.status);
7761
+ json(res, credentialRefusalBody(refusal), refusal.status);
7938
7762
  return;
7939
7763
  }
7940
- jsonNoCors(res, { moved: true });
7764
+ json(res, { moved: true });
7941
7765
  } catch (err) {
7942
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to move credential" }, 500);
7766
+ json(res, { error: err instanceof Error ? err.message : "Failed to move credential" }, 500);
7943
7767
  }
7944
- }).catch(() => jsonNoCors(res, { error: "invalid_setup_input" }, 400));
7768
+ }).catch(() => json(res, { error: "invalid_setup_input" }, 400));
7945
7769
  return;
7946
7770
  }
7947
7771
  if (path === "/api/llm/test" && req.method === "POST") {
7948
- if (refuseCrossSiteRequest(req) !== void 0) {
7949
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
7950
- return;
7951
- }
7952
7772
  readJsonBody(req).then(async (body) => {
7953
7773
  const rawBody = body;
7954
7774
  if ("apiKey" in rawBody) {
7955
- jsonNoCors(res, {
7775
+ json(res, {
7956
7776
  success: false,
7957
7777
  error: "invalid_request",
7958
7778
  message: "apiKey is not accepted in LLM test requests. Save credentials for the named config instead."
@@ -7962,7 +7782,7 @@ function createRouter(dbOrDeps) {
7962
7782
  const providerName = typeof body.provider === "string" ? body.provider.trim() : "";
7963
7783
  const modelName = typeof body.model === "string" ? body.model.trim() : "";
7964
7784
  if (!providerName || !modelName) {
7965
- jsonNoCors(res, { success: false, error: "invalid_request", message: "provider and model are required" }, 400);
7785
+ json(res, { success: false, error: "invalid_request", message: "provider and model are required" }, 400);
7966
7786
  return;
7967
7787
  }
7968
7788
  const configName = typeof body.configName === "string" ? body.configName.trim() : "";
@@ -7987,7 +7807,7 @@ function createRouter(dbOrDeps) {
7987
7807
  }
7988
7808
  const parsedConfig = ModelConfigSchema2.safeParse(llmCandidate);
7989
7809
  if (!parsedConfig.success) {
7990
- jsonNoCors(res, {
7810
+ json(res, {
7991
7811
  success: false,
7992
7812
  error: "invalid_request",
7993
7813
  message: parsedConfig.error.issues.map((issue) => `${issue.path.map(String).join(".") || "config"}: ${issue.message}`).join("; ")
@@ -7999,7 +7819,7 @@ function createRouter(dbOrDeps) {
7999
7819
  const coreAuth = await import("@vostride/agent-qa-core");
8000
7820
  const resolvedAuth = await coreAuth.resolveLLMAuth(configName, llmConfig2);
8001
7821
  if (resolvedAuth.kind === "missing") {
8002
- jsonNoCors(res, {
7822
+ json(res, {
8003
7823
  success: false,
8004
7824
  error: "missing_credential",
8005
7825
  message: resolvedAuth.message
@@ -8028,7 +7848,7 @@ function createRouter(dbOrDeps) {
8028
7848
  } finally {
8029
7849
  clearTimeout(timeout);
8030
7850
  }
8031
- jsonNoCors(res, {
7851
+ json(res, {
8032
7852
  success: true,
8033
7853
  model: modelName,
8034
7854
  provider: providerName,
@@ -8043,7 +7863,7 @@ function createRouter(dbOrDeps) {
8043
7863
  const providerMessage = withDeclaredMask(declaredMask, (mask) => mask(unsavedMessage));
8044
7864
  const errorClass = classifyProviderError2(providerMessage ?? unsavedMessage, statusCode);
8045
7865
  const message = providerMessage === void 0 ? providerErrorSentence2(errorClass, "").trimEnd() : providerErrorSentence2(errorClass, providerMessage);
8046
- jsonNoCors(res, {
7866
+ json(res, {
8047
7867
  success: false,
8048
7868
  error: errorClass,
8049
7869
  message,
@@ -8053,7 +7873,7 @@ function createRouter(dbOrDeps) {
8053
7873
  ...unauthenticated ? { unauthenticated: true, authMessage } : {}
8054
7874
  });
8055
7875
  }
8056
- }).catch(() => jsonNoCors(res, { success: false, error: "invalid_request", message: "Invalid request body" }, 400));
7876
+ }).catch(() => json(res, { success: false, error: "invalid_request", message: "Invalid request body" }, 400));
8057
7877
  return;
8058
7878
  }
8059
7879
  if (path === "/api/cache/purge" && req.method === "POST") {
@@ -8062,56 +7882,15 @@ function createRouter(dbOrDeps) {
8062
7882
  return;
8063
7883
  }
8064
7884
  readJsonBody(req).then(async (body) => {
8065
- if (!body.file && !body.all) {
8066
- json(res, { error: "Either file or all is required" }, 400);
7885
+ if (body.all !== true) {
7886
+ json(res, { error: "all is required" }, 400);
8067
7887
  return;
8068
7888
  }
8069
7889
  try {
8070
7890
  const cfg = await readConfigValues(configManager);
8071
- const cacheDir = cfg.services?.cache?.dir ?? cfg.cache?.dir;
8072
- if (!cacheDir) {
8073
- json(res, { error: "Cache directory not configured" }, 400);
8074
- return;
8075
- }
8076
7891
  const configDir = deps.configPath ? dirname7(resolve3(deps.configPath)) : process.cwd();
8077
- const resolvedCacheDir = resolve3(configDir, cacheDir);
8078
- let branchDirs;
8079
- try {
8080
- branchDirs = await readdir(resolvedCacheDir);
8081
- } catch (err) {
8082
- if (err.code === "ENOENT") {
8083
- json(res, { purged: 0 });
8084
- return;
8085
- }
8086
- throw err;
8087
- }
8088
- let purged = 0;
8089
- for (const branch of branchDirs) {
8090
- const branchPath = join4(resolvedCacheDir, branch);
8091
- let entryStat;
8092
- try {
8093
- entryStat = await stat5(branchPath);
8094
- } catch {
8095
- continue;
8096
- }
8097
- if (!entryStat.isDirectory()) {
8098
- await rm5(branchPath, { force: true });
8099
- continue;
8100
- }
8101
- let files;
8102
- try {
8103
- files = await readdir(branchPath);
8104
- } catch {
8105
- continue;
8106
- }
8107
- for (const file of files) {
8108
- if (!file.endsWith(".json")) continue;
8109
- await rm5(join4(branchPath, file), { force: true });
8110
- purged++;
8111
- }
8112
- await rm5(branchPath, { recursive: true, force: true }).catch(() => {
8113
- });
8114
- }
7892
+ const cacheDir = cfg.services?.cache?.dir ?? DEFAULT_AGENT_QA_CACHE_DIR;
7893
+ const purged = await purgeActionCache(resolve3(configDir, cacheDir));
8115
7894
  json(res, { purged });
8116
7895
  } catch (err) {
8117
7896
  json(res, { error: err instanceof Error ? err.message : "Cache purge failed" }, 500);
@@ -8195,7 +7974,7 @@ function createRouter(dbOrDeps) {
8195
7974
  return;
8196
7975
  }
8197
7976
  const workspaceEnv = await readWorkspaceEnvVars(workspacePaths);
8198
- const result = await new DockerHookExecutor2().execute(resolvedHook, {
7977
+ const result = await new DockerHookExecutor2({ version: getAgentQaVersion2() }).execute(resolvedHook, {
8199
7978
  env: {
8200
7979
  ...workspaceEnv,
8201
7980
  ...validation.overrides
@@ -8231,7 +8010,7 @@ function createRouter(dbOrDeps) {
8231
8010
  variables: shown.variables,
8232
8011
  sandbox: {
8233
8012
  runtime: resolvedHook.runtime,
8234
- image: RUNTIME_IMAGE_MAP[resolvedHook.runtime],
8013
+ image: hookRunnerImage(resolvedHook.runtime, getAgentQaVersion2()),
8235
8014
  networkMode: resolvedHook.network ? "enabled" : "disabled",
8236
8015
  dockerVersion: null,
8237
8016
  networkLogsAvailable: shown.networkLogs.length > 0,
@@ -8435,12 +8214,8 @@ function createRouter(dbOrDeps) {
8435
8214
  return;
8436
8215
  }
8437
8216
  if (req.method === "GET" && /^\/api\/live-editor\/sessions\/[^/]+\/live-url$/.test(path)) {
8438
- if (refuseCrossSiteRequest(req) !== void 0) {
8439
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
8440
- return;
8441
- }
8442
8217
  if (!deps.mintEditorLiveUrl) {
8443
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
8218
+ json(res, { error: "live_view_unavailable" }, 503);
8444
8219
  return;
8445
8220
  }
8446
8221
  let editorSessionId;
@@ -8449,33 +8224,32 @@ function createRouter(dbOrDeps) {
8449
8224
  path.slice("/api/live-editor/sessions/".length, -"/live-url".length)
8450
8225
  );
8451
8226
  } catch {
8452
- jsonNoCors(res, { error: "unknown_session" }, 404);
8227
+ json(res, { error: "unknown_session" }, 404);
8453
8228
  return;
8454
8229
  }
8455
8230
  void deps.mintEditorLiveUrl(editorSessionId).then(
8456
8231
  (result) => {
8457
- jsonNoCors(res, result);
8232
+ json(res, result);
8458
8233
  },
8459
8234
  (error) => {
8460
8235
  const name = error instanceof Error ? error.name : "";
8461
8236
  if (name === "UnknownLiveEditorSessionError") {
8462
- jsonNoCors(res, { error: "unknown_session" }, 404);
8237
+ json(res, { error: "unknown_session" }, 404);
8463
8238
  return;
8464
8239
  }
8465
8240
  if (name === "LiveViewUnavailableError") {
8466
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
8241
+ json(res, { error: "live_view_unavailable" }, 503);
8467
8242
  return;
8468
8243
  }
8469
- jsonNoCors(res, { error: "live_url_mint_failed" }, 500);
8244
+ json(res, { error: "live_url_mint_failed" }, 500);
8470
8245
  }
8471
8246
  );
8472
8247
  return;
8473
8248
  }
8474
8249
  const liveEditorAuthStateMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)\/auth-state$/);
8475
8250
  if (liveEditorAuthStateMatch && req.method === "POST") {
8476
- if (refuseCrossSiteDisplayRead(req, res)) return;
8477
8251
  if (!sessionManager) {
8478
- jsonNoCors(res, { error: "Live editor not available" }, 503);
8252
+ json(res, { error: "Live editor not available" }, 503);
8479
8253
  return;
8480
8254
  }
8481
8255
  ;
@@ -8486,35 +8260,34 @@ function createRouter(dbOrDeps) {
8486
8260
  const body = await readJsonBody(req);
8487
8261
  const parsedName = AuthStateNameSchema.safeParse(typeof body?.name === "string" ? body.name : "");
8488
8262
  if (!parsedName.success) {
8489
- jsonNoCors(res, { error: "Auth state name must be a lowercase slug." }, 400);
8263
+ json(res, { error: "Auth state name must be a lowercase slug." }, 400);
8490
8264
  return;
8491
8265
  }
8492
8266
  stateName = parsedName.data;
8493
8267
  session = sessionManager.getSession(decodeURIComponent(liveEditorAuthStateMatch[1]));
8494
8268
  if (!session) {
8495
- jsonNoCors(res, { error: "Session not found" }, 404);
8269
+ json(res, { error: "Session not found" }, 404);
8496
8270
  return;
8497
8271
  }
8498
8272
  if (typeof session.captureWebAuthState !== "function") {
8499
- jsonNoCors(res, {
8273
+ json(res, {
8500
8274
  error: `Could not save auth state "${stateName}" for target "${getSessionTargetNameForAuthState(session)}".`
8501
8275
  }, 500);
8502
8276
  return;
8503
8277
  }
8504
8278
  const authState = await session.captureWebAuthState(stateName, { replace: body?.replace === true });
8505
- jsonNoCors(res, { authState });
8279
+ json(res, { authState });
8506
8280
  } catch (err) {
8507
8281
  const targetName = getSessionTargetNameForAuthState(session);
8508
8282
  const response = buildAuthStateSaveErrorMessage(stateName || "unknown", targetName, err);
8509
- jsonNoCors(res, { error: response.message }, response.status);
8283
+ json(res, { error: response.message }, response.status);
8510
8284
  }
8511
8285
  })();
8512
8286
  return;
8513
8287
  }
8514
8288
  if (path === "/api/live-editor/sessions" && req.method === "POST") {
8515
- if (refuseCrossSiteDisplayRead(req, res)) return;
8516
8289
  if (!sessionManager) {
8517
- jsonNoCors(res, { error: "Live editor not available" }, 503);
8290
+ json(res, { error: "Live editor not available" }, 503);
8518
8291
  return;
8519
8292
  }
8520
8293
  ;
@@ -8522,16 +8295,10 @@ function createRouter(dbOrDeps) {
8522
8295
  try {
8523
8296
  const body = await readJsonBody(req);
8524
8297
  const targetName = typeof body?.targetName === "string" ? body.targetName : void 0;
8525
- const production = await resolveLiveTargetProduction(configManager, targetName);
8526
- const productionRefusal = liveProductionRefusal(production);
8527
- if (productionRefusal !== void 0) {
8528
- jsonNoCors(res, { error: productionRefusal.message }, 403);
8529
- return;
8530
- }
8531
8298
  const platform = body?.platform ?? "web";
8532
8299
  const runtimeLLM = await resolveDashboardRuntimeLLM(configManager, llmConfig, authFetch);
8533
8300
  if (!runtimeLLM.ok) {
8534
- jsonNoCors(res, { error: runtimeLLM.error }, 400);
8301
+ json(res, { error: runtimeLLM.error }, 400);
8535
8302
  return;
8536
8303
  }
8537
8304
  const setupHooks = Array.isArray(body?.setupHooks) ? body.setupHooks.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
@@ -8550,14 +8317,14 @@ function createRouter(dbOrDeps) {
8550
8317
  let resolvedHooks = /* @__PURE__ */ new Map();
8551
8318
  let hookRegistryError;
8552
8319
  if (!workspacePaths) {
8553
- jsonNoCors(res, { error: "Workspace path resolution not available" }, 500);
8320
+ json(res, { error: "Workspace path resolution not available" }, 500);
8554
8321
  return;
8555
8322
  }
8556
8323
  try {
8557
8324
  const envContent = await readFile6(workspacePaths.envFile.absolutePath, "utf-8");
8558
8325
  envVars = parseEnvFile(envContent);
8559
8326
  } catch {
8560
- jsonNoCors(res, { error: `workspace.envFile file could not be loaded: ${workspacePaths.envFile.workspaceRelativePath}` }, 400);
8327
+ json(res, { error: `workspace.envFile file could not be loaded: ${workspacePaths.envFile.workspaceRelativePath}` }, 400);
8561
8328
  return;
8562
8329
  }
8563
8330
  try {
@@ -8565,7 +8332,7 @@ function createRouter(dbOrDeps) {
8565
8332
  secretStore = SecretStore.fromEnvContent(secretsContent);
8566
8333
  secretRedactor = new SecretRedactor(secretStore);
8567
8334
  } catch {
8568
- jsonNoCors(res, { error: `workspace.secretsFile file could not be loaded: ${workspacePaths.secretsFile.workspaceRelativePath}` }, 400);
8335
+ json(res, { error: `workspace.secretsFile file could not be loaded: ${workspacePaths.secretsFile.workspaceRelativePath}` }, 400);
8569
8336
  return;
8570
8337
  }
8571
8338
  const hookCatalog = await readWorkspaceHooks(configManager, deps.configPath);
@@ -8576,6 +8343,11 @@ function createRouter(dbOrDeps) {
8576
8343
  ...suiteFileManager === void 0 ? {} : { findSuite: (id) => suiteFileManager.findBySuiteId(id) }
8577
8344
  };
8578
8345
  const plannerLimits = await resolveLivePlannerLimits(configManager, entityRef, entityReaders);
8346
+ const navigationTimeoutMs = await resolveLiveNavigationTimeoutMs(
8347
+ configManager,
8348
+ entityRef,
8349
+ entityReaders
8350
+ );
8579
8351
  const sessionSuiteRef = entityRef?.type === "suite" ? entityRef : void 0;
8580
8352
  const plannerLimitsForTest = (testId) => resolveLivePlannerLimits(
8581
8353
  configManager,
@@ -8586,7 +8358,6 @@ function createRouter(dbOrDeps) {
8586
8358
  {
8587
8359
  platform,
8588
8360
  targetName,
8589
- production: { productionTargetNames: production.productionTargetNames },
8590
8361
  plannerLimits,
8591
8362
  plannerLimitsForTest,
8592
8363
  llmConfig: runtimeLLM.llmConfig,
@@ -8600,6 +8371,7 @@ function createRouter(dbOrDeps) {
8600
8371
  hookRegistryError,
8601
8372
  url: body?.url,
8602
8373
  headless: body?.headless ?? false,
8374
+ navigationTimeoutMs,
8603
8375
  useDeviceName: typeof body?.useDeviceName === "string" ? body.useDeviceName : void 0,
8604
8376
  appState: body?.appState === "preserve" || body?.appState === "reset" ? body.appState : void 0,
8605
8377
  bundleId: typeof body?.bundleId === "string" ? body.bundleId : void 0,
@@ -8609,38 +8381,37 @@ function createRouter(dbOrDeps) {
8609
8381
  },
8610
8382
  entityRef
8611
8383
  );
8612
- jsonNoCors(res, { sessionId, sessionNumber }, 201);
8384
+ json(res, { sessionId, sessionNumber }, 201);
8613
8385
  } catch (err) {
8614
8386
  const message = err instanceof Error ? err.message : String(err);
8615
- const status = err instanceof LiveProductionRefusedError ? 403 : err instanceof LivePlannerLimitsRefusedError ? 400 : 500;
8616
- jsonNoCors(res, { error: message }, status);
8387
+ const refused = err instanceof LivePlannerLimitsRefusedError || err instanceof LiveNavigationTimeoutRefusedError;
8388
+ const status = refused ? 400 : 500;
8389
+ json(res, { error: message }, status);
8617
8390
  }
8618
8391
  })();
8619
8392
  return;
8620
8393
  }
8621
8394
  if (path === "/api/live-editor/sessions" && req.method === "GET") {
8622
- if (refuseCrossSiteDisplayRead(req, res)) return;
8623
8395
  if (!sessionManager) {
8624
- jsonNoCors(res, { error: "Live editor not available" }, 503);
8396
+ json(res, { error: "Live editor not available" }, 503);
8625
8397
  return;
8626
8398
  }
8627
- jsonNoCors(res, { sessions: sessionManager.listSessions() });
8399
+ json(res, { sessions: sessionManager.listSessions() });
8628
8400
  return;
8629
8401
  }
8630
8402
  const liveEditorSessionMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)$/);
8631
8403
  if (liveEditorSessionMatch && req.method === "DELETE") {
8632
- if (refuseCrossSiteDisplayRead(req, res)) return;
8633
8404
  if (!sessionManager) {
8634
- jsonNoCors(res, { error: "Live editor not available" }, 503);
8405
+ json(res, { error: "Live editor not available" }, 503);
8635
8406
  return;
8636
8407
  }
8637
8408
  const sessionId = liveEditorSessionMatch[1];
8638
8409
  (async () => {
8639
8410
  const terminated = await sessionManager.terminateSession(sessionId);
8640
8411
  if (terminated) {
8641
- jsonNoCors(res, { ok: true });
8412
+ json(res, { ok: true });
8642
8413
  } else {
8643
- jsonNoCors(res, { error: "Session not found" }, 404);
8414
+ json(res, { error: "Session not found" }, 404);
8644
8415
  }
8645
8416
  })();
8646
8417
  return;
@@ -8689,6 +8460,7 @@ function createRouter(dbOrDeps) {
8689
8460
  // src/queue/job-queue.ts
8690
8461
  import { EventEmitter } from "node:events";
8691
8462
  import { cpus } from "node:os";
8463
+ import { printRunText as printRunText2 } from "@vostride/agent-qa-core";
8692
8464
  var NOT_CANCELLABLE = { kind: "not-cancellable" };
8693
8465
  var JobQueue = class extends EventEmitter {
8694
8466
  db;
@@ -8711,8 +8483,7 @@ var JobQueue = class extends EventEmitter {
8711
8483
  this.pollInterval = setInterval(() => this.dispatchReported("a poll"), intervalMs);
8712
8484
  this.on("enqueued", () => this.dispatchReported("an enqueue"));
8713
8485
  }
8714
- // 2026-10-17 (444-16, D-50): the server closes the database after this; a run that settles later
8715
- // still frees its slot, and nothing reads the closed database.
8486
+ // The server closes the database after this; a run settling later still frees its slot.
8716
8487
  stop() {
8717
8488
  this.stopped = true;
8718
8489
  if (this.pollInterval) {
@@ -8798,7 +8569,10 @@ var JobQueue = class extends EventEmitter {
8798
8569
  this.processNext();
8799
8570
  } catch (error) {
8800
8571
  const message = error instanceof Error ? error.message : String(error);
8801
- console.error(`Queue could not dispatch the next run after ${trigger}: ${message}`);
8572
+ printRunText2(
8573
+ `Queue could not dispatch the next run after ${trigger}: ${message}`,
8574
+ (text) => console.error(text)
8575
+ );
8802
8576
  }
8803
8577
  }
8804
8578
  getActiveCount() {
@@ -8807,9 +8581,7 @@ var JobQueue = class extends EventEmitter {
8807
8581
  getConcurrency() {
8808
8582
  return this.concurrency;
8809
8583
  }
8810
- // 2026-10-16 (442-05, D-06 to D-08): only the owner cancels a running row; gone is an orphan.
8811
- // 442-15 (GAP-D2): own is the stamp this handle wrote, or equal non-null start times.
8812
- // 2026-10-17 (444-06, D-11): a running row is never written here; the run answers with its verdict.
8584
+ // Only the owner stops its running row, never written here; a gone owner's row is an orphan.
8813
8585
  cancel(runId) {
8814
8586
  const run = this.db.getRun(runId);
8815
8587
  if (!run) return NOT_CANCELLABLE;
@@ -8839,21 +8611,7 @@ var JobQueue = class extends EventEmitter {
8839
8611
  const run = this.db.getRun(runId);
8840
8612
  return run && run.status !== "running" && run.status !== "pending" ? { kind: "finished", status: run.status } : NOT_CANCELLABLE;
8841
8613
  }
8842
- // 2026-08-24 (Phase 402 plan 09, D-11): THE CHILD-RUN CANCELLATION FAN-OUT IS DELETED WITH THE
8843
- // MODEL THAT MINTED ITS CHILDREN.
8844
- //
8845
- // ~19 lines stood here walking the run's child rows and cancelling each member row. Under
8846
- // the one-run model a parallel suite is ONE run row with member handles (402-04) — nothing writes
8847
- // the run row's parent-link column any more (405-07 then deleted the column outright), so the
8848
- // walk could only ever have read an empty list while reading as live cancellation policy.
8849
- // Cancelling the run cancels the run.
8850
- //
8851
- // WHAT THIS DOES NOT TOUCH: `parallel` below. D-11 keeps the queue's parallel flag as CROSS-RUN
8852
- // concurrency of INDEPENDENT jobs, which is what `findNextEligible` has always used it for. What
8853
- // was deleted is parallel-SUITE semantics, not the queue's ability to run two unrelated jobs.
8854
- //
8855
- // HISTORICAL ROWS: a pre-402 suite run still lists its members on the read side; cancelling
8856
- // such a run now leaves those member rows in whatever state their own process left them.
8614
+ // `parallel` is cross-run concurrency of independent jobs; a suite is one run row.
8857
8615
  findNextEligible() {
8858
8616
  if (this.activeCount >= this.concurrency) return void 0;
8859
8617
  const pending = this.db.getPendingRuns();
@@ -8876,7 +8634,7 @@ var JobQueue = class extends EventEmitter {
8876
8634
  };
8877
8635
 
8878
8636
  // src/tests/suite-file-manager.ts
8879
- import { readFile as readFile7, mkdir as mkdir4, stat as stat6, rm as rm6 } from "node:fs/promises";
8637
+ import { readFile as readFile7, mkdir as mkdir4, stat as stat5, rm as rm5 } from "node:fs/promises";
8880
8638
  import { dirname as dirname8, basename as basename6 } from "node:path";
8881
8639
  import { generateSuiteId } from "@vostride/agent-qa-ids";
8882
8640
  import { parse as parseYaml7 } from "yaml";
@@ -8887,14 +8645,14 @@ var SuiteFileManager = class {
8887
8645
  this.testFileManager = testFileManager;
8888
8646
  }
8889
8647
  testFileManager;
8890
- // 2026-10-18 (444-33, D-65; 444-35, D-66): each list call reads the workspace itself; each file is written whole and one writer at a time.
8648
+ // Each list call reads the workspace itself; each file is written whole, one writer at a time.
8891
8649
  underLock = createPathLock();
8892
8650
  async list() {
8893
8651
  const records = await discoverWorkspaceFiles4({ workspace: this.workspace, kind: "suite" });
8894
8652
  const files = [];
8895
8653
  for (const record of records) {
8896
8654
  try {
8897
- const s = await stat6(record.absolutePath);
8655
+ const s = await stat5(record.absolutePath);
8898
8656
  if (s.isFile()) {
8899
8657
  let suiteName = basename6(record.workspaceRelativePath);
8900
8658
  let testCount = 0;
@@ -8971,7 +8729,7 @@ var SuiteFileManager = class {
8971
8729
  }
8972
8730
  async delete(relativePath) {
8973
8731
  const target = await this.resolveTarget(relativePath, true);
8974
- await this.underLock(target.absolutePath, () => rm6(target.absolutePath));
8732
+ await this.underLock(target.absolutePath, () => rm5(target.absolutePath));
8975
8733
  }
8976
8734
  async validate(content) {
8977
8735
  try {
@@ -9028,6 +8786,7 @@ import { request } from "node:http";
9028
8786
  import { spawn } from "node:child_process";
9029
8787
  import {
9030
8788
  formatAppiumInstallGuidance,
8789
+ printRunText as printRunText3,
9031
8790
  resolveAppiumExecutable
9032
8791
  } from "@vostride/agent-qa-core";
9033
8792
  import treeKill from "tree-kill";
@@ -9155,21 +8914,21 @@ var AppiumManager = class {
9155
8914
  if (err.code === "ENOENT") {
9156
8915
  console.error(`${APPIUM_PREFIX} Appium not found. ${formatAppiumInstallGuidance()}`);
9157
8916
  } else {
9158
- console.error(`${APPIUM_PREFIX} Error: ${err.message}`);
8917
+ printRunText3(`${APPIUM_PREFIX} Error: ${err.message}`, (text) => console.error(text));
9159
8918
  }
9160
8919
  });
9161
8920
  child.stdout?.on("data", (chunk) => {
9162
8921
  if (this.logLevel !== "debug") return;
9163
8922
  const lines = chunk.toString().split("\n").filter(Boolean);
9164
8923
  for (const line of lines) {
9165
- console.log(`${APPIUM_PREFIX} ${pc.dim(line)}`);
8924
+ printRunText3(`${APPIUM_PREFIX} ${pc.dim(line)}`, (text) => console.log(text));
9166
8925
  }
9167
8926
  });
9168
8927
  child.stderr?.on("data", (chunk) => {
9169
8928
  if (this.logLevel !== "debug") return;
9170
8929
  const lines = chunk.toString().split("\n").filter(Boolean);
9171
8930
  for (const line of lines) {
9172
- console.error(`${APPIUM_PREFIX} ${pc.dim(line)}`);
8931
+ printRunText3(`${APPIUM_PREFIX} ${pc.dim(line)}`, (text) => console.error(text));
9173
8932
  }
9174
8933
  });
9175
8934
  const start = Date.now();
@@ -9251,10 +9010,7 @@ var SessionManager = class {
9251
9010
  this.deps = deps;
9252
9011
  }
9253
9012
  sessions = /* @__PURE__ */ new Map();
9254
- // Per-entity counter keyed by `${entityType}:${entityId}`.
9255
- // Monotonic and ephemeral: resets on server restart per CONTEXT D-30.
9256
- // Never decremented on terminate — a re-open of a terminated entity yields
9257
- // the next number so "Session #3" after "#2" ends does not confuse users.
9013
+ // Monotonic per entity and reset on restart, so a reopened entity gets the next number.
9258
9014
  counters = /* @__PURE__ */ new Map();
9259
9015
  async createSession(config, entity) {
9260
9016
  if (!config.llmConfig) {
@@ -9311,7 +9067,8 @@ var SessionManager = class {
9311
9067
  };
9312
9068
 
9313
9069
  // src/live-editor/ws-handler.ts
9314
- import { parseTestFile } from "@vostride/agent-qa-core";
9070
+ import { format as format2 } from "node:util";
9071
+ import { parseTestFile, printRunText as printRunText4 } from "@vostride/agent-qa-core";
9315
9072
 
9316
9073
  // src/live-editor/types.ts
9317
9074
  var LIVE_EDITOR_UNKNOWN_SESSION_CLOSE_CODE = 4404;
@@ -9498,12 +9255,14 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
9498
9255
  try {
9499
9256
  raw = await testFileManager.read(msg.path);
9500
9257
  } catch (readErr) {
9501
- console.error(`[ws-handler] execute-test: read failed for path=${msg.path}`, readErr);
9258
+ const line = `[ws-handler] execute-test: read failed for path=${msg.path}`;
9259
+ printRunText4(format2(line, readErr), () => console.error(line, readErr));
9502
9260
  throw new Error(`Cannot read test file "${msg.path}": ${readErr?.message ?? String(readErr)}`);
9503
9261
  }
9504
9262
  const parsed = parseTestFile(raw, msg.path);
9505
9263
  if (parsed.errors.length > 0) {
9506
- console.error(`[ws-handler] execute-test: parse errors for ${msg.path}`, parsed.errors);
9264
+ const line = `[ws-handler] execute-test: parse errors for ${msg.path}`;
9265
+ printRunText4(format2(line, parsed.errors), () => console.error(line, parsed.errors));
9507
9266
  throw new Error(`Parse error in ${msg.path}: ${parsed.errors[0].message}`);
9508
9267
  }
9509
9268
  const testDef = parsed.tests[0];
@@ -9513,7 +9272,13 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
9513
9272
  const stepInstructions = testDef.steps.map(
9514
9273
  (s) => typeof s === "string" ? s : s.step
9515
9274
  );
9516
- console.log(`[ws-handler] execute-test: starting "${testDef.name}" (${stepInstructions.length} steps, setup=${(testDef.setup ?? []).length}, teardown=${(testDef.teardown ?? []).length})`);
9275
+ const stepCount = stepInstructions.length;
9276
+ const setupCount = (testDef.setup ?? []).length;
9277
+ const teardownCount = (testDef.teardown ?? []).length;
9278
+ printRunText4(
9279
+ `[ws-handler] execute-test: starting "${testDef.name}" (${stepCount} steps, setup=${setupCount}, teardown=${teardownCount})`,
9280
+ (text) => console.log(text)
9281
+ );
9517
9282
  reply({
9518
9283
  type: "test-start",
9519
9284
  test: {
@@ -9535,7 +9300,11 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
9535
9300
  teardown: testDef.teardown ?? []
9536
9301
  }
9537
9302
  );
9538
- console.log(`[ws-handler] execute-test: finished "${testDef.name}" status=${result.status} duration=${result.duration}ms${result.error ? ` error=${result.error}` : ""}`);
9303
+ const errorPart = result.error ? ` error=${result.error}` : "";
9304
+ printRunText4(
9305
+ `[ws-handler] execute-test: finished "${testDef.name}" status=${result.status} duration=${result.duration}ms${errorPart}`,
9306
+ (text) => console.log(text)
9307
+ );
9539
9308
  reply({
9540
9309
  type: "test-complete",
9541
9310
  test: {
@@ -9548,11 +9317,9 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
9548
9317
  });
9549
9318
  reply({ type: "session-state", state: session.getState() });
9550
9319
  } catch (err) {
9551
- console.error(`[ws-handler] execute-test: caught error for path=${msg.path}`, {
9552
- message: err?.message,
9553
- stack: err?.stack,
9554
- code: err?.code
9555
- });
9320
+ const line = `[ws-handler] execute-test: caught error for path=${msg.path}`;
9321
+ const detail = { message: err?.message, stack: err?.stack, code: err?.code };
9322
+ printRunText4(format2(line, detail), () => console.error(line, detail));
9556
9323
  reply({
9557
9324
  type: "test-error",
9558
9325
  test: {
@@ -9809,8 +9576,7 @@ function renderLiveEmbedPage(input) {
9809
9576
  if (pending === null) return;
9810
9577
  var header = pending;
9811
9578
  pending = null;
9812
- // NO MIME TYPE: the decoder sniffs the bytes, so a JPEG screencast frame and a PNG snapshot
9813
- // both paint in every host, not only in Chromium (428 RESEARCH A2).
9579
+ // No MIME type: the decoder sniffs, so JPEG and PNG frames paint in every host.
9814
9580
  var blob = new Blob([event.data]);
9815
9581
  createImageBitmap(blob).then(function (bitmap) {
9816
9582
  if (canvas.width !== header.width || canvas.height !== header.height) {
@@ -9834,8 +9600,7 @@ function renderLiveEmbedPage(input) {
9834
9600
  try { msg = JSON.parse(event.data); } catch (e) { return; }
9835
9601
 
9836
9602
  if (msg.type === 'ready') {
9837
- // THE PAINTER IS CHOSEN BY THE DECLARED KIND \u2014 never by a browser name, an adapter class or
9838
- // a transport guess (LIVE-04's prohibition). A snapshot paints on the same canvas (428 D-11).
9603
+ // The declared kind picks the painter, never a browser name, adapter or transport guess.
9839
9604
  var kind = msg.stream ? msg.stream.kind : undefined;
9840
9605
  if (kind === 'cdp-screencast' || kind === 'snapshot') {
9841
9606
  setState('connecting', 'waiting for the first frame\u2026');
@@ -9882,8 +9647,7 @@ function renderLiveEmbedPage(input) {
9882
9647
  report({ event: 'lost' });
9883
9648
  });
9884
9649
 
9885
- // A HOST THAT RE-MINTS replaces the fragment; the socket renews in place rather than reloading,
9886
- // so a renewal never costs the viewer the stream it is watching (D-02).
9650
+ // A re-minting host replaces the fragment; the socket renews in place and the stream continues.
9887
9651
  window.addEventListener('hashchange', function () {
9888
9652
  var next = location.hash.slice(1);
9889
9653
  if (next === '' || socket.readyState !== 1) return;
@@ -10093,7 +9857,8 @@ function createFrameProxy(deps) {
10093
9857
 
10094
9858
  // src/server/server.ts
10095
9859
  import {
10096
- isLoopbackHost as isLoopbackHost2
9860
+ isLoopbackHost as isLoopbackHost2,
9861
+ printRunText as printRunText5
10097
9862
  } from "@vostride/agent-qa-core";
10098
9863
  var MIME_TYPES = {
10099
9864
  ".html": "text/html; charset=utf-8",
@@ -10113,9 +9878,10 @@ var MIME_TYPES = {
10113
9878
  ".webp": "image/webp"
10114
9879
  };
10115
9880
  var DashboardRemoteBindRefusedError = class extends Error {
10116
- constructor(host) {
9881
+ constructor(host, port) {
9882
+ const p = String(port === 0 ? 3470 : port);
10117
9883
  super(
10118
- `The dashboard was asked to bind ${host}, which is not a loopback interface. Nothing this server serves \u2014 the run trigger, the queue enqueue, the run service, the artifact serving, the config reads \u2014 asks for a credential, so a non-loopback bind would publish an anonymous run trigger to the network. Leave services.dashboard.host unset (it defaults to ${DEFAULT_DASHBOARD_HOST}), and reach a remote dashboard through an authenticating proxy or a tunnel you control.`
9884
+ `The dashboard was asked to bind ${host}, which is not a loopback interface. Nothing this server serves \u2014 the run trigger, the queue enqueue, the run service, the artifact serving, the config reads \u2014 asks for a credential, so a non-loopback bind would publish an anonymous run trigger to the network. Leave services.dashboard.host unset (it defaults to ${DEFAULT_DASHBOARD_HOST}). To use the dashboard from another machine, forward its port with ssh -L ${p}:localhost:${p} <host> and open http://localhost:${p}/.`
10119
9885
  );
10120
9886
  this.host = host;
10121
9887
  this.name = "DashboardRemoteBindRefusedError";
@@ -10127,7 +9893,7 @@ async function startServer(opts) {
10127
9893
  const runServiceAttach = opts.runServiceAttach ?? runService?.attach;
10128
9894
  const host = opts.host?.trim() || DEFAULT_DASHBOARD_HOST;
10129
9895
  if (!isLoopbackHost2(host)) {
10130
- throw new DashboardRemoteBindRefusedError(host);
9896
+ throw new DashboardRemoteBindRefusedError(host, port);
10131
9897
  }
10132
9898
  const resolvedConfigPath = configPath ?? resolve4("agent-qa.config.yaml");
10133
9899
  const configManager = new ConfigManager(resolvedConfigPath, opts.declaredSecretMask);
@@ -10162,7 +9928,7 @@ async function startServer(opts) {
10162
9928
  failureSummary = "Test timed out -- process was killed";
10163
9929
  } else if (details.refusedBeforeStart) {
10164
9930
  dbStatus = "failed";
10165
- refusalProse = details.neutralReason ?? details.reason;
9931
+ refusalProse = details.reason;
10166
9932
  failureSummary = refusalProse ? `Refused before the run started: ${refusalProse}`.slice(0, 300) : "Refused before the run started";
10167
9933
  } else if (status === "completed") {
10168
9934
  dbStatus = "failed";
@@ -10243,7 +10009,10 @@ async function startServer(opts) {
10243
10009
  mobileRuns.set(run.id, platform);
10244
10010
  } catch (err) {
10245
10011
  const message = err instanceof Error ? err.message : String(err);
10246
- console.error(`Failed to start Appium for run ${run.id}: ${message}`);
10012
+ printRunText5(
10013
+ `Failed to start Appium for run ${run.id}: ${message}`,
10014
+ (text) => console.error(text)
10015
+ );
10247
10016
  db.updateRun(run.id, {
10248
10017
  status: "failed",
10249
10018
  failureSummary: `Appium server failed to start: ${message}`,
@@ -10263,19 +10032,9 @@ async function startServer(opts) {
10263
10032
  runId: run.id,
10264
10033
  args,
10265
10034
  source: meta.isSuite ? "suite" : "dashboard",
10266
- // 2026-10-01 (432-14, D-14): no confirm scope is read off the row any more. The browser that
10267
- // triggered a run is not asked anything, so nothing about it has to survive the wait.
10268
10035
  attributes: run.attributes,
10269
- // T-401-G26 (401-23): a persisted row's timeout is VALIDATED at the seam that consumes it.
10270
- // `in-process-backend.ts:211` reads `opts.timeout ?? defaultTimeout` and validates AFTER, so
10271
- // any non-nullish nonsense here skips the default and leaves the run with no reclamation
10272
- // timer at all. An unusable value falls back to the backend's default rather than a number
10273
- // this call invented.
10036
+ // The backend takes `opts.timeout ?? defaultTimeout`, so an unusable value falls back here.
10274
10037
  timeout: sanitizeQueuedExecutionTimeout(meta.timeout),
10275
- // THE LEASE ABOVE IS UNCHANGED; ONLY ITS DELIVERY IS. `acquireLease` still runs where it ran,
10276
- // still keys on this run id and still owns the bundled Appium's lifecycle (design-of-record).
10277
- // What used to ride `AGENT_QA_APPIUM_URL` in a child's environment now travels as an
10278
- // argument, because there is no child to carry an environment.
10279
10038
  ...isMobile ? { appiumUrl: appiumManager.getUrl() } : {}
10280
10039
  });
10281
10040
  });
@@ -10286,16 +10045,14 @@ async function startServer(opts) {
10286
10045
  appiumManager,
10287
10046
  configManager,
10288
10047
  configPath: resolvedConfigPath,
10289
- // 428 D-07: each created session registers here, which is what the frame socket attaches.
10048
+ // Each created session registers here, which is what the frame socket attaches.
10290
10049
  ...liveEditorSubjects === void 0 ? {} : { liveEditorSubjects }
10291
10050
  });
10292
10051
  const servesLiveView = liveView !== void 0 && (runServiceAttach !== void 0 || liveEditorSubjects !== void 0);
10293
10052
  const apiRouter = createRouter({
10294
10053
  db,
10295
10054
  workspacePaths,
10296
- // 401-07: conditionally spread on the same house rule `agentHost` follows — an absent
10297
- // dependency is an ABSENT field, never a present `undefined`, so the router's own 503 arm is
10298
- // what decides, in one place.
10055
+ // Absent dependencies are absent fields, so the router's own 503 arm decides in one place.
10299
10056
  ...executionBackend === void 0 ? {} : { executionBackend },
10300
10057
  jobQueue,
10301
10058
  testFileManager,
@@ -10306,27 +10063,13 @@ async function startServer(opts) {
10306
10063
  llmConfig,
10307
10064
  authFetch,
10308
10065
  sessionManager,
10309
- // 400-01: conditionally spread on the house rule an absent dependency is an ABSENT field and
10310
- // never a present `undefined` — the router's own 501 arm is what decides, in one place.
10066
+ // Absent dependencies are absent fields, so the router's own 501 arm decides in one place.
10311
10067
  ...agentHost === void 0 ? {} : { agentHost },
10312
- // 402-05 (D-15): the same conditional-spread house rule. Absent means a legacy mount rather
10313
- // than an unconfigured service, and the router's own 503 arm is what decides, in one place.
10314
10068
  ...runServiceReadiness === void 0 ? {} : { runServiceReadiness },
10315
- // 402-08 (D-01): the LOCAL run-stream door's attach port, READ OFF `runService` rather than
10316
- // composed again. That derivation is the "one session registry serves both doors" claim in its
10317
- // structural form — there is no second wiring a composition root could forget or point
10318
- // somewhere else, and the two doors cannot end up attached to different registries. Absent
10319
- // `runService` means no registry exists at all, so the same conditional-spread house rule
10320
- // applies and the router's own 503 arm decides.
10069
+ // Read off `runService` by default, so both doors attach to one session registry.
10321
10070
  ...runServiceAttach === void 0 ? {} : { runServiceAttach },
10322
- // 403 D-03: the mint route's port, on the same conditional-spread house rule — absent means
10323
- // this host serves no live view and the route's own 503 arm decides, in one place.
10324
10071
  ...liveView === void 0 ? {} : { mintLiveUrl: liveView.mintLiveUrl },
10325
- // 428 D-07: the editor mint, on the same house rule — absent means the editor mint route's own
10326
- // 503 decides.
10327
10072
  ...liveView?.mintEditorLiveUrl === void 0 ? {} : { mintEditorLiveUrl: liveView.mintEditorLiveUrl },
10328
- // 404-05 (D-16): the same conditional-spread house rule — absent means this host serves no
10329
- // post-processing push channel and the stream arm's own 503 decides, in one place.
10330
10073
  ...postProcessingRelay === void 0 ? {} : { postProcessingRelay }
10331
10074
  });
10332
10075
  const runServiceRouter = runService === void 0 ? void 0 : createRunServiceRoutes(runService);
@@ -10346,13 +10089,15 @@ async function startServer(opts) {
10346
10089
  apiRouter(req, res);
10347
10090
  return;
10348
10091
  }
10092
+ if (refuseNonLoopbackHost(req) !== void 0) {
10093
+ const address = server.address();
10094
+ const boundPort = typeof address === "object" && address !== null ? address.port : port;
10095
+ refuseForeignHostPage(res, boundPort);
10096
+ return;
10097
+ }
10349
10098
  if (req.method === "OPTIONS") {
10350
- res.writeHead(204, {
10351
- "Access-Control-Allow-Origin": "*",
10352
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
10353
- "Access-Control-Allow-Headers": "Content-Type"
10354
- });
10355
- res.end();
10099
+ res.writeHead(404, { "Content-Type": "text/plain" });
10100
+ res.end("Not Found");
10356
10101
  return;
10357
10102
  }
10358
10103
  if (req.method === "GET" && path.startsWith(LIVE_EMBED_PATH_PREFIX)) {
@@ -10387,17 +10132,16 @@ async function startServer(opts) {
10387
10132
  return;
10388
10133
  }
10389
10134
  if (uiDir) {
10390
- const filePath = path === "/" ? join5(uiDir, "index.html") : join5(uiDir, path);
10135
+ const filePath = path === "/" ? join4(uiDir, "index.html") : join4(uiDir, path);
10391
10136
  try {
10392
- const fileStat = await stat7(filePath);
10137
+ const fileStat = await stat6(filePath);
10393
10138
  if (fileStat.isFile()) {
10394
10139
  const content = await readFile8(filePath);
10395
10140
  const ext = extname(filePath);
10396
10141
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
10397
10142
  res.writeHead(200, {
10398
10143
  "Content-Type": contentType,
10399
- "Content-Length": content.length,
10400
- "Access-Control-Allow-Origin": "*"
10144
+ "Content-Length": content.length
10401
10145
  });
10402
10146
  res.end(content);
10403
10147
  return;
@@ -10405,12 +10149,11 @@ async function startServer(opts) {
10405
10149
  } catch {
10406
10150
  }
10407
10151
  try {
10408
- const indexPath = join5(uiDir, "index.html");
10152
+ const indexPath = join4(uiDir, "index.html");
10409
10153
  const content = await readFile8(indexPath);
10410
10154
  res.writeHead(200, {
10411
10155
  "Content-Type": "text/html",
10412
- "Content-Length": content.length,
10413
- "Access-Control-Allow-Origin": "*"
10156
+ "Content-Length": content.length
10414
10157
  });
10415
10158
  res.end(content);
10416
10159
  return;
@@ -10495,7 +10238,8 @@ async function startServer(opts) {
10495
10238
  session.clearIdleTimer();
10496
10239
  const handler = createSessionHandler(session, () => {
10497
10240
  void sessionManager.terminateSession(sessionId).catch((error) => {
10498
- console.error(`[live-editor] could not end session ${sessionId}`, error);
10241
+ const line = `[live-editor] could not end session ${sessionId}`;
10242
+ printRunText5(format3(line, error), () => console.error(line, error));
10499
10243
  });
10500
10244
  }, (event) => {
10501
10245
  db.insertTokenEvent(event);