loki-mode 10.7.0 → 10.7.1

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 (96) hide show
  1. package/README.md +1 -0
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/dashboard/__init__.py +1 -1
  5. package/docs/INSTALLATION.md +1 -1
  6. package/docs/v10/CONTROL-PLANE.md +7 -0
  7. package/docs/v10/DECISIONS.md +41 -0
  8. package/docs/v10/ENGINE-LAWS.md +155 -0
  9. package/docs/v10/FAILURE-CLASSES.md +145 -0
  10. package/docs/v10/GUIDE.md +4 -2
  11. package/docs/v10/L0-WAVE1.md +81 -0
  12. package/docs/v10/MORNING-BRIEF-1003.md +11 -25
  13. package/docs/v10/OPERATING-MODEL.md +1 -0
  14. package/docs/v10/PROGRESS.md +28 -0
  15. package/loki-ts/dist/loki.js +802 -781
  16. package/loki-ts/package.json +1 -0
  17. package/mcp/__init__.py +1 -1
  18. package/package.json +1 -1
  19. package/packages/control-plane/dist/server.js +293 -19
  20. package/packages/control-plane/drizzle/0002_tiresome_fixer.sql +7 -0
  21. package/packages/control-plane/drizzle/0003_integrity.sql +4 -0
  22. package/packages/control-plane/drizzle/meta/0002_snapshot.json +409 -0
  23. package/packages/control-plane/drizzle/meta/0003_snapshot.json +437 -0
  24. package/packages/control-plane/drizzle/meta/_journal.json +14 -0
  25. package/packages/control-plane/src/db/migrate.ts +6 -2
  26. package/packages/control-plane/src/db/prune.ts +91 -0
  27. package/packages/control-plane/src/db/schema.ts +13 -0
  28. package/packages/control-plane/src/server/app.ts +20 -1
  29. package/packages/control-plane/src/server/integrity.ts +147 -0
  30. package/packages/control-plane/src/server/runs.ts +46 -4
  31. package/packages/control-plane/src/shipper/discover.ts +32 -3
  32. package/packages/control-plane/src/shipper/ship.ts +15 -0
  33. package/packages/control-plane/src/shipper/watch.ts +3 -1
  34. package/packages/control-plane/ui/dist/assets/index-DHDI_dHM.css +1 -0
  35. package/packages/control-plane/ui/dist/assets/index-DaQr0MPS.js +129 -0
  36. package/packages/control-plane/ui/dist/index.html +2 -2
  37. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  38. package/web-app/dist/assets/{AdminPage-BQPb5rc5.js → AdminPage-BJSU5hY1.js} +1 -1
  39. package/web-app/dist/assets/{Avatar-Cttco6FC.js → Avatar-CSOSpw9c.js} +1 -1
  40. package/web-app/dist/assets/{Badge-CPb6kpbH.js → Badge-COr7vj0D.js} +1 -1
  41. package/web-app/dist/assets/{Button-caTL28a4.js → Button-DGoWFERp.js} +1 -1
  42. package/web-app/dist/assets/{CockpitPage-bEbLx6ph.js → CockpitPage-CBpWkNXY.js} +1 -1
  43. package/web-app/dist/assets/{ComparePage-DNtJSij-.js → ComparePage-BwT8vqrL.js} +1 -1
  44. package/web-app/dist/assets/{ErrorBoundary-BrgC3K44.js → ErrorBoundary-D7SicDDA.js} +1 -1
  45. package/web-app/dist/assets/{EvidenceReceiptPanel-Q-mSrUUQ.js → EvidenceReceiptPanel-bx-5liG2.js} +1 -1
  46. package/web-app/dist/assets/{GitHubIssuesPanel-DbmYl_Er.js → GitHubIssuesPanel-BAeeWLqY.js} +1 -1
  47. package/web-app/dist/assets/{GitHubPRsPanel-C6PCHM4B.js → GitHubPRsPanel-CssD8lGI.js} +1 -1
  48. package/web-app/dist/assets/{HomePage-7zsL_k_e.js → HomePage-SL5PT3m5.js} +2 -2
  49. package/web-app/dist/assets/{LoginPage-Cl4ON8RQ.js → LoginPage-C8hFOZOM.js} +1 -1
  50. package/web-app/dist/assets/{MagicPage-BNEAhTqH.js → MagicPage-xHbHkrPf.js} +1 -1
  51. package/web-app/dist/assets/{MetricsPage-DyBH-88q.js → MetricsPage-B_2CKFcC.js} +1 -1
  52. package/web-app/dist/assets/{NotFoundPage-CIVr21yL.js → NotFoundPage-CXwXMq35.js} +1 -1
  53. package/web-app/dist/assets/{ProjectPage-BMeVYocF.js → ProjectPage-AaisMQI-.js} +3 -3
  54. package/web-app/dist/assets/{ProjectsPage-D-H2WRVU.js → ProjectsPage-Cej5ResX.js} +1 -1
  55. package/web-app/dist/assets/{SettingsPage-o0kR9RAt.js → SettingsPage-DKv4mn9H.js} +1 -1
  56. package/web-app/dist/assets/{ShowcasePage-DtfMDMjp.js → ShowcasePage-Dkiyzne4.js} +1 -1
  57. package/web-app/dist/assets/{SystemSettingsPage-CkXdpZ_T.js → SystemSettingsPage-DaBLQ5dU.js} +1 -1
  58. package/web-app/dist/assets/{TeamsPage-Cu5H9cQ0.js → TeamsPage-B2yUr31k.js} +1 -1
  59. package/web-app/dist/assets/{TemplatesPage-3wdbVwil.js → TemplatesPage-CfYiANhU.js} +1 -1
  60. package/web-app/dist/assets/{TerminalOutput-CWn301xh.js → TerminalOutput-CMXoexLM.js} +1 -1
  61. package/web-app/dist/assets/{activity-YGaAt4zJ.js → activity-B0dzoZtY.js} +1 -1
  62. package/web-app/dist/assets/{bell-C28q0GO7.js → bell-DFOTaSoo.js} +1 -1
  63. package/web-app/dist/assets/{bot-DLcQGI92.js → bot-Dz-WSQyP.js} +1 -1
  64. package/web-app/dist/assets/{check-w0sBNlcm.js → check-L1QIRueJ.js} +1 -1
  65. package/web-app/dist/assets/{chevron-left-W7O0vC_m.js → chevron-left-1TrnIEZS.js} +1 -1
  66. package/web-app/dist/assets/{circle-alert-Cxjq9fxB.js → circle-alert-8Bf6xEDx.js} +1 -1
  67. package/web-app/dist/assets/{clock-C8EqzmNl.js → clock-DB8NE6P5.js} +1 -1
  68. package/web-app/dist/assets/{cloud-xNgp4g3Y.js → cloud-DUSL3wZS.js} +1 -1
  69. package/web-app/dist/assets/{code-xml-B-lyNDLJ.js → code-xml-DbjryWNl.js} +1 -1
  70. package/web-app/dist/assets/{database-DoW5lmWc.js → database-DrezrMSs.js} +1 -1
  71. package/web-app/dist/assets/{dollar-sign-BcARA5sC.js → dollar-sign-CGbaLMzR.js} +1 -1
  72. package/web-app/dist/assets/{file-code-corner-CehcII0J.js → file-code-corner-Dfypa-r4.js} +1 -1
  73. package/web-app/dist/assets/{file-plus-DI9Zo5hP.js → file-plus-CzV5L2Gd.js} +1 -1
  74. package/web-app/dist/assets/{globe-CiZWBk3H.js → globe-BoYjTwPQ.js} +1 -1
  75. package/web-app/dist/assets/{hammer-3qv_T2N-.js → hammer-WO-HuiyJ.js} +1 -1
  76. package/web-app/dist/assets/{index-gF9grAz-.js → index-CjIOUjnd.js} +3 -3
  77. package/web-app/dist/assets/{layers-CN7d0By9.js → layers-Bn6jtKNk.js} +1 -1
  78. package/web-app/dist/assets/{loader-circle-DywcM-im.js → loader-circle-gERWRnmW.js} +1 -1
  79. package/web-app/dist/assets/{lock-_hdSmkU4.js → lock-BIiMLJq8.js} +1 -1
  80. package/web-app/dist/assets/{package-BV5zgqxJ.js → package-dLvDSVys.js} +1 -1
  81. package/web-app/dist/assets/{plus-CU_Hh4-x.js → plus-BHM3ZLhZ.js} +1 -1
  82. package/web-app/dist/assets/{refresh-cw-DMK-fNl-.js → refresh-cw-BAp_alv6.js} +1 -1
  83. package/web-app/dist/assets/{rotate-ccw-CEB3_v5_.js → rotate-ccw-CBhwq_MG.js} +1 -1
  84. package/web-app/dist/assets/{scroll-text-BSdXxcRN.js → scroll-text-DoS-6dfU.js} +1 -1
  85. package/web-app/dist/assets/{server-CAMlP1yJ.js → server-5JgdDWQO.js} +1 -1
  86. package/web-app/dist/assets/{shield-alert-B-r1Dt3h.js → shield-alert-DjmlLT34.js} +1 -1
  87. package/web-app/dist/assets/{thumbs-up-CYfxw9o4.js → thumbs-up-BtiEXl3p.js} +1 -1
  88. package/web-app/dist/assets/{trash-2-DX3nGPS8.js → trash-2-ClsWU_Vg.js} +1 -1
  89. package/web-app/dist/assets/{trending-down-Bt-hSntd.js → trending-down-DPS4M7_X.js} +1 -1
  90. package/web-app/dist/assets/{trending-up-DVSlvQp8.js → trending-up-D6OJ38-V.js} +1 -1
  91. package/web-app/dist/assets/{upload-D45RRHAP.js → upload-DOCzLUpF.js} +1 -1
  92. package/web-app/dist/assets/{usePolling-fq_Xj8iw.js → usePolling-CljQTWj_.js} +1 -1
  93. package/web-app/dist/assets/{user-BWcJkLlr.js → user-C0G82Weu.js} +1 -1
  94. package/web-app/dist/index.html +1 -1
  95. package/packages/control-plane/ui/dist/assets/index-CUQ65vJY.css +0 -1
  96. package/packages/control-plane/ui/dist/assets/index-CwwGfp6E.js +0 -129
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "version": "bun src/cli.ts version",
13
+ "postinstall": "cd ../packages/control-plane && bun install --frozen-lockfile",
13
14
  "typecheck": "bun run --bun tsc --noEmit",
