loki-mode 8.1.0 → 8.2.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.
- package/README.md +32 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +45 -4
- package/autonomy/council-v2.sh +63 -1
- package/autonomy/grill.sh +32 -0
- package/autonomy/lib/done-recognition.sh +16 -4
- package/autonomy/lib/fast_verify.py +346 -0
- package/autonomy/lib/no_mock_scan.py +21 -1
- package/autonomy/lib/prd-enrich.sh +7 -1
- package/autonomy/lib/proof-generator.py +146 -6
- package/autonomy/lib/proof-template.html +60 -12
- package/autonomy/lib/proof-verify.py +52 -0
- package/autonomy/loki +209 -2
- package/autonomy/verify.sh +12 -6
- package/dashboard/__init__.py +1 -1
- package/events/emit.sh +66 -6
- package/loki-ts/dist/loki.js +319 -316
- package/mcp/__init__.py +30 -12
- package/mcp/server.py +150 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +72 -0
- package/providers/codex.sh +13 -0
- package/providers/loader.sh +10 -4
- package/providers/model_catalog.json +50 -0
- package/providers/models.sh +26 -3
- package/providers/opencode.sh +145 -0
- package/references/design-archetypes.md +85 -0
|
@@ -97,8 +97,14 @@ _loki_prd_enrich_invoke() {
|
|
|
97
97
|
# Decide whether enrichment should even be attempted. Returns 0 (attempt)
|
|
98
98
|
# only when the active provider is claude and not in degraded mode.
|
|
99
99
|
_loki_prd_enrich_provider_ok() {
|
|
100
|
-
[ "${LOKI_PROVIDER:-claude}" = "claude" ] || return 1
|
|
101
100
|
[ "${PROVIDER_DEGRADED:-false}" != "true" ] || return 1
|
|
101
|
+
# v8.2.0: capability, not identity. A provider exposing the timeout-able
|
|
102
|
+
# argv seam can run enrichment regardless of which CLI it is. Mirrors
|
|
103
|
+
# _loki_done_recog_provider_ok (autonomy/lib/done-recognition.sh).
|
|
104
|
+
if type provider_invoke_argv >/dev/null 2>&1; then
|
|
105
|
+
return 0
|
|
106
|
+
fi
|
|
107
|
+
[ "${LOKI_PROVIDER:-claude}" = "claude" ] || return 1
|
|
102
108
|
# v8: the raw-SDK enrich path (LOKI_SDK_PRD_ENRICH=1) needs no claude binary,
|
|
103
109
|
# so attempt is viable when that path is usable (bridge + bun). The invoke fn
|
|
104
110
|
# still fails closed to claude on an SDK miss.
|
|
@@ -243,6 +243,70 @@ def _norm_gate_status(raw):
|
|
|
243
243
|
return "inconclusive"
|
|
244
244
|
|
|
245
245
|
|
|
246
|
+
# TRUST-4: gate provenance. The discriminating property of a verification signal
|
|
247
|
+
# is NOT how many checks ran -- it is whether the checker sits OUTSIDE the agent's
|
|
248
|
+
# control. arXiv 2606.28438 shows AI-self-gates "look strong early but later lose
|
|
249
|
+
# their filtering effect", drifting into "a rubber-stamp regime where acceptance
|
|
250
|
+
# scores rise while benchmark correctness falls". arXiv 2607.05904 shows a judge
|
|
251
|
+
# conditioned on a candidate "scores plausibility, not correctness": self-play
|
|
252
|
+
# drove judge pass rate 0.72 -> 0.94 while TRUE accuracy stayed 0.20, and "a
|
|
253
|
+
# strict three-judge ensemble still accepts 55% of them". So a model-coupled gate
|
|
254
|
+
# is REPORTED but may never lift the headline.
|
|
255
|
+
#
|
|
256
|
+
# ADVISORY = the agent (or a model it prompts) authored the verdict. Everything
|
|
257
|
+
# else is EXOGENOUS: a deterministic script whose output the agent cannot write.
|
|
258
|
+
#
|
|
259
|
+
# Membership is keyed on the ADVISORY side only, and an UNKNOWN gate defaults to
|
|
260
|
+
# EXOGENOUS. That direction is load-bearing and must never be inverted: an
|
|
261
|
+
# unrecognized gate then still counts against VERIFIED (fail-closed). Defaulting
|
|
262
|
+
# unknown gates to advisory would let any newly-added or renamed gate silently
|
|
263
|
+
# lose its power to block -- the exact fake-green vector this split exists to
|
|
264
|
+
# close. Names are matched on a normalized key because run.sh emits BOTH
|
|
265
|
+
# spellings for the same gate (static-analysis / static_analysis, test-mutation /
|
|
266
|
+
# mutation_integrity), verified against run.sh's gate_failures writers.
|
|
267
|
+
_ADVISORY_GATES = frozenset((
|
|
268
|
+
# The agent writes both the test and the fix, so a green suite is a claim
|
|
269
|
+
# about its own work, not an independent measurement.
|
|
270
|
+
"test_coverage", "unit_tests", "test_suite", "semantic_tests", "tests",
|
|
271
|
+
# LLM-judgment gates: blind council, devil's advocate, magic-module debate.
|
|
272
|
+
"code_review", "devils_advocate", "devil_advocate", "magic_debate",
|
|
273
|
+
"council", "anti_sycophancy",
|
|
274
|
+
))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _gate_key(name):
|
|
278
|
+
"""Normalize a gate name for provenance lookup.
|
|
279
|
+
|
|
280
|
+
run.sh emits the same gate under multiple spellings (`static-analysis` vs
|
|
281
|
+
`static_analysis`), and track_gate_failure appends `_PAUSED`/`_ESCALATED`
|
|
282
|
+
/`_not_run` suffixes. Fold all of them onto one key so classification cannot
|
|
283
|
+
be defeated by a cosmetic rename.
|
|
284
|
+
"""
|
|
285
|
+
s = str(name or "").strip().lower().replace("-", "_")
|
|
286
|
+
s = re.sub(r"_(paused|escalated|not_run|blocked)$", "", s)
|
|
287
|
+
return s
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _gate_provenance(name):
|
|
291
|
+
"""'advisory' for a model-authored gate, else 'exogenous' (fail-closed)."""
|
|
292
|
+
return "advisory" if _gate_key(name) in _ADVISORY_GATES else "exogenous"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _is_exogenous(gate):
|
|
296
|
+
"""Provenance of a collected gate dict, honoring the stamped value.
|
|
297
|
+
|
|
298
|
+
Reads the `provenance` key stamped by _collect_quality_gates so the
|
|
299
|
+
`unresolved` override (a gate that HALTED the run counts as an execution
|
|
300
|
+
fact) is respected. Falls back to name lookup for a gate dict that never
|
|
301
|
+
passed through the collector. Fail-closed: anything not positively
|
|
302
|
+
identified as advisory counts as exogenous.
|
|
303
|
+
"""
|
|
304
|
+
stamped = gate.get("provenance")
|
|
305
|
+
if stamped:
|
|
306
|
+
return stamped == "exogenous"
|
|
307
|
+
return _gate_provenance(gate.get("name")) == "exogenous"
|
|
308
|
+
|
|
309
|
+
|
|
246
310
|
def _collect_quality_gates(loki_dir):
|
|
247
311
|
gates_raw = _read_json(
|
|
248
312
|
os.path.join(loki_dir, "state", "quality-gates.json"), default=None
|
|
@@ -354,14 +418,48 @@ def _collect_quality_gates(loki_dir):
|
|
|
354
418
|
for name in failed_names:
|
|
355
419
|
if name in by_name:
|
|
356
420
|
by_name[name]["status"] = "failed"
|
|
421
|
+
by_name[name]["unresolved"] = True
|
|
357
422
|
else:
|
|
358
|
-
gate = {"name": name, "status": "failed"}
|
|
423
|
+
gate = {"name": name, "status": "failed", "unresolved": True}
|
|
359
424
|
gates.append(gate)
|
|
360
425
|
by_name[name] = gate
|
|
361
426
|
|
|
427
|
+
# TRUST-4: stamp provenance on every gate at the single point the list is
|
|
428
|
+
# finalized, so every downstream reader (headline, template, verifier) sees
|
|
429
|
+
# the same classification and none can drift.
|
|
430
|
+
# An UNRESOLVED gate (listed in gate-failures.txt) is classified EXOGENOUS
|
|
431
|
+
# even when the gate itself is model-coupled. The fact being recorded is not
|
|
432
|
+
# "a judge disliked the code" -- it is "the run halted here and never
|
|
433
|
+
# cleared this blocker", which is an execution outcome the agent did not
|
|
434
|
+
# author. Without this, a run stopped dead by an unresolved code_review
|
|
435
|
+
# would emit a green receipt: the "cryptographically valid but semantically
|
|
436
|
+
# false green" the gate-failures merge above exists to prevent.
|
|
437
|
+
for gate in gates:
|
|
438
|
+
gate["provenance"] = (
|
|
439
|
+
"exogenous" if gate.get("unresolved")
|
|
440
|
+
else _gate_provenance(gate.get("name"))
|
|
441
|
+
)
|
|
442
|
+
|
|
362
443
|
total = len(gates)
|
|
363
444
|
passed = sum(1 for gate in gates if gate.get("status") == "passed")
|
|
364
|
-
|
|
445
|
+
exo = [g for g in gates if g.get("provenance") == "exogenous"]
|
|
446
|
+
adv = [g for g in gates if g.get("provenance") == "advisory"]
|
|
447
|
+
return {
|
|
448
|
+
"passed": passed,
|
|
449
|
+
"total": total,
|
|
450
|
+
"gates": gates,
|
|
451
|
+
# Pre-split counts so the renderer never has to re-derive provenance.
|
|
452
|
+
"exogenous": {
|
|
453
|
+
"passed": sum(1 for g in exo if g.get("status") == "passed"),
|
|
454
|
+
"total": len(exo),
|
|
455
|
+
"gates": exo,
|
|
456
|
+
},
|
|
457
|
+
"advisory": {
|
|
458
|
+
"passed": sum(1 for g in adv if g.get("status") == "passed"),
|
|
459
|
+
"total": len(adv),
|
|
460
|
+
"gates": adv,
|
|
461
|
+
},
|
|
462
|
+
}
|
|
365
463
|
|
|
366
464
|
|
|
367
465
|
def _collect_build(loki_dir):
|
|
@@ -1032,8 +1130,18 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
|
|
|
1032
1130
|
"execution": termination,
|
|
1033
1131
|
"build": build,
|
|
1034
1132
|
"tests": tests,
|
|
1133
|
+
# TRUST-4: carry `provenance` into the facts projection. _compute_headline
|
|
1134
|
+
# and _compute_degraded read THIS list, so dropping the field silently
|
|
1135
|
+
# sent them back to name-only lookup -- which mis-classified an
|
|
1136
|
+
# UNRESOLVED code_review (a run-halting execution fact) as advisory and
|
|
1137
|
+
# green-washed a blocked run.
|
|
1035
1138
|
"quality_gates": [
|
|
1036
|
-
{
|
|
1139
|
+
{
|
|
1140
|
+
"name": g.get("name", ""),
|
|
1141
|
+
"status": g.get("status", "not_run"),
|
|
1142
|
+
"provenance": g.get("provenance")
|
|
1143
|
+
or _gate_provenance(g.get("name")),
|
|
1144
|
+
}
|
|
1037
1145
|
for g in (quality_gates.get("gates") or [])
|
|
1038
1146
|
],
|
|
1039
1147
|
"security": security,
|
|
@@ -1197,8 +1305,14 @@ def _compute_degraded(facts):
|
|
|
1197
1305
|
else ("exit_code=%s" % build.get("exit_code"))
|
|
1198
1306
|
out.append({"item": "build", "status": build.get("status"),
|
|
1199
1307
|
"reason": reason})
|
|
1308
|
+
# TRUST-4: only EXOGENOUS gates enter the degraded ledger, because degraded[]
|
|
1309
|
+
# is an INPUT to the headline (a non-empty ledger blocks VERIFIED). Letting an
|
|
1310
|
+
# advisory gate in here would give a model-authored verdict the power to
|
|
1311
|
+
# downgrade the headline through the back door, which is exactly what this
|
|
1312
|
+
# split forbids. Advisory outcomes are still reported in full -- they render
|
|
1313
|
+
# from quality_gates.advisory, which the template shows verbatim.
|
|
1200
1314
|
for g in facts.get("quality_gates") or []:
|
|
1201
|
-
if g.get("status") in weak:
|
|
1315
|
+
if g.get("status") in weak and _is_exogenous(g):
|
|
1202
1316
|
out.append({"item": "quality_gate:%s" % g.get("name", ""),
|
|
1203
1317
|
"status": g.get("status"),
|
|
1204
1318
|
"reason": "gate %s" % g.get("status")})
|
|
@@ -1289,12 +1403,21 @@ def _compute_headline(facts, degraded):
|
|
|
1289
1403
|
and execution_outcome not in ("complete", "completed", "success")
|
|
1290
1404
|
)
|
|
1291
1405
|
)
|
|
1406
|
+
# TRUST-4: only an EXOGENOUS gate failure forces NOT VERIFIED. An advisory
|
|
1407
|
+
# (model-authored) gate is reported but cannot move the verdict in EITHER
|
|
1408
|
+
# direction -- see _ADVISORY_GATES for the research basis. Note the
|
|
1409
|
+
# asymmetry is deliberate and one-way: advisory results are barred from
|
|
1410
|
+
# UPGRADING a verdict (below, in any_verified), and barred from downgrading
|
|
1411
|
+
# one here, because a judge that scores plausibility is not a measurement.
|
|
1412
|
+
# tests.status keeps its own hard-fail check: it is the recorded suite
|
|
1413
|
+
# outcome (an exit code), not the model's opinion of the suite.
|
|
1292
1414
|
any_failed = (
|
|
1293
1415
|
execution_failed
|
|
1294
1416
|
or tests.get("status") == "failed"
|
|
1295
1417
|
or build.get("status") == "failed"
|
|
1296
1418
|
or any(g.get("status") == "failed"
|
|
1297
|
-
for g in (facts.get("quality_gates") or [])
|
|
1419
|
+
for g in (facts.get("quality_gates") or [])
|
|
1420
|
+
if _is_exogenous(g))
|
|
1298
1421
|
or sec_high
|
|
1299
1422
|
or fn_failed
|
|
1300
1423
|
)
|
|
@@ -1315,11 +1438,15 @@ def _compute_headline(facts, degraded):
|
|
|
1315
1438
|
# produced code emit "VERIFIED WITH GAPS" - a fake-green at the receipt. Only
|
|
1316
1439
|
# a fact that actually ran and passed (tests/build verified, or a passed gate)
|
|
1317
1440
|
# may qualify; otherwise the honest headline is NOT VERIFIED.
|
|
1441
|
+
# TRUST-4: an advisory PASS is not positive evidence. A run whose ONLY green
|
|
1442
|
+
# signals are a council vote and a devil's-advocate nod has proven nothing
|
|
1443
|
+
# deterministically, so it must not reach "VERIFIED WITH GAPS" on that basis.
|
|
1318
1444
|
any_verified = (
|
|
1319
1445
|
tests.get("status") == "verified"
|
|
1320
1446
|
or build.get("status") == "verified"
|
|
1321
1447
|
or any(g.get("status") == "passed"
|
|
1322
|
-
for g in (facts.get("quality_gates") or [])
|
|
1448
|
+
for g in (facts.get("quality_gates") or [])
|
|
1449
|
+
if _is_exogenous(g))
|
|
1323
1450
|
)
|
|
1324
1451
|
if any_verified and degraded:
|
|
1325
1452
|
return "VERIFIED WITH GAPS"
|
|
@@ -1481,6 +1608,19 @@ def _render_fallback_html(proof):
|
|
|
1481
1608
|
ver = proof.get("verification", {})
|
|
1482
1609
|
rows.append('<p class="hash">Integrity hash (%s): %s</p>' % (
|
|
1483
1610
|
esc(ver.get("algo", "sha256")), esc(ver.get("hash", ""))))
|
|
1611
|
+
# Signing state, stated plainly. Mirrors renderProvenance in
|
|
1612
|
+
# proof-template.html (the primary renderer); this fallback path must not
|
|
1613
|
+
# be quieter about provenance than the page it stands in for.
|
|
1614
|
+
if ver.get("gpg_signature"):
|
|
1615
|
+
rows.append("<p>Signature: SIGNED (detached GPG over the canonical "
|
|
1616
|
+
"bytes). A verifier holding the signer public key can "
|
|
1617
|
+
"confirm provenance offline: loki proof verify <id></p>")
|
|
1618
|
+
else:
|
|
1619
|
+
rows.append("<p>Signature: UNSIGNED. The integrity hash proves the "
|
|
1620
|
+
"bytes were not edited after hashing; it does NOT prove "
|
|
1621
|
+
"who produced them, so this receipt trusts its generator. "
|
|
1622
|
+
"To sign future receipts, set LOKI_PROOF_GPG_KEY to a gpg "
|
|
1623
|
+
"key id (see docs/SIGNED-RECEIPTS.md).</p>")
|
|
1484
1624
|
red = proof.get("redaction", {})
|
|
1485
1625
|
rows.append("<p>Redaction applied: %s (%s redactions, rules v%s)</p>" % (
|
|
1486
1626
|
esc(red.get("applied")), esc(red.get("redactions_count")),
|
|
@@ -1117,6 +1117,24 @@
|
|
|
1117
1117
|
card.innerHTML = (head ? '<div style="margin-bottom:14px;">' + head + "</div>" : "") + body + link;
|
|
1118
1118
|
}
|
|
1119
1119
|
|
|
1120
|
+
// Render one row of gate chips. Shared by the exogenous and advisory blocks.
|
|
1121
|
+
function gateChips(gates) {
|
|
1122
|
+
var chips = "";
|
|
1123
|
+
if (!Array.isArray(gates)) return "";
|
|
1124
|
+
for (var i = 0; i < gates.length; i++) {
|
|
1125
|
+
var st = String(g(gates[i], "status", "")).toLowerCase();
|
|
1126
|
+
var cls = (st === "pass" || st === "passed") ? "pass" : (st === "skip" || st === "skipped") ? "skip" : "fail";
|
|
1127
|
+
var mark = cls === "pass" ? "OK" : cls === "skip" ? "-" : "X";
|
|
1128
|
+
chips += '<span class="gate ' + cls + '"><span class="mk">' + mark + "</span>" +
|
|
1129
|
+
esc(g(gates[i], "name", "gate")) + "</span>";
|
|
1130
|
+
}
|
|
1131
|
+
return chips ? '<div class="gates">' + chips + "</div>" : "";
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// TRUST-4: quality gates split by PROVENANCE, not by outcome. The two blocks
|
|
1135
|
+
// read from quality_gates.exogenous / quality_gates.advisory, the exact field
|
|
1136
|
+
// names proof-generator.py writes. Falls back to the flat pre-split list so an
|
|
1137
|
+
// older receipt still renders.
|
|
1120
1138
|
function renderGates(p) {
|
|
1121
1139
|
var sec = document.getElementById("secGates");
|
|
1122
1140
|
var card = document.getElementById("gatesCard");
|
|
@@ -1127,18 +1145,34 @@
|
|
|
1127
1145
|
sec.classList.add("hide");
|
|
1128
1146
|
return;
|
|
1129
1147
|
}
|
|
1130
|
-
var
|
|
1131
|
-
var
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1148
|
+
var exo = g(p, "quality_gates.exogenous", null);
|
|
1149
|
+
var adv = g(p, "quality_gates.advisory", null);
|
|
1150
|
+
if (!exo && !adv) {
|
|
1151
|
+
// Pre-TRUST-4 receipt: render the flat list exactly as before.
|
|
1152
|
+
card.innerHTML = '<p class="note" style="margin:0 0 12px;">' + passed +
|
|
1153
|
+
" of " + total + " gates passed.</p>" + gateChips(gates);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
var html = "";
|
|
1157
|
+
var eg = g(exo, "gates", []);
|
|
1158
|
+
if (Array.isArray(eg) && eg.length > 0) {
|
|
1159
|
+
html += '<h3 style="margin:0 0 4px;font-size:14px;">Independent checks (count toward the verdict)</h3>' +
|
|
1160
|
+
'<p class="note" style="margin:0 0 10px;">' +
|
|
1161
|
+
num(g(exo, "passed", 0)) + " of " + num(g(exo, "total", 0)) +
|
|
1162
|
+
" passed. These are deterministic programs the agent cannot write the " +
|
|
1163
|
+
"result of, so only these set the verdict above.</p>" + gateChips(eg);
|
|
1164
|
+
}
|
|
1165
|
+
var ag = g(adv, "gates", []);
|
|
1166
|
+
if (Array.isArray(ag) && ag.length > 0) {
|
|
1167
|
+
html += '<h3 style="margin:18px 0 4px;font-size:14px;">Advisory checks (reported, not counted)</h3>' +
|
|
1168
|
+
'<p class="opinion-note"><span aria-hidden="true">!</span>' +
|
|
1169
|
+
num(g(adv, "passed", 0)) + " of " + num(g(adv, "total", 0)) +
|
|
1170
|
+
" passed. These are the agent judging its own work (it wrote both the " +
|
|
1171
|
+
"tests and the code, or a model graded the result), so a model that is " +
|
|
1172
|
+
"confidently wrong scores itself green here. They are shown for context " +
|
|
1173
|
+
"and can never raise or lower the verdict.</p>" + gateChips(ag);
|
|
1140
1174
|
}
|
|
1141
|
-
card.innerHTML =
|
|
1175
|
+
card.innerHTML = html;
|
|
1142
1176
|
}
|
|
1143
1177
|
|
|
1144
1178
|
function renderFlagged(p) {
|
|
@@ -1204,9 +1238,23 @@
|
|
|
1204
1238
|
if (hash) {
|
|
1205
1239
|
kv += '<div class="k">Integrity hash</div><div class="v">' + esc(algo) + ":" + esc(hash) + "</div>";
|
|
1206
1240
|
}
|
|
1241
|
+
// Signing state, named explicitly. The hash-note below already says the
|
|
1242
|
+
// hash does not prove provenance; without this row a reader is told what
|
|
1243
|
+
// the receipt CANNOT do but never which state this particular receipt is
|
|
1244
|
+
// in, nor how to change it. Signing is gated on LOKI_PROOF_GPG_KEY in
|
|
1245
|
+
// proof-generator.py and is OFF by default -- deliberately, because a
|
|
1246
|
+
// self-minted key would flip proof-verify.py's generator_trusted to false
|
|
1247
|
+
// while the key came from the very process under audit.
|
|
1248
|
+
var signed = !!g(p, "verification.gpg_signature", "");
|
|
1249
|
+
kv += '<div class="k">Signature</div><div class="v">' +
|
|
1250
|
+
(signed ? "SIGNED (detached GPG)" : "UNSIGNED") + "</div>";
|
|
1251
|
+
var signNote = signed
|
|
1252
|
+
? '<p class="hash-note">This receipt carries a detached GPG signature over the same canonical bytes that were hashed. A verifier holding the signer public key can confirm its provenance offline with <code>loki proof verify</code>.</p>'
|
|
1253
|
+
: '<p class="hash-note">This receipt is UNSIGNED, so it trusts the generator that produced it (<code>loki proof verify</code> reports <code>generator_trusted: true</code>). To sign future receipts, set <code>LOKI_PROOF_GPG_KEY</code> to a GPG key id before the run.</p>';
|
|
1207
1254
|
card.innerHTML = '<div class="kv">' + kv + "</div>" +
|
|
1208
1255
|
(hash ? '<p class="hash-note">The integrity hash is a ' + esc(algo) +
|
|
1209
|
-
" digest of this proof. It proves the artifact has not been altered after it was emitted. It does not, by itself, prove who or what produced the work.</p>" : "")
|
|
1256
|
+
" digest of this proof. It proves the artifact has not been altered after it was emitted. It does not, by itself, prove who or what produced the work.</p>" : "") +
|
|
1257
|
+
signNote;
|
|
1210
1258
|
}
|
|
1211
1259
|
|
|
1212
1260
|
function renderCta(p) {
|
|
@@ -65,6 +65,7 @@ CLI:
|
|
|
65
65
|
import hashlib
|
|
66
66
|
import json
|
|
67
67
|
import os
|
|
68
|
+
import re
|
|
68
69
|
import subprocess
|
|
69
70
|
import sys
|
|
70
71
|
|
|
@@ -176,6 +177,57 @@ def _to_int(v, default=0):
|
|
|
176
177
|
# headline re-derivation (MUST match proof-generator._compute_headline exactly)
|
|
177
178
|
# ---------------------------------------------------------------------------
|
|
178
179
|
|
|
180
|
+
# DRIFT-GUARD: mirrored from proof-generator.py. A gate the AGENT authored the
|
|
181
|
+
# input to (it writes both the test and the fix) or that is an LLM judgment
|
|
182
|
+
# cannot be independent evidence, so it must never force or lift a headline.
|
|
183
|
+
# Keyed on the ADVISORY set so an unknown or renamed gate defaults to EXOGENOUS
|
|
184
|
+
# and keeps its power to block -- fail-closed, never fail-open.
|
|
185
|
+
#
|
|
186
|
+
# This block exists because the generator gained a provenance split while this
|
|
187
|
+
# file did not, and the two then disagreed about what a receipt meant. The
|
|
188
|
+
# verifier is the INDEPENDENT re-derivation users are told to trust; if it
|
|
189
|
+
# cannot reproduce the generator's headline, `loki proof verify` reports drift
|
|
190
|
+
# on a receipt that was never tampered with -- a false alarm from our own trust
|
|
191
|
+
# artifact, which is worse than no verifier at all.
|
|
192
|
+
_ADVISORY_GATES = frozenset((
|
|
193
|
+
"test_coverage", "unit_tests", "test_suite", "semantic_tests", "tests",
|
|
194
|
+
"code_review", "devils_advocate", "devil_advocate", "magic_debate",
|
|
195
|
+
"council", "anti_sycophancy",
|
|
196
|
+
))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _gate_key(name):
|
|
200
|
+
"""Normalize a gate name for provenance lookup (mirrors the generator).
|
|
201
|
+
|
|
202
|
+
run.sh emits the same gate under multiple spellings (`static-analysis` vs
|
|
203
|
+
`static_analysis`) and track_gate_failure appends `_PAUSED`/`_ESCALATED`,
|
|
204
|
+
so an exact-match lookup would misfile real gates.
|
|
205
|
+
"""
|
|
206
|
+
s = str(name or "").strip().lower().replace("-", "_")
|
|
207
|
+
return re.sub(r"_(paused|escalated|not_run|blocked)$", "", s)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _gate_provenance(name):
|
|
211
|
+
"""'advisory' for a model-authored gate, else 'exogenous' (fail-closed)."""
|
|
212
|
+
return "advisory" if _gate_key(name) in _ADVISORY_GATES else "exogenous"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _is_exogenous(gate):
|
|
216
|
+
"""Provenance of a gate dict, honoring a stamped value when present.
|
|
217
|
+
|
|
218
|
+
The generator stamps `provenance` so its `unresolved` override survives (a
|
|
219
|
+
gate that HALTED the run is an execution fact, not a model opinion). We
|
|
220
|
+
honor that stamp and fall back to name lookup for older receipts written
|
|
221
|
+
before the split existed.
|
|
222
|
+
"""
|
|
223
|
+
if not isinstance(gate, dict):
|
|
224
|
+
return True
|
|
225
|
+
stamped = gate.get("provenance")
|
|
226
|
+
if stamped:
|
|
227
|
+
return stamped == "exogenous"
|
|
228
|
+
return _gate_provenance(gate.get("name")) == "exogenous"
|
|
229
|
+
|
|
230
|
+
|
|
179
231
|
def _compute_headline(facts, degraded):
|
|
180
232
|
"""Deterministic headline re-derived from the recorded facts.
|
|
181
233
|
|