loki-mode 10.7.0 → 10.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +1 -0
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/loki +4 -1
  5. package/autonomy/run.sh +9 -5
  6. package/dashboard/__init__.py +1 -1
  7. package/dashboard/server.py +6 -8
  8. package/docs/INSTALLATION.md +1 -1
  9. package/docs/v10/CONTROL-PLANE.md +7 -0
  10. package/docs/v10/CP-ENTERPRISE-UI.md +263 -2
  11. package/docs/v10/DECISIONS.md +41 -0
  12. package/docs/v10/ENGINE-LAWS.md +155 -0
  13. package/docs/v10/FAILURE-CLASSES.md +166 -0
  14. package/docs/v10/GUIDE.md +4 -2
  15. package/docs/v10/L0-WAVE1.md +81 -0
  16. package/docs/v10/METRICS.md +6 -0
  17. package/docs/v10/MORNING-BRIEF-1003.md +11 -25
  18. package/docs/v10/OPERATING-MODEL.md +1 -0
  19. package/docs/v10/PROGRESS.md +28 -0
  20. package/loki-ts/data/model-pricing.json +6 -6
  21. package/loki-ts/dist/loki.js +851 -837
  22. package/loki-ts/package.json +2 -1
  23. package/mcp/__init__.py +1 -1
  24. package/package.json +2 -2
  25. package/packages/control-plane/dist/server.js +17103 -5411
  26. package/packages/control-plane/drizzle/0002_tiresome_fixer.sql +7 -0
  27. package/packages/control-plane/drizzle/0003_integrity.sql +4 -0
  28. package/packages/control-plane/drizzle/0004_cpe03_local_repos_actions.sql +17 -0
  29. package/packages/control-plane/drizzle/meta/0002_snapshot.json +409 -0
  30. package/packages/control-plane/drizzle/meta/0003_snapshot.json +437 -0
  31. package/packages/control-plane/drizzle/meta/0004_snapshot.json +542 -0
  32. package/packages/control-plane/drizzle/meta/_journal.json +22 -1
  33. package/packages/control-plane/package.json +4 -1
  34. package/packages/control-plane/src/db/migrate.ts +6 -2
  35. package/packages/control-plane/src/db/prune.ts +91 -0
  36. package/packages/control-plane/src/db/schema.ts +32 -0
  37. package/packages/control-plane/src/server/app.ts +30 -22
  38. package/packages/control-plane/src/server/audit.ts +15 -0
  39. package/packages/control-plane/src/server/auth.ts +7 -1
  40. package/packages/control-plane/src/server/integrity.ts +147 -0
  41. package/packages/control-plane/src/server/legacy/routes-metrics.ts +18 -0
  42. package/packages/control-plane/src/server/legacy/routes.ts +237 -0
  43. package/packages/control-plane/src/server/legacy/shim.ts +195 -0
  44. package/packages/control-plane/src/server/repos.ts +27 -0
  45. package/packages/control-plane/src/server/routes/artifacts.ts +104 -0
  46. package/packages/control-plane/src/server/routes/audit.ts +17 -0
  47. package/packages/control-plane/src/server/routes/config.ts +319 -0
  48. package/packages/control-plane/src/server/routes/control.ts +75 -0
  49. package/packages/control-plane/src/server/routes/cost.ts +70 -0
  50. package/packages/control-plane/src/server/routes/cost_ledger.ts +210 -0
  51. package/packages/control-plane/src/server/routes/fleet.ts +101 -0
  52. package/packages/control-plane/src/server/routes/index.ts +68 -0
  53. package/packages/control-plane/src/server/routes/integrations.ts +123 -0
  54. package/packages/control-plane/src/server/routes/merge_risk.ts +209 -0
  55. package/packages/control-plane/src/server/routes/metrics.ts +93 -0
  56. package/packages/control-plane/src/server/routes/notify.ts +48 -0
  57. package/packages/control-plane/src/server/routes/providers.ts +111 -0
  58. package/packages/control-plane/src/server/routes/start.ts +39 -0
  59. package/packages/control-plane/src/server/routes/stats.ts +76 -0
  60. package/packages/control-plane/src/server/routes/stream.ts +85 -0
  61. package/packages/control-plane/src/server/routes/verify.ts +66 -0
  62. package/packages/control-plane/src/server/runs.ts +46 -4
  63. package/packages/control-plane/src/server/spawn.ts +79 -0
  64. package/packages/control-plane/src/shipper/discover.ts +50 -3
  65. package/packages/control-plane/src/shipper/ship.ts +15 -0
  66. package/packages/control-plane/src/shipper/watch.ts +3 -1
  67. package/packages/control-plane/ui/dist/assets/index-BQbFv65M.css +1 -0
  68. package/packages/control-plane/ui/dist/assets/index-Dbhu244h.js +155 -0
  69. package/packages/control-plane/ui/dist/index.html +2 -2
  70. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  71. package/web-app/dist/assets/{AdminPage-BQPb5rc5.js → AdminPage-DDDvUz0N.js} +1 -1
  72. package/web-app/dist/assets/{Avatar-Cttco6FC.js → Avatar-Dcdz46i6.js} +1 -1
  73. package/web-app/dist/assets/{Badge-CPb6kpbH.js → Badge-BB24cCpK.js} +1 -1
  74. package/web-app/dist/assets/{Button-caTL28a4.js → Button-D0-DmPZL.js} +1 -1
  75. package/web-app/dist/assets/{CockpitPage-bEbLx6ph.js → CockpitPage-CdjpU2JW.js} +1 -1
  76. package/web-app/dist/assets/{ComparePage-DNtJSij-.js → ComparePage-DT5U0y0d.js} +1 -1
  77. package/web-app/dist/assets/{ErrorBoundary-BrgC3K44.js → ErrorBoundary-lEHcl0EO.js} +1 -1
  78. package/web-app/dist/assets/{EvidenceReceiptPanel-Q-mSrUUQ.js → EvidenceReceiptPanel-GpnWvdCr.js} +1 -1
  79. package/web-app/dist/assets/{GitHubIssuesPanel-DbmYl_Er.js → GitHubIssuesPanel-C7VOh1B7.js} +1 -1
  80. package/web-app/dist/assets/{GitHubPRsPanel-C6PCHM4B.js → GitHubPRsPanel-G_kOuINv.js} +1 -1
  81. package/web-app/dist/assets/{HomePage-7zsL_k_e.js → HomePage-PqRisOny.js} +2 -2
  82. package/web-app/dist/assets/{LoginPage-Cl4ON8RQ.js → LoginPage-DU7pFAkf.js} +1 -1
  83. package/web-app/dist/assets/{MagicPage-BNEAhTqH.js → MagicPage-C5JWIWMk.js} +1 -1
  84. package/web-app/dist/assets/{MetricsPage-DyBH-88q.js → MetricsPage-CbIGS_qo.js} +1 -1
  85. package/web-app/dist/assets/{NotFoundPage-CIVr21yL.js → NotFoundPage-vU1pvdSt.js} +1 -1
  86. package/web-app/dist/assets/{ProjectPage-BMeVYocF.js → ProjectPage-SsixIPPv.js} +3 -3
  87. package/web-app/dist/assets/{ProjectsPage-D-H2WRVU.js → ProjectsPage-BfQKSR9k.js} +1 -1
  88. package/web-app/dist/assets/{SettingsPage-o0kR9RAt.js → SettingsPage-B8YfXAom.js} +1 -1
  89. package/web-app/dist/assets/{ShowcasePage-DtfMDMjp.js → ShowcasePage-Ba407f2-.js} +1 -1
  90. package/web-app/dist/assets/{SystemSettingsPage-CkXdpZ_T.js → SystemSettingsPage-DzNQhhBS.js} +1 -1
  91. package/web-app/dist/assets/{TeamsPage-Cu5H9cQ0.js → TeamsPage-tqUnpZ15.js} +1 -1
  92. package/web-app/dist/assets/{TemplatesPage-3wdbVwil.js → TemplatesPage-ClB5EpI3.js} +1 -1
  93. package/web-app/dist/assets/{TerminalOutput-CWn301xh.js → TerminalOutput-CBvpYfEu.js} +1 -1
  94. package/web-app/dist/assets/{activity-YGaAt4zJ.js → activity-C1s0L_X9.js} +1 -1
  95. package/web-app/dist/assets/{bell-C28q0GO7.js → bell-DMEiXr73.js} +1 -1
  96. package/web-app/dist/assets/{bot-DLcQGI92.js → bot-BF_xIOxu.js} +1 -1
  97. package/web-app/dist/assets/{check-w0sBNlcm.js → check-Dv1Ynvrs.js} +1 -1
  98. package/web-app/dist/assets/{chevron-left-W7O0vC_m.js → chevron-left-Ddhsod7S.js} +1 -1
  99. package/web-app/dist/assets/{circle-alert-Cxjq9fxB.js → circle-alert-OATHUkBZ.js} +1 -1
  100. package/web-app/dist/assets/{clock-C8EqzmNl.js → clock-C1Ltva07.js} +1 -1
  101. package/web-app/dist/assets/{cloud-xNgp4g3Y.js → cloud-B-S4oOy9.js} +1 -1
  102. package/web-app/dist/assets/{code-xml-B-lyNDLJ.js → code-xml-D3Femn_F.js} +1 -1
  103. package/web-app/dist/assets/{database-DoW5lmWc.js → database-DBSeFgO3.js} +1 -1
  104. package/web-app/dist/assets/{dollar-sign-BcARA5sC.js → dollar-sign-sgDgNuGE.js} +1 -1
  105. package/web-app/dist/assets/{file-code-corner-CehcII0J.js → file-code-corner-C-MbdZEc.js} +1 -1
  106. package/web-app/dist/assets/{file-plus-DI9Zo5hP.js → file-plus-CCB6yqXv.js} +1 -1
  107. package/web-app/dist/assets/{globe-CiZWBk3H.js → globe-DlbrNAj1.js} +1 -1
  108. package/web-app/dist/assets/{hammer-3qv_T2N-.js → hammer-B-DWm695.js} +1 -1
  109. package/web-app/dist/assets/{index-gF9grAz-.js → index-Dvny-1SU.js} +3 -3
  110. package/web-app/dist/assets/{layers-CN7d0By9.js → layers-99qsKpOT.js} +1 -1
  111. package/web-app/dist/assets/{loader-circle-DywcM-im.js → loader-circle-ZvZyZl3J.js} +1 -1
  112. package/web-app/dist/assets/{lock-_hdSmkU4.js → lock-DosGjcxP.js} +1 -1
  113. package/web-app/dist/assets/{package-BV5zgqxJ.js → package-C-PumesN.js} +1 -1
  114. package/web-app/dist/assets/{plus-CU_Hh4-x.js → plus-CTYYskTB.js} +1 -1
  115. package/web-app/dist/assets/{refresh-cw-DMK-fNl-.js → refresh-cw-qW2ztTFS.js} +1 -1
  116. package/web-app/dist/assets/{rotate-ccw-CEB3_v5_.js → rotate-ccw-ZIKrf-vO.js} +1 -1
  117. package/web-app/dist/assets/{scroll-text-BSdXxcRN.js → scroll-text-BamlXjAT.js} +1 -1
  118. package/web-app/dist/assets/{server-CAMlP1yJ.js → server-BljXb79I.js} +1 -1
  119. package/web-app/dist/assets/{shield-alert-B-r1Dt3h.js → shield-alert-Dr6eT9c_.js} +1 -1
  120. package/web-app/dist/assets/{thumbs-up-CYfxw9o4.js → thumbs-up-v3oBXZgU.js} +1 -1
  121. package/web-app/dist/assets/{trash-2-DX3nGPS8.js → trash-2-BOuIrCpi.js} +1 -1
  122. package/web-app/dist/assets/{trending-down-Bt-hSntd.js → trending-down-Dp7jMCgH.js} +1 -1
  123. package/web-app/dist/assets/{trending-up-DVSlvQp8.js → trending-up-CDiur-nP.js} +1 -1
  124. package/web-app/dist/assets/{upload-D45RRHAP.js → upload-DxIHXhNE.js} +1 -1
  125. package/web-app/dist/assets/{usePolling-fq_Xj8iw.js → usePolling-D2Uu-YZb.js} +1 -1
  126. package/web-app/dist/assets/{user-BWcJkLlr.js → user-BVWCUAfF.js} +1 -1
  127. package/web-app/dist/index.html +1 -1
  128. package/packages/control-plane/src/server/actions.ts +0 -60
  129. package/packages/control-plane/ui/dist/assets/index-CUQ65vJY.css +0 -1
  130. package/packages/control-plane/ui/dist/assets/index-CwwGfp6E.js +0 -129
