@vostride/agent-qa-dashboard 0.2.3 → 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.
package/dist/index.cjs CHANGED
@@ -98,7 +98,6 @@ __export(index_exports, {
98
98
  renderLiveEmbedPage: () => renderLiveEmbedPage,
99
99
  resolveDashboardDbPath: () => resolveDashboardDbPath,
100
100
  resolveRunOwnerHost: () => resolveRunOwnerHost,
101
- runDenialOf: () => runDenialOf,
102
101
  runRowIdentity: () => runRowIdentity,
103
102
  serializeConfigWrite: () => serializeConfigWrite,
104
103
  servePostProcessingRead: () => servePostProcessingRead,
@@ -136,7 +135,6 @@ var MIGRATIONS = [
136
135
  llm_provider TEXT,
137
136
  parallel INTEGER DEFAULT 0,
138
137
  error_log TEXT,
139
- memory_log TEXT,
140
138
  test_id TEXT,
141
139
  suite_id TEXT,
142
140
  owner_pid INTEGER,
@@ -159,9 +157,7 @@ var MIGRATIONS = [
159
157
  variables TEXT,
160
158
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
161
159
  hook_id TEXT,
162
- -- 405 D-01: where in the run tree this hook ran. Sourced from HookFinishedPayload's own
163
- -- optional testIndex/stepIndex; NULL means a run-scoped hook, which is a real state and
164
- -- not a missing value.
160
+ -- Where in the run tree this hook ran; NULL means a run-scoped hook, not a missing value.
165
161
  test_index INTEGER,
166
162
  step_index INTEGER
167
163
  )`,
@@ -175,10 +171,7 @@ var MIGRATIONS = [
175
171
  )`,
176
172
  `CREATE TABLE IF NOT EXISTS run_artifacts (
177
173
  run_id TEXT PRIMARY KEY REFERENCES runs(id) ON DELETE CASCADE,
178
- -- 2026-08-27 (405-07, RESEARCH Open Q4): TWO KINDS, because two is all anything mints.
179
- -- The set used to admit two more, named for the parent/child run tree 402-09 retired; no
180
- -- writer has produced either since, so admitting them was the schema agreeing to store a
181
- -- distinction the product no longer makes.
174
+ -- Two kinds, because no writer mints any other.
182
175
  kind TEXT NOT NULL CHECK(kind IN ('test', 'unknown')),
183
176
  schema_version INTEGER NOT NULL,
184
177
  payload TEXT NOT NULL DEFAULT '{}',
@@ -186,17 +179,7 @@ var MIGRATIONS = [
186
179
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
187
180
  updated_at TEXT DEFAULT CURRENT_TIMESTAMP
188
181
  )`,
189
- // ---- The v2 bus-shaped projection (405 D-01) ----------------------------------------
190
- //
191
- // THE COLUMNS ARE THE PAYLOAD'S OWN NAMES, snake_cased and nothing else. No field is
192
- // re-derived, renamed or re-interpreted on the way in — that is D-01's whole claim, and
193
- // it is what makes the read a mirror rather than a second opinion. Nested payload objects
194
- // (`planned_action`, `grounding`, `result`, `artifact_refs`) are `JSON.stringify` of the
195
- // field VERBATIM; the projection never reaches inside one.
196
- //
197
- // `test_index` IS THE HANDLE (RESEARCH D4). `handleId(runId, testIndex)` is how this
198
- // product spells run-scoped member identity, so minting a second id here would be a
199
- // second spelling of the same fact.
182
+ // Columns are the payload's names snake_cased; `test_index` is the handle, so no second id.
200
183
  `CREATE TABLE IF NOT EXISTS run_members (
201
184
  run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
202
185
  test_index INTEGER NOT NULL,
@@ -211,15 +194,12 @@ var MIGRATIONS = [
211
194
  duration_ms INTEGER,
212
195
  failure_summary TEXT,
213
196
  retry_count INTEGER,
214
- -- D-17 ordinal. Starts at 1 and rises on testRetryScheduled; never reset by an upsert.
197
+ -- Starts at 1 and rises on testRetryScheduled; never reset by an upsert.
215
198
  attempts INTEGER NOT NULL DEFAULT 1,
216
199
  recording_json TEXT,
217
200
  PRIMARY KEY (run_id, test_index)
218
201
  )`,
219
- // The token columns are NULLABLE AND NEVER DEFAULT 0 (D-05/D-06). The retired v1 projection
220
- // declared these `INTEGER DEFAULT 0`, and that zero was a measurement nobody made — the exact
221
- // lie this shape refuses to be able to tell. 405-04 landed the producer, so a step the runner
222
- // measured carries real counts and a step it did not carries NULL, which stays honest.
202
+ // Token columns are nullable with no default, so a step nobody measured carries NULL, not 0.
223
203
  `CREATE TABLE IF NOT EXISTS run_steps (
224
204
  run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
225
205
  test_index INTEGER NOT NULL,
@@ -741,9 +721,7 @@ var DashboardDatabase = class {
741
721
  if (owner) this.ownStamps.set(id, owner);
742
722
  return id;
743
723
  }
744
- // 2026-10-17 (443-02, D-07): one statement that inserts a fresh row or adopts this handle's own
745
- // running row; any other row is refused and left untouched. An adopt that names no name or
746
- // attributes keeps the row's own (443-04).
724
+ // Inserts a fresh row or adopts this handle's own running row; any other row is left untouched.
747
725
  claimRunningRow(id, fields) {
748
726
  const stamp = this.ownStamps.get(id) ?? this.readOwnStamp();
749
727
  const result = this.db.prepare(`
@@ -812,10 +790,6 @@ var DashboardDatabase = class {
812
790
  fields.push("error_log = ?");
813
791
  values.push(updates.errorLog);
814
792
  }
815
- if (updates.memoryLog !== void 0) {
816
- fields.push("memory_log = ?");
817
- values.push(updates.memoryLog);
818
- }
819
793
  if (updates.attributes !== void 0) {
820
794
  fields.push("attributes = ?");
821
795
  values.push(JSON.stringify(updates.attributes));
@@ -918,26 +892,8 @@ var DashboardDatabase = class {
918
892
  this.ownStamps.set(id, stamp);
919
893
  return true;
920
894
  }
921
- // ---- The v2 bus-shaped projection (Phase 405 plan 01, D-01) ---------------------------------
922
- //
923
- // SIX MEMBERS, AND EVERY ONE OF THEM MIRRORS. The columns are the bus payload's own names; the
924
- // nested objects are `JSON.stringify` of the field verbatim. Nothing in this block decides what
925
- // a status means, re-derives a duration or fills a token count — a projection that interpreted
926
- // would be a second opinion about a run the event log already settled.
927
- /**
928
- * Write or refresh ONE member row, keyed by the handle `(run_id, test_index)`.
929
- *
930
- * AN UPSERT AND NOT TWO WRITES, because the member row is fed by two events at two times:
931
- * `testStarted` knows the identity fields and `testFinished` knows the outcome — and D-18
932
- * requires a member whose stream begins with a terminal event to render anyway, so the second
933
- * one alone has to be able to create the row.
934
- *
935
- * ONLY SUPPLIED FIELDS MOVE. `COALESCE(excluded.x, run_members.x)` on every optional column, so
936
- * a `testFinished` that carries no name cannot blank the name `testStarted` wrote. `attempts` is
937
- * excluded from the update entirely: it is the D-17 retry ordinal, owned by
938
- * `incrementRunMemberAttempts`, and an upsert that reset it would erase the fact the header
939
- * renders.
940
- */
895
+ // The projection mirrors bus payload names verbatim and interprets nothing.
896
+ // An upsert because a member's first event may be terminal; `COALESCE` keeps unsupplied fields.
941
897
  upsertRunMember(member) {
942
898
  this.db.prepare(`
943
899
  INSERT INTO run_members (
@@ -975,26 +931,13 @@ var DashboardDatabase = class {
975
931
  member.recording === void 0 ? null : JSON.stringify(member.recording)
976
932
  );
977
933
  }
978
- /**
979
- * Raise ONE member's attempt ordinal (D-17). A no-op when the member row does not exist yet —
980
- * a retry scheduled before any identity event is a real ordering, and inventing a row here
981
- * would put a member on the page that nothing has said anything about.
982
- */
934
+ // A no-op without a member row, so a retry before any identity event adds no member.
983
935
  incrementRunMemberAttempts(runId, testIndex) {
984
936
  this.db.prepare(
985
937
  "UPDATE run_members SET attempts = attempts + 1 WHERE run_id = ? AND test_index = ?"
986
938
  ).run(runId, testIndex);
987
939
  }
988
- /**
989
- * Drop ONE HANDLE's projected step and sub-action rows (D-17).
990
- *
991
- * SCOPED TO THE HANDLE, which was the whole change from the v1 run-wide step delete this
992
- * replaced (deleted outright by 405-07's census): that member wiped every step of the RUN on any
993
- * member's retry, so in a suite the abandoned attempt of test 3 deleted the completed history of
994
- * tests 0-2. The member row itself survives — its outcome and its attempt count are facts about
995
- * the member, not about the attempt. So do its setup and teardown hook rows, which run once per
996
- * member outside the retry loop; only the attempt's `inline` hook rows go (433-41, CR-01).
997
- */
940
+ // Scoped to the handle: the member row and its setup and teardown hooks survive a retry.
998
941
  deleteMemberProjection(runId, testIndex) {
999
942
  this.db.prepare("DELETE FROM run_steps WHERE run_id = ? AND test_index = ?").run(runId, testIndex);
1000
943
  this.db.prepare("DELETE FROM run_sub_actions WHERE run_id = ? AND test_index = ?").run(runId, testIndex);
@@ -1002,12 +945,7 @@ var DashboardDatabase = class {
1002
945
  "DELETE FROM execution_logs WHERE run_id = ? AND test_index = ? AND phase = 'inline'"
1003
946
  ).run(runId, testIndex);
1004
947
  }
1005
- /**
1006
- * Write ONE finalized step row. `INSERT OR REPLACE` because a re-run of the same handle re-emits
1007
- * the same `(test_index, step_index)` and the LAST attempt is the one the page renders (D-17).
1008
- *
1009
- * The three token columns stay NULL when `tokenUsage` is absent — never 0 (D-05).
1010
- */
948
+ // `INSERT OR REPLACE`, so a re-run's last attempt wins; tokens stay NULL when absent, never 0.
1011
949
  insertRunStep(step) {
1012
950
  this.db.prepare(`
1013
951
  INSERT OR REPLACE INTO run_steps (
@@ -1067,14 +1005,7 @@ var DashboardDatabase = class {
1067
1005
  subAction.recordingOffsetMs ?? null
1068
1006
  );
1069
1007
  }
1070
- /**
1071
- * THE run-detail read (D-03). `undefined` when no `runs` row exists — absence is a RESULT here,
1072
- * and turning it into a typed refusal is the `runs.detail` op's job, so the "not found"
1073
- * vocabulary lives in the function layer rather than in this store.
1074
- *
1075
- * A run that exists and projected nothing answers three empty arrays, which is a DIFFERENT fact
1076
- * and stays a different answer.
1077
- */
1008
+ // `undefined` when no run row exists; a run that projected nothing answers three empty arrays.
1078
1009
  readRunDetailRows(runId) {
1079
1010
  const run = this.db.prepare("SELECT id FROM runs WHERE id = ?").get(runId);
1080
1011
  if (run === void 0) return void 0;
@@ -1214,7 +1145,7 @@ var DashboardDatabase = class {
1214
1145
  offset: opts.offset
1215
1146
  }));
1216
1147
  }
1217
- // 2026-10-17 (444-05, D-09): one batched member read per 400 returned rows, after pagination.
1148
+ // Members are read for the returned rows only, after pagination.
1218
1149
  withMemberSummaries(runs) {
1219
1150
  for (let offset = 0; offset < runs.length; offset += 400) {
1220
1151
  const batch = runs.slice(offset, offset + 400);
@@ -1257,7 +1188,7 @@ var DashboardDatabase = class {
1257
1188
  owner: { pid: row.owner_pid, incarnation: row.owner_incarnation, host: row.owner_host ?? "" }
1258
1189
  }));
1259
1190
  }
1260
- // 2026-10-16 (442-01, D-04): one compare-and-set on the stamped owner; never read-then-write.
1191
+ // One compare-and-set on the stamped owner; never read-then-write.
1261
1192
  finalizeOwnRun(id, updates) {
1262
1193
  const fields = ["status = ?", "ended_at = ?"];
1263
1194
  const values = [updates.status, updates.endedAt];
@@ -1279,8 +1210,7 @@ var DashboardDatabase = class {
1279
1210
  this.ownStamps.delete(id);
1280
1211
  return true;
1281
1212
  }
1282
- // 2026-10-17 (443-13, GAP-D1; 444-04): no status condition, because the verdict may already have
1283
- // moved the row off running; the owner columns alone decide whose row it is.
1213
+ // No status condition, as the verdict may have moved the row off running; owner columns decide.
1284
1214
  closeOwnRun(id, updates) {
1285
1215
  const stamp = this.ownStamps.get(id) ?? this.readOwnStamp();
1286
1216
  const result = this.db.prepare(`
@@ -1291,8 +1221,7 @@ var DashboardDatabase = class {
1291
1221
  this.ownStamps.delete(id);
1292
1222
  return true;
1293
1223
  }
1294
- // 2026-10-16 (442-15, GAP-D2): own is the stamp this handle wrote, so a NULL start time here is
1295
- // this handle's own write, never an inference about another process.
1224
+ // Own is the stamp this handle wrote, so a NULL start time here is this handle's own write.
1296
1225
  isOwnStamp(id, owner) {
1297
1226
  const stamp = this.ownStamps.get(id);
1298
1227
  return stamp !== void 0 && stamp.pid === owner.pid && stamp.incarnation === owner.incarnation && stamp.host === owner.host;
@@ -1391,20 +1320,7 @@ var DashboardDatabase = class {
1391
1320
  `).all(...params);
1392
1321
  return rows.map((row) => ({ value: row.value, count: row.count }));
1393
1322
  }
1394
- /**
1395
- * Delete ONE run and everything the schema hangs off it (405-07, D-02/D-14 · T-405-27).
1396
- *
1397
- * ONE ID, AND THE CASCADE DOES THE REST. This used to open with a recursive `run_tree` CTE that
1398
- * walked the child rows of a suite parent, then hand-delete their step and trace rows in a
1399
- * transaction. Nothing has written a child row since 402-09 — one `runs` row per run, member
1400
- * identity on `run_members` — so the walk could only ever return the one row it was seeded with,
1401
- * and the hand-deletes named tables this schema no longer has. Every table that references a run
1402
- * declares `ON DELETE CASCADE`, so the single statement below is the whole deletion.
1403
- *
1404
- * AND IT TOUCHES NO FILE. The two path arrays it used to collect fed the router's artifact
1405
- * cleanup; both are gone. The run's directory under `.agent-qa/runs/` — its event log and its
1406
- * content-addressed blobs — is the run's own record and is not the dashboard's to remove.
1407
- */
1323
+ // Every table referencing a run cascades on delete, and no file is touched.
1408
1324
  deleteRun(id) {
1409
1325
  const changes = this.db.prepare("DELETE FROM runs WHERE id = ?").run(id);
1410
1326
  const deleted = Number(changes.changes ?? 0) > 0;
@@ -1491,15 +1407,7 @@ var DashboardDatabase = class {
1491
1407
  getRunArtifactBundle(runId) {
1492
1408
  return { artifact: this.getRunArtifact(runId) };
1493
1409
  }
1494
- /**
1495
- * The runs a self-heal happened on (405-07, D-02).
1496
- *
1497
- * READ OFF `run_steps.self_heal_occurred`, WHICH IS THE SURVIVING HEALING FACT. This scanned the
1498
- * v1 wide step table for `status = 'healed'`; that table had no producer left and its readers
1499
- * went with it. `selfHealOccurred` is the bus payload's own boolean, projected verbatim by the
1500
- * subscriber — so the flake rate below is now computed from what the runner actually reported
1501
- * rather than from a status word a retired projection wrote.
1502
- */
1410
+ // Read off `run_steps.self_heal_occurred`, the bus payload's own boolean.
1503
1411
  getHealedRunIds(runIds) {
1504
1412
  if (runIds.length === 0) return /* @__PURE__ */ new Set();
1505
1413
  const placeholders = runIds.map(() => "?").join(", ");
@@ -1522,7 +1430,7 @@ var DashboardDatabase = class {
1522
1430
  passed,
1523
1431
  failed,
1524
1432
  flaky,
1525
- // 2026-10-18 (444-24, D-59): a group's rate is passed ÷ (passed + failed); neither is no rate.
1433
+ // A group's rate is passed ÷ (passed + failed); neither is no rate.
1526
1434
  passRate: passed + failed > 0 ? passed / (passed + failed) : null,
1527
1435
  flakeRate: totalRuns > 0 ? flaky / totalRuns : 0,
1528
1436
  avgDuration: totalRuns > 0 ? totalDuration / totalRuns : 0
@@ -1584,28 +1492,6 @@ var DashboardDatabase = class {
1584
1492
  FROM run_steps
1585
1493
  WHERE run_id IN (${runIds.map(() => "?").join(",")})
1586
1494
  `).get(...runIds).total_tokens || 0 : 0;
1587
- const memorySummary = {
1588
- runs: 0,
1589
- added: 0,
1590
- confirmed: 0,
1591
- deprecated: 0,
1592
- deleted: 0,
1593
- curatorTokens: 0
1594
- };
1595
- for (const run of runs) {
1596
- if (!run.memoryLog) continue;
1597
- memorySummary.runs++;
1598
- try {
1599
- const parsed = JSON.parse(run.memoryLog);
1600
- memorySummary.added += Number(parsed.added ?? 0) || 0;
1601
- memorySummary.confirmed += Number(parsed.confirmed ?? 0) || 0;
1602
- memorySummary.deprecated += Number(parsed.deprecated ?? 0) || 0;
1603
- memorySummary.deleted += Number(parsed.deleted ?? 0) || 0;
1604
- const tokenUsage = parsed.tokenUsage && typeof parsed.tokenUsage === "object" ? parsed.tokenUsage : {};
1605
- memorySummary.curatorTokens += Number(tokenUsage.totalTokens ?? 0) || 0;
1606
- } catch {
1607
- }
1608
- }
1609
1495
  return {
1610
1496
  totalRuns: summary.totalRuns,
1611
1497
  passed: summary.passed,
@@ -1614,8 +1500,7 @@ var DashboardDatabase = class {
1614
1500
  flakeRate: summary.flakeRate,
1615
1501
  avgDuration: summary.avgDuration,
1616
1502
  totalTokens,
1617
- runs: daily,
1618
- memory: memorySummary
1503
+ runs: daily
1619
1504
  };
1620
1505
  }
1621
1506
  getCostStats(opts = {}) {
@@ -1790,12 +1675,7 @@ var DashboardDatabase = class {
1790
1675
  testRunUnmeasuredSteps: unmeasuredSteps
1791
1676
  };
1792
1677
  }
1793
- /**
1794
- * The window's settled run ids, newest first, capped at `limit`, with their full count, and the
1795
- * count of the window's runs still in flight (433-30, 433-42). A settled run has started and is
1796
- * neither `pending` nor `running`; every other row in the window is in flight. Ids only: the
1797
- * Insights knowledge-change read dispatches each through the agent host.
1798
- */
1678
+ // Settled run ids newest first, capped at `limit`, with the full and in-flight counts.
1799
1679
  listInsightsWindowRunIds(opts) {
1800
1680
  const conditions = [];
1801
1681
  const params = [];
@@ -1951,7 +1831,7 @@ var DashboardDatabase = class {
1951
1831
  const limit = opts.limit ?? 25;
1952
1832
  return rows.slice(0, limit);
1953
1833
  }
1954
- // 2026-10-17 (444-05, D-35): grouped by member file path; only pass- and fail-class outcomes count.
1834
+ // Grouped by member file path; only pass- and fail-class outcomes count.
1955
1835
  getFlakyTests(opts) {
1956
1836
  const minRuns = opts?.minRuns ?? 3;
1957
1837
  const limit = opts?.limit ?? 50;
@@ -2076,7 +1956,6 @@ var DashboardDatabase = class {
2076
1956
  endedAt: r.ended_at,
2077
1957
  failureSummary: r.failure_summary ?? null,
2078
1958
  errorLog: r.error_log ?? null,
2079
- memoryLog: r.memory_log ?? null,
2080
1959
  testId: r.test_id ?? null,
2081
1960
  suiteId: r.suite_id ?? null,
2082
1961
  platform: r.platform ?? "web",
@@ -2400,11 +2279,7 @@ function createDashboardRunIngest(deps) {
2400
2279
  ...payload.status === void 0 ? {} : { status: payload.status },
2401
2280
  ...payload.durationMs === void 0 ? {} : { durationMs: payload.durationMs },
2402
2281
  ...payload.error === void 0 ? {} : { error: payload.error },
2403
- // 2026-08-27 (405 plan 04, D-05/D-06): the producer landed this plan, so the three token
2404
- // columns 405-01 declared stop being always-NULL. Spread-when-defined, like every other
2405
- // optional above it — the DB layer writes NULL for all three when the key is absent, and
2406
- // a NULL row is what makes `runs.detail` able to say "M of K steps unmeasured" instead of
2407
- // reporting a zero nobody measured.
2282
+ // Spread when defined, so an unmeasured step stays NULL and `runs.detail` counts it so.
2408
2283
  ...payload.tokenUsage === void 0 ? {} : { tokenUsage: payload.tokenUsage },
2409
2284
  ...payload.modelUsage === void 0 ? {} : { modelUsage: payload.modelUsage },
2410
2285
  ...payload.evidence === void 0 ? {} : { evidence: payload.evidence },
@@ -2454,8 +2329,7 @@ function createDashboardRunIngest(deps) {
2454
2329
  stdout: readString(payload.stdout) ?? null,
2455
2330
  stderr: readString(payload.stderr) ?? null,
2456
2331
  variables: payload.variables ?? null,
2457
- // 405 D-01: where in the run tree this hook ran. Absent stays NULL — a run-scoped hook
2458
- // is a real state, and inventing handle 0 for it would put a setup hook inside a test.
2332
+ // Absent stays NULL: a run-scoped hook is a real state, not handle 0.
2459
2333
  ...payload.testIndex === void 0 ? {} : { testIndex: payload.testIndex },
2460
2334
  ...payload.stepIndex === void 0 ? {} : { stepIndex: payload.stepIndex }
2461
2335
  });
@@ -2553,6 +2427,7 @@ var import_node_crypto9 = require("node:crypto");
2553
2427
  var import_promises9 = require("node:fs/promises");
2554
2428
  var import_node_path12 = require("node:path");
2555
2429
  var import_node_os3 = require("node:os");
2430
+ var import_node_util5 = require("node:util");
2556
2431
 
2557
2432
  // src/server/routes.ts
2558
2433
  var import_node_crypto7 = require("node:crypto");
@@ -2727,7 +2602,7 @@ var TestFileManager = class {
2727
2602
  constructor(workspace) {
2728
2603
  this.workspace = workspace;
2729
2604
  }
2730
- // 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.
2605
+ // Each list call reads the workspace itself; each file is written whole, one writer at a time.
2731
2606
  underLock = createPathLock();
2732
2607
  async list() {
2733
2608
  const records = await (0, import_agent_qa_core3.discoverWorkspaceFiles)({ workspace: this.workspace, kind: "test" });
@@ -2891,8 +2766,7 @@ var ConfigManager = class _ConfigManager {
2891
2766
  this.configPath = configPath;
2892
2767
  this.mask = mask;
2893
2768
  }
2894
- // 441 GAP3-D5: a whole document may keep a marker the file already holds, as often, but never
2895
- // add one. A missing file holds none.
2769
+ // A whole document may keep a marker the file already holds, as often, but never add one.
2896
2770
  static addsShownMarker(current, next) {
2897
2771
  const held = shownMarkerCounts(current);
2898
2772
  for (const [marker, count] of shownMarkerCounts(next)) {
@@ -2900,13 +2774,13 @@ var ConfigManager = class _ConfigManager {
2900
2774
  }
2901
2775
  return false;
2902
2776
  }
2903
- // 429-05 Pitfall 3: a half-edited file is named, never served as the partial config yaml recovers.
2777
+ // A half-edited file is named, never served as the partial config yaml recovers.
2904
2778
  async readWithRevision() {
2905
2779
  const read = await this.readParsed();
2906
2780
  if (read.kind !== "ok") return read;
2907
2781
  return { kind: "ok", config: read.config, raw: read.raw, revision: read.revision };
2908
2782
  }
2909
- // 441-21 D-29: the payload and the file view pass through the declared mask and nothing else.
2783
+ // The payload and the file view pass through the declared mask and nothing else.
2910
2784
  async readMasked() {
2911
2785
  const read = await this.readParsed();
2912
2786
  if (read.kind === "absent") {
@@ -2950,8 +2824,7 @@ var ConfigManager = class _ConfigManager {
2950
2824
  revision
2951
2825
  };
2952
2826
  }
2953
- // 429-01 D-19/D-22: one transaction per request, serialised so overlapping autosaves never
2954
- // read the same bytes and drop each other's field.
2827
+ // One transaction per request, serialised so overlapping autosaves never drop each other's field.
2955
2828
  async applyChanges(changes, options = {}) {
2956
2829
  const marked = changeWithShownMarker(changes);
2957
2830
  if (marked !== void 0) {
@@ -2966,13 +2839,11 @@ var ConfigManager = class _ConfigManager {
2966
2839
  }
2967
2840
  return this.serialized(() => this.applyChangesOnce(changes, options.capturePriors === true));
2968
2841
  }
2969
- // 429-11 D-24: an Undo is one more transaction on the same mutex, newest change first. A field
2970
- // that no longer holds what the change left there was written since, and is never overwritten.
2842
+ // An Undo restores newest first; a field written after the change is never overwritten.
2971
2843
  async restorePriors(priors) {
2972
2844
  return this.serialized(() => this.restorePriorsOnce(priors));
2973
2845
  }
2974
- // 432-07 D-05: a generated whole-file change (a scaffold, a hooks file) publishes only over the
2975
- // bytes it was built from, read again in this path's turn. Anything else is somebody's edit.
2846
+ // A generated whole-file change publishes only over the bytes it is built from, re-read in turn.
2976
2847
  async replaceDocument(expectedRevision, content) {
2977
2848
  return this.serialized(async () => {
2978
2849
  let current;
@@ -3099,8 +2970,7 @@ var ConfigManager = class _ConfigManager {
3099
2970
  await this.writeAtomic(next);
3100
2971
  return capturePriors ? { kind: "ok", revision: revisionOf(next), baseRevision, priors } : { kind: "ok", revision: revisionOf(next), baseRevision };
3101
2972
  }
3102
- // 429-05 Pitfall 8 / Pattern 1 step 6: the temp file sits beside the real file, keeps its mode
3103
- // (chmod, because writeFile's mode is filtered by the umask), and replaces it in one rename.
2973
+ // The temp file sits beside the real one and is chmodded, as the umask filters `writeFile` modes.
3104
2974
  async writeAtomic(content) {
3105
2975
  const target = await this.writeTarget();
3106
2976
  const directory = (0, import_node_path5.dirname)(target);
@@ -4300,6 +4170,12 @@ var HookRegistryManager = class {
4300
4170
 
4301
4171
  // src/server/body-parser.ts
4302
4172
  var DEFAULT_MAX_BYTES = 1024 * 1024;
4173
+ function carriesNonJsonBody(req) {
4174
+ const hasBody = req.headers["transfer-encoding"] !== void 0 || Number(req.headers["content-length"] ?? "0") > 0;
4175
+ if (!hasBody) return false;
4176
+ const type = req.headers["content-type"];
4177
+ return type === void 0 || type.split(";", 1)[0].trim().toLowerCase() !== "application/json";
4178
+ }
4303
4179
  function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
4304
4180
  return new Promise((resolve7, reject) => {
4305
4181
  const chunks = [];
@@ -4334,6 +4210,7 @@ function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
4334
4210
  // src/live-editor/live-session.ts
4335
4211
  var import_node_crypto6 = require("node:crypto");
4336
4212
  var import_node_path9 = require("node:path");
4213
+ var import_node_util3 = require("node:util");
4337
4214
  var import_agent_qa_core8 = require("@vostride/agent-qa-core");
4338
4215
  var import_agent_qa_runner = require("@vostride/agent-qa-runner");
4339
4216
  var import_agent_qa_runner_local = require("@vostride/agent-qa-runner-local");
@@ -4421,12 +4298,10 @@ async function loadLocalBindings(configPath) {
4421
4298
  try {
4422
4299
  const content = await (0, import_promises6.readFile)(filePath, "utf-8");
4423
4300
  const parsed = (0, import_yaml5.parse)(content);
4424
- return parsed && typeof parsed === "object" ? {
4425
- devices: parsed.devices,
4426
- providers: parsed.providers,
4427
- apps: parsed.apps,
4428
- filePath
4429
- } : { filePath };
4301
+ (0, import_agent_qa_core7.refuseRetiredLocalBindings)(parsed, filePath);
4302
+ if (!parsed || typeof parsed !== "object") return { filePath };
4303
+ const bindings = parsed;
4304
+ return { devices: bindings.devices, apps: bindings.apps, filePath };
4430
4305
  } catch (err) {
4431
4306
  if (err.code === "ENOENT") return null;
4432
4307
  throw err;
@@ -4516,44 +4391,15 @@ async function prepareMobileLiveSession(input) {
4516
4391
  appActivity: resolved.app.appActivity,
4517
4392
  deepLinkAppId: resolved.app.deepLinkAppId,
4518
4393
  appPath: resolved.app.install?.path,
4519
- browserstackApp: resolved.app.install?.browserstack,
4520
4394
  appState: resolved.appState
4521
4395
  }
4522
4396
  };
4523
4397
  }
4524
4398
 
4525
4399
  // src/live-editor/live-session.ts
4526
- var LiveProductionRefusedError = class extends Error {
4527
- productionTargetNames;
4528
- constructor(productionTargetNames) {
4529
- super((0, import_agent_qa_schema3.buildProductionRefusal)(productionTargetNames));
4530
- this.name = "LiveProductionRefusedError";
4531
- this.productionTargetNames = (0, import_agent_qa_schema3.distinctInFirstOccurrenceOrder)(productionTargetNames);
4532
- }
4533
- };
4534
4400
  function isConfigRecord(value) {
4535
4401
  return value !== null && typeof value === "object" && !Array.isArray(value);
4536
4402
  }
4537
- function liveTargetName(targetName) {
4538
- if (typeof targetName !== "string") return void 0;
4539
- const trimmed = targetName.trim();
4540
- return trimmed.length > 0 ? trimmed : void 0;
4541
- }
4542
- async function resolveLiveTargetProduction(configManager, targetName) {
4543
- const name = liveTargetName(targetName);
4544
- if (name === void 0 || configManager === void 0) {
4545
- return { productionTargetNames: [], allowProduction: false };
4546
- }
4547
- const config = await readConfigValues(configManager);
4548
- const registry = isConfigRecord(config.registry) ? config.registry : void 0;
4549
- const targets = registry !== void 0 && isConfigRecord(registry.targets) ? registry.targets : void 0;
4550
- const declared = targets !== void 0 && Object.hasOwn(targets, name) ? targets[name] : void 0;
4551
- const production = isConfigRecord(declared) && declared.environment === "production";
4552
- return {
4553
- productionTargetNames: production ? [name] : [],
4554
- allowProduction: config.allowProduction === true
4555
- };
4556
- }
4557
4403
  async function readEntityUse(entity, readers) {
4558
4404
  if (entity === void 0) return void 0;
4559
4405
  const find = entity.type === "suite" ? readers.findSuite : readers.findTest;
@@ -4573,6 +4419,12 @@ var LivePlannerLimitsRefusedError = class extends Error {
4573
4419
  this.name = "LivePlannerLimitsRefusedError";
4574
4420
  }
4575
4421
  };
4422
+ var LiveNavigationTimeoutRefusedError = class extends Error {
4423
+ constructor(message) {
4424
+ super(message);
4425
+ this.name = "LiveNavigationTimeoutRefusedError";
4426
+ }
4427
+ };
4576
4428
  var LIVE_LIMIT_LEAVES = ["maxActions", "maxPlanningTurns"];
4577
4429
  function shownLimitValue(value) {
4578
4430
  return typeof value === "number" ? String(value) : JSON.stringify(value) ?? String(value);
@@ -4621,9 +4473,30 @@ async function resolveLivePlannerLimits(configManager, entities, readers) {
4621
4473
  }
4622
4474
  return (0, import_agent_qa_schema3.resolvePlannerLimits)(config.use, ...uses);
4623
4475
  }
4624
- function liveProductionRefusal(found) {
4625
- if (found.productionTargetNames.length === 0 || found.allowProduction) return void 0;
4626
- return new LiveProductionRefusedError(found.productionTargetNames);
4476
+ async function resolveLiveNavigationTimeoutMs(configManager, entities, readers) {
4477
+ const config = configManager === void 0 ? {} : await readConfigValues(configManager);
4478
+ const layers = [
4479
+ { layer: "project config", use: config.use }
4480
+ ];
4481
+ for (const entity of [].concat(entities ?? [])) {
4482
+ const use = await readEntityUse(entity, readers);
4483
+ layers.push({ layer: `${entity.type} file ${entity.id}`, use });
4484
+ }
4485
+ let navigation;
4486
+ for (const { layer, use } of layers) {
4487
+ if (!isConfigRecord(use) || !isConfigRecord(use.timeout)) continue;
4488
+ if (!Object.hasOwn(use.timeout, "navigation")) continue;
4489
+ const value = use.timeout.navigation;
4490
+ const parsed = import_agent_qa_core8.TimeoutConfigSchema.shape.navigation.safeParse(value);
4491
+ if (!parsed.success) {
4492
+ const issue = parsed.error.issues[0]?.message ?? "invalid value";
4493
+ throw new LiveNavigationTimeoutRefusedError(
4494
+ `${layer} sets use.timeout.navigation to ${shownLimitValue(value)}: ${issue}. The CLI refuses this file too.`
4495
+ );
4496
+ }
4497
+ navigation = parsed.data;
4498
+ }
4499
+ return navigation;
4627
4500
  }
4628
4501
  var IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
4629
4502
  var LIVE_HUB_CLOSE_LIMIT_MS = 2e3;
@@ -4759,8 +4632,7 @@ var LiveSession = class {
4759
4632
  planner = null;
4760
4633
  verifier = null;
4761
4634
  modelProfiles = null;
4762
- // D-14 (Phase 360): the run-scoped storage-state store, constructed lazily on first use and
4763
- // rooted at the session's `.agent-qa` runtime dir (the FsStorageStateStore rooting idiom).
4635
+ // Built lazily on first use, rooted at the session's `.agent-qa` runtime dir.
4764
4636
  storageStateStore = null;
4765
4637
  model = null;
4766
4638
  variableStore = null;
@@ -4788,15 +4660,14 @@ var LiveSession = class {
4788
4660
  mobileAppiumLease = null;
4789
4661
  secretStore;
4790
4662
  secretRedactor;
4791
- // 428 D-07: one hub per session, handle 0, created once the adapter exists. A session whose
4792
- // setup hooks failed has no adapter and therefore no hub; its subject declares `none`.
4663
+ // One hub per session at handle 0, created once the adapter exists; no adapter means no hub.
4793
4664
  liveHub = null;
4794
4665
  attachment = null;
4795
4666
  resolveEnded = () => {
4796
4667
  };
4797
4668
  ended;
4798
4669
  cleanupRun = null;
4799
- // RESEARCH P4: which control socket is the current one. Only its close may detach the session.
4670
+ // Which control socket is the current one; only its close may detach the session.
4800
4671
  attachmentGeneration = 0;
4801
4672
  releaseAttachment = null;
4802
4673
  get modelName() {
@@ -4818,11 +4689,7 @@ var LiveSession = class {
4818
4689
  hub.register(0, registration?.capability, registration?.fallback);
4819
4690
  this.liveHub = hub;
4820
4691
  }
4821
- /**
4822
- * Make a control socket the current one. `release` is how the session lets go of it later: the
4823
- * previous attachment's is called with `superseded` here (428 WR-01), so one socket drives the
4824
- * session at a time.
4825
- */
4692
+ // The previous attachment's `release` gets `superseded`, so one socket drives the session.
4826
4693
  beginAttachment(release) {
4827
4694
  const previous = this.releaseAttachment;
4828
4695
  this.attachmentGeneration += 1;
@@ -4841,11 +4708,7 @@ var LiveSession = class {
4841
4708
  }
4842
4709
  this.pendingMessages = [];
4843
4710
  }
4844
- /**
4845
- * A command's reply, for whichever control socket is attached now (428 CR-01). A command can
4846
- * outlive the socket that issued it, and its outcome belongs to the page, not to that socket:
4847
- * with none attached it is held and flushed to the next one.
4848
- */
4711
+ // A reply can outlive its socket, so it goes to the current socket or waits for the next one.
4849
4712
  deliver(message) {
4850
4713
  if (this.messageSink) {
4851
4714
  this.messageSink(message);
@@ -4956,7 +4819,7 @@ var LiveSession = class {
4956
4819
  continue;
4957
4820
  }
4958
4821
  const result = await (0, import_agent_qa_runner.runHooks)([hookDef], {
4959
- executor: new import_agent_qa_runner_local.DockerHookExecutor(),
4822
+ executor: new import_agent_qa_runner_local.DockerHookExecutor({ version: (0, import_agent_qa_core8.getAgentQaVersion)() }),
4960
4823
  env: this.getHookEnvVars(),
4961
4824
  secretStore: this.secretStore,
4962
4825
  secretRedactor: this.secretRedactor
@@ -5030,7 +4893,6 @@ var LiveSession = class {
5030
4893
  device: config.device ? {
5031
4894
  name: config.device.name,
5032
4895
  platform,
5033
- transport: "local",
5034
4896
  match: config.device
5035
4897
  } : void 0
5036
4898
  };
@@ -5095,15 +4957,6 @@ var LiveSession = class {
5095
4957
  }
5096
4958
  }
5097
4959
  async initialize(config) {
5098
- const found = await resolveLiveTargetProduction(this.deps.configManager, config.targetName);
5099
- const refusal = liveProductionRefusal({
5100
- productionTargetNames: (0, import_agent_qa_schema3.distinctInFirstOccurrenceOrder)([
5101
- ...config.production?.productionTargetNames ?? [],
5102
- ...found.productionTargetNames
5103
- ]),
5104
- allowProduction: found.allowProduction
5105
- });
5106
- if (refusal !== void 0) throw refusal;
5107
4960
  this.platform = config.platform;
5108
4961
  this.targetName = typeof config.targetName === "string" && config.targetName.trim().length > 0 ? config.targetName.trim() : null;
5109
4962
  this._modelName = config.llmConfig.model ?? "unknown";
@@ -5138,7 +4991,11 @@ var LiveSession = class {
5138
4991
  } else {
5139
4992
  const { WebPlatformAdapter } = await import("@vostride/agent-qa-web");
5140
4993
  const adapter = new WebPlatformAdapter();
5141
- await adapter.setup({ platform: "web", browser: { name: "chromium", headless: config.headless ?? true } });
4994
+ await adapter.setup({
4995
+ platform: "web",
4996
+ browser: { name: "chromium", headless: config.headless ?? true },
4997
+ ...config.navigationTimeoutMs === void 0 ? {} : { timeouts: { navigation: config.navigationTimeoutMs } }
4998
+ });
5142
4999
  this.adapter = adapter;
5143
5000
  if (config.url) {
5144
5001
  const resolvedUrl = this.resolveSecrets(config.url);
@@ -5200,7 +5057,7 @@ var LiveSession = class {
5200
5057
  }
5201
5058
  const startedAt = performance.now();
5202
5059
  try {
5203
- const result = await new import_agent_qa_runner_local.DockerHookExecutor().execute(hookDef, {
5060
+ const result = await new import_agent_qa_runner_local.DockerHookExecutor({ version: (0, import_agent_qa_core8.getAgentQaVersion)() }).execute(hookDef, {
5204
5061
  env: this.getHookEnvVars(),
5205
5062
  secretEnv: (0, import_agent_qa_runner.resolveSecretEnv)(this.secretStore),
5206
5063
  secretRedactor: this.secretRedactor
@@ -5367,12 +5224,7 @@ var LiveSession = class {
5367
5224
  };
5368
5225
  return this.redact(result);
5369
5226
  }
5370
- // Inner step execution — no gating. Callers must own the executing/status
5371
- // lifecycle (executeStepCommand for single steps, executeTestCommand for
5372
- // the per-test loop that runs many steps under one outer gate).
5373
- // D-14 (Phase 360): lazily construct the run-scoped storage-state store, rooted at the session's
5374
- // `.agent-qa` runtime dir. All fs writes live in runner-local (validate:arch storageNeutral);
5375
- // the loop records only { name, fingerprint }. Absent configPath falls back to the cwd root.
5227
+ // Lazily built and rooted at the session's `.agent-qa` dir; fs writes stay in runner-local.
5376
5228
  async getStorageStateStore() {
5377
5229
  let store = this.storageStateStore;
5378
5230
  if (!store) {
@@ -5511,7 +5363,9 @@ ${errorLines.join("\n")}`;
5511
5363
  testLimits = this.plannerLimitsForTest !== void 0 && testDraft.testId.length > 0 ? await this.plannerLimitsForTest(testDraft.testId) : this.plannerLimits ?? (0, import_agent_qa_schema3.resolvePlannerLimits)();
5512
5364
  } catch (error) {
5513
5365
  if (!(error instanceof LivePlannerLimitsRefusedError)) throw error;
5514
- console.warn(`[live-session] executeTestCommand: ${error.message}`, { testName: testDraft.testName });
5366
+ const line = `[live-session] executeTestCommand: ${error.message}`;
5367
+ const detail = { testName: testDraft.testName };
5368
+ (0, import_agent_qa_core8.printRunText)((0, import_node_util3.format)(line, detail), () => console.warn(line, detail));
5515
5369
  return this.redact({
5516
5370
  status: "failed",
5517
5371
  duration: performance.now() - start,
@@ -5531,11 +5385,13 @@ ${errorLines.join("\n")}`;
5531
5385
  if (!setupResult.allPassed) {
5532
5386
  const failedHook = setupHookPayloads.find((h) => h.status === "failed");
5533
5387
  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";
5534
- console.warn(`[live-session] executeTestCommand: ${detail}`, {
5388
+ const line = `[live-session] executeTestCommand: ${detail}`;
5389
+ const context = {
5535
5390
  testName: testDraft.testName,
5536
5391
  setupHooks: testDraft.setup,
5537
5392
  failedHook: failedHook ? { name: failedHook.hookName, stderr: failedHook.stderr, error: failedHook.error } : null
5538
- });
5393
+ };
5394
+ (0, import_agent_qa_core8.printRunText)((0, import_node_util3.format)(line, context), () => console.warn(line, context));
5539
5395
  return {
5540
5396
  status: "failed",
5541
5397
  duration: performance.now() - start,
@@ -5613,7 +5469,9 @@ ${errorLines.join("\n")}`;
5613
5469
  topLevelError = failedHook ? `Teardown hook "${failedHook.hookName}" failed${failedHook.error ? `: ${failedHook.error}` : ""}` : "Teardown hook failed";
5614
5470
  }
5615
5471
  if (topLevelError) {
5616
- console.warn(`[live-session] executeTestCommand: ${topLevelError}`, { testName: testDraft.testName });
5472
+ const line = `[live-session] executeTestCommand: ${topLevelError}`;
5473
+ const detail = { testName: testDraft.testName };
5474
+ (0, import_agent_qa_core8.printRunText)((0, import_node_util3.format)(line, detail), () => console.warn(line, detail));
5617
5475
  }
5618
5476
  return this.redact({
5619
5477
  status: finalStatus,
@@ -5629,10 +5487,7 @@ ${errorLines.join("\n")}`;
5629
5487
  this.status = "idle";
5630
5488
  }
5631
5489
  }
5632
- // Thin wrapper around runLifecycleHooks that also captures the dispatched
5633
- // hook payloads (hook-complete) for inclusion in the LiveTestResultPayload.
5634
- // The capture sits beside the message sink rather than wrapping it, so a
5635
- // control socket that reattaches mid-hook keeps its sink (428 CR-01).
5490
+ // Captures dispatched hook payloads beside the message sink, so a reattached socket keeps it.
5636
5491
  async runLifecycleHooksCapturing(phase, hookIds, sink, owner = { scope: "suite" }) {
5637
5492
  this.hookCaptures.add(sink);
5638
5493
  try {
@@ -5876,6 +5731,21 @@ function refuseUnparseableRequest(res) {
5876
5731
  });
5877
5732
  res.end(body);
5878
5733
  }
5734
+ function refuseForeignHostPage(res, port) {
5735
+ if (res.headersSent) {
5736
+ res.destroy();
5737
+ return;
5738
+ }
5739
+ const p = String(port);
5740
+ const body = `The agent-qa dashboard answers only at localhost, 127.0.0.1 or [::1].
5741
+ Open http://localhost:${p}/ on the machine it runs on, or forward the port with ssh -L ${p}:localhost:${p} <host>.
5742
+ `;
5743
+ res.writeHead(403, {
5744
+ "Content-Type": "text/plain; charset=utf-8",
5745
+ "Content-Length": Buffer.byteLength(body)
5746
+ });
5747
+ res.end(body);
5748
+ }
5879
5749
  function backstopServerError(res, req, error) {
5880
5750
  const kind = error instanceof Error ? error.constructor.name : typeof error;
5881
5751
  console.error(
@@ -5955,10 +5825,6 @@ function respondJson(res, status, data) {
5955
5825
  res.end(body);
5956
5826
  }
5957
5827
  function serveRunEvidenceRead(req, res, deps) {
5958
- if (deps.boundary === "human" && refuseCrossSiteRequest(req)) {
5959
- respondJson(res, 403, { error: "cross_site_refused" });
5960
- return;
5961
- }
5962
5828
  const query = new URL(req.url ?? "/", "http://localhost").searchParams;
5963
5829
  const entries = [["runId", deps.runId]];
5964
5830
  const seen = /* @__PURE__ */ new Set(["runId"]);
@@ -6000,10 +5866,6 @@ function respondFeedbackRead(res, deps, functionId, input, parse) {
6000
5866
  });
6001
5867
  }
6002
5868
  function serveRunMarksHistoryRead(req, res, deps) {
6003
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
6004
- respondJson(res, 403, { error: "cross_site_refused" });
6005
- return;
6006
- }
6007
5869
  if (new URL(req.url ?? "/", "http://localhost").search !== "") {
6008
5870
  respondJson(res, 400, { error: "invalid_marks_input" });
6009
5871
  return;
@@ -6022,10 +5884,6 @@ function serveRunMarksHistoryRead(req, res, deps) {
6022
5884
  );
6023
5885
  }
6024
5886
  function serveRunMarkPreviewRead(req, res, deps) {
6025
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
6026
- respondJson(res, 403, { error: "cross_site_refused" });
6027
- return;
6028
- }
6029
5887
  void readJsonBody(req).then((body) => {
6030
5888
  const parsed = import_agent_qa_schema4.RunsMarkInputSchema.safeParse(body);
6031
5889
  if (!parsed.success) {
@@ -6048,10 +5906,6 @@ function serveRunMarkPreviewRead(req, res, deps) {
6048
5906
  });
6049
5907
  }
6050
5908
  function serveRunDetailRead(req, res, deps) {
6051
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
6052
- respondJson(res, 403, { error: "cross_site_refused" });
6053
- return;
6054
- }
6055
5909
  void readRunDetail(deps.host, deps.runId).then(
6056
5910
  (outcome) => {
6057
5911
  if (outcome.kind === "refused") {
@@ -6201,10 +6055,6 @@ async function readRunAnalysis(host, runId) {
6201
6055
  }
6202
6056
  }
6203
6057
  function serveRunAnalysisRead(req, res, deps) {
6204
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
6205
- respondJson(res, 403, { error: "cross_site_refused" });
6206
- return;
6207
- }
6208
6058
  void readRunAnalysis(deps.host, deps.runId).then(
6209
6059
  (outcome) => {
6210
6060
  if (outcome.kind === "refused") {
@@ -6428,10 +6278,7 @@ function serveRunEventStream(req, res, deps) {
6428
6278
  else pending.push(frame);
6429
6279
  };
6430
6280
  const frameOf = (event) => (
6431
- // D-13, all three parts: the id IS the seq, the event name IS the envelope type, and the
6432
- // data IS the envelope's own JSON. No projection, no rename, no second vocabulary. The filter
6433
- // DROPS envelopes; it never edits one, so a filtered stream's frames are byte-identical to the
6434
- // firehose's — with gaps in the ids, which is what seq-as-id means once a filter exists.
6281
+ // The filter drops envelopes and never edits one, so filtered frames match the firehose bytes.
6435
6282
  `id: ${String(event.seq)}
6436
6283
  event: ${event.type}
6437
6284
  data: ${JSON.stringify(event)}
@@ -6493,12 +6340,6 @@ data: ${JSON.stringify(event)}
6493
6340
  deps.openStreams?.add(teardown);
6494
6341
  req.on("close", teardown);
6495
6342
  }
6496
- function runDenialOf(error) {
6497
- if (typeof error !== "object" || error === null) return void 0;
6498
- const neutralReason = error.neutralReason;
6499
- if (typeof neutralReason !== "string" || neutralReason.trim().length === 0) return void 0;
6500
- return { error: "run_denied", neutralReason };
6501
- }
6502
6343
  function createRunServiceRoutes(ports) {
6503
6344
  const openStreams = /* @__PURE__ */ new Set();
6504
6345
  function handleStart(req, res) {
@@ -6529,12 +6370,7 @@ function createRunServiceRoutes(ports) {
6529
6370
  ack = await ports.invokeRunStart(
6530
6371
  parsed.data.patterns === void 0 ? {} : { patterns: parsed.data.patterns }
6531
6372
  );
6532
- } catch (error) {
6533
- const denial = runDenialOf(error);
6534
- if (denial !== void 0) {
6535
- respondJson2(res, 403, denial);
6536
- return;
6537
- }
6373
+ } catch {
6538
6374
  refuse(res, 500, "run_start_failed");
6539
6375
  return;
6540
6376
  }
@@ -6614,6 +6450,10 @@ function createRunServiceRoutes(ports) {
6614
6450
  refuse(res, 403, "cross_site_refused");
6615
6451
  return;
6616
6452
  }
6453
+ if (carriesNonJsonBody(req)) {
6454
+ refuse(res, 415, "unsupported_media_type");
6455
+ return;
6456
+ }
6617
6457
  const matched = matchRunServiceRoute(req.method, path);
6618
6458
  if (matched === void 0) {
6619
6459
  refuse(res, 404, "unknown_route");
@@ -6879,8 +6719,7 @@ data: ${JSON.stringify(again.body)}
6879
6719
  // A failed re-read is not a reason to close a live channel: the next envelope will trigger
6880
6720
  // another, and the page's own poll is the floor underneath both.
6881
6721
  () => void 0
6882
- // …but a throw from the arm ABOVE is not a failed re-read, it is a dead socket, and `.then`'s
6883
- // failure arm never sees its own fulfilment arm's throw (UF-01, on the nested chain).
6722
+ // A throw from the arm above is a dead socket, and `.then`'s failure arm never sees it.
6884
6723
  ).catch(failStream);
6885
6724
  });
6886
6725
  heartbeat = setInterval(() => {
@@ -6917,10 +6756,6 @@ async function readRunKnowledge(host, runId) {
6917
6756
  }
6918
6757
  }
6919
6758
  function serveRunKnowledgeRead(req, res, deps) {
6920
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
6921
- respondJson(res, 403, { error: "cross_site_refused" });
6922
- return;
6923
- }
6924
6759
  void readRunKnowledge(deps.host, deps.runId).then(
6925
6760
  (outcome) => {
6926
6761
  if (outcome.kind === "refused") {
@@ -6961,10 +6796,6 @@ async function markRun(host, runId, body) {
6961
6796
  }
6962
6797
  }
6963
6798
  function registerRunMarkRoute(req, res, deps) {
6964
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
6965
- respondJson(res, 403, { error: "cross_site_refused" });
6966
- return;
6967
- }
6968
6799
  void readJsonBody(req).then(async (body) => {
6969
6800
  const outcome = await markRun(deps.host, deps.runId, body);
6970
6801
  if (outcome.kind === "refused") {
@@ -6982,10 +6813,6 @@ function registerRunMarkRoute(req, res, deps) {
6982
6813
  // src/server/config-routes.ts
6983
6814
  var import_agent_qa_schema10 = require("@vostride/agent-qa-schema");
6984
6815
  function registerConfigWriteRoute(req, res, deps) {
6985
- if (refuseCrossSiteRequest(req) !== void 0) {
6986
- respondJson(res, 403, { error: "cross_site_refused" });
6987
- return;
6988
- }
6989
6816
  void readJsonBody(req).then(async (body) => {
6990
6817
  const parsed = import_agent_qa_schema10.ConfigWriteRequestSchema.safeParse(body);
6991
6818
  if (!parsed.success) {
@@ -7043,10 +6870,6 @@ function registerConfigUndoRoute(req, res, deps) {
7043
6870
  respondJson(res, 405, { error: "method_not_allowed" });
7044
6871
  return;
7045
6872
  }
7046
- if (refuseCrossSiteRequest(req) !== void 0) {
7047
- respondJson(res, 403, { error: "cross_site_refused" });
7048
- return;
7049
- }
7050
6873
  void readJsonBody(req).then(async (body) => {
7051
6874
  const parsed = import_agent_qa_schema10.ConfigUndoRequestSchema.safeParse(body);
7052
6875
  if (!parsed.success) {
@@ -7276,10 +7099,6 @@ async function connectName(request2, deps) {
7276
7099
  });
7277
7100
  }
7278
7101
  function registerLlmConnectRoute(req, res, deps) {
7279
- if (refuseCrossSiteRequest(req) !== void 0) {
7280
- respondJson(res, 403, { error: "cross_site_refused" });
7281
- return;
7282
- }
7283
7102
  void readJsonBody(req).then(async (body) => {
7284
7103
  const answer = await connectLlm(body, deps);
7285
7104
  respondJson(res, answer.status, answer.body);
@@ -7601,25 +7420,10 @@ function llmConnectionTestTimeoutMs(config) {
7601
7420
  return REMOTE_LLM_CONNECTION_TEST_TIMEOUT_MS;
7602
7421
  }
7603
7422
  function json(res, data, status = 200) {
7604
- const body = JSON.stringify(data);
7605
- res.writeHead(status, {
7606
- "Content-Type": "application/json",
7607
- "Access-Control-Allow-Origin": "*",
7608
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
7609
- "Access-Control-Allow-Headers": "Content-Type"
7610
- });
7611
- res.end(body);
7612
- }
7613
- function jsonNoCors(res, data, status = 200) {
7614
7423
  const body = JSON.stringify(data);
7615
7424
  res.writeHead(status, { "Content-Type": "application/json" });
7616
7425
  res.end(body);
7617
7426
  }
7618
- function refuseCrossSiteDisplayRead(req, res) {
7619
- if (refuseCrossSiteRequest(req) === void 0) return false;
7620
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
7621
- return true;
7622
- }
7623
7427
  var INSIGHTS_KNOWLEDGE_RUN_CAP = 200;
7624
7428
  var INSIGHTS_KNOWLEDGE_CONCURRENCY = 4;
7625
7429
  async function isPostProcessingSkipped(host, runId) {
@@ -7683,14 +7487,6 @@ async function readInsightsKnowledgeChanges(host, db, window) {
7683
7487
  function notFound(res, message = "Not found") {
7684
7488
  json(res, { error: message }, 404);
7685
7489
  }
7686
- function cors(res) {
7687
- res.writeHead(204, {
7688
- "Access-Control-Allow-Origin": "*",
7689
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
7690
- "Access-Control-Allow-Headers": "Content-Type"
7691
- });
7692
- res.end();
7693
- }
7694
7490
  function isPlainRecord(value) {
7695
7491
  return value !== null && typeof value === "object" && !Array.isArray(value);
7696
7492
  }
@@ -7743,7 +7539,7 @@ function normalizeRunFilterValue(value) {
7743
7539
  function buildRunRequestAttributes(input) {
7744
7540
  const internal = (0, import_agent_qa_core11.buildInternalRunAttributes)({
7745
7541
  trigger: input.trigger,
7746
- runner: input.runner ?? "local"
7542
+ runner: "local"
7747
7543
  });
7748
7544
  const user = (0, import_agent_qa_core11.validateUserRunAttributes)(input.userAttributes, "run attributes");
7749
7545
  return (0, import_agent_qa_core11.mergeRunAttributes)(internal, user);
@@ -8188,9 +7984,7 @@ var RESPONSE_SAFE_RUN_METADATA_KEYS = /* @__PURE__ */ new Set([
8188
7984
  "timeoutSource",
8189
7985
  "timeoutBaseMs",
8190
7986
  "timeoutBufferMs",
8191
- "suiteIndex",
8192
- "farmSessionUrl",
8193
- "runDevice"
7987
+ "suiteIndex"
8194
7988
  ]);
8195
7989
  function sanitizeRunMetadataForResponse(metadata) {
8196
7990
  if (metadata === null || metadata === void 0) return metadata ?? null;
@@ -8220,17 +8014,7 @@ var RESPONSE_SAFE_RUN_READ_METHODS = [
8220
8014
  "getRunsByTest"
8221
8015
  ];
8222
8016
  var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
8223
- // 2026-08-27 (405-01, D-01/D-03): the v2 projection's six members.
8224
- //
8225
- // The five WRITES return nothing; they are here for the same reason `insertRun` is.
8226
- //
8227
- // `readRunDetailRows` IS A READ AND IS STILL HERE, and that is the judgement this set exists to
8228
- // make explicit rather than to hide. It returns members, steps and sub-actions — the bus
8229
- // payloads as the subscriber MASKED them (T-363-05) — and NO `RunRow` and no
8230
- // `RunArtifactRow`. It therefore carries none of the run metadata the 401-23 scrub law exists
8231
- // to strip, so wrapping it would be wrapping a shape that has nothing to strip. This router
8232
- // does not call it either: the detail arm dispatches through the injected host, which is the
8233
- // whole of D-03.
8017
+ // `readRunDetailRows` returns masked bus payloads and no run row, so it has no metadata to strip.
8234
8018
  "deleteMemberProjection",
8235
8019
  "incrementRunMemberAttempts",
8236
8020
  "insertRunStep",
@@ -8241,23 +8025,23 @@ var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
8241
8025
  "finalizeRunArtifact",
8242
8026
  "getRunsBySuiteIdCount",
8243
8027
  "getRunsByTestCount",
8244
- // 433-30: run ids and a count, no row; each id is dispatched through the agent host.
8028
+ // Run ids and a count, no row; each id is dispatched through the agent host.
8245
8029
  "listInsightsWindowRunIds",
8246
8030
  "insertPendingRun",
8247
8031
  "insertRun",
8248
8032
  "insertRunArtifact",
8249
8033
  "listRunAttributeKeys",
8250
8034
  "listRunAttributeValues",
8251
- // 2026-10-16 (442-01, D-01): an owner (pid, incarnation, host), no run metadata.
8035
+ // An owner (pid, incarnation, host), no run metadata.
8252
8036
  "readRunOwner",
8253
- // 2026-10-16 (442-01, D-04): ids and owners only, no run metadata.
8037
+ // Ids and owners only, no run metadata.
8254
8038
  "listRunningRunOwners",
8255
- // 2026-10-16 (442-01, D-04): compare-and-set writes that return a boolean.
8039
+ // Compare-and-set writes that return a boolean.
8256
8040
  "finalizeOrphanRun",
8257
8041
  "finalizeOwnRun",
8258
- // 2026-10-17 (443-13, GAP-D1): a compare-and-set write that returns a boolean, no run metadata.
8042
+ // A compare-and-set write that returns a boolean, no run metadata.
8259
8043
  "closeOwnRun",
8260
- // 2026-10-17 (443-02, D-07): a compare-and-set write that returns a boolean, no run metadata.
8044
+ // A compare-and-set write that returns a boolean, no run metadata.
8261
8045
  "claimRunningRow",
8262
8046
  "stageRunArtifact",
8263
8047
  "updateRun",
@@ -8328,10 +8112,7 @@ function withResponseSafeRunReads(db) {
8328
8112
  // `sanitizeArtifactForResponse`; a caller reaching for the bare row has no such step, so the
8329
8113
  // projection is its only choke point. Same allowlist, so the two cannot drift.
8330
8114
  getRunArtifact: { value: (runId) => scrubArtifact(db.getRunArtifact(runId)) },
8331
- // 405-07 (D-02): the bundle is ONE artifact now. It used to carry the child runs of a suite
8332
- // parent, whose `run` rows this override scrubbed; nothing has written a child row since
8333
- // 402-09, and the shape went with the column. `getRunArtifact` above remains the choke point
8334
- // for the payload itself, and this wrapper delegates to it.
8115
+ // One artifact; `getRunArtifact` is the choke point for its payload, and this delegates to it.
8335
8116
  getRunArtifactBundle: {
8336
8117
  value: (runId) => ({ artifact: scrubArtifact(db.getRunArtifactBundle(runId).artifact) })
8337
8118
  }
@@ -8479,10 +8260,18 @@ function createRouter(dbOrDeps) {
8479
8260
  json(res, { error: "Bad Request" }, 400);
8480
8261
  return;
8481
8262
  }
8263
+ if (refuseCrossSiteRequest(req) !== void 0) {
8264
+ json(res, { error: "cross_site_refused" }, 403);
8265
+ return;
8266
+ }
8267
+ if (carriesNonJsonBody(req)) {
8268
+ json(res, { error: "unsupported_media_type" }, 415);
8269
+ return;
8270
+ }
8482
8271
  const path = url.pathname;
8483
8272
  if (path === "/api/config" && req.method === "PATCH") {
8484
8273
  if (!configManager) {
8485
- jsonNoCors(res, { error: "config_unavailable" }, 503);
8274
+ json(res, { error: "config_unavailable" }, 503);
8486
8275
  return;
8487
8276
  }
8488
8277
  registerConfigWriteRoute(req, res, { configManager, undoStore });
@@ -8502,7 +8291,7 @@ function createRouter(dbOrDeps) {
8502
8291
  }
8503
8292
  if (path === "/api/config/revision" && req.method === "GET") {
8504
8293
  if (!configManager) {
8505
- jsonNoCors(res, { error: "config_unavailable" }, 503);
8294
+ json(res, { error: "config_unavailable" }, 503);
8506
8295
  return;
8507
8296
  }
8508
8297
  registerConfigRevisionRoute(req, res, { configManager });
@@ -8510,7 +8299,7 @@ function createRouter(dbOrDeps) {
8510
8299
  }
8511
8300
  if (path === "/api/llm/connect" && req.method === "POST") {
8512
8301
  if (!configManager) {
8513
- jsonNoCors(res, { error: "config_unavailable" }, 503);
8302
+ json(res, { error: "config_unavailable" }, 503);
8514
8303
  return;
8515
8304
  }
8516
8305
  registerLlmConnectRoute(req, res, {
@@ -8522,16 +8311,6 @@ function createRouter(dbOrDeps) {
8522
8311
  });
8523
8312
  return;
8524
8313
  }
8525
- if (path === "/api/runs/trigger" || path === "/api/queue/enqueue") {
8526
- if (refuseCrossSiteRequest(req) !== void 0) {
8527
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
8528
- return;
8529
- }
8530
- }
8531
- if (req.method === "OPTIONS") {
8532
- cors(res);
8533
- return;
8534
- }
8535
8314
  if (path === "/api/auth-states" && req.method === "GET") {
8536
8315
  if (!configManager || !deps.configPath) {
8537
8316
  json(res, { error: "Auth-state metadata not available" }, 503);
@@ -8555,11 +8334,10 @@ function createRouter(dbOrDeps) {
8555
8334
  return;
8556
8335
  }
8557
8336
  if (path === "/api/runs" && req.method === "GET") {
8558
- if (refuseCrossSiteDisplayRead(req, res)) return;
8559
8337
  const statuses = url.searchParams.getAll("status");
8560
8338
  const unknownStatus = statuses.find((word) => !isRunStatusWord(word));
8561
8339
  if (unknownStatus !== void 0) {
8562
- jsonNoCors(res, { error: `Unknown run status: ${unknownStatus}.` }, 400);
8340
+ json(res, { error: `Unknown run status: ${unknownStatus}.` }, 400);
8563
8341
  return;
8564
8342
  }
8565
8343
  const name = url.searchParams.get("name") ?? void 0;
@@ -8591,12 +8369,12 @@ function createRouter(dbOrDeps) {
8591
8369
  )).sort((a, b) => a.localeCompare(b));
8592
8370
  const filtered = platformFilteredRuns.filter((run) => matchesTargetFilter(run, target));
8593
8371
  const runs = filtered.slice(offset, offset + limit);
8594
- jsonNoCors(res, {
8372
+ json(res, {
8595
8373
  runs,
8596
8374
  total: filtered.length,
8597
8375
  targets: targetOptions
8598
8376
  });
8599
- }).catch(() => jsonNoCors(res, { error: "Failed to list runs" }, 500));
8377
+ }).catch(() => json(res, { error: "Failed to list runs" }, 500));
8600
8378
  return;
8601
8379
  }
8602
8380
  if (path === "/api/runs/attributes/keys" && req.method === "GET") {
@@ -8634,7 +8412,6 @@ function createRouter(dbOrDeps) {
8634
8412
  }
8635
8413
  const runArtifactMatch = path.match(/^\/api\/runs\/([^/]+)\/artifact$/);
8636
8414
  if (runArtifactMatch && req.method === "GET") {
8637
- if (refuseCrossSiteDisplayRead(req, res)) return;
8638
8415
  const runId = decodeURIComponent(runArtifactMatch[1]);
8639
8416
  const run = db.getRun(runId);
8640
8417
  if (!run) {
@@ -8642,7 +8419,7 @@ function createRouter(dbOrDeps) {
8642
8419
  return;
8643
8420
  }
8644
8421
  const bundle = db.getRunArtifactBundle(runId);
8645
- jsonNoCors(res, {
8422
+ json(res, {
8646
8423
  run,
8647
8424
  artifact: sanitizeArtifactForResponse(bundle.artifact),
8648
8425
  missingSections: getArtifactMissingSections(bundle.artifact)
@@ -8650,7 +8427,6 @@ function createRouter(dbOrDeps) {
8650
8427
  return;
8651
8428
  }
8652
8429
  if (path.startsWith("/api/runs/") && path.endsWith("/execution-logs") && req.method === "GET") {
8653
- if (refuseCrossSiteDisplayRead(req, res)) return;
8654
8430
  const segments = path.split("/");
8655
8431
  const id = segments[3];
8656
8432
  const run = db.getRun(id);
@@ -8661,37 +8437,37 @@ function createRouter(dbOrDeps) {
8661
8437
  const stepId = url.searchParams.get("stepId") ?? void 0;
8662
8438
  const type = url.searchParams.get("type") ?? void 0;
8663
8439
  const logs = db.getExecutionLogs({ runId: id, stepId, type });
8664
- jsonNoCors(res, { logs });
8440
+ json(res, { logs });
8665
8441
  return;
8666
8442
  }
8667
8443
  if (path.startsWith("/api/runs/") && path.endsWith("/mark") && req.method === "POST") {
8668
8444
  if (deps.agentHost === void 0) {
8669
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
8445
+ json(res, { error: "agent_host_unavailable" }, 503);
8670
8446
  return;
8671
8447
  }
8672
8448
  let runId;
8673
8449
  try {
8674
8450
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark".length));
8675
8451
  } catch {
8676
- jsonNoCors(res, { error: "invalid_mark_input" }, 400);
8452
+ json(res, { error: "invalid_mark_input" }, 400);
8677
8453
  return;
8678
8454
  }
8679
- registerRunMarkRoute(req, res, { host: deps.agentHost, runId, boundary: "human" });
8455
+ registerRunMarkRoute(req, res, { host: deps.agentHost, runId });
8680
8456
  return;
8681
8457
  }
8682
8458
  if (req.method === "POST" && /^\/api\/runs\/[^/]+\/mark-preview$/.test(path)) {
8683
8459
  if (deps.agentHost === void 0) {
8684
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
8460
+ json(res, { error: "agent_host_unavailable" }, 503);
8685
8461
  return;
8686
8462
  }
8687
8463
  let runId;
8688
8464
  try {
8689
8465
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark-preview".length));
8690
8466
  } catch {
8691
- jsonNoCors(res, { error: "invalid_run_id" }, 400);
8467
+ json(res, { error: "invalid_run_id" }, 400);
8692
8468
  return;
8693
8469
  }
8694
- serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost, boundary: "human" });
8470
+ serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost });
8695
8471
  return;
8696
8472
  }
8697
8473
  if (path.startsWith("/api/runs/") && path.endsWith("/cancel") && req.method === "POST") {
@@ -8770,44 +8546,39 @@ function createRouter(dbOrDeps) {
8770
8546
  return;
8771
8547
  }
8772
8548
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/live-url$/.test(path)) {
8773
- const crossSite = refuseCrossSiteRequest(req);
8774
- if (crossSite !== void 0) {
8775
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
8776
- return;
8777
- }
8778
8549
  if (!deps.mintLiveUrl) {
8779
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
8550
+ json(res, { error: "live_view_unavailable" }, 503);
8780
8551
  return;
8781
8552
  }
8782
8553
  let liveUrlRunId;
8783
8554
  try {
8784
8555
  liveUrlRunId = decodeURIComponent(path.slice("/api/runs/".length, -"/live-url".length));
8785
8556
  } catch {
8786
- jsonNoCors(res, { error: "unknown_run" }, 404);
8557
+ json(res, { error: "unknown_run" }, 404);
8787
8558
  return;
8788
8559
  }
8789
8560
  const rawHandle = handleParamOf(req);
8790
8561
  const parsedHandle = (0, import_agent_qa_schema12.parseHandleParam)(rawHandle);
8791
8562
  if (parsedHandle.kind === "invalid") {
8792
- jsonNoCors(res, { error: "invalid_handle" }, 400);
8563
+ json(res, { error: "invalid_handle" }, 400);
8793
8564
  return;
8794
8565
  }
8795
8566
  const handle = parsedHandle.kind === "absent" ? 0 : parsedHandle.index;
8796
8567
  void deps.mintLiveUrl(liveUrlRunId, handle).then(
8797
8568
  (result) => {
8798
- jsonNoCors(res, result);
8569
+ json(res, result);
8799
8570
  },
8800
8571
  (error) => {
8801
8572
  const name = error instanceof Error ? error.name : "";
8802
8573
  if (name === "UnknownRunError") {
8803
- jsonNoCors(res, { error: "unknown_run" }, 404);
8574
+ json(res, { error: "unknown_run" }, 404);
8804
8575
  return;
8805
8576
  }
8806
8577
  if (name === "LiveViewUnavailableError") {
8807
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
8578
+ json(res, { error: "live_view_unavailable" }, 503);
8808
8579
  return;
8809
8580
  }
8810
- jsonNoCors(res, { error: "live_url_mint_failed" }, 500);
8581
+ json(res, { error: "live_url_mint_failed" }, 500);
8811
8582
  }
8812
8583
  );
8813
8584
  return;
@@ -8836,32 +8607,32 @@ function createRouter(dbOrDeps) {
8836
8607
  }
8837
8608
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/evidence$/.test(path)) {
8838
8609
  if (!deps.agentHost) {
8839
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
8610
+ json(res, { error: "agent_host_unavailable" }, 503);
8840
8611
  return;
8841
8612
  }
8842
8613
  let runId;
8843
8614
  try {
8844
8615
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/evidence".length));
8845
8616
  } catch {
8846
- jsonNoCors(res, { error: "invalid_run_id" }, 400);
8617
+ json(res, { error: "invalid_run_id" }, 400);
8847
8618
  return;
8848
8619
  }
8849
- serveRunEvidenceRead(req, res, { runId, host: deps.agentHost, boundary: "human" });
8620
+ serveRunEvidenceRead(req, res, { runId, host: deps.agentHost });
8850
8621
  return;
8851
8622
  }
8852
8623
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/marks$/.test(path)) {
8853
8624
  if (!deps.agentHost) {
8854
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
8625
+ json(res, { error: "agent_host_unavailable" }, 503);
8855
8626
  return;
8856
8627
  }
8857
8628
  let runId;
8858
8629
  try {
8859
8630
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/marks".length));
8860
8631
  } catch {
8861
- jsonNoCors(res, { error: "invalid_run_id" }, 400);
8632
+ json(res, { error: "invalid_run_id" }, 400);
8862
8633
  return;
8863
8634
  }
8864
- serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost, boundary: "human" });
8635
+ serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost });
8865
8636
  return;
8866
8637
  }
8867
8638
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/detail$/.test(path)) {
@@ -8876,7 +8647,7 @@ function createRouter(dbOrDeps) {
8876
8647
  json(res, { error: "invalid_run_id" }, 400);
8877
8648
  return;
8878
8649
  }
8879
- serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost, boundary: "human" });
8650
+ serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost });
8880
8651
  return;
8881
8652
  }
8882
8653
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/analysis$/.test(path)) {
@@ -8891,7 +8662,7 @@ function createRouter(dbOrDeps) {
8891
8662
  json(res, { error: "invalid_run_id" }, 400);
8892
8663
  return;
8893
8664
  }
8894
- serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost, boundary: "human" });
8665
+ serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost });
8895
8666
  return;
8896
8667
  }
8897
8668
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/knowledge$/.test(path)) {
@@ -8908,8 +8679,7 @@ function createRouter(dbOrDeps) {
8908
8679
  }
8909
8680
  serveRunKnowledgeRead(req, res, {
8910
8681
  runId: knowledgeRunId,
8911
- host: deps.agentHost,
8912
- boundary: "human"
8682
+ host: deps.agentHost
8913
8683
  });
8914
8684
  return;
8915
8685
  }
@@ -8959,14 +8729,13 @@ function createRouter(dbOrDeps) {
8959
8729
  notFound(res);
8960
8730
  return;
8961
8731
  }
8962
- if (refuseCrossSiteDisplayRead(req, res)) return;
8963
8732
  const id = segments[3];
8964
8733
  const run = db.getRun(id);
8965
8734
  if (!run) {
8966
8735
  notFound(res, "Run not found");
8967
8736
  return;
8968
8737
  }
8969
- jsonNoCors(res, { run });
8738
+ json(res, { run });
8970
8739
  return;
8971
8740
  }
8972
8741
  if (path === "/api/stats/costs" && req.method === "GET") {
@@ -8977,19 +8746,18 @@ function createRouter(dbOrDeps) {
8977
8746
  return;
8978
8747
  }
8979
8748
  if (path === "/api/insights/knowledge-changes" && req.method === "GET") {
8980
- if (refuseCrossSiteDisplayRead(req, res)) return;
8981
8749
  if (!deps.agentHost) {
8982
- jsonNoCors(res, { status: "unavailable", reason: "agent_host_unavailable" }, 503);
8750
+ json(res, { status: "unavailable", reason: "agent_host_unavailable" }, 503);
8983
8751
  return;
8984
8752
  }
8985
8753
  const from = url.searchParams.get("from") ?? void 0;
8986
8754
  const to = url.searchParams.get("to") ?? void 0;
8987
8755
  void readInsightsKnowledgeChanges(deps.agentHost, db, { from, to }).then(
8988
8756
  (body) => {
8989
- jsonNoCors(res, body);
8757
+ json(res, body);
8990
8758
  },
8991
8759
  () => {
8992
- jsonNoCors(res, { status: "unavailable", reason: "read_failed" }, 500);
8760
+ json(res, { status: "unavailable", reason: "read_failed" }, 500);
8993
8761
  }
8994
8762
  );
8995
8763
  return;
@@ -9055,10 +8823,7 @@ function createRouter(dbOrDeps) {
9055
8823
  serveRunEventStream(req, res, {
9056
8824
  runId,
9057
8825
  handleParam: handleParamOf(req),
9058
- // 403-03 (D-13 · INVK-05): the resume point, read from the SAME pair the authed door reads
9059
- // it from. `url.searchParams.get('lastEventId')` would resolve a repeated parameter to its
9060
- // first value — the one answer `lastEventIdParamOf` exists to refuse — and a second
9061
- // header/query precedence rule here would be a second contract for where a client resumes.
8826
+ // Read from the same pair as the authed door; `searchParams.get` takes a repeated first.
9062
8827
  resume: resumePointOf(req.headers["last-event-id"], lastEventIdParamOf(req)),
9063
8828
  attach: deps.runServiceAttach
9064
8829
  });
@@ -9100,7 +8865,6 @@ function createRouter(dbOrDeps) {
9100
8865
  try {
9101
8866
  attributes = buildRunRequestAttributes({
9102
8867
  trigger: "api",
9103
- runner: "local",
9104
8868
  userAttributes: body.attributes
9105
8869
  });
9106
8870
  } catch (err) {
@@ -9108,25 +8872,10 @@ function createRouter(dbOrDeps) {
9108
8872
  return;
9109
8873
  }
9110
8874
  const args = normalizedFileTarget ? [normalizedFileTarget.executionPath] : [];
9111
- const source = isSuiteFile ? "suite" : "dashboard";
9112
- try {
9113
- await executionBackend?.preflight?.({ args, source });
9114
- } catch (error) {
9115
- const denial = runDenialOf(error);
9116
- if (denial !== void 0) {
9117
- json(res, denial, 403);
9118
- return;
9119
- }
9120
- json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
9121
- return;
9122
- }
9123
8875
  const runId = jobQueue.enqueue({
9124
8876
  name: body.name,
9125
8877
  filePath: normalizedFileTarget?.storagePath,
9126
- // 2026-08-24 (402-09, D-11): NO PARENT KIND IS MINTED. A suite run is ONE run row
9127
- // with member handles, so a parent artifact kind would claim parenthood over rows
9128
- // nothing creates. `isSuiteFile` still travels in `metadata.isSuite` below, and the run's
9129
- // own `suite_id` plus the artifact's `source.kind` snapshot still say it is a suite.
8878
+ // No parent kind is minted; `metadata.isSuite` and `suite_id` say it is a suite.
9130
8879
  kind: "test",
9131
8880
  attributes,
9132
8881
  priority: body.priority,
@@ -9209,17 +8958,6 @@ function createRouter(dbOrDeps) {
9209
8958
  args.push("--disable-cache");
9210
8959
  }
9211
8960
  const isSuite = Boolean(isSuiteFile);
9212
- try {
9213
- await executionBackend?.preflight?.({ args, source: isSuite ? "suite" : "dashboard" });
9214
- } catch (error) {
9215
- const denial = runDenialOf(error);
9216
- if (denial !== void 0) {
9217
- json(res, denial, 403);
9218
- return;
9219
- }
9220
- json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
9221
- return;
9222
- }
9223
8961
  let testName = body.file ? (0, import_node_path10.basename)(normalizedFileTarget?.storagePath ?? body.file, ".yaml") : "unknown";
9224
8962
  let fileParallel;
9225
8963
  let platform;
@@ -9262,11 +9000,10 @@ function createRouter(dbOrDeps) {
9262
9000
  const runId = jobQueue.enqueue({
9263
9001
  name: testName,
9264
9002
  filePath: normalizedFileTarget?.storagePath ?? body.file,
9265
- // 2026-08-24 (402-09, D-11): no parent kind minted — see the enqueue route above.
9003
+ // No parent kind is minted; see the enqueue route above.
9266
9004
  kind: "test",
9267
9005
  attributes: buildRunRequestAttributes({
9268
- trigger: triggerSource.trigger,
9269
- runner: body.local === false ? "browserstack" : "local"
9006
+ trigger: triggerSource.trigger
9270
9007
  }),
9271
9008
  platform,
9272
9009
  parallel: effectiveParallel,
@@ -9819,9 +9556,8 @@ function createRouter(dbOrDeps) {
9819
9556
  return;
9820
9557
  }
9821
9558
  if (path === "/api/config" && req.method === "GET") {
9822
- if (refuseCrossSiteDisplayRead(req, res)) return;
9823
9559
  if (!configManager) {
9824
- jsonNoCors(res, { error: "Config management not available" }, 501);
9560
+ json(res, { error: "Config management not available" }, 501);
9825
9561
  return;
9826
9562
  }
9827
9563
  ;
@@ -9829,7 +9565,7 @@ function createRouter(dbOrDeps) {
9829
9565
  try {
9830
9566
  const read = await configManager.readMasked();
9831
9567
  if (read.kind === "file_invalid") {
9832
- jsonNoCors(res, { error: "file_invalid", message: read.message }, 409);
9568
+ json(res, { error: "file_invalid", message: read.message }, 409);
9833
9569
  return;
9834
9570
  }
9835
9571
  const { config, revision, lines, fileIssues } = read;
@@ -9839,7 +9575,7 @@ function createRouter(dbOrDeps) {
9839
9575
  const activeLlm = use?.llm ?? config.defaultLLM;
9840
9576
  const defaultCfg = Array.isArray(llms) ? llms.find((c) => c?.name === activeLlm) : void 0;
9841
9577
  const provider = defaultCfg?.provider ?? null;
9842
- jsonNoCors(res, {
9578
+ json(res, {
9843
9579
  config,
9844
9580
  provider,
9845
9581
  revision,
@@ -9847,45 +9583,17 @@ function createRouter(dbOrDeps) {
9847
9583
  ...fileIssues.length > 0 ? { fileIssues } : {}
9848
9584
  });
9849
9585
  } catch (err) {
9850
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to read config" }, 500);
9586
+ json(res, { error: err instanceof Error ? err.message : "Failed to read config" }, 500);
9851
9587
  }
9852
9588
  })();
9853
9589
  return;
9854
9590
  }
9855
- if (path === "/api/config/farm/test-connection" && req.method === "POST") {
9856
- readJsonBody(req).then(async (body) => {
9857
- if (body.provider !== "browserstack") {
9858
- json(res, { error: "Only BrowserStack is currently supported" }, 400);
9859
- return;
9860
- }
9861
- try {
9862
- const auth = Buffer.from(`${body.username}:${body.accessKey}`).toString("base64");
9863
- const response = await fetch("https://api-cloud.browserstack.com/app-automate/devices.json", {
9864
- headers: { Authorization: `Basic ${auth}` }
9865
- });
9866
- if (response.ok) {
9867
- json(res, { success: true });
9868
- } else if (response.status === 401) {
9869
- json(res, { success: false, error: "Invalid credentials" });
9870
- } else {
9871
- json(res, { success: false, error: `BrowserStack API returned ${response.status}` });
9872
- }
9873
- } catch (err) {
9874
- json(res, { success: false, error: err instanceof Error ? err.message : "Connection failed" });
9875
- }
9876
- }).catch(() => json(res, { error: "Invalid request body" }, 400));
9877
- return;
9878
- }
9879
9591
  if (path.startsWith("/api/auth/") && req.method === "DELETE") {
9880
9592
  const segments = path.split("/");
9881
9593
  if (segments.length === 4 && segments[2] === "auth") {
9882
9594
  const providerName = decodeURIComponent(segments[3]);
9883
9595
  if (providerName === "status" || providerName === "credential" || providerName === "oauth") {
9884
9596
  } else {
9885
- if (refuseCrossSiteRequest(req) !== void 0) {
9886
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
9887
- return;
9888
- }
9889
9597
  ;
9890
9598
  (async () => {
9891
9599
  try {
@@ -9896,13 +9604,13 @@ function createRouter(dbOrDeps) {
9896
9604
  return credential;
9897
9605
  });
9898
9606
  if (removed === void 0) {
9899
- jsonNoCors(res, { deleted: false });
9607
+ json(res, { deleted: false });
9900
9608
  return;
9901
9609
  }
9902
9610
  const undo = undoStore.put({ kind: "credential", name: providerName, credential: removed }, Date.now());
9903
- jsonNoCors(res, { deleted: true, undo });
9611
+ json(res, { deleted: true, undo });
9904
9612
  } catch (err) {
9905
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to delete credential" }, 500);
9613
+ json(res, { error: err instanceof Error ? err.message : "Failed to delete credential" }, 500);
9906
9614
  }
9907
9615
  })();
9908
9616
  return;
@@ -10012,27 +9720,21 @@ function createRouter(dbOrDeps) {
10012
9720
  const segments = path.split("/");
10013
9721
  const provider = segments.length >= 6 ? decodeURIComponent(segments[4] ?? "") : "";
10014
9722
  const action = segments.length >= 6 ? segments[5] : "";
10015
- const authDoor = req.method === "POST" && (action === "start" || action === "exchange");
10016
- if (authDoor && refuseCrossSiteRequest(req) !== void 0) {
10017
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
10018
- return;
10019
- }
10020
9723
  const plugin = provider ? (0, import_agent_qa_core11.getLLMAuthProviderPlugin)(provider) : void 0;
10021
9724
  if (!plugin) {
10022
- ;
10023
- (authDoor ? jsonNoCors : json)(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
9725
+ json(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
10024
9726
  return;
10025
9727
  }
10026
9728
  if (action === "start" && req.method === "POST") {
10027
9729
  readJsonBody(req).then(async (body) => {
10028
9730
  if (!plugin.startAuth) {
10029
- jsonNoCors(res, { error: `Provider "${provider}" does not support dashboard auth start` }, 400);
9731
+ json(res, { error: `Provider "${provider}" does not support dashboard auth start` }, 400);
10030
9732
  return;
10031
9733
  }
10032
9734
  try {
10033
9735
  const target = await requirePluginAuthConfig(configManager, body.configName, plugin);
10034
9736
  if (!target.ok) {
10035
- jsonNoCors(res, credentialRefusalBody(target), target.status);
9737
+ json(res, credentialRefusalBody(target), target.status);
10036
9738
  return;
10037
9739
  }
10038
9740
  const started = await plugin.startAuth({
@@ -10068,16 +9770,16 @@ function createRouter(dbOrDeps) {
10068
9770
  session.error = err instanceof Error ? err.message : "Authentication failed";
10069
9771
  }).finally(() => cleanupPluginOAuthSession(session));
10070
9772
  }
10071
- jsonNoCors(res, {
9773
+ json(res, {
10072
9774
  authorizeUrl: started.authorizeUrl,
10073
9775
  sessionId,
10074
9776
  mode: plugin.dashboardAuth.mode,
10075
9777
  created: target.created
10076
9778
  });
10077
9779
  } catch (err) {
10078
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to start auth plugin flow" }, 500);
9780
+ json(res, { error: err instanceof Error ? err.message : "Failed to start auth plugin flow" }, 500);
10079
9781
  }
10080
- }).catch(() => jsonNoCors(res, { error: "Invalid request body" }, 400));
9782
+ }).catch(() => json(res, { error: "Invalid request body" }, 400));
10081
9783
  return;
10082
9784
  }
10083
9785
  if (action === "result" && req.method === "GET") {
@@ -10105,22 +9807,22 @@ function createRouter(dbOrDeps) {
10105
9807
  if (action === "exchange" && req.method === "POST") {
10106
9808
  readJsonBody(req).then(async (body) => {
10107
9809
  if (!plugin.exchangeCode) {
10108
- jsonNoCors(res, { error: `Provider "${provider}" does not support code exchange` }, 400);
9810
+ json(res, { error: `Provider "${provider}" does not support code exchange` }, 400);
10109
9811
  return;
10110
9812
  }
10111
9813
  const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
10112
9814
  const code = typeof body.code === "string" ? body.code.trim() : "";
10113
9815
  if (!sessionId || !code) {
10114
- jsonNoCors(res, { error: "sessionId and code are required" }, 400);
9816
+ json(res, { error: "sessionId and code are required" }, 400);
10115
9817
  return;
10116
9818
  }
10117
9819
  const session = pluginOAuthSessions.get(sessionId);
10118
9820
  if (!session || session.providerId !== provider) {
10119
- jsonNoCors(res, { error: "Auth session not found" }, 404);
9821
+ json(res, { error: "Auth session not found" }, 404);
10120
9822
  return;
10121
9823
  }
10122
9824
  if (session.status !== "pending") {
10123
- jsonNoCors(res, { error: "Auth session is no longer pending" }, 409);
9825
+ json(res, { error: "Auth session is no longer pending" }, 409);
10124
9826
  return;
10125
9827
  }
10126
9828
  try {
@@ -10142,61 +9844,57 @@ function createRouter(dbOrDeps) {
10142
9844
  session.error = CREDENTIAL_PROVIDER_CONFLICT.message;
10143
9845
  cleanupPluginOAuthSession(session);
10144
9846
  pluginOAuthSessions.delete(sessionId);
10145
- jsonNoCors(res, credentialRefusalBody(CREDENTIAL_PROVIDER_CONFLICT), CREDENTIAL_PROVIDER_CONFLICT.status);
9847
+ json(res, credentialRefusalBody(CREDENTIAL_PROVIDER_CONFLICT), CREDENTIAL_PROVIDER_CONFLICT.status);
10146
9848
  return;
10147
9849
  }
10148
9850
  session.status = "completed";
10149
9851
  cleanupPluginOAuthSession(session);
10150
9852
  pluginOAuthSessions.delete(sessionId);
10151
- jsonNoCors(res, { status: "completed", saved: true });
9853
+ json(res, { status: "completed", saved: true });
10152
9854
  } catch (err) {
10153
9855
  session.status = "error";
10154
9856
  session.error = err instanceof Error ? err.message : "Token exchange failed";
10155
9857
  cleanupPluginOAuthSession(session);
10156
9858
  pluginOAuthSessions.delete(sessionId);
10157
- jsonNoCors(res, { error: session.error }, 500);
9859
+ json(res, { error: session.error }, 500);
10158
9860
  }
10159
- }).catch(() => jsonNoCors(res, { error: "Invalid request body" }, 400));
9861
+ }).catch(() => json(res, { error: "Invalid request body" }, 400));
10160
9862
  return;
10161
9863
  }
10162
9864
  json(res, { error: "Auth plugin route not found" }, 404);
10163
9865
  return;
10164
9866
  }
10165
9867
  if (path === "/api/auth/credential" && req.method === "POST") {
10166
- if (refuseCrossSiteRequest(req) !== void 0) {
10167
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
10168
- return;
10169
- }
10170
9868
  readJsonBody(req).then(async (body) => {
10171
9869
  const provider = typeof body.provider === "string" ? body.provider.trim() : "";
10172
9870
  const credentialType = typeof body.type === "string" ? body.type.trim() : "";
10173
9871
  const secret = typeof body.secret === "string" ? body.secret.trim() : "";
10174
9872
  if (typeof body.configName !== "string" || body.configName.trim() === "") {
10175
- jsonNoCors(res, { error: "configName is required" }, 400);
9873
+ json(res, { error: "configName is required" }, 400);
10176
9874
  return;
10177
9875
  }
10178
9876
  if (!provider || !isKnownLLMProvider(provider)) {
10179
- jsonNoCors(res, { error: "valid provider is required" }, 400);
9877
+ json(res, { error: "valid provider is required" }, 400);
10180
9878
  return;
10181
9879
  }
10182
9880
  if (credentialType !== "api-key" && credentialType !== "bearer-token") {
10183
- jsonNoCors(res, { error: "type must be api-key or bearer-token" }, 400);
9881
+ json(res, { error: "type must be api-key or bearer-token" }, 400);
10184
9882
  return;
10185
9883
  }
10186
9884
  if (!secret) {
10187
- jsonNoCors(res, { error: "secret is required" }, 400);
9885
+ json(res, { error: "secret is required" }, 400);
10188
9886
  return;
10189
9887
  }
10190
9888
  if ((0, import_agent_qa_core11.getLLMAuthProviderPlugin)(provider)) {
10191
- jsonNoCors(res, { error: "Subscription providers use OAuth login" }, 400);
9889
+ json(res, { error: "Subscription providers use OAuth login" }, 400);
10192
9890
  return;
10193
9891
  }
10194
9892
  if (credentialType === "bearer-token" && provider !== "anthropic-compatible") {
10195
- jsonNoCors(res, { error: "bearer-token credentials are only supported for anthropic-compatible configs" }, 400);
9893
+ json(res, { error: "bearer-token credentials are only supported for anthropic-compatible configs" }, 400);
10196
9894
  return;
10197
9895
  }
10198
9896
  if (credentialType === "api-key" && !API_KEY_CREDENTIAL_PROVIDERS.has(provider)) {
10199
- jsonNoCors(res, { error: "api-key credentials are not supported for this provider" }, 400);
9897
+ json(res, { error: "api-key credentials are not supported for this provider" }, 400);
10200
9898
  return;
10201
9899
  }
10202
9900
  try {
@@ -10211,35 +9909,31 @@ function createRouter(dbOrDeps) {
10211
9909
  return checked;
10212
9910
  });
10213
9911
  if (!target.ok) {
10214
- jsonNoCors(res, credentialRefusalBody(target), target.status);
9912
+ json(res, credentialRefusalBody(target), target.status);
10215
9913
  return;
10216
9914
  }
10217
- jsonNoCors(res, { saved: true, created: target.created });
9915
+ json(res, { saved: true, created: target.created });
10218
9916
  } catch (err) {
10219
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to save credential" }, 500);
9917
+ json(res, { error: err instanceof Error ? err.message : "Failed to save credential" }, 500);
10220
9918
  }
10221
- }).catch(() => jsonNoCors(res, { error: "Invalid request body" }, 400));
9919
+ }).catch(() => json(res, { error: "Invalid request body" }, 400));
10222
9920
  return;
10223
9921
  }
10224
9922
  if (path === "/api/auth/credential/move" && req.method === "POST") {
10225
- if (refuseCrossSiteRequest(req) !== void 0) {
10226
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
10227
- return;
10228
- }
10229
9923
  readJsonBody(req).then(async (raw) => {
10230
9924
  const parsed = import_agent_qa_schema12.LlmCredentialMoveRequestSchema.safeParse(raw);
10231
9925
  if (!parsed.success) {
10232
- jsonNoCors(res, { error: "invalid_setup_input" }, 400);
9926
+ json(res, { error: "invalid_setup_input" }, 400);
10233
9927
  return;
10234
9928
  }
10235
9929
  const from = checkCredentialName(parsed.data.from);
10236
9930
  const to = checkCredentialName(parsed.data.to);
10237
9931
  if (!from.ok || !to.ok) {
10238
- jsonNoCors(res, { error: "name_invalid" }, 400);
9932
+ json(res, { error: "name_invalid" }, 400);
10239
9933
  return;
10240
9934
  }
10241
9935
  if (!configManager) {
10242
- jsonNoCors(res, { error: "Config manager is required to move credentials" }, 400);
9936
+ json(res, { error: "Config manager is required to move credentials" }, 400);
10243
9937
  return;
10244
9938
  }
10245
9939
  try {
@@ -10257,25 +9951,21 @@ function createRouter(dbOrDeps) {
10257
9951
  return void 0;
10258
9952
  });
10259
9953
  if (refusal) {
10260
- jsonNoCors(res, credentialRefusalBody(refusal), refusal.status);
9954
+ json(res, credentialRefusalBody(refusal), refusal.status);
10261
9955
  return;
10262
9956
  }
10263
- jsonNoCors(res, { moved: true });
9957
+ json(res, { moved: true });
10264
9958
  } catch (err) {
10265
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to move credential" }, 500);
9959
+ json(res, { error: err instanceof Error ? err.message : "Failed to move credential" }, 500);
10266
9960
  }
10267
- }).catch(() => jsonNoCors(res, { error: "invalid_setup_input" }, 400));
9961
+ }).catch(() => json(res, { error: "invalid_setup_input" }, 400));
10268
9962
  return;
10269
9963
  }
10270
9964
  if (path === "/api/llm/test" && req.method === "POST") {
10271
- if (refuseCrossSiteRequest(req) !== void 0) {
10272
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
10273
- return;
10274
- }
10275
9965
  readJsonBody(req).then(async (body) => {
10276
9966
  const rawBody = body;
10277
9967
  if ("apiKey" in rawBody) {
10278
- jsonNoCors(res, {
9968
+ json(res, {
10279
9969
  success: false,
10280
9970
  error: "invalid_request",
10281
9971
  message: "apiKey is not accepted in LLM test requests. Save credentials for the named config instead."
@@ -10285,7 +9975,7 @@ function createRouter(dbOrDeps) {
10285
9975
  const providerName = typeof body.provider === "string" ? body.provider.trim() : "";
10286
9976
  const modelName = typeof body.model === "string" ? body.model.trim() : "";
10287
9977
  if (!providerName || !modelName) {
10288
- jsonNoCors(res, { success: false, error: "invalid_request", message: "provider and model are required" }, 400);
9978
+ json(res, { success: false, error: "invalid_request", message: "provider and model are required" }, 400);
10289
9979
  return;
10290
9980
  }
10291
9981
  const configName = typeof body.configName === "string" ? body.configName.trim() : "";
@@ -10310,7 +10000,7 @@ function createRouter(dbOrDeps) {
10310
10000
  }
10311
10001
  const parsedConfig = import_agent_qa_core11.ModelConfigSchema.safeParse(llmCandidate);
10312
10002
  if (!parsedConfig.success) {
10313
- jsonNoCors(res, {
10003
+ json(res, {
10314
10004
  success: false,
10315
10005
  error: "invalid_request",
10316
10006
  message: parsedConfig.error.issues.map((issue) => `${issue.path.map(String).join(".") || "config"}: ${issue.message}`).join("; ")
@@ -10322,7 +10012,7 @@ function createRouter(dbOrDeps) {
10322
10012
  const coreAuth = await import("@vostride/agent-qa-core");
10323
10013
  const resolvedAuth = await coreAuth.resolveLLMAuth(configName, llmConfig2);
10324
10014
  if (resolvedAuth.kind === "missing") {
10325
- jsonNoCors(res, {
10015
+ json(res, {
10326
10016
  success: false,
10327
10017
  error: "missing_credential",
10328
10018
  message: resolvedAuth.message
@@ -10351,7 +10041,7 @@ function createRouter(dbOrDeps) {
10351
10041
  } finally {
10352
10042
  clearTimeout(timeout);
10353
10043
  }
10354
- jsonNoCors(res, {
10044
+ json(res, {
10355
10045
  success: true,
10356
10046
  model: modelName,
10357
10047
  provider: providerName,
@@ -10366,7 +10056,7 @@ function createRouter(dbOrDeps) {
10366
10056
  const providerMessage = withDeclaredMask(declaredMask, (mask) => mask(unsavedMessage));
10367
10057
  const errorClass = (0, import_agent_qa_schema12.classifyProviderError)(providerMessage ?? unsavedMessage, statusCode);
10368
10058
  const message = providerMessage === void 0 ? (0, import_agent_qa_schema12.providerErrorSentence)(errorClass, "").trimEnd() : (0, import_agent_qa_schema12.providerErrorSentence)(errorClass, providerMessage);
10369
- jsonNoCors(res, {
10059
+ json(res, {
10370
10060
  success: false,
10371
10061
  error: errorClass,
10372
10062
  message,
@@ -10376,7 +10066,7 @@ function createRouter(dbOrDeps) {
10376
10066
  ...unauthenticated ? { unauthenticated: true, authMessage } : {}
10377
10067
  });
10378
10068
  }
10379
- }).catch(() => jsonNoCors(res, { success: false, error: "invalid_request", message: "Invalid request body" }, 400));
10069
+ }).catch(() => json(res, { success: false, error: "invalid_request", message: "Invalid request body" }, 400));
10380
10070
  return;
10381
10071
  }
10382
10072
  if (path === "/api/cache/purge" && req.method === "POST") {
@@ -10385,56 +10075,15 @@ function createRouter(dbOrDeps) {
10385
10075
  return;
10386
10076
  }
10387
10077
  readJsonBody(req).then(async (body) => {
10388
- if (!body.file && !body.all) {
10389
- json(res, { error: "Either file or all is required" }, 400);
10078
+ if (body.all !== true) {
10079
+ json(res, { error: "all is required" }, 400);
10390
10080
  return;
10391
10081
  }
10392
10082
  try {
10393
10083
  const cfg = await readConfigValues(configManager);
10394
- const cacheDir = cfg.services?.cache?.dir ?? cfg.cache?.dir;
10395
- if (!cacheDir) {
10396
- json(res, { error: "Cache directory not configured" }, 400);
10397
- return;
10398
- }
10399
10084
  const configDir = deps.configPath ? (0, import_node_path10.dirname)((0, import_node_path10.resolve)(deps.configPath)) : process.cwd();
10400
- const resolvedCacheDir = (0, import_node_path10.resolve)(configDir, cacheDir);
10401
- let branchDirs;
10402
- try {
10403
- branchDirs = await (0, import_promises7.readdir)(resolvedCacheDir);
10404
- } catch (err) {
10405
- if (err.code === "ENOENT") {
10406
- json(res, { purged: 0 });
10407
- return;
10408
- }
10409
- throw err;
10410
- }
10411
- let purged = 0;
10412
- for (const branch of branchDirs) {
10413
- const branchPath = (0, import_node_path10.join)(resolvedCacheDir, branch);
10414
- let entryStat;
10415
- try {
10416
- entryStat = await (0, import_promises7.stat)(branchPath);
10417
- } catch {
10418
- continue;
10419
- }
10420
- if (!entryStat.isDirectory()) {
10421
- await (0, import_promises7.rm)(branchPath, { force: true });
10422
- continue;
10423
- }
10424
- let files;
10425
- try {
10426
- files = await (0, import_promises7.readdir)(branchPath);
10427
- } catch {
10428
- continue;
10429
- }
10430
- for (const file of files) {
10431
- if (!file.endsWith(".json")) continue;
10432
- await (0, import_promises7.rm)((0, import_node_path10.join)(branchPath, file), { force: true });
10433
- purged++;
10434
- }
10435
- await (0, import_promises7.rm)(branchPath, { recursive: true, force: true }).catch(() => {
10436
- });
10437
- }
10085
+ const cacheDir = cfg.services?.cache?.dir ?? import_agent_qa_core11.DEFAULT_AGENT_QA_CACHE_DIR;
10086
+ const purged = await (0, import_agent_qa_runner_local2.purgeActionCache)((0, import_node_path10.resolve)(configDir, cacheDir));
10438
10087
  json(res, { purged });
10439
10088
  } catch (err) {
10440
10089
  json(res, { error: err instanceof Error ? err.message : "Cache purge failed" }, 500);
@@ -10518,7 +10167,7 @@ function createRouter(dbOrDeps) {
10518
10167
  return;
10519
10168
  }
10520
10169
  const workspaceEnv = await readWorkspaceEnvVars(workspacePaths);
10521
- const result = await new import_agent_qa_runner_local2.DockerHookExecutor().execute(resolvedHook, {
10170
+ const result = await new import_agent_qa_runner_local2.DockerHookExecutor({ version: (0, import_agent_qa_core11.getAgentQaVersion)() }).execute(resolvedHook, {
10522
10171
  env: {
10523
10172
  ...workspaceEnv,
10524
10173
  ...validation.overrides
@@ -10554,7 +10203,7 @@ function createRouter(dbOrDeps) {
10554
10203
  variables: shown.variables,
10555
10204
  sandbox: {
10556
10205
  runtime: resolvedHook.runtime,
10557
- image: import_agent_qa_core11.RUNTIME_IMAGE_MAP[resolvedHook.runtime],
10206
+ image: (0, import_agent_qa_core11.hookRunnerImage)(resolvedHook.runtime, (0, import_agent_qa_core11.getAgentQaVersion)()),
10558
10207
  networkMode: resolvedHook.network ? "enabled" : "disabled",
10559
10208
  dockerVersion: null,
10560
10209
  networkLogsAvailable: shown.networkLogs.length > 0,
@@ -10758,12 +10407,8 @@ function createRouter(dbOrDeps) {
10758
10407
  return;
10759
10408
  }
10760
10409
  if (req.method === "GET" && /^\/api\/live-editor\/sessions\/[^/]+\/live-url$/.test(path)) {
10761
- if (refuseCrossSiteRequest(req) !== void 0) {
10762
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
10763
- return;
10764
- }
10765
10410
  if (!deps.mintEditorLiveUrl) {
10766
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
10411
+ json(res, { error: "live_view_unavailable" }, 503);
10767
10412
  return;
10768
10413
  }
10769
10414
  let editorSessionId;
@@ -10772,33 +10417,32 @@ function createRouter(dbOrDeps) {
10772
10417
  path.slice("/api/live-editor/sessions/".length, -"/live-url".length)
10773
10418
  );
10774
10419
  } catch {
10775
- jsonNoCors(res, { error: "unknown_session" }, 404);
10420
+ json(res, { error: "unknown_session" }, 404);
10776
10421
  return;
10777
10422
  }
10778
10423
  void deps.mintEditorLiveUrl(editorSessionId).then(
10779
10424
  (result) => {
10780
- jsonNoCors(res, result);
10425
+ json(res, result);
10781
10426
  },
10782
10427
  (error) => {
10783
10428
  const name = error instanceof Error ? error.name : "";
10784
10429
  if (name === "UnknownLiveEditorSessionError") {
10785
- jsonNoCors(res, { error: "unknown_session" }, 404);
10430
+ json(res, { error: "unknown_session" }, 404);
10786
10431
  return;
10787
10432
  }
10788
10433
  if (name === "LiveViewUnavailableError") {
10789
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
10434
+ json(res, { error: "live_view_unavailable" }, 503);
10790
10435
  return;
10791
10436
  }
10792
- jsonNoCors(res, { error: "live_url_mint_failed" }, 500);
10437
+ json(res, { error: "live_url_mint_failed" }, 500);
10793
10438
  }
10794
10439
  );
10795
10440
  return;
10796
10441
  }
10797
10442
  const liveEditorAuthStateMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)\/auth-state$/);
10798
10443
  if (liveEditorAuthStateMatch && req.method === "POST") {
10799
- if (refuseCrossSiteDisplayRead(req, res)) return;
10800
10444
  if (!sessionManager) {
10801
- jsonNoCors(res, { error: "Live editor not available" }, 503);
10445
+ json(res, { error: "Live editor not available" }, 503);
10802
10446
  return;
10803
10447
  }
10804
10448
  ;
@@ -10809,35 +10453,34 @@ function createRouter(dbOrDeps) {
10809
10453
  const body = await readJsonBody(req);
10810
10454
  const parsedName = import_agent_qa_core11.AuthStateNameSchema.safeParse(typeof body?.name === "string" ? body.name : "");
10811
10455
  if (!parsedName.success) {
10812
- jsonNoCors(res, { error: "Auth state name must be a lowercase slug." }, 400);
10456
+ json(res, { error: "Auth state name must be a lowercase slug." }, 400);
10813
10457
  return;
10814
10458
  }
10815
10459
  stateName = parsedName.data;
10816
10460
  session = sessionManager.getSession(decodeURIComponent(liveEditorAuthStateMatch[1]));
10817
10461
  if (!session) {
10818
- jsonNoCors(res, { error: "Session not found" }, 404);
10462
+ json(res, { error: "Session not found" }, 404);
10819
10463
  return;
10820
10464
  }
10821
10465
  if (typeof session.captureWebAuthState !== "function") {
10822
- jsonNoCors(res, {
10466
+ json(res, {
10823
10467
  error: `Could not save auth state "${stateName}" for target "${getSessionTargetNameForAuthState(session)}".`
10824
10468
  }, 500);
10825
10469
  return;
10826
10470
  }
10827
10471
  const authState = await session.captureWebAuthState(stateName, { replace: body?.replace === true });
10828
- jsonNoCors(res, { authState });
10472
+ json(res, { authState });
10829
10473
  } catch (err) {
10830
10474
  const targetName = getSessionTargetNameForAuthState(session);
10831
10475
  const response = buildAuthStateSaveErrorMessage(stateName || "unknown", targetName, err);
10832
- jsonNoCors(res, { error: response.message }, response.status);
10476
+ json(res, { error: response.message }, response.status);
10833
10477
  }
10834
10478
  })();
10835
10479
  return;
10836
10480
  }
10837
10481
  if (path === "/api/live-editor/sessions" && req.method === "POST") {
10838
- if (refuseCrossSiteDisplayRead(req, res)) return;
10839
10482
  if (!sessionManager) {
10840
- jsonNoCors(res, { error: "Live editor not available" }, 503);
10483
+ json(res, { error: "Live editor not available" }, 503);
10841
10484
  return;
10842
10485
  }
10843
10486
  ;
@@ -10845,16 +10488,10 @@ function createRouter(dbOrDeps) {
10845
10488
  try {
10846
10489
  const body = await readJsonBody(req);
10847
10490
  const targetName = typeof body?.targetName === "string" ? body.targetName : void 0;
10848
- const production = await resolveLiveTargetProduction(configManager, targetName);
10849
- const productionRefusal = liveProductionRefusal(production);
10850
- if (productionRefusal !== void 0) {
10851
- jsonNoCors(res, { error: productionRefusal.message }, 403);
10852
- return;
10853
- }
10854
10491
  const platform = body?.platform ?? "web";
10855
10492
  const runtimeLLM = await resolveDashboardRuntimeLLM(configManager, llmConfig, authFetch);
10856
10493
  if (!runtimeLLM.ok) {
10857
- jsonNoCors(res, { error: runtimeLLM.error }, 400);
10494
+ json(res, { error: runtimeLLM.error }, 400);
10858
10495
  return;
10859
10496
  }
10860
10497
  const setupHooks = Array.isArray(body?.setupHooks) ? body.setupHooks.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
@@ -10873,14 +10510,14 @@ function createRouter(dbOrDeps) {
10873
10510
  let resolvedHooks = /* @__PURE__ */ new Map();
10874
10511
  let hookRegistryError;
10875
10512
  if (!workspacePaths) {
10876
- jsonNoCors(res, { error: "Workspace path resolution not available" }, 500);
10513
+ json(res, { error: "Workspace path resolution not available" }, 500);
10877
10514
  return;
10878
10515
  }
10879
10516
  try {
10880
10517
  const envContent = await (0, import_promises7.readFile)(workspacePaths.envFile.absolutePath, "utf-8");
10881
10518
  envVars = (0, import_agent_qa_core11.parseEnvFile)(envContent);
10882
10519
  } catch {
10883
- jsonNoCors(res, { error: `workspace.envFile file could not be loaded: ${workspacePaths.envFile.workspaceRelativePath}` }, 400);
10520
+ json(res, { error: `workspace.envFile file could not be loaded: ${workspacePaths.envFile.workspaceRelativePath}` }, 400);
10884
10521
  return;
10885
10522
  }
10886
10523
  try {
@@ -10888,7 +10525,7 @@ function createRouter(dbOrDeps) {
10888
10525
  secretStore = import_agent_qa_runner2.SecretStore.fromEnvContent(secretsContent);
10889
10526
  secretRedactor = new import_agent_qa_runner2.SecretRedactor(secretStore);
10890
10527
  } catch {
10891
- jsonNoCors(res, { error: `workspace.secretsFile file could not be loaded: ${workspacePaths.secretsFile.workspaceRelativePath}` }, 400);
10528
+ json(res, { error: `workspace.secretsFile file could not be loaded: ${workspacePaths.secretsFile.workspaceRelativePath}` }, 400);
10892
10529
  return;
10893
10530
  }
10894
10531
  const hookCatalog = await readWorkspaceHooks(configManager, deps.configPath);
@@ -10899,6 +10536,11 @@ function createRouter(dbOrDeps) {
10899
10536
  ...suiteFileManager === void 0 ? {} : { findSuite: (id) => suiteFileManager.findBySuiteId(id) }
10900
10537
  };
10901
10538
  const plannerLimits = await resolveLivePlannerLimits(configManager, entityRef, entityReaders);
10539
+ const navigationTimeoutMs = await resolveLiveNavigationTimeoutMs(
10540
+ configManager,
10541
+ entityRef,
10542
+ entityReaders
10543
+ );
10902
10544
  const sessionSuiteRef = entityRef?.type === "suite" ? entityRef : void 0;
10903
10545
  const plannerLimitsForTest = (testId) => resolveLivePlannerLimits(
10904
10546
  configManager,
@@ -10909,7 +10551,6 @@ function createRouter(dbOrDeps) {
10909
10551
  {
10910
10552
  platform,
10911
10553
  targetName,
10912
- production: { productionTargetNames: production.productionTargetNames },
10913
10554
  plannerLimits,
10914
10555
  plannerLimitsForTest,
10915
10556
  llmConfig: runtimeLLM.llmConfig,
@@ -10923,6 +10564,7 @@ function createRouter(dbOrDeps) {
10923
10564
  hookRegistryError,
10924
10565
  url: body?.url,
10925
10566
  headless: body?.headless ?? false,
10567
+ navigationTimeoutMs,
10926
10568
  useDeviceName: typeof body?.useDeviceName === "string" ? body.useDeviceName : void 0,
10927
10569
  appState: body?.appState === "preserve" || body?.appState === "reset" ? body.appState : void 0,
10928
10570
  bundleId: typeof body?.bundleId === "string" ? body.bundleId : void 0,
@@ -10932,38 +10574,37 @@ function createRouter(dbOrDeps) {
10932
10574
  },
10933
10575
  entityRef
10934
10576
  );
10935
- jsonNoCors(res, { sessionId, sessionNumber }, 201);
10577
+ json(res, { sessionId, sessionNumber }, 201);
10936
10578
  } catch (err) {
10937
10579
  const message = err instanceof Error ? err.message : String(err);
10938
- const status = err instanceof LiveProductionRefusedError ? 403 : err instanceof LivePlannerLimitsRefusedError ? 400 : 500;
10939
- jsonNoCors(res, { error: message }, status);
10580
+ const refused = err instanceof LivePlannerLimitsRefusedError || err instanceof LiveNavigationTimeoutRefusedError;
10581
+ const status = refused ? 400 : 500;
10582
+ json(res, { error: message }, status);
10940
10583
  }
10941
10584
  })();
10942
10585
  return;
10943
10586
  }
10944
10587
  if (path === "/api/live-editor/sessions" && req.method === "GET") {
10945
- if (refuseCrossSiteDisplayRead(req, res)) return;
10946
10588
  if (!sessionManager) {
10947
- jsonNoCors(res, { error: "Live editor not available" }, 503);
10589
+ json(res, { error: "Live editor not available" }, 503);
10948
10590
  return;
10949
10591
  }
10950
- jsonNoCors(res, { sessions: sessionManager.listSessions() });
10592
+ json(res, { sessions: sessionManager.listSessions() });
10951
10593
  return;
10952
10594
  }
10953
10595
  const liveEditorSessionMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)$/);
10954
10596
  if (liveEditorSessionMatch && req.method === "DELETE") {
10955
- if (refuseCrossSiteDisplayRead(req, res)) return;
10956
10597
  if (!sessionManager) {
10957
- jsonNoCors(res, { error: "Live editor not available" }, 503);
10598
+ json(res, { error: "Live editor not available" }, 503);
10958
10599
  return;
10959
10600
  }
10960
10601
  const sessionId = liveEditorSessionMatch[1];
10961
10602
  (async () => {
10962
10603
  const terminated = await sessionManager.terminateSession(sessionId);
10963
10604
  if (terminated) {
10964
- jsonNoCors(res, { ok: true });
10605
+ json(res, { ok: true });
10965
10606
  } else {
10966
- jsonNoCors(res, { error: "Session not found" }, 404);
10607
+ json(res, { error: "Session not found" }, 404);
10967
10608
  }
10968
10609
  })();
10969
10610
  return;
@@ -11012,6 +10653,7 @@ function createRouter(dbOrDeps) {
11012
10653
  // src/queue/job-queue.ts
11013
10654
  var import_node_events = require("node:events");
11014
10655
  var import_node_os2 = require("node:os");
10656
+ var import_agent_qa_core12 = require("@vostride/agent-qa-core");
11015
10657
  var NOT_CANCELLABLE = { kind: "not-cancellable" };
11016
10658
  var JobQueue = class extends import_node_events.EventEmitter {
11017
10659
  db;
@@ -11034,8 +10676,7 @@ var JobQueue = class extends import_node_events.EventEmitter {
11034
10676
  this.pollInterval = setInterval(() => this.dispatchReported("a poll"), intervalMs);
11035
10677
  this.on("enqueued", () => this.dispatchReported("an enqueue"));
11036
10678
  }
11037
- // 2026-10-17 (444-16, D-50): the server closes the database after this; a run that settles later
11038
- // still frees its slot, and nothing reads the closed database.
10679
+ // The server closes the database after this; a run settling later still frees its slot.
11039
10680
  stop() {
11040
10681
  this.stopped = true;
11041
10682
  if (this.pollInterval) {
@@ -11121,7 +10762,10 @@ var JobQueue = class extends import_node_events.EventEmitter {
11121
10762
  this.processNext();
11122
10763
  } catch (error) {
11123
10764
  const message = error instanceof Error ? error.message : String(error);
11124
- console.error(`Queue could not dispatch the next run after ${trigger}: ${message}`);
10765
+ (0, import_agent_qa_core12.printRunText)(
10766
+ `Queue could not dispatch the next run after ${trigger}: ${message}`,
10767
+ (text) => console.error(text)
10768
+ );
11125
10769
  }
11126
10770
  }
11127
10771
  getActiveCount() {
@@ -11130,9 +10774,7 @@ var JobQueue = class extends import_node_events.EventEmitter {
11130
10774
  getConcurrency() {
11131
10775
  return this.concurrency;
11132
10776
  }
11133
- // 2026-10-16 (442-05, D-06 to D-08): only the owner cancels a running row; gone is an orphan.
11134
- // 442-15 (GAP-D2): own is the stamp this handle wrote, or equal non-null start times.
11135
- // 2026-10-17 (444-06, D-11): a running row is never written here; the run answers with its verdict.
10777
+ // Only the owner stops its running row, never written here; a gone owner's row is an orphan.
11136
10778
  cancel(runId) {
11137
10779
  const run = this.db.getRun(runId);
11138
10780
  if (!run) return NOT_CANCELLABLE;
@@ -11162,21 +10804,7 @@ var JobQueue = class extends import_node_events.EventEmitter {
11162
10804
  const run = this.db.getRun(runId);
11163
10805
  return run && run.status !== "running" && run.status !== "pending" ? { kind: "finished", status: run.status } : NOT_CANCELLABLE;
11164
10806
  }
11165
- // 2026-08-24 (Phase 402 plan 09, D-11): THE CHILD-RUN CANCELLATION FAN-OUT IS DELETED WITH THE
11166
- // MODEL THAT MINTED ITS CHILDREN.
11167
- //
11168
- // ~19 lines stood here walking the run's child rows and cancelling each member row. Under
11169
- // the one-run model a parallel suite is ONE run row with member handles (402-04) — nothing writes
11170
- // the run row's parent-link column any more (405-07 then deleted the column outright), so the
11171
- // walk could only ever have read an empty list while reading as live cancellation policy.
11172
- // Cancelling the run cancels the run.
11173
- //
11174
- // WHAT THIS DOES NOT TOUCH: `parallel` below. D-11 keeps the queue's parallel flag as CROSS-RUN
11175
- // concurrency of INDEPENDENT jobs, which is what `findNextEligible` has always used it for. What
11176
- // was deleted is parallel-SUITE semantics, not the queue's ability to run two unrelated jobs.
11177
- //
11178
- // HISTORICAL ROWS: a pre-402 suite run still lists its members on the read side; cancelling
11179
- // such a run now leaves those member rows in whatever state their own process left them.
10807
+ // `parallel` is cross-run concurrency of independent jobs; a suite is one run row.
11180
10808
  findNextEligible() {
11181
10809
  if (this.activeCount >= this.concurrency) return void 0;
11182
10810
  const pending = this.db.getPendingRuns();
@@ -11203,17 +10831,17 @@ var import_promises8 = require("node:fs/promises");
11203
10831
  var import_node_path11 = require("node:path");
11204
10832
  var import_agent_qa_ids3 = require("@vostride/agent-qa-ids");
11205
10833
  var import_yaml8 = require("yaml");
11206
- var import_agent_qa_core12 = require("@vostride/agent-qa-core");
10834
+ var import_agent_qa_core13 = require("@vostride/agent-qa-core");
11207
10835
  var SuiteFileManager = class {
11208
10836
  constructor(workspace, testFileManager) {
11209
10837
  this.workspace = workspace;
11210
10838
  this.testFileManager = testFileManager;
11211
10839
  }
11212
10840
  testFileManager;
11213
- // 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.
10841
+ // Each list call reads the workspace itself; each file is written whole, one writer at a time.
11214
10842
  underLock = createPathLock();
11215
10843
  async list() {
11216
- const records = await (0, import_agent_qa_core12.discoverWorkspaceFiles)({ workspace: this.workspace, kind: "suite" });
10844
+ const records = await (0, import_agent_qa_core13.discoverWorkspaceFiles)({ workspace: this.workspace, kind: "suite" });
11217
10845
  const files = [];
11218
10846
  for (const record of records) {
11219
10847
  try {
@@ -11275,7 +10903,7 @@ var SuiteFileManager = class {
11275
10903
  if (!trimmedPath) {
11276
10904
  throw new Error("Suite path is required");
11277
10905
  }
11278
- const target = await (0, import_agent_qa_core12.resolveWorkspaceFileTarget)({
10906
+ const target = await (0, import_agent_qa_core13.resolveWorkspaceFileTarget)({
11279
10907
  workspace: this.workspace,
11280
10908
  kind: "suite",
11281
10909
  filePath: trimmedPath,
@@ -11299,7 +10927,7 @@ var SuiteFileManager = class {
11299
10927
  async validate(content) {
11300
10928
  try {
11301
10929
  const parsed = (0, import_yaml8.parse)(content);
11302
- const result = import_agent_qa_core12.SuiteDefinitionSchema.safeParse(parsed);
10930
+ const result = import_agent_qa_core13.SuiteDefinitionSchema.safeParse(parsed);
11303
10931
  if (!result.success) {
11304
10932
  return {
11305
10933
  valid: false,
@@ -11337,7 +10965,7 @@ var SuiteFileManager = class {
11337
10965
  }
11338
10966
  }
11339
10967
  async resolveTarget(relativePath, requireExisting) {
11340
- return (0, import_agent_qa_core12.resolveWorkspaceFileTarget)({
10968
+ return (0, import_agent_qa_core13.resolveWorkspaceFileTarget)({
11341
10969
  workspace: this.workspace,
11342
10970
  kind: "suite",
11343
10971
  filePath: relativePath,
@@ -11349,7 +10977,7 @@ var SuiteFileManager = class {
11349
10977
  // src/execution/appium-manager.ts
11350
10978
  var import_node_http = require("node:http");
11351
10979
  var import_node_child_process2 = require("node:child_process");
11352
- var import_agent_qa_core13 = require("@vostride/agent-qa-core");
10980
+ var import_agent_qa_core14 = require("@vostride/agent-qa-core");
11353
10981
  var import_tree_kill = __toESM(require("tree-kill"), 1);
11354
10982
  var import_picocolors = __toESM(require("picocolors"), 1);
11355
10983
  var APPIUM_PREFIX = import_picocolors.default.magenta("[appium]");
@@ -11370,7 +10998,7 @@ var AppiumManager = class {
11370
10998
  this.startupTimeoutMs = opts?.startupTimeoutMs ?? 3e4;
11371
10999
  this.pollIntervalMs = opts?.pollIntervalMs ?? 500;
11372
11000
  this.logLevel = opts?.logLevel ?? "normal";
11373
- this.appiumResolver = opts?.appiumResolver ?? import_agent_qa_core13.resolveAppiumExecutable;
11001
+ this.appiumResolver = opts?.appiumResolver ?? import_agent_qa_core14.resolveAppiumExecutable;
11374
11002
  }
11375
11003
  async acquire() {
11376
11004
  await this.ensureRunning();
@@ -11473,23 +11101,23 @@ var AppiumManager = class {
11473
11101
  this.managed = true;
11474
11102
  child.on("error", (err) => {
11475
11103
  if (err.code === "ENOENT") {
11476
- console.error(`${APPIUM_PREFIX} Appium not found. ${(0, import_agent_qa_core13.formatAppiumInstallGuidance)()}`);
11104
+ console.error(`${APPIUM_PREFIX} Appium not found. ${(0, import_agent_qa_core14.formatAppiumInstallGuidance)()}`);
11477
11105
  } else {
11478
- console.error(`${APPIUM_PREFIX} Error: ${err.message}`);
11106
+ (0, import_agent_qa_core14.printRunText)(`${APPIUM_PREFIX} Error: ${err.message}`, (text) => console.error(text));
11479
11107
  }
11480
11108
  });
11481
11109
  child.stdout?.on("data", (chunk) => {
11482
11110
  if (this.logLevel !== "debug") return;
11483
11111
  const lines = chunk.toString().split("\n").filter(Boolean);
11484
11112
  for (const line of lines) {
11485
- console.log(`${APPIUM_PREFIX} ${import_picocolors.default.dim(line)}`);
11113
+ (0, import_agent_qa_core14.printRunText)(`${APPIUM_PREFIX} ${import_picocolors.default.dim(line)}`, (text) => console.log(text));
11486
11114
  }
11487
11115
  });
11488
11116
  child.stderr?.on("data", (chunk) => {
11489
11117
  if (this.logLevel !== "debug") return;
11490
11118
  const lines = chunk.toString().split("\n").filter(Boolean);
11491
11119
  for (const line of lines) {
11492
- console.error(`${APPIUM_PREFIX} ${import_picocolors.default.dim(line)}`);
11120
+ (0, import_agent_qa_core14.printRunText)(`${APPIUM_PREFIX} ${import_picocolors.default.dim(line)}`, (text) => console.error(text));
11493
11121
  }
11494
11122
  });
11495
11123
  const start = Date.now();
@@ -11571,10 +11199,7 @@ var SessionManager = class {
11571
11199
  this.deps = deps;
11572
11200
  }
11573
11201
  sessions = /* @__PURE__ */ new Map();
11574
- // Per-entity counter keyed by `${entityType}:${entityId}`.
11575
- // Monotonic and ephemeral: resets on server restart per CONTEXT D-30.
11576
- // Never decremented on terminate — a re-open of a terminated entity yields
11577
- // the next number so "Session #3" after "#2" ends does not confuse users.
11202
+ // Monotonic per entity and reset on restart, so a reopened entity gets the next number.
11578
11203
  counters = /* @__PURE__ */ new Map();
11579
11204
  async createSession(config, entity) {
11580
11205
  if (!config.llmConfig) {
@@ -11631,7 +11256,8 @@ var SessionManager = class {
11631
11256
  };
11632
11257
 
11633
11258
  // src/live-editor/ws-handler.ts
11634
- var import_agent_qa_core14 = require("@vostride/agent-qa-core");
11259
+ var import_node_util4 = require("node:util");
11260
+ var import_agent_qa_core15 = require("@vostride/agent-qa-core");
11635
11261
 
11636
11262
  // src/live-editor/types.ts
11637
11263
  var LIVE_EDITOR_UNKNOWN_SESSION_CLOSE_CODE = 4404;
@@ -11818,12 +11444,14 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
11818
11444
  try {
11819
11445
  raw = await testFileManager.read(msg.path);
11820
11446
  } catch (readErr) {
11821
- console.error(`[ws-handler] execute-test: read failed for path=${msg.path}`, readErr);
11447
+ const line = `[ws-handler] execute-test: read failed for path=${msg.path}`;
11448
+ (0, import_agent_qa_core15.printRunText)((0, import_node_util4.format)(line, readErr), () => console.error(line, readErr));
11822
11449
  throw new Error(`Cannot read test file "${msg.path}": ${readErr?.message ?? String(readErr)}`);
11823
11450
  }
11824
- const parsed = (0, import_agent_qa_core14.parseTestFile)(raw, msg.path);
11451
+ const parsed = (0, import_agent_qa_core15.parseTestFile)(raw, msg.path);
11825
11452
  if (parsed.errors.length > 0) {
11826
- console.error(`[ws-handler] execute-test: parse errors for ${msg.path}`, parsed.errors);
11453
+ const line = `[ws-handler] execute-test: parse errors for ${msg.path}`;
11454
+ (0, import_agent_qa_core15.printRunText)((0, import_node_util4.format)(line, parsed.errors), () => console.error(line, parsed.errors));
11827
11455
  throw new Error(`Parse error in ${msg.path}: ${parsed.errors[0].message}`);
11828
11456
  }
11829
11457
  const testDef = parsed.tests[0];
@@ -11833,7 +11461,13 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
11833
11461
  const stepInstructions = testDef.steps.map(
11834
11462
  (s) => typeof s === "string" ? s : s.step
11835
11463
  );
11836
- console.log(`[ws-handler] execute-test: starting "${testDef.name}" (${stepInstructions.length} steps, setup=${(testDef.setup ?? []).length}, teardown=${(testDef.teardown ?? []).length})`);
11464
+ const stepCount = stepInstructions.length;
11465
+ const setupCount = (testDef.setup ?? []).length;
11466
+ const teardownCount = (testDef.teardown ?? []).length;
11467
+ (0, import_agent_qa_core15.printRunText)(
11468
+ `[ws-handler] execute-test: starting "${testDef.name}" (${stepCount} steps, setup=${setupCount}, teardown=${teardownCount})`,
11469
+ (text) => console.log(text)
11470
+ );
11837
11471
  reply({
11838
11472
  type: "test-start",
11839
11473
  test: {
@@ -11855,7 +11489,11 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
11855
11489
  teardown: testDef.teardown ?? []
11856
11490
  }
11857
11491
  );
11858
- console.log(`[ws-handler] execute-test: finished "${testDef.name}" status=${result.status} duration=${result.duration}ms${result.error ? ` error=${result.error}` : ""}`);
11492
+ const errorPart = result.error ? ` error=${result.error}` : "";
11493
+ (0, import_agent_qa_core15.printRunText)(
11494
+ `[ws-handler] execute-test: finished "${testDef.name}" status=${result.status} duration=${result.duration}ms${errorPart}`,
11495
+ (text) => console.log(text)
11496
+ );
11859
11497
  reply({
11860
11498
  type: "test-complete",
11861
11499
  test: {
@@ -11868,11 +11506,9 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
11868
11506
  });
11869
11507
  reply({ type: "session-state", state: session.getState() });
11870
11508
  } catch (err) {
11871
- console.error(`[ws-handler] execute-test: caught error for path=${msg.path}`, {
11872
- message: err?.message,
11873
- stack: err?.stack,
11874
- code: err?.code
11875
- });
11509
+ const line = `[ws-handler] execute-test: caught error for path=${msg.path}`;
11510
+ const detail = { message: err?.message, stack: err?.stack, code: err?.code };
11511
+ (0, import_agent_qa_core15.printRunText)((0, import_node_util4.format)(line, detail), () => console.error(line, detail));
11876
11512
  reply({
11877
11513
  type: "test-error",
11878
11514
  test: {
@@ -12023,11 +11659,9 @@ var LIVE_EMBED_STATES = [
12023
11659
  "no-stream",
12024
11660
  "expired",
12025
11661
  "refused",
12026
- // THIS MEMBER'S STREAM ENDED WHILE THE RUN CONTINUES (403 CR-03). Separate the whole run's end,
12027
- // because a person watching one member of a suite needs to know which of the two happened — and
12028
- // neither may be shown as `live` over whichever frame happened to be last.
11662
+ // This member's stream ended while the run continues, so the viewer knows which one ended.
12029
11663
  "stream-ended",
12030
- // AN EDITOR SESSION ENDED (428 D-07/D-09). An editor subject has no run to end.
11664
+ // An editor session ended; an editor subject has no run to end.
12031
11665
  "session-ended",
12032
11666
  "ended",
12033
11667
  "lost"
@@ -12156,8 +11790,7 @@ function renderLiveEmbedPage(input) {
12156
11790
  if (pending === null) return;
12157
11791
  var header = pending;
12158
11792
  pending = null;
12159
- // NO MIME TYPE: the decoder sniffs the bytes, so a JPEG screencast frame and a PNG snapshot
12160
- // both paint in every host, not only in Chromium (428 RESEARCH A2).
11793
+ // No MIME type: the decoder sniffs, so JPEG and PNG frames paint in every host.
12161
11794
  var blob = new Blob([event.data]);
12162
11795
  createImageBitmap(blob).then(function (bitmap) {
12163
11796
  if (canvas.width !== header.width || canvas.height !== header.height) {
@@ -12181,8 +11814,7 @@ function renderLiveEmbedPage(input) {
12181
11814
  try { msg = JSON.parse(event.data); } catch (e) { return; }
12182
11815
 
12183
11816
  if (msg.type === 'ready') {
12184
- // THE PAINTER IS CHOSEN BY THE DECLARED KIND \u2014 never by a browser name, an adapter class or
12185
- // a transport guess (LIVE-04's prohibition). A snapshot paints on the same canvas (428 D-11).
11817
+ // The declared kind picks the painter, never a browser name, adapter or transport guess.
12186
11818
  var kind = msg.stream ? msg.stream.kind : undefined;
12187
11819
  if (kind === 'cdp-screencast' || kind === 'snapshot') {
12188
11820
  setState('connecting', 'waiting for the first frame\u2026');
@@ -12229,8 +11861,7 @@ function renderLiveEmbedPage(input) {
12229
11861
  report({ event: 'lost' });
12230
11862
  });
12231
11863
 
12232
- // A HOST THAT RE-MINTS replaces the fragment; the socket renews in place rather than reloading,
12233
- // so a renewal never costs the viewer the stream it is watching (D-02).
11864
+ // A re-minting host replaces the fragment; the socket renews in place and the stream continues.
12234
11865
  window.addEventListener('hashchange', function () {
12235
11866
  var next = location.hash.slice(1);
12236
11867
  if (next === '' || socket.readyState !== 1) return;
@@ -12439,7 +12070,7 @@ function createFrameProxy(deps) {
12439
12070
  }
12440
12071
 
12441
12072
  // src/server/server.ts
12442
- var import_agent_qa_core15 = require("@vostride/agent-qa-core");
12073
+ var import_agent_qa_core16 = require("@vostride/agent-qa-core");
12443
12074
  var MIME_TYPES = {
12444
12075
  ".html": "text/html; charset=utf-8",
12445
12076
  ".js": "application/javascript",
@@ -12458,9 +12089,10 @@ var MIME_TYPES = {
12458
12089
  ".webp": "image/webp"
12459
12090
  };
12460
12091
  var DashboardRemoteBindRefusedError = class extends Error {
12461
- constructor(host) {
12092
+ constructor(host, port) {
12093
+ const p = String(port === 0 ? 3470 : port);
12462
12094
  super(
12463
- `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.`
12095
+ `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}/.`
12464
12096
  );
12465
12097
  this.host = host;
12466
12098
  this.name = "DashboardRemoteBindRefusedError";
@@ -12471,8 +12103,8 @@ async function startServer(opts) {
12471
12103
  const { db, port = 3470, uiDir, workspacePaths, configPath, llmConfig, authFetch, agentHost, executionBackend, runService, runServiceReadiness, liveView, liveEditorSubjects, postProcessingRelay } = opts;
12472
12104
  const runServiceAttach = opts.runServiceAttach ?? runService?.attach;
12473
12105
  const host = opts.host?.trim() || DEFAULT_DASHBOARD_HOST;
12474
- if (!(0, import_agent_qa_core15.isLoopbackHost)(host)) {
12475
- throw new DashboardRemoteBindRefusedError(host);
12106
+ if (!(0, import_agent_qa_core16.isLoopbackHost)(host)) {
12107
+ throw new DashboardRemoteBindRefusedError(host, port);
12476
12108
  }
12477
12109
  const resolvedConfigPath = configPath ?? (0, import_node_path12.resolve)("agent-qa.config.yaml");
12478
12110
  const configManager = new ConfigManager(resolvedConfigPath, opts.declaredSecretMask);
@@ -12507,7 +12139,7 @@ async function startServer(opts) {
12507
12139
  failureSummary = "Test timed out -- process was killed";
12508
12140
  } else if (details.refusedBeforeStart) {
12509
12141
  dbStatus = "failed";
12510
- refusalProse = details.neutralReason ?? details.reason;
12142
+ refusalProse = details.reason;
12511
12143
  failureSummary = refusalProse ? `Refused before the run started: ${refusalProse}`.slice(0, 300) : "Refused before the run started";
12512
12144
  } else if (status === "completed") {
12513
12145
  dbStatus = "failed";
@@ -12588,7 +12220,10 @@ async function startServer(opts) {
12588
12220
  mobileRuns.set(run.id, platform);
12589
12221
  } catch (err) {
12590
12222
  const message = err instanceof Error ? err.message : String(err);
12591
- console.error(`Failed to start Appium for run ${run.id}: ${message}`);
12223
+ (0, import_agent_qa_core16.printRunText)(
12224
+ `Failed to start Appium for run ${run.id}: ${message}`,
12225
+ (text) => console.error(text)
12226
+ );
12592
12227
  db.updateRun(run.id, {
12593
12228
  status: "failed",
12594
12229
  failureSummary: `Appium server failed to start: ${message}`,
@@ -12608,19 +12243,9 @@ async function startServer(opts) {
12608
12243
  runId: run.id,
12609
12244
  args,
12610
12245
  source: meta.isSuite ? "suite" : "dashboard",
12611
- // 2026-10-01 (432-14, D-14): no confirm scope is read off the row any more. The browser that
12612
- // triggered a run is not asked anything, so nothing about it has to survive the wait.
12613
12246
  attributes: run.attributes,
12614
- // T-401-G26 (401-23): a persisted row's timeout is VALIDATED at the seam that consumes it.
12615
- // `in-process-backend.ts:211` reads `opts.timeout ?? defaultTimeout` and validates AFTER, so
12616
- // any non-nullish nonsense here skips the default and leaves the run with no reclamation
12617
- // timer at all. An unusable value falls back to the backend's default rather than a number
12618
- // this call invented.
12247
+ // The backend takes `opts.timeout ?? defaultTimeout`, so an unusable value falls back here.
12619
12248
  timeout: sanitizeQueuedExecutionTimeout(meta.timeout),
12620
- // THE LEASE ABOVE IS UNCHANGED; ONLY ITS DELIVERY IS. `acquireLease` still runs where it ran,
12621
- // still keys on this run id and still owns the bundled Appium's lifecycle (design-of-record).
12622
- // What used to ride `AGENT_QA_APPIUM_URL` in a child's environment now travels as an
12623
- // argument, because there is no child to carry an environment.
12624
12249
  ...isMobile ? { appiumUrl: appiumManager.getUrl() } : {}
12625
12250
  });
12626
12251
  });
@@ -12631,16 +12256,14 @@ async function startServer(opts) {
12631
12256
  appiumManager,
12632
12257
  configManager,
12633
12258
  configPath: resolvedConfigPath,
12634
- // 428 D-07: each created session registers here, which is what the frame socket attaches.
12259
+ // Each created session registers here, which is what the frame socket attaches.
12635
12260
  ...liveEditorSubjects === void 0 ? {} : { liveEditorSubjects }
12636
12261
  });
12637
12262
  const servesLiveView = liveView !== void 0 && (runServiceAttach !== void 0 || liveEditorSubjects !== void 0);
12638
12263
  const apiRouter = createRouter({
12639
12264
  db,
12640
12265
  workspacePaths,
12641
- // 401-07: conditionally spread on the same house rule `agentHost` follows — an absent
12642
- // dependency is an ABSENT field, never a present `undefined`, so the router's own 503 arm is
12643
- // what decides, in one place.
12266
+ // Absent dependencies are absent fields, so the router's own 503 arm decides in one place.
12644
12267
  ...executionBackend === void 0 ? {} : { executionBackend },
12645
12268
  jobQueue,
12646
12269
  testFileManager,
@@ -12651,27 +12274,13 @@ async function startServer(opts) {
12651
12274
  llmConfig,
12652
12275
  authFetch,
12653
12276
  sessionManager,
12654
- // 400-01: conditionally spread on the house rule an absent dependency is an ABSENT field and
12655
- // never a present `undefined` — the router's own 501 arm is what decides, in one place.
12277
+ // Absent dependencies are absent fields, so the router's own 501 arm decides in one place.
12656
12278
  ...agentHost === void 0 ? {} : { agentHost },
12657
- // 402-05 (D-15): the same conditional-spread house rule. Absent means a legacy mount rather
12658
- // than an unconfigured service, and the router's own 503 arm is what decides, in one place.
12659
12279
  ...runServiceReadiness === void 0 ? {} : { runServiceReadiness },
12660
- // 402-08 (D-01): the LOCAL run-stream door's attach port, READ OFF `runService` rather than
12661
- // composed again. That derivation is the "one session registry serves both doors" claim in its
12662
- // structural form — there is no second wiring a composition root could forget or point
12663
- // somewhere else, and the two doors cannot end up attached to different registries. Absent
12664
- // `runService` means no registry exists at all, so the same conditional-spread house rule
12665
- // applies and the router's own 503 arm decides.
12280
+ // Read off `runService` by default, so both doors attach to one session registry.
12666
12281
  ...runServiceAttach === void 0 ? {} : { runServiceAttach },
12667
- // 403 D-03: the mint route's port, on the same conditional-spread house rule — absent means
12668
- // this host serves no live view and the route's own 503 arm decides, in one place.
12669
12282
  ...liveView === void 0 ? {} : { mintLiveUrl: liveView.mintLiveUrl },
12670
- // 428 D-07: the editor mint, on the same house rule — absent means the editor mint route's own
12671
- // 503 decides.
12672
12283
  ...liveView?.mintEditorLiveUrl === void 0 ? {} : { mintEditorLiveUrl: liveView.mintEditorLiveUrl },
12673
- // 404-05 (D-16): the same conditional-spread house rule — absent means this host serves no
12674
- // post-processing push channel and the stream arm's own 503 decides, in one place.
12675
12284
  ...postProcessingRelay === void 0 ? {} : { postProcessingRelay }
12676
12285
  });
12677
12286
  const runServiceRouter = runService === void 0 ? void 0 : createRunServiceRoutes(runService);
@@ -12691,13 +12300,15 @@ async function startServer(opts) {
12691
12300
  apiRouter(req, res);
12692
12301
  return;
12693
12302
  }
12303
+ if (refuseNonLoopbackHost(req) !== void 0) {
12304
+ const address = server.address();
12305
+ const boundPort = typeof address === "object" && address !== null ? address.port : port;
12306
+ refuseForeignHostPage(res, boundPort);
12307
+ return;
12308
+ }
12694
12309
  if (req.method === "OPTIONS") {
12695
- res.writeHead(204, {
12696
- "Access-Control-Allow-Origin": "*",
12697
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
12698
- "Access-Control-Allow-Headers": "Content-Type"
12699
- });
12700
- res.end();
12310
+ res.writeHead(404, { "Content-Type": "text/plain" });
12311
+ res.end("Not Found");
12701
12312
  return;
12702
12313
  }
12703
12314
  if (req.method === "GET" && path.startsWith(LIVE_EMBED_PATH_PREFIX)) {
@@ -12741,8 +12352,7 @@ async function startServer(opts) {
12741
12352
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
12742
12353
  res.writeHead(200, {
12743
12354
  "Content-Type": contentType,
12744
- "Content-Length": content.length,
12745
- "Access-Control-Allow-Origin": "*"
12355
+ "Content-Length": content.length
12746
12356
  });
12747
12357
  res.end(content);
12748
12358
  return;
@@ -12754,8 +12364,7 @@ async function startServer(opts) {
12754
12364
  const content = await (0, import_promises9.readFile)(indexPath);
12755
12365
  res.writeHead(200, {
12756
12366
  "Content-Type": "text/html",
12757
- "Content-Length": content.length,
12758
- "Access-Control-Allow-Origin": "*"
12367
+ "Content-Length": content.length
12759
12368
  });
12760
12369
  res.end(content);
12761
12370
  return;
@@ -12840,7 +12449,8 @@ async function startServer(opts) {
12840
12449
  session.clearIdleTimer();
12841
12450
  const handler = createSessionHandler(session, () => {
12842
12451
  void sessionManager.terminateSession(sessionId).catch((error) => {
12843
- console.error(`[live-editor] could not end session ${sessionId}`, error);
12452
+ const line = `[live-editor] could not end session ${sessionId}`;
12453
+ (0, import_agent_qa_core16.printRunText)((0, import_node_util5.format)(line, error), () => console.error(line, error));
12844
12454
  });
12845
12455
  }, (event) => {
12846
12456
  db.insertTokenEvent(event);
@@ -12873,7 +12483,7 @@ async function startServer(opts) {
12873
12483
  server.listen(port, host, () => {
12874
12484
  server.off("error", onError);
12875
12485
  console.log(
12876
- (0, import_agent_qa_core15.isLoopbackHost)(host) ? `Dashboard running at http://localhost:${port}` : `Dashboard running at http://${host.includes(":") ? `[${host}]` : host}:${port}`
12486
+ (0, import_agent_qa_core16.isLoopbackHost)(host) ? `Dashboard running at http://localhost:${port}` : `Dashboard running at http://${host.includes(":") ? `[${host}]` : host}:${port}`
12877
12487
  );
12878
12488
  resolve7({
12879
12489
  server,
@@ -12930,9 +12540,6 @@ var InProcessExecutionBackend = class extends import_node_events2.EventEmitter {
12930
12540
  this.options = options;
12931
12541
  this.retentionMs = options.retentionMs ?? DEFAULT_HANDLE_RETENTION_MS;
12932
12542
  }
12933
- async preflight(opts) {
12934
- await this.options.preflightRun?.(opts);
12935
- }
12936
12543
  execute(opts) {
12937
12544
  const handle = {
12938
12545
  runId: opts.runId,
@@ -13008,9 +12615,7 @@ var InProcessExecutionBackend = class extends import_node_events2.EventEmitter {
13008
12615
  settle(handle, error) {
13009
12616
  if (handle.timeoutTimer) clearTimeout(handle.timeoutTimer);
13010
12617
  let refusedBeforeStart = false;
13011
- let neutralReason;
13012
12618
  if (error !== void 0) {
13013
- neutralReason = dashboardNeutralReasonOf(error);
13014
12619
  const message = error instanceof Error ? error.message : String(error);
13015
12620
  for (const line of message.split("\n")) handle.output.push(line);
13016
12621
  refusedBeforeStart = handle.session === void 0;
@@ -13024,7 +12629,6 @@ var InProcessExecutionBackend = class extends import_node_events2.EventEmitter {
13024
12629
  const details = {
13025
12630
  ...handle.completionReason === void 0 ? {} : { reason: handle.completionReason },
13026
12631
  ...refusedBeforeStart ? { refusedBeforeStart: true } : {},
13027
- ...neutralReason === void 0 ? {} : { neutralReason },
13028
12632
  output: [...handle.output]
13029
12633
  };
13030
12634
  this.emit("run-settled", handle.runId, handle.status, details);
@@ -13034,12 +12638,6 @@ var InProcessExecutionBackend = class extends import_node_events2.EventEmitter {
13034
12638
  cleanup.unref();
13035
12639
  }
13036
12640
  };
13037
- function dashboardNeutralReasonOf(error) {
13038
- if (typeof error !== "object" || error === null) return void 0;
13039
- const carried = error.neutralReason;
13040
- if (typeof carried !== "string" || carried.trim().length === 0) return void 0;
13041
- return carried;
13042
- }
13043
12641
  function createInProcessExecutionBackend(options) {
13044
12642
  return new InProcessExecutionBackend(options);
13045
12643
  }
@@ -13175,7 +12773,6 @@ function createLiveEditorSubjects() {
13175
12773
  renderLiveEmbedPage,
13176
12774
  resolveDashboardDbPath,
13177
12775
  resolveRunOwnerHost,
13178
- runDenialOf,
13179
12776
  runRowIdentity,
13180
12777
  serializeConfigWrite,
13181
12778
  servePostProcessingRead,