@vostride/agent-qa-dashboard 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -24,7 +24,6 @@ var MIGRATIONS = [
24
24
  llm_provider TEXT,
25
25
  parallel INTEGER DEFAULT 0,
26
26
  error_log TEXT,
27
- memory_log TEXT,
28
27
  test_id TEXT,
29
28
  suite_id TEXT,
30
29
  owner_pid INTEGER,
@@ -47,9 +46,7 @@ var MIGRATIONS = [
47
46
  variables TEXT,
48
47
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
49
48
  hook_id TEXT,
50
- -- 405 D-01: where in the run tree this hook ran. Sourced from HookFinishedPayload's own
51
- -- optional testIndex/stepIndex; NULL means a run-scoped hook, which is a real state and
52
- -- not a missing value.
49
+ -- Where in the run tree this hook ran; NULL means a run-scoped hook, not a missing value.
53
50
  test_index INTEGER,
54
51
  step_index INTEGER
55
52
  )`,
@@ -63,10 +60,7 @@ var MIGRATIONS = [
63
60
  )`,
64
61
  `CREATE TABLE IF NOT EXISTS run_artifacts (
65
62
  run_id TEXT PRIMARY KEY REFERENCES runs(id) ON DELETE CASCADE,
66
- -- 2026-08-27 (405-07, RESEARCH Open Q4): TWO KINDS, because two is all anything mints.
67
- -- The set used to admit two more, named for the parent/child run tree 402-09 retired; no
68
- -- writer has produced either since, so admitting them was the schema agreeing to store a
69
- -- distinction the product no longer makes.
63
+ -- Two kinds, because no writer mints any other.
70
64
  kind TEXT NOT NULL CHECK(kind IN ('test', 'unknown')),
71
65
  schema_version INTEGER NOT NULL,
72
66
  payload TEXT NOT NULL DEFAULT '{}',
@@ -74,17 +68,7 @@ var MIGRATIONS = [
74
68
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
75
69
  updated_at TEXT DEFAULT CURRENT_TIMESTAMP
76
70
  )`,
77
- // ---- The v2 bus-shaped projection (405 D-01) ----------------------------------------
78
- //
79
- // THE COLUMNS ARE THE PAYLOAD'S OWN NAMES, snake_cased and nothing else. No field is
80
- // re-derived, renamed or re-interpreted on the way in — that is D-01's whole claim, and
81
- // it is what makes the read a mirror rather than a second opinion. Nested payload objects
82
- // (`planned_action`, `grounding`, `result`, `artifact_refs`) are `JSON.stringify` of the
83
- // field VERBATIM; the projection never reaches inside one.
84
- //
85
- // `test_index` IS THE HANDLE (RESEARCH D4). `handleId(runId, testIndex)` is how this
86
- // product spells run-scoped member identity, so minting a second id here would be a
87
- // second spelling of the same fact.
71
+ // Columns are the payload's names snake_cased; `test_index` is the handle, so no second id.
88
72
  `CREATE TABLE IF NOT EXISTS run_members (
89
73
  run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
90
74
  test_index INTEGER NOT NULL,
@@ -99,15 +83,12 @@ var MIGRATIONS = [
99
83
  duration_ms INTEGER,
100
84
  failure_summary TEXT,
101
85
  retry_count INTEGER,
102
- -- D-17 ordinal. Starts at 1 and rises on testRetryScheduled; never reset by an upsert.
86
+ -- Starts at 1 and rises on testRetryScheduled; never reset by an upsert.
103
87
  attempts INTEGER NOT NULL DEFAULT 1,
104
88
  recording_json TEXT,
105
89
  PRIMARY KEY (run_id, test_index)
106
90
  )`,
107
- // The token columns are NULLABLE AND NEVER DEFAULT 0 (D-05/D-06). The retired v1 projection
108
- // declared these `INTEGER DEFAULT 0`, and that zero was a measurement nobody made — the exact
109
- // lie this shape refuses to be able to tell. 405-04 landed the producer, so a step the runner
110
- // measured carries real counts and a step it did not carries NULL, which stays honest.
91
+ // Token columns are nullable with no default, so a step nobody measured carries NULL, not 0.
111
92
  `CREATE TABLE IF NOT EXISTS run_steps (
112
93
  run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
113
94
  test_index INTEGER NOT NULL,
@@ -638,9 +619,7 @@ var DashboardDatabase = class {
638
619
  if (owner) this.ownStamps.set(id, owner);
639
620
  return id;
640
621
  }
641
- // 2026-10-17 (443-02, D-07): one statement that inserts a fresh row or adopts this handle's own
642
- // running row; any other row is refused and left untouched. An adopt that names no name or
643
- // attributes keeps the row's own (443-04).
622
+ // Inserts a fresh row or adopts this handle's own running row; any other row is left untouched.
644
623
  claimRunningRow(id, fields) {
645
624
  const stamp = this.ownStamps.get(id) ?? this.readOwnStamp();
646
625
  const result = this.db.prepare(`
@@ -709,10 +688,6 @@ var DashboardDatabase = class {
709
688
  fields.push("error_log = ?");
710
689
  values.push(updates.errorLog);
711
690
  }
712
- if (updates.memoryLog !== void 0) {
713
- fields.push("memory_log = ?");
714
- values.push(updates.memoryLog);
715
- }
716
691
  if (updates.attributes !== void 0) {
717
692
  fields.push("attributes = ?");
718
693
  values.push(JSON.stringify(updates.attributes));
@@ -815,26 +790,8 @@ var DashboardDatabase = class {
815
790
  this.ownStamps.set(id, stamp);
816
791
  return true;
817
792
  }
818
- // ---- The v2 bus-shaped projection (Phase 405 plan 01, D-01) ---------------------------------
819
- //
820
- // SIX MEMBERS, AND EVERY ONE OF THEM MIRRORS. The columns are the bus payload's own names; the
821
- // nested objects are `JSON.stringify` of the field verbatim. Nothing in this block decides what
822
- // a status means, re-derives a duration or fills a token count — a projection that interpreted
823
- // would be a second opinion about a run the event log already settled.
824
- /**
825
- * Write or refresh ONE member row, keyed by the handle `(run_id, test_index)`.
826
- *
827
- * AN UPSERT AND NOT TWO WRITES, because the member row is fed by two events at two times:
828
- * `testStarted` knows the identity fields and `testFinished` knows the outcome — and D-18
829
- * requires a member whose stream begins with a terminal event to render anyway, so the second
830
- * one alone has to be able to create the row.
831
- *
832
- * ONLY SUPPLIED FIELDS MOVE. `COALESCE(excluded.x, run_members.x)` on every optional column, so
833
- * a `testFinished` that carries no name cannot blank the name `testStarted` wrote. `attempts` is
834
- * excluded from the update entirely: it is the D-17 retry ordinal, owned by
835
- * `incrementRunMemberAttempts`, and an upsert that reset it would erase the fact the header
836
- * renders.
837
- */
793
+ // The projection mirrors bus payload names verbatim and interprets nothing.
794
+ // An upsert because a member's first event may be terminal; `COALESCE` keeps unsupplied fields.
838
795
  upsertRunMember(member) {
839
796
  this.db.prepare(`
840
797
  INSERT INTO run_members (
@@ -872,26 +829,13 @@ var DashboardDatabase = class {
872
829
  member.recording === void 0 ? null : JSON.stringify(member.recording)
873
830
  );
874
831
  }
875
- /**
876
- * Raise ONE member's attempt ordinal (D-17). A no-op when the member row does not exist yet —
877
- * a retry scheduled before any identity event is a real ordering, and inventing a row here
878
- * would put a member on the page that nothing has said anything about.
879
- */
832
+ // A no-op without a member row, so a retry before any identity event adds no member.
880
833
  incrementRunMemberAttempts(runId, testIndex) {
881
834
  this.db.prepare(
882
835
  "UPDATE run_members SET attempts = attempts + 1 WHERE run_id = ? AND test_index = ?"
883
836
  ).run(runId, testIndex);
884
837
  }
885
- /**
886
- * Drop ONE HANDLE's projected step and sub-action rows (D-17).
887
- *
888
- * SCOPED TO THE HANDLE, which was the whole change from the v1 run-wide step delete this
889
- * replaced (deleted outright by 405-07's census): that member wiped every step of the RUN on any
890
- * member's retry, so in a suite the abandoned attempt of test 3 deleted the completed history of
891
- * tests 0-2. The member row itself survives — its outcome and its attempt count are facts about
892
- * the member, not about the attempt. So do its setup and teardown hook rows, which run once per
893
- * member outside the retry loop; only the attempt's `inline` hook rows go (433-41, CR-01).
894
- */
838
+ // Scoped to the handle: the member row and its setup and teardown hooks survive a retry.
895
839
  deleteMemberProjection(runId, testIndex) {
896
840
  this.db.prepare("DELETE FROM run_steps WHERE run_id = ? AND test_index = ?").run(runId, testIndex);
897
841
  this.db.prepare("DELETE FROM run_sub_actions WHERE run_id = ? AND test_index = ?").run(runId, testIndex);
@@ -899,12 +843,7 @@ var DashboardDatabase = class {
899
843
  "DELETE FROM execution_logs WHERE run_id = ? AND test_index = ? AND phase = 'inline'"
900
844
  ).run(runId, testIndex);
901
845
  }
902
- /**
903
- * Write ONE finalized step row. `INSERT OR REPLACE` because a re-run of the same handle re-emits
904
- * the same `(test_index, step_index)` and the LAST attempt is the one the page renders (D-17).
905
- *
906
- * The three token columns stay NULL when `tokenUsage` is absent — never 0 (D-05).
907
- */
846
+ // `INSERT OR REPLACE`, so a re-run's last attempt wins; tokens stay NULL when absent, never 0.
908
847
  insertRunStep(step) {
909
848
  this.db.prepare(`
910
849
  INSERT OR REPLACE INTO run_steps (
@@ -964,14 +903,7 @@ var DashboardDatabase = class {
964
903
  subAction.recordingOffsetMs ?? null
965
904
  );
966
905
  }
967
- /**
968
- * THE run-detail read (D-03). `undefined` when no `runs` row exists — absence is a RESULT here,
969
- * and turning it into a typed refusal is the `runs.detail` op's job, so the "not found"
970
- * vocabulary lives in the function layer rather than in this store.
971
- *
972
- * A run that exists and projected nothing answers three empty arrays, which is a DIFFERENT fact
973
- * and stays a different answer.
974
- */
906
+ // `undefined` when no run row exists; a run that projected nothing answers three empty arrays.
975
907
  readRunDetailRows(runId) {
976
908
  const run = this.db.prepare("SELECT id FROM runs WHERE id = ?").get(runId);
977
909
  if (run === void 0) return void 0;
@@ -1111,7 +1043,7 @@ var DashboardDatabase = class {
1111
1043
  offset: opts.offset
1112
1044
  }));
1113
1045
  }
1114
- // 2026-10-17 (444-05, D-09): one batched member read per 400 returned rows, after pagination.
1046
+ // Members are read for the returned rows only, after pagination.
1115
1047
  withMemberSummaries(runs) {
1116
1048
  for (let offset = 0; offset < runs.length; offset += 400) {
1117
1049
  const batch = runs.slice(offset, offset + 400);
@@ -1154,7 +1086,7 @@ var DashboardDatabase = class {
1154
1086
  owner: { pid: row.owner_pid, incarnation: row.owner_incarnation, host: row.owner_host ?? "" }
1155
1087
  }));
1156
1088
  }
1157
- // 2026-10-16 (442-01, D-04): one compare-and-set on the stamped owner; never read-then-write.
1089
+ // One compare-and-set on the stamped owner; never read-then-write.
1158
1090
  finalizeOwnRun(id, updates) {
1159
1091
  const fields = ["status = ?", "ended_at = ?"];
1160
1092
  const values = [updates.status, updates.endedAt];
@@ -1176,8 +1108,7 @@ var DashboardDatabase = class {
1176
1108
  this.ownStamps.delete(id);
1177
1109
  return true;
1178
1110
  }
1179
- // 2026-10-17 (443-13, GAP-D1; 444-04): no status condition, because the verdict may already have
1180
- // moved the row off running; the owner columns alone decide whose row it is.
1111
+ // No status condition, as the verdict may have moved the row off running; owner columns decide.
1181
1112
  closeOwnRun(id, updates) {
1182
1113
  const stamp = this.ownStamps.get(id) ?? this.readOwnStamp();
1183
1114
  const result = this.db.prepare(`
@@ -1188,8 +1119,7 @@ var DashboardDatabase = class {
1188
1119
  this.ownStamps.delete(id);
1189
1120
  return true;
1190
1121
  }
1191
- // 2026-10-16 (442-15, GAP-D2): own is the stamp this handle wrote, so a NULL start time here is
1192
- // this handle's own write, never an inference about another process.
1122
+ // Own is the stamp this handle wrote, so a NULL start time here is this handle's own write.
1193
1123
  isOwnStamp(id, owner) {
1194
1124
  const stamp = this.ownStamps.get(id);
1195
1125
  return stamp !== void 0 && stamp.pid === owner.pid && stamp.incarnation === owner.incarnation && stamp.host === owner.host;
@@ -1288,20 +1218,7 @@ var DashboardDatabase = class {
1288
1218
  `).all(...params);
1289
1219
  return rows.map((row) => ({ value: row.value, count: row.count }));
1290
1220
  }
1291
- /**
1292
- * Delete ONE run and everything the schema hangs off it (405-07, D-02/D-14 · T-405-27).
1293
- *
1294
- * ONE ID, AND THE CASCADE DOES THE REST. This used to open with a recursive `run_tree` CTE that
1295
- * walked the child rows of a suite parent, then hand-delete their step and trace rows in a
1296
- * transaction. Nothing has written a child row since 402-09 — one `runs` row per run, member
1297
- * identity on `run_members` — so the walk could only ever return the one row it was seeded with,
1298
- * and the hand-deletes named tables this schema no longer has. Every table that references a run
1299
- * declares `ON DELETE CASCADE`, so the single statement below is the whole deletion.
1300
- *
1301
- * AND IT TOUCHES NO FILE. The two path arrays it used to collect fed the router's artifact
1302
- * cleanup; both are gone. The run's directory under `.agent-qa/runs/` — its event log and its
1303
- * content-addressed blobs — is the run's own record and is not the dashboard's to remove.
1304
- */
1221
+ // Every table referencing a run cascades on delete, and no file is touched.
1305
1222
  deleteRun(id) {
1306
1223
  const changes = this.db.prepare("DELETE FROM runs WHERE id = ?").run(id);
1307
1224
  const deleted = Number(changes.changes ?? 0) > 0;
@@ -1388,15 +1305,7 @@ var DashboardDatabase = class {
1388
1305
  getRunArtifactBundle(runId) {
1389
1306
  return { artifact: this.getRunArtifact(runId) };
1390
1307
  }
1391
- /**
1392
- * The runs a self-heal happened on (405-07, D-02).
1393
- *
1394
- * READ OFF `run_steps.self_heal_occurred`, WHICH IS THE SURVIVING HEALING FACT. This scanned the
1395
- * v1 wide step table for `status = 'healed'`; that table had no producer left and its readers
1396
- * went with it. `selfHealOccurred` is the bus payload's own boolean, projected verbatim by the
1397
- * subscriber — so the flake rate below is now computed from what the runner actually reported
1398
- * rather than from a status word a retired projection wrote.
1399
- */
1308
+ // Read off `run_steps.self_heal_occurred`, the bus payload's own boolean.
1400
1309
  getHealedRunIds(runIds) {
1401
1310
  if (runIds.length === 0) return /* @__PURE__ */ new Set();
1402
1311
  const placeholders = runIds.map(() => "?").join(", ");
@@ -1419,7 +1328,7 @@ var DashboardDatabase = class {
1419
1328
  passed,
1420
1329
  failed,
1421
1330
  flaky,
1422
- // 2026-10-18 (444-24, D-59): a group's rate is passed ÷ (passed + failed); neither is no rate.
1331
+ // A group's rate is passed ÷ (passed + failed); neither is no rate.
1423
1332
  passRate: passed + failed > 0 ? passed / (passed + failed) : null,
1424
1333
  flakeRate: totalRuns > 0 ? flaky / totalRuns : 0,
1425
1334
  avgDuration: totalRuns > 0 ? totalDuration / totalRuns : 0
@@ -1481,28 +1390,6 @@ var DashboardDatabase = class {
1481
1390
  FROM run_steps
1482
1391
  WHERE run_id IN (${runIds.map(() => "?").join(",")})
1483
1392
  `).get(...runIds).total_tokens || 0 : 0;
1484
- const memorySummary = {
1485
- runs: 0,
1486
- added: 0,
1487
- confirmed: 0,
1488
- deprecated: 0,
1489
- deleted: 0,
1490
- curatorTokens: 0
1491
- };
1492
- for (const run of runs) {
1493
- if (!run.memoryLog) continue;
1494
- memorySummary.runs++;
1495
- try {
1496
- const parsed = JSON.parse(run.memoryLog);
1497
- memorySummary.added += Number(parsed.added ?? 0) || 0;
1498
- memorySummary.confirmed += Number(parsed.confirmed ?? 0) || 0;
1499
- memorySummary.deprecated += Number(parsed.deprecated ?? 0) || 0;
1500
- memorySummary.deleted += Number(parsed.deleted ?? 0) || 0;
1501
- const tokenUsage = parsed.tokenUsage && typeof parsed.tokenUsage === "object" ? parsed.tokenUsage : {};
1502
- memorySummary.curatorTokens += Number(tokenUsage.totalTokens ?? 0) || 0;
1503
- } catch {
1504
- }
1505
- }
1506
1393
  return {
1507
1394
  totalRuns: summary.totalRuns,
1508
1395
  passed: summary.passed,
@@ -1511,8 +1398,7 @@ var DashboardDatabase = class {
1511
1398
  flakeRate: summary.flakeRate,
1512
1399
  avgDuration: summary.avgDuration,
1513
1400
  totalTokens,
1514
- runs: daily,
1515
- memory: memorySummary
1401
+ runs: daily
1516
1402
  };
1517
1403
  }
1518
1404
  getCostStats(opts = {}) {
@@ -1687,12 +1573,7 @@ var DashboardDatabase = class {
1687
1573
  testRunUnmeasuredSteps: unmeasuredSteps
1688
1574
  };
1689
1575
  }
1690
- /**
1691
- * The window's settled run ids, newest first, capped at `limit`, with their full count, and the
1692
- * count of the window's runs still in flight (433-30, 433-42). A settled run has started and is
1693
- * neither `pending` nor `running`; every other row in the window is in flight. Ids only: the
1694
- * Insights knowledge-change read dispatches each through the agent host.
1695
- */
1576
+ // Settled run ids newest first, capped at `limit`, with the full and in-flight counts.
1696
1577
  listInsightsWindowRunIds(opts) {
1697
1578
  const conditions = [];
1698
1579
  const params = [];
@@ -1848,7 +1729,7 @@ var DashboardDatabase = class {
1848
1729
  const limit = opts.limit ?? 25;
1849
1730
  return rows.slice(0, limit);
1850
1731
  }
1851
- // 2026-10-17 (444-05, D-35): grouped by member file path; only pass- and fail-class outcomes count.
1732
+ // Grouped by member file path; only pass- and fail-class outcomes count.
1852
1733
  getFlakyTests(opts) {
1853
1734
  const minRuns = opts?.minRuns ?? 3;
1854
1735
  const limit = opts?.limit ?? 50;
@@ -1973,7 +1854,6 @@ var DashboardDatabase = class {
1973
1854
  endedAt: r.ended_at,
1974
1855
  failureSummary: r.failure_summary ?? null,
1975
1856
  errorLog: r.error_log ?? null,
1976
- memoryLog: r.memory_log ?? null,
1977
1857
  testId: r.test_id ?? null,
1978
1858
  suiteId: r.suite_id ?? null,
1979
1859
  platform: r.platform ?? "web",
@@ -2301,11 +2181,7 @@ function createDashboardRunIngest(deps) {
2301
2181
  ...payload.status === void 0 ? {} : { status: payload.status },
2302
2182
  ...payload.durationMs === void 0 ? {} : { durationMs: payload.durationMs },
2303
2183
  ...payload.error === void 0 ? {} : { error: payload.error },
2304
- // 2026-08-27 (405 plan 04, D-05/D-06): the producer landed this plan, so the three token
2305
- // columns 405-01 declared stop being always-NULL. Spread-when-defined, like every other
2306
- // optional above it — the DB layer writes NULL for all three when the key is absent, and
2307
- // a NULL row is what makes `runs.detail` able to say "M of K steps unmeasured" instead of
2308
- // reporting a zero nobody measured.
2184
+ // Spread when defined, so an unmeasured step stays NULL and `runs.detail` counts it so.
2309
2185
  ...payload.tokenUsage === void 0 ? {} : { tokenUsage: payload.tokenUsage },
2310
2186
  ...payload.modelUsage === void 0 ? {} : { modelUsage: payload.modelUsage },
2311
2187
  ...payload.evidence === void 0 ? {} : { evidence: payload.evidence },
@@ -2355,8 +2231,7 @@ function createDashboardRunIngest(deps) {
2355
2231
  stdout: readString(payload.stdout) ?? null,
2356
2232
  stderr: readString(payload.stderr) ?? null,
2357
2233
  variables: payload.variables ?? null,
2358
- // 405 D-01: where in the run tree this hook ran. Absent stays NULL — a run-scoped hook
2359
- // is a real state, and inventing handle 0 for it would put a setup hook inside a test.
2234
+ // Absent stays NULL: a run-scoped hook is a real state, not handle 0.
2360
2235
  ...payload.testIndex === void 0 ? {} : { testIndex: payload.testIndex },
2361
2236
  ...payload.stepIndex === void 0 ? {} : { stepIndex: payload.stepIndex }
2362
2237
  });
@@ -2451,14 +2326,15 @@ function createDashboardRunIngest(deps) {
2451
2326
  // src/server/server.ts
2452
2327
  import { createServer } from "node:http";
2453
2328
  import { randomBytes } from "node:crypto";
2454
- import { readFile as readFile8, stat as stat7 } from "node:fs/promises";
2455
- import { join as join5, extname, resolve as resolve6 } from "node:path";
2329
+ import { readFile as readFile8, stat as stat6 } from "node:fs/promises";
2330
+ import { join as join4, extname, resolve as resolve6 } from "node:path";
2456
2331
  import { cpus as cpus2 } from "node:os";
2332
+ import { format as format3 } from "node:util";
2457
2333
 
2458
2334
  // src/server/routes.ts
2459
2335
  import { randomUUID as randomUUID7 } from "node:crypto";
2460
- import { readFile as readFile6, writeFile as writeFile4, stat as stat5, readdir, rm as rm5 } from "node:fs/promises";
2461
- import { join as join4, isAbsolute as isAbsolute2, basename as basename5, resolve as resolve5, dirname as dirname9 } from "node:path";
2336
+ import { readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
2337
+ import { isAbsolute as isAbsolute2, basename as basename5, resolve as resolve5, dirname as dirname9 } from "node:path";
2462
2338
 
2463
2339
  // src/tests/test-file-manager.ts
2464
2340
  import { readFile, mkdir, rm as rm2, stat as stat2 } from "node:fs/promises";
@@ -2631,7 +2507,7 @@ var TestFileManager = class {
2631
2507
  constructor(workspace) {
2632
2508
  this.workspace = workspace;
2633
2509
  }
2634
- // 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.
2510
+ // Each list call reads the workspace itself; each file is written whole, one writer at a time.
2635
2511
  underLock = createPathLock();
2636
2512
  async list() {
2637
2513
  const records = await discoverWorkspaceFiles({ workspace: this.workspace, kind: "test" });
@@ -2808,8 +2684,7 @@ var ConfigManager = class _ConfigManager {
2808
2684
  this.configPath = configPath;
2809
2685
  this.mask = mask;
2810
2686
  }
2811
- // 441 GAP3-D5: a whole document may keep a marker the file already holds, as often, but never
2812
- // add one. A missing file holds none.
2687
+ // A whole document may keep a marker the file already holds, as often, but never add one.
2813
2688
  static addsShownMarker(current, next) {
2814
2689
  const held = shownMarkerCounts(current);
2815
2690
  for (const [marker, count] of shownMarkerCounts(next)) {
@@ -2817,13 +2692,13 @@ var ConfigManager = class _ConfigManager {
2817
2692
  }
2818
2693
  return false;
2819
2694
  }
2820
- // 429-05 Pitfall 3: a half-edited file is named, never served as the partial config yaml recovers.
2695
+ // A half-edited file is named, never served as the partial config yaml recovers.
2821
2696
  async readWithRevision() {
2822
2697
  const read = await this.readParsed();
2823
2698
  if (read.kind !== "ok") return read;
2824
2699
  return { kind: "ok", config: read.config, raw: read.raw, revision: read.revision };
2825
2700
  }
2826
- // 441-21 D-29: the payload and the file view pass through the declared mask and nothing else.
2701
+ // The payload and the file view pass through the declared mask and nothing else.
2827
2702
  async readMasked() {
2828
2703
  const read = await this.readParsed();
2829
2704
  if (read.kind === "absent") {
@@ -2867,8 +2742,7 @@ var ConfigManager = class _ConfigManager {
2867
2742
  revision
2868
2743
  };
2869
2744
  }
2870
- // 429-01 D-19/D-22: one transaction per request, serialised so overlapping autosaves never
2871
- // read the same bytes and drop each other's field.
2745
+ // One transaction per request, serialised so overlapping autosaves never drop each other's field.
2872
2746
  async applyChanges(changes, options = {}) {
2873
2747
  const marked = changeWithShownMarker(changes);
2874
2748
  if (marked !== void 0) {
@@ -2883,13 +2757,11 @@ var ConfigManager = class _ConfigManager {
2883
2757
  }
2884
2758
  return this.serialized(() => this.applyChangesOnce(changes, options.capturePriors === true));
2885
2759
  }
2886
- // 429-11 D-24: an Undo is one more transaction on the same mutex, newest change first. A field
2887
- // that no longer holds what the change left there was written since, and is never overwritten.
2760
+ // An Undo restores newest first; a field written after the change is never overwritten.
2888
2761
  async restorePriors(priors) {
2889
2762
  return this.serialized(() => this.restorePriorsOnce(priors));
2890
2763
  }
2891
- // 432-07 D-05: a generated whole-file change (a scaffold, a hooks file) publishes only over the
2892
- // bytes it was built from, read again in this path's turn. Anything else is somebody's edit.
2764
+ // A generated whole-file change publishes only over the bytes it is built from, re-read in turn.
2893
2765
  async replaceDocument(expectedRevision, content) {
2894
2766
  return this.serialized(async () => {
2895
2767
  let current;
@@ -3016,8 +2888,7 @@ var ConfigManager = class _ConfigManager {
3016
2888
  await this.writeAtomic(next);
3017
2889
  return capturePriors ? { kind: "ok", revision: revisionOf(next), baseRevision, priors } : { kind: "ok", revision: revisionOf(next), baseRevision };
3018
2890
  }
3019
- // 429-05 Pitfall 8 / Pattern 1 step 6: the temp file sits beside the real file, keeps its mode
3020
- // (chmod, because writeFile's mode is filtered by the umask), and replaces it in one rename.
2891
+ // The temp file sits beside the real one and is chmodded, as the umask filters `writeFile` modes.
3021
2892
  async writeAtomic(content) {
3022
2893
  const target = await this.writeTarget();
3023
2894
  const directory = dirname5(target);
@@ -4221,6 +4092,12 @@ var HookRegistryManager = class {
4221
4092
 
4222
4093
  // src/server/body-parser.ts
4223
4094
  var DEFAULT_MAX_BYTES = 1024 * 1024;
4095
+ function carriesNonJsonBody(req) {
4096
+ const hasBody = req.headers["transfer-encoding"] !== void 0 || Number(req.headers["content-length"] ?? "0") > 0;
4097
+ if (!hasBody) return false;
4098
+ const type = req.headers["content-type"];
4099
+ return type === void 0 || type.split(";", 1)[0].trim().toLowerCase() !== "application/json";
4100
+ }
4224
4101
  function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
4225
4102
  return new Promise((resolve7, reject) => {
4226
4103
  const chunks = [];
@@ -4255,12 +4132,16 @@ function readJsonBody(req, maxBytes = DEFAULT_MAX_BYTES) {
4255
4132
  // src/live-editor/live-session.ts
4256
4133
  import { randomUUID as randomUUID6 } from "node:crypto";
4257
4134
  import { dirname as dirname8, resolve as resolvePath2 } from "node:path";
4135
+ import { format } from "node:util";
4258
4136
  import {
4259
4137
  AUTH_STATE_SCHEMA_VERSION,
4260
4138
  MobileSetupError as MobileSetupError2,
4261
4139
  PlannerConfigSchema,
4262
4140
  SuiteDefinitionSchema,
4263
4141
  TestDefinitionSchema,
4142
+ TimeoutConfigSchema,
4143
+ getAgentQaVersion,
4144
+ printRunText,
4264
4145
  readAuthStateMetadata,
4265
4146
  resolveAuthStatePaths,
4266
4147
  writeAuthStateFiles
@@ -4289,7 +4170,7 @@ import {
4289
4170
  resolveVisionGate,
4290
4171
  visionFromConfig
4291
4172
  } from "@vostride/agent-qa-model";
4292
- import { buildProductionRefusal, distinctInFirstOccurrenceOrder, resolvePlannerLimits } from "@vostride/agent-qa-schema";
4173
+ import { resolvePlannerLimits } from "@vostride/agent-qa-schema";
4293
4174
  import { parse as parseYaml5 } from "yaml";
4294
4175
 
4295
4176
  // src/llm-model-mapping.ts
@@ -4368,6 +4249,7 @@ import { dirname as dirname7, join as join3 } from "node:path";
4368
4249
  import { parse as parseYaml4 } from "yaml";
4369
4250
  import {
4370
4251
  MobileSetupError,
4252
+ refuseRetiredLocalBindings,
4371
4253
  resolveMobileRunConfig
4372
4254
  } from "@vostride/agent-qa-core";
4373
4255
  async function loadLocalBindings(configPath) {
@@ -4375,12 +4257,10 @@ async function loadLocalBindings(configPath) {
4375
4257
  try {
4376
4258
  const content = await readFile5(filePath, "utf-8");
4377
4259
  const parsed = parseYaml4(content);
4378
- return parsed && typeof parsed === "object" ? {
4379
- devices: parsed.devices,
4380
- providers: parsed.providers,
4381
- apps: parsed.apps,
4382
- filePath
4383
- } : { filePath };
4260
+ refuseRetiredLocalBindings(parsed, filePath);
4261
+ if (!parsed || typeof parsed !== "object") return { filePath };
4262
+ const bindings = parsed;
4263
+ return { devices: bindings.devices, apps: bindings.apps, filePath };
4384
4264
  } catch (err) {
4385
4265
  if (err.code === "ENOENT") return null;
4386
4266
  throw err;
@@ -4470,44 +4350,15 @@ async function prepareMobileLiveSession(input) {
4470
4350
  appActivity: resolved.app.appActivity,
4471
4351
  deepLinkAppId: resolved.app.deepLinkAppId,
4472
4352
  appPath: resolved.app.install?.path,
4473
- browserstackApp: resolved.app.install?.browserstack,
4474
4353
  appState: resolved.appState
4475
4354
  }
4476
4355
  };
4477
4356
  }
4478
4357
 
4479
4358
  // src/live-editor/live-session.ts
4480
- var LiveProductionRefusedError = class extends Error {
4481
- productionTargetNames;
4482
- constructor(productionTargetNames) {
4483
- super(buildProductionRefusal(productionTargetNames));
4484
- this.name = "LiveProductionRefusedError";
4485
- this.productionTargetNames = distinctInFirstOccurrenceOrder(productionTargetNames);
4486
- }
4487
- };
4488
4359
  function isConfigRecord(value) {
4489
4360
  return value !== null && typeof value === "object" && !Array.isArray(value);
4490
4361
  }
4491
- function liveTargetName(targetName) {
4492
- if (typeof targetName !== "string") return void 0;
4493
- const trimmed = targetName.trim();
4494
- return trimmed.length > 0 ? trimmed : void 0;
4495
- }
4496
- async function resolveLiveTargetProduction(configManager, targetName) {
4497
- const name = liveTargetName(targetName);
4498
- if (name === void 0 || configManager === void 0) {
4499
- return { productionTargetNames: [], allowProduction: false };
4500
- }
4501
- const config = await readConfigValues(configManager);
4502
- const registry = isConfigRecord(config.registry) ? config.registry : void 0;
4503
- const targets = registry !== void 0 && isConfigRecord(registry.targets) ? registry.targets : void 0;
4504
- const declared = targets !== void 0 && Object.hasOwn(targets, name) ? targets[name] : void 0;
4505
- const production = isConfigRecord(declared) && declared.environment === "production";
4506
- return {
4507
- productionTargetNames: production ? [name] : [],
4508
- allowProduction: config.allowProduction === true
4509
- };
4510
- }
4511
4362
  async function readEntityUse(entity, readers) {
4512
4363
  if (entity === void 0) return void 0;
4513
4364
  const find = entity.type === "suite" ? readers.findSuite : readers.findTest;
@@ -4527,6 +4378,12 @@ var LivePlannerLimitsRefusedError = class extends Error {
4527
4378
  this.name = "LivePlannerLimitsRefusedError";
4528
4379
  }
4529
4380
  };
4381
+ var LiveNavigationTimeoutRefusedError = class extends Error {
4382
+ constructor(message) {
4383
+ super(message);
4384
+ this.name = "LiveNavigationTimeoutRefusedError";
4385
+ }
4386
+ };
4530
4387
  var LIVE_LIMIT_LEAVES = ["maxActions", "maxPlanningTurns"];
4531
4388
  function shownLimitValue(value) {
4532
4389
  return typeof value === "number" ? String(value) : JSON.stringify(value) ?? String(value);
@@ -4575,9 +4432,30 @@ async function resolveLivePlannerLimits(configManager, entities, readers) {
4575
4432
  }
4576
4433
  return resolvePlannerLimits(config.use, ...uses);
4577
4434
  }
4578
- function liveProductionRefusal(found) {
4579
- if (found.productionTargetNames.length === 0 || found.allowProduction) return void 0;
4580
- return new LiveProductionRefusedError(found.productionTargetNames);
4435
+ async function resolveLiveNavigationTimeoutMs(configManager, entities, readers) {
4436
+ const config = configManager === void 0 ? {} : await readConfigValues(configManager);
4437
+ const layers = [
4438
+ { layer: "project config", use: config.use }
4439
+ ];
4440
+ for (const entity of [].concat(entities ?? [])) {
4441
+ const use = await readEntityUse(entity, readers);
4442
+ layers.push({ layer: `${entity.type} file ${entity.id}`, use });
4443
+ }
4444
+ let navigation;
4445
+ for (const { layer, use } of layers) {
4446
+ if (!isConfigRecord(use) || !isConfigRecord(use.timeout)) continue;
4447
+ if (!Object.hasOwn(use.timeout, "navigation")) continue;
4448
+ const value = use.timeout.navigation;
4449
+ const parsed = TimeoutConfigSchema.shape.navigation.safeParse(value);
4450
+ if (!parsed.success) {
4451
+ const issue = parsed.error.issues[0]?.message ?? "invalid value";
4452
+ throw new LiveNavigationTimeoutRefusedError(
4453
+ `${layer} sets use.timeout.navigation to ${shownLimitValue(value)}: ${issue}. The CLI refuses this file too.`
4454
+ );
4455
+ }
4456
+ navigation = parsed.data;
4457
+ }
4458
+ return navigation;
4581
4459
  }
4582
4460
  var IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
4583
4461
  var LIVE_HUB_CLOSE_LIMIT_MS = 2e3;
@@ -4713,8 +4591,7 @@ var LiveSession = class {
4713
4591
  planner = null;
4714
4592
  verifier = null;
4715
4593
  modelProfiles = null;
4716
- // D-14 (Phase 360): the run-scoped storage-state store, constructed lazily on first use and
4717
- // rooted at the session's `.agent-qa` runtime dir (the FsStorageStateStore rooting idiom).
4594
+ // Built lazily on first use, rooted at the session's `.agent-qa` runtime dir.
4718
4595
  storageStateStore = null;
4719
4596
  model = null;
4720
4597
  variableStore = null;
@@ -4742,15 +4619,14 @@ var LiveSession = class {
4742
4619
  mobileAppiumLease = null;
4743
4620
  secretStore;
4744
4621
  secretRedactor;
4745
- // 428 D-07: one hub per session, handle 0, created once the adapter exists. A session whose
4746
- // setup hooks failed has no adapter and therefore no hub; its subject declares `none`.
4622
+ // One hub per session at handle 0, created once the adapter exists; no adapter means no hub.
4747
4623
  liveHub = null;
4748
4624
  attachment = null;
4749
4625
  resolveEnded = () => {
4750
4626
  };
4751
4627
  ended;
4752
4628
  cleanupRun = null;
4753
- // RESEARCH P4: which control socket is the current one. Only its close may detach the session.
4629
+ // Which control socket is the current one; only its close may detach the session.
4754
4630
  attachmentGeneration = 0;
4755
4631
  releaseAttachment = null;
4756
4632
  get modelName() {
@@ -4772,11 +4648,7 @@ var LiveSession = class {
4772
4648
  hub.register(0, registration?.capability, registration?.fallback);
4773
4649
  this.liveHub = hub;
4774
4650
  }
4775
- /**
4776
- * Make a control socket the current one. `release` is how the session lets go of it later: the
4777
- * previous attachment's is called with `superseded` here (428 WR-01), so one socket drives the
4778
- * session at a time.
4779
- */
4651
+ // The previous attachment's `release` gets `superseded`, so one socket drives the session.
4780
4652
  beginAttachment(release) {
4781
4653
  const previous = this.releaseAttachment;
4782
4654
  this.attachmentGeneration += 1;
@@ -4795,11 +4667,7 @@ var LiveSession = class {
4795
4667
  }
4796
4668
  this.pendingMessages = [];
4797
4669
  }
4798
- /**
4799
- * A command's reply, for whichever control socket is attached now (428 CR-01). A command can
4800
- * outlive the socket that issued it, and its outcome belongs to the page, not to that socket:
4801
- * with none attached it is held and flushed to the next one.
4802
- */
4670
+ // A reply can outlive its socket, so it goes to the current socket or waits for the next one.
4803
4671
  deliver(message) {
4804
4672
  if (this.messageSink) {
4805
4673
  this.messageSink(message);
@@ -4910,7 +4778,7 @@ var LiveSession = class {
4910
4778
  continue;
4911
4779
  }
4912
4780
  const result = await runHooks([hookDef], {
4913
- executor: new DockerHookExecutor(),
4781
+ executor: new DockerHookExecutor({ version: getAgentQaVersion() }),
4914
4782
  env: this.getHookEnvVars(),
4915
4783
  secretStore: this.secretStore,
4916
4784
  secretRedactor: this.secretRedactor
@@ -4984,7 +4852,6 @@ var LiveSession = class {
4984
4852
  device: config.device ? {
4985
4853
  name: config.device.name,
4986
4854
  platform,
4987
- transport: "local",
4988
4855
  match: config.device
4989
4856
  } : void 0
4990
4857
  };
@@ -5049,15 +4916,6 @@ var LiveSession = class {
5049
4916
  }
5050
4917
  }
5051
4918
  async initialize(config) {
5052
- const found = await resolveLiveTargetProduction(this.deps.configManager, config.targetName);
5053
- const refusal = liveProductionRefusal({
5054
- productionTargetNames: distinctInFirstOccurrenceOrder([
5055
- ...config.production?.productionTargetNames ?? [],
5056
- ...found.productionTargetNames
5057
- ]),
5058
- allowProduction: found.allowProduction
5059
- });
5060
- if (refusal !== void 0) throw refusal;
5061
4919
  this.platform = config.platform;
5062
4920
  this.targetName = typeof config.targetName === "string" && config.targetName.trim().length > 0 ? config.targetName.trim() : null;
5063
4921
  this._modelName = config.llmConfig.model ?? "unknown";
@@ -5092,7 +4950,11 @@ var LiveSession = class {
5092
4950
  } else {
5093
4951
  const { WebPlatformAdapter } = await import("@vostride/agent-qa-web");
5094
4952
  const adapter = new WebPlatformAdapter();
5095
- await adapter.setup({ platform: "web", browser: { name: "chromium", headless: config.headless ?? true } });
4953
+ await adapter.setup({
4954
+ platform: "web",
4955
+ browser: { name: "chromium", headless: config.headless ?? true },
4956
+ ...config.navigationTimeoutMs === void 0 ? {} : { timeouts: { navigation: config.navigationTimeoutMs } }
4957
+ });
5096
4958
  this.adapter = adapter;
5097
4959
  if (config.url) {
5098
4960
  const resolvedUrl = this.resolveSecrets(config.url);
@@ -5154,7 +5016,7 @@ var LiveSession = class {
5154
5016
  }
5155
5017
  const startedAt = performance.now();
5156
5018
  try {
5157
- const result = await new DockerHookExecutor().execute(hookDef, {
5019
+ const result = await new DockerHookExecutor({ version: getAgentQaVersion() }).execute(hookDef, {
5158
5020
  env: this.getHookEnvVars(),
5159
5021
  secretEnv: resolveSecretEnv(this.secretStore),
5160
5022
  secretRedactor: this.secretRedactor
@@ -5321,12 +5183,7 @@ var LiveSession = class {
5321
5183
  };
5322
5184
  return this.redact(result);
5323
5185
  }
5324
- // Inner step execution — no gating. Callers must own the executing/status
5325
- // lifecycle (executeStepCommand for single steps, executeTestCommand for
5326
- // the per-test loop that runs many steps under one outer gate).
5327
- // D-14 (Phase 360): lazily construct the run-scoped storage-state store, rooted at the session's
5328
- // `.agent-qa` runtime dir. All fs writes live in runner-local (validate:arch storageNeutral);
5329
- // the loop records only { name, fingerprint }. Absent configPath falls back to the cwd root.
5186
+ // Lazily built and rooted at the session's `.agent-qa` dir; fs writes stay in runner-local.
5330
5187
  async getStorageStateStore() {
5331
5188
  let store = this.storageStateStore;
5332
5189
  if (!store) {
@@ -5465,7 +5322,9 @@ ${errorLines.join("\n")}`;
5465
5322
  testLimits = this.plannerLimitsForTest !== void 0 && testDraft.testId.length > 0 ? await this.plannerLimitsForTest(testDraft.testId) : this.plannerLimits ?? resolvePlannerLimits();
5466
5323
  } catch (error) {
5467
5324
  if (!(error instanceof LivePlannerLimitsRefusedError)) throw error;
5468
- console.warn(`[live-session] executeTestCommand: ${error.message}`, { testName: testDraft.testName });
5325
+ const line = `[live-session] executeTestCommand: ${error.message}`;
5326
+ const detail = { testName: testDraft.testName };
5327
+ printRunText(format(line, detail), () => console.warn(line, detail));
5469
5328
  return this.redact({
5470
5329
  status: "failed",
5471
5330
  duration: performance.now() - start,
@@ -5485,11 +5344,13 @@ ${errorLines.join("\n")}`;
5485
5344
  if (!setupResult.allPassed) {
5486
5345
  const failedHook = setupHookPayloads.find((h) => h.status === "failed");
5487
5346
  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";
5488
- console.warn(`[live-session] executeTestCommand: ${detail}`, {
5347
+ const line = `[live-session] executeTestCommand: ${detail}`;
5348
+ const context = {
5489
5349
  testName: testDraft.testName,
5490
5350
  setupHooks: testDraft.setup,
5491
5351
  failedHook: failedHook ? { name: failedHook.hookName, stderr: failedHook.stderr, error: failedHook.error } : null
5492
- });
5352
+ };
5353
+ printRunText(format(line, context), () => console.warn(line, context));
5493
5354
  return {
5494
5355
  status: "failed",
5495
5356
  duration: performance.now() - start,
@@ -5567,7 +5428,9 @@ ${errorLines.join("\n")}`;
5567
5428
  topLevelError = failedHook ? `Teardown hook "${failedHook.hookName}" failed${failedHook.error ? `: ${failedHook.error}` : ""}` : "Teardown hook failed";
5568
5429
  }
5569
5430
  if (topLevelError) {
5570
- console.warn(`[live-session] executeTestCommand: ${topLevelError}`, { testName: testDraft.testName });
5431
+ const line = `[live-session] executeTestCommand: ${topLevelError}`;
5432
+ const detail = { testName: testDraft.testName };
5433
+ printRunText(format(line, detail), () => console.warn(line, detail));
5571
5434
  }
5572
5435
  return this.redact({
5573
5436
  status: finalStatus,
@@ -5583,10 +5446,7 @@ ${errorLines.join("\n")}`;
5583
5446
  this.status = "idle";
5584
5447
  }
5585
5448
  }
5586
- // Thin wrapper around runLifecycleHooks that also captures the dispatched
5587
- // hook payloads (hook-complete) for inclusion in the LiveTestResultPayload.
5588
- // The capture sits beside the message sink rather than wrapping it, so a
5589
- // control socket that reattaches mid-hook keeps its sink (428 CR-01).
5449
+ // Captures dispatched hook payloads beside the message sink, so a reattached socket keeps it.
5590
5450
  async runLifecycleHooksCapturing(phase, hookIds, sink, owner = { scope: "suite" }) {
5591
5451
  this.hookCaptures.add(sink);
5592
5452
  try {
@@ -5830,6 +5690,21 @@ function refuseUnparseableRequest(res) {
5830
5690
  });
5831
5691
  res.end(body);
5832
5692
  }
5693
+ function refuseForeignHostPage(res, port) {
5694
+ if (res.headersSent) {
5695
+ res.destroy();
5696
+ return;
5697
+ }
5698
+ const p = String(port);
5699
+ const body = `The agent-qa dashboard answers only at localhost, 127.0.0.1 or [::1].
5700
+ Open http://localhost:${p}/ on the machine it runs on, or forward the port with ssh -L ${p}:localhost:${p} <host>.
5701
+ `;
5702
+ res.writeHead(403, {
5703
+ "Content-Type": "text/plain; charset=utf-8",
5704
+ "Content-Length": Buffer.byteLength(body)
5705
+ });
5706
+ res.end(body);
5707
+ }
5833
5708
  function backstopServerError(res, req, error) {
5834
5709
  const kind = error instanceof Error ? error.constructor.name : typeof error;
5835
5710
  console.error(
@@ -5935,10 +5810,6 @@ function respondJson(res, status, data) {
5935
5810
  res.end(body);
5936
5811
  }
5937
5812
  function serveRunEvidenceRead(req, res, deps) {
5938
- if (deps.boundary === "human" && refuseCrossSiteRequest(req)) {
5939
- respondJson(res, 403, { error: "cross_site_refused" });
5940
- return;
5941
- }
5942
5813
  const query = new URL(req.url ?? "/", "http://localhost").searchParams;
5943
5814
  const entries = [["runId", deps.runId]];
5944
5815
  const seen = /* @__PURE__ */ new Set(["runId"]);
@@ -5980,10 +5851,6 @@ function respondFeedbackRead(res, deps, functionId, input, parse) {
5980
5851
  });
5981
5852
  }
5982
5853
  function serveRunMarksHistoryRead(req, res, deps) {
5983
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
5984
- respondJson(res, 403, { error: "cross_site_refused" });
5985
- return;
5986
- }
5987
5854
  if (new URL(req.url ?? "/", "http://localhost").search !== "") {
5988
5855
  respondJson(res, 400, { error: "invalid_marks_input" });
5989
5856
  return;
@@ -6002,10 +5869,6 @@ function serveRunMarksHistoryRead(req, res, deps) {
6002
5869
  );
6003
5870
  }
6004
5871
  function serveRunMarkPreviewRead(req, res, deps) {
6005
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
6006
- respondJson(res, 403, { error: "cross_site_refused" });
6007
- return;
6008
- }
6009
5872
  void readJsonBody(req).then((body) => {
6010
5873
  const parsed = RunsMarkInputSchema.safeParse(body);
6011
5874
  if (!parsed.success) {
@@ -6028,10 +5891,6 @@ function serveRunMarkPreviewRead(req, res, deps) {
6028
5891
  });
6029
5892
  }
6030
5893
  function serveRunDetailRead(req, res, deps) {
6031
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
6032
- respondJson(res, 403, { error: "cross_site_refused" });
6033
- return;
6034
- }
6035
5894
  void readRunDetail(deps.host, deps.runId).then(
6036
5895
  (outcome) => {
6037
5896
  if (outcome.kind === "refused") {
@@ -6183,10 +6042,6 @@ async function readRunAnalysis(host, runId) {
6183
6042
  }
6184
6043
  }
6185
6044
  function serveRunAnalysisRead(req, res, deps) {
6186
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
6187
- respondJson(res, 403, { error: "cross_site_refused" });
6188
- return;
6189
- }
6190
6045
  void readRunAnalysis(deps.host, deps.runId).then(
6191
6046
  (outcome) => {
6192
6047
  if (outcome.kind === "refused") {
@@ -6410,10 +6265,7 @@ function serveRunEventStream(req, res, deps) {
6410
6265
  else pending.push(frame);
6411
6266
  };
6412
6267
  const frameOf = (event) => (
6413
- // D-13, all three parts: the id IS the seq, the event name IS the envelope type, and the
6414
- // data IS the envelope's own JSON. No projection, no rename, no second vocabulary. The filter
6415
- // DROPS envelopes; it never edits one, so a filtered stream's frames are byte-identical to the
6416
- // firehose's — with gaps in the ids, which is what seq-as-id means once a filter exists.
6268
+ // The filter drops envelopes and never edits one, so filtered frames match the firehose bytes.
6417
6269
  `id: ${String(event.seq)}
6418
6270
  event: ${event.type}
6419
6271
  data: ${JSON.stringify(event)}
@@ -6475,12 +6327,6 @@ data: ${JSON.stringify(event)}
6475
6327
  deps.openStreams?.add(teardown);
6476
6328
  req.on("close", teardown);
6477
6329
  }
6478
- function runDenialOf(error) {
6479
- if (typeof error !== "object" || error === null) return void 0;
6480
- const neutralReason = error.neutralReason;
6481
- if (typeof neutralReason !== "string" || neutralReason.trim().length === 0) return void 0;
6482
- return { error: "run_denied", neutralReason };
6483
- }
6484
6330
  function createRunServiceRoutes(ports) {
6485
6331
  const openStreams = /* @__PURE__ */ new Set();
6486
6332
  function handleStart(req, res) {
@@ -6511,12 +6357,7 @@ function createRunServiceRoutes(ports) {
6511
6357
  ack = await ports.invokeRunStart(
6512
6358
  parsed.data.patterns === void 0 ? {} : { patterns: parsed.data.patterns }
6513
6359
  );
6514
- } catch (error) {
6515
- const denial = runDenialOf(error);
6516
- if (denial !== void 0) {
6517
- respondJson2(res, 403, denial);
6518
- return;
6519
- }
6360
+ } catch {
6520
6361
  refuse(res, 500, "run_start_failed");
6521
6362
  return;
6522
6363
  }
@@ -6596,6 +6437,10 @@ function createRunServiceRoutes(ports) {
6596
6437
  refuse(res, 403, "cross_site_refused");
6597
6438
  return;
6598
6439
  }
6440
+ if (carriesNonJsonBody(req)) {
6441
+ refuse(res, 415, "unsupported_media_type");
6442
+ return;
6443
+ }
6599
6444
  const matched = matchRunServiceRoute(req.method, path);
6600
6445
  if (matched === void 0) {
6601
6446
  refuse(res, 404, "unknown_route");
@@ -6863,8 +6708,7 @@ data: ${JSON.stringify(again.body)}
6863
6708
  // A failed re-read is not a reason to close a live channel: the next envelope will trigger
6864
6709
  // another, and the page's own poll is the floor underneath both.
6865
6710
  () => void 0
6866
- // …but a throw from the arm ABOVE is not a failed re-read, it is a dead socket, and `.then`'s
6867
- // failure arm never sees its own fulfilment arm's throw (UF-01, on the nested chain).
6711
+ // A throw from the arm above is a dead socket, and `.then`'s failure arm never sees it.
6868
6712
  ).catch(failStream);
6869
6713
  });
6870
6714
  heartbeat = setInterval(() => {
@@ -6903,10 +6747,6 @@ async function readRunKnowledge(host, runId) {
6903
6747
  }
6904
6748
  }
6905
6749
  function serveRunKnowledgeRead(req, res, deps) {
6906
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
6907
- respondJson(res, 403, { error: "cross_site_refused" });
6908
- return;
6909
- }
6910
6750
  void readRunKnowledge(deps.host, deps.runId).then(
6911
6751
  (outcome) => {
6912
6752
  if (outcome.kind === "refused") {
@@ -6949,10 +6789,6 @@ async function markRun(host, runId, body) {
6949
6789
  }
6950
6790
  }
6951
6791
  function registerRunMarkRoute(req, res, deps) {
6952
- if (deps.boundary === "human" && refuseCrossSiteRequest(req) !== void 0) {
6953
- respondJson(res, 403, { error: "cross_site_refused" });
6954
- return;
6955
- }
6956
6792
  void readJsonBody(req).then(async (body) => {
6957
6793
  const outcome = await markRun(deps.host, deps.runId, body);
6958
6794
  if (outcome.kind === "refused") {
@@ -6975,10 +6811,6 @@ import {
6975
6811
  isPageWritableConfigPath
6976
6812
  } from "@vostride/agent-qa-schema";
6977
6813
  function registerConfigWriteRoute(req, res, deps) {
6978
- if (refuseCrossSiteRequest(req) !== void 0) {
6979
- respondJson(res, 403, { error: "cross_site_refused" });
6980
- return;
6981
- }
6982
6814
  void readJsonBody(req).then(async (body) => {
6983
6815
  const parsed = ConfigWriteRequestSchema.safeParse(body);
6984
6816
  if (!parsed.success) {
@@ -7036,10 +6868,6 @@ function registerConfigUndoRoute(req, res, deps) {
7036
6868
  respondJson(res, 405, { error: "method_not_allowed" });
7037
6869
  return;
7038
6870
  }
7039
- if (refuseCrossSiteRequest(req) !== void 0) {
7040
- respondJson(res, 403, { error: "cross_site_refused" });
7041
- return;
7042
- }
7043
6871
  void readJsonBody(req).then(async (body) => {
7044
6872
  const parsed = ConfigUndoRequestSchema.safeParse(body);
7045
6873
  if (!parsed.success) {
@@ -7282,10 +7110,6 @@ async function connectName(request2, deps) {
7282
7110
  });
7283
7111
  }
7284
7112
  function registerLlmConnectRoute(req, res, deps) {
7285
- if (refuseCrossSiteRequest(req) !== void 0) {
7286
- respondJson(res, 403, { error: "cross_site_refused" });
7287
- return;
7288
- }
7289
7113
  void readJsonBody(req).then(async (body) => {
7290
7114
  const answer = await connectLlm(body, deps);
7291
7115
  respondJson(res, answer.status, answer.body);
@@ -7297,9 +7121,9 @@ function registerLlmConnectRoute(req, res, deps) {
7297
7121
  }
7298
7122
 
7299
7123
  // src/server/routes.ts
7300
- import { AuthStateNameSchema, buildInternalRunAttributes, mergeRunAttributes, readAuth as readAuth2, writeAuth as writeAuth2, removeAuth as removeAuth2, getAgentQaVersion, getAgentQaUpdateStatus, getLLMAuthProviderPlugin as getLLMAuthProviderPlugin2, listAuthStateMetadata, listLLMAuthProviderPlugins, ModelConfigSchema as ModelConfigSchema2, NameSlugSchema as NameSlugSchema2, TimeoutConfigSchema, AgentQaConfigSchema as AgentQaConfigSchema3, TestDefinitionSchema as TestDefinitionSchema2, SuiteDefinitionSchema as SuiteDefinitionSchema2, parseEnvFile, serializeEnvFile, RUNTIME_IMAGE_MAP, validateUserRunAttributes, discoverWorkspaceFiles as discoverWorkspaceFiles3, isWorkspacePathMatch, resolveWorkspaceFileTarget as resolveWorkspaceFileTarget2, SizeString } from "@vostride/agent-qa-core";
7124
+ import { AuthStateNameSchema, buildInternalRunAttributes, DEFAULT_AGENT_QA_CACHE_DIR, mergeRunAttributes, readAuth as readAuth2, writeAuth as writeAuth2, removeAuth as removeAuth2, getAgentQaVersion as getAgentQaVersion2, getAgentQaUpdateStatus, getLLMAuthProviderPlugin as getLLMAuthProviderPlugin2, listAuthStateMetadata, listLLMAuthProviderPlugins, ModelConfigSchema as ModelConfigSchema2, NameSlugSchema as NameSlugSchema2, TimeoutConfigSchema as TimeoutConfigSchema2, AgentQaConfigSchema as AgentQaConfigSchema3, TestDefinitionSchema as TestDefinitionSchema2, SuiteDefinitionSchema as SuiteDefinitionSchema2, parseEnvFile, serializeEnvFile, hookRunnerImage, validateUserRunAttributes, discoverWorkspaceFiles as discoverWorkspaceFiles3, isWorkspacePathMatch, resolveWorkspaceFileTarget as resolveWorkspaceFileTarget2, SizeString } from "@vostride/agent-qa-core";
7301
7125
  import { SecretStore, SecretRedactor } from "@vostride/agent-qa-runner";
7302
- import { DockerHookExecutor as DockerHookExecutor2 } from "@vostride/agent-qa-runner-local";
7126
+ import { DockerHookExecutor as DockerHookExecutor2, purgeActionCache } from "@vostride/agent-qa-runner-local";
7303
7127
  import { createModel as createModel3, fetchLiveModels, getModelCatalog, listProviders, mergeCatalog, resolveVisionConfig } from "@vostride/agent-qa-model";
7304
7128
  import { parse as parseYaml6 } from "yaml";
7305
7129
  var LLM_PROVIDER_MODES = /* @__PURE__ */ new Set([
@@ -7607,25 +7431,10 @@ function llmConnectionTestTimeoutMs(config) {
7607
7431
  return REMOTE_LLM_CONNECTION_TEST_TIMEOUT_MS;
7608
7432
  }
7609
7433
  function json(res, data, status = 200) {
7610
- const body = JSON.stringify(data);
7611
- res.writeHead(status, {
7612
- "Content-Type": "application/json",
7613
- "Access-Control-Allow-Origin": "*",
7614
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
7615
- "Access-Control-Allow-Headers": "Content-Type"
7616
- });
7617
- res.end(body);
7618
- }
7619
- function jsonNoCors(res, data, status = 200) {
7620
7434
  const body = JSON.stringify(data);
7621
7435
  res.writeHead(status, { "Content-Type": "application/json" });
7622
7436
  res.end(body);
7623
7437
  }
7624
- function refuseCrossSiteDisplayRead(req, res) {
7625
- if (refuseCrossSiteRequest(req) === void 0) return false;
7626
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
7627
- return true;
7628
- }
7629
7438
  var INSIGHTS_KNOWLEDGE_RUN_CAP = 200;
7630
7439
  var INSIGHTS_KNOWLEDGE_CONCURRENCY = 4;
7631
7440
  async function isPostProcessingSkipped(host, runId) {
@@ -7689,14 +7498,6 @@ async function readInsightsKnowledgeChanges(host, db, window) {
7689
7498
  function notFound(res, message = "Not found") {
7690
7499
  json(res, { error: message }, 404);
7691
7500
  }
7692
- function cors(res) {
7693
- res.writeHead(204, {
7694
- "Access-Control-Allow-Origin": "*",
7695
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
7696
- "Access-Control-Allow-Headers": "Content-Type"
7697
- });
7698
- res.end();
7699
- }
7700
7501
  function isPlainRecord(value) {
7701
7502
  return value !== null && typeof value === "object" && !Array.isArray(value);
7702
7503
  }
@@ -7749,7 +7550,7 @@ function normalizeRunFilterValue(value) {
7749
7550
  function buildRunRequestAttributes(input) {
7750
7551
  const internal = buildInternalRunAttributes({
7751
7552
  trigger: input.trigger,
7752
- runner: input.runner ?? "local"
7553
+ runner: "local"
7753
7554
  });
7754
7555
  const user = validateUserRunAttributes(input.userAttributes, "run attributes");
7755
7556
  return mergeRunAttributes(internal, user);
@@ -7884,7 +7685,7 @@ function parseTimeoutConfigTestTimeoutMs(timeoutConfig) {
7884
7685
  return transformedTimeout;
7885
7686
  }
7886
7687
  }
7887
- const parsed = TimeoutConfigSchema.partial().safeParse(timeoutConfig);
7688
+ const parsed = TimeoutConfigSchema2.partial().safeParse(timeoutConfig);
7888
7689
  if (!parsed.success) return void 0;
7889
7690
  const testTimeout = parsed.data.test;
7890
7691
  return typeof testTimeout === "number" && Number.isFinite(testTimeout) && testTimeout > 0 ? testTimeout : void 0;
@@ -8194,9 +7995,7 @@ var RESPONSE_SAFE_RUN_METADATA_KEYS = /* @__PURE__ */ new Set([
8194
7995
  "timeoutSource",
8195
7996
  "timeoutBaseMs",
8196
7997
  "timeoutBufferMs",
8197
- "suiteIndex",
8198
- "farmSessionUrl",
8199
- "runDevice"
7998
+ "suiteIndex"
8200
7999
  ]);
8201
8000
  function sanitizeRunMetadataForResponse(metadata) {
8202
8001
  if (metadata === null || metadata === void 0) return metadata ?? null;
@@ -8226,17 +8025,7 @@ var RESPONSE_SAFE_RUN_READ_METHODS = [
8226
8025
  "getRunsByTest"
8227
8026
  ];
8228
8027
  var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
8229
- // 2026-08-27 (405-01, D-01/D-03): the v2 projection's six members.
8230
- //
8231
- // The five WRITES return nothing; they are here for the same reason `insertRun` is.
8232
- //
8233
- // `readRunDetailRows` IS A READ AND IS STILL HERE, and that is the judgement this set exists to
8234
- // make explicit rather than to hide. It returns members, steps and sub-actions — the bus
8235
- // payloads as the subscriber MASKED them (T-363-05) — and NO `RunRow` and no
8236
- // `RunArtifactRow`. It therefore carries none of the run metadata the 401-23 scrub law exists
8237
- // to strip, so wrapping it would be wrapping a shape that has nothing to strip. This router
8238
- // does not call it either: the detail arm dispatches through the injected host, which is the
8239
- // whole of D-03.
8028
+ // `readRunDetailRows` returns masked bus payloads and no run row, so it has no metadata to strip.
8240
8029
  "deleteMemberProjection",
8241
8030
  "incrementRunMemberAttempts",
8242
8031
  "insertRunStep",
@@ -8247,23 +8036,23 @@ var RUN_READS_WITHOUT_ROWS = /* @__PURE__ */ new Set([
8247
8036
  "finalizeRunArtifact",
8248
8037
  "getRunsBySuiteIdCount",
8249
8038
  "getRunsByTestCount",
8250
- // 433-30: run ids and a count, no row; each id is dispatched through the agent host.
8039
+ // Run ids and a count, no row; each id is dispatched through the agent host.
8251
8040
  "listInsightsWindowRunIds",
8252
8041
  "insertPendingRun",
8253
8042
  "insertRun",
8254
8043
  "insertRunArtifact",
8255
8044
  "listRunAttributeKeys",
8256
8045
  "listRunAttributeValues",
8257
- // 2026-10-16 (442-01, D-01): an owner (pid, incarnation, host), no run metadata.
8046
+ // An owner (pid, incarnation, host), no run metadata.
8258
8047
  "readRunOwner",
8259
- // 2026-10-16 (442-01, D-04): ids and owners only, no run metadata.
8048
+ // Ids and owners only, no run metadata.
8260
8049
  "listRunningRunOwners",
8261
- // 2026-10-16 (442-01, D-04): compare-and-set writes that return a boolean.
8050
+ // Compare-and-set writes that return a boolean.
8262
8051
  "finalizeOrphanRun",
8263
8052
  "finalizeOwnRun",
8264
- // 2026-10-17 (443-13, GAP-D1): a compare-and-set write that returns a boolean, no run metadata.
8053
+ // A compare-and-set write that returns a boolean, no run metadata.
8265
8054
  "closeOwnRun",
8266
- // 2026-10-17 (443-02, D-07): a compare-and-set write that returns a boolean, no run metadata.
8055
+ // A compare-and-set write that returns a boolean, no run metadata.
8267
8056
  "claimRunningRow",
8268
8057
  "stageRunArtifact",
8269
8058
  "updateRun",
@@ -8334,10 +8123,7 @@ function withResponseSafeRunReads(db) {
8334
8123
  // `sanitizeArtifactForResponse`; a caller reaching for the bare row has no such step, so the
8335
8124
  // projection is its only choke point. Same allowlist, so the two cannot drift.
8336
8125
  getRunArtifact: { value: (runId) => scrubArtifact(db.getRunArtifact(runId)) },
8337
- // 405-07 (D-02): the bundle is ONE artifact now. It used to carry the child runs of a suite
8338
- // parent, whose `run` rows this override scrubbed; nothing has written a child row since
8339
- // 402-09, and the shape went with the column. `getRunArtifact` above remains the choke point
8340
- // for the payload itself, and this wrapper delegates to it.
8126
+ // One artifact; `getRunArtifact` is the choke point for its payload, and this delegates to it.
8341
8127
  getRunArtifactBundle: {
8342
8128
  value: (runId) => ({ artifact: scrubArtifact(db.getRunArtifactBundle(runId).artifact) })
8343
8129
  }
@@ -8485,10 +8271,18 @@ function createRouter(dbOrDeps) {
8485
8271
  json(res, { error: "Bad Request" }, 400);
8486
8272
  return;
8487
8273
  }
8274
+ if (refuseCrossSiteRequest(req) !== void 0) {
8275
+ json(res, { error: "cross_site_refused" }, 403);
8276
+ return;
8277
+ }
8278
+ if (carriesNonJsonBody(req)) {
8279
+ json(res, { error: "unsupported_media_type" }, 415);
8280
+ return;
8281
+ }
8488
8282
  const path = url.pathname;
8489
8283
  if (path === "/api/config" && req.method === "PATCH") {
8490
8284
  if (!configManager) {
8491
- jsonNoCors(res, { error: "config_unavailable" }, 503);
8285
+ json(res, { error: "config_unavailable" }, 503);
8492
8286
  return;
8493
8287
  }
8494
8288
  registerConfigWriteRoute(req, res, { configManager, undoStore });
@@ -8508,7 +8302,7 @@ function createRouter(dbOrDeps) {
8508
8302
  }
8509
8303
  if (path === "/api/config/revision" && req.method === "GET") {
8510
8304
  if (!configManager) {
8511
- jsonNoCors(res, { error: "config_unavailable" }, 503);
8305
+ json(res, { error: "config_unavailable" }, 503);
8512
8306
  return;
8513
8307
  }
8514
8308
  registerConfigRevisionRoute(req, res, { configManager });
@@ -8516,7 +8310,7 @@ function createRouter(dbOrDeps) {
8516
8310
  }
8517
8311
  if (path === "/api/llm/connect" && req.method === "POST") {
8518
8312
  if (!configManager) {
8519
- jsonNoCors(res, { error: "config_unavailable" }, 503);
8313
+ json(res, { error: "config_unavailable" }, 503);
8520
8314
  return;
8521
8315
  }
8522
8316
  registerLlmConnectRoute(req, res, {
@@ -8528,16 +8322,6 @@ function createRouter(dbOrDeps) {
8528
8322
  });
8529
8323
  return;
8530
8324
  }
8531
- if (path === "/api/runs/trigger" || path === "/api/queue/enqueue") {
8532
- if (refuseCrossSiteRequest(req) !== void 0) {
8533
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
8534
- return;
8535
- }
8536
- }
8537
- if (req.method === "OPTIONS") {
8538
- cors(res);
8539
- return;
8540
- }
8541
8325
  if (path === "/api/auth-states" && req.method === "GET") {
8542
8326
  if (!configManager || !deps.configPath) {
8543
8327
  json(res, { error: "Auth-state metadata not available" }, 503);
@@ -8561,11 +8345,10 @@ function createRouter(dbOrDeps) {
8561
8345
  return;
8562
8346
  }
8563
8347
  if (path === "/api/runs" && req.method === "GET") {
8564
- if (refuseCrossSiteDisplayRead(req, res)) return;
8565
8348
  const statuses = url.searchParams.getAll("status");
8566
8349
  const unknownStatus = statuses.find((word) => !isRunStatusWord(word));
8567
8350
  if (unknownStatus !== void 0) {
8568
- jsonNoCors(res, { error: `Unknown run status: ${unknownStatus}.` }, 400);
8351
+ json(res, { error: `Unknown run status: ${unknownStatus}.` }, 400);
8569
8352
  return;
8570
8353
  }
8571
8354
  const name = url.searchParams.get("name") ?? void 0;
@@ -8597,12 +8380,12 @@ function createRouter(dbOrDeps) {
8597
8380
  )).sort((a, b) => a.localeCompare(b));
8598
8381
  const filtered = platformFilteredRuns.filter((run) => matchesTargetFilter(run, target));
8599
8382
  const runs = filtered.slice(offset, offset + limit);
8600
- jsonNoCors(res, {
8383
+ json(res, {
8601
8384
  runs,
8602
8385
  total: filtered.length,
8603
8386
  targets: targetOptions
8604
8387
  });
8605
- }).catch(() => jsonNoCors(res, { error: "Failed to list runs" }, 500));
8388
+ }).catch(() => json(res, { error: "Failed to list runs" }, 500));
8606
8389
  return;
8607
8390
  }
8608
8391
  if (path === "/api/runs/attributes/keys" && req.method === "GET") {
@@ -8640,7 +8423,6 @@ function createRouter(dbOrDeps) {
8640
8423
  }
8641
8424
  const runArtifactMatch = path.match(/^\/api\/runs\/([^/]+)\/artifact$/);
8642
8425
  if (runArtifactMatch && req.method === "GET") {
8643
- if (refuseCrossSiteDisplayRead(req, res)) return;
8644
8426
  const runId = decodeURIComponent(runArtifactMatch[1]);
8645
8427
  const run = db.getRun(runId);
8646
8428
  if (!run) {
@@ -8648,7 +8430,7 @@ function createRouter(dbOrDeps) {
8648
8430
  return;
8649
8431
  }
8650
8432
  const bundle = db.getRunArtifactBundle(runId);
8651
- jsonNoCors(res, {
8433
+ json(res, {
8652
8434
  run,
8653
8435
  artifact: sanitizeArtifactForResponse(bundle.artifact),
8654
8436
  missingSections: getArtifactMissingSections(bundle.artifact)
@@ -8656,7 +8438,6 @@ function createRouter(dbOrDeps) {
8656
8438
  return;
8657
8439
  }
8658
8440
  if (path.startsWith("/api/runs/") && path.endsWith("/execution-logs") && req.method === "GET") {
8659
- if (refuseCrossSiteDisplayRead(req, res)) return;
8660
8441
  const segments = path.split("/");
8661
8442
  const id = segments[3];
8662
8443
  const run = db.getRun(id);
@@ -8667,37 +8448,37 @@ function createRouter(dbOrDeps) {
8667
8448
  const stepId = url.searchParams.get("stepId") ?? void 0;
8668
8449
  const type = url.searchParams.get("type") ?? void 0;
8669
8450
  const logs = db.getExecutionLogs({ runId: id, stepId, type });
8670
- jsonNoCors(res, { logs });
8451
+ json(res, { logs });
8671
8452
  return;
8672
8453
  }
8673
8454
  if (path.startsWith("/api/runs/") && path.endsWith("/mark") && req.method === "POST") {
8674
8455
  if (deps.agentHost === void 0) {
8675
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
8456
+ json(res, { error: "agent_host_unavailable" }, 503);
8676
8457
  return;
8677
8458
  }
8678
8459
  let runId;
8679
8460
  try {
8680
8461
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark".length));
8681
8462
  } catch {
8682
- jsonNoCors(res, { error: "invalid_mark_input" }, 400);
8463
+ json(res, { error: "invalid_mark_input" }, 400);
8683
8464
  return;
8684
8465
  }
8685
- registerRunMarkRoute(req, res, { host: deps.agentHost, runId, boundary: "human" });
8466
+ registerRunMarkRoute(req, res, { host: deps.agentHost, runId });
8686
8467
  return;
8687
8468
  }
8688
8469
  if (req.method === "POST" && /^\/api\/runs\/[^/]+\/mark-preview$/.test(path)) {
8689
8470
  if (deps.agentHost === void 0) {
8690
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
8471
+ json(res, { error: "agent_host_unavailable" }, 503);
8691
8472
  return;
8692
8473
  }
8693
8474
  let runId;
8694
8475
  try {
8695
8476
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/mark-preview".length));
8696
8477
  } catch {
8697
- jsonNoCors(res, { error: "invalid_run_id" }, 400);
8478
+ json(res, { error: "invalid_run_id" }, 400);
8698
8479
  return;
8699
8480
  }
8700
- serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost, boundary: "human" });
8481
+ serveRunMarkPreviewRead(req, res, { runId, host: deps.agentHost });
8701
8482
  return;
8702
8483
  }
8703
8484
  if (path.startsWith("/api/runs/") && path.endsWith("/cancel") && req.method === "POST") {
@@ -8776,44 +8557,39 @@ function createRouter(dbOrDeps) {
8776
8557
  return;
8777
8558
  }
8778
8559
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/live-url$/.test(path)) {
8779
- const crossSite = refuseCrossSiteRequest(req);
8780
- if (crossSite !== void 0) {
8781
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
8782
- return;
8783
- }
8784
8560
  if (!deps.mintLiveUrl) {
8785
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
8561
+ json(res, { error: "live_view_unavailable" }, 503);
8786
8562
  return;
8787
8563
  }
8788
8564
  let liveUrlRunId;
8789
8565
  try {
8790
8566
  liveUrlRunId = decodeURIComponent(path.slice("/api/runs/".length, -"/live-url".length));
8791
8567
  } catch {
8792
- jsonNoCors(res, { error: "unknown_run" }, 404);
8568
+ json(res, { error: "unknown_run" }, 404);
8793
8569
  return;
8794
8570
  }
8795
8571
  const rawHandle = handleParamOf(req);
8796
8572
  const parsedHandle = parseHandleParam2(rawHandle);
8797
8573
  if (parsedHandle.kind === "invalid") {
8798
- jsonNoCors(res, { error: "invalid_handle" }, 400);
8574
+ json(res, { error: "invalid_handle" }, 400);
8799
8575
  return;
8800
8576
  }
8801
8577
  const handle = parsedHandle.kind === "absent" ? 0 : parsedHandle.index;
8802
8578
  void deps.mintLiveUrl(liveUrlRunId, handle).then(
8803
8579
  (result) => {
8804
- jsonNoCors(res, result);
8580
+ json(res, result);
8805
8581
  },
8806
8582
  (error) => {
8807
8583
  const name = error instanceof Error ? error.name : "";
8808
8584
  if (name === "UnknownRunError") {
8809
- jsonNoCors(res, { error: "unknown_run" }, 404);
8585
+ json(res, { error: "unknown_run" }, 404);
8810
8586
  return;
8811
8587
  }
8812
8588
  if (name === "LiveViewUnavailableError") {
8813
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
8589
+ json(res, { error: "live_view_unavailable" }, 503);
8814
8590
  return;
8815
8591
  }
8816
- jsonNoCors(res, { error: "live_url_mint_failed" }, 500);
8592
+ json(res, { error: "live_url_mint_failed" }, 500);
8817
8593
  }
8818
8594
  );
8819
8595
  return;
@@ -8842,32 +8618,32 @@ function createRouter(dbOrDeps) {
8842
8618
  }
8843
8619
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/evidence$/.test(path)) {
8844
8620
  if (!deps.agentHost) {
8845
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
8621
+ json(res, { error: "agent_host_unavailable" }, 503);
8846
8622
  return;
8847
8623
  }
8848
8624
  let runId;
8849
8625
  try {
8850
8626
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/evidence".length));
8851
8627
  } catch {
8852
- jsonNoCors(res, { error: "invalid_run_id" }, 400);
8628
+ json(res, { error: "invalid_run_id" }, 400);
8853
8629
  return;
8854
8630
  }
8855
- serveRunEvidenceRead(req, res, { runId, host: deps.agentHost, boundary: "human" });
8631
+ serveRunEvidenceRead(req, res, { runId, host: deps.agentHost });
8856
8632
  return;
8857
8633
  }
8858
8634
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/marks$/.test(path)) {
8859
8635
  if (!deps.agentHost) {
8860
- jsonNoCors(res, { error: "agent_host_unavailable" }, 503);
8636
+ json(res, { error: "agent_host_unavailable" }, 503);
8861
8637
  return;
8862
8638
  }
8863
8639
  let runId;
8864
8640
  try {
8865
8641
  runId = decodeURIComponent(path.slice("/api/runs/".length, -"/marks".length));
8866
8642
  } catch {
8867
- jsonNoCors(res, { error: "invalid_run_id" }, 400);
8643
+ json(res, { error: "invalid_run_id" }, 400);
8868
8644
  return;
8869
8645
  }
8870
- serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost, boundary: "human" });
8646
+ serveRunMarksHistoryRead(req, res, { runId, host: deps.agentHost });
8871
8647
  return;
8872
8648
  }
8873
8649
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/detail$/.test(path)) {
@@ -8882,7 +8658,7 @@ function createRouter(dbOrDeps) {
8882
8658
  json(res, { error: "invalid_run_id" }, 400);
8883
8659
  return;
8884
8660
  }
8885
- serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost, boundary: "human" });
8661
+ serveRunDetailRead(req, res, { runId: detailRunId, host: deps.agentHost });
8886
8662
  return;
8887
8663
  }
8888
8664
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/analysis$/.test(path)) {
@@ -8897,7 +8673,7 @@ function createRouter(dbOrDeps) {
8897
8673
  json(res, { error: "invalid_run_id" }, 400);
8898
8674
  return;
8899
8675
  }
8900
- serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost, boundary: "human" });
8676
+ serveRunAnalysisRead(req, res, { runId: analysisRunId, host: deps.agentHost });
8901
8677
  return;
8902
8678
  }
8903
8679
  if (req.method === "GET" && /^\/api\/runs\/[^/]+\/knowledge$/.test(path)) {
@@ -8914,8 +8690,7 @@ function createRouter(dbOrDeps) {
8914
8690
  }
8915
8691
  serveRunKnowledgeRead(req, res, {
8916
8692
  runId: knowledgeRunId,
8917
- host: deps.agentHost,
8918
- boundary: "human"
8693
+ host: deps.agentHost
8919
8694
  });
8920
8695
  return;
8921
8696
  }
@@ -8965,14 +8740,13 @@ function createRouter(dbOrDeps) {
8965
8740
  notFound(res);
8966
8741
  return;
8967
8742
  }
8968
- if (refuseCrossSiteDisplayRead(req, res)) return;
8969
8743
  const id = segments[3];
8970
8744
  const run = db.getRun(id);
8971
8745
  if (!run) {
8972
8746
  notFound(res, "Run not found");
8973
8747
  return;
8974
8748
  }
8975
- jsonNoCors(res, { run });
8749
+ json(res, { run });
8976
8750
  return;
8977
8751
  }
8978
8752
  if (path === "/api/stats/costs" && req.method === "GET") {
@@ -8983,19 +8757,18 @@ function createRouter(dbOrDeps) {
8983
8757
  return;
8984
8758
  }
8985
8759
  if (path === "/api/insights/knowledge-changes" && req.method === "GET") {
8986
- if (refuseCrossSiteDisplayRead(req, res)) return;
8987
8760
  if (!deps.agentHost) {
8988
- jsonNoCors(res, { status: "unavailable", reason: "agent_host_unavailable" }, 503);
8761
+ json(res, { status: "unavailable", reason: "agent_host_unavailable" }, 503);
8989
8762
  return;
8990
8763
  }
8991
8764
  const from = url.searchParams.get("from") ?? void 0;
8992
8765
  const to = url.searchParams.get("to") ?? void 0;
8993
8766
  void readInsightsKnowledgeChanges(deps.agentHost, db, { from, to }).then(
8994
8767
  (body) => {
8995
- jsonNoCors(res, body);
8768
+ json(res, body);
8996
8769
  },
8997
8770
  () => {
8998
- jsonNoCors(res, { status: "unavailable", reason: "read_failed" }, 500);
8771
+ json(res, { status: "unavailable", reason: "read_failed" }, 500);
8999
8772
  }
9000
8773
  );
9001
8774
  return;
@@ -9061,10 +8834,7 @@ function createRouter(dbOrDeps) {
9061
8834
  serveRunEventStream(req, res, {
9062
8835
  runId,
9063
8836
  handleParam: handleParamOf(req),
9064
- // 403-03 (D-13 · INVK-05): the resume point, read from the SAME pair the authed door reads
9065
- // it from. `url.searchParams.get('lastEventId')` would resolve a repeated parameter to its
9066
- // first value — the one answer `lastEventIdParamOf` exists to refuse — and a second
9067
- // header/query precedence rule here would be a second contract for where a client resumes.
8837
+ // Read from the same pair as the authed door; `searchParams.get` takes a repeated first.
9068
8838
  resume: resumePointOf(req.headers["last-event-id"], lastEventIdParamOf(req)),
9069
8839
  attach: deps.runServiceAttach
9070
8840
  });
@@ -9106,7 +8876,6 @@ function createRouter(dbOrDeps) {
9106
8876
  try {
9107
8877
  attributes = buildRunRequestAttributes({
9108
8878
  trigger: "api",
9109
- runner: "local",
9110
8879
  userAttributes: body.attributes
9111
8880
  });
9112
8881
  } catch (err) {
@@ -9114,25 +8883,10 @@ function createRouter(dbOrDeps) {
9114
8883
  return;
9115
8884
  }
9116
8885
  const args = normalizedFileTarget ? [normalizedFileTarget.executionPath] : [];
9117
- const source = isSuiteFile ? "suite" : "dashboard";
9118
- try {
9119
- await executionBackend?.preflight?.({ args, source });
9120
- } catch (error) {
9121
- const denial = runDenialOf(error);
9122
- if (denial !== void 0) {
9123
- json(res, denial, 403);
9124
- return;
9125
- }
9126
- json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
9127
- return;
9128
- }
9129
8886
  const runId = jobQueue.enqueue({
9130
8887
  name: body.name,
9131
8888
  filePath: normalizedFileTarget?.storagePath,
9132
- // 2026-08-24 (402-09, D-11): NO PARENT KIND IS MINTED. A suite run is ONE run row
9133
- // with member handles, so a parent artifact kind would claim parenthood over rows
9134
- // nothing creates. `isSuiteFile` still travels in `metadata.isSuite` below, and the run's
9135
- // own `suite_id` plus the artifact's `source.kind` snapshot still say it is a suite.
8889
+ // No parent kind is minted; `metadata.isSuite` and `suite_id` say it is a suite.
9136
8890
  kind: "test",
9137
8891
  attributes,
9138
8892
  priority: body.priority,
@@ -9215,17 +8969,6 @@ function createRouter(dbOrDeps) {
9215
8969
  args.push("--disable-cache");
9216
8970
  }
9217
8971
  const isSuite = Boolean(isSuiteFile);
9218
- try {
9219
- await executionBackend?.preflight?.({ args, source: isSuite ? "suite" : "dashboard" });
9220
- } catch (error) {
9221
- const denial = runDenialOf(error);
9222
- if (denial !== void 0) {
9223
- json(res, denial, 403);
9224
- return;
9225
- }
9226
- json(res, { error: error instanceof Error ? error.message : String(error) }, 400);
9227
- return;
9228
- }
9229
8972
  let testName = body.file ? basename5(normalizedFileTarget?.storagePath ?? body.file, ".yaml") : "unknown";
9230
8973
  let fileParallel;
9231
8974
  let platform;
@@ -9268,11 +9011,10 @@ function createRouter(dbOrDeps) {
9268
9011
  const runId = jobQueue.enqueue({
9269
9012
  name: testName,
9270
9013
  filePath: normalizedFileTarget?.storagePath ?? body.file,
9271
- // 2026-08-24 (402-09, D-11): no parent kind minted — see the enqueue route above.
9014
+ // No parent kind is minted; see the enqueue route above.
9272
9015
  kind: "test",
9273
9016
  attributes: buildRunRequestAttributes({
9274
- trigger: triggerSource.trigger,
9275
- runner: body.local === false ? "browserstack" : "local"
9017
+ trigger: triggerSource.trigger
9276
9018
  }),
9277
9019
  platform,
9278
9020
  parallel: effectiveParallel,
@@ -9781,7 +9523,7 @@ function createRouter(dbOrDeps) {
9781
9523
  return;
9782
9524
  }
9783
9525
  if (path === "/api/app-metadata" && req.method === "GET") {
9784
- const version = getAgentQaVersion().trim() || "0.0.0";
9526
+ const version = getAgentQaVersion2().trim() || "0.0.0";
9785
9527
  (async () => {
9786
9528
  try {
9787
9529
  const status = await getAgentQaUpdateStatus();
@@ -9825,9 +9567,8 @@ function createRouter(dbOrDeps) {
9825
9567
  return;
9826
9568
  }
9827
9569
  if (path === "/api/config" && req.method === "GET") {
9828
- if (refuseCrossSiteDisplayRead(req, res)) return;
9829
9570
  if (!configManager) {
9830
- jsonNoCors(res, { error: "Config management not available" }, 501);
9571
+ json(res, { error: "Config management not available" }, 501);
9831
9572
  return;
9832
9573
  }
9833
9574
  ;
@@ -9835,7 +9576,7 @@ function createRouter(dbOrDeps) {
9835
9576
  try {
9836
9577
  const read = await configManager.readMasked();
9837
9578
  if (read.kind === "file_invalid") {
9838
- jsonNoCors(res, { error: "file_invalid", message: read.message }, 409);
9579
+ json(res, { error: "file_invalid", message: read.message }, 409);
9839
9580
  return;
9840
9581
  }
9841
9582
  const { config, revision, lines, fileIssues } = read;
@@ -9845,7 +9586,7 @@ function createRouter(dbOrDeps) {
9845
9586
  const activeLlm = use?.llm ?? config.defaultLLM;
9846
9587
  const defaultCfg = Array.isArray(llms) ? llms.find((c) => c?.name === activeLlm) : void 0;
9847
9588
  const provider = defaultCfg?.provider ?? null;
9848
- jsonNoCors(res, {
9589
+ json(res, {
9849
9590
  config,
9850
9591
  provider,
9851
9592
  revision,
@@ -9853,45 +9594,17 @@ function createRouter(dbOrDeps) {
9853
9594
  ...fileIssues.length > 0 ? { fileIssues } : {}
9854
9595
  });
9855
9596
  } catch (err) {
9856
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to read config" }, 500);
9597
+ json(res, { error: err instanceof Error ? err.message : "Failed to read config" }, 500);
9857
9598
  }
9858
9599
  })();
9859
9600
  return;
9860
9601
  }
9861
- if (path === "/api/config/farm/test-connection" && req.method === "POST") {
9862
- readJsonBody(req).then(async (body) => {
9863
- if (body.provider !== "browserstack") {
9864
- json(res, { error: "Only BrowserStack is currently supported" }, 400);
9865
- return;
9866
- }
9867
- try {
9868
- const auth = Buffer.from(`${body.username}:${body.accessKey}`).toString("base64");
9869
- const response = await fetch("https://api-cloud.browserstack.com/app-automate/devices.json", {
9870
- headers: { Authorization: `Basic ${auth}` }
9871
- });
9872
- if (response.ok) {
9873
- json(res, { success: true });
9874
- } else if (response.status === 401) {
9875
- json(res, { success: false, error: "Invalid credentials" });
9876
- } else {
9877
- json(res, { success: false, error: `BrowserStack API returned ${response.status}` });
9878
- }
9879
- } catch (err) {
9880
- json(res, { success: false, error: err instanceof Error ? err.message : "Connection failed" });
9881
- }
9882
- }).catch(() => json(res, { error: "Invalid request body" }, 400));
9883
- return;
9884
- }
9885
9602
  if (path.startsWith("/api/auth/") && req.method === "DELETE") {
9886
9603
  const segments = path.split("/");
9887
9604
  if (segments.length === 4 && segments[2] === "auth") {
9888
9605
  const providerName = decodeURIComponent(segments[3]);
9889
9606
  if (providerName === "status" || providerName === "credential" || providerName === "oauth") {
9890
9607
  } else {
9891
- if (refuseCrossSiteRequest(req) !== void 0) {
9892
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
9893
- return;
9894
- }
9895
9608
  ;
9896
9609
  (async () => {
9897
9610
  try {
@@ -9902,13 +9615,13 @@ function createRouter(dbOrDeps) {
9902
9615
  return credential;
9903
9616
  });
9904
9617
  if (removed === void 0) {
9905
- jsonNoCors(res, { deleted: false });
9618
+ json(res, { deleted: false });
9906
9619
  return;
9907
9620
  }
9908
9621
  const undo = undoStore.put({ kind: "credential", name: providerName, credential: removed }, Date.now());
9909
- jsonNoCors(res, { deleted: true, undo });
9622
+ json(res, { deleted: true, undo });
9910
9623
  } catch (err) {
9911
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to delete credential" }, 500);
9624
+ json(res, { error: err instanceof Error ? err.message : "Failed to delete credential" }, 500);
9912
9625
  }
9913
9626
  })();
9914
9627
  return;
@@ -10018,27 +9731,21 @@ function createRouter(dbOrDeps) {
10018
9731
  const segments = path.split("/");
10019
9732
  const provider = segments.length >= 6 ? decodeURIComponent(segments[4] ?? "") : "";
10020
9733
  const action = segments.length >= 6 ? segments[5] : "";
10021
- const authDoor = req.method === "POST" && (action === "start" || action === "exchange");
10022
- if (authDoor && refuseCrossSiteRequest(req) !== void 0) {
10023
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
10024
- return;
10025
- }
10026
9734
  const plugin = provider ? getLLMAuthProviderPlugin2(provider) : void 0;
10027
9735
  if (!plugin) {
10028
- ;
10029
- (authDoor ? jsonNoCors : json)(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
9736
+ json(res, { error: `Auth plugin provider "${provider}" is not registered` }, 404);
10030
9737
  return;
10031
9738
  }
10032
9739
  if (action === "start" && req.method === "POST") {
10033
9740
  readJsonBody(req).then(async (body) => {
10034
9741
  if (!plugin.startAuth) {
10035
- jsonNoCors(res, { error: `Provider "${provider}" does not support dashboard auth start` }, 400);
9742
+ json(res, { error: `Provider "${provider}" does not support dashboard auth start` }, 400);
10036
9743
  return;
10037
9744
  }
10038
9745
  try {
10039
9746
  const target = await requirePluginAuthConfig(configManager, body.configName, plugin);
10040
9747
  if (!target.ok) {
10041
- jsonNoCors(res, credentialRefusalBody(target), target.status);
9748
+ json(res, credentialRefusalBody(target), target.status);
10042
9749
  return;
10043
9750
  }
10044
9751
  const started = await plugin.startAuth({
@@ -10074,16 +9781,16 @@ function createRouter(dbOrDeps) {
10074
9781
  session.error = err instanceof Error ? err.message : "Authentication failed";
10075
9782
  }).finally(() => cleanupPluginOAuthSession(session));
10076
9783
  }
10077
- jsonNoCors(res, {
9784
+ json(res, {
10078
9785
  authorizeUrl: started.authorizeUrl,
10079
9786
  sessionId,
10080
9787
  mode: plugin.dashboardAuth.mode,
10081
9788
  created: target.created
10082
9789
  });
10083
9790
  } catch (err) {
10084
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to start auth plugin flow" }, 500);
9791
+ json(res, { error: err instanceof Error ? err.message : "Failed to start auth plugin flow" }, 500);
10085
9792
  }
10086
- }).catch(() => jsonNoCors(res, { error: "Invalid request body" }, 400));
9793
+ }).catch(() => json(res, { error: "Invalid request body" }, 400));
10087
9794
  return;
10088
9795
  }
10089
9796
  if (action === "result" && req.method === "GET") {
@@ -10111,22 +9818,22 @@ function createRouter(dbOrDeps) {
10111
9818
  if (action === "exchange" && req.method === "POST") {
10112
9819
  readJsonBody(req).then(async (body) => {
10113
9820
  if (!plugin.exchangeCode) {
10114
- jsonNoCors(res, { error: `Provider "${provider}" does not support code exchange` }, 400);
9821
+ json(res, { error: `Provider "${provider}" does not support code exchange` }, 400);
10115
9822
  return;
10116
9823
  }
10117
9824
  const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
10118
9825
  const code = typeof body.code === "string" ? body.code.trim() : "";
10119
9826
  if (!sessionId || !code) {
10120
- jsonNoCors(res, { error: "sessionId and code are required" }, 400);
9827
+ json(res, { error: "sessionId and code are required" }, 400);
10121
9828
  return;
10122
9829
  }
10123
9830
  const session = pluginOAuthSessions.get(sessionId);
10124
9831
  if (!session || session.providerId !== provider) {
10125
- jsonNoCors(res, { error: "Auth session not found" }, 404);
9832
+ json(res, { error: "Auth session not found" }, 404);
10126
9833
  return;
10127
9834
  }
10128
9835
  if (session.status !== "pending") {
10129
- jsonNoCors(res, { error: "Auth session is no longer pending" }, 409);
9836
+ json(res, { error: "Auth session is no longer pending" }, 409);
10130
9837
  return;
10131
9838
  }
10132
9839
  try {
@@ -10148,61 +9855,57 @@ function createRouter(dbOrDeps) {
10148
9855
  session.error = CREDENTIAL_PROVIDER_CONFLICT.message;
10149
9856
  cleanupPluginOAuthSession(session);
10150
9857
  pluginOAuthSessions.delete(sessionId);
10151
- jsonNoCors(res, credentialRefusalBody(CREDENTIAL_PROVIDER_CONFLICT), CREDENTIAL_PROVIDER_CONFLICT.status);
9858
+ json(res, credentialRefusalBody(CREDENTIAL_PROVIDER_CONFLICT), CREDENTIAL_PROVIDER_CONFLICT.status);
10152
9859
  return;
10153
9860
  }
10154
9861
  session.status = "completed";
10155
9862
  cleanupPluginOAuthSession(session);
10156
9863
  pluginOAuthSessions.delete(sessionId);
10157
- jsonNoCors(res, { status: "completed", saved: true });
9864
+ json(res, { status: "completed", saved: true });
10158
9865
  } catch (err) {
10159
9866
  session.status = "error";
10160
9867
  session.error = err instanceof Error ? err.message : "Token exchange failed";
10161
9868
  cleanupPluginOAuthSession(session);
10162
9869
  pluginOAuthSessions.delete(sessionId);
10163
- jsonNoCors(res, { error: session.error }, 500);
9870
+ json(res, { error: session.error }, 500);
10164
9871
  }
10165
- }).catch(() => jsonNoCors(res, { error: "Invalid request body" }, 400));
9872
+ }).catch(() => json(res, { error: "Invalid request body" }, 400));
10166
9873
  return;
10167
9874
  }
10168
9875
  json(res, { error: "Auth plugin route not found" }, 404);
10169
9876
  return;
10170
9877
  }
10171
9878
  if (path === "/api/auth/credential" && req.method === "POST") {
10172
- if (refuseCrossSiteRequest(req) !== void 0) {
10173
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
10174
- return;
10175
- }
10176
9879
  readJsonBody(req).then(async (body) => {
10177
9880
  const provider = typeof body.provider === "string" ? body.provider.trim() : "";
10178
9881
  const credentialType = typeof body.type === "string" ? body.type.trim() : "";
10179
9882
  const secret = typeof body.secret === "string" ? body.secret.trim() : "";
10180
9883
  if (typeof body.configName !== "string" || body.configName.trim() === "") {
10181
- jsonNoCors(res, { error: "configName is required" }, 400);
9884
+ json(res, { error: "configName is required" }, 400);
10182
9885
  return;
10183
9886
  }
10184
9887
  if (!provider || !isKnownLLMProvider(provider)) {
10185
- jsonNoCors(res, { error: "valid provider is required" }, 400);
9888
+ json(res, { error: "valid provider is required" }, 400);
10186
9889
  return;
10187
9890
  }
10188
9891
  if (credentialType !== "api-key" && credentialType !== "bearer-token") {
10189
- jsonNoCors(res, { error: "type must be api-key or bearer-token" }, 400);
9892
+ json(res, { error: "type must be api-key or bearer-token" }, 400);
10190
9893
  return;
10191
9894
  }
10192
9895
  if (!secret) {
10193
- jsonNoCors(res, { error: "secret is required" }, 400);
9896
+ json(res, { error: "secret is required" }, 400);
10194
9897
  return;
10195
9898
  }
10196
9899
  if (getLLMAuthProviderPlugin2(provider)) {
10197
- jsonNoCors(res, { error: "Subscription providers use OAuth login" }, 400);
9900
+ json(res, { error: "Subscription providers use OAuth login" }, 400);
10198
9901
  return;
10199
9902
  }
10200
9903
  if (credentialType === "bearer-token" && provider !== "anthropic-compatible") {
10201
- jsonNoCors(res, { error: "bearer-token credentials are only supported for anthropic-compatible configs" }, 400);
9904
+ json(res, { error: "bearer-token credentials are only supported for anthropic-compatible configs" }, 400);
10202
9905
  return;
10203
9906
  }
10204
9907
  if (credentialType === "api-key" && !API_KEY_CREDENTIAL_PROVIDERS.has(provider)) {
10205
- jsonNoCors(res, { error: "api-key credentials are not supported for this provider" }, 400);
9908
+ json(res, { error: "api-key credentials are not supported for this provider" }, 400);
10206
9909
  return;
10207
9910
  }
10208
9911
  try {
@@ -10217,35 +9920,31 @@ function createRouter(dbOrDeps) {
10217
9920
  return checked;
10218
9921
  });
10219
9922
  if (!target.ok) {
10220
- jsonNoCors(res, credentialRefusalBody(target), target.status);
9923
+ json(res, credentialRefusalBody(target), target.status);
10221
9924
  return;
10222
9925
  }
10223
- jsonNoCors(res, { saved: true, created: target.created });
9926
+ json(res, { saved: true, created: target.created });
10224
9927
  } catch (err) {
10225
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to save credential" }, 500);
9928
+ json(res, { error: err instanceof Error ? err.message : "Failed to save credential" }, 500);
10226
9929
  }
10227
- }).catch(() => jsonNoCors(res, { error: "Invalid request body" }, 400));
9930
+ }).catch(() => json(res, { error: "Invalid request body" }, 400));
10228
9931
  return;
10229
9932
  }
10230
9933
  if (path === "/api/auth/credential/move" && req.method === "POST") {
10231
- if (refuseCrossSiteRequest(req) !== void 0) {
10232
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
10233
- return;
10234
- }
10235
9934
  readJsonBody(req).then(async (raw) => {
10236
9935
  const parsed = LlmCredentialMoveRequestSchema.safeParse(raw);
10237
9936
  if (!parsed.success) {
10238
- jsonNoCors(res, { error: "invalid_setup_input" }, 400);
9937
+ json(res, { error: "invalid_setup_input" }, 400);
10239
9938
  return;
10240
9939
  }
10241
9940
  const from = checkCredentialName(parsed.data.from);
10242
9941
  const to = checkCredentialName(parsed.data.to);
10243
9942
  if (!from.ok || !to.ok) {
10244
- jsonNoCors(res, { error: "name_invalid" }, 400);
9943
+ json(res, { error: "name_invalid" }, 400);
10245
9944
  return;
10246
9945
  }
10247
9946
  if (!configManager) {
10248
- jsonNoCors(res, { error: "Config manager is required to move credentials" }, 400);
9947
+ json(res, { error: "Config manager is required to move credentials" }, 400);
10249
9948
  return;
10250
9949
  }
10251
9950
  try {
@@ -10263,25 +9962,21 @@ function createRouter(dbOrDeps) {
10263
9962
  return void 0;
10264
9963
  });
10265
9964
  if (refusal) {
10266
- jsonNoCors(res, credentialRefusalBody(refusal), refusal.status);
9965
+ json(res, credentialRefusalBody(refusal), refusal.status);
10267
9966
  return;
10268
9967
  }
10269
- jsonNoCors(res, { moved: true });
9968
+ json(res, { moved: true });
10270
9969
  } catch (err) {
10271
- jsonNoCors(res, { error: err instanceof Error ? err.message : "Failed to move credential" }, 500);
9970
+ json(res, { error: err instanceof Error ? err.message : "Failed to move credential" }, 500);
10272
9971
  }
10273
- }).catch(() => jsonNoCors(res, { error: "invalid_setup_input" }, 400));
9972
+ }).catch(() => json(res, { error: "invalid_setup_input" }, 400));
10274
9973
  return;
10275
9974
  }
10276
9975
  if (path === "/api/llm/test" && req.method === "POST") {
10277
- if (refuseCrossSiteRequest(req) !== void 0) {
10278
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
10279
- return;
10280
- }
10281
9976
  readJsonBody(req).then(async (body) => {
10282
9977
  const rawBody = body;
10283
9978
  if ("apiKey" in rawBody) {
10284
- jsonNoCors(res, {
9979
+ json(res, {
10285
9980
  success: false,
10286
9981
  error: "invalid_request",
10287
9982
  message: "apiKey is not accepted in LLM test requests. Save credentials for the named config instead."
@@ -10291,7 +9986,7 @@ function createRouter(dbOrDeps) {
10291
9986
  const providerName = typeof body.provider === "string" ? body.provider.trim() : "";
10292
9987
  const modelName = typeof body.model === "string" ? body.model.trim() : "";
10293
9988
  if (!providerName || !modelName) {
10294
- jsonNoCors(res, { success: false, error: "invalid_request", message: "provider and model are required" }, 400);
9989
+ json(res, { success: false, error: "invalid_request", message: "provider and model are required" }, 400);
10295
9990
  return;
10296
9991
  }
10297
9992
  const configName = typeof body.configName === "string" ? body.configName.trim() : "";
@@ -10316,7 +10011,7 @@ function createRouter(dbOrDeps) {
10316
10011
  }
10317
10012
  const parsedConfig = ModelConfigSchema2.safeParse(llmCandidate);
10318
10013
  if (!parsedConfig.success) {
10319
- jsonNoCors(res, {
10014
+ json(res, {
10320
10015
  success: false,
10321
10016
  error: "invalid_request",
10322
10017
  message: parsedConfig.error.issues.map((issue) => `${issue.path.map(String).join(".") || "config"}: ${issue.message}`).join("; ")
@@ -10328,7 +10023,7 @@ function createRouter(dbOrDeps) {
10328
10023
  const coreAuth = await import("@vostride/agent-qa-core");
10329
10024
  const resolvedAuth = await coreAuth.resolveLLMAuth(configName, llmConfig2);
10330
10025
  if (resolvedAuth.kind === "missing") {
10331
- jsonNoCors(res, {
10026
+ json(res, {
10332
10027
  success: false,
10333
10028
  error: "missing_credential",
10334
10029
  message: resolvedAuth.message
@@ -10357,7 +10052,7 @@ function createRouter(dbOrDeps) {
10357
10052
  } finally {
10358
10053
  clearTimeout(timeout);
10359
10054
  }
10360
- jsonNoCors(res, {
10055
+ json(res, {
10361
10056
  success: true,
10362
10057
  model: modelName,
10363
10058
  provider: providerName,
@@ -10372,7 +10067,7 @@ function createRouter(dbOrDeps) {
10372
10067
  const providerMessage = withDeclaredMask(declaredMask, (mask) => mask(unsavedMessage));
10373
10068
  const errorClass = classifyProviderError2(providerMessage ?? unsavedMessage, statusCode);
10374
10069
  const message = providerMessage === void 0 ? providerErrorSentence2(errorClass, "").trimEnd() : providerErrorSentence2(errorClass, providerMessage);
10375
- jsonNoCors(res, {
10070
+ json(res, {
10376
10071
  success: false,
10377
10072
  error: errorClass,
10378
10073
  message,
@@ -10382,7 +10077,7 @@ function createRouter(dbOrDeps) {
10382
10077
  ...unauthenticated ? { unauthenticated: true, authMessage } : {}
10383
10078
  });
10384
10079
  }
10385
- }).catch(() => jsonNoCors(res, { success: false, error: "invalid_request", message: "Invalid request body" }, 400));
10080
+ }).catch(() => json(res, { success: false, error: "invalid_request", message: "Invalid request body" }, 400));
10386
10081
  return;
10387
10082
  }
10388
10083
  if (path === "/api/cache/purge" && req.method === "POST") {
@@ -10391,56 +10086,15 @@ function createRouter(dbOrDeps) {
10391
10086
  return;
10392
10087
  }
10393
10088
  readJsonBody(req).then(async (body) => {
10394
- if (!body.file && !body.all) {
10395
- json(res, { error: "Either file or all is required" }, 400);
10089
+ if (body.all !== true) {
10090
+ json(res, { error: "all is required" }, 400);
10396
10091
  return;
10397
10092
  }
10398
10093
  try {
10399
10094
  const cfg = await readConfigValues(configManager);
10400
- const cacheDir = cfg.services?.cache?.dir ?? cfg.cache?.dir;
10401
- if (!cacheDir) {
10402
- json(res, { error: "Cache directory not configured" }, 400);
10403
- return;
10404
- }
10405
10095
  const configDir = deps.configPath ? dirname9(resolve5(deps.configPath)) : process.cwd();
10406
- const resolvedCacheDir = resolve5(configDir, cacheDir);
10407
- let branchDirs;
10408
- try {
10409
- branchDirs = await readdir(resolvedCacheDir);
10410
- } catch (err) {
10411
- if (err.code === "ENOENT") {
10412
- json(res, { purged: 0 });
10413
- return;
10414
- }
10415
- throw err;
10416
- }
10417
- let purged = 0;
10418
- for (const branch of branchDirs) {
10419
- const branchPath = join4(resolvedCacheDir, branch);
10420
- let entryStat;
10421
- try {
10422
- entryStat = await stat5(branchPath);
10423
- } catch {
10424
- continue;
10425
- }
10426
- if (!entryStat.isDirectory()) {
10427
- await rm5(branchPath, { force: true });
10428
- continue;
10429
- }
10430
- let files;
10431
- try {
10432
- files = await readdir(branchPath);
10433
- } catch {
10434
- continue;
10435
- }
10436
- for (const file of files) {
10437
- if (!file.endsWith(".json")) continue;
10438
- await rm5(join4(branchPath, file), { force: true });
10439
- purged++;
10440
- }
10441
- await rm5(branchPath, { recursive: true, force: true }).catch(() => {
10442
- });
10443
- }
10096
+ const cacheDir = cfg.services?.cache?.dir ?? DEFAULT_AGENT_QA_CACHE_DIR;
10097
+ const purged = await purgeActionCache(resolve5(configDir, cacheDir));
10444
10098
  json(res, { purged });
10445
10099
  } catch (err) {
10446
10100
  json(res, { error: err instanceof Error ? err.message : "Cache purge failed" }, 500);
@@ -10524,7 +10178,7 @@ function createRouter(dbOrDeps) {
10524
10178
  return;
10525
10179
  }
10526
10180
  const workspaceEnv = await readWorkspaceEnvVars(workspacePaths);
10527
- const result = await new DockerHookExecutor2().execute(resolvedHook, {
10181
+ const result = await new DockerHookExecutor2({ version: getAgentQaVersion2() }).execute(resolvedHook, {
10528
10182
  env: {
10529
10183
  ...workspaceEnv,
10530
10184
  ...validation.overrides
@@ -10560,7 +10214,7 @@ function createRouter(dbOrDeps) {
10560
10214
  variables: shown.variables,
10561
10215
  sandbox: {
10562
10216
  runtime: resolvedHook.runtime,
10563
- image: RUNTIME_IMAGE_MAP[resolvedHook.runtime],
10217
+ image: hookRunnerImage(resolvedHook.runtime, getAgentQaVersion2()),
10564
10218
  networkMode: resolvedHook.network ? "enabled" : "disabled",
10565
10219
  dockerVersion: null,
10566
10220
  networkLogsAvailable: shown.networkLogs.length > 0,
@@ -10764,12 +10418,8 @@ function createRouter(dbOrDeps) {
10764
10418
  return;
10765
10419
  }
10766
10420
  if (req.method === "GET" && /^\/api\/live-editor\/sessions\/[^/]+\/live-url$/.test(path)) {
10767
- if (refuseCrossSiteRequest(req) !== void 0) {
10768
- jsonNoCors(res, { error: "cross_site_refused" }, 403);
10769
- return;
10770
- }
10771
10421
  if (!deps.mintEditorLiveUrl) {
10772
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
10422
+ json(res, { error: "live_view_unavailable" }, 503);
10773
10423
  return;
10774
10424
  }
10775
10425
  let editorSessionId;
@@ -10778,33 +10428,32 @@ function createRouter(dbOrDeps) {
10778
10428
  path.slice("/api/live-editor/sessions/".length, -"/live-url".length)
10779
10429
  );
10780
10430
  } catch {
10781
- jsonNoCors(res, { error: "unknown_session" }, 404);
10431
+ json(res, { error: "unknown_session" }, 404);
10782
10432
  return;
10783
10433
  }
10784
10434
  void deps.mintEditorLiveUrl(editorSessionId).then(
10785
10435
  (result) => {
10786
- jsonNoCors(res, result);
10436
+ json(res, result);
10787
10437
  },
10788
10438
  (error) => {
10789
10439
  const name = error instanceof Error ? error.name : "";
10790
10440
  if (name === "UnknownLiveEditorSessionError") {
10791
- jsonNoCors(res, { error: "unknown_session" }, 404);
10441
+ json(res, { error: "unknown_session" }, 404);
10792
10442
  return;
10793
10443
  }
10794
10444
  if (name === "LiveViewUnavailableError") {
10795
- jsonNoCors(res, { error: "live_view_unavailable" }, 503);
10445
+ json(res, { error: "live_view_unavailable" }, 503);
10796
10446
  return;
10797
10447
  }
10798
- jsonNoCors(res, { error: "live_url_mint_failed" }, 500);
10448
+ json(res, { error: "live_url_mint_failed" }, 500);
10799
10449
  }
10800
10450
  );
10801
10451
  return;
10802
10452
  }
10803
10453
  const liveEditorAuthStateMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)\/auth-state$/);
10804
10454
  if (liveEditorAuthStateMatch && req.method === "POST") {
10805
- if (refuseCrossSiteDisplayRead(req, res)) return;
10806
10455
  if (!sessionManager) {
10807
- jsonNoCors(res, { error: "Live editor not available" }, 503);
10456
+ json(res, { error: "Live editor not available" }, 503);
10808
10457
  return;
10809
10458
  }
10810
10459
  ;
@@ -10815,35 +10464,34 @@ function createRouter(dbOrDeps) {
10815
10464
  const body = await readJsonBody(req);
10816
10465
  const parsedName = AuthStateNameSchema.safeParse(typeof body?.name === "string" ? body.name : "");
10817
10466
  if (!parsedName.success) {
10818
- jsonNoCors(res, { error: "Auth state name must be a lowercase slug." }, 400);
10467
+ json(res, { error: "Auth state name must be a lowercase slug." }, 400);
10819
10468
  return;
10820
10469
  }
10821
10470
  stateName = parsedName.data;
10822
10471
  session = sessionManager.getSession(decodeURIComponent(liveEditorAuthStateMatch[1]));
10823
10472
  if (!session) {
10824
- jsonNoCors(res, { error: "Session not found" }, 404);
10473
+ json(res, { error: "Session not found" }, 404);
10825
10474
  return;
10826
10475
  }
10827
10476
  if (typeof session.captureWebAuthState !== "function") {
10828
- jsonNoCors(res, {
10477
+ json(res, {
10829
10478
  error: `Could not save auth state "${stateName}" for target "${getSessionTargetNameForAuthState(session)}".`
10830
10479
  }, 500);
10831
10480
  return;
10832
10481
  }
10833
10482
  const authState = await session.captureWebAuthState(stateName, { replace: body?.replace === true });
10834
- jsonNoCors(res, { authState });
10483
+ json(res, { authState });
10835
10484
  } catch (err) {
10836
10485
  const targetName = getSessionTargetNameForAuthState(session);
10837
10486
  const response = buildAuthStateSaveErrorMessage(stateName || "unknown", targetName, err);
10838
- jsonNoCors(res, { error: response.message }, response.status);
10487
+ json(res, { error: response.message }, response.status);
10839
10488
  }
10840
10489
  })();
10841
10490
  return;
10842
10491
  }
10843
10492
  if (path === "/api/live-editor/sessions" && req.method === "POST") {
10844
- if (refuseCrossSiteDisplayRead(req, res)) return;
10845
10493
  if (!sessionManager) {
10846
- jsonNoCors(res, { error: "Live editor not available" }, 503);
10494
+ json(res, { error: "Live editor not available" }, 503);
10847
10495
  return;
10848
10496
  }
10849
10497
  ;
@@ -10851,16 +10499,10 @@ function createRouter(dbOrDeps) {
10851
10499
  try {
10852
10500
  const body = await readJsonBody(req);
10853
10501
  const targetName = typeof body?.targetName === "string" ? body.targetName : void 0;
10854
- const production = await resolveLiveTargetProduction(configManager, targetName);
10855
- const productionRefusal = liveProductionRefusal(production);
10856
- if (productionRefusal !== void 0) {
10857
- jsonNoCors(res, { error: productionRefusal.message }, 403);
10858
- return;
10859
- }
10860
10502
  const platform = body?.platform ?? "web";
10861
10503
  const runtimeLLM = await resolveDashboardRuntimeLLM(configManager, llmConfig, authFetch);
10862
10504
  if (!runtimeLLM.ok) {
10863
- jsonNoCors(res, { error: runtimeLLM.error }, 400);
10505
+ json(res, { error: runtimeLLM.error }, 400);
10864
10506
  return;
10865
10507
  }
10866
10508
  const setupHooks = Array.isArray(body?.setupHooks) ? body.setupHooks.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
@@ -10879,14 +10521,14 @@ function createRouter(dbOrDeps) {
10879
10521
  let resolvedHooks = /* @__PURE__ */ new Map();
10880
10522
  let hookRegistryError;
10881
10523
  if (!workspacePaths) {
10882
- jsonNoCors(res, { error: "Workspace path resolution not available" }, 500);
10524
+ json(res, { error: "Workspace path resolution not available" }, 500);
10883
10525
  return;
10884
10526
  }
10885
10527
  try {
10886
10528
  const envContent = await readFile6(workspacePaths.envFile.absolutePath, "utf-8");
10887
10529
  envVars = parseEnvFile(envContent);
10888
10530
  } catch {
10889
- jsonNoCors(res, { error: `workspace.envFile file could not be loaded: ${workspacePaths.envFile.workspaceRelativePath}` }, 400);
10531
+ json(res, { error: `workspace.envFile file could not be loaded: ${workspacePaths.envFile.workspaceRelativePath}` }, 400);
10890
10532
  return;
10891
10533
  }
10892
10534
  try {
@@ -10894,7 +10536,7 @@ function createRouter(dbOrDeps) {
10894
10536
  secretStore = SecretStore.fromEnvContent(secretsContent);
10895
10537
  secretRedactor = new SecretRedactor(secretStore);
10896
10538
  } catch {
10897
- jsonNoCors(res, { error: `workspace.secretsFile file could not be loaded: ${workspacePaths.secretsFile.workspaceRelativePath}` }, 400);
10539
+ json(res, { error: `workspace.secretsFile file could not be loaded: ${workspacePaths.secretsFile.workspaceRelativePath}` }, 400);
10898
10540
  return;
10899
10541
  }
10900
10542
  const hookCatalog = await readWorkspaceHooks(configManager, deps.configPath);
@@ -10905,6 +10547,11 @@ function createRouter(dbOrDeps) {
10905
10547
  ...suiteFileManager === void 0 ? {} : { findSuite: (id) => suiteFileManager.findBySuiteId(id) }
10906
10548
  };
10907
10549
  const plannerLimits = await resolveLivePlannerLimits(configManager, entityRef, entityReaders);
10550
+ const navigationTimeoutMs = await resolveLiveNavigationTimeoutMs(
10551
+ configManager,
10552
+ entityRef,
10553
+ entityReaders
10554
+ );
10908
10555
  const sessionSuiteRef = entityRef?.type === "suite" ? entityRef : void 0;
10909
10556
  const plannerLimitsForTest = (testId) => resolveLivePlannerLimits(
10910
10557
  configManager,
@@ -10915,7 +10562,6 @@ function createRouter(dbOrDeps) {
10915
10562
  {
10916
10563
  platform,
10917
10564
  targetName,
10918
- production: { productionTargetNames: production.productionTargetNames },
10919
10565
  plannerLimits,
10920
10566
  plannerLimitsForTest,
10921
10567
  llmConfig: runtimeLLM.llmConfig,
@@ -10929,6 +10575,7 @@ function createRouter(dbOrDeps) {
10929
10575
  hookRegistryError,
10930
10576
  url: body?.url,
10931
10577
  headless: body?.headless ?? false,
10578
+ navigationTimeoutMs,
10932
10579
  useDeviceName: typeof body?.useDeviceName === "string" ? body.useDeviceName : void 0,
10933
10580
  appState: body?.appState === "preserve" || body?.appState === "reset" ? body.appState : void 0,
10934
10581
  bundleId: typeof body?.bundleId === "string" ? body.bundleId : void 0,
@@ -10938,38 +10585,37 @@ function createRouter(dbOrDeps) {
10938
10585
  },
10939
10586
  entityRef
10940
10587
  );
10941
- jsonNoCors(res, { sessionId, sessionNumber }, 201);
10588
+ json(res, { sessionId, sessionNumber }, 201);
10942
10589
  } catch (err) {
10943
10590
  const message = err instanceof Error ? err.message : String(err);
10944
- const status = err instanceof LiveProductionRefusedError ? 403 : err instanceof LivePlannerLimitsRefusedError ? 400 : 500;
10945
- jsonNoCors(res, { error: message }, status);
10591
+ const refused = err instanceof LivePlannerLimitsRefusedError || err instanceof LiveNavigationTimeoutRefusedError;
10592
+ const status = refused ? 400 : 500;
10593
+ json(res, { error: message }, status);
10946
10594
  }
10947
10595
  })();
10948
10596
  return;
10949
10597
  }
10950
10598
  if (path === "/api/live-editor/sessions" && req.method === "GET") {
10951
- if (refuseCrossSiteDisplayRead(req, res)) return;
10952
10599
  if (!sessionManager) {
10953
- jsonNoCors(res, { error: "Live editor not available" }, 503);
10600
+ json(res, { error: "Live editor not available" }, 503);
10954
10601
  return;
10955
10602
  }
10956
- jsonNoCors(res, { sessions: sessionManager.listSessions() });
10603
+ json(res, { sessions: sessionManager.listSessions() });
10957
10604
  return;
10958
10605
  }
10959
10606
  const liveEditorSessionMatch = path.match(/^\/api\/live-editor\/sessions\/([^/]+)$/);
10960
10607
  if (liveEditorSessionMatch && req.method === "DELETE") {
10961
- if (refuseCrossSiteDisplayRead(req, res)) return;
10962
10608
  if (!sessionManager) {
10963
- jsonNoCors(res, { error: "Live editor not available" }, 503);
10609
+ json(res, { error: "Live editor not available" }, 503);
10964
10610
  return;
10965
10611
  }
10966
10612
  const sessionId = liveEditorSessionMatch[1];
10967
10613
  (async () => {
10968
10614
  const terminated = await sessionManager.terminateSession(sessionId);
10969
10615
  if (terminated) {
10970
- jsonNoCors(res, { ok: true });
10616
+ json(res, { ok: true });
10971
10617
  } else {
10972
- jsonNoCors(res, { error: "Session not found" }, 404);
10618
+ json(res, { error: "Session not found" }, 404);
10973
10619
  }
10974
10620
  })();
10975
10621
  return;
@@ -11018,6 +10664,7 @@ function createRouter(dbOrDeps) {
11018
10664
  // src/queue/job-queue.ts
11019
10665
  import { EventEmitter } from "node:events";
11020
10666
  import { cpus } from "node:os";
10667
+ import { printRunText as printRunText2 } from "@vostride/agent-qa-core";
11021
10668
  var NOT_CANCELLABLE = { kind: "not-cancellable" };
11022
10669
  var JobQueue = class extends EventEmitter {
11023
10670
  db;
@@ -11040,8 +10687,7 @@ var JobQueue = class extends EventEmitter {
11040
10687
  this.pollInterval = setInterval(() => this.dispatchReported("a poll"), intervalMs);
11041
10688
  this.on("enqueued", () => this.dispatchReported("an enqueue"));
11042
10689
  }
11043
- // 2026-10-17 (444-16, D-50): the server closes the database after this; a run that settles later
11044
- // still frees its slot, and nothing reads the closed database.
10690
+ // The server closes the database after this; a run settling later still frees its slot.
11045
10691
  stop() {
11046
10692
  this.stopped = true;
11047
10693
  if (this.pollInterval) {
@@ -11127,7 +10773,10 @@ var JobQueue = class extends EventEmitter {
11127
10773
  this.processNext();
11128
10774
  } catch (error) {
11129
10775
  const message = error instanceof Error ? error.message : String(error);
11130
- console.error(`Queue could not dispatch the next run after ${trigger}: ${message}`);
10776
+ printRunText2(
10777
+ `Queue could not dispatch the next run after ${trigger}: ${message}`,
10778
+ (text) => console.error(text)
10779
+ );
11131
10780
  }
11132
10781
  }
11133
10782
  getActiveCount() {
@@ -11136,9 +10785,7 @@ var JobQueue = class extends EventEmitter {
11136
10785
  getConcurrency() {
11137
10786
  return this.concurrency;
11138
10787
  }
11139
- // 2026-10-16 (442-05, D-06 to D-08): only the owner cancels a running row; gone is an orphan.
11140
- // 442-15 (GAP-D2): own is the stamp this handle wrote, or equal non-null start times.
11141
- // 2026-10-17 (444-06, D-11): a running row is never written here; the run answers with its verdict.
10788
+ // Only the owner stops its running row, never written here; a gone owner's row is an orphan.
11142
10789
  cancel(runId) {
11143
10790
  const run = this.db.getRun(runId);
11144
10791
  if (!run) return NOT_CANCELLABLE;
@@ -11168,21 +10815,7 @@ var JobQueue = class extends EventEmitter {
11168
10815
  const run = this.db.getRun(runId);
11169
10816
  return run && run.status !== "running" && run.status !== "pending" ? { kind: "finished", status: run.status } : NOT_CANCELLABLE;
11170
10817
  }
11171
- // 2026-08-24 (Phase 402 plan 09, D-11): THE CHILD-RUN CANCELLATION FAN-OUT IS DELETED WITH THE
11172
- // MODEL THAT MINTED ITS CHILDREN.
11173
- //
11174
- // ~19 lines stood here walking the run's child rows and cancelling each member row. Under
11175
- // the one-run model a parallel suite is ONE run row with member handles (402-04) — nothing writes
11176
- // the run row's parent-link column any more (405-07 then deleted the column outright), so the
11177
- // walk could only ever have read an empty list while reading as live cancellation policy.
11178
- // Cancelling the run cancels the run.
11179
- //
11180
- // WHAT THIS DOES NOT TOUCH: `parallel` below. D-11 keeps the queue's parallel flag as CROSS-RUN
11181
- // concurrency of INDEPENDENT jobs, which is what `findNextEligible` has always used it for. What
11182
- // was deleted is parallel-SUITE semantics, not the queue's ability to run two unrelated jobs.
11183
- //
11184
- // HISTORICAL ROWS: a pre-402 suite run still lists its members on the read side; cancelling
11185
- // such a run now leaves those member rows in whatever state their own process left them.
10818
+ // `parallel` is cross-run concurrency of independent jobs; a suite is one run row.
11186
10819
  findNextEligible() {
11187
10820
  if (this.activeCount >= this.concurrency) return void 0;
11188
10821
  const pending = this.db.getPendingRuns();
@@ -11205,7 +10838,7 @@ var JobQueue = class extends EventEmitter {
11205
10838
  };
11206
10839
 
11207
10840
  // src/tests/suite-file-manager.ts
11208
- import { readFile as readFile7, mkdir as mkdir4, stat as stat6, rm as rm6 } from "node:fs/promises";
10841
+ import { readFile as readFile7, mkdir as mkdir4, stat as stat5, rm as rm5 } from "node:fs/promises";
11209
10842
  import { dirname as dirname10, basename as basename6 } from "node:path";
11210
10843
  import { generateSuiteId } from "@vostride/agent-qa-ids";
11211
10844
  import { parse as parseYaml7 } from "yaml";
@@ -11216,14 +10849,14 @@ var SuiteFileManager = class {
11216
10849
  this.testFileManager = testFileManager;
11217
10850
  }
11218
10851
  testFileManager;
11219
- // 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.
10852
+ // Each list call reads the workspace itself; each file is written whole, one writer at a time.
11220
10853
  underLock = createPathLock();
11221
10854
  async list() {
11222
10855
  const records = await discoverWorkspaceFiles4({ workspace: this.workspace, kind: "suite" });
11223
10856
  const files = [];
11224
10857
  for (const record of records) {
11225
10858
  try {
11226
- const s = await stat6(record.absolutePath);
10859
+ const s = await stat5(record.absolutePath);
11227
10860
  if (s.isFile()) {
11228
10861
  let suiteName = basename6(record.workspaceRelativePath);
11229
10862
  let testCount = 0;
@@ -11300,7 +10933,7 @@ var SuiteFileManager = class {
11300
10933
  }
11301
10934
  async delete(relativePath) {
11302
10935
  const target = await this.resolveTarget(relativePath, true);
11303
- await this.underLock(target.absolutePath, () => rm6(target.absolutePath));
10936
+ await this.underLock(target.absolutePath, () => rm5(target.absolutePath));
11304
10937
  }
11305
10938
  async validate(content) {
11306
10939
  try {
@@ -11357,6 +10990,7 @@ import { request } from "node:http";
11357
10990
  import { spawn } from "node:child_process";
11358
10991
  import {
11359
10992
  formatAppiumInstallGuidance,
10993
+ printRunText as printRunText3,
11360
10994
  resolveAppiumExecutable
11361
10995
  } from "@vostride/agent-qa-core";
11362
10996
  import treeKill from "tree-kill";
@@ -11484,21 +11118,21 @@ var AppiumManager = class {
11484
11118
  if (err.code === "ENOENT") {
11485
11119
  console.error(`${APPIUM_PREFIX} Appium not found. ${formatAppiumInstallGuidance()}`);
11486
11120
  } else {
11487
- console.error(`${APPIUM_PREFIX} Error: ${err.message}`);
11121
+ printRunText3(`${APPIUM_PREFIX} Error: ${err.message}`, (text) => console.error(text));
11488
11122
  }
11489
11123
  });
11490
11124
  child.stdout?.on("data", (chunk) => {
11491
11125
  if (this.logLevel !== "debug") return;
11492
11126
  const lines = chunk.toString().split("\n").filter(Boolean);
11493
11127
  for (const line of lines) {
11494
- console.log(`${APPIUM_PREFIX} ${pc.dim(line)}`);
11128
+ printRunText3(`${APPIUM_PREFIX} ${pc.dim(line)}`, (text) => console.log(text));
11495
11129
  }
11496
11130
  });
11497
11131
  child.stderr?.on("data", (chunk) => {
11498
11132
  if (this.logLevel !== "debug") return;
11499
11133
  const lines = chunk.toString().split("\n").filter(Boolean);
11500
11134
  for (const line of lines) {
11501
- console.error(`${APPIUM_PREFIX} ${pc.dim(line)}`);
11135
+ printRunText3(`${APPIUM_PREFIX} ${pc.dim(line)}`, (text) => console.error(text));
11502
11136
  }
11503
11137
  });
11504
11138
  const start = Date.now();
@@ -11580,10 +11214,7 @@ var SessionManager = class {
11580
11214
  this.deps = deps;
11581
11215
  }
11582
11216
  sessions = /* @__PURE__ */ new Map();
11583
- // Per-entity counter keyed by `${entityType}:${entityId}`.
11584
- // Monotonic and ephemeral: resets on server restart per CONTEXT D-30.
11585
- // Never decremented on terminate — a re-open of a terminated entity yields
11586
- // the next number so "Session #3" after "#2" ends does not confuse users.
11217
+ // Monotonic per entity and reset on restart, so a reopened entity gets the next number.
11587
11218
  counters = /* @__PURE__ */ new Map();
11588
11219
  async createSession(config, entity) {
11589
11220
  if (!config.llmConfig) {
@@ -11640,7 +11271,8 @@ var SessionManager = class {
11640
11271
  };
11641
11272
 
11642
11273
  // src/live-editor/ws-handler.ts
11643
- import { parseTestFile } from "@vostride/agent-qa-core";
11274
+ import { format as format2 } from "node:util";
11275
+ import { parseTestFile, printRunText as printRunText4 } from "@vostride/agent-qa-core";
11644
11276
 
11645
11277
  // src/live-editor/types.ts
11646
11278
  var LIVE_EDITOR_UNKNOWN_SESSION_CLOSE_CODE = 4404;
@@ -11827,12 +11459,14 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
11827
11459
  try {
11828
11460
  raw = await testFileManager.read(msg.path);
11829
11461
  } catch (readErr) {
11830
- console.error(`[ws-handler] execute-test: read failed for path=${msg.path}`, readErr);
11462
+ const line = `[ws-handler] execute-test: read failed for path=${msg.path}`;
11463
+ printRunText4(format2(line, readErr), () => console.error(line, readErr));
11831
11464
  throw new Error(`Cannot read test file "${msg.path}": ${readErr?.message ?? String(readErr)}`);
11832
11465
  }
11833
11466
  const parsed = parseTestFile(raw, msg.path);
11834
11467
  if (parsed.errors.length > 0) {
11835
- console.error(`[ws-handler] execute-test: parse errors for ${msg.path}`, parsed.errors);
11468
+ const line = `[ws-handler] execute-test: parse errors for ${msg.path}`;
11469
+ printRunText4(format2(line, parsed.errors), () => console.error(line, parsed.errors));
11836
11470
  throw new Error(`Parse error in ${msg.path}: ${parsed.errors[0].message}`);
11837
11471
  }
11838
11472
  const testDef = parsed.tests[0];
@@ -11842,7 +11476,13 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
11842
11476
  const stepInstructions = testDef.steps.map(
11843
11477
  (s) => typeof s === "string" ? s : s.step
11844
11478
  );
11845
- console.log(`[ws-handler] execute-test: starting "${testDef.name}" (${stepInstructions.length} steps, setup=${(testDef.setup ?? []).length}, teardown=${(testDef.teardown ?? []).length})`);
11479
+ const stepCount = stepInstructions.length;
11480
+ const setupCount = (testDef.setup ?? []).length;
11481
+ const teardownCount = (testDef.teardown ?? []).length;
11482
+ printRunText4(
11483
+ `[ws-handler] execute-test: starting "${testDef.name}" (${stepCount} steps, setup=${setupCount}, teardown=${teardownCount})`,
11484
+ (text) => console.log(text)
11485
+ );
11846
11486
  reply({
11847
11487
  type: "test-start",
11848
11488
  test: {
@@ -11864,7 +11504,11 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
11864
11504
  teardown: testDef.teardown ?? []
11865
11505
  }
11866
11506
  );
11867
- console.log(`[ws-handler] execute-test: finished "${testDef.name}" status=${result.status} duration=${result.duration}ms${result.error ? ` error=${result.error}` : ""}`);
11507
+ const errorPart = result.error ? ` error=${result.error}` : "";
11508
+ printRunText4(
11509
+ `[ws-handler] execute-test: finished "${testDef.name}" status=${result.status} duration=${result.duration}ms${errorPart}`,
11510
+ (text) => console.log(text)
11511
+ );
11868
11512
  reply({
11869
11513
  type: "test-complete",
11870
11514
  test: {
@@ -11877,11 +11521,9 @@ function createSessionHandler(session, onTerminate, onTokenUsage, testFileManage
11877
11521
  });
11878
11522
  reply({ type: "session-state", state: session.getState() });
11879
11523
  } catch (err) {
11880
- console.error(`[ws-handler] execute-test: caught error for path=${msg.path}`, {
11881
- message: err?.message,
11882
- stack: err?.stack,
11883
- code: err?.code
11884
- });
11524
+ const line = `[ws-handler] execute-test: caught error for path=${msg.path}`;
11525
+ const detail = { message: err?.message, stack: err?.stack, code: err?.code };
11526
+ printRunText4(format2(line, detail), () => console.error(line, detail));
11885
11527
  reply({
11886
11528
  type: "test-error",
11887
11529
  test: {
@@ -12032,11 +11674,9 @@ var LIVE_EMBED_STATES = [
12032
11674
  "no-stream",
12033
11675
  "expired",
12034
11676
  "refused",
12035
- // THIS MEMBER'S STREAM ENDED WHILE THE RUN CONTINUES (403 CR-03). Separate the whole run's end,
12036
- // because a person watching one member of a suite needs to know which of the two happened — and
12037
- // neither may be shown as `live` over whichever frame happened to be last.
11677
+ // This member's stream ended while the run continues, so the viewer knows which one ended.
12038
11678
  "stream-ended",
12039
- // AN EDITOR SESSION ENDED (428 D-07/D-09). An editor subject has no run to end.
11679
+ // An editor session ended; an editor subject has no run to end.
12040
11680
  "session-ended",
12041
11681
  "ended",
12042
11682
  "lost"
@@ -12165,8 +11805,7 @@ function renderLiveEmbedPage(input) {
12165
11805
  if (pending === null) return;
12166
11806
  var header = pending;
12167
11807
  pending = null;
12168
- // NO MIME TYPE: the decoder sniffs the bytes, so a JPEG screencast frame and a PNG snapshot
12169
- // both paint in every host, not only in Chromium (428 RESEARCH A2).
11808
+ // No MIME type: the decoder sniffs, so JPEG and PNG frames paint in every host.
12170
11809
  var blob = new Blob([event.data]);
12171
11810
  createImageBitmap(blob).then(function (bitmap) {
12172
11811
  if (canvas.width !== header.width || canvas.height !== header.height) {
@@ -12190,8 +11829,7 @@ function renderLiveEmbedPage(input) {
12190
11829
  try { msg = JSON.parse(event.data); } catch (e) { return; }
12191
11830
 
12192
11831
  if (msg.type === 'ready') {
12193
- // THE PAINTER IS CHOSEN BY THE DECLARED KIND \u2014 never by a browser name, an adapter class or
12194
- // a transport guess (LIVE-04's prohibition). A snapshot paints on the same canvas (428 D-11).
11832
+ // The declared kind picks the painter, never a browser name, adapter or transport guess.
12195
11833
  var kind = msg.stream ? msg.stream.kind : undefined;
12196
11834
  if (kind === 'cdp-screencast' || kind === 'snapshot') {
12197
11835
  setState('connecting', 'waiting for the first frame\u2026');
@@ -12238,8 +11876,7 @@ function renderLiveEmbedPage(input) {
12238
11876
  report({ event: 'lost' });
12239
11877
  });
12240
11878
 
12241
- // A HOST THAT RE-MINTS replaces the fragment; the socket renews in place rather than reloading,
12242
- // so a renewal never costs the viewer the stream it is watching (D-02).
11879
+ // A re-minting host replaces the fragment; the socket renews in place and the stream continues.
12243
11880
  window.addEventListener('hashchange', function () {
12244
11881
  var next = location.hash.slice(1);
12245
11882
  if (next === '' || socket.readyState !== 1) return;
@@ -12449,7 +12086,8 @@ function createFrameProxy(deps) {
12449
12086
 
12450
12087
  // src/server/server.ts
12451
12088
  import {
12452
- isLoopbackHost as isLoopbackHost2
12089
+ isLoopbackHost as isLoopbackHost2,
12090
+ printRunText as printRunText5
12453
12091
  } from "@vostride/agent-qa-core";
12454
12092
  var MIME_TYPES = {
12455
12093
  ".html": "text/html; charset=utf-8",
@@ -12469,9 +12107,10 @@ var MIME_TYPES = {
12469
12107
  ".webp": "image/webp"
12470
12108
  };
12471
12109
  var DashboardRemoteBindRefusedError = class extends Error {
12472
- constructor(host) {
12110
+ constructor(host, port) {
12111
+ const p = String(port === 0 ? 3470 : port);
12473
12112
  super(
12474
- `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.`
12113
+ `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}/.`
12475
12114
  );
12476
12115
  this.host = host;
12477
12116
  this.name = "DashboardRemoteBindRefusedError";
@@ -12483,7 +12122,7 @@ async function startServer(opts) {
12483
12122
  const runServiceAttach = opts.runServiceAttach ?? runService?.attach;
12484
12123
  const host = opts.host?.trim() || DEFAULT_DASHBOARD_HOST;
12485
12124
  if (!isLoopbackHost2(host)) {
12486
- throw new DashboardRemoteBindRefusedError(host);
12125
+ throw new DashboardRemoteBindRefusedError(host, port);
12487
12126
  }
12488
12127
  const resolvedConfigPath = configPath ?? resolve6("agent-qa.config.yaml");
12489
12128
  const configManager = new ConfigManager(resolvedConfigPath, opts.declaredSecretMask);
@@ -12518,7 +12157,7 @@ async function startServer(opts) {
12518
12157
  failureSummary = "Test timed out -- process was killed";
12519
12158
  } else if (details.refusedBeforeStart) {
12520
12159
  dbStatus = "failed";
12521
- refusalProse = details.neutralReason ?? details.reason;
12160
+ refusalProse = details.reason;
12522
12161
  failureSummary = refusalProse ? `Refused before the run started: ${refusalProse}`.slice(0, 300) : "Refused before the run started";
12523
12162
  } else if (status === "completed") {
12524
12163
  dbStatus = "failed";
@@ -12599,7 +12238,10 @@ async function startServer(opts) {
12599
12238
  mobileRuns.set(run.id, platform);
12600
12239
  } catch (err) {
12601
12240
  const message = err instanceof Error ? err.message : String(err);
12602
- console.error(`Failed to start Appium for run ${run.id}: ${message}`);
12241
+ printRunText5(
12242
+ `Failed to start Appium for run ${run.id}: ${message}`,
12243
+ (text) => console.error(text)
12244
+ );
12603
12245
  db.updateRun(run.id, {
12604
12246
  status: "failed",
12605
12247
  failureSummary: `Appium server failed to start: ${message}`,
@@ -12619,19 +12261,9 @@ async function startServer(opts) {
12619
12261
  runId: run.id,
12620
12262
  args,
12621
12263
  source: meta.isSuite ? "suite" : "dashboard",
12622
- // 2026-10-01 (432-14, D-14): no confirm scope is read off the row any more. The browser that
12623
- // triggered a run is not asked anything, so nothing about it has to survive the wait.
12624
12264
  attributes: run.attributes,
12625
- // T-401-G26 (401-23): a persisted row's timeout is VALIDATED at the seam that consumes it.
12626
- // `in-process-backend.ts:211` reads `opts.timeout ?? defaultTimeout` and validates AFTER, so
12627
- // any non-nullish nonsense here skips the default and leaves the run with no reclamation
12628
- // timer at all. An unusable value falls back to the backend's default rather than a number
12629
- // this call invented.
12265
+ // The backend takes `opts.timeout ?? defaultTimeout`, so an unusable value falls back here.
12630
12266
  timeout: sanitizeQueuedExecutionTimeout(meta.timeout),
12631
- // THE LEASE ABOVE IS UNCHANGED; ONLY ITS DELIVERY IS. `acquireLease` still runs where it ran,
12632
- // still keys on this run id and still owns the bundled Appium's lifecycle (design-of-record).
12633
- // What used to ride `AGENT_QA_APPIUM_URL` in a child's environment now travels as an
12634
- // argument, because there is no child to carry an environment.
12635
12267
  ...isMobile ? { appiumUrl: appiumManager.getUrl() } : {}
12636
12268
  });
12637
12269
  });
@@ -12642,16 +12274,14 @@ async function startServer(opts) {
12642
12274
  appiumManager,
12643
12275
  configManager,
12644
12276
  configPath: resolvedConfigPath,
12645
- // 428 D-07: each created session registers here, which is what the frame socket attaches.
12277
+ // Each created session registers here, which is what the frame socket attaches.
12646
12278
  ...liveEditorSubjects === void 0 ? {} : { liveEditorSubjects }
12647
12279
  });
12648
12280
  const servesLiveView = liveView !== void 0 && (runServiceAttach !== void 0 || liveEditorSubjects !== void 0);
12649
12281
  const apiRouter = createRouter({
12650
12282
  db,
12651
12283
  workspacePaths,
12652
- // 401-07: conditionally spread on the same house rule `agentHost` follows — an absent
12653
- // dependency is an ABSENT field, never a present `undefined`, so the router's own 503 arm is
12654
- // what decides, in one place.
12284
+ // Absent dependencies are absent fields, so the router's own 503 arm decides in one place.
12655
12285
  ...executionBackend === void 0 ? {} : { executionBackend },
12656
12286
  jobQueue,
12657
12287
  testFileManager,
@@ -12662,27 +12292,13 @@ async function startServer(opts) {
12662
12292
  llmConfig,
12663
12293
  authFetch,
12664
12294
  sessionManager,
12665
- // 400-01: conditionally spread on the house rule an absent dependency is an ABSENT field and
12666
- // never a present `undefined` — the router's own 501 arm is what decides, in one place.
12295
+ // Absent dependencies are absent fields, so the router's own 501 arm decides in one place.
12667
12296
  ...agentHost === void 0 ? {} : { agentHost },
12668
- // 402-05 (D-15): the same conditional-spread house rule. Absent means a legacy mount rather
12669
- // than an unconfigured service, and the router's own 503 arm is what decides, in one place.
12670
12297
  ...runServiceReadiness === void 0 ? {} : { runServiceReadiness },
12671
- // 402-08 (D-01): the LOCAL run-stream door's attach port, READ OFF `runService` rather than
12672
- // composed again. That derivation is the "one session registry serves both doors" claim in its
12673
- // structural form — there is no second wiring a composition root could forget or point
12674
- // somewhere else, and the two doors cannot end up attached to different registries. Absent
12675
- // `runService` means no registry exists at all, so the same conditional-spread house rule
12676
- // applies and the router's own 503 arm decides.
12298
+ // Read off `runService` by default, so both doors attach to one session registry.
12677
12299
  ...runServiceAttach === void 0 ? {} : { runServiceAttach },
12678
- // 403 D-03: the mint route's port, on the same conditional-spread house rule — absent means
12679
- // this host serves no live view and the route's own 503 arm decides, in one place.
12680
12300
  ...liveView === void 0 ? {} : { mintLiveUrl: liveView.mintLiveUrl },
12681
- // 428 D-07: the editor mint, on the same house rule — absent means the editor mint route's own
12682
- // 503 decides.
12683
12301
  ...liveView?.mintEditorLiveUrl === void 0 ? {} : { mintEditorLiveUrl: liveView.mintEditorLiveUrl },
12684
- // 404-05 (D-16): the same conditional-spread house rule — absent means this host serves no
12685
- // post-processing push channel and the stream arm's own 503 decides, in one place.
12686
12302
  ...postProcessingRelay === void 0 ? {} : { postProcessingRelay }
12687
12303
  });
12688
12304
  const runServiceRouter = runService === void 0 ? void 0 : createRunServiceRoutes(runService);
@@ -12702,13 +12318,15 @@ async function startServer(opts) {
12702
12318
  apiRouter(req, res);
12703
12319
  return;
12704
12320
  }
12321
+ if (refuseNonLoopbackHost(req) !== void 0) {
12322
+ const address = server.address();
12323
+ const boundPort = typeof address === "object" && address !== null ? address.port : port;
12324
+ refuseForeignHostPage(res, boundPort);
12325
+ return;
12326
+ }
12705
12327
  if (req.method === "OPTIONS") {
12706
- res.writeHead(204, {
12707
- "Access-Control-Allow-Origin": "*",
12708
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
12709
- "Access-Control-Allow-Headers": "Content-Type"
12710
- });
12711
- res.end();
12328
+ res.writeHead(404, { "Content-Type": "text/plain" });
12329
+ res.end("Not Found");
12712
12330
  return;
12713
12331
  }
12714
12332
  if (req.method === "GET" && path.startsWith(LIVE_EMBED_PATH_PREFIX)) {
@@ -12743,17 +12361,16 @@ async function startServer(opts) {
12743
12361
  return;
12744
12362
  }
12745
12363
  if (uiDir) {
12746
- const filePath = path === "/" ? join5(uiDir, "index.html") : join5(uiDir, path);
12364
+ const filePath = path === "/" ? join4(uiDir, "index.html") : join4(uiDir, path);
12747
12365
  try {
12748
- const fileStat = await stat7(filePath);
12366
+ const fileStat = await stat6(filePath);
12749
12367
  if (fileStat.isFile()) {
12750
12368
  const content = await readFile8(filePath);
12751
12369
  const ext = extname(filePath);
12752
12370
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
12753
12371
  res.writeHead(200, {
12754
12372
  "Content-Type": contentType,
12755
- "Content-Length": content.length,
12756
- "Access-Control-Allow-Origin": "*"
12373
+ "Content-Length": content.length
12757
12374
  });
12758
12375
  res.end(content);
12759
12376
  return;
@@ -12761,12 +12378,11 @@ async function startServer(opts) {
12761
12378
  } catch {
12762
12379
  }
12763
12380
  try {
12764
- const indexPath = join5(uiDir, "index.html");
12381
+ const indexPath = join4(uiDir, "index.html");
12765
12382
  const content = await readFile8(indexPath);
12766
12383
  res.writeHead(200, {
12767
12384
  "Content-Type": "text/html",
12768
- "Content-Length": content.length,
12769
- "Access-Control-Allow-Origin": "*"
12385
+ "Content-Length": content.length
12770
12386
  });
12771
12387
  res.end(content);
12772
12388
  return;
@@ -12851,7 +12467,8 @@ async function startServer(opts) {
12851
12467
  session.clearIdleTimer();
12852
12468
  const handler = createSessionHandler(session, () => {
12853
12469
  void sessionManager.terminateSession(sessionId).catch((error) => {
12854
- console.error(`[live-editor] could not end session ${sessionId}`, error);
12470
+ const line = `[live-editor] could not end session ${sessionId}`;
12471
+ printRunText5(format3(line, error), () => console.error(line, error));
12855
12472
  });
12856
12473
  }, (event) => {
12857
12474
  db.insertTokenEvent(event);
@@ -12941,9 +12558,6 @@ var InProcessExecutionBackend = class extends EventEmitter2 {
12941
12558
  this.options = options;
12942
12559
  this.retentionMs = options.retentionMs ?? DEFAULT_HANDLE_RETENTION_MS;
12943
12560
  }
12944
- async preflight(opts) {
12945
- await this.options.preflightRun?.(opts);
12946
- }
12947
12561
  execute(opts) {
12948
12562
  const handle = {
12949
12563
  runId: opts.runId,
@@ -13019,9 +12633,7 @@ var InProcessExecutionBackend = class extends EventEmitter2 {
13019
12633
  settle(handle, error) {
13020
12634
  if (handle.timeoutTimer) clearTimeout(handle.timeoutTimer);
13021
12635
  let refusedBeforeStart = false;
13022
- let neutralReason;
13023
12636
  if (error !== void 0) {
13024
- neutralReason = dashboardNeutralReasonOf(error);
13025
12637
  const message = error instanceof Error ? error.message : String(error);
13026
12638
  for (const line of message.split("\n")) handle.output.push(line);
13027
12639
  refusedBeforeStart = handle.session === void 0;
@@ -13035,7 +12647,6 @@ var InProcessExecutionBackend = class extends EventEmitter2 {
13035
12647
  const details = {
13036
12648
  ...handle.completionReason === void 0 ? {} : { reason: handle.completionReason },
13037
12649
  ...refusedBeforeStart ? { refusedBeforeStart: true } : {},
13038
- ...neutralReason === void 0 ? {} : { neutralReason },
13039
12650
  output: [...handle.output]
13040
12651
  };
13041
12652
  this.emit("run-settled", handle.runId, handle.status, details);
@@ -13045,12 +12656,6 @@ var InProcessExecutionBackend = class extends EventEmitter2 {
13045
12656
  cleanup.unref();
13046
12657
  }
13047
12658
  };
13048
- function dashboardNeutralReasonOf(error) {
13049
- if (typeof error !== "object" || error === null) return void 0;
13050
- const carried = error.neutralReason;
13051
- if (typeof carried !== "string" || carried.trim().length === 0) return void 0;
13052
- return carried;
13053
- }
13054
12659
  function createInProcessExecutionBackend(options) {
13055
12660
  return new InProcessExecutionBackend(options);
13056
12661
  }
@@ -13185,7 +12790,6 @@ export {
13185
12790
  renderLiveEmbedPage,
13186
12791
  resolveDashboardDbPath,
13187
12792
  resolveRunOwnerHost,
13188
- runDenialOf,
13189
12793
  runRowIdentity,
13190
12794
  serializeConfigWrite,
13191
12795
  servePostProcessingRead,