@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.
@@ -45,6 +45,7 @@ var import_node_crypto8 = require("node:crypto");
45
45
  var import_promises9 = require("node:fs/promises");
46
46
  var import_node_path10 = require("node:path");
47
47
  var import_node_os3 = require("node:os");
48
+ var import_node_util5 = require("node:util");
48
49
 
49
50
  // src/db/run-owner-host.ts
50
51
  var import_node_child_process = require("node:child_process");
@@ -340,7 +341,7 @@ var TestFileManager = class {
340
341
  constructor(workspace) {
341
342
  this.workspace = workspace;
342
343
  }
343
- // 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.
344
+ // Each list call reads the workspace itself; each file is written whole, one writer at a time.
344
345
  underLock = createPathLock();
345
346
  async list() {
346
347
  const records = await (0, import_agent_qa_core.discoverWorkspaceFiles)({ workspace: this.workspace, kind: "test" });
@@ -504,8 +505,7 @@ var ConfigManager = class _ConfigManager {
504
505
  this.configPath = configPath;
505
506
  this.mask = mask;
506
507
  }
507
- // 441 GAP3-D5: a whole document may keep a marker the file already holds, as often, but never
508
- // add one. A missing file holds none.
508
+ // A whole document may keep a marker the file already holds, as often, but never add one.
509
509
  static addsShownMarker(current, next) {
510
510
  const held = shownMarkerCounts(current);
511
511
  for (const [marker, count] of shownMarkerCounts(next)) {
@@ -513,13 +513,13 @@ var ConfigManager = class _ConfigManager {
513
513
  }
514
514
  return false;
515
515
  }
516
- // 429-05 Pitfall 3: a half-edited file is named, never served as the partial config yaml recovers.
516
+ // A half-edited file is named, never served as the partial config yaml recovers.
517
517
  async readWithRevision() {
518
518
  const read = await this.readParsed();
519
519
  if (read.kind !== "ok") return read;
520
520
  return { kind: "ok", config: read.config, raw: read.raw, revision: read.revision };
521
521
  }
522
- // 441-21 D-29: the payload and the file view pass through the declared mask and nothing else.
522
+ // The payload and the file view pass through the declared mask and nothing else.
523
523
  async readMasked() {
524
524
  const read = await this.readParsed();
525
525
  if (read.kind === "absent") {
@@ -563,8 +563,7 @@ var ConfigManager = class _ConfigManager {
563
563
  revision
564
564
  };
565
565
  }
566
- // 429-01 D-19/D-22: one transaction per request, serialised so overlapping autosaves never
567
- // read the same bytes and drop each other's field.
566
+ // One transaction per request, serialised so overlapping autosaves never drop each other's field.
568
567
  async applyChanges(changes, options = {}) {
569
568
  const marked = changeWithShownMarker(changes);
570
569
  if (marked !== void 0) {
@@ -579,13 +578,11 @@ var ConfigManager = class _ConfigManager {
579
578
  }
580
579
  return this.serialized(() => this.applyChangesOnce(changes, options.capturePriors === true));
581
580
  }
582
- // 429-11 D-24: an Undo is one more transaction on the same mutex, newest change first. A field
583
- // that no longer holds what the change left there was written since, and is never overwritten.
581
+ // An Undo restores newest first; a field written after the change is never overwritten.
584
582
  async restorePriors(priors) {
585
583
  return this.serialized(() => this.restorePriorsOnce(priors));
586
584
  }
587
- // 432-07 D-05: a generated whole-file change (a scaffold, a hooks file) publishes only over the
588
- // bytes it was built from, read again in this path's turn. Anything else is somebody's edit.
585
+ // A generated whole-file change publishes only over the bytes it is built from, re-read in turn.
589
586
  async replaceDocument(expectedRevision, content) {
590
587
  return this.serialized(async () => {
591
588
  let current;
@@ -712,8 +709,7 @@ var ConfigManager = class _ConfigManager {
712
709
  await this.writeAtomic(next);
713
710
  return capturePriors ? { kind: "ok", revision: revisionOf(next), baseRevision, priors } : { kind: "ok", revision: revisionOf(next), baseRevision };
714
711
  }
715
- // 429-05 Pitfall 8 / Pattern 1 step 6: the temp file sits beside the real file, keeps its mode
716
- // (chmod, because writeFile's mode is filtered by the umask), and replaces it in one rename.
712
+ // The temp file sits beside the real one and is chmodded, as the umask filters `writeFile` modes.
717
713
  async writeAtomic(content) {
718
714
  const target = await this.writeTarget();
719
715
  const directory = (0, import_node_path3.dirname)(target);
@@ -1913,6 +1909,12 @@ var HookRegistryManager = class {
1913
1909
 
1914
1910
  // src/server/body-parser.ts
1915
1911
  var DEFAULT_MAX_BYTES = 1024 * 1024;
1912
+ function carriesNonJsonBody(req) {
1913
+ const hasBody = req.headers["transfer-encoding"] !== void 0 || Number(req.headers["content-length"] ?? "0") > 0;
1914
+ if (!hasBody) return false;
1915
+ const type = req.headers["content-type"];
1916
+ return type === void 0 || type.split(";", 1)[0].trim().toLowerCase() !== "application/json";
1917
+ }
1916
1918
  function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
1917
1919
  return new Promise((resolve5, reject) => {
1918
1920
  const chunks = [];
@@ -1947,6 +1949,7 @@ function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
1947
1949
  // src/live-editor/live-session.ts
1948
1950
  var import_node_crypto5 = require("node:crypto");
1949
1951
  var import_node_path7 = require("node:path");
1952
+ var import_node_util3 = require("node:util");
1950
1953
  var import_agent_qa_core6 = require("@vostride/agent-qa-core");
1951
1954
  var import_agent_qa_runner = require("@vostride/agent-qa-runner");
1952
1955
  var import_agent_qa_runner_local = require("@vostride/agent-qa-runner-local");
@@ -2034,12 +2037,10 @@ async function loadLocalBindings(configPath) {
2034
2037
  try {
2035
2038
  const content = await (0, import_promises6.readFile)(filePath, "utf-8");
2036
2039
  const parsed = (0, import_yaml5.parse)(content);
2037
- return parsed && typeof parsed === "object" ? {
2038
- devices: parsed.devices,
2039
- providers: parsed.providers,
2040
- apps: parsed.apps,
2041
- filePath
2042
- } : { filePath };
2040
+ (0, import_agent_qa_core5.refuseRetiredLocalBindings)(parsed, filePath);
2041
+ if (!parsed || typeof parsed !== "object") return { filePath };
2042
+ const bindings = parsed;
2043
+ return { devices: bindings.devices, apps: bindings.apps, filePath };
2043
2044
  } catch (err) {
2044
2045
  if (err.code === "ENOENT") return null;
2045
2046
  throw err;
@@ -2129,44 +2130,15 @@ async function prepareMobileLiveSession(input) {
2129
2130
  appActivity: resolved.app.appActivity,
2130
2131
  deepLinkAppId: resolved.app.deepLinkAppId,
2131
2132
  appPath: resolved.app.install?.path,
2132
- browserstackApp: resolved.app.install?.browserstack,
2133
2133
  appState: resolved.appState
2134
2134
  }
2135
2135
  };
2136
2136
  }
2137
2137
 
2138
2138
  // src/live-editor/live-session.ts
2139
- var LiveProductionRefusedError = class extends Error {
2140
- productionTargetNames;
2141
- constructor(productionTargetNames) {
2142
- super((0, import_agent_qa_schema2.buildProductionRefusal)(productionTargetNames));
2143
- this.name = "LiveProductionRefusedError";
2144
- this.productionTargetNames = (0, import_agent_qa_schema2.distinctInFirstOccurrenceOrder)(productionTargetNames);
2145
- }
2146
- };
2147
2139
  function isConfigRecord(value) {
2148
2140
  return value !== null && typeof value === "object" && !Array.isArray(value);
2149
2141
  }
2150
- function liveTargetName(targetName) {
2151
- if (typeof targetName !== "string") return void 0;
2152
- const trimmed = targetName.trim();
2153
- return trimmed.length > 0 ? trimmed : void 0;
2154
- }
2155
- async function resolveLiveTargetProduction(configManager, targetName) {
2156
- const name = liveTargetName(targetName);
2157
- if (name === void 0 || configManager === void 0) {
2158
- return { productionTargetNames: [], allowProduction: false };
2159
- }
2160
- const config = await readConfigValues(configManager);
2161
- const registry = isConfigRecord(config.registry) ? config.registry : void 0;
2162
- const targets = registry !== void 0 && isConfigRecord(registry.targets) ? registry.targets : void 0;
2163
- const declared = targets !== void 0 && Object.hasOwn(targets, name) ? targets[name] : void 0;
2164
- const production = isConfigRecord(declared) && declared.environment === "production";
2165
- return {
2166
- productionTargetNames: production ? [name] : [],
2167
- allowProduction: config.allowProduction === true
2168
- };
2169
- }
2170
2142
  async function readEntityUse(entity, readers) {
2171
2143
  if (entity === void 0) return void 0;
2172
2144
  const find = entity.type === "suite" ? readers.findSuite : readers.findTest;
@@ -2186,6 +2158,12 @@ var LivePlannerLimitsRefusedError = class extends Error {
2186
2158
  this.name = "LivePlannerLimitsRefusedError";
2187
2159
  }
2188
2160
  };
2161
+ var LiveNavigationTimeoutRefusedError = class extends Error {
2162
+ constructor(message) {
2163
+ super(message);
2164
+ this.name = "LiveNavigationTimeoutRefusedError";
2165
+ }
2166
+ };
2189
2167
  var LIVE_LIMIT_LEAVES = ["maxActions", "maxPlanningTurns"];
2190
2168
  function shownLimitValue(value) {
2191
2169
  return typeof value === "number" ? String(value) : JSON.stringify(value) ?? String(value);
@@ -2234,9 +2212,30 @@ async function resolveLivePlannerLimits(configManager, entities, readers) {
2234
2212
  }
2235
2213
  return (0, import_agent_qa_schema2.resolvePlannerLimits)(config.use, ...uses);
2236
2214
  }
2237
- function liveProductionRefusal(found) {
2238
- if (found.productionTargetNames.length === 0 || found.allowProduction) return void 0;
2239
- return new LiveProductionRefusedError(found.productionTargetNames);
2215
+ async function resolveLiveNavigationTimeoutMs(configManager, entities, readers) {
2216
+ const config = configManager === void 0 ? {} : await readConfigValues(configManager);
2217
+ const layers = [
2218
+ { layer: "project config", use: config.use }
2219
+ ];
2220
+ for (const entity of [].concat(entities ?? [])) {
2221
+ const use = await readEntityUse(entity, readers);
2222
+ layers.push({ layer: `${entity.type} file ${entity.id}`, use });
2223
+ }
2224
+ let navigation;
2225
+ for (const { layer, use } of layers) {
2226
+ if (!isConfigRecord(use) || !isConfigRecord(use.timeout)) continue;
2227
+ if (!Object.hasOwn(use.timeout, "navigation")) continue;
2228
+ const value = use.timeout.navigation;
2229
+ const parsed = import_agent_qa_core6.TimeoutConfigSchema.shape.navigation.safeParse(value);
2230
+ if (!parsed.success) {
2231
+ const issue = parsed.error.issues[0]?.message ?? "invalid value";
2232
+ throw new LiveNavigationTimeoutRefusedError(
2233
+ `${layer} sets use.timeout.navigation to ${shownLimitValue(value)}: ${issue}. The CLI refuses this file too.`
2234
+ );
2235
+ }
2236
+ navigation = parsed.data;
2237
+ }
2238
+ return navigation;
2240
2239
  }
2241
2240
  var IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
2242
2241
  var LIVE_HUB_CLOSE_LIMIT_MS = 2e3;
@@ -2372,8 +2371,7 @@ var LiveSession = class {
2372
2371
  planner = null;
2373
2372
  verifier = null;
2374
2373
  modelProfiles = null;
2375
- // D-14 (Phase 360): the run-scoped storage-state store, constructed lazily on first use and
2376
- // rooted at the session's `.agent-qa` runtime dir (the FsStorageStateStore rooting idiom).
2374
+ // Built lazily on first use, rooted at the session's `.agent-qa` runtime dir.
2377
2375
  storageStateStore = null;
2378
2376
  model = null;
2379
2377
  variableStore = null;
@@ -2401,15 +2399,14 @@ var LiveSession = class {
2401
2399
  mobileAppiumLease = null;
2402
2400
  secretStore;
2403
2401
  secretRedactor;
2404
- // 428 D-07: one hub per session, handle 0, created once the adapter exists. A session whose
2405
- // setup hooks failed has no adapter and therefore no hub; its subject declares `none`.
2402
+ // One hub per session at handle 0, created once the adapter exists; no adapter means no hub.
2406
2403
  liveHub = null;
2407
2404
  attachment = null;
2408
2405
  resolveEnded = () => {
2409
2406
  };
2410
2407
  ended;
2411
2408
  cleanupRun = null;
2412
- // RESEARCH P4: which control socket is the current one. Only its close may detach the session.
2409
+ // Which control socket is the current one; only its close may detach the session.
2413
2410
  attachmentGeneration = 0;
2414
2411
  releaseAttachment = null;
2415
2412
  get modelName() {
@@ -2431,11 +2428,7 @@ var LiveSession = class {
2431
2428
  hub.register(0, registration?.capability, registration?.fallback);
2432
2429
  this.liveHub = hub;
2433
2430
  }
2434
- /**
2435
- * Make a control socket the current one. `release` is how the session lets go of it later: the
2436
- * previous attachment's is called with `superseded` here (428 WR-01), so one socket drives the
2437
- * session at a time.
2438
- */
2431
+ // The previous attachment's `release` gets `superseded`, so one socket drives the session.
2439
2432
  beginAttachment(release) {
2440
2433
  const previous = this.releaseAttachment;
2441
2434
  this.attachmentGeneration += 1;
@@ -2454,11 +2447,7 @@ var LiveSession = class {
2454
2447
  }
2455
2448
  this.pendingMessages = [];
2456
2449
  }
2457
- /**
2458
- * A command's reply, for whichever control socket is attached now (428 CR-01). A command can
2459
- * outlive the socket that issued it, and its outcome belongs to the page, not to that socket:
2460
- * with none attached it is held and flushed to the next one.
2461
- */
2450
+ // A reply can outlive its socket, so it goes to the current socket or waits for the next one.
2462
2451
  deliver(message) {
2463
2452
  if (this.messageSink) {
2464
2453
  this.messageSink(message);
@@ -2569,7 +2558,7 @@ var LiveSession = class {
2569
2558
  continue;
2570
2559
  }
2571
2560
  const result = await (0, import_agent_qa_runner.runHooks)([hookDef], {
2572
- executor: new import_agent_qa_runner_local.DockerHookExecutor(),
2561
+ executor: new import_agent_qa_runner_local.DockerHookExecutor({ version: (0, import_agent_qa_core6.getAgentQaVersion)() }),
2573
2562
  env: this.getHookEnvVars(),
2574
2563
  secretStore: this.secretStore,
2575
2564
  secretRedactor: this.secretRedactor
@@ -2643,7 +2632,6 @@ var LiveSession = class {
2643
2632
  device: config.device ? {
2644
2633
  name: config.device.name,
2645
2634
  platform,
2646
- transport: "local",
2647
2635
  match: config.device
2648
2636
  } : void 0
2649
2637
  };
@@ -2708,15 +2696,6 @@ var LiveSession = class {
2708
2696
  }
2709
2697
  }
2710
2698
  async initialize(config) {
2711
- const found = await resolveLiveTargetProduction(this.deps.configManager, config.targetName);
2712
- const refusal = liveProductionRefusal({
2713
- productionTargetNames: (0, import_agent_qa_schema2.distinctInFirstOccurrenceOrder)([
2714
- ...config.production?.productionTargetNames ?? [],
2715
- ...found.productionTargetNames
2716
- ]),
2717
- allowProduction: found.allowProduction
2718
- });
2719
- if (refusal !== void 0) throw refusal;
2720
2699
  this.platform = config.platform;
2721
2700
  this.targetName = typeof config.targetName === "string" && config.targetName.trim().length > 0 ? config.targetName.trim() : null;
2722
2701
  this._modelName = config.llmConfig.model ?? "unknown";
@@ -2751,7 +2730,11 @@ var LiveSession = class {
2751
2730
  } else {
2752
2731
  const { WebPlatformAdapter } = await import("@vostride/agent-qa-web");
2753
2732
  const adapter = new WebPlatformAdapter();
2754
- await adapter.setup({ platform: "web", browser: { name: "chromium", headless: config.headless ?? true } });
2733
+ await adapter.setup({
2734
+ platform: "web",
2735
+ browser: { name: "chromium", headless: config.headless ?? true },
2736
+ ...config.navigationTimeoutMs === void 0 ? {} : { timeouts: { navigation: config.navigationTimeoutMs } }
2737
+ });
2755
2738
  this.adapter = adapter;
2756
2739
  if (config.url) {
2757
2740
  const resolvedUrl = this.resolveSecrets(config.url);
@@ -2813,7 +2796,7 @@ var LiveSession = class {
2813
2796
  }
2814
2797
  const startedAt = performance.now();
2815
2798
  try {
2816
- const result = await new import_agent_qa_runner_local.DockerHookExecutor().execute(hookDef, {
2799
+ const result = await new import_agent_qa_runner_local.DockerHookExecutor({ version: (0, import_agent_qa_core6.getAgentQaVersion)() }).execute(hookDef, {
2817
2800
  env: this.getHookEnvVars(),
2818
2801
  secretEnv: (0, import_agent_qa_runner.resolveSecretEnv)(this.secretStore),
2819
2802
  secretRedactor: this.secretRedactor
@@ -2980,12 +2963,7 @@ var LiveSession = class {
2980
2963
  };
2981
2964
  return this.redact(result);
2982
2965
  }
2983
- // Inner step execution — no gating. Callers must own the executing/status
2984
- // lifecycle (executeStepCommand for single steps, executeTestCommand for
2985
- // the per-test loop that runs many steps under one outer gate).
2986
- // D-14 (Phase 360): lazily construct the run-scoped storage-state store, rooted at the session's
2987
- // `.agent-qa` runtime dir. All fs writes live in runner-local (validate:arch storageNeutral);
2988
- // the loop records only { name, fingerprint }. Absent configPath falls back to the cwd root.
2966
+ // Lazily built and rooted at the session's `.agent-qa` dir; fs writes stay in runner-local.
2989
2967
  async getStorageStateStore() {
2990
2968
  let store = this.storageStateStore;
2991
2969
  if (!store) {
@@ -3124,7 +3102,9 @@ ${errorLines.join("\n")}`;
3124
3102
  testLimits = this.plannerLimitsForTest !== void 0 && testDraft.testId.length > 0 ? await this.plannerLimitsForTest(testDraft.testId) : this.plannerLimits ?? (0, import_agent_qa_schema2.resolvePlannerLimits)();
3125
3103
  } catch (error) {
3126
3104
  if (!(error instanceof LivePlannerLimitsRefusedError)) throw error;
3127
- console.warn(`[live-session] executeTestCommand: ${error.message}`, { testName: testDraft.testName });
3105
+ const line = `[live-session] executeTestCommand: ${error.message}`;
3106
+ const detail = { testName: testDraft.testName };
3107
+ (0, import_agent_qa_core6.printRunText)((0, import_node_util3.format)(line, detail), () => console.warn(line, detail));
3128
3108
  return this.redact({
3129
3109
  status: "failed",
3130
3110
  duration: performance.now() - start,
@@ -3144,11 +3124,13 @@ ${errorLines.join("\n")}`;
3144
3124
  if (!setupResult.allPassed) {
3145
3125
  const failedHook = setupHookPayloads.find((h) => h.status === "failed");
3146
3126
  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";
3147
- console.warn(`[live-session] executeTestCommand: ${detail}`, {
3127
+ const line = `[live-session] executeTestCommand: ${detail}`;
3128
+ const context = {
3148
3129
  testName: testDraft.testName,
3149
3130
  setupHooks: testDraft.setup,
3150
3131
  failedHook: failedHook ? { name: failedHook.hookName, stderr: failedHook.stderr, error: failedHook.error } : null
3151
- });
3132
+ };
3133
+ (0, import_agent_qa_core6.printRunText)((0, import_node_util3.format)(line, context), () => console.warn(line, context));
3152
3134
  return {
3153
3135
  status: "failed",
3154
3136
  duration: performance.now() - start,
@@ -3226,7 +3208,9 @@ ${errorLines.join("\n")}`;
3226
3208
  topLevelError = failedHook ? `Teardown hook "${failedHook.hookName}" failed${failedHook.error ? `: ${failedHook.error}` : ""}` : "Teardown hook failed";
3227
3209
  }
3228
3210
  if (topLevelError) {
3229
- console.warn(`[live-session] executeTestCommand: ${topLevelError}`, { testName: testDraft.testName });
3211
+ const line = `[live-session] executeTestCommand: ${topLevelError}`;
3212
+ const detail = { testName: testDraft.testName };
3213
+ (0, import_agent_qa_core6.printRunText)((0, import_node_util3.format)(line, detail), () => console.warn(line, detail));
3230
3214
  }
3231
3215
  return this.redact({
3232
3216
  status: finalStatus,
@@ -3242,10 +3226,7 @@ ${errorLines.join("\n")}`;
3242
3226
  this.status = "idle";
3243
3227
  }
3244
3228
  }
3245
- // Thin wrapper around runLifecycleHooks that also captures the dispatched
3246
- // hook payloads (hook-complete) for inclusion in the LiveTestResultPayload.
3247
- // The capture sits beside the message sink rather than wrapping it, so a
3248
- // control socket that reattaches mid-hook keeps its sink (428 CR-01).
3229
+ // Captures dispatched hook payloads beside the message sink, so a reattached socket keeps it.
3249
3230
  async runLifecycleHooksCapturing(phase, hookIds, sink, owner = { scope: "suite" }) {
3250
3231
  this.hookCaptures.add(sink);
3251
3232
  try {
@@ -3489,6 +3470,21 @@ function refuseUnparseableRequest(res) {
3489
3470
  });
3490
3471
  res.end(body);
3491
3472
  }
3473
+ function refuseForeignHostPage(res, port) {
3474
+ if (res.headersSent) {
3475
+ res.destroy();
3476
+ return;
3477
+ }
3478
+ const p = String(port);
3479
+ const body = `The agent-qa dashboard answers only at localhost, 127.0.0.1 or [::1].
3480
+ Open http://localhost:${p}/ on the machine it runs on, or forward the port with ssh -L ${p}:localhost:${p} <host>.
3481
+ `;
3482
+ res.writeHead(403, {
3483
+ "Content-Type": "text/plain; charset=utf-8",
3484
+ "Content-Length": Buffer.byteLength(body)
3485
+ });
3486
+ res.end(body);
3487
+ }
3492
3488
  function backstopServerError(res, req, error) {
3493
3489
  const kind = error instanceof Error ? error.constructor.name : typeof error;
3494
3490
  console.error(
@@ -3568,10 +3564,6 @@ function respondJson(res, status, data) {
3568
3564
  res.end(body);
3569
3565
  }
3570
3566
  function serveRunEvidenceRead(req, res, deps) {
3571
- if (deps.boundary === "human" && refuseCrossSiteRequest(req)) {
3572
- respondJson(res, 403, { error: "cross_site_refused" });
3573
- return;
3574
- }
3575
3567
  const query = new URL(req.url ?? "/", "http://localhost").searchParams;
3576
3568
  const entries = [["runId", deps.runId]];
3577
3569
  const seen = /* @__PURE__ */ new Set(["runId"]);
@@ -3613,10 +3605,6 @@ function respondFeedbackRead(res, deps, functionId, input, parse) {
3613
3605
  });
3614
3606
  }
3615
3607
  function serveRunMarksHistoryRead(req, res, deps) {
3616
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
3617
- respondJson(res, 403, { error: "cross_site_refused" });
3618
- return;
3619
- }
3620
3608
  if (new URL(req.url ?? "/", "http://localhost").search !== "") {
3621
3609
  respondJson(res, 400, { error: "invalid_marks_input" });
3622
3610
  return;
@@ -3635,10 +3623,6 @@ function serveRunMarksHistoryRead(req, res, deps) {
3635
3623
  );
3636
3624
  }
3637
3625
  function serveRunMarkPreviewRead(req, res, deps) {
3638
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
3639
- respondJson(res, 403, { error: "cross_site_refused" });
3640
- return;
3641
- }
3642
3626
  void readJsonBody(req).then((body) => {
3643
3627
  const parsed = import_agent_qa_schema3.RunsMarkInputSchema.safeParse(body);
3644
3628
  if (!parsed.success) {
@@ -3661,10 +3645,6 @@ function serveRunMarkPreviewRead(req, res, deps) {
3661
3645
  });
3662
3646
  }
3663
3647
  function serveRunDetailRead(req, res, deps) {
3664
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
3665
- respondJson(res, 403, { error: "cross_site_refused" });
3666
- return;
3667
- }
3668
3648
  void readRunDetail(deps.host, deps.runId).then(
3669
3649
  (outcome) => {
3670
3650
  if (outcome.kind === "refused") {
@@ -3814,10 +3794,6 @@ async function readRunAnalysis(host, runId) {
3814
3794
  }
3815
3795
  }
3816
3796
  function serveRunAnalysisRead(req, res, deps) {
3817
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
3818
- respondJson(res, 403, { error: "cross_site_refused" });
3819
- return;
3820
- }
3821
3797
  void readRunAnalysis(deps.host, deps.runId).then(
3822
3798
  (outcome) => {
3823
3799
  if (outcome.kind === "refused") {
@@ -4041,10 +4017,7 @@ function serveRunEventStream(req, res, deps) {
4041
4017
  else pending.push(frame);
4042
4018
  };
4043
4019
  const frameOf = (event) => (
4044
- // D-13, all three parts: the id IS the seq, the event name IS the envelope type, and the
4045
- // data IS the envelope's own JSON. No projection, no rename, no second vocabulary. The filter
4046
- // DROPS envelopes; it never edits one, so a filtered stream's frames are byte-identical to the
4047
- // firehose's — with gaps in the ids, which is what seq-as-id means once a filter exists.
4020
+ // The filter drops envelopes and never edits one, so filtered frames match the firehose bytes.
4048
4021
  `id: ${String(event.seq)}
4049
4022
  event: ${event.type}
4050
4023
  data: ${JSON.stringify(event)}
@@ -4106,12 +4079,6 @@ data: ${JSON.stringify(event)}
4106
4079
  deps.openStreams?.add(teardown);
4107
4080
  req.on("close", teardown);
4108
4081
  }
4109
- function runDenialOf(error) {
4110
- if (typeof error !== "object" || error === null) return void 0;
4111
- const neutralReason = error.neutralReason;
4112
- if (typeof neutralReason !== "string" || neutralReason.trim().length === 0) return void 0;
4113
- return { error: "run_denied", neutralReason };
4114
- }
4115
4082
  function createRunServiceRoutes(ports) {
4116
4083
  const openStreams = /* @__PURE__ */ new Set();
4117
4084
  function handleStart(req, res) {
@@ -4142,12 +4109,7 @@ function createRunServiceRoutes(ports) {
4142
4109
  ack = await ports.invokeRunStart(
4143
4110
  parsed.data.patterns === void 0 ? {} : { patterns: parsed.data.patterns }
4144
4111
  );
4145
- } catch (error) {
4146
- const denial = runDenialOf(error);
4147
- if (denial !== void 0) {
4148
- respondJson2(res, 403, denial);
4149
- return;
4150
- }
4112
+ } catch {
4151
4113
  refuse(res, 500, "run_start_failed");
4152
4114
  return;
4153
4115
  }
@@ -4227,6 +4189,10 @@ function createRunServiceRoutes(ports) {
4227
4189
  refuse(res, 403, "cross_site_refused");
4228
4190
  return;
4229
4191
  }
4192
+ if (carriesNonJsonBody(req)) {
4193
+ refuse(res, 415, "unsupported_media_type");
4194
+ return;
4195
+ }
4230
4196
  const matched = matchRunServiceRoute(req.method, path);
4231
4197
  if (matched === void 0) {
4232
4198
  refuse(res, 404, "unknown_route");
@@ -4492,8 +4458,7 @@ data: ${JSON.stringify(again.body)}
4492
4458
  // A failed re-read is not a reason to close a live channel: the next envelope will trigger
4493
4459
  // another, and the page's own poll is the floor underneath both.
4494
4460
  () => void 0
4495
- // …but a throw from the arm ABOVE is not a failed re-read, it is a dead socket, and `.then`'s
4496
- // failure arm never sees its own fulfilment arm's throw (UF-01, on the nested chain).
4461
+ // A throw from the arm above is a dead socket, and `.then`'s failure arm never sees it.
4497
4462
  ).catch(failStream);
4498
4463
  });
4499
4464
  heartbeat = setInterval(() => {
@@ -4530,10 +4495,6 @@ async function readRunKnowledge(host, runId) {
4530
4495
  }
4531
4496
  }
4532
4497
  function serveRunKnowledgeRead(req, res, deps) {
4533
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
4534
- respondJson(res, 403, { error: "cross_site_refused" });
4535
- return;
4536
- }
4537
4498
  void readRunKnowledge(deps.host, deps.runId).then(
4538
4499
  (outcome) => {
4539
4500
  if (outcome.kind === "refused") {
@@ -4574,10 +4535,6 @@ async function markRun(host, runId, body) {
4574
4535
  }
4575
4536
  }
4576
4537
  function registerRunMarkRoute(req, res, deps) {
4577
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
4578
- respondJson(res, 403, { error: "cross_site_refused" });
4579
- return;
4580
- }
4581
4538
  void readJsonBody(req).then(async (body) => {
4582
4539
  const outcome = await markRun(deps.host, deps.runId, body);
4583
4540
  if (outcome.kind === "refused") {
@@ -4595,10 +4552,6 @@ function registerRunMarkRoute(req, res, deps) {
4595
4552
  // src/server/config-routes.ts
4596
4553
  var import_agent_qa_schema9 = require("@vostride/agent-qa-schema");
4597
4554
  function registerConfigWriteRoute(req, res, deps) {
4598
- if (refuseCrossSiteRequest(req) !== void 0) {
4599
- respondJson(res, 403, { error: "cross_site_refused" });
4600
- return;
4601
- }
4602
4555
  void readJsonBody(req).then(async (body) => {
4603
4556
  const parsed = import_agent_qa_schema9.ConfigWriteRequestSchema.safeParse(body);
4604
4557
  if (!parsed.success) {
@@ -4656,10 +4609,6 @@ function registerConfigUndoRoute(req, res, deps) {
4656
4609
  respondJson(res, 405, { error: "method_not_allowed" });
4657
4610
  return;
4658
4611
  }
4659
- if (refuseCrossSiteRequest(req) !== void 0) {
4660
- respondJson(res, 403, { error: "cross_site_refused" });
4661
- return;
4662
- }
4663
4612
  void readJsonBody(req).then(async (body) => {
4664
4613
  const parsed = import_agent_qa_schema9.ConfigUndoRequestSchema.safeParse(body);
4665
4614
  if (!parsed.success) {
@@ -4889,10 +4838,6 @@ async function connectName(request2, deps) {
4889
4838
  });
4890
4839
  }
4891
4840
  function registerLlmConnectRoute(req, res, deps) {
4892
- if (refuseCrossSiteRequest(req) !== void 0) {
4893
- respondJson(res, 403, { error: "cross_site_refused" });
4894
- return;
4895
- }
4896
4841
  void readJsonBody(req).then(async (body) => {
4897
4842
  const answer = await connectLlm(body, deps);
4898
4843
  respondJson(res, answer.status, answer.body);
@@ -5214,25 +5159,10 @@ function llmConnectionTestTimeoutMs(config) {
5214
5159
  return REMOTE_LLM_CONNECTION_TEST_TIMEOUT_MS;
5215
5160
  }
5216
5161
  function json(res, data, status = 200) {
5217
- const body = JSON.stringify(data);
5218
- res.writeHead(status, {
5219
- "Content-Type": "application/json",
5220
- "Access-Control-Allow-Origin": "*",
5221
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
5222
- "Access-Control-Allow-Headers": "Content-Type"
5223
- });
5224
- res.end(body);
5225
- }
5226
- function jsonNoCors(res, data, status = 200) {
5227
5162
  const body = JSON.stringify(data);
5228
5163
  res.writeHead(status, { "Content-Type": "application/json" });
5229
5164
  res.end(body);
5230
5165
  }
5231
- function refuseCrossSiteDisplayRead(req, res) {
5232
- if (refuseCrossSiteRequest(req) === void 0) return false;
5233
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
5234
- return true;
5235
- }
5236
5166
  var INSIGHTS_KNOWLEDGE_RUN_CAP = 200;
5237
5167
  var INSIGHTS_KNOWLEDGE_CONCURRENCY = 4;
5238
5168
  async function isPostProcessingSkipped(host, runId) {
@@ -5296,14 +5226,6 @@ async function readInsightsKnowledgeChanges(host, db, window) {
5296
5226
  function notFound(res, message = "Not found") {
5297
5227
  json(res, { error: message }, 404);
5298
5228
  }
5299
- function cors(res) {
5300
- res.writeHead(204, {
5301
- "Access-Control-Allow-Origin": "*",
5302
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
5303
- "Access-Control-Allow-Headers": "Content-Type"
5304
- });
5305
- res.end();
5306
- }
5307
5229
  function isPlainRecord(value) {
5308
5230
  return value !== null && typeof value === "object" && !Array.isArray(value);
5309
5231
  }
@@ -5356,7 +5278,7 @@ function normalizeRunFilterValue(value) {
5356
5278
  function buildRunRequestAttributes(input) {
5357
5279
  const internal = (0, import_agent_qa_core9.buildInternalRunAttributes)({
5358
5280
  trigger: input.trigger,
5359
- runner: input.runner ?? "local"
5281
+ runner: "local"
5360
5282
  });
5361
5283
  const user = (0, import_agent_qa_core9.validateUserRunAttributes)(input.userAttributes, "run attributes");
5362
5284
  return (0, import_agent_qa_core9.mergeRunAttributes)(internal, user);
@@ -5801,9 +5723,7 @@ var RESPONSE_SAFE_RUN_METADATA_KEYS = /* @__PURE__ */ new Set([
5801
5723
  "timeoutSource",
5802
5724
  "timeoutBaseMs",
5803
5725
  "timeoutBufferMs",
5804
- "suiteIndex",
5805
- "farmSessionUrl",
5806
- "runDevice"
5726
+ "suiteIndex"
5807
5727
  ]);
5808
5728
  function sanitizeRunMetadataForResponse(metadata) {
5809
5729
  if (metadata === null || metadata === void 0) return metadata ?? null;
@@ -5833,17 +5753,7 @@ var RESPONSE_SAFE_RUN_READ_METHODS = [
5833
5753
  "getRunsByTest"
5834
5754
  ];
5835
5755
  var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
5836
- // 2026-08-27 (405-01, D-01/D-03): the v2 projection's six members.
5837
- //
5838
- // The five WRITES return nothing; they are here for the same reason `insertRun` is.
5839
- //
5840
- // `readRunDetailRows` IS A READ AND IS STILL HERE, and that is the judgement this set exists to
5841
- // make explicit rather than to hide. It returns members, steps and sub-actions — the bus
5842
- // payloads as the subscriber MASKED them (T-363-05) — and NO `RunRow` and no
5843
- // `RunArtifactRow`. It therefore carries none of the run metadata the 401-23 scrub law exists
5844
- // to strip, so wrapping it would be wrapping a shape that has nothing to strip. This router
5845
- // does not call it either: the detail arm dispatches through the injected host, which is the
5846
- // whole of D-03.
5756
+ // `readRunDetailRows` returns masked bus payloads and no run row, so it has no metadata to strip.
5847
5757
  "deleteMemberProjection",
5848
5758
  "incrementRunMemberAttempts",
5849
5759
  "insertRunStep",
@@ -5854,23 +5764,23 @@ var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
5854
5764
  "finalizeRunArtifact",
5855
5765
  "getRunsBySuiteIdCount",
5856
5766
  "getRunsByTestCount",
5857
- // 433-30: run ids and a count, no row; each id is dispatched through the agent host.
5767
+ // Run ids and a count, no row; each id is dispatched through the agent host.
5858
5768
  "listInsightsWindowRunIds",
5859
5769
  "insertPendingRun",
5860
5770
  "insertRun",
5861
5771
  "insertRunArtifact",
5862
5772
  "listRunAttributeKeys",
5863
5773
  "listRunAttributeValues",
5864
- // 2026-10-16 (442-01, D-01): an owner (pid, incarnation, host), no run metadata.
5774
+ // An owner (pid, incarnation, host), no run metadata.
5865
5775
  "readRunOwner",
5866
- // 2026-10-16 (442-01, D-04): ids and owners only, no run metadata.
5776
+ // Ids and owners only, no run metadata.
5867
5777
  "listRunningRunOwners",
5868
- // 2026-10-16 (442-01, D-04): compare-and-set writes that return a boolean.
5778
+ // Compare-and-set writes that return a boolean.
5869
5779
  "finalizeOrphanRun",
5870
5780
  "finalizeOwnRun",
5871
- // 2026-10-17 (443-13, GAP-D1): a compare-and-set write that returns a boolean, no run metadata.
5781
+ // A compare-and-set write that returns a boolean, no run metadata.
5872
5782
  "closeOwnRun",
5873
- // 2026-10-17 (443-02, D-07): a compare-and-set write that returns a boolean, no run metadata.
5783
+ // A compare-and-set write that returns a boolean, no run metadata.
5874
5784
  "claimRunningRow",
5875
5785
  "stageRunArtifact",
5876
5786
  "updateRun",
@@ -5941,10 +5851,7 @@ function withResponseSafeRunReads(db) {
5941
5851
  // `sanitizeArtifactForResponse`; a caller reaching for the bare row has no such step, so the
5942
5852
  // projection is its only choke point. Same allowlist, so the two cannot drift.
5943
5853
  getRunArtifact: { value: (runId) => scrubArtifact(db.getRunArtifact(runId)) },
5944
- // 405-07 (D-02): the bundle is ONE artifact now. It used to carry the child runs of a suite
5945
- // parent, whose `run` rows this override scrubbed; nothing has written a child row since
5946
- // 402-09, and the shape went with the column. `getRunArtifact` above remains the choke point
5947
- // for the payload itself, and this wrapper delegates to it.
5854
+ // One artifact; `getRunArtifact` is the choke point for its payload, and this delegates to it.
5948
5855
  getRunArtifactBundle: {
5949
5856
  value: (runId) => ({ artifact: scrubArtifact(db.getRunArtifactBundle(runId).artifact) })
5950
5857
  }
@@ -6092,10 +5999,18 @@ function createRouter(dbOrDeps) {
6092
5999
  json(res, { error: "Bad Request" }, 400);
6093
6000
  return;
6094
6001
  }
6002
+ if (refuseCrossSiteRequest(req) !== void 0) {
6003
+ json(res, { error: "cross_site_refused" }, 403);
6004
+ return;
6005
+ }
6006
+ if (carriesNonJsonBody(req)) {
6007
+ json(res, { error: "unsupported_media_type" }, 415);
6008
+ return;
6009
+ }
6095
6010
  const path = url.pathname;
6096
6011
  if (path === "/api/config" && req.method === "PATCH") {
6097
6012
  if (!configManager) {
6098
- jsonNoCors(res, { error: "config_unavailable" }, 503);
6013
+ json(res, { error: "config_unavailable" }, 503);
6099
6014
  return;
6100
6015
  }
6101
6016
  registerConfigWriteRoute(req, res, { configManager, undoStore });
@@ -6115,7 +6030,7 @@ function createRouter(dbOrDeps) {
6115
6030
  }
6116
6031
  if (path === "/api/config/revision" && req.method === "GET") {
6117
6032
  if (!configManager) {
6118
- jsonNoCors(res, { error: "config_unavailable" }, 503);
6033
+ json(res, { error: "config_unavailable" }, 503);
6119
6034
  return;
6120
6035
  }
6121
6036
  registerConfigRevisionRoute(req, res, { configManager });
@@ -6123,7 +6038,7 @@ function createRouter(dbOrDeps) {
6123
6038
  }
6124
6039
  if (path === "/api/llm/connect" && req.method === "POST") {
6125
6040
  if (!configManager) {
6126
- jsonNoCors(res, { error: "config_unavailable" }, 503);
6041
+ json(res, { error: "config_unavailable" }, 503);
6127
6042
  return;
6128
6043
  }
6129
6044
  registerLlmConnectRoute(req, res, {
@@ -6135,16 +6050,6 @@ function createRouter(dbOrDeps) {
6135
6050
  });
6136
6051
  return;
6137
6052
  }
6138
- if (path === "/api/runs/trigger" || path === "/api/queue/enqueue") {
6139
- if (refuseCrossSiteRequest(req) !== void 0) {
6140
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
6141
- return;
6142
- }
6143
- }
6144
- if (req.method === "OPTIONS") {
6145
- cors(res);
6146
- return;
6147
- }
6148
6053
  if (path === "/api/auth-states" && req.method === "GET") {
6149
6054
  if (!configManager || !deps.configPath) {
6150
6055
  json(res, { error: "Auth-state metadata not available" }, 503);
@@ -6168,11 +6073,10 @@ function createRouter(dbOrDeps) {
6168
6073
  return;
6169
6074
  }
6170
6075
  if (path === "/api/runs" && req.method === "GET") {
6171
- if (refuseCrossSiteDisplayRead(req, res)) return;
6172
6076
  const statuses = url.searchParams.getAll("status");
6173
6077
  const unknownStatus = statuses.find((word) => !isRunStatusWord(word));
6174
6078
  if (unknownStatus !== void 0) {
6175
- jsonNoCors(res, { error: `Unknown run status: ${unknownStatus}.` }, 400);
6079
+ json(res, { error: `Unknown run status: ${unknownStatus}.` }, 400);
6176
6080
  return;
6177
6081
  }
6178
6082
  const name = url.searchParams.get("name") ?? void 0;
@@ -6204,12 +6108,12 @@ function createRouter(dbOrDeps) {
6204
6108
  )).sort((a, b) => a.localeCompare(b));
6205
6109
  const filtered = platformFilteredRuns.filter((run) => matchesTargetFilter(run, target));
6206
6110
  const runs = filtered.slice(offset, offset + limit);
6207
- jsonNoCors(res, {
6111
+ json(res, {
6208
6112
  runs,
6209
6113
  total: filtered.length,
6210
6114
  targets: targetOptions
6211
6115
  });
6212
- }).catch(() => jsonNoCors(res, { error: "Failed to list runs" }, 500));
6116
+ }).catch(() => json(res, { error: "Failed to list runs" }, 500));
6213
6117
  return;
6214
6118
  }
6215
6119
  if (path === "/api/runs/attributes/keys" && req.method === "GET") {
@@ -6247,7 +6151,6 @@ function createRouter(dbOrDeps) {
6247
6151
  }
6248
6152
  const runArtifactMatch = path.match(/^\/api\/runs\/([^/]+)\/artifact$/);
6249
6153
  if (runArtifactMatch && req.method === "GET") {
6250
- if (refuseCrossSiteDisplayRead(req, res)) return;
6251
6154
  const runId = decodeURIComponent(runArtifactMatch[1]);
6252
6155
  const run = db.getRun(runId);
6253
6156
  if (!run) {
@@ -6255,7 +6158,7 @@ function createRouter(dbOrDeps) {
6255
6158
  return;
6256
6159
  }
6257
6160
  const bundle = db.getRunArtifactBundle(runId);
6258
- jsonNoCors(res, {
6161
+ json(res, {
6259
6162
  run,
6260
6163
  artifact: sanitizeArtifactForResponse(bundle.artifact),
6261
6164
  missingSections: getArtifactMissingSections(bundle.artifact)
@@ -6263,7 +6166,6 @@ function createRouter(dbOrDeps) {
6263
6166
  return;
6264
6167
  }
6265
6168
  if (path.startsWith("/api/runs/") && path.endsWith("/execution-logs") && req.method === "GET") {
6266
- if (refuseCrossSiteDisplayRead(req, res)) return;
6267
6169
  const segments = path.split("/");
6268
6170
  const id = segments[3];
6269
6171
  const run = db.getRun(id);
@@ -6274,37 +6176,37 @@ function createRouter(dbOrDeps) {
6274
6176
  const stepId = url.searchParams.get("stepId") ?? void 0;
6275
6177
  const type = url.searchParams.get("type") ?? void 0;
6276
6178
  const logs = db.getExecutionLogs({ runId: id, stepId, type });
6277
- jsonNoCors(res, { logs });
6179
+ json(res, { logs });
6278
6180
  return;
6279
6181
  }
6280
6182
  if (path.startsWith("/api/runs/") && path.endsWith("/mark") && req.method === "POST") {
6281
6183
  if (deps.agentHost === void 0) {
6282
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
6184
+ json(res, { error: "agent_host_unavailable" }, 503);
6283
6185
  return;
6284
6186
  }
6285
6187
  let runId;
6286
6188
  try {
6287
6189
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark".length));
6288
6190
  } catch {
6289
- jsonNoCors(res, { error: "invalid_mark_input" }, 400);
6191
+ json(res, { error: "invalid_mark_input" }, 400);
6290
6192
  return;
6291
6193
  }
6292
- registerRunMarkRoute(req, res, { host: deps.agentHost, runId, boundary: "human" });
6194
+ registerRunMarkRoute(req, res, { host: deps.agentHost, runId });
6293
6195
  return;
6294
6196
  }
6295
6197
  if (req.method === "POST" && /^\/api\/runs\/[^/]+\/mark-preview$/.test(path)) {
6296
6198
  if (deps.agentHost === void 0) {
6297
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
6199
+ json(res, { error: "agent_host_unavailable" }, 503);
6298
6200
  return;
6299
6201
  }
6300
6202
  let runId;
6301
6203
  try {
6302
6204
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark-preview".length));
6303
6205
  } catch {
6304
- jsonNoCors(res, { error: "invalid_run_id" }, 400);
6206
+ json(res, { error: "invalid_run_id" }, 400);
6305
6207
  return;
6306
6208
  }
6307
- serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost, boundary: "human" });
6209
+ serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost });
6308
6210
  return;
6309
6211
  }
6310
6212
  if (path.startsWith("/api/runs/") && path.endsWith("/cancel") && req.method === "POST") {
@@ -6383,44 +6285,39 @@ function createRouter(dbOrDeps) {
6383
6285
  return;
6384
6286
  }
6385
6287
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/live-url$/.test(path)) {
6386
- const crossSite = refuseCrossSiteRequest(req);
6387
- if (crossSite !== void 0) {
6388
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
6389
- return;
6390
- }
6391
6288
  if (!deps.mintLiveUrl) {
6392
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
6289
+ json(res, { error: "live_view_unavailable" }, 503);
6393
6290
  return;
6394
6291
  }
6395
6292
  let liveUrlRunId;
6396
6293
  try {
6397
6294
  liveUrlRunId = decodeURIComponent(path.slice("/api/runs/".length, -"/live-url".length));
6398
6295
  } catch {
6399
- jsonNoCors(res, { error: "unknown_run" }, 404);
6296
+ json(res, { error: "unknown_run" }, 404);
6400
6297
  return;
6401
6298
  }
6402
6299
  const rawHandle = handleParamOf(req);
6403
6300
  const parsedHandle = (0, import_agent_qa_schema11.parseHandleParam)(rawHandle);
6404
6301
  if (parsedHandle.kind === "invalid") {
6405
- jsonNoCors(res, { error: "invalid_handle" }, 400);
6302
+ json(res, { error: "invalid_handle" }, 400);
6406
6303
  return;
6407
6304
  }
6408
6305
  const handle = parsedHandle.kind === "absent" ? 0 : parsedHandle.index;
6409
6306
  void deps.mintLiveUrl(liveUrlRunId, handle).then(
6410
6307
  (result) => {
6411
- jsonNoCors(res, result);
6308
+ json(res, result);
6412
6309
  },
6413
6310
  (error) => {
6414
6311
  const name = error instanceof Error ? error.name : "";
6415
6312
  if (name === "UnknownRunError") {
6416
- jsonNoCors(res, { error: "unknown_run" }, 404);
6313
+ json(res, { error: "unknown_run" }, 404);
6417
6314
  return;
6418
6315
  }
6419
6316
  if (name === "LiveViewUnavailableError") {
6420
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
6317
+ json(res, { error: "live_view_unavailable" }, 503);
6421
6318
  return;
6422
6319
  }
6423
- jsonNoCors(res, { error: "live_url_mint_failed" }, 500);
6320
+ json(res, { error: "live_url_mint_failed" }, 500);
6424
6321
  }
6425
6322
  );
6426
6323
  return;
@@ -6449,32 +6346,32 @@ function createRouter(dbOrDeps) {
6449
6346
  }
6450
6347
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/evidence$/.test(path)) {
6451
6348
  if (!deps.agentHost) {
6452
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
6349
+ json(res, { error: "agent_host_unavailable" }, 503);
6453
6350
  return;
6454
6351
  }
6455
6352
  let runId;
6456
6353
  try {
6457
6354
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/evidence".length));
6458
6355
  } catch {
6459
- jsonNoCors(res, { error: "invalid_run_id" }, 400);
6356
+ json(res, { error: "invalid_run_id" }, 400);
6460
6357
  return;
6461
6358
  }
6462
- serveRunEvidenceRead(req, res, { runId, host: deps.agentHost, boundary: "human" });
6359
+ serveRunEvidenceRead(req, res, { runId, host: deps.agentHost });
6463
6360
  return;
6464
6361
  }
6465
6362
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/marks$/.test(path)) {
6466
6363
  if (!deps.agentHost) {
6467
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
6364
+ json(res, { error: "agent_host_unavailable" }, 503);
6468
6365
  return;
6469
6366
  }
6470
6367
  let runId;
6471
6368
  try {
6472
6369
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/marks".length));
6473
6370
  } catch {
6474
- jsonNoCors(res, { error: "invalid_run_id" }, 400);
6371
+ json(res, { error: "invalid_run_id" }, 400);
6475
6372
  return;
6476
6373
  }
6477
- serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost, boundary: "human" });
6374
+ serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost });
6478
6375
  return;
6479
6376
  }
6480
6377
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/detail$/.test(path)) {
@@ -6489,7 +6386,7 @@ function createRouter(dbOrDeps) {
6489
6386
  json(res, { error: "invalid_run_id" }, 400);
6490
6387
  return;
6491
6388
  }
6492
- serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost, boundary: "human" });
6389
+ serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost });
6493
6390
  return;
6494
6391
  }
6495
6392
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/analysis$/.test(path)) {
@@ -6504,7 +6401,7 @@ function createRouter(dbOrDeps) {
6504
6401
  json(res, { error: "invalid_run_id" }, 400);
6505
6402
  return;
6506
6403
  }
6507
- serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost, boundary: "human" });
6404
+ serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost });
6508
6405
  return;
6509
6406
  }
6510
6407
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/knowledge$/.test(path)) {
@@ -6521,8 +6418,7 @@ function createRouter(dbOrDeps) {
6521
6418
  }
6522
6419
  serveRunKnowledgeRead(req, res, {
6523
6420
  runId: knowledgeRunId,
6524
- host: deps.agentHost,
6525
- boundary: "human"
6421
+ host: deps.agentHost
6526
6422
  });
6527
6423
  return;
6528
6424
  }
@@ -6572,14 +6468,13 @@ function createRouter(dbOrDeps) {
6572
6468
  notFound(res);
6573
6469
  return;
6574
6470
  }
6575
- if (refuseCrossSiteDisplayRead(req, res)) return;
6576
6471
  const id = segments[3];
6577
6472
  const run = db.getRun(id);
6578
6473
  if (!run) {
6579
6474
  notFound(res, "Run not found");
6580
6475
  return;
6581
6476
  }
6582
- jsonNoCors(res, { run });
6477
+ json(res, { run });
6583
6478
  return;
6584
6479
  }
6585
6480
  if (path === "/api/stats/costs" && req.method === "GET") {
@@ -6590,19 +6485,18 @@ function createRouter(dbOrDeps) {
6590
6485
  return;
6591
6486
  }
6592
6487
  if (path === "/api/insights/knowledge-changes" && req.method === "GET") {
6593
- if (refuseCrossSiteDisplayRead(req, res)) return;
6594
6488
  if (!deps.agentHost) {
6595
- jsonNoCors(res, { status: "unavailable", reason: "agent_host_unavailable" }, 503);
6489
+ json(res, { status: "unavailable", reason: "agent_host_unavailable" }, 503);
6596
6490
  return;
6597
6491
  }
6598
6492
  const from = url.searchParams.get("from") ?? void 0;
6599
6493
  const to = url.searchParams.get("to") ?? void 0;
6600
6494
  void readInsightsKnowledgeChanges(deps.agentHost, db, { from, to }).then(
6601
6495
  (body) => {
6602
- jsonNoCors(res, body);
6496
+ json(res, body);
6603
6497
  },
6604
6498
  () => {
6605
- jsonNoCors(res, { status: "unavailable", reason: "read_failed" }, 500);
6499
+ json(res, { status: "unavailable", reason: "read_failed" }, 500);
6606
6500
  }
6607
6501
  );
6608
6502
  return;
@@ -6668,10 +6562,7 @@ function createRouter(dbOrDeps) {
6668
6562
  serveRunEventStream(req, res, {
6669
6563
  runId,
6670
6564
  handleParam: handleParamOf(req),
6671
- // 403-03 (D-13 · INVK-05): the resume point, read from the SAME pair the authed door reads
6672
- // it from. `url.searchParams.get('lastEventId')` would resolve a repeated parameter to its
6673
- // first value — the one answer `lastEventIdParamOf` exists to refuse — and a second
6674
- // header/query precedence rule here would be a second contract for where a client resumes.
6565
+ // Read from the same pair as the authed door; `searchParams.get` takes a repeated first.
6675
6566
  resume: resumePointOf(req.headers["last-event-id"], lastEventIdParamOf(req)),
6676
6567
  attach: deps.runServiceAttach
6677
6568
  });
@@ -6713,7 +6604,6 @@ function createRouter(dbOrDeps) {
6713
6604
  try {
6714
6605
  attributes = buildRunRequestAttributes({
6715
6606
  trigger: "api",
6716
- runner: "local",
6717
6607
  userAttributes: body.attributes
6718
6608
  });
6719
6609
  } catch (err) {
@@ -6721,25 +6611,10 @@ function createRouter(dbOrDeps) {
6721
6611
  return;
6722
6612
  }
6723
6613
  const args = normalizedFileTarget ? [normalizedFileTarget.executionPath] : [];
6724
- const source = isSuiteFile ? "suite" : "dashboard";
6725
- try {
6726
- await executionBackend?.preflight?.({ args, source });
6727
- } catch (error) {
6728
- const denial = runDenialOf(error);
6729
- if (denial !== void 0) {
6730
- json(res, denial, 403);
6731
- return;
6732
- }
6733
- json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
6734
- return;
6735
- }
6736
6614
  const runId = jobQueue.enqueue({
6737
6615
  name: body.name,
6738
6616
  filePath: normalizedFileTarget?.storagePath,
6739
- // 2026-08-24 (402-09, D-11): NO PARENT KIND IS MINTED. A suite run is ONE run row
6740
- // with member handles, so a parent artifact kind would claim parenthood over rows
6741
- // nothing creates. `isSuiteFile` still travels in `metadata.isSuite` below, and the run's
6742
- // own `suite_id` plus the artifact's `source.kind` snapshot still say it is a suite.
6617
+ // No parent kind is minted; `metadata.isSuite` and `suite_id` say it is a suite.
6743
6618
  kind: "test",
6744
6619
  attributes,
6745
6620
  priority: body.priority,
@@ -6822,17 +6697,6 @@ function createRouter(dbOrDeps) {
6822
6697
  args.push("--disable-cache");
6823
6698
  }
6824
6699
  const isSuite = Boolean(isSuiteFile);
6825
- try {
6826
- await executionBackend?.preflight?.({ args, source: isSuite ? "suite" : "dashboard" });
6827
- } catch (error) {
6828
- const denial = runDenialOf(error);
6829
- if (denial !== void 0) {
6830
- json(res, denial, 403);
6831
- return;
6832
- }
6833
- json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
6834
- return;
6835
- }
6836
6700
  let testName = body.file ? (0, import_node_path8.basename)(normalizedFileTarget?.storagePath ?? body.file, ".yaml") : "unknown";
6837
6701
  let fileParallel;
6838
6702
  let platform;
@@ -6875,11 +6739,10 @@ function createRouter(dbOrDeps) {
6875
6739
  const runId = jobQueue.enqueue({
6876
6740
  name: testName,
6877
6741
  filePath: normalizedFileTarget?.storagePath ?? body.file,
6878
- // 2026-08-24 (402-09, D-11): no parent kind minted — see the enqueue route above.
6742
+ // No parent kind is minted; see the enqueue route above.
6879
6743
  kind: "test",
6880
6744
  attributes: buildRunRequestAttributes({
6881
- trigger: triggerSource.trigger,
6882
- runner: body.local === false ? "browserstack" : "local"
6745
+ trigger: triggerSource.trigger
6883
6746
  }),
6884
6747
  platform,
6885
6748
  parallel: effectiveParallel,
@@ -7432,9 +7295,8 @@ function createRouter(dbOrDeps) {
7432
7295
  return;
7433
7296
  }
7434
7297
  if (path === "/api/config" && req.method === "GET") {
7435
- if (refuseCrossSiteDisplayRead(req, res)) return;
7436
7298
  if (!configManager) {
7437
- jsonNoCors(res, { error: "Config management not available" }, 501);
7299
+ json(res, { error: "Config management not available" }, 501);
7438
7300
  return;
7439
7301
  }
7440
7302
  ;
@@ -7442,7 +7304,7 @@ function createRouter(dbOrDeps) {
7442
7304
  try {
7443
7305
  const read = await configManager.readMasked();
7444
7306
  if (read.kind === "file_invalid") {
7445
- jsonNoCors(res, { error: "file_invalid", message: read.message }, 409);
7307
+ json(res, { error: "file_invalid", message: read.message }, 409);
7446
7308
  return;
7447
7309
  }
7448
7310
  const { config, revision, lines, fileIssues } = read;
@@ -7452,7 +7314,7 @@ function createRouter(dbOrDeps) {
7452
7314
  const activeLlm = use?.llm ?? config.defaultLLM;
7453
7315
  const defaultCfg = Array.isArray(llms) ? llms.find((c) => c?.name === activeLlm) : void 0;
7454
7316
  const provider = defaultCfg?.provider ?? null;
7455
- jsonNoCors(res, {
7317
+ json(res, {
7456
7318
  config,
7457
7319
  provider,
7458
7320
  revision,
@@ -7460,45 +7322,17 @@ function createRouter(dbOrDeps) {
7460
7322
  ...fileIssues.length > 0 ? { fileIssues } : {}
7461
7323
  });
7462
7324
  } catch (err) {
7463
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to read config" }, 500);
7325
+ json(res, { error: err instanceof Error ? err.message : "Failed to read config" }, 500);
7464
7326
  }
7465
7327
  })();
7466
7328
  return;
7467
7329
  }
7468
- if (path === "/api/config/farm/test-connection" && req.method === "POST") {
7469
- readJsonBody(req).then(async (body) => {
7470
- if (body.provider !== "browserstack") {
7471
- json(res, { error: "Only BrowserStack is currently supported" }, 400);
7472
- return;
7473
- }
7474
- try {
7475
- const auth = Buffer.from(`${body.username}:${body.accessKey}`).toString("base64");
7476
- const response = await fetch("https://api-cloud.browserstack.com/app-automate/devices.json", {
7477
- headers: { Authorization: `Basic ${auth}` }
7478
- });
7479
- if (response.ok) {
7480
- json(res, { success: true });
7481
- } else if (response.status === 401) {
7482
- json(res, { success: false, error: "Invalid credentials" });
7483
- } else {
7484
- json(res, { success: false, error: `BrowserStack API returned ${response.status}` });
7485
- }
7486
- } catch (err) {
7487
- json(res, { success: false, error: err instanceof Error ? err.message : "Connection failed" });
7488
- }
7489
- }).catch(() => json(res, { error: "Invalid request body" }, 400));
7490
- return;
7491
- }
7492
7330
  if (path.startsWith("/api/auth/") && req.method === "DELETE") {
7493
7331
  const segments = path.split("/");
7494
7332
  if (segments.length === 4 && segments[2] === "auth") {
7495
7333
  const providerName = decodeURIComponent(segments[3]);
7496
7334
  if (providerName === "status" || providerName === "credential" || providerName === "oauth") {
7497
7335
  } else {
7498
- if (refuseCrossSiteRequest(req) !== void 0) {
7499
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
7500
- return;
7501
- }
7502
7336
  ;
7503
7337
  (async () => {
7504
7338
  try {
@@ -7509,13 +7343,13 @@ function createRouter(dbOrDeps) {
7509
7343
  return credential;
7510
7344
  });
7511
7345
  if (removed === void 0) {
7512
- jsonNoCors(res, { deleted: false });
7346
+ json(res, { deleted: false });
7513
7347
  return;
7514
7348
  }
7515
7349
  const undo = undoStore.put({ kind: "credential", name: providerName, credential: removed }, Date.now());
7516
- jsonNoCors(res, { deleted: true, undo });
7350
+ json(res, { deleted: true, undo });
7517
7351
  } catch (err) {
7518
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to delete credential" }, 500);
7352
+ json(res, { error: err instanceof Error ? err.message : "Failed to delete credential" }, 500);
7519
7353
  }
7520
7354
  })();
7521
7355
  return;
@@ -7625,27 +7459,21 @@ function createRouter(dbOrDeps) {
7625
7459
  const segments = path.split("/");
7626
7460
  const provider = segments.length >= 6 ? decodeURIComponent(segments[4] ?? "") : "";
7627
7461
  const action = segments.length >= 6 ? segments[5] : "";
7628
- const authDoor = req.method === "POST" && (action === "start" || action === "exchange");
7629
- if (authDoor && refuseCrossSiteRequest(req) !== void 0) {
7630
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
7631
- return;
7632
- }
7633
7462
  const plugin = provider ? (0, import_agent_qa_core9.getLLMAuthProviderPlugin)(provider) : void 0;
7634
7463
  if (!plugin) {
7635
- ;
7636
- (authDoor ? jsonNoCors : json)(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
7464
+ json(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
7637
7465
  return;
7638
7466
  }
7639
7467
  if (action === "start" && req.method === "POST") {
7640
7468
  readJsonBody(req).then(async (body) => {
7641
7469
  if (!plugin.startAuth) {
7642
- jsonNoCors(res, { error: `Provider "${provider}" does not support dashboard auth start` }, 400);
7470
+ json(res, { error: `Provider "${provider}" does not support dashboard auth start` }, 400);
7643
7471
  return;
7644
7472
  }
7645
7473
  try {
7646
7474
  const target = await requirePluginAuthConfig(configManager, body.configName, plugin);
7647
7475
  if (!target.ok) {
7648
- jsonNoCors(res, credentialRefusalBody(target), target.status);
7476
+ json(res, credentialRefusalBody(target), target.status);
7649
7477
  return;
7650
7478
  }
7651
7479
  const started = await plugin.startAuth({
@@ -7681,16 +7509,16 @@ function createRouter(dbOrDeps) {
7681
7509
  session.error = err instanceof Error ? err.message : "Authentication failed";
7682
7510
  }).finally(() => cleanupPluginOAuthSession(session));
7683
7511
  }
7684
- jsonNoCors(res, {
7512
+ json(res, {
7685
7513
  authorizeUrl: started.authorizeUrl,
7686
7514
  sessionId,
7687
7515
  mode: plugin.dashboardAuth.mode,
7688
7516
  created: target.created
7689
7517
  });
7690
7518
  } catch (err) {
7691
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to start auth plugin flow" }, 500);
7519
+ json(res, { error: err instanceof Error ? err.message : "Failed to start auth plugin flow" }, 500);
7692
7520
  }
7693
- }).catch(() => jsonNoCors(res, { error: "Invalid request body" }, 400));
7521
+ }).catch(() => json(res, { error: "Invalid request body" }, 400));
7694
7522
  return;
7695
7523
  }
7696
7524
  if (action === "result" && req.method === "GET") {
@@ -7718,22 +7546,22 @@ function createRouter(dbOrDeps) {
7718
7546
  if (action === "exchange" && req.method === "POST") {
7719
7547
  readJsonBody(req).then(async (body) => {
7720
7548
  if (!plugin.exchangeCode) {
7721
- jsonNoCors(res, { error: `Provider "${provider}" does not support code exchange` }, 400);
7549
+ json(res, { error: `Provider "${provider}" does not support code exchange` }, 400);
7722
7550
  return;
7723
7551
  }
7724
7552
  const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
7725
7553
  const code = typeof body.code === "string" ? body.code.trim() : "";
7726
7554
  if (!sessionId || !code) {
7727
- jsonNoCors(res, { error: "sessionId and code are required" }, 400);
7555
+ json(res, { error: "sessionId and code are required" }, 400);
7728
7556
  return;
7729
7557
  }
7730
7558
  const session = pluginOAuthSessions.get(sessionId);
7731
7559
  if (!session || session.providerId !== provider) {
7732
- jsonNoCors(res, { error: "Auth session not found" }, 404);
7560
+ json(res, { error: "Auth session not found" }, 404);
7733
7561
  return;
7734
7562
  }
7735
7563
  if (session.status !== "pending") {
7736
- jsonNoCors(res, { error: "Auth session is no longer pending" }, 409);
7564
+ json(res, { error: "Auth session is no longer pending" }, 409);
7737
7565
  return;
7738
7566
  }
7739
7567
  try {
@@ -7755,61 +7583,57 @@ function createRouter(dbOrDeps) {
7755
7583
  session.error = CREDENTIAL_PROVIDER_CONFLICT.message;
7756
7584
  cleanupPluginOAuthSession(session);
7757
7585
  pluginOAuthSessions.delete(sessionId);
7758
- jsonNoCors(res, credentialRefusalBody(CREDENTIAL_PROVIDER_CONFLICT), CREDENTIAL_PROVIDER_CONFLICT.status);
7586
+ json(res, credentialRefusalBody(CREDENTIAL_PROVIDER_CONFLICT), CREDENTIAL_PROVIDER_CONFLICT.status);
7759
7587
  return;
7760
7588
  }
7761
7589
  session.status = "completed";
7762
7590
  cleanupPluginOAuthSession(session);
7763
7591
  pluginOAuthSessions.delete(sessionId);
7764
- jsonNoCors(res, { status: "completed", saved: true });
7592
+ json(res, { status: "completed", saved: true });
7765
7593
  } catch (err) {
7766
7594
  session.status = "error";
7767
7595
  session.error = err instanceof Error ? err.message : "Token exchange failed";
7768
7596
  cleanupPluginOAuthSession(session);
7769
7597
  pluginOAuthSessions.delete(sessionId);
7770
- jsonNoCors(res, { error: session.error }, 500);
7598
+ json(res, { error: session.error }, 500);
7771
7599
  }
7772
- }).catch(() => jsonNoCors(res, { error: "Invalid request body" }, 400));
7600
+ }).catch(() => json(res, { error: "Invalid request body" }, 400));
7773
7601
  return;
7774
7602
  }
7775
7603
  json(res, { error: "Auth plugin route not found" }, 404);
7776
7604
  return;
7777
7605
  }
7778
7606
  if (path === "/api/auth/credential" && req.method === "POST") {
7779
- if (refuseCrossSiteRequest(req) !== void 0) {
7780
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
7781
- return;
7782
- }
7783
7607
  readJsonBody(req).then(async (body) => {
7784
7608
  const provider = typeof body.provider === "string" ? body.provider.trim() : "";
7785
7609
  const credentialType = typeof body.type === "string" ? body.type.trim() : "";
7786
7610
  const secret = typeof body.secret === "string" ? body.secret.trim() : "";
7787
7611
  if (typeof body.configName !== "string" || body.configName.trim() === "") {
7788
- jsonNoCors(res, { error: "configName is required" }, 400);
7612
+ json(res, { error: "configName is required" }, 400);
7789
7613
  return;
7790
7614
  }
7791
7615
  if (!provider || !isKnownLLMProvider(provider)) {
7792
- jsonNoCors(res, { error: "valid provider is required" }, 400);
7616
+ json(res, { error: "valid provider is required" }, 400);
7793
7617
  return;
7794
7618
  }
7795
7619
  if (credentialType !== "api-key" && credentialType !== "bearer-token") {
7796
- jsonNoCors(res, { error: "type must be api-key or bearer-token" }, 400);
7620
+ json(res, { error: "type must be api-key or bearer-token" }, 400);
7797
7621
  return;
7798
7622
  }
7799
7623
  if (!secret) {
7800
- jsonNoCors(res, { error: "secret is required" }, 400);
7624
+ json(res, { error: "secret is required" }, 400);
7801
7625
  return;
7802
7626
  }
7803
7627
  if ((0, import_agent_qa_core9.getLLMAuthProviderPlugin)(provider)) {
7804
- jsonNoCors(res, { error: "Subscription providers use OAuth login" }, 400);
7628
+ json(res, { error: "Subscription providers use OAuth login" }, 400);
7805
7629
  return;
7806
7630
  }
7807
7631
  if (credentialType === "bearer-token" && provider !== "anthropic-compatible") {
7808
- jsonNoCors(res, { error: "bearer-token credentials are only supported for anthropic-compatible configs" }, 400);
7632
+ json(res, { error: "bearer-token credentials are only supported for anthropic-compatible configs" }, 400);
7809
7633
  return;
7810
7634
  }
7811
7635
  if (credentialType === "api-key" && !API_KEY_CREDENTIAL_PROVIDERS.has(provider)) {
7812
- jsonNoCors(res, { error: "api-key credentials are not supported for this provider" }, 400);
7636
+ json(res, { error: "api-key credentials are not supported for this provider" }, 400);
7813
7637
  return;
7814
7638
  }
7815
7639
  try {
@@ -7824,35 +7648,31 @@ function createRouter(dbOrDeps) {
7824
7648
  return checked;
7825
7649
  });
7826
7650
  if (!target.ok) {
7827
- jsonNoCors(res, credentialRefusalBody(target), target.status);
7651
+ json(res, credentialRefusalBody(target), target.status);
7828
7652
  return;
7829
7653
  }
7830
- jsonNoCors(res, { saved: true, created: target.created });
7654
+ json(res, { saved: true, created: target.created });
7831
7655
  } catch (err) {
7832
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to save credential" }, 500);
7656
+ json(res, { error: err instanceof Error ? err.message : "Failed to save credential" }, 500);
7833
7657
  }
7834
- }).catch(() => jsonNoCors(res, { error: "Invalid request body" }, 400));
7658
+ }).catch(() => json(res, { error: "Invalid request body" }, 400));
7835
7659
  return;
7836
7660
  }
7837
7661
  if (path === "/api/auth/credential/move" && req.method === "POST") {
7838
- if (refuseCrossSiteRequest(req) !== void 0) {
7839
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
7840
- return;
7841
- }
7842
7662
  readJsonBody(req).then(async (raw) => {
7843
7663
  const parsed = import_agent_qa_schema11.LlmCredentialMoveRequestSchema.safeParse(raw);
7844
7664
  if (!parsed.success) {
7845
- jsonNoCors(res, { error: "invalid_setup_input" }, 400);
7665
+ json(res, { error: "invalid_setup_input" }, 400);
7846
7666
  return;
7847
7667
  }
7848
7668
  const from = checkCredentialName(parsed.data.from);
7849
7669
  const to = checkCredentialName(parsed.data.to);
7850
7670
  if (!from.ok || !to.ok) {
7851
- jsonNoCors(res, { error: "name_invalid" }, 400);
7671
+ json(res, { error: "name_invalid" }, 400);
7852
7672
  return;
7853
7673
  }
7854
7674
  if (!configManager) {
7855
- jsonNoCors(res, { error: "Config manager is required to move credentials" }, 400);
7675
+ json(res, { error: "Config manager is required to move credentials" }, 400);
7856
7676
  return;
7857
7677
  }
7858
7678
  try {
@@ -7870,25 +7690,21 @@ function createRouter(dbOrDeps) {
7870
7690
  return void 0;
7871
7691
  });
7872
7692
  if (refusal) {
7873
- jsonNoCors(res, credentialRefusalBody(refusal), refusal.status);
7693
+ json(res, credentialRefusalBody(refusal), refusal.status);
7874
7694
  return;
7875
7695
  }
7876
- jsonNoCors(res, { moved: true });
7696
+ json(res, { moved: true });
7877
7697
  } catch (err) {
7878
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to move credential" }, 500);
7698
+ json(res, { error: err instanceof Error ? err.message : "Failed to move credential" }, 500);
7879
7699
  }
7880
- }).catch(() => jsonNoCors(res, { error: "invalid_setup_input" }, 400));
7700
+ }).catch(() => json(res, { error: "invalid_setup_input" }, 400));
7881
7701
  return;
7882
7702
  }
7883
7703
  if (path === "/api/llm/test" && req.method === "POST") {
7884
- if (refuseCrossSiteRequest(req) !== void 0) {
7885
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
7886
- return;
7887
- }
7888
7704
  readJsonBody(req).then(async (body) => {
7889
7705
  const rawBody = body;
7890
7706
  if ("apiKey" in rawBody) {
7891
- jsonNoCors(res, {
7707
+ json(res, {
7892
7708
  success: false,
7893
7709
  error: "invalid_request",
7894
7710
  message: "apiKey is not accepted in LLM test requests. Save credentials for the named config instead."
@@ -7898,7 +7714,7 @@ function createRouter(dbOrDeps) {
7898
7714
  const providerName = typeof body.provider === "string" ? body.provider.trim() : "";
7899
7715
  const modelName = typeof body.model === "string" ? body.model.trim() : "";
7900
7716
  if (!providerName || !modelName) {
7901
- jsonNoCors(res, { success: false, error: "invalid_request", message: "provider and model are required" }, 400);
7717
+ json(res, { success: false, error: "invalid_request", message: "provider and model are required" }, 400);
7902
7718
  return;
7903
7719
  }
7904
7720
  const configName = typeof body.configName === "string" ? body.configName.trim() : "";
@@ -7923,7 +7739,7 @@ function createRouter(dbOrDeps) {
7923
7739
  }
7924
7740
  const parsedConfig = import_agent_qa_core9.ModelConfigSchema.safeParse(llmCandidate);
7925
7741
  if (!parsedConfig.success) {
7926
- jsonNoCors(res, {
7742
+ json(res, {
7927
7743
  success: false,
7928
7744
  error: "invalid_request",
7929
7745
  message: parsedConfig.error.issues.map((issue) => `${issue.path.map(String).join(".") || "config"}: ${issue.message}`).join("; ")
@@ -7935,7 +7751,7 @@ function createRouter(dbOrDeps) {
7935
7751
  const coreAuth = await import("@vostride/agent-qa-core");
7936
7752
  const resolvedAuth = await coreAuth.resolveLLMAuth(configName, llmConfig2);
7937
7753
  if (resolvedAuth.kind === "missing") {
7938
- jsonNoCors(res, {
7754
+ json(res, {
7939
7755
  success: false,
7940
7756
  error: "missing_credential",
7941
7757
  message: resolvedAuth.message
@@ -7964,7 +7780,7 @@ function createRouter(dbOrDeps) {
7964
7780
  } finally {
7965
7781
  clearTimeout(timeout);
7966
7782
  }
7967
- jsonNoCors(res, {
7783
+ json(res, {
7968
7784
  success: true,
7969
7785
  model: modelName,
7970
7786
  provider: providerName,
@@ -7979,7 +7795,7 @@ function createRouter(dbOrDeps) {
7979
7795
  const providerMessage = withDeclaredMask(declaredMask, (mask) => mask(unsavedMessage));
7980
7796
  const errorClass = (0, import_agent_qa_schema11.classifyProviderError)(providerMessage ?? unsavedMessage, statusCode);
7981
7797
  const message = providerMessage === void 0 ? (0, import_agent_qa_schema11.providerErrorSentence)(errorClass, "").trimEnd() : (0, import_agent_qa_schema11.providerErrorSentence)(errorClass, providerMessage);
7982
- jsonNoCors(res, {
7798
+ json(res, {
7983
7799
  success: false,
7984
7800
  error: errorClass,
7985
7801
  message,
@@ -7989,7 +7805,7 @@ function createRouter(dbOrDeps) {
7989
7805
  ...unauthenticated ? { unauthenticated: true, authMessage } : {}
7990
7806
  });
7991
7807
  }
7992
- }).catch(() => jsonNoCors(res, { success: false, error: "invalid_request", message: "Invalid request body" }, 400));
7808
+ }).catch(() => json(res, { success: false, error: "invalid_request", message: "Invalid request body" }, 400));
7993
7809
  return;
7994
7810
  }
7995
7811
  if (path === "/api/cache/purge" && req.method === "POST") {
@@ -7998,56 +7814,15 @@ function createRouter(dbOrDeps) {
7998
7814
  return;
7999
7815
  }
8000
7816
  readJsonBody(req).then(async (body) => {
8001
- if (!body.file && !body.all) {
8002
- json(res, { error: "Either file or all is required" }, 400);
7817
+ if (body.all !== true) {
7818
+ json(res, { error: "all is required" }, 400);
8003
7819
  return;
8004
7820
  }
8005
7821
  try {
8006
7822
  const cfg = await readConfigValues(configManager);
8007
- const cacheDir = cfg.services?.cache?.dir ?? cfg.cache?.dir;
8008
- if (!cacheDir) {
8009
- json(res, { error: "Cache directory not configured" }, 400);
8010
- return;
8011
- }
8012
7823
  const configDir = deps.configPath ? (0, import_node_path8.dirname)((0, import_node_path8.resolve)(deps.configPath)) : process.cwd();
8013
- const resolvedCacheDir = (0, import_node_path8.resolve)(configDir, cacheDir);
8014
- let branchDirs;
8015
- try {
8016
- branchDirs = await (0, import_promises7.readdir)(resolvedCacheDir);
8017
- } catch (err) {
8018
- if (err.code === "ENOENT") {
8019
- json(res, { purged: 0 });
8020
- return;
8021
- }
8022
- throw err;
8023
- }
8024
- let purged = 0;
8025
- for (const branch of branchDirs) {
8026
- const branchPath = (0, import_node_path8.join)(resolvedCacheDir, branch);
8027
- let entryStat;
8028
- try {
8029
- entryStat = await (0, import_promises7.stat)(branchPath);
8030
- } catch {
8031
- continue;
8032
- }
8033
- if (!entryStat.isDirectory()) {
8034
- await (0, import_promises7.rm)(branchPath, { force: true });
8035
- continue;
8036
- }
8037
- let files;
8038
- try {
8039
- files = await (0, import_promises7.readdir)(branchPath);
8040
- } catch {
8041
- continue;
8042
- }
8043
- for (const file of files) {
8044
- if (!file.endsWith(".json")) continue;
8045
- await (0, import_promises7.rm)((0, import_node_path8.join)(branchPath, file), { force: true });
8046
- purged++;
8047
- }
8048
- await (0, import_promises7.rm)(branchPath, { recursive: true, force: true }).catch(() => {
8049
- });
8050
- }
7824
+ const cacheDir = cfg.services?.cache?.dir ?? import_agent_qa_core9.DEFAULT_AGENT_QA_CACHE_DIR;
7825
+ const purged = await (0, import_agent_qa_runner_local2.purgeActionCache)((0, import_node_path8.resolve)(configDir, cacheDir));
8051
7826
  json(res, { purged });
8052
7827
  } catch (err) {
8053
7828
  json(res, { error: err instanceof Error ? err.message : "Cache purge failed" }, 500);
@@ -8131,7 +7906,7 @@ function createRouter(dbOrDeps) {
8131
7906
  return;
8132
7907
  }
8133
7908
  const workspaceEnv = await readWorkspaceEnvVars(workspacePaths);
8134
- const result = await new import_agent_qa_runner_local2.DockerHookExecutor().execute(resolvedHook, {
7909
+ const result = await new import_agent_qa_runner_local2.DockerHookExecutor({ version: (0, import_agent_qa_core9.getAgentQaVersion)() }).execute(resolvedHook, {
8135
7910
  env: {
8136
7911
  ...workspaceEnv,
8137
7912
  ...validation.overrides
@@ -8167,7 +7942,7 @@ function createRouter(dbOrDeps) {
8167
7942
  variables: shown.variables,
8168
7943
  sandbox: {
8169
7944
  runtime: resolvedHook.runtime,
8170
- image: import_agent_qa_core9.RUNTIME_IMAGE_MAP[resolvedHook.runtime],
7945
+ image: (0, import_agent_qa_core9.hookRunnerImage)(resolvedHook.runtime, (0, import_agent_qa_core9.getAgentQaVersion)()),
8171
7946
  networkMode: resolvedHook.network ? "enabled" : "disabled",
8172
7947
  dockerVersion: null,
8173
7948
  networkLogsAvailable: shown.networkLogs.length > 0,
@@ -8371,12 +8146,8 @@ function createRouter(dbOrDeps) {
8371
8146
  return;
8372
8147
  }
8373
8148
  if (req.method === "GET" && /^\/api\/live-editor\/sessions\/[^/]+\/live-url$/.test(path)) {
8374
- if (refuseCrossSiteRequest(req) !== void 0) {
8375
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
8376
- return;
8377
- }
8378
8149
  if (!deps.mintEditorLiveUrl) {
8379
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
8150
+ json(res, { error: "live_view_unavailable" }, 503);
8380
8151
  return;
8381
8152
  }
8382
8153
  let editorSessionId;
@@ -8385,33 +8156,32 @@ function createRouter(dbOrDeps) {
8385
8156
  path.slice("/api/live-editor/sessions/".length, -"/live-url".length)
8386
8157
  );
8387
8158
  } catch {
8388
- jsonNoCors(res, { error: "unknown_session" }, 404);
8159
+ json(res, { error: "unknown_session" }, 404);
8389
8160
  return;
8390
8161
  }
8391
8162
  void deps.mintEditorLiveUrl(editorSessionId).then(
8392
8163
  (result) => {
8393
- jsonNoCors(res, result);
8164
+ json(res, result);
8394
8165
  },
8395
8166
  (error) => {
8396
8167
  const name = error instanceof Error ? error.name : "";
8397
8168
  if (name === "UnknownLiveEditorSessionError") {
8398
- jsonNoCors(res, { error: "unknown_session" }, 404);
8169
+ json(res, { error: "unknown_session" }, 404);
8399
8170
  return;
8400
8171
  }
8401
8172
  if (name === "LiveViewUnavailableError") {
8402
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
8173
+ json(res, { error: "live_view_unavailable" }, 503);
8403
8174
  return;
8404
8175
  }
8405
- jsonNoCors(res, { error: "live_url_mint_failed" }, 500);
8176
+ json(res, { error: "live_url_mint_failed" }, 500);
8406
8177
  }
8407
8178
  );
8408
8179
  return;
8409
8180
  }
8410
8181
  const liveEditorAuthStateMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)\/auth-state$/);
8411
8182
  if (liveEditorAuthStateMatch && req.method === "POST") {
8412
- if (refuseCrossSiteDisplayRead(req, res)) return;
8413
8183
  if (!sessionManager) {
8414
- jsonNoCors(res, { error: "Live editor not available" }, 503);
8184
+ json(res, { error: "Live editor not available" }, 503);
8415
8185
  return;
8416
8186
  }
8417
8187
  ;
@@ -8422,35 +8192,34 @@ function createRouter(dbOrDeps) {
8422
8192
  const body = await readJsonBody(req);
8423
8193
  const parsedName = import_agent_qa_core9.AuthStateNameSchema.safeParse(typeof body?.name === "string" ? body.name : "");
8424
8194
  if (!parsedName.success) {
8425
- jsonNoCors(res, { error: "Auth state name must be a lowercase slug." }, 400);
8195
+ json(res, { error: "Auth state name must be a lowercase slug." }, 400);
8426
8196
  return;
8427
8197
  }
8428
8198
  stateName = parsedName.data;
8429
8199
  session = sessionManager.getSession(decodeURIComponent(liveEditorAuthStateMatch[1]));
8430
8200
  if (!session) {
8431
- jsonNoCors(res, { error: "Session not found" }, 404);
8201
+ json(res, { error: "Session not found" }, 404);
8432
8202
  return;
8433
8203
  }
8434
8204
  if (typeof session.captureWebAuthState !== "function") {
8435
- jsonNoCors(res, {
8205
+ json(res, {
8436
8206
  error: `Could not save auth state "${stateName}" for target "${getSessionTargetNameForAuthState(session)}".`
8437
8207
  }, 500);
8438
8208
  return;
8439
8209
  }
8440
8210
  const authState = await session.captureWebAuthState(stateName, { replace: body?.replace === true });
8441
- jsonNoCors(res, { authState });
8211
+ json(res, { authState });
8442
8212
  } catch (err) {
8443
8213
  const targetName = getSessionTargetNameForAuthState(session);
8444
8214
  const response = buildAuthStateSaveErrorMessage(stateName || "unknown", targetName, err);
8445
- jsonNoCors(res, { error: response.message }, response.status);
8215
+ json(res, { error: response.message }, response.status);
8446
8216
  }
8447
8217
  })();
8448
8218
  return;
8449
8219
  }
8450
8220
  if (path === "/api/live-editor/sessions" && req.method === "POST") {
8451
- if (refuseCrossSiteDisplayRead(req, res)) return;
8452
8221
  if (!sessionManager) {
8453
- jsonNoCors(res, { error: "Live editor not available" }, 503);
8222
+ json(res, { error: "Live editor not available" }, 503);
8454
8223
  return;
8455
8224
  }
8456
8225
  ;
@@ -8458,16 +8227,10 @@ function createRouter(dbOrDeps) {
8458
8227
  try {
8459
8228
  const body = await readJsonBody(req);
8460
8229
  const targetName = typeof body?.targetName === "string" ? body.targetName : void 0;
8461
- const production = await resolveLiveTargetProduction(configManager, targetName);
8462
- const productionRefusal = liveProductionRefusal(production);
8463
- if (productionRefusal !== void 0) {
8464
- jsonNoCors(res, { error: productionRefusal.message }, 403);
8465
- return;
8466
- }
8467
8230
  const platform = body?.platform ?? "web";
8468
8231
  const runtimeLLM = await resolveDashboardRuntimeLLM(configManager, llmConfig, authFetch);
8469
8232
  if (!runtimeLLM.ok) {
8470
- jsonNoCors(res, { error: runtimeLLM.error }, 400);
8233
+ json(res, { error: runtimeLLM.error }, 400);
8471
8234
  return;
8472
8235
  }
8473
8236
  const setupHooks = Array.isArray(body?.setupHooks) ? body.setupHooks.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
@@ -8486,14 +8249,14 @@ function createRouter(dbOrDeps) {
8486
8249
  let resolvedHooks = /* @__PURE__ */ new Map();
8487
8250
  let hookRegistryError;
8488
8251
  if (!workspacePaths) {
8489
- jsonNoCors(res, { error: "Workspace path resolution not available" }, 500);
8252
+ json(res, { error: "Workspace path resolution not available" }, 500);
8490
8253
  return;
8491
8254
  }
8492
8255
  try {
8493
8256
  const envContent = await (0, import_promises7.readFile)(workspacePaths.envFile.absolutePath, "utf-8");
8494
8257
  envVars = (0, import_agent_qa_core9.parseEnvFile)(envContent);
8495
8258
  } catch {
8496
- jsonNoCors(res, { error: `workspace.envFile file could not be loaded: ${workspacePaths.envFile.workspaceRelativePath}` }, 400);
8259
+ json(res, { error: `workspace.envFile file could not be loaded: ${workspacePaths.envFile.workspaceRelativePath}` }, 400);
8497
8260
  return;
8498
8261
  }
8499
8262
  try {
@@ -8501,7 +8264,7 @@ function createRouter(dbOrDeps) {
8501
8264
  secretStore = import_agent_qa_runner2.SecretStore.fromEnvContent(secretsContent);
8502
8265
  secretRedactor = new import_agent_qa_runner2.SecretRedactor(secretStore);
8503
8266
  } catch {
8504
- jsonNoCors(res, { error: `workspace.secretsFile file could not be loaded: ${workspacePaths.secretsFile.workspaceRelativePath}` }, 400);
8267
+ json(res, { error: `workspace.secretsFile file could not be loaded: ${workspacePaths.secretsFile.workspaceRelativePath}` }, 400);
8505
8268
  return;
8506
8269
  }
8507
8270
  const hookCatalog = await readWorkspaceHooks(configManager, deps.configPath);
@@ -8512,6 +8275,11 @@ function createRouter(dbOrDeps) {
8512
8275
  ...suiteFileManager === void 0 ? {} : { findSuite: (id) => suiteFileManager.findBySuiteId(id) }
8513
8276
  };
8514
8277
  const plannerLimits = await resolveLivePlannerLimits(configManager, entityRef, entityReaders);
8278
+ const navigationTimeoutMs = await resolveLiveNavigationTimeoutMs(
8279
+ configManager,
8280
+ entityRef,
8281
+ entityReaders
8282
+ );
8515
8283
  const sessionSuiteRef = entityRef?.type === "suite" ? entityRef : void 0;
8516
8284
  const plannerLimitsForTest = (testId) => resolveLivePlannerLimits(
8517
8285
  configManager,
@@ -8522,7 +8290,6 @@ function createRouter(dbOrDeps) {
8522
8290
  {
8523
8291
  platform,
8524
8292
  targetName,
8525
- production: { productionTargetNames: production.productionTargetNames },
8526
8293
  plannerLimits,
8527
8294
  plannerLimitsForTest,
8528
8295
  llmConfig: runtimeLLM.llmConfig,
@@ -8536,6 +8303,7 @@ function createRouter(dbOrDeps) {
8536
8303
  hookRegistryError,
8537
8304
  url: body?.url,
8538
8305
  headless: body?.headless ?? false,
8306
+ navigationTimeoutMs,
8539
8307
  useDeviceName: typeof body?.useDeviceName === "string" ? body.useDeviceName : void 0,
8540
8308
  appState: body?.appState === "preserve" || body?.appState === "reset" ? body.appState : void 0,
8541
8309
  bundleId: typeof body?.bundleId === "string" ? body.bundleId : void 0,
@@ -8545,38 +8313,37 @@ function createRouter(dbOrDeps) {
8545
8313
  },
8546
8314
  entityRef
8547
8315
  );
8548
- jsonNoCors(res, { sessionId, sessionNumber }, 201);
8316
+ json(res, { sessionId, sessionNumber }, 201);
8549
8317
  } catch (err) {
8550
8318
  const message = err instanceof Error ? err.message : String(err);
8551
- const status = err instanceof LiveProductionRefusedError ? 403 : err instanceof LivePlannerLimitsRefusedError ? 400 : 500;
8552
- jsonNoCors(res, { error: message }, status);
8319
+ const refused = err instanceof LivePlannerLimitsRefusedError || err instanceof LiveNavigationTimeoutRefusedError;
8320
+ const status = refused ? 400 : 500;
8321
+ json(res, { error: message }, status);
8553
8322
  }
8554
8323
  })();
8555
8324
  return;
8556
8325
  }
8557
8326
  if (path === "/api/live-editor/sessions" && req.method === "GET") {
8558
- if (refuseCrossSiteDisplayRead(req, res)) return;
8559
8327
  if (!sessionManager) {
8560
- jsonNoCors(res, { error: "Live editor not available" }, 503);
8328
+ json(res, { error: "Live editor not available" }, 503);
8561
8329
  return;
8562
8330
  }
8563
- jsonNoCors(res, { sessions: sessionManager.listSessions() });
8331
+ json(res, { sessions: sessionManager.listSessions() });
8564
8332
  return;
8565
8333
  }
8566
8334
  const liveEditorSessionMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)$/);
8567
8335
  if (liveEditorSessionMatch && req.method === "DELETE") {
8568
- if (refuseCrossSiteDisplayRead(req, res)) return;
8569
8336
  if (!sessionManager) {
8570
- jsonNoCors(res, { error: "Live editor not available" }, 503);
8337
+ json(res, { error: "Live editor not available" }, 503);
8571
8338
  return;
8572
8339
  }
8573
8340
  const sessionId = liveEditorSessionMatch[1];
8574
8341
  (async () => {
8575
8342
  const terminated = await sessionManager.terminateSession(sessionId);
8576
8343
  if (terminated) {
8577
- jsonNoCors(res, { ok: true });
8344
+ json(res, { ok: true });
8578
8345
  } else {
8579
- jsonNoCors(res, { error: "Session not found" }, 404);
8346
+ json(res, { error: "Session not found" }, 404);
8580
8347
  }
8581
8348
  })();
8582
8349
  return;
@@ -8625,6 +8392,7 @@ function createRouter(dbOrDeps) {
8625
8392
  // src/queue/job-queue.ts
8626
8393
  var import_node_events = require("node:events");
8627
8394
  var import_node_os2 = require("node:os");
8395
+ var import_agent_qa_core10 = require("@vostride/agent-qa-core");
8628
8396
  var NOT_CANCELLABLE = { kind: "not-cancellable" };
8629
8397
  var JobQueue = class extends import_node_events.EventEmitter {
8630
8398
  db;
@@ -8647,8 +8415,7 @@ var JobQueue = class extends import_node_events.EventEmitter {
8647
8415
  this.pollInterval = setInterval(() => this.dispatchReported("a poll"), intervalMs);
8648
8416
  this.on("enqueued", () => this.dispatchReported("an enqueue"));
8649
8417
  }
8650
- // 2026-10-17 (444-16, D-50): the server closes the database after this; a run that settles later
8651
- // still frees its slot, and nothing reads the closed database.
8418
+ // The server closes the database after this; a run settling later still frees its slot.
8652
8419
  stop() {
8653
8420
  this.stopped = true;
8654
8421
  if (this.pollInterval) {
@@ -8734,7 +8501,10 @@ var JobQueue = class extends import_node_events.EventEmitter {
8734
8501
  this.processNext();
8735
8502
  } catch (error) {
8736
8503
  const message = error instanceof Error ? error.message : String(error);
8737
- console.error(`Queue could not dispatch the next run after ${trigger}: ${message}`);
8504
+ (0, import_agent_qa_core10.printRunText)(
8505
+ `Queue could not dispatch the next run after ${trigger}: ${message}`,
8506
+ (text) => console.error(text)
8507
+ );
8738
8508
  }
8739
8509
  }
8740
8510
  getActiveCount() {
@@ -8743,9 +8513,7 @@ var JobQueue = class extends import_node_events.EventEmitter {
8743
8513
  getConcurrency() {
8744
8514
  return this.concurrency;
8745
8515
  }
8746
- // 2026-10-16 (442-05, D-06 to D-08): only the owner cancels a running row; gone is an orphan.
8747
- // 442-15 (GAP-D2): own is the stamp this handle wrote, or equal non-null start times.
8748
- // 2026-10-17 (444-06, D-11): a running row is never written here; the run answers with its verdict.
8516
+ // Only the owner stops its running row, never written here; a gone owner's row is an orphan.
8749
8517
  cancel(runId) {
8750
8518
  const run = this.db.getRun(runId);
8751
8519
  if (!run) return NOT_CANCELLABLE;
@@ -8775,21 +8543,7 @@ var JobQueue = class extends import_node_events.EventEmitter {
8775
8543
  const run = this.db.getRun(runId);
8776
8544
  return run && run.status !== "running" && run.status !== "pending" ? { kind: "finished", status: run.status } : NOT_CANCELLABLE;
8777
8545
  }
8778
- // 2026-08-24 (Phase 402 plan 09, D-11): THE CHILD-RUN CANCELLATION FAN-OUT IS DELETED WITH THE
8779
- // MODEL THAT MINTED ITS CHILDREN.
8780
- //
8781
- // ~19 lines stood here walking the run's child rows and cancelling each member row. Under
8782
- // the one-run model a parallel suite is ONE run row with member handles (402-04) — nothing writes
8783
- // the run row's parent-link column any more (405-07 then deleted the column outright), so the
8784
- // walk could only ever have read an empty list while reading as live cancellation policy.
8785
- // Cancelling the run cancels the run.
8786
- //
8787
- // WHAT THIS DOES NOT TOUCH: `parallel` below. D-11 keeps the queue's parallel flag as CROSS-RUN
8788
- // concurrency of INDEPENDENT jobs, which is what `findNextEligible` has always used it for. What
8789
- // was deleted is parallel-SUITE semantics, not the queue's ability to run two unrelated jobs.
8790
- //
8791
- // HISTORICAL ROWS: a pre-402 suite run still lists its members on the read side; cancelling
8792
- // such a run now leaves those member rows in whatever state their own process left them.
8546
+ // `parallel` is cross-run concurrency of independent jobs; a suite is one run row.
8793
8547
  findNextEligible() {
8794
8548
  if (this.activeCount >= this.concurrency) return void 0;
8795
8549
  const pending = this.db.getPendingRuns();
@@ -8816,17 +8570,17 @@ var import_promises8 = require("node:fs/promises");
8816
8570
  var import_node_path9 = require("node:path");
8817
8571
  var import_agent_qa_ids3 = require("@vostride/agent-qa-ids");
8818
8572
  var import_yaml8 = require("yaml");
8819
- var import_agent_qa_core10 = require("@vostride/agent-qa-core");
8573
+ var import_agent_qa_core11 = require("@vostride/agent-qa-core");
8820
8574
  var SuiteFileManager = class {
8821
8575
  constructor(workspace, testFileManager) {
8822
8576
  this.workspace = workspace;
8823
8577
  this.testFileManager = testFileManager;
8824
8578
  }
8825
8579
  testFileManager;
8826
- // 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.
8580
+ // Each list call reads the workspace itself; each file is written whole, one writer at a time.
8827
8581
  underLock = createPathLock();
8828
8582
  async list() {
8829
- const records = await (0, import_agent_qa_core10.discoverWorkspaceFiles)({ workspace: this.workspace, kind: "suite" });
8583
+ const records = await (0, import_agent_qa_core11.discoverWorkspaceFiles)({ workspace: this.workspace, kind: "suite" });
8830
8584
  const files = [];
8831
8585
  for (const record of records) {
8832
8586
  try {
@@ -8888,7 +8642,7 @@ var SuiteFileManager = class {
8888
8642
  if (!trimmedPath) {
8889
8643
  throw new Error("Suite path is required");
8890
8644
  }
8891
- const target = await (0, import_agent_qa_core10.resolveWorkspaceFileTarget)({
8645
+ const target = await (0, import_agent_qa_core11.resolveWorkspaceFileTarget)({
8892
8646
  workspace: this.workspace,
8893
8647
  kind: "suite",
8894
8648
  filePath: trimmedPath,
@@ -8912,7 +8666,7 @@ var SuiteFileManager = class {
8912
8666
  async validate(content) {
8913
8667
  try {
8914
8668
  const parsed = (0, import_yaml8.parse)(content);
8915
- const result = import_agent_qa_core10.SuiteDefinitionSchema.safeParse(parsed);
8669
+ const result = import_agent_qa_core11.SuiteDefinitionSchema.safeParse(parsed);
8916
8670
  if (!result.success) {
8917
8671
  return {
8918
8672
  valid: false,
@@ -8950,7 +8704,7 @@ var SuiteFileManager = class {
8950
8704
  }
8951
8705
  }
8952
8706
  async resolveTarget(relativePath, requireExisting) {
8953
- return (0, import_agent_qa_core10.resolveWorkspaceFileTarget)({
8707
+ return (0, import_agent_qa_core11.resolveWorkspaceFileTarget)({
8954
8708
  workspace: this.workspace,
8955
8709
  kind: "suite",
8956
8710
  filePath: relativePath,
@@ -8962,7 +8716,7 @@ var SuiteFileManager = class {
8962
8716
  // src/execution/appium-manager.ts
8963
8717
  var import_node_http = require("node:http");
8964
8718
  var import_node_child_process2 = require("node:child_process");
8965
- var import_agent_qa_core11 = require("@vostride/agent-qa-core");
8719
+ var import_agent_qa_core12 = require("@vostride/agent-qa-core");
8966
8720
  var import_tree_kill = __toESM(require("tree-kill"), 1);
8967
8721
  var import_picocolors = __toESM(require("picocolors"), 1);
8968
8722
  var APPIUM_PREFIX = import_picocolors.default.magenta("[appium]");
@@ -8983,7 +8737,7 @@ var AppiumManager = class {
8983
8737
  this.startupTimeoutMs = opts?.startupTimeoutMs ?? 3e4;
8984
8738
  this.pollIntervalMs = opts?.pollIntervalMs ?? 500;
8985
8739
  this.logLevel = opts?.logLevel ?? "normal";
8986
- this.appiumResolver = opts?.appiumResolver ?? import_agent_qa_core11.resolveAppiumExecutable;
8740
+ this.appiumResolver = opts?.appiumResolver ?? import_agent_qa_core12.resolveAppiumExecutable;
8987
8741
  }
8988
8742
  async acquire() {
8989
8743
  await this.ensureRunning();
@@ -9086,23 +8840,23 @@ var AppiumManager = class {
9086
8840
  this.managed = true;
9087
8841
  child.on("error", (err) => {
9088
8842
  if (err.code === "ENOENT") {
9089
- console.error(`${APPIUM_PREFIX} Appium not found. ${(0, import_agent_qa_core11.formatAppiumInstallGuidance)()}`);
8843
+ console.error(`${APPIUM_PREFIX} Appium not found. ${(0, import_agent_qa_core12.formatAppiumInstallGuidance)()}`);
9090
8844
  } else {
9091
- console.error(`${APPIUM_PREFIX} Error: ${err.message}`);
8845
+ (0, import_agent_qa_core12.printRunText)(`${APPIUM_PREFIX} Error: ${err.message}`, (text) => console.error(text));
9092
8846
  }
9093
8847
  });
9094
8848
  child.stdout?.on("data", (chunk) => {
9095
8849
  if (this.logLevel !== "debug") return;
9096
8850
  const lines = chunk.toString().split("\n").filter(Boolean);
9097
8851
  for (const line of lines) {
9098
- console.log(`${APPIUM_PREFIX} ${import_picocolors.default.dim(line)}`);
8852
+ (0, import_agent_qa_core12.printRunText)(`${APPIUM_PREFIX} ${import_picocolors.default.dim(line)}`, (text) => console.log(text));
9099
8853
  }
9100
8854
  });
9101
8855
  child.stderr?.on("data", (chunk) => {
9102
8856
  if (this.logLevel !== "debug") return;
9103
8857
  const lines = chunk.toString().split("\n").filter(Boolean);
9104
8858
  for (const line of lines) {
9105
- console.error(`${APPIUM_PREFIX} ${import_picocolors.default.dim(line)}`);
8859
+ (0, import_agent_qa_core12.printRunText)(`${APPIUM_PREFIX} ${import_picocolors.default.dim(line)}`, (text) => console.error(text));
9106
8860
  }
9107
8861
  });
9108
8862
  const start = Date.now();
@@ -9184,10 +8938,7 @@ var SessionManager = class {
9184
8938
  this.deps = deps;
9185
8939
  }
9186
8940
  sessions = /* @__PURE__ */ new Map();
9187
- // Per-entity counter keyed by `${entityType}:${entityId}`.
9188
- // Monotonic and ephemeral: resets on server restart per CONTEXT D-30.
9189
- // Never decremented on terminate — a re-open of a terminated entity yields
9190
- // the next number so "Session #3" after "#2" ends does not confuse users.
8941
+ // Monotonic per entity and reset on restart, so a reopened entity gets the next number.
9191
8942
  counters = /* @__PURE__ */ new Map();
9192
8943
  async createSession(config, entity) {
9193
8944
  if (!config.llmConfig) {
@@ -9244,7 +8995,8 @@ var SessionManager = class {
9244
8995
  };
9245
8996
 
9246
8997
  // src/live-editor/ws-handler.ts
9247
- var import_agent_qa_core12 = require("@vostride/agent-qa-core");
8998
+ var import_node_util4 = require("node:util");
8999
+ var import_agent_qa_core13 = require("@vostride/agent-qa-core");
9248
9000
 
9249
9001
  // src/live-editor/types.ts
9250
9002
  var LIVE_EDITOR_UNKNOWN_SESSION_CLOSE_CODE = 4404;
@@ -9431,12 +9183,14 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
9431
9183
  try {
9432
9184
  raw = await testFileManager.read(msg.path);
9433
9185
  } catch (readErr) {
9434
- console.error(`[ws-handler] execute-test: read failed for path=${msg.path}`, readErr);
9186
+ const line = `[ws-handler] execute-test: read failed for path=${msg.path}`;
9187
+ (0, import_agent_qa_core13.printRunText)((0, import_node_util4.format)(line, readErr), () => console.error(line, readErr));
9435
9188
  throw new Error(`Cannot read test file "${msg.path}": ${readErr?.message ?? String(readErr)}`);
9436
9189
  }
9437
- const parsed = (0, import_agent_qa_core12.parseTestFile)(raw, msg.path);
9190
+ const parsed = (0, import_agent_qa_core13.parseTestFile)(raw, msg.path);
9438
9191
  if (parsed.errors.length > 0) {
9439
- console.error(`[ws-handler] execute-test: parse errors for ${msg.path}`, parsed.errors);
9192
+ const line = `[ws-handler] execute-test: parse errors for ${msg.path}`;
9193
+ (0, import_agent_qa_core13.printRunText)((0, import_node_util4.format)(line, parsed.errors), () => console.error(line, parsed.errors));
9440
9194
  throw new Error(`Parse error in ${msg.path}: ${parsed.errors[0].message}`);
9441
9195
  }
9442
9196
  const testDef = parsed.tests[0];
@@ -9446,7 +9200,13 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
9446
9200
  const stepInstructions = testDef.steps.map(
9447
9201
  (s) => typeof s === "string" ? s : s.step
9448
9202
  );
9449
- console.log(`[ws-handler] execute-test: starting "${testDef.name}" (${stepInstructions.length} steps, setup=${(testDef.setup ?? []).length}, teardown=${(testDef.teardown ?? []).length})`);
9203
+ const stepCount = stepInstructions.length;
9204
+ const setupCount = (testDef.setup ?? []).length;
9205
+ const teardownCount = (testDef.teardown ?? []).length;
9206
+ (0, import_agent_qa_core13.printRunText)(
9207
+ `[ws-handler] execute-test: starting "${testDef.name}" (${stepCount} steps, setup=${setupCount}, teardown=${teardownCount})`,
9208
+ (text) => console.log(text)
9209
+ );
9450
9210
  reply({
9451
9211
  type: "test-start",
9452
9212
  test: {
@@ -9468,7 +9228,11 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
9468
9228
  teardown: testDef.teardown ?? []
9469
9229
  }
9470
9230
  );
9471
- console.log(`[ws-handler] execute-test: finished "${testDef.name}" status=${result.status} duration=${result.duration}ms${result.error ? ` error=${result.error}` : ""}`);
9231
+ const errorPart = result.error ? ` error=${result.error}` : "";
9232
+ (0, import_agent_qa_core13.printRunText)(
9233
+ `[ws-handler] execute-test: finished "${testDef.name}" status=${result.status} duration=${result.duration}ms${errorPart}`,
9234
+ (text) => console.log(text)
9235
+ );
9472
9236
  reply({
9473
9237
  type: "test-complete",
9474
9238
  test: {
@@ -9481,11 +9245,9 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
9481
9245
  });
9482
9246
  reply({ type: "session-state", state: session.getState() });
9483
9247
  } catch (err) {
9484
- console.error(`[ws-handler] execute-test: caught error for path=${msg.path}`, {
9485
- message: err?.message,
9486
- stack: err?.stack,
9487
- code: err?.code
9488
- });
9248
+ const line = `[ws-handler] execute-test: caught error for path=${msg.path}`;
9249
+ const detail = { message: err?.message, stack: err?.stack, code: err?.code };
9250
+ (0, import_agent_qa_core13.printRunText)((0, import_node_util4.format)(line, detail), () => console.error(line, detail));
9489
9251
  reply({
9490
9252
  type: "test-error",
9491
9253
  test: {
@@ -9742,8 +9504,7 @@ function renderLiveEmbedPage(input) {
9742
9504
  if (pending === null) return;
9743
9505
  var header = pending;
9744
9506
  pending = null;
9745
- // NO MIME TYPE: the decoder sniffs the bytes, so a JPEG screencast frame and a PNG snapshot
9746
- // both paint in every host, not only in Chromium (428 RESEARCH A2).
9507
+ // No MIME type: the decoder sniffs, so JPEG and PNG frames paint in every host.
9747
9508
  var blob = new Blob([event.data]);
9748
9509
  createImageBitmap(blob).then(function (bitmap) {
9749
9510
  if (canvas.width !== header.width || canvas.height !== header.height) {
@@ -9767,8 +9528,7 @@ function renderLiveEmbedPage(input) {
9767
9528
  try { msg = JSON.parse(event.data); } catch (e) { return; }
9768
9529
 
9769
9530
  if (msg.type === 'ready') {
9770
- // THE PAINTER IS CHOSEN BY THE DECLARED KIND \u2014 never by a browser name, an adapter class or
9771
- // a transport guess (LIVE-04's prohibition). A snapshot paints on the same canvas (428 D-11).
9531
+ // The declared kind picks the painter, never a browser name, adapter or transport guess.
9772
9532
  var kind = msg.stream ? msg.stream.kind : undefined;
9773
9533
  if (kind === 'cdp-screencast' || kind === 'snapshot') {
9774
9534
  setState('connecting', 'waiting for the first frame\u2026');
@@ -9815,8 +9575,7 @@ function renderLiveEmbedPage(input) {
9815
9575
  report({ event: 'lost' });
9816
9576
  });
9817
9577
 
9818
- // A HOST THAT RE-MINTS replaces the fragment; the socket renews in place rather than reloading,
9819
- // so a renewal never costs the viewer the stream it is watching (D-02).
9578
+ // A re-minting host replaces the fragment; the socket renews in place and the stream continues.
9820
9579
  window.addEventListener('hashchange', function () {
9821
9580
  var next = location.hash.slice(1);
9822
9581
  if (next === '' || socket.readyState !== 1) return;
@@ -10025,7 +9784,7 @@ function createFrameProxy(deps) {
10025
9784
  }
10026
9785
 
10027
9786
  // src/server/server.ts
10028
- var import_agent_qa_core13 = require("@vostride/agent-qa-core");
9787
+ var import_agent_qa_core14 = require("@vostride/agent-qa-core");
10029
9788
  var MIME_TYPES = {
10030
9789
  ".html": "text/html; charset=utf-8",
10031
9790
  ".js": "application/javascript",
@@ -10044,9 +9803,10 @@ var MIME_TYPES = {
10044
9803
  ".webp": "image/webp"
10045
9804
  };
10046
9805
  var DashboardRemoteBindRefusedError = class extends Error {
10047
- constructor(host) {
9806
+ constructor(host, port) {
9807
+ const p = String(port === 0 ? 3470 : port);
10048
9808
  super(
10049
- `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.`
9809
+ `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}/.`
10050
9810
  );
10051
9811
  this.host = host;
10052
9812
  this.name = "DashboardRemoteBindRefusedError";
@@ -10057,8 +9817,8 @@ async function startServer(opts) {
10057
9817
  const { db, port = 3470, uiDir, workspacePaths, configPath, llmConfig, authFetch, agentHost, executionBackend, runService, runServiceReadiness, liveView, liveEditorSubjects, postProcessingRelay } = opts;
10058
9818
  const runServiceAttach = opts.runServiceAttach ?? runService?.attach;
10059
9819
  const host = opts.host?.trim() || DEFAULT_DASHBOARD_HOST;
10060
- if (!(0, import_agent_qa_core13.isLoopbackHost)(host)) {
10061
- throw new DashboardRemoteBindRefusedError(host);
9820
+ if (!(0, import_agent_qa_core14.isLoopbackHost)(host)) {
9821
+ throw new DashboardRemoteBindRefusedError(host, port);
10062
9822
  }
10063
9823
  const resolvedConfigPath = configPath ?? (0, import_node_path10.resolve)("agent-qa.config.yaml");
10064
9824
  const configManager = new ConfigManager(resolvedConfigPath, opts.declaredSecretMask);
@@ -10093,7 +9853,7 @@ async function startServer(opts) {
10093
9853
  failureSummary = "Test timed out -- process was killed";
10094
9854
  } else if (details.refusedBeforeStart) {
10095
9855
  dbStatus = "failed";
10096
- refusalProse = details.neutralReason ?? details.reason;
9856
+ refusalProse = details.reason;
10097
9857
  failureSummary = refusalProse ? `Refused before the run started: ${refusalProse}`.slice(0, 300) : "Refused before the run started";
10098
9858
  } else if (status === "completed") {
10099
9859
  dbStatus = "failed";
@@ -10174,7 +9934,10 @@ async function startServer(opts) {
10174
9934
  mobileRuns.set(run.id, platform);
10175
9935
  } catch (err) {
10176
9936
  const message = err instanceof Error ? err.message : String(err);
10177
- console.error(`Failed to start Appium for run ${run.id}: ${message}`);
9937
+ (0, import_agent_qa_core14.printRunText)(
9938
+ `Failed to start Appium for run ${run.id}: ${message}`,
9939
+ (text) => console.error(text)
9940
+ );
10178
9941
  db.updateRun(run.id, {
10179
9942
  status: "failed",
10180
9943
  failureSummary: `Appium server failed to start: ${message}`,
@@ -10194,19 +9957,9 @@ async function startServer(opts) {
10194
9957
  runId: run.id,
10195
9958
  args,
10196
9959
  source: meta.isSuite ? "suite" : "dashboard",
10197
- // 2026-10-01 (432-14, D-14): no confirm scope is read off the row any more. The browser that
10198
- // triggered a run is not asked anything, so nothing about it has to survive the wait.
10199
9960
  attributes: run.attributes,
10200
- // T-401-G26 (401-23): a persisted row's timeout is VALIDATED at the seam that consumes it.
10201
- // `in-process-backend.ts:211` reads `opts.timeout ?? defaultTimeout` and validates AFTER, so
10202
- // any non-nullish nonsense here skips the default and leaves the run with no reclamation
10203
- // timer at all. An unusable value falls back to the backend's default rather than a number
10204
- // this call invented.
9961
+ // The backend takes `opts.timeout ?? defaultTimeout`, so an unusable value falls back here.
10205
9962
  timeout: sanitizeQueuedExecutionTimeout(meta.timeout),
10206
- // THE LEASE ABOVE IS UNCHANGED; ONLY ITS DELIVERY IS. `acquireLease` still runs where it ran,
10207
- // still keys on this run id and still owns the bundled Appium's lifecycle (design-of-record).
10208
- // What used to ride `AGENT_QA_APPIUM_URL` in a child's environment now travels as an
10209
- // argument, because there is no child to carry an environment.
10210
9963
  ...isMobile ? { appiumUrl: appiumManager.getUrl() } : {}
10211
9964
  });
10212
9965
  });
@@ -10217,16 +9970,14 @@ async function startServer(opts) {
10217
9970
  appiumManager,
10218
9971
  configManager,
10219
9972
  configPath: resolvedConfigPath,
10220
- // 428 D-07: each created session registers here, which is what the frame socket attaches.
9973
+ // Each created session registers here, which is what the frame socket attaches.
10221
9974
  ...liveEditorSubjects === void 0 ? {} : { liveEditorSubjects }
10222
9975
  });
10223
9976
  const servesLiveView = liveView !== void 0 && (runServiceAttach !== void 0 || liveEditorSubjects !== void 0);
10224
9977
  const apiRouter = createRouter({
10225
9978
  db,
10226
9979
  workspacePaths,
10227
- // 401-07: conditionally spread on the same house rule `agentHost` follows — an absent
10228
- // dependency is an ABSENT field, never a present `undefined`, so the router's own 503 arm is
10229
- // what decides, in one place.
9980
+ // Absent dependencies are absent fields, so the router's own 503 arm decides in one place.
10230
9981
  ...executionBackend === void 0 ? {} : { executionBackend },
10231
9982
  jobQueue,
10232
9983
  testFileManager,
@@ -10237,27 +9988,13 @@ async function startServer(opts) {
10237
9988
  llmConfig,
10238
9989
  authFetch,
10239
9990
  sessionManager,
10240
- // 400-01: conditionally spread on the house rule an absent dependency is an ABSENT field and
10241
- // never a present `undefined` — the router's own 501 arm is what decides, in one place.
9991
+ // Absent dependencies are absent fields, so the router's own 501 arm decides in one place.
10242
9992
  ...agentHost === void 0 ? {} : { agentHost },
10243
- // 402-05 (D-15): the same conditional-spread house rule. Absent means a legacy mount rather
10244
- // than an unconfigured service, and the router's own 503 arm is what decides, in one place.
10245
9993
  ...runServiceReadiness === void 0 ? {} : { runServiceReadiness },
10246
- // 402-08 (D-01): the LOCAL run-stream door's attach port, READ OFF `runService` rather than
10247
- // composed again. That derivation is the "one session registry serves both doors" claim in its
10248
- // structural form — there is no second wiring a composition root could forget or point
10249
- // somewhere else, and the two doors cannot end up attached to different registries. Absent
10250
- // `runService` means no registry exists at all, so the same conditional-spread house rule
10251
- // applies and the router's own 503 arm decides.
9994
+ // Read off `runService` by default, so both doors attach to one session registry.
10252
9995
  ...runServiceAttach === void 0 ? {} : { runServiceAttach },
10253
- // 403 D-03: the mint route's port, on the same conditional-spread house rule — absent means
10254
- // this host serves no live view and the route's own 503 arm decides, in one place.
10255
9996
  ...liveView === void 0 ? {} : { mintLiveUrl: liveView.mintLiveUrl },
10256
- // 428 D-07: the editor mint, on the same house rule — absent means the editor mint route's own
10257
- // 503 decides.
10258
9997
  ...liveView?.mintEditorLiveUrl === void 0 ? {} : { mintEditorLiveUrl: liveView.mintEditorLiveUrl },
10259
- // 404-05 (D-16): the same conditional-spread house rule — absent means this host serves no
10260
- // post-processing push channel and the stream arm's own 503 decides, in one place.
10261
9998
  ...postProcessingRelay === void 0 ? {} : { postProcessingRelay }
10262
9999
  });
10263
10000
  const runServiceRouter = runService === void 0 ? void 0 : createRunServiceRoutes(runService);
@@ -10277,13 +10014,15 @@ async function startServer(opts) {
10277
10014
  apiRouter(req, res);
10278
10015
  return;
10279
10016
  }
10017
+ if (refuseNonLoopbackHost(req) !== void 0) {
10018
+ const address = server.address();
10019
+ const boundPort = typeof address === "object" && address !== null ? address.port : port;
10020
+ refuseForeignHostPage(res, boundPort);
10021
+ return;
10022
+ }
10280
10023
  if (req.method === "OPTIONS") {
10281
- res.writeHead(204, {
10282
- "Access-Control-Allow-Origin": "*",
10283
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
10284
- "Access-Control-Allow-Headers": "Content-Type"
10285
- });
10286
- res.end();
10024
+ res.writeHead(404, { "Content-Type": "text/plain" });
10025
+ res.end("Not Found");
10287
10026
  return;
10288
10027
  }
10289
10028
  if (req.method === "GET" && path.startsWith(LIVE_EMBED_PATH_PREFIX)) {
@@ -10327,8 +10066,7 @@ async function startServer(opts) {
10327
10066
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
10328
10067
  res.writeHead(200, {
10329
10068
  "Content-Type": contentType,
10330
- "Content-Length": content.length,
10331
- "Access-Control-Allow-Origin": "*"
10069
+ "Content-Length": content.length
10332
10070
  });
10333
10071
  res.end(content);
10334
10072
  return;
@@ -10340,8 +10078,7 @@ async function startServer(opts) {
10340
10078
  const content = await (0, import_promises9.readFile)(indexPath);
10341
10079
  res.writeHead(200, {
10342
10080
  "Content-Type": "text/html",
10343
- "Content-Length": content.length,
10344
- "Access-Control-Allow-Origin": "*"
10081
+ "Content-Length": content.length
10345
10082
  });
10346
10083
  res.end(content);
10347
10084
  return;
@@ -10426,7 +10163,8 @@ async function startServer(opts) {
10426
10163
  session.clearIdleTimer();
10427
10164
  const handler = createSessionHandler(session, () => {
10428
10165
  void sessionManager.terminateSession(sessionId).catch((error) => {
10429
- console.error(`[live-editor] could not end session ${sessionId}`, error);
10166
+ const line = `[live-editor] could not end session ${sessionId}`;
10167
+ (0, import_agent_qa_core14.printRunText)((0, import_node_util5.format)(line, error), () => console.error(line, error));
10430
10168
  });
10431
10169
  }, (event) => {
10432
10170
  db.insertTokenEvent(event);
@@ -10459,7 +10197,7 @@ async function startServer(opts) {
10459
10197
  server.listen(port, host, () => {
10460
10198
  server.off("error", onError);
10461
10199
  console.log(
10462
- (0, import_agent_qa_core13.isLoopbackHost)(host) ? `Dashboard running at http://localhost:${port}` : `Dashboard running at http://${host.includes(":") ? `[${host}]` : host}:${port}`
10200
+ (0, import_agent_qa_core14.isLoopbackHost)(host) ? `Dashboard running at http://localhost:${port}` : `Dashboard running at http://${host.includes(":") ? `[${host}]` : host}:${port}`
10463
10201
  );
10464
10202
  resolve5({
10465
10203
  server,