@@ -49,9 +49,41 @@ export const runs = sqliteTable("runs", {
49
49
  conflict: integer("conflict").notNull().default(0),
50
50
  groupId: text("group_id"),
51
51
  unitId: text("unit_id"),
52
+ attested: integer("attested"), // EL-FC08b: null = never evaluated (reads as unattested)
53
+ sigChecked: integer("sig_checked"), // log seal signature verified against a configured key
54
+ integrityKeyFp: text("integrity_key_fp"), // key-set fingerprint the integrity result was computed under; a change triggers a boot recompute
55
+ integrityReasons: text("integrity_reasons"), // JSON string[]
52
56
  }, (t) => [
53
57
  primaryKey({ columns: [t.sourceId, t.runId] }),
54
58
  index("runs_started").on(t.startedAt),
55
59
  index("runs_verdict").on(t.verdict),
56
60
  index("runs_group").on(t.groupId),
57
61
  ]);
62
+
63
+ // Append-only record of destructive operator actions (run removal, prune). Written before the delete runs.
64
+ export const audit = sqliteTable("audit", {
65
+ id: integer("id").primaryKey({ autoIncrement: true }),
66
+ ts: text("ts").notNull(),
67
+ action: text("action").notNull(),
68
+ actor: text("actor").notNull(),
69
+ detail: text("detail", { mode: "json" }).notNull(),
70
+ });
71
+
72
+ // LOCAL-ONLY: written by local discovery (shipper/discover.ts), never by /v1/ingest. realpath is never returned over the API.
73
+ export const localRepos = sqliteTable("local_repos", {
74
+ sourceId: text("source_id").primaryKey(),
75
+ realpath: text("realpath").notNull(),
76
+ name: text("name").notNull(),
77
+ discoveredAt: text("discovered_at").notNull(),
78
+ });
79
+
80
+ // Audit trail of UI-initiated actions (start, stop, retry, resume, answer, config write).
81
+ export const actions = sqliteTable("actions", {
82
+ id: integer("id").primaryKey({ autoIncrement: true }),
83
+ ts: text("ts").notNull(),
84
+ actor: text("actor").notNull(),
85
+ kind: text("kind").notNull(),
86
+ target: text("target"),
87
+ result: text("result").notNull(),
88
+ detail: text("detail"),
89
+ }, (t) => [index("actions_ts").on(t.ts)]);
@@ -4,10 +4,14 @@ import { Hono, type Context } from "hono";
4
4
  import { openDb } from "../db/migrate.ts";
5
5
  import { ingest } from "./ingest.ts";
6
6
  import { defaultAnswerDir, writeAnswer } from "./answer.ts";
7
- import { listRuns, runDetail } from "./runs.ts";
8
- import { hostGuard, isLoopbackHost, tokenGuard } from "./auth.ts";
7
+ import { listRuns, recomputeLegacy, runDetail } from "./runs.ts";
8
+ import { hostGuard, isLoopbackHost, peerIsLoopback, tokenGuard } from "./auth.ts";
9
9
  import { backfill } from "../shipper/backfill.ts";
