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.
- package/README.md +1 -0
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/v10/CONTROL-PLANE.md +7 -0
- package/docs/v10/DECISIONS.md +41 -0
- package/docs/v10/ENGINE-LAWS.md +155 -0
- package/docs/v10/FAILURE-CLASSES.md +145 -0
- package/docs/v10/GUIDE.md +4 -2
- package/docs/v10/L0-WAVE1.md +81 -0
- package/docs/v10/MORNING-BRIEF-1003.md +11 -25
- package/docs/v10/OPERATING-MODEL.md +1 -0
- package/docs/v10/PROGRESS.md +28 -0
- package/loki-ts/dist/loki.js +802 -781
- package/loki-ts/package.json +1 -0
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/packages/control-plane/dist/server.js +293 -19
- package/packages/control-plane/drizzle/0002_tiresome_fixer.sql +7 -0
- package/packages/control-plane/drizzle/0003_integrity.sql +4 -0
- package/packages/control-plane/drizzle/meta/0002_snapshot.json +409 -0
- package/packages/control-plane/drizzle/meta/0003_snapshot.json +437 -0
- package/packages/control-plane/drizzle/meta/_journal.json +14 -0
- package/packages/control-plane/src/db/migrate.ts +6 -2
- package/packages/control-plane/src/db/prune.ts +91 -0
- package/packages/control-plane/src/db/schema.ts +13 -0
- package/packages/control-plane/src/server/app.ts +20 -1
- package/packages/control-plane/src/server/integrity.ts +147 -0
- package/packages/control-plane/src/server/runs.ts +46 -4
- package/packages/control-plane/src/shipper/discover.ts +32 -3
- package/packages/control-plane/src/shipper/ship.ts +15 -0
- package/packages/control-plane/src/shipper/watch.ts +3 -1
- package/packages/control-plane/ui/dist/assets/index-DHDI_dHM.css +1 -0
- package/packages/control-plane/ui/dist/assets/index-DaQr0MPS.js +129 -0
- package/packages/control-plane/ui/dist/index.html +2 -2
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-BQPb5rc5.js → AdminPage-BJSU5hY1.js} +1 -1
- package/web-app/dist/assets/{Avatar-Cttco6FC.js → Avatar-CSOSpw9c.js} +1 -1
- package/web-app/dist/assets/{Badge-CPb6kpbH.js → Badge-COr7vj0D.js} +1 -1
- package/web-app/dist/assets/{Button-caTL28a4.js → Button-DGoWFERp.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-bEbLx6ph.js → CockpitPage-CBpWkNXY.js} +1 -1
- package/web-app/dist/assets/{ComparePage-DNtJSij-.js → ComparePage-BwT8vqrL.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-BrgC3K44.js → ErrorBoundary-D7SicDDA.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-Q-mSrUUQ.js → EvidenceReceiptPanel-bx-5liG2.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-DbmYl_Er.js → GitHubIssuesPanel-BAeeWLqY.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-C6PCHM4B.js → GitHubPRsPanel-CssD8lGI.js} +1 -1
- package/web-app/dist/assets/{HomePage-7zsL_k_e.js → HomePage-SL5PT3m5.js} +2 -2
- package/web-app/dist/assets/{LoginPage-Cl4ON8RQ.js → LoginPage-C8hFOZOM.js} +1 -1
- package/web-app/dist/assets/{MagicPage-BNEAhTqH.js → MagicPage-xHbHkrPf.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-DyBH-88q.js → MetricsPage-B_2CKFcC.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-CIVr21yL.js → NotFoundPage-CXwXMq35.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-BMeVYocF.js → ProjectPage-AaisMQI-.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-D-H2WRVU.js → ProjectsPage-Cej5ResX.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-o0kR9RAt.js → SettingsPage-DKv4mn9H.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-DtfMDMjp.js → ShowcasePage-Dkiyzne4.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-CkXdpZ_T.js → SystemSettingsPage-DaBLQ5dU.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-Cu5H9cQ0.js → TeamsPage-B2yUr31k.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-3wdbVwil.js → TemplatesPage-CfYiANhU.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-CWn301xh.js → TerminalOutput-CMXoexLM.js} +1 -1
- package/web-app/dist/assets/{activity-YGaAt4zJ.js → activity-B0dzoZtY.js} +1 -1
- package/web-app/dist/assets/{bell-C28q0GO7.js → bell-DFOTaSoo.js} +1 -1
- package/web-app/dist/assets/{bot-DLcQGI92.js → bot-Dz-WSQyP.js} +1 -1
- package/web-app/dist/assets/{check-w0sBNlcm.js → check-L1QIRueJ.js} +1 -1
- package/web-app/dist/assets/{chevron-left-W7O0vC_m.js → chevron-left-1TrnIEZS.js} +1 -1
- package/web-app/dist/assets/{circle-alert-Cxjq9fxB.js → circle-alert-8Bf6xEDx.js} +1 -1
- package/web-app/dist/assets/{clock-C8EqzmNl.js → clock-DB8NE6P5.js} +1 -1
- package/web-app/dist/assets/{cloud-xNgp4g3Y.js → cloud-DUSL3wZS.js} +1 -1
- package/web-app/dist/assets/{code-xml-B-lyNDLJ.js → code-xml-DbjryWNl.js} +1 -1
- package/web-app/dist/assets/{database-DoW5lmWc.js → database-DrezrMSs.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-BcARA5sC.js → dollar-sign-CGbaLMzR.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-CehcII0J.js → file-code-corner-Dfypa-r4.js} +1 -1
- package/web-app/dist/assets/{file-plus-DI9Zo5hP.js → file-plus-CzV5L2Gd.js} +1 -1
- package/web-app/dist/assets/{globe-CiZWBk3H.js → globe-BoYjTwPQ.js} +1 -1
- package/web-app/dist/assets/{hammer-3qv_T2N-.js → hammer-WO-HuiyJ.js} +1 -1
- package/web-app/dist/assets/{index-gF9grAz-.js → index-CjIOUjnd.js} +3 -3
- package/web-app/dist/assets/{layers-CN7d0By9.js → layers-Bn6jtKNk.js} +1 -1
- package/web-app/dist/assets/{loader-circle-DywcM-im.js → loader-circle-gERWRnmW.js} +1 -1
- package/web-app/dist/assets/{lock-_hdSmkU4.js → lock-BIiMLJq8.js} +1 -1
- package/web-app/dist/assets/{package-BV5zgqxJ.js → package-dLvDSVys.js} +1 -1
- package/web-app/dist/assets/{plus-CU_Hh4-x.js → plus-BHM3ZLhZ.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-DMK-fNl-.js → refresh-cw-BAp_alv6.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-CEB3_v5_.js → rotate-ccw-CBhwq_MG.js} +1 -1
- package/web-app/dist/assets/{scroll-text-BSdXxcRN.js → scroll-text-DoS-6dfU.js} +1 -1
- package/web-app/dist/assets/{server-CAMlP1yJ.js → server-5JgdDWQO.js} +1 -1
- package/web-app/dist/assets/{shield-alert-B-r1Dt3h.js → shield-alert-DjmlLT34.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-CYfxw9o4.js → thumbs-up-BtiEXl3p.js} +1 -1
- package/web-app/dist/assets/{trash-2-DX3nGPS8.js → trash-2-ClsWU_Vg.js} +1 -1
- package/web-app/dist/assets/{trending-down-Bt-hSntd.js → trending-down-DPS4M7_X.js} +1 -1
- package/web-app/dist/assets/{trending-up-DVSlvQp8.js → trending-up-D6OJ38-V.js} +1 -1
- package/web-app/dist/assets/{upload-D45RRHAP.js → upload-DOCzLUpF.js} +1 -1
- package/web-app/dist/assets/{usePolling-fq_Xj8iw.js → usePolling-CljQTWj_.js} +1 -1
- package/web-app/dist/assets/{user-BWcJkLlr.js → user-C0G82Weu.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/packages/control-plane/ui/dist/assets/index-CUQ65vJY.css +0 -1
- package/packages/control-plane/ui/dist/assets/index-CwwGfp6E.js +0 -129
package/loki-ts/package.json
CHANGED
|
@@ -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
package/package.json
CHANGED
|
@@ -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 = [
|
|
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
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
|
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 =
|
|
5925
|
-
const b =
|
|
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
|
|
5965
|
-
import { existsSync as existsSync3, readFileSync as
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
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))
|