14
15
  "test": "LOKI_NO_BROWSER=1 bun test",
15
16
  "bench": "bun run scripts/bench.ts",
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '10.7.0'
78
+ __version__ = '10.7.1'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "10.7.0",
4
+ "version": "10.7.1",
5
5
  "description": "An autonomous software factory that knows what it is supposed to deliver, and proves it did.",
6
6
  "keywords": [
7
7
  "agent",
@@ -5490,7 +5490,8 @@ var exports_schema = {};
5490
5490
  __export(exports_schema, {
5491
5491
  sources: () => sources,
5492
5492
  runs: () => runs,
5493
- events: () => events
5493
+ events: () => events,
5494
+ audit: () => audit
5494
5495
  });
5495
5496
  var sources = sqliteTable("sources", {
5496
5497
  id: text("id").primaryKey(),
@@ -5537,20 +5538,35 @@ var runs = sqliteTable("runs", {
5537
5538
  tampered: integer("tampered").notNull(),
5538
5539
  conflict: integer("conflict").notNull().default(0),
5539
5540
  groupId: text("group_id"),
5540
- unitId: text("unit_id")
5541
+ unitId: text("unit_id"),
5542
+ attested: integer("attested"),
5543
+ sigChecked: integer("sig_checked"),
5544
+ integrityKeyFp: text("integrity_key_fp"),
5545
+ integrityReasons: text("integrity_reasons")
5541
5546
  }, (t) => [
5542
5547
  primaryKey({ columns: [t.sourceId, t.runId] }),
5543
5548
  index("runs_started").on(t.startedAt),
5544
5549
  index("runs_verdict").on(t.verdict),
5545
5550
  index("runs_group").on(t.groupId)
5546
5551
  ]);
5552
+ var audit = sqliteTable("audit", {
5553
+ id: integer("id").primaryKey({ autoIncrement: true }),
5554
+ ts: text("ts").notNull(),
5555
+ action: text("action").notNull(),
5556
+ actor: text("actor").notNull(),
5557
+ detail: text("detail", { mode: "json" }).notNull()
5558
+ });
5547
5559
 
5548
5560
  // src/db/migrate.ts
5549
5561
  function openDb(path) {
5550
5562
  const sqlite = new Database2(path, { create: true });
5551
5563
  sqlite.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;");
5552
5564
  const db = drizzle(sqlite, { schema: exports_schema });
5553
- const migrationsFolder = [join(import.meta.dir, "../../drizzle"), join(import.meta.dir, "../drizzle")].find((d) => existsSync(join(d, "meta/_journal.json")));
5565
+ const migrationsFolder = [
5566
+ join(import.meta.dir, "../../drizzle"),
5567
+ join(import.meta.dir, "../drizzle"),
5568
+ join(import.meta.dir, "../../packages/control-plane/drizzle")
5569
+ ].find((d) => existsSync(join(d, "meta/_journal.json")));
5554
5570
  if (!migrationsFolder)
5555
5571
  throw new Error("drizzle migrations folder not found");
5556
5572
  migrate(db, { migrationsFolder });
@@ -5558,7 +5574,7 @@ function openDb(path) {
5558
5574
  }
5559
5575
 
5560
5576
  // src/server/ingest.ts
5561
- import { createHash } from "crypto";
5577
+ import { createHash as createHash2 } from "crypto";
5562
5578
 
5563
5579
  // ../../loki-ts/src/engine10/events.ts
5564
5580
  import { closeSync, existsSync as existsSync2, fstatSync, mkdirSync, openSync, readFileSync, readSync, writeSync } from "fs";
@@ -5718,6 +5734,149 @@ function writeAnswer(dir, source, run, answer) {
5718
5734
  return { status: 200, body: { ok: true, path, resume: `loki answer ${run}` } };
5719
5735
  }
5720
5736
 
5737
+ // src/server/integrity.ts
5738
+ import { createHash, createPublicKey, verify } from "crypto";
5739
+ import { readFileSync as readFileSync2 } from "fs";
5740
+ var SHA = /^[0-9a-f]{64}$/;
5741
+ var SEAL_LINE = "log.sealed";
5742
+ var REDACTED = /\[REDACTED(?::[A-Z_]+)?\]/;
5743
+ var REDACTED_NOTE = "log redacted before ingest; seal not checkable";
5744
+ var UNATTESTED_SUFFIX = " (unattested)";
5745
+ var SUCCESS_VERDICTS = new Set(["VERIFIED", "ALREADY_SATISFIED"]);
5746
+ var normVerdict = (v) => typeof v === "string" ? v.trim().toUpperCase() : null;
5747
+ var kidOf = (pub) => createHash("sha256").update(`{"crv":"Ed25519","kty":"OKP","x":"${pub.export({ format: "jwk" }).x}"}`).digest("base64url");
5748
+ function pubkeysFromEnv(env = process.env) {
5749
+ const keys = (env["LOKI_CP_RECEIPT_PUBKEYS"] ?? "").split(":").map((f) => f.trim()).filter(Boolean).flatMap((f) => {
5750
+ try {
5751
+ return [createPublicKey(readFileSync2(f))];
5752
+ } catch {
5753
+ return [];
5754
+ }
5755
+ });
5756
+ const fn = (kid) => keys.find((k) => kidOf(k) === kid);
5757
+ fn.configured = keys.length > 0;
5758
+ fn.fingerprint = createHash("sha256").update(keys.map(kidOf).sort().join(",")).digest("hex").slice(0, 16);
5759
+ return fn;
5760
+ }
5761
+ var line = (e) => JSON.stringify({ v: e.v, seq: e.seq, ts: e.ts, run: e.run, type: e.type, stage: e.stage, data: e.data });
5762
+ var verdictOf = (e) => typeof e?.data["verdict"] === "string" ? e.data["verdict"].trim().toUpperCase() : null;
5763
+ function sealedPrefix(evs) {
5764
+ const i = evs.findIndex((e) => e.type === SEAL_LINE);
5765
+ return i < 0 ? [...evs] : evs.slice(0, i + 1);
5766
+ }
5767
+ function verifyRunIntegrity(evs, opts = {}) {
5768
+ const bad = [], notes = [];
5769
+ const doneAt = evs.findIndex((e) => e.type === "run.completed");
5770
+ const sealedAt = evs.map((e, i) => e.type === "receipt.sealed" ? i : -1).filter((i) => i >= 0);
5771
+ const finalised = doneAt >= 0 || sealedAt.length > 0;
5772
+ const lines = evs.map(line);
5773
+ const keysConfigured = opts.keysConfigured ?? opts.pubkeyFor?.configured ?? false;
5774
+ let sigChecked = false;
5775
+ if (finalised) {
5776
+ const gap = evs.findIndex((e, i) => e.seq !== i);
5777
+ if (gap >= 0)
5778
+ bad.push(`event log has a seq gap, duplicate or missing start at position ${gap} (seq ${evs[gap].seq})`);
5779
+ }
5780
+ if (evs.some((e) => e.type === "tamper.detected"))
5781
+ bad.push("the supervisor detected events.jsonl being modified during the run");
5782
+ const firstSeal = evs.findIndex((e) => e.type === SEAL_LINE);
5783
+ if (firstSeal >= 0 && evs.slice(firstSeal + 1).some((e) => e.type === "run.completed" || e.type === "receipt.sealed" || typeof e.data["verdict"] === "string"))
5784
+ bad.push("a verdict-bearing event after log.sealed is unauthenticated");
5785
+ if (evs.filter((e) => e.type === "run.completed").length > 1)
5786
+ bad.push("more than one run.completed (a verdict appended after the seal is unauthenticated)");
5787
+ const hashBad = [];
5788
+ const redacted = REDACTED.test(JSON.stringify(doneAt >= 0 ? evs.slice(0, doneAt + 2).map((e) => e.data) : evs.map((e) => e.data)));
5789
+ for (const i of sealedAt) {
5790
+ const d = evs[i].data;
5791
+ if (typeof d["receipt_sha256"] !== "string" || !SHA.test(d["receipt_sha256"]))
5792
+ hashBad.push(`receipt.sealed at seq ${evs[i].seq} has no valid receipt_sha256`);
5793
+ const bound = d["events_sha256"];
5794
+ if (typeof bound === "string") {
5795
+ const h = createHash("sha256");
5796
+ let ok = false;
5797
+ for (const l of lines) {
5798
+ if (h.copy().digest("hex") === bound) {
5799
+ ok = true;
5800
+ break;
5801
+ }
5802
+ h.update(l + `
5803
+ `);
5804
+ }
5805
+ if (!ok && h.digest("hex") === bound)
5806
+ ok = true;
5807
+ if (!ok)
5808
+ hashBad.push("receipt.sealed events_sha256 does not match any prefix of the ingested log");
5809
+ }
5810
+ }
5811
+ const sealLines = evs.map((e, i) => e.type === SEAL_LINE ? i : -1).filter((i) => i >= 0);
5812
+ if (sealLines.some((i) => i !== doneAt + 1))
5813
+ bad.push(`${SEAL_LINE} is not the line right after run.completed`);
5814
+ const ls = doneAt >= 0 && evs[doneAt + 1]?.type === SEAL_LINE ? evs[doneAt + 1].data : null;
5815
+ const signedSeals = sealedAt.map((i) => evs[i].data).filter((d) => d["signed"] === true);
5816
+ const signedReceipt = signedSeals.length > 0;
5817
+ if (ls) {
5818
+ const sha = createHash("sha256").update(lines.slice(0, doneAt + 1).join(`
5819
+ `) + `
5820
+ `).digest("hex");
5821
+ const sig = typeof ls["sig"] === "string" ? Buffer.from(ls["sig"], "base64url") : null;
5822
+ const kid = ls["kid"];
5823
+ if (ls["tampered"] !== false)
5824
+ bad.push(`${SEAL_LINE} line records a tamper or is malformed`);
5825
+ else if (typeof kid !== "string" || !sig || sig.length !== 64)
5826
+ hashBad.push(`${SEAL_LINE} line is malformed or records a tamper`);
5827
+ else {
5828
+ if (signedSeals.some((d) => typeof d["kid"] === "string" && d["kid"] !== kid))
5829
+ hashBad.push(`${SEAL_LINE} kid differs from the receipt kid`);
5830
+ if (ls["events_sha256"] !== sha)
5831
+ hashBad.push(`${SEAL_LINE} events_sha256 does not match the ingested log`);
5832
+ else {
5833
+ const pub = opts.pubkeyFor?.(kid);
5834
+ if (pub) {
5835
+ sigChecked = true;
5836
+ if (!verify(null, Buffer.from(`${sha}:false`), pub, sig))
5837
+ bad.push(`${SEAL_LINE} signature does not verify`);
5838
+ } else if (keysConfigured)
5839
+ bad.push(`${SEAL_LINE} kid ${kid} has no configured public key`);
5840
+ }
5841
+ }
5842
+ } else if (signedReceipt && doneAt >= 0)
5843
+ bad.push(`signed receipt but no ${SEAL_LINE} line after run.completed`);
5844
+ let unverifiable = false;
5845
+ if (hashBad.length > 0) {
5846
+ if (redacted) {
5847
+ unverifiable = true;
5848
+ sigChecked = false;
5849
+ notes.push(REDACTED_NOTE);
5850
+ } else
5851
+ bad.push(...hashBad);
5852
+ }
5853
+ let claimsVerified = false;
5854
+ const claimed = doneAt >= 0 ? verdictOf(evs[doneAt]) : null;
5855
+ if (claimed !== null && SUCCESS_VERDICTS.has(claimed)) {
5856
+ claimsVerified = true;
5857
+ const before = sealedAt.filter((i) => i < doneAt);
5858
+ if (before.length === 0)
5859
+ bad.push(`run.completed claims ${claimed} but no receipt.sealed precedes it`);
5860
+ else if (!before.some((i) => verdictOf(evs[i]) === claimed))
5861
+ bad.push(`run.completed claims ${claimed} but the sealed receipt does not`);
5862
+ }
5863
+ const tampered = bad.length > 0;
5864
+ const attested = !tampered && !unverifiable && (!claimsVerified || signedReceipt && ls !== null);
5865
+ return { tampered, attested, sig_checked: sigChecked && !tampered, reasons: [...bad, ...notes] };
5866
+ }
5867
+ function effectiveVerdict(r) {
5868
+ if (r.tampered)
5869
+ return "TAMPERED";
5870
+ const v = normVerdict(r.verdict);
5871
+ if (v === null)
5872
+ return r.verdict;
5873
+ if (r.attested === false)
5874
+ return SUCCESS_VERDICTS.has(v) ? "UNVERIFIED" : `${r.verdict}${UNATTESTED_SUFFIX}`;
5875
+ if (SUCCESS_VERDICTS.has(v))
5876
+ return r.sig_checked === false ? `${v} (signature not checked)` : v;
5877
+ return r.verdict;
5878
+ }
5879
+
5721
5880
  // src/server/runs.ts
5722
5881
  var str = (x) => typeof x === "string" ? x : null;
5723
5882
  function loadEvents(db, sourceId, runId) {
@@ -5727,8 +5886,11 @@ function rebuildRun(db, sourceId, runId) {
5727
5886
  const evs = loadEvents(db, sourceId, runId);
5728
5887
  if (evs.length === 0)
5729
5888
  return;
5730
- const f = fold(evs);
5731
- const pc = partialCost(evs, f.run.tampered);
5889
+ const f = fold(sealedPrefix(evs));
5890
+ const keys = pubkeysFromEnv();
5891
+ const integ = verifyRunIntegrity(evs, { pubkeyFor: keys });
5892
+ const tampered = f.run.tampered || integ.tampered;
5893
+ const pc = partialCost(evs, tampered);
5732
5894
  const sd = f.run.started?.data ?? {};
5733
5895
  const done = f.run.completed?.data ?? {};
5734
5896
  const pr = evs.find((e) => e.type === "pr.opened")?.data;
@@ -5756,10 +5918,22 @@ function rebuildRun(db, sourceId, runId) {
5756
5918
  wallS: typeof done.wall_s === "number" ? done.wall_s : null,
5757
5919
  lastSeq: f.lastSeq,
5758
5920
  lastEventAt: evs[evs.length - 1]?.ts ?? null,
5759
- tampered: Number(f.run.tampered)
5921
+ tampered: Number(tampered),
5922
+ attested: Number(integ.attested),
5923
+ sigChecked: Number(integ.sig_checked),
5924
+ integrityKeyFp: keys.fingerprint ?? "",
5925
+ integrityReasons: JSON.stringify(integ.reasons)
5760
5926
  };
5761
5927
  db.insert(runs).values(row).onConflictDoUpdate({ target: [runs.sourceId, runs.runId], set: row }).run();
5762
5928
  }
5929
+ var reasonsOf = (j) => {
5930
+ try {
5931
+ const x = JSON.parse(j ?? "[]");
5932
+ return Array.isArray(x) ? x.filter((s) => typeof s === "string") : [];
5933
+ } catch {
5934
+ return [];
5935
+ }
5936
+ };
5763
5937
  var parseRun = (r) => ({
5764
5938
  source_id: r.sourceId,
5765
5939
  run_id: r.runId,
@@ -5786,6 +5960,10 @@ var parseRun = (r) => ({
5786
5960
  last_event_at: r.lastEventAt,
5787
5961
  tampered: r.tampered === 1,
5788
5962
  conflict: r.conflict === 1,
5963
+ attested: r.attested === 1,
5964
+ sig_checked: r.sigChecked === 1,
5965
+ integrity_reasons: reasonsOf(r.integrityReasons),
5966
+ effective_verdict: effectiveVerdict({ verdict: r.verdict, tampered: r.tampered === 1, attested: r.attested === 1, sig_checked: r.sigChecked === 1 }),
5789
5967
  status: r.endedAt ? "completed" : "running",
5790
5968
  elapsed_s: elapsedS(r)
5791
5969
  });
@@ -5824,10 +6002,44 @@ var elapsedS = (r) => {
5824
6002
  const end = r.endedAt ? Date.parse(r.endedAt) : Date.now();
5825
6003
  return Math.max(0, (end - Date.parse(r.startedAt)) / 1000);
5826
6004
  };
6005
+ var SIG_SUFFIX = " (signature not checked)";
6006
+ var verdictFilter = (v) => {
6007
+ if (!v)
6008
+ return;
6009
+ if (v === "TAMPERED")
6010
+ return eq(runs.tampered, 1);
6011
+ if (v === "UNVERIFIED")
6012
+ return and(eq(runs.tampered, 0), sql`${runs.verdict} is not null`, sql`coalesce(${runs.attested}, 0) != 1`);
6013
+ if (SUCCESS_VERDICTS.has(v))
6014
+ return and(eq(runs.verdict, v), eq(runs.tampered, 0), eq(runs.attested, 1), eq(runs.sigChecked, 1));
6015
+ if (v.endsWith(SIG_SUFFIX) && SUCCESS_VERDICTS.has(v.slice(0, -SIG_SUFFIX.length)))
6016
+ return and(eq(runs.verdict, v.slice(0, -SIG_SUFFIX.length)), eq(runs.tampered, 0), eq(runs.attested, 1), sql`coalesce(${runs.sigChecked}, 0) != 1`);
6017
+ return and(eq(runs.verdict, v), eq(runs.tampered, 0), eq(runs.attested, 1));
6018
+ };
6019
+ var RECOMPUTE_CHUNK = 200;
6020
+ function recomputeLegacy(db) {
6021
+ const fp = pubkeysFromEnv().fingerprint ?? "";
6022
+ let n = 0;
6023
+ for (;; ) {
6024
+ const rows = db.select({ s: runs.sourceId, r: runs.runId }).from(runs).where(sql`${runs.attested} is null or ${runs.integrityKeyFp} is not ${fp}`).limit(RECOMPUTE_CHUNK).all();
6025
+ if (rows.length === 0)
6026
+ return n;
6027
+ db.transaction((tx) => {
6028
+ const t = tx;
6029
+ for (const x of rows) {
6030
+ if (loadEvents(t, x.s, x.r).length === 0) {
6031
+ t.update(runs).set({ attested: 0, sigChecked: 0, integrityKeyFp: fp, integrityReasons: JSON.stringify(["stored events are missing; integrity cannot be evaluated"]) }).where(and(eq(runs.sourceId, x.s), eq(runs.runId, x.r))).run();
6032
+ } else
6033
+ rebuildRun(t, x.s, x.r);
6034
+ }
6035
+ });
6036
+ n += rows.length;
6037
+ }
6038
+ }
5827
6039
  function listRuns(db, q) {
5828
6040
  const limit = Math.min(Math.max(q.limit ?? 50, 1), 200);
5829
6041
  const offset = Math.max(Number.parseInt(q.cursor ?? "0", 10) || 0, 0);
5830
- const where = and(q.verdict ? eq(runs.verdict, q.verdict) : undefined, q.repo ? eq(runs.originRepo, q.repo) : undefined, q.group_id ? eq(runs.groupId, q.group_id) : undefined, q.since ? gte(runs.startedAt, q.since) : undefined, q.until ? lt(runs.startedAt, q.until) : undefined);
6042
+ const where = and(verdictFilter(q.verdict), q.repo ? eq(runs.originRepo, q.repo) : undefined, q.group_id ? eq(runs.groupId, q.group_id) : undefined, q.since ? gte(runs.startedAt, q.since) : undefined, q.until ? lt(runs.startedAt, q.until) : undefined);
5831
6043
  const total = db.select({ n: sql`count(*)` }).from(runs).where(where).get()?.n ?? 0;
5832
6044
  const rows = db.select().from(runs).where(where).orderBy(desc(runs.startedAt), asc(runs.runId)).limit(limit).offset(offset).all();
5833
6045
  return { runs: rows.map((r) => withLive(db, r)), total, next_cursor: offset + rows.length < total ? String(offset + rows.length) : null };
@@ -5883,7 +6095,7 @@ function ingest(db, body) {
5883
6095
  if (e.run !== runId)
5884
6096
  return { status: 400, body: { error: `events[${i}]: run does not match run_id` } };
5885
6097
  const json = JSON.stringify(e);
5886
- rows.push({ e, json, sha: createHash("sha256").update(json).digest("hex") });
6098
+ rows.push({ e, json, sha: createHash2("sha256").update(json).digest("hex") });
5887
6099
  }
5888
6100
  const now = new Date().toISOString();
5889
6101
  let accepted = 0, duplicate = 0, conflict = 0;
@@ -5912,7 +6124,7 @@ function ingest(db, body) {
5912
6124
  }
5913
6125
 
5914
6126
  // src/server/auth.ts
5915
- import { createHash as createHash2, timingSafeEqual } from "crypto";
6127
+ import { createHash as createHash3, timingSafeEqual } from "crypto";
5916
6128
  var LOOPBACK_BINDS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
5917
6129
  function isLoopbackHost(host) {
5918
6130
  return !!host && /^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/i.test(host);
@@ -5921,8 +6133,8 @@ function tokenMatches(header, token) {
5921
6133
  const m = /^Bearer (.+)$/i.exec(header ?? "");
5922
6134
  if (!m)
5923
6135
  return false;
5924
- const a = createHash2("sha256").update(m[1] ?? "").digest();
5925
- const b = createHash2("sha256").update(token).digest();
6136
+ const a = createHash3("sha256").update(m[1] ?? "").digest();
6137
+ const b = createHash3("sha256").update(token).digest();
5926
6138
  return timingSafeEqual(a, b);
5927
6139
  }
5928
6140
  function bindRefusal(env) {
@@ -5961,8 +6173,8 @@ import { readdirSync } from "fs";
5961
6173
  import { join as join4 } from "path";
5962
6174
 
5963
6175
  // src/shipper/ship.ts
5964
- import { createHash as createHash3 } from "crypto";
5965
- import { existsSync as existsSync3, readFileSync as readFileSync2, realpathSync, renameSync, writeFileSync as writeFileSync2 } from "fs";
6176
+ import { createHash as createHash4 } from "crypto";
6177
+ import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync, renameSync, writeFileSync as writeFileSync2 } from "fs";
5966
6178
  import { hostname } from "os";
5967
6179
  import { join as join3 } from "path";
5968
6180
  var BATCH = 200;
@@ -5972,14 +6184,14 @@ function sourceId(repoDir) {
5972
6184
  try {
5973
6185
  real2 = realpathSync(repoDir);
5974
6186
  } catch {}
5975
- return createHash3("sha256").update(`${hostname()}\x00${real2}`).digest("hex").slice(0, 16);
6187
+ return createHash4("sha256").update(`${hostname()}\x00${real2}`).digest("hex").slice(0, 16);
5976
6188
  }
5977
6189
  var redactDeep = (x) => typeof x === "string" ? redactSecrets(x) : Array.isArray(x) ? x.map(redactDeep) : x && typeof x === "object" ? Object.fromEntries(Object.entries(x).map(([k, v]) => [k, redactDeep(v)])) : x;
5978
6190
  var redactEvent = (e) => ({ ...e, data: redactDeep(e.data) });
5979
6191
  var backoffMs = (failures, rnd = Math.random) => Math.min(60000, 1000 * 2 ** Math.max(0, failures - 1)) * (1 + 0.25 * rnd());
5980
6192
  function readShip(runDir, url) {
5981
6193
  try {
5982
- const s = JSON.parse(readFileSync2(join3(runDir, "ship.json"), "utf8"));
6194
+ const s = JSON.parse(readFileSync3(join3(runDir, "ship.json"), "utf8"));
5983
6195
  if (s.url === url && Number.isInteger(s.acked_seq))
5984
6196
  return { acked_seq: s.acked_seq, url };
5985
6197
  } catch {}
@@ -5996,7 +6208,7 @@ async function shipRun(o) {
5996
6208
  const all = readEvents(join3(o.runDir, "events.jsonl"));
5997
6209
  let lines = 0;
5998
6210
  try {
5999
- lines = readFileSync2(join3(o.runDir, "events.jsonl"), "utf8").split(`
6211
+ lines = readFileSync3(join3(o.runDir, "events.jsonl"), "utf8").split(`
6000
6212
  `).filter((l) => l.trim() !== "").length;
6001
6213
  } catch {}
6002
6214
  const skipped = lines - all.length;
@@ -6079,9 +6291,49 @@ async function backfill(o) {
6079
6291
  return { runs: runs2, sent, failed };
6080
6292
  }
6081
6293
 
6294
+ // src/db/prune.ts
6295
+ var n = (sqlite, sql2, ...args) => sqlite.query(sql2).get(...args).n;
6296
+ function countFor(sqlite, keys) {
6297
+ const bySource = new Map;
6298
+ for (const k of keys)
6299
+ bySource.set(k.source_id, [...bySource.get(k.source_id) ?? [], k.run_id]);
6300
+ let events2 = 0, sources2 = 0;
6301
+ for (const [src, runIds] of bySource) {
6302
+ let ev = 0;
6303
+ for (const r of runIds)
6304
+ ev += n(sqlite, "select count(*) n from events where source_id = ? and run_id = ?", src, r);
6305
+ events2 += ev;
6306
+ const runsLeft = n(sqlite, "select count(*) n from runs where source_id = ?", src) - runIds.length;
6307
+ const eventsLeft = n(sqlite, "select count(*) n from events where source_id = ?", src) - ev;
6308
+ if (runsLeft <= 0 && eventsLeft <= 0 && sqlite.query("select 1 x from sources where id = ?").get(src))
6309
+ sources2++;
6310
+ }
6311
+ return { runs: keys.length, events: events2, sources: sources2 };
6312
+ }
6313
+ function audit2(sqlite, action, actor, detail) {
6314
+ sqlite.query("insert into audit (ts, action, actor, detail) values (?, ?, ?, ?)").run(new Date().toISOString(), action, actor, JSON.stringify(detail));
6315
+ }
6316
+ var dropOrphanSources = (sqlite, ids) => {
6317
+ for (const id of ids)
6318
+ sqlite.query("delete from sources where id = ? and not exists (select 1 from runs where source_id = ?) and not exists (select 1 from events where source_id = ?)").run(id, id, id);
6319
+ };
6320
+ function removeRun(sqlite, source, run, actor = "http") {
6321
+ let out = null;
6322
+ sqlite.transaction(() => {
6323
+ if (!sqlite.query("select 1 x from runs where source_id = ? and run_id = ?").get(source, run))
6324
+ return;
6325
+ out = countFor(sqlite, [{ source_id: source, run_id: run }]);
6326
+ audit2(sqlite, "run.remove", actor, { source_id: source, run_id: run, counts: out });
6327
+ sqlite.query("delete from events where source_id = ? and run_id = ?").run(source, run);
6328
+ sqlite.query("delete from runs where source_id = ? and run_id = ?").run(source, run);
6329
+ dropOrphanSources(sqlite, [source]);
6330
+ }).immediate();
6331
+ return out;
6332
+ }
6333
+
6082
6334
  // src/server/actions.ts
6083
6335
  import { spawn } from "child_process";
6084
- import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
6336
+ import { existsSync as existsSync4, readFileSync as readFileSync4, statSync } from "fs";
6085
6337
  import { homedir as homedir2 } from "os";
6086
6338
  import { join as join5, resolve } from "path";
6087
6339
  var ISSUE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}\/[A-Za-z0-9._-]{1,100}#[1-9][0-9]{0,8}$/;
@@ -6095,7 +6347,7 @@ function childEnv(env = process.env) {
6095
6347
  }
6096
6348
  function registryRepos(env = process.env) {
6097
6349
  try {
6098
- const j = JSON.parse(readFileSync3(join5(env.HOME || homedir2(), ".loki", "dashboard", "projects.json"), "utf8"));
6350
+ const j = JSON.parse(readFileSync4(join5(env.HOME || homedir2(), ".loki", "dashboard", "projects.json"), "utf8"));
6099
6351
  return Object.values(j.projects ?? {}).map((p) => typeof p?.path === "string" ? p.path : "").filter((p) => p && existsSync4(p) && statSync(p).isDirectory());
6100
6352
  } catch {
6101
6353
  return [];
@@ -6149,6 +6401,7 @@ var defaultUiDir = () => [join6(import.meta.dir, "../../ui/dist"), join6(import.
6149
6401
  function createApp(opts) {
6150
6402
  const uiDir = opts.uiDir ?? defaultUiDir();
6151
6403
  const { db, sqlite } = openDb(opts.dbPath);
6404
+ recomputeLegacy(db);
6152
6405
  let ready = true;
6153
6406
  const answerDir = opts.answerDir ?? defaultAnswerDir();
6154
6407
  const app = new Hono3;
@@ -6234,6 +6487,27 @@ function createApp(opts) {
6234
6487
  });
6235
6488
  return c.json({ runs: r.runs, sent: r.sent, failed: r.failed });
6236
6489
  });
6490
+ act.delete("/v1/runs/:source/:run", async (c) => {
6491
+ if (!peerIsLoopback(c) || !isLoopbackHost(c.req.header("host")))
6492
+ return c.json({ error: "loopback only" }, 403);
6493
+ if (!(c.req.header("content-type") ?? "").toLowerCase().startsWith("application/json"))
6494
+ return c.json({ error: "content-type must be application/json" }, 400);
6495
+ const origin = c.req.header("origin");
6496
+ if (origin !== undefined) {
6497
+ let oh = "";
6498
+ try {
6499
+ oh = new URL(origin).host.toLowerCase();
6500
+ } catch {}
6501
+ if (!oh || oh !== (c.req.header("host") ?? "").toLowerCase())
6502
+ return c.json({ error: "origin not allowed" }, 403);
6503
+ }
6504
+ const source = c.req.param("source"), run = c.req.param("run");
6505
+ const r = removeRun(sqlite, source, run);
6506
+ if (!r)
6507
+ return c.json({ error: "run not found" }, 404);
6508
+ const left = sqlite.query("select count(*) n from runs").get().n;
6509
+ return c.json({ ok: true, removed: r, remaining_runs: left });
6510
+ });
6237
6511
  act.get("/v1/repos", (c) => peerIsLoopback(c) ? c.json({ repos: [...new Set([repoDir, ...registryRepos()])] }) : c.json({ error: "loopback only" }, 403));
6238
6512
  act.post("/v1/start", async (c) => {
6239
6513
  if (!local(c))
@@ -0,0 +1,7 @@
1
+ CREATE TABLE `audit` (
2
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
+ `ts` text NOT NULL,
4
+ `action` text NOT NULL,
5
+ `actor` text NOT NULL,
6
+ `detail` text NOT NULL
7
+ );
@@ -0,0 +1,4 @@
1
+ ALTER TABLE `runs` ADD `attested` integer;--> statement-breakpoint
2
+ ALTER TABLE `runs` ADD `sig_checked` integer;--> statement-breakpoint
3
+ ALTER TABLE `runs` ADD `integrity_key_fp` text;--> statement-breakpoint
4
+ ALTER TABLE `runs` ADD `integrity_reasons` text;