10
- import { planStart, registryRepos, spawnStart } from "./actions.ts";
10
+ import { removeRun } from "../db/prune.ts";
11
+ import type { spawnStart } from "./spawn.ts";
12
+ import { syncLocalRepos } from "./repos.ts";
13
+ import { registerRoutes } from "./routes/index.ts";
14
+ import { legacyShim } from "./legacy/shim.ts";
11
15
 
12
16
  const MAX_BODY = 1_000_000;
13
17
 
@@ -18,6 +22,7 @@ const defaultUiDir = () => [join(import.meta.dir, "../../ui/dist"), join(import.
18
22
  export function createApp(opts: { dbPath: string; uiDir?: string; answerDir?: string; token?: string; loopbackOnly?: boolean; repoDir?: string; startBin?: string; spawnImpl?: typeof spawnStart }) {
19
23
  const uiDir = opts.uiDir ?? defaultUiDir();
20
24
  const { db, sqlite } = openDb(opts.dbPath);
25
+ recomputeLegacy(db);
21
26
  let ready = true;
22
27
  const answerDir = opts.answerDir ?? defaultAnswerDir();
23
28
  const app = new Hono();
@@ -71,13 +76,8 @@ export function createApp(opts: { dbPath: string; uiDir?: string; answerDir?: st
71
76
  // Machine-touching actions exist ONLY on a loopback-bound server (otherwise they are never registered: 404).
72
77
  // Each request must also come from a loopback peer (the real socket address, not the spoofable Host header; unknown peer fails closed) and carry JSON (blocks cross-site form posts).
73
78
  const act = opts.loopbackOnly ? app : new Hono();
74
- const peerIsLoopback = (c: Context): boolean => {
75
- const ip = (c.env as { requestIP?: (r: Request) => { address?: string } | null } | undefined)?.requestIP?.(c.req.raw)?.address;
76
- return typeof ip === "string" && /^(127\.\d+\.\d+\.\d+|::1|::ffff:127\.\d+\.\d+\.\d+)$/.test(ip);
77
- };
78
79
  const local = (c: Context) => peerIsLoopback(c) && isLoopbackHost(c.req.header("host")) && (c.req.header("content-type") ?? "").toLowerCase().startsWith("application/json");
79
80
  const repoDir = opts.repoDir ?? process.cwd();
80
- const inflight = new Set<string>();
81
81
  act.post("/v1/import", async (c) => {
82
82
  if (!local(c)) return c.json({ error: "loopback JSON requests only" }, 403);
83
83
  const r = await backfill({
@@ -87,21 +87,26 @@ export function createApp(opts: { dbPath: string; uiDir?: string; answerDir?: st
87
87
  });
88
88
  return c.json({ runs: r.runs, sent: r.sent, failed: r.failed });
89
89
  });
90
- act.get("/v1/repos", (c) => peerIsLoopback(c) ? c.json({ repos: [...new Set<string>([repoDir, ...registryRepos()])] }) : c.json({ error: "loopback only" }, 403));
91
- act.post("/v1/start", async (c) => {
92
- if (!local(c)) return c.json({ error: "loopback JSON requests only" }, 403);
93
- const text = await c.req.text();
94
- if (text.length > 20_000) return c.json({ error: "body too large" }, 413);
95
- let body: unknown;
96
- try { body = JSON.parse(text); } catch { return c.json({ error: "invalid JSON" }, 400); }
97
- const plan = planStart(body, [repoDir, ...registryRepos()], opts.startBin);
98
- if (!plan.ok) return c.json({ error: plan.error }, 400);
99
- if (inflight.has(plan.cwd)) return c.json({ error: "a run is already starting or running in this repo" }, 409);
100
- inflight.add(plan.cwd);
101
- const r = await (opts.spawnImpl ?? spawnStart)(plan.argv, plan.cwd, () => inflight.delete(plan.cwd));
102
- if ("error" in r) { inflight.delete(plan.cwd); return c.json({ error: r.error }, 500); }
103
- return c.json({ ok: true, pid: r.pid, command: plan.argv.slice(1).join(" ") });
90
+ // Remove one run (and its events). Loopback-only like /v1/start (never registered on a non-loopback server); bearer via tokenGuard when a token is set. JSON content type blocks cross-site form posts;
91
+ // a browser Origin must match the host this server is served on. Absent Origin (curl, the CLI) is allowed. The audit row is written inside the delete transaction, before the rows go.
92
+ act.delete("/v1/runs/:source/:run", async (c) => {
93
+ if (!peerIsLoopback(c) || !isLoopbackHost(c.req.header("host"))) return c.json({ error: "loopback only" }, 403);
94
+ if (!(c.req.header("content-type") ?? "").toLowerCase().startsWith("application/json")) return c.json({ error: "content-type must be application/json" }, 400);
95
+ const origin = c.req.header("origin");
96
+ if (origin !== undefined) {
97
+ let oh = "";
98
+ try { oh = new URL(origin).host.toLowerCase(); } catch { /* refused below */ }
99
+ if (!oh || oh !== (c.req.header("host") ?? "").toLowerCase()) return c.json({ error: "origin not allowed" }, 403);
100
+ }
101
+ const source = c.req.param("source"), run = c.req.param("run");
102
+ const r = removeRun(sqlite, source, run);
103
+ if (!r) return c.json({ error: "run not found" }, 404);
104
+ const left = (sqlite.query("select count(*) n from runs").get() as { n: number }).n;
105
+ return c.json({ ok: true, removed: r, remaining_runs: left });
104
106
  });
107
+ // Local discovery fills local_repos (never /v1/ingest). GET /v1/repos (names only, loopback guard) is mounted by routes/index.ts.
108
+ syncLocalRepos(db, repoDir);
109
+ registerRoutes({ app, act, db, repoDir, token: opts.token, peerIsLoopback, local, startBin: opts.startBin, spawnImpl: opts.spawnImpl, answerDir });
105
110
  // :id is `source:run` (run ids never contain a colon)
106
111
  app.get("/v1/runs/:id", (c) => {
107
112
  const id = c.req.param("id");
@@ -109,6 +114,9 @@ export function createApp(opts: { dbPath: string; uiDir?: string; answerDir?: st
109
114
  return i < 0 ? c.json({ error: "id must be source:run" }, 404) : detail(c, id.slice(0, i), id.slice(i + 1));
110
115
  });
111
116
 
117
+ // Legacy dashboard paths (CPE-24) are answered by the shim; mounted before the SPA fallback so index.html never shadows them.
118
+ app.route("/", legacyShim({ db, token: opts.token, repoDir }));
119
+
112
120
  // Static UI with SPA fallback. Registered last so /v1, /health and /ready always win.
113
121
  app.get("*", (c) => {
114
122
  let p: string;
@@ -0,0 +1,15 @@
1
+ // Audit helper: one row per UI-initiated action. Never throws (an audit failure must not break the action) and never stores secrets.
2
+ import type { Db } from "../db/migrate.ts";
3
+ import { actions } from "../db/schema.ts";
4
+
5
+ export type AuditEntry = { actor?: string; kind: string; target?: string; result: string; detail?: string };
6
+
7
+ const clip = (s: string | undefined, n: number): string | null => (s === undefined ? null : s.slice(0, n));
8
+
9
+ /** Records an action. Returns true when the row was written. */
10
+ export function audit(db: Db, e: AuditEntry): boolean {
11
+ try {
12
+ db.insert(actions).values({ ts: new Date().toISOString(), actor: e.actor ?? "local", kind: e.kind, target: clip(e.target, 500), result: e.result, detail: clip(e.detail, 2000) }).run();
13
+ return true;
14
+ } catch { return false; }
15
+ }
@@ -1,6 +1,6 @@
1
1
  // Control Plane auth: bearer token on /v1, Host allowlist on loopback (DNS rebinding), bind refusal without a token.
2
2
  import { createHash, timingSafeEqual } from "node:crypto";
3
- import type { MiddlewareHandler } from "hono";
3
+ import type { Context, MiddlewareHandler } from "hono";
4
4
 
5
5
  const LOOPBACK_BINDS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
6
6
 
@@ -9,6 +9,12 @@ export function isLoopbackHost(host: string | undefined | null): boolean {
9
9
  return !!host && /^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/i.test(host);
10
10
  }
11
11
 
12
+ /** True when the real socket peer (not the spoofable Host header) is loopback. An unknown peer fails closed. */
13
+ export function peerIsLoopback(c: Context): boolean {
14
+ const ip = (c.env as { requestIP?: (r: Request) => { address?: string } | null } | undefined)?.requestIP?.(c.req.raw)?.address;
15
+ return typeof ip === "string" && /^(127\.\d+\.\d+\.\d+|::1|::ffff:127\.\d+\.\d+\.\d+)$/.test(ip);
16
+ }
17
+
12
18
  /** Constant-time check of an Authorization header against the token. Hashing first makes the compare length-safe. */
13
19
  export function tokenMatches(header: string | undefined | null, token: string): boolean {
14
20
  const m = /^Bearer (.+)$/i.exec(header ?? "");
@@ -0,0 +1,147 @@
1
+ // EL-FC08b (D86, FC-08, L2): integrity of an INGESTED event log, run on every rebuild of a run's projection.
2
+ // Pure: no db. The same checks `loki verify` makes on events.jsonl (loki-ts/src/engine10/verify_cmd.ts: checkEventLog, checkLogSeal),
3
+ // restated over stored envelopes. Option taken for sharing: a CP-local module plus test/server/integrity-parity.test.ts, which runs both
4
+ // implementations over the same fixtures, including signed ones (unknown kid, mismatched kid, appended run.completed), so they cannot drift.
5
+ // TAMPERED means positive evidence of forgery. A log the CP cannot check (redacted before ingest) is UNVERIFIED, never TAMPERED and never VERIFIED.
6
+ import { createHash, createPublicKey, verify, type KeyObject } from "node:crypto";
7
+ import { readFileSync } from "node:fs";
8
+ import type { EventEnvelope } from "../../../../loki-ts/src/engine10/types.ts";
9
+
10
+ export interface RunIntegrity {
11
+ /** Positive evidence of forgery, loss or edit. Always shown as TAMPERED. */
12
+ tampered: boolean;
13
+ /** A VERIFIED claim backed by a signed receipt.sealed and a sealing line, with a log that could be checked. False means unattested (shown UNVERIFIED). */
14
+ attested: boolean;
15
+ /** The sealing line's signature was verified against a configured public key. False: shape and hash chain only. */
16
+ sig_checked: boolean;
17
+ reasons: string[];
18
+ }
19
+ export type KeyResolver = ((kid: string) => KeyObject | undefined) & { configured?: boolean; fingerprint?: string };
20
+ export interface IntegrityOpts {
21
+ pubkeyFor?: KeyResolver;
22
+ /** At least one key is configured: an unknown kid is then TAMPERED, as in the engine's checkLogSeal. Defaults to pubkeyFor.configured. */
23
+ keysConfigured?: boolean;
24
+ }
25
+
26
+ const SHA = /^[0-9a-f]{64}$/;
27
+ const SEAL_LINE = "log.sealed";
28
+ const REDACTED = /\[REDACTED(?::[A-Z_]+)?\]/;
29
+ export const REDACTED_NOTE = "log redacted before ingest; seal not checkable";
30
+ export const UNCHECKED_SIG = "VERIFIED (signature not checked)";
31
+ export const UNATTESTED_SUFFIX = " (unattested)";
32
+ /** Outcomes that claim success: each needs a signed seal, exactly like VERIFIED. */
33
+ export const SUCCESS_VERDICTS: ReadonlySet<string> = new Set(["VERIFIED", "ALREADY_SATISFIED"]);
34
+ const normVerdict = (v: string | null | undefined): string | null => (typeof v === "string" ? v.trim().toUpperCase() : null);
35
+
36
+ /** Same derivation as loki-ts/src/engine10/stages/seal.ts kidOf (parity-tested). */
37
+ export const kidOf = (pub: KeyObject): string => createHash("sha256").update(`{"crv":"Ed25519","kty":"OKP","x":"${pub.export({ format: "jwk" }).x}"}`).digest("base64url");
38
+
39
+ /** Public keys from LOKI_CP_RECEIPT_PUBKEYS (colon-separated PEM files, like `loki verify` retired keys), by kid. Unreadable files are skipped. */
40
+ export function pubkeysFromEnv(env: Record<string, string | undefined> = process.env): KeyResolver {
41
+ const keys = (env["LOKI_CP_RECEIPT_PUBKEYS"] ?? "").split(":").map((f) => f.trim()).filter(Boolean).flatMap((f) => {
42
+ try { return [createPublicKey(readFileSync(f))]; } catch { return []; }
43
+ });
44
+ const fn: KeyResolver = (kid) => keys.find((k) => kidOf(k) === kid);
45
+ fn.configured = keys.length > 0;
46
+ fn.fingerprint = createHash("sha256").update(keys.map(kidOf).sort().join(",")).digest("hex").slice(0, 16); // identifies the key set a row was judged under
47
+ return fn;
48
+ }
49
+
50
+ // The engine writes JSON.stringify(envelope) in this key order, so a stored envelope re-serialises to the bytes it was hashed as.
51
+ const line = (e: EventEnvelope): string => JSON.stringify({ v: e.v, seq: e.seq, ts: e.ts, run: e.run, type: e.type, stage: e.stage, data: e.data });
52
+ const verdictOf = (e: EventEnvelope | undefined): string | null => (typeof e?.data["verdict"] === "string" ? e.data["verdict"].trim().toUpperCase() : null);
53
+
54
+ /** Events up to and including the sealing line (the authenticated prefix); everything when the run has none. Lines after it are unauthenticated. */
55
+ export function sealedPrefix<T extends { type: string }>(evs: readonly T[]): T[] {
56
+ const i = evs.findIndex((e) => e.type === SEAL_LINE);
57
+ return i < 0 ? [...evs] : evs.slice(0, i + 1);
58
+ }
59
+
60
+ export function verifyRunIntegrity(evs: readonly EventEnvelope[], opts: IntegrityOpts = {}): RunIntegrity {
61
+ const bad: string[] = [], notes: string[] = [];
62
+ const doneAt = evs.findIndex((e) => e.type === "run.completed");
63
+ const sealedAt = evs.map((e, i) => (e.type === "receipt.sealed" ? i : -1)).filter((i) => i >= 0);
64
+ const finalised = doneAt >= 0 || sealedAt.length > 0;
65
+ const lines = evs.map(line);
66
+ const keysConfigured = opts.keysConfigured ?? opts.pubkeyFor?.configured ?? false;
67
+ let sigChecked = false;
68
+
69
+ if (finalised) { // a live run may still be missing a batch; once it claims an end, every seq must be present
70
+ const gap = evs.findIndex((e, i) => e.seq !== i);
71
+ if (gap >= 0) bad.push(`event log has a seq gap, duplicate or missing start at position ${gap} (seq ${evs[gap]!.seq})`);
72
+ }
73
+ if (evs.some((e) => e.type === "tamper.detected")) bad.push("the supervisor detected events.jsonl being modified during the run");
74
+ const firstSeal = evs.findIndex((e) => e.type === SEAL_LINE);
75
+ if (firstSeal >= 0 && evs.slice(firstSeal + 1).some((e) => e.type === "run.completed" || e.type === "receipt.sealed" || typeof e.data["verdict"] === "string")) bad.push("a verdict-bearing event after log.sealed is unauthenticated");
76
+ if (evs.filter((e) => e.type === "run.completed").length > 1) bad.push("more than one run.completed (a verdict appended after the seal is unauthenticated)");
77
+
78
+ // Hash problems are tamper evidence unless a redaction placeholder explains the changed bytes.
79
+ const hashBad: string[] = [];
80
+ const redacted = REDACTED.test(JSON.stringify(doneAt >= 0 ? evs.slice(0, doneAt + 2).map((e) => e.data) : evs.map((e) => e.data)));
81
+
82
+ for (const i of sealedAt) {
83
+ const d = evs[i]!.data;
84
+ if (typeof d["receipt_sha256"] !== "string" || !SHA.test(d["receipt_sha256"])) hashBad.push(`receipt.sealed at seq ${evs[i]!.seq} has no valid receipt_sha256`);
85
+ const bound = d["events_sha256"]; // optional on the event: the engine keeps it in receipt.json, which the CP never sees
86
+ if (typeof bound === "string") {
87
+ const h = createHash("sha256");
88
+ let ok = false;
89
+ for (const l of lines) { if (h.copy().digest("hex") === bound) { ok = true; break; } h.update(l + "\n"); }
90
+ if (!ok && h.digest("hex") === bound) ok = true;
91
+ if (!ok) hashBad.push("receipt.sealed events_sha256 does not match any prefix of the ingested log");
92
+ }
93
+ }
94
+
95
+ const sealLines = evs.map((e, i) => (e.type === SEAL_LINE ? i : -1)).filter((i) => i >= 0);
96
+ if (sealLines.some((i) => i !== doneAt + 1)) bad.push(`${SEAL_LINE} is not the line right after run.completed`);
97
+ const ls = doneAt >= 0 && evs[doneAt + 1]?.type === SEAL_LINE ? evs[doneAt + 1]!.data : null;
98
+ const signedSeals = sealedAt.map((i) => evs[i]!.data).filter((d) => d["signed"] === true);
99
+ const signedReceipt = signedSeals.length > 0;
100
+ if (ls) {
101
+ const sha = createHash("sha256").update(lines.slice(0, doneAt + 1).join("\n") + "\n").digest("hex");
102
+ const sig = typeof ls["sig"] === "string" ? Buffer.from(ls["sig"], "base64url") : null;
103
+ const kid = ls["kid"];
104
+ if (ls["tampered"] !== false) bad.push(`${SEAL_LINE} line records a tamper or is malformed`);
105
+ else if (typeof kid !== "string" || !sig || sig.length !== 64) hashBad.push(`${SEAL_LINE} line is malformed or records a tamper`);
106
+ else {
107
+ if (signedSeals.some((d) => typeof d["kid"] === "string" && d["kid"] !== kid)) hashBad.push(`${SEAL_LINE} kid differs from the receipt kid`);
108
+ if (ls["events_sha256"] !== sha) hashBad.push(`${SEAL_LINE} events_sha256 does not match the ingested log`);
109
+ else {
110
+ const pub = opts.pubkeyFor?.(kid);
111
+ if (pub) { sigChecked = true; if (!verify(null, Buffer.from(`${sha}:false`), pub, sig)) bad.push(`${SEAL_LINE} signature does not verify`); }
112
+ else if (keysConfigured) bad.push(`${SEAL_LINE} kid ${kid} has no configured public key`);
113
+ }
114
+ }
115
+ } else if (signedReceipt && doneAt >= 0) bad.push(`signed receipt but no ${SEAL_LINE} line after run.completed`);
116
+
117
+ let unverifiable = false;
118
+ if (hashBad.length > 0) {
119
+ if (redacted) { unverifiable = true; sigChecked = false; notes.push(REDACTED_NOTE); } else bad.push(...hashBad);
120
+ }
121
+
122
+ // A VERIFIED claim (first run.completed only) needs a receipt sealed before it, for the same verdict.
123
+ let claimsVerified = false;
124
+ const claimed = doneAt >= 0 ? verdictOf(evs[doneAt]) : null;
125
+ if (claimed !== null && SUCCESS_VERDICTS.has(claimed)) {
126
+ claimsVerified = true;
127
+ const before = sealedAt.filter((i) => i < doneAt);
128
+ if (before.length === 0) bad.push(`run.completed claims ${claimed} but no receipt.sealed precedes it`);
129
+ else if (!before.some((i) => verdictOf(evs[i]) === claimed)) bad.push(`run.completed claims ${claimed} but the sealed receipt does not`);
130
+ }
131
+
132
+ const tampered = bad.length > 0;
133
+ const attested = !tampered && !unverifiable && (!claimsVerified || (signedReceipt && ls !== null));
134
+ return { tampered, attested, sig_checked: sigChecked && !tampered, reasons: [...bad, ...notes] };
135
+ }
136
+
137
+ /** The one display verdict (FC-08). TAMPERED when the log failed integrity. When it is not attested, a success claim shows UNVERIFIED and any other verdict carries an
138
+ * "(unattested)" marker. An attested success whose seal no key checked shows "<verdict> (signature not checked)"; plain VERIFIED only for a signature-checked run.
139
+ * sig_checked undefined (older captures) reads as checked. */
140
+ export function effectiveVerdict(r: { verdict: string | null; tampered: boolean; attested?: boolean; sig_checked?: boolean }): string | null {
141
+ if (r.tampered) return "TAMPERED";
142
+ const v = normVerdict(r.verdict);
143
+ if (v === null) return r.verdict;
144
+ if (r.attested === false) return SUCCESS_VERDICTS.has(v) ? "UNVERIFIED" : `${r.verdict}${UNATTESTED_SUFFIX}`;
145
+ if (SUCCESS_VERDICTS.has(v)) return r.sig_checked === false ? `${v} (signature not checked)` : v;
146
+ return r.verdict;
147
+ }
@@ -0,0 +1,18 @@
1
+ // CPE24-P1: legacy (port 57374) mappings for metrics, cost and fleet spend. Each reads the same source as its /v1 route (routes/metrics.ts,
2
+ // routes/cost_ledger.ts, routes/fleet.ts) and keeps the legacy response shape. The shim's guard (legacyGuard) has already run.
3
+ import type { Context } from "hono";
4
+ import { costSnapshot, costTimeline, lokiDirOf } from "../routes/cost_ledger.ts";
5
+ import { fleetRuns, fleetSummary, parseInactive } from "../routes/fleet.ts";
6
+ import { buildMetricsText } from "../routes/metrics.ts";
7
+
8
+ export function metricsMapped(repoDir: string): Record<string, (c: Context) => Response> {
9
+ const inactive = (c: Context) => parseInactive(c.req.query("include_inactive"));
10
+ return {
11
+ "GET /api/cost": (c) => c.json(costSnapshot(lokiDirOf(repoDir))),
12
+ "GET /api/cost/timeline": (c) => c.json(costTimeline(lokiDirOf(repoDir))),
13
+ "GET /metrics": (c) => c.text(buildMetricsText(repoDir), 200, { "content-type": "text/plain; charset=utf-8" }),
14
+ // Legacy rows carry the project path; the guarded shim serves only authenticated or loopback callers, like legacy.
15
+ "GET /api/fleet/runs": (c) => c.json(fleetRuns(inactive(c))),
16
+ "GET /api/fleet/summary": (c) => c.json(fleetSummary(inactive(c))),
17
+ };
18
+ }
@@ -0,0 +1,237 @@
1
+ // GENERATED from docs/v10/CP-ENTERPRISE-UI.md section 5.2 (one row per legacy dashboard route that the shim answers).
2
+ // test/server/legacy-shim.test.ts asserts this table and the doc table agree. Regenerate both together, never one alone.
3
+ export type LegacyAction = "map" | "308" | "410" | "501";
4
+ export interface LegacyRoute { method: string; path: string; action: LegacyAction; open: boolean; migrate: string }
5
+
6
+ export const LEGACY_ROUTES: readonly LegacyRoute[] = [
7
+ { method: "GET", path: "/openapi.json", action: "410", open: true, migrate: "" },
8
+ { method: "GET", path: "/docs", action: "410", open: true, migrate: "" },
9
+ { method: "GET", path: "/docs/oauth2-redirect", action: "410", open: true, migrate: "" },
10
+ { method: "GET", path: "/redoc", action: "410", open: true, migrate: "" },
11
+ { method: "POST", path: "/api/v2/tenants", action: "501", open: false, migrate: "" },
12
+ { method: "GET", path: "/api/v2/tenants", action: "501", open: false, migrate: "" },
13
+ { method: "GET", path: "/api/v2/tenants/{tenant_id}", action: "501", open: false, migrate: "" },
14
+ { method: "PUT", path: "/api/v2/tenants/{tenant_id}", action: "410", open: false, migrate: "" },
15
+ { method: "DELETE", path: "/api/v2/tenants/{tenant_id}", action: "501", open: false, migrate: "" },
16
+ { method: "GET", path: "/api/v2/tenants/{tenant_id}/projects", action: "410", open: false, migrate: "" },
17
+ { method: "POST", path: "/api/v2/runs", action: "501", open: false, migrate: "" },
18
+ { method: "GET", path: "/api/v2/runs", action: "map", open: false, migrate: "/v1/runs" },
19
+ { method: "GET", path: "/api/v2/runs/{run_id}", action: "map", open: false, migrate: "/v1/runs/:id" },
20
+ { method: "POST", path: "/api/v2/runs/{run_id}/cancel", action: "501", open: false, migrate: "" },
21
+ { method: "POST", path: "/api/v2/runs/{run_id}/replay", action: "501", open: false, migrate: "" },
22
+ { method: "GET", path: "/api/v2/runs/{run_id}/timeline", action: "map", open: false, migrate: "/v1/runs/:id" },
23
+ { method: "POST", path: "/api/v2/api-keys", action: "501", open: false, migrate: "" },
24
+ { method: "GET", path: "/api/v2/api-keys", action: "501", open: false, migrate: "" },
25
+ { method: "GET", path: "/api/v2/api-keys/{identifier}", action: "410", open: false, migrate: "" },
26
+ { method: "PUT", path: "/api/v2/api-keys/{identifier}", action: "410", open: false, migrate: "" },
27
+ { method: "DELETE", path: "/api/v2/api-keys/{identifier}", action: "501", open: false, migrate: "" },
28
+ { method: "POST", path: "/api/v2/api-keys/{identifier}/rotate", action: "501", open: false, migrate: "" },
29
+ { method: "GET", path: "/api/v2/policies", action: "410", open: false, migrate: "" },
30
+ { method: "PUT", path: "/api/v2/policies", action: "410", open: false, migrate: "" },
31
+ { method: "POST", path: "/api/v2/policies/evaluate", action: "410", open: false, migrate: "" },
32
+ { method: "GET", path: "/api/v2/audit", action: "map", open: false, migrate: "/v1/audit" },
33
+ { method: "GET", path: "/api/v2/audit/verify", action: "501", open: false, migrate: "" },
34
+ { method: "GET", path: "/api/v2/audit/export", action: "410", open: false, migrate: "" },
35
+ { method: "GET", path: "/api/operator/runs/{run_id}", action: "410", open: false, migrate: "" },
36
+ { method: "GET", path: "/api/operator/tests", action: "410", open: false, migrate: "" },
37
+ { method: "GET", path: "/api/operator/receipts", action: "410", open: false, migrate: "" },
38
+ { method: "GET", path: "/api/operator/releases", action: "410", open: false, migrate: "" },
39
+ { method: "GET", path: "/api/operator/phases", action: "410", open: false, migrate: "" },
40
+ { method: "GET", path: "/api/operator/workspaces/runs", action: "410", open: false, migrate: "" },
41
+ { method: "GET", path: "/api/operator/workspaces/runs/{ws}/{run_id}", action: "410", open: false, migrate: "" },
42
+ { method: "GET", path: "/api/onboarding/state", action: "410", open: false, migrate: "" },
43
+ { method: "POST", path: "/api/onboarding/provider", action: "410", open: false, migrate: "" },
44
+ { method: "POST", path: "/api/onboarding/github", action: "410", open: false, migrate: "" },
45
+ { method: "GET", path: "/api/onboarding/repos", action: "410", open: false, migrate: "" },
46
+ { method: "POST", path: "/api/onboarding/repo", action: "410", open: false, migrate: "" },
47
+ { method: "GET", path: "/api/backlog/issues", action: "410", open: false, migrate: "" },
48
+ { method: "POST", path: "/api/backlog/run", action: "410", open: false, migrate: "" },
49
+ { method: "GET", path: "/api/backlog/status", action: "410", open: false, migrate: "" },
50
+ { method: "GET", path: "/api/v1/runs", action: "410", open: false, migrate: "" },
51
+ { method: "GET", path: "/api/v1/runs/{run_id}", action: "410", open: false, migrate: "" },
52
+ { method: "POST", path: "/api/v1/runs", action: "410", open: false, migrate: "" },
53
+ { method: "POST", path: "/api/v1/runs/{run_id}/stop", action: "410", open: false, migrate: "" },
54
+ { method: "GET", path: "/start", action: "308", open: false, migrate: "/" },
55
+ { method: "MOUNT", path: "/lab", action: "308", open: true, migrate: "/" },
56
+ { method: "GET", path: "/api/providers/models", action: "410", open: true, migrate: "" },
57
+ { method: "GET", path: "/.well-known/agent.json", action: "map", open: true, migrate: "/v1/runs" },
58
+ { method: "GET", path: "/api/status", action: "map", open: false, migrate: "/v1/runs" },
59
+ { method: "GET", path: "/api/projects", action: "501", open: false, migrate: "" },
60
+ { method: "POST", path: "/api/projects", action: "501", open: false, migrate: "" },
61
+ { method: "GET", path: "/api/projects/{project_id}", action: "501", open: false, migrate: "" },
62
+ { method: "PUT", path: "/api/projects/{project_id}", action: "501", open: false, migrate: "" },
63
+ { method: "DELETE", path: "/api/projects/{project_id}", action: "501", open: false, migrate: "" },
64
+ { method: "GET", path: "/api/tasks", action: "501", open: false, migrate: "" },
65
+ { method: "POST", path: "/api/tasks", action: "501", open: false, migrate: "" },
66
+ { method: "GET", path: "/api/tasks/{task_id}", action: "501", open: false, migrate: "" },
67
+ { method: "PUT", path: "/api/tasks/{task_id}", action: "501", open: false, migrate: "" },
68
+ { method: "DELETE", path: "/api/tasks/{task_id}", action: "501", open: false, migrate: "" },
69
+ { method: "POST", path: "/api/tasks/{task_id}/move", action: "501", open: false, migrate: "" },
70
+ { method: "WS", path: "/ws", action: "501", open: true, migrate: "" },
71
+ { method: "GET", path: "/api/registry/projects", action: "410", open: false, migrate: "" },
72
+ { method: "POST", path: "/api/registry/projects", action: "501", open: false, migrate: "" },
73
+ { method: "GET", path: "/api/registry/projects/{identifier}", action: "410", open: false, migrate: "" },
74
+ { method: "DELETE", path: "/api/registry/projects/{identifier}", action: "410", open: false, migrate: "" },
75
+ { method: "GET", path: "/api/registry/projects/{identifier}/health", action: "410", open: false, migrate: "" },
76
+ { method: "POST", path: "/api/registry/projects/{identifier}/access", action: "410", open: false, migrate: "" },
77
+ { method: "GET", path: "/api/registry/discover", action: "410", open: false, migrate: "" },
78
+ { method: "POST", path: "/api/registry/sync", action: "501", open: false, migrate: "" },
79
+ { method: "GET", path: "/api/registry/tasks", action: "410", open: false, migrate: "" },
80
+ { method: "GET", path: "/api/registry/learnings", action: "410", open: false, migrate: "" },
81
+ { method: "GET", path: "/api/fleet/runs", action: "map", open: false, migrate: "/v1/fleet/runs" },
82
+ { method: "GET", path: "/api/fleet/summary", action: "map", open: false, migrate: "/v1/fleet/summary" },
83
+ { method: "GET", path: "/api/fleet/runs/{identifier}", action: "410", open: false, migrate: "" },
84
+ { method: "POST", path: "/api/focus", action: "501", open: false, migrate: "" },
85
+ { method: "GET", path: "/api/focus", action: "410", open: false, migrate: "" },
86
+ { method: "DELETE", path: "/api/focus", action: "410", open: false, migrate: "" },
87
+ { method: "GET", path: "/api/session/model", action: "410", open: false, migrate: "" },
88
+ { method: "POST", path: "/api/session/model", action: "501", open: false, migrate: "" },
89
+ { method: "GET", path: "/api/running-projects", action: "410", open: false, migrate: "" },
90
+ { method: "POST", path: "/api/control/start", action: "501", open: false, migrate: "" },
91
+ { method: "GET", path: "/api/control/builds/{execution_id}", action: "410", open: false, migrate: "" },
92
+ { method: "GET", path: "/api/control/builds/{execution_id}/proof", action: "410", open: false, migrate: "" },
93
+ { method: "POST", path: "/api/control/builds/{execution_id}/stop", action: "410", open: false, migrate: "" },
94
+ { method: "POST", path: "/api/running-projects/stop", action: "410", open: false, migrate: "" },
95
+ { method: "POST", path: "/api/fleet/runs/{identifier}/retry", action: "410", open: false, migrate: "" },
96
+ { method: "POST", path: "/api/fleet/runs/{identifier}/cancel", action: "410", open: false, migrate: "" },
97
+ { method: "GET", path: "/api/enterprise/status", action: "410", open: true, migrate: "" },
98
+ { method: "GET", path: "/api/auth/info", action: "410", open: true, migrate: "" },
99
+ { method: "POST", path: "/api/enterprise/tokens", action: "410", open: false, migrate: "" },
100
+ { method: "GET", path: "/api/enterprise/tokens", action: "410", open: false, migrate: "" },
101
+ { method: "DELETE", path: "/api/enterprise/tokens/{identifier}", action: "410", open: false, migrate: "" },
102
+ { method: "GET", path: "/api/enterprise/audit", action: "410", open: false, migrate: "" },
103
+ { method: "GET", path: "/api/enterprise/audit/summary", action: "410", open: false, migrate: "" },
104
+ { method: "GET", path: "/api/compliance", action: "410", open: false, migrate: "" },
105
+ { method: "GET", path: "/api/memory/summary", action: "410", open: false, migrate: "" },
106
+ { method: "GET", path: "/api/memory/episodes", action: "501", open: false, migrate: "" },
107
+ { method: "GET", path: "/api/memory/episodes/{episode_id}", action: "501", open: false, migrate: "" },
108
+ { method: "GET", path: "/api/memory/patterns", action: "501", open: false, migrate: "" },
109
+ { method: "GET", path: "/api/memory/patterns/{pattern_id}", action: "501", open: false, migrate: "" },
110
+ { method: "GET", path: "/api/memory/skills", action: "501", open: false, migrate: "" },
111
+ { method: "GET", path: "/api/memory/skills/{skill_id}", action: "410", open: false, migrate: "" },
112
+ { method: "GET", path: "/api/memory/economics", action: "501", open: false, migrate: "" },
113
+ { method: "POST", path: "/api/memory/consolidate", action: "501", open: false, migrate: "" },
114
+ { method: "POST", path: "/api/memory/retrieve", action: "501", open: false, migrate: "" },
115
+ { method: "GET", path: "/api/memory/index", action: "410", open: false, migrate: "" },
116
+ { method: "GET", path: "/api/memory/timeline", action: "410", open: false, migrate: "" },
117
+ { method: "GET", path: "/api/memory/files", action: "410", open: false, migrate: "" },
118
+ { method: "GET", path: "/api/memory/file", action: "410", open: false, migrate: "" },
119
+ { method: "GET", path: "/api/memory/search", action: "410", open: false, migrate: "" },
120
+ { method: "GET", path: "/api/memory/stats", action: "410", open: false, migrate: "" },
121
+ { method: "GET", path: "/api/learning/metrics", action: "501", open: false, migrate: "" },
122
+ { method: "GET", path: "/api/learning/trends", action: "410", open: false, migrate: "" },
123
+ { method: "GET", path: "/api/learning/signals", action: "410", open: false, migrate: "" },
124
+ { method: "GET", path: "/api/learning/aggregation", action: "410", open: false, migrate: "" },
125
+ { method: "POST", path: "/api/learning/aggregate", action: "501", open: false, migrate: "" },
126
+ { method: "GET", path: "/api/learning/preferences", action: "410", open: false, migrate: "" },
127
+ { method: "GET", path: "/api/learning/errors", action: "410", open: false, migrate: "" },
128
+ { method: "GET", path: "/api/learning/success", action: "410", open: false, migrate: "" },
129
+ { method: "GET", path: "/api/learning/tools", action: "410", open: false, migrate: "" },
130
+ { method: "POST", path: "/api/control/pause", action: "501", open: false, migrate: "" },
131
+ { method: "POST", path: "/api/control/resume", action: "501", open: false, migrate: "" },
132
+ { method: "POST", path: "/api/control/stop", action: "501", open: false, migrate: "" },
133
+ { method: "GET", path: "/api/cost", action: "map", open: false, migrate: "/v1/cost/snapshot" },
134
+ { method: "GET", path: "/api/budget", action: "501", open: false, migrate: "" },
135
+ { method: "GET", path: "/api/cost/timeline", action: "map", open: false, migrate: "/v1/cost/timeline" },
136
+ { method: "GET", path: "/api/trust/trajectory", action: "410", open: false, migrate: "" },
137
+ { method: "GET", path: "/api/gate-policy", action: "410", open: false, migrate: "" },
138
+ { method: "GET", path: "/api/pricing", action: "410", open: false, migrate: "" },
139
+ { method: "GET", path: "/api/council/state", action: "410", open: false, migrate: "" },
140
+ { method: "GET", path: "/api/council/verdicts", action: "410", open: false, migrate: "" },
141
+ { method: "GET", path: "/api/council/convergence", action: "410", open: false, migrate: "" },
142
+ { method: "GET", path: "/api/council/report", action: "410", open: false, migrate: "" },
143
+ { method: "POST", path: "/api/council/force-review", action: "501", open: false, migrate: "" },
144
+ { method: "GET", path: "/api/council/transcripts", action: "501", open: false, migrate: "" },
145
+ { method: "GET", path: "/api/council/transcripts/{iteration_id}", action: "410", open: false, migrate: "" },
146
+ { method: "GET", path: "/api/context", action: "501", open: false, migrate: "" },
147
+ { method: "GET", path: "/api/notifications", action: "410", open: false, migrate: "" },
148
+ { method: "GET", path: "/api/notifications/triggers", action: "410", open: false, migrate: "" },
149
+ { method: "PUT", path: "/api/notifications/triggers", action: "501", open: false, migrate: "" },
150
+ { method: "POST", path: "/api/notifications/{notification_id}/acknowledge", action: "410", open: false, migrate: "" },
151
+ { method: "POST", path: "/api/notifications/{notification_id}/unacknowledge", action: "410", open: false, migrate: "" },
152
+ { method: "GET", path: "/api/checkpoints", action: "501", open: false, migrate: "" },
153
+ { method: "GET", path: "/api/checkpoints/{checkpoint_id}", action: "410", open: false, migrate: "" },
154
+ { method: "POST", path: "/api/checkpoints", action: "501", open: false, migrate: "" },
155
+ { method: "POST", path: "/api/checkpoints/{checkpoint_id}/rollback", action: "501", open: false, migrate: "" },
156
+ { method: "GET", path: "/api/agents", action: "410", open: false, migrate: "" },
157
+ { method: "POST", path: "/api/agents/{agent_id}/kill", action: "501", open: false, migrate: "" },
158
+ { method: "POST", path: "/api/agents/{agent_id}/pause", action: "501", open: false, migrate: "" },
159
+ { method: "POST", path: "/api/agents/{agent_id}/resume", action: "501", open: false, migrate: "" },
160
+ { method: "GET", path: "/api/logs", action: "410", open: false, migrate: "" },
161
+ { method: "POST", path: "/api/collab/join", action: "410", open: false, migrate: "" },
162
+ { method: "POST", path: "/api/collab/leave", action: "410", open: false, migrate: "" },
163
+ { method: "GET", path: "/api/collab/users", action: "410", open: false, migrate: "" },
164
+ { method: "GET", path: "/api/collab/users/{user_id}", action: "410", open: false, migrate: "" },
165
+ { method: "POST", path: "/api/collab/users/{user_id}/heartbeat", action: "410", open: false, migrate: "" },
166
+ { method: "POST", path: "/api/collab/users/{user_id}/cursor", action: "410", open: false, migrate: "" },
167
+ { method: "POST", path: "/api/collab/users/{user_id}/status", action: "410", open: false, migrate: "" },
168
+ { method: "GET", path: "/api/collab/presence", action: "410", open: false, migrate: "" },
169
+ { method: "GET", path: "/api/collab/file/{file_path:path}", action: "410", open: false, migrate: "" },
170
+ { method: "GET", path: "/api/collab/state", action: "410", open: false, migrate: "" },
171
+ { method: "GET", path: "/api/collab/state/value", action: "410", open: false, migrate: "" },
172
+ { method: "POST", path: "/api/collab/operation", action: "410", open: false, migrate: "" },
173
+ { method: "POST", path: "/api/collab/sync", action: "410", open: false, migrate: "" },
174
+ { method: "GET", path: "/api/collab/history", action: "410", open: false, migrate: "" },
175
+ { method: "WS", path: "/ws/collab", action: "410", open: true, migrate: "" },
176
+ { method: "GET", path: "/api/collab/status", action: "410", open: false, migrate: "" },
177
+ { method: "GET", path: "/api/secrets/status", action: "410", open: false, migrate: "" },
178
+ { method: "GET", path: "/api/github/status", action: "410", open: false, migrate: "" },
179
+ { method: "GET", path: "/api/github/tasks", action: "410", open: false, migrate: "" },
180
+ { method: "GET", path: "/api/github/sync-log", action: "410", open: false, migrate: "" },
181
+ { method: "GET", path: "/api/health/processes", action: "410", open: false, migrate: "" },
182
+ { method: "GET", path: "/metrics", action: "map", open: false, migrate: "/v1/metrics" },
183
+ { method: "GET", path: "/api/checklist", action: "410", open: false, migrate: "" },
184
+ { method: "GET", path: "/api/usage", action: "410", open: false, migrate: "" },
185
+ { method: "GET", path: "/api/checklist/summary", action: "410", open: false, migrate: "" },
186
+ { method: "GET", path: "/api/prd-observations", action: "410", open: false, migrate: "" },
187
+ { method: "GET", path: "/api/checklist/waivers", action: "410", open: false, migrate: "" },
188
+ { method: "POST", path: "/api/checklist/waivers", action: "501", open: false, migrate: "" },
189
+ { method: "DELETE", path: "/api/checklist/waivers/{item_id}", action: "501", open: false, migrate: "" },
190
+ { method: "GET", path: "/api/council/gate", action: "501", open: false, migrate: "" },
191
+ { method: "GET", path: "/api/app-runner/status", action: "410", open: false, migrate: "" },
192
+ { method: "GET", path: "/api/app-runner/logs", action: "410", open: false, migrate: "" },
193
+ { method: "GET", path: "/api/app-runner/errors", action: "410", open: false, migrate: "" },
194
+ { method: "POST", path: "/api/control/app-restart", action: "501", open: false, migrate: "" },
195
+ { method: "POST", path: "/api/control/app-stop", action: "501", open: false, migrate: "" },
196
+ { method: "GET", path: "/api/playwright/results", action: "410", open: false, migrate: "" },
197
+ { method: "GET", path: "/api/playwright/screenshot", action: "410", open: false, migrate: "" },
198
+ { method: "GET", path: "/api/failures", action: "410", open: false, migrate: "" },
199
+ { method: "GET", path: "/api/prompt-versions", action: "410", open: false, migrate: "" },
200
+ { method: "POST", path: "/api/prompt-optimize", action: "501", open: false, migrate: "" },
201
+ { method: "GET", path: "/api/activity", action: "501", open: false, migrate: "" },
202
+ { method: "GET", path: "/api/session-diff", action: "410", open: false, migrate: "" },
203
+ { method: "POST", path: "/api/activity", action: "410", open: false, migrate: "" },
204
+ { method: "GET", path: "/cost", action: "308", open: false, migrate: "/" },
205
+ { method: "GET", path: "/trust", action: "308", open: false, migrate: "/" },
206
+ { method: "GET", path: "/", action: "308", open: false, migrate: "/" },
207
+ { method: "GET", path: "/api/quality-score", action: "410", open: false, migrate: "" },
208
+ { method: "GET", path: "/api/quality-score/history", action: "410", open: false, migrate: "" },
209
+ { method: "POST", path: "/api/quality-scan", action: "501", open: false, migrate: "" },
210
+ { method: "GET", path: "/api/quality-report", action: "501", open: false, migrate: "" },
211
+ { method: "GET", path: "/api/migration/list", action: "501", open: false, migrate: "" },
212
+ { method: "POST", path: "/api/migration/start", action: "410", open: false, migrate: "" },
213
+ { method: "GET", path: "/api/migration/{migration_id}/status", action: "501", open: false, migrate: "" },
214
+ { method: "GET", path: "/api/migration/{migration_id}/plan", action: "410", open: false, migrate: "" },
215
+ { method: "GET", path: "/api/migration/{migration_id}/features", action: "410", open: false, migrate: "" },
216
+ { method: "GET", path: "/api/migration/{migration_id}/seams", action: "410", open: false, migrate: "" },
217
+ { method: "POST", path: "/api/migration/{migration_id}/advance", action: "410", open: false, migrate: "" },
218
+ { method: "POST", path: "/api/migration/{migration_id}/start-phase", action: "410", open: false, migrate: "" },
219
+ { method: "GET", path: "/api/managed/events", action: "501", open: false, migrate: "" },
220
+ { method: "GET", path: "/api/managed/status", action: "501", open: false, migrate: "" },
221
+ { method: "GET", path: "/api/managed/memory_versions/{memory_id}", action: "410", open: false, migrate: "" },
222
+ { method: "GET", path: "/api/findings/{iteration}", action: "410", open: false, migrate: "" },
223
+ { method: "GET", path: "/api/quality/architecture", action: "410", open: false, migrate: "" },
224
+ { method: "GET", path: "/api/learnings", action: "501", open: false, migrate: "" },
225
+ { method: "GET", path: "/api/escalations", action: "501", open: false, migrate: "" },
226
+ { method: "GET", path: "/api/escalations/{filename}", action: "410", open: false, migrate: "" },
227
+ { method: "GET", path: "/api/phases", action: "410", open: false, migrate: "" },
228
+ { method: "GET", path: "/api/proofs", action: "501", open: false, migrate: "" },
229
+ { method: "GET", path: "/api/proofs/summary", action: "501", open: false, migrate: "" },
230
+ { method: "GET", path: "/api/proofs/{run_id}", action: "501", open: false, migrate: "" },
231
+ { method: "GET", path: "/api/proofs/{run_id}/html", action: "410", open: false, migrate: "" },
232
+ { method: "GET", path: "/api/spec", action: "410", open: false, migrate: "" },
233
+ { method: "GET", path: "/api/spec/history", action: "410", open: false, migrate: "" },
234
+ { method: "GET", path: "/api/wiki", action: "410", open: false, migrate: "" },
235
+ { method: "GET", path: "/api/wiki/{section}", action: "410", open: false, migrate: "" },
236
+ { method: "POST", path: "/api/wiki/ask", action: "501", open: false, migrate: "" },
237
+ ];