loki-mode 10.5.11 → 10.5.13

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 (92) hide show
  1. package/README.md +30 -962
  2. package/SKILL.md +3 -3
  3. package/VERSION +1 -1
  4. package/autonomy/app-runner.sh +2 -1
  5. package/autonomy/completion-council.sh +40 -3
  6. package/autonomy/lib/doctor-fix.py +1 -1
  7. package/autonomy/lib/proof-generator.py +40 -84
  8. package/autonomy/lib/proof-template.html +7 -11
  9. package/autonomy/loki +118 -69
  10. package/autonomy/receipt_jwt.py +59 -14
  11. package/autonomy/run.sh +91 -124
  12. package/autonomy/trigger-server.py +2 -2
  13. package/autonomy/verify.sh +37 -2
  14. package/dashboard/__init__.py +1 -1
  15. package/dashboard/server.py +2 -2
  16. package/docs/COMPETITIVE-NEXT-10.md +3 -4
  17. package/docs/INSTALLATION.md +1 -1
  18. package/docs/README-FULL.md +977 -0
  19. package/docs/SIGNED-RECEIPTS.md +40 -82
  20. package/docs/TOOLS.md +0 -23
  21. package/docs/VERIFICATION-COST.md +2 -2
  22. package/docs/v10/ADOPTION-PLAN.md +1 -1
  23. package/docs/v10/BOARD.md +42 -22
  24. package/docs/v10/DECISIONS.md +11 -0
  25. package/docs/v10/ENGINE.md +4 -4
  26. package/docs/v10/FOUNDER-QUEUE.md +1 -0
  27. package/docs/v10/GUIDE.md +0 -4
  28. package/docs/v10/PROGRESS.md +43 -0
  29. package/loki-ts/dist/loki.js +587 -563
  30. package/mcp/__init__.py +1 -1
  31. package/package.json +2 -2
  32. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  33. package/tools/gate-status.py +3 -46
  34. package/tools/receipt-attest.py +1 -1
  35. package/web-app/dist/assets/{AdminPage-bi-XcWhD.js → AdminPage-C8zraQ0u.js} +1 -1
  36. package/web-app/dist/assets/{Avatar-CmeIKHo6.js → Avatar-DnjIA4Ap.js} +1 -1
  37. package/web-app/dist/assets/{Badge-Bc3RMd7Y.js → Badge-C487hf0b.js} +1 -1
  38. package/web-app/dist/assets/{Button-Dc7yzWmJ.js → Button-5AWAEksl.js} +1 -1
  39. package/web-app/dist/assets/{CockpitPage-C_aRTgNm.js → CockpitPage-B6_1gTFO.js} +1 -1
  40. package/web-app/dist/assets/{ComparePage-B8eqE3GD.js → ComparePage-DDbC8-yi.js} +1 -1
  41. package/web-app/dist/assets/{ErrorBoundary-BRVSyrME.js → ErrorBoundary-CWHKwjEp.js} +1 -1
  42. package/web-app/dist/assets/{EvidenceReceiptPanel-DBkXGSAF.js → EvidenceReceiptPanel-MfJq92Ty.js} +1 -1
  43. package/web-app/dist/assets/{GitHubIssuesPanel-rRZ4rP-a.js → GitHubIssuesPanel-BBdijzdg.js} +1 -1
  44. package/web-app/dist/assets/{GitHubPRsPanel-NMj2a_-3.js → GitHubPRsPanel-DjBHyymT.js} +1 -1
  45. package/web-app/dist/assets/{HomePage-YGPKiV9F.js → HomePage-B_6m7ITK.js} +2 -2
  46. package/web-app/dist/assets/{LoginPage-CVx7_jtD.js → LoginPage-OGgMvfjT.js} +1 -1
  47. package/web-app/dist/assets/{MagicPage-G9BuqW4T.js → MagicPage-m4MSfxWf.js} +1 -1
  48. package/web-app/dist/assets/{MetricsPage-CrPo66D6.js → MetricsPage-D8Ea1ALe.js} +1 -1
  49. package/web-app/dist/assets/{NotFoundPage-CbjchFXV.js → NotFoundPage-ttvXL-7K.js} +1 -1
  50. package/web-app/dist/assets/{ProjectPage-CmPoA170.js → ProjectPage-CDX8tSPV.js} +3 -3
  51. package/web-app/dist/assets/{ProjectsPage-CfVMc4nw.js → ProjectsPage-BQ73nYWf.js} +1 -1
  52. package/web-app/dist/assets/{SettingsPage-6nZ-vONY.js → SettingsPage-B6k71q2g.js} +1 -1
  53. package/web-app/dist/assets/{ShowcasePage-CxdZjDzY.js → ShowcasePage-CNbwtr7D.js} +1 -1
  54. package/web-app/dist/assets/{SystemSettingsPage-Bu0kwwMe.js → SystemSettingsPage-CD2qys43.js} +1 -1
  55. package/web-app/dist/assets/{TeamsPage-Ck6xWgOM.js → TeamsPage-DAmsRIVD.js} +1 -1
  56. package/web-app/dist/assets/{TemplatesPage-BduvcCsH.js → TemplatesPage-CcRyyUhL.js} +1 -1
  57. package/web-app/dist/assets/{TerminalOutput-BmleKz7Q.js → TerminalOutput-DRquxtQ1.js} +1 -1
  58. package/web-app/dist/assets/{activity-xUOENXj1.js → activity-DUDUi3Qd.js} +1 -1
  59. package/web-app/dist/assets/{bell-Do1DnngS.js → bell-tDpCOcHx.js} +1 -1
  60. package/web-app/dist/assets/{bot-Csus-lhk.js → bot-87IR4aTF.js} +1 -1
  61. package/web-app/dist/assets/{check-CDMuF2fW.js → check-C2Y2z-Ge.js} +1 -1
  62. package/web-app/dist/assets/{chevron-left-1pxhei7a.js → chevron-left-CsPFT88x.js} +1 -1
  63. package/web-app/dist/assets/{circle-alert-HufOSfwc.js → circle-alert-Cp2UJikd.js} +1 -1
  64. package/web-app/dist/assets/{clock-BoCMViPQ.js → clock-DZ1cwNyf.js} +1 -1
  65. package/web-app/dist/assets/{cloud-B7JfKA_r.js → cloud-q5TrIOV-.js} +1 -1
  66. package/web-app/dist/assets/{code-xml-BduQ3MqM.js → code-xml-0TNGOS3c.js} +1 -1
  67. package/web-app/dist/assets/{database-CQ9s31cU.js → database-g73vgDVx.js} +1 -1
  68. package/web-app/dist/assets/{dollar-sign-CVJu5-A0.js → dollar-sign-DAKkR_MO.js} +1 -1
  69. package/web-app/dist/assets/{file-code-corner-BPu1BX6Z.js → file-code-corner-DTAvdxjl.js} +1 -1
  70. package/web-app/dist/assets/{file-plus-CGa7y-QG.js → file-plus-D5SB-1Uk.js} +1 -1
  71. package/web-app/dist/assets/{globe-ClS0c8Cw.js → globe-j5YaNmx9.js} +1 -1
  72. package/web-app/dist/assets/{hammer-CeljzUTd.js → hammer-C9yoXOC6.js} +1 -1
  73. package/web-app/dist/assets/{index-Dn35XhiL.js → index--EayJsJ-.js} +3 -3
  74. package/web-app/dist/assets/{layers-CzGZ-NNL.js → layers-B4mxkWdH.js} +1 -1
  75. package/web-app/dist/assets/{loader-circle-Dy1j6q2e.js → loader-circle-CP-qD1W2.js} +1 -1
  76. package/web-app/dist/assets/{lock-pmDyRsWL.js → lock-DwHNUSTU.js} +1 -1
  77. package/web-app/dist/assets/{package-C1dXDph_.js → package-BxLWioQ8.js} +1 -1
  78. package/web-app/dist/assets/{plus-CKPSHefh.js → plus-B-43Byzl.js} +1 -1
  79. package/web-app/dist/assets/{refresh-cw-CRmwRgDw.js → refresh-cw-5s47zAFV.js} +1 -1
  80. package/web-app/dist/assets/{rotate-ccw-Br04gZR3.js → rotate-ccw-BewZ8_Rx.js} +1 -1
  81. package/web-app/dist/assets/{scroll-text-BHjpthNc.js → scroll-text-DPNpHL-Z.js} +1 -1
  82. package/web-app/dist/assets/{server-Db3IfTpl.js → server-hL8_-vRq.js} +1 -1
  83. package/web-app/dist/assets/{shield-alert-jJXWKLDZ.js → shield-alert-DAGc2aLO.js} +1 -1
  84. package/web-app/dist/assets/{thumbs-up-BksBTkqT.js → thumbs-up-DRGzcWEH.js} +1 -1
  85. package/web-app/dist/assets/{trash-2-CzXuGa-4.js → trash-2-Bo_YPIqS.js} +1 -1
  86. package/web-app/dist/assets/{trending-down-c_JXJq0s.js → trending-down-BQ1cK9F3.js} +1 -1
  87. package/web-app/dist/assets/{trending-up-DtvURYKn.js → trending-up-BpnkTO1T.js} +1 -1
  88. package/web-app/dist/assets/{upload-BSsOPrmR.js → upload-DDdQNxmW.js} +1 -1
  89. package/web-app/dist/assets/{usePolling-CFWdgkmg.js → usePolling-z_FXsGz7.js} +1 -1
  90. package/web-app/dist/assets/{user-CL-HjV4R.js → user-obPH42HX.js} +1 -1
  91. package/web-app/dist/index.html +1 -1
  92. package/tools/signing-status.py +0 -292
package/SKILL.md CHANGED
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: loki-mode
3
- description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
3
+ description: An autonomous software factory that knows what it is supposed to deliver, and proves it did. Use when the user says Loki Mode or asks to build, fix or verify software autonomously.
4
4
  ---
5
5
 
6
- # Loki Mode v10.5.11
6
+ # Loki Mode v10.5.13
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
470
470
 
471
471
  ---
472
472
 
473
- **v10.5.11 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v10.5.13 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 10.5.11
1
+ 10.5.13
@@ -1015,7 +1015,8 @@ _install_node_deps() {
1015
1015
  local dir="$1"
1016
1016
  if [ -f "$dir/package.json" ] && [ ! -d "$dir/node_modules" ]; then
1017
1017
  log_step "App Runner: installing node dependencies..."
1018
- (cd "$dir" && npm install >> "$_APP_RUNNER_DIR/app.log" 2>&1) || \
1018
+ # shellcheck disable=SC2046 # A-132: no lockfile the repo did not have
1019
+ (cd "$dir" && npm install $([ -f "$dir/package-lock.json" ] || echo --no-package-lock) >> "$_APP_RUNNER_DIR/app.log" 2>&1) || \
1019
1020
  log_warn "App Runner: npm install failed, app may not start"
1020
1021
  fi
1021
1022
  }
@@ -1865,6 +1865,30 @@ _loki_test_provenance() {
1865
1865
  # actually ran and was red); every inconclusive case passes through so a
1866
1866
  # legitimate completion is never falsely stopped. Default-on; opt out with
1867
1867
  # LOKI_EVIDENCE_GATE=0 (byte-identical to prior behavior, no read/write).
1868
+ # A-133: true (rc 0) when the changed-file union (arg 2, newline list, .loki/ already
1869
+ # excluded) is a trivial diff: at most 2 files and 20 changed lines, and none of
1870
+ # them a test, CI, auth/security, route/entrypoint, manifest, Docker or SQL file. Only the
1871
+ # costly persistence, auth and tenant probes are skipped; the free boot read never is.
1872
+ # Opt out: LOKI_EVIDENCE_TRIVIAL_SKIP=0.
1873
+ _council_trivial_diff() {
1874
+ local base="$1" files="$2" n added deleted f total=0
1875
+ [ "${LOKI_EVIDENCE_TRIVIAL_SKIP:-1}" = "0" ] && return 1
1876
+ n=$(printf '%s\n' "$files" | wc -l | tr -d ' ')
1877
+ [ "$n" -le 2 ] || return 1
1878
+ printf '%s\n' "$files" | grep -qiE '(^|/)(tests?|__tests__|specs?|e2e|cypress|playwright)(/|$)|\.(test|spec)\.[^/]+$|(^|/)test_[^/]+$|_test\.[^/]+$|conftest\.py$|^\.github/|^\.gitlab-ci|^\.circleci/|Jenkinsfile|azure-pipelines|^\.buildkite/|^\.travis|auth|login|logout|session|passw|secret|credential|token|jwt|oauth|saml|sso|security|permission|rbac|acl|crypt|\.env|\.pem$|\.key$|middleware|policy|tenant|rls|route|router|controller|endpoint|(^|/)api/|(^|/)pages/|(^|/)handlers?/|(^|/)(server|app|main|index)\.[a-z]+$|package\.json|Dockerfile|docker-compose|\.sql$|migrations?/' && return 1
1879
+ # Tracked changes vs the run base (committed, staged and unstaged), then untracked files.
1880
+ while read -r added deleted f; do
1881
+ [ -n "$f" ] || continue
1882
+ case "$added$deleted" in *[!0-9]*) return 1 ;; esac # binary: not trivial
1883
+ total=$((total + added + deleted))
1884
+ done < <(git diff --numstat "$base" -- . ':!.loki' 2>/dev/null || echo "x x x")
1885
+ while IFS= read -r f; do
1886
+ [ -n "$f" ] || continue
1887
+ total=$((total + $(wc -l < "$f" 2>/dev/null || echo 99)))
1888
+ done < <(git ls-files --others --exclude-standard -- . ':!.loki' 2>/dev/null)
1889
+ [ "$total" -le 20 ]
1890
+ }
1891
+
1868
1892
  council_evidence_gate() {
1869
1893
  # P2 (S-116): when the caller sets _LOKI_EVIDENCE_REQUIRE_TESTS=1, inconclusive
1870
1894
  # test evidence blocks instead of passing through. council_evaluate sets it as
@@ -1908,6 +1932,7 @@ council_evidence_gate() {
1908
1932
  # diff_inconclusive stays "false" on the conclusive branch below.
1909
1933
  local diff_inconclusive="false"
1910
1934
  local diff_inconclusive_reason=""
1935
+ local _trivial_diff="false"
1911
1936
  if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
1912
1937
  # No git repo => cannot prove fabrication => inconclusive => pass-through.
1913
1938
  diff_inconclusive="true"
@@ -1954,6 +1979,7 @@ council_evidence_gate() {
1954
1979
  local union_files
1955
1980
  union_files=$(printf '%s\n%s\n%s\n%s\n' "$committed_files" "$unstaged_files" "$staged_files" "$untracked_files" | grep -v '^$' | grep -vE '^\.loki/' | sort -u)
1956
1981
  if [ -n "$union_files" ]; then
1982
+ _council_trivial_diff "$base_sha" "$union_files" && _trivial_diff="true"
1957
1983
  diff_files=$(printf '%s\n' "$union_files" | wc -l | tr -d ' ')
1958
1984
  else
1959
1985
  diff_files=0
@@ -2249,6 +2275,11 @@ INCONCLUSIVE_EOF
2249
2275
  "inconclusive": $test_inconclusive,
2250
2276
  "inconclusive_reason": "$test_inconclusive_reason"
2251
2277
  },
2278
+ "boot": {
2279
+ "ok": $([ "${boot_fails:-false}" = "true" ] && echo false || echo true),
2280
+ "inconclusive": ${boot_inconclusive:-false},
2281
+ "reason": "${boot_inconclusive_reason:-}"
2282
+ },
2252
2283
  "nomock": {
2253
2284
  "ok": $_nomock_ok,
2254
2285
  "inconclusive": ${nomock_inconclusive:-false},
@@ -2458,6 +2489,8 @@ PYEOF
2458
2489
 
2459
2490
  if [ "$_proof_persist_disabled" = "true" ]; then
2460
2491
  persist_inconclusive="true"; persist_inconclusive_reason="persist_gate_disabled"
2492
+ elif [ "$_trivial_diff" = "true" ]; then
2493
+ persist_inconclusive="true"; persist_inconclusive_reason="trivial_diff"
2461
2494
  elif [ "$_proof_serveable" != "true" ]; then
2462
2495
  persist_inconclusive="true"; persist_inconclusive_reason="not_serveable"
2463
2496
  elif command -v python3 >/dev/null 2>&1; then
@@ -2522,6 +2555,8 @@ PYEOF
2522
2555
 
2523
2556
  if [ "$_proof_auth_disabled" = "true" ]; then
2524
2557
  auth_inconclusive="true"; auth_inconclusive_reason="auth_gate_disabled"
2558
+ elif [ "$_trivial_diff" = "true" ]; then
2559
+ auth_inconclusive="true"; auth_inconclusive_reason="trivial_diff"
2525
2560
  elif [ "$_proof_serveable" != "true" ]; then
2526
2561
  auth_inconclusive="true"; auth_inconclusive_reason="not_serveable"
2527
2562
  elif command -v python3 >/dev/null 2>&1; then
@@ -2601,6 +2636,8 @@ PYEOF
2601
2636
  # isolation blocks.
2602
2637
  if [ "$_proof_authz_disabled" = "true" ]; then
2603
2638
  authz_inconclusive="true"; authz_inconclusive_reason="authz_gate_disabled"
2639
+ elif [ "$_trivial_diff" = "true" ]; then
2640
+ authz_inconclusive="true"; authz_inconclusive_reason="trivial_diff"
2604
2641
  elif [ "$_proof_serveable" != "true" ]; then
2605
2642
  authz_inconclusive="true"; authz_inconclusive_reason="not_serveable"
2606
2643
  elif command -v python3 >/dev/null 2>&1; then
@@ -2740,13 +2777,13 @@ PYEOF
2740
2777
  if [ "$nomock_inconclusive" = "true" ] && [ "$nomock_inconclusive_reason" != "nomock_gate_disabled" ]; then
2741
2778
  log_warn "[Council] Evidence gate: no-mock not confirmed (${nomock_inconclusive_reason}). Pass-through; set LOKI_PROOF_NOMOCK=0 to silence."
2742
2779
  fi
2743
- if [ "$persist_inconclusive" = "true" ] && [ "$persist_inconclusive_reason" != "persist_gate_disabled" ]; then
2780
+ if [ "$persist_inconclusive" = "true" ] && [ "$persist_inconclusive_reason" != "persist_gate_disabled" ] && [ "$_trivial_diff" != "true" ]; then
2744
2781
  log_warn "[Council] Evidence gate: persistence not proven (${persist_inconclusive_reason}). Pass-through; set LOKI_PROOF_PERSIST=0 to silence, or expose a create form the driver can exercise (LOKI_PROOF_CREATE_SELECTOR)."
2745
2782
  fi
2746
- if [ "$auth_inconclusive" = "true" ] && [ "$auth_inconclusive_reason" != "auth_gate_disabled" ]; then
2783
+ if [ "$auth_inconclusive" = "true" ] && [ "$auth_inconclusive_reason" != "auth_gate_disabled" ] && [ "$_trivial_diff" != "true" ]; then
2747
2784
  log_warn "[Council] Evidence gate: auth enforcement not proven (${auth_inconclusive_reason}). Pass-through; set LOKI_PROOF_AUTH=0 to silence, or set LOKI_PROOF_PROTECTED_PATH so the driver can test a logged-out request."
2748
2785
  fi
2749
- if [ "$authz_inconclusive" = "true" ] && [ "$authz_inconclusive_reason" != "authz_gate_disabled" ]; then
2786
+ if [ "$authz_inconclusive" = "true" ] && [ "$authz_inconclusive_reason" != "authz_gate_disabled" ] && [ "$_trivial_diff" != "true" ]; then
2750
2787
  log_warn "[Council] Evidence gate: tenant isolation not proven (${authz_inconclusive_reason}). Pass-through; set LOKI_PROOF_AUTHZ=0 to silence, or configure LOKI_PROOF_AUTHZ_* selectors so the driver can drive two distinct sessions."
2751
2788
  fi
2752
2789
  _write_evidence_details "pass"
@@ -58,7 +58,7 @@ INPUT CONTRACT. Blockers are DERIVED, not read: `doctor --json` emits no
58
58
  blocker list (that string is built only in the text path, autonomy/loki:11367).
59
59
  Anything with status "fail" is a blocker, and failable things live in three
60
60
  places -- `checks[]`, plus the siblings `ai_provider` and `disk`. The advisory
61
- siblings (`sentrux`, `receipt_signing`, `memory`, `model_catalog`) are never
61
+ siblings (`sentrux`, `memory`, `model_catalog`) are never
62
62
  "fail" by construction and are not scanned.
63
63
 
64
64
  KNOWN LIMITATION, stated rather than papered over: the text path reports two
@@ -1056,9 +1056,8 @@ def _git_diffstat(target_dir, include_diffs):
1056
1056
  into _diff_sha256, the receipt's integrity hash, which is written on EVERY
1057
1057
  run. A verifier recomputing it therefore attests to the understated stat,
1058
1058
  and the receipt is exactly the artifact users are told to trust. (Detached
1059
- GPG signing is a separate opt-in layer gated on LOKI_PROOF_GPG_KEY and
1060
- default OFF; when enabled it signs these same bytes, but the integrity hash
1061
- is the always-on path.)
1059
+ Ed25519 signing attests to the same digest; the integrity hash is the
1060
+ always-on path.)
1062
1061
 
1063
1062
  Order of preference:
1064
1063
  1. _LOKI_RUN_START_SHA -- the run's own baseline (run.sh exports it).
@@ -1303,26 +1302,6 @@ def _canonical(obj):
1303
1302
  return json.dumps(obj, sort_keys=True, separators=(",", ":"))
1304
1303
 
1305
1304
 
1306
- def _gpg_detached_sign(data, key_id):
1307
- """Produce an ASCII-armored gpg detached signature over `data`.
1308
-
1309
- Returns the armored signature string, or None on any failure (gpg missing,
1310
- key not found, timeout). Best-effort: signing is an optional add-on and
1311
- never blocks proof emission. Local-only: invokes the on-PATH gpg, no network.
1312
- """
1313
- try:
1314
- proc = subprocess.run(
1315
- ["gpg", "--batch", "--yes", "--armor", "--detach-sign",
1316
- "--local-user", key_id, "--output", "-"],
1317
- input=data, capture_output=True, timeout=30,
1318
- )
1319
- if proc.returncode != 0 or not proc.stdout:
1320
- return None
1321
- return proc.stdout.decode("utf-8", errors="replace")
1322
- except Exception:
1323
- return None
1324
-
1325
-
1326
1305
  def _build_proof(args, loki_dir, target_dir, repo_root):
1327
1306
  generated_at = _utc_now_iso()
1328
1307
  run_id = args.run_id or os.environ.get("LOKI_SESSION_ID") or _gen_run_id()
@@ -1965,16 +1944,16 @@ def _render_fallback_html(proof):
1965
1944
  # Signing state, stated plainly. Mirrors renderProvenance in
1966
1945
  # proof-template.html (the primary renderer); this fallback path must not
1967
1946
  # be quieter about provenance than the page it stands in for.
1968
- if ver.get("gpg_signature"):
1969
- rows.append("<p>Signature: SIGNED (detached GPG over the canonical "
1970
- "bytes). A verifier holding the signer public key can "
1971
- "confirm provenance offline: loki proof verify &lt;id&gt;</p>")
1947
+ if ver.get("attestation"):
1948
+ rows.append("<p>Signature: SIGNED (Ed25519 attestation over the "
1949
+ "integrity hash). Confirm provenance offline: "
1950
+ "loki proof verify &lt;id&gt;</p>")
1972
1951
  else:
1973
1952
  rows.append("<p>Signature: UNSIGNED. The integrity hash proves the "
1974
1953
  "bytes were not edited after hashing; it does NOT prove "
1975
1954
  "who produced them, so this receipt trusts its generator. "
1976
- "To sign future receipts, set LOKI_PROOF_GPG_KEY to a gpg "
1977
- "key id (see docs/SIGNED-RECEIPTS.md).</p>")
1955
+ "To sign future receipts, install the cryptography "
1956
+ "package (see docs/SIGNED-RECEIPTS.md).</p>")
1978
1957
 
1979
1958
  # Model provenance, mirrored here for the same reason the signature row is:
1980
1959
  # this fallback "must not be quieter about provenance than the page it
@@ -2150,61 +2129,38 @@ def generate(args):
2150
2129
  "scope": "integrity",
2151
2130
  }
2152
2131
 
2153
- # Optional, env-gated gpg detached signature over the SAME canonical bytes
2154
- # that were hashed (the pre-verification form a verifier reconstructs).
2155
- # Default OFF: absent LOKI_PROOF_GPG_KEY -> no signature field, bytes
2156
- # byte-identical to the unsigned proof. Never an external service, never
2157
- # required, best-effort (a gpg failure is swallowed: the proof still emits).
2158
- gpg_key = os.environ.get("LOKI_PROOF_GPG_KEY", "").strip()
2159
- if gpg_key:
2160
- sig = _gpg_detached_sign(canonical_bytes, gpg_key)
2161
- if sig:
2162
- verification["gpg_signature"] = sig
2163
-
2164
- # Optional Ed25519 attestation over the SAME digest, for the case gpg
2165
- # cannot serve: a receipt checked by someone who does not hold the signing
2166
- # key. gpg proves provenance only to a verifier who already imported the
2167
- # public key, which a customer, auditor or CI system handed a proof.json
2168
- # generally has not. An attestation is checkable against a published JWKS
2169
- # (or a jwks.json file, offline) with no key exchange at all.
2170
- #
2171
- # Binds the DIGEST, not the body, so it attests to exactly the bytes a
2172
- # verifier independently recomputes. Written INSIDE `verification`, which
2173
- # the hash excludes -- anywhere else would change the hashed bytes and make
2174
- # every honest receipt read as tampered the moment it was signed.
2175
- #
2176
- # Same default-OFF discipline as gpg above: absent the key, no field, and
2177
- # the bytes stay identical to an unattested proof. Best-effort for the same
2178
- # reason -- a signing failure must not cost the user their receipt.
2179
- # This env check is an EARLY EXIT, not the control that enforces default-off.
2180
- # Mutation testing showed that removing it changes nothing observable:
2181
- # load_signing_key() returns (None, "") when neither variable is set, so the
2182
- # block below cannot attest anyway. It is kept because it skips an import
2183
- # and a try/except on the overwhelmingly common unconfigured path -- stated
2184
- # here rather than left to imply a guarantee it does not provide.
2185
- att_key = (os.environ.get("LOKI_RECEIPT_SIGNING_KEY_FILE", "").strip()
2186
- or os.environ.get("LOKI_RECEIPT_SIGNING_KEY", "").strip())
2187
- if att_key:
2188
- try:
2189
- sys.path.insert(0, os.path.dirname(os.path.dirname(
2190
- os.path.abspath(__file__))))
2191
- from receipt_jwt import load_signing_key, sign_attestation
2192
- _priv, _kid = load_signing_key()
2193
- if _priv is not None:
2194
- _tok = sign_attestation(
2195
- _priv, _kid,
2196
- job_id=str(redacted.get("run_id") or ""),
2197
- run_id=str(redacted.get("run_id") or ""),
2198
- receipt_hash=digest,
2199
- )
2200
- if _tok:
2201
- verification["attestation"] = _tok
2202
- verification["attestation_kid"] = _kid
2203
- except Exception:
2204
- # Swallowed deliberately, matching the gpg path: an unattested
2205
- # receipt is a valid state with an existing verdict. Losing the
2206
- # receipt entirely because signing failed would be strictly worse.
2207
- pass
2132
+ if os.environ.get("LOKI_PROOF_GPG_KEY", "").strip():
2133
+ sys.stderr.write(
2134
+ "warn: LOKI_PROOF_GPG_KEY is no longer supported and is ignored. "
2135
+ "Receipts are signed with Ed25519 automatically; to use your own "
2136
+ "key set LOKI_RECEIPT_SIGNING_KEY or LOKI_RECEIPT_SIGNING_KEY_FILE "
2137
+ "(docs/SIGNED-RECEIPTS.md).\n")
2138
+
2139
+ # Ed25519 attestation over the SAME digest, checkable against a published
2140
+ # JWKS (or a jwks.json file, offline) with no key exchange. Binds the
2141
+ # DIGEST, not the body, and is written INSIDE `verification`, which the hash
2142
+ # excludes: anywhere else would change the hashed bytes and make every
2143
+ # honest receipt read as tampered. load_signing_key() auto-generates the
2144
+ # default local key, so a first run signs. Best-effort: a signing failure
2145
+ # must not cost the user their receipt (an unattested receipt has its own
2146
+ # verdict). Missing `cryptography` yields no attestation.
2147
+ try:
2148
+ sys.path.insert(0, os.path.dirname(os.path.dirname(
2149
+ os.path.abspath(__file__))))
2150
+ from receipt_jwt import load_signing_key, sign_attestation
2151
+ _priv, _kid = load_signing_key()
2152
+ if _priv is not None:
2153
+ _tok = sign_attestation(
2154
+ _priv, _kid,
2155
+ job_id=str(redacted.get("run_id") or ""),
2156
+ run_id=str(redacted.get("run_id") or ""),
2157
+ receipt_hash=digest,
2158
+ )
2159
+ if _tok:
2160
+ verification["attestation"] = _tok
2161
+ verification["attestation_kid"] = _kid
2162
+ except Exception:
2163
+ pass
2208
2164
 
2209
2165
  redacted["verification"] = verification
2210
2166
 
@@ -1255,19 +1255,15 @@
1255
1255
  if (hash) {
1256
1256
  kv += '<div class="k">Integrity hash</div><div class="v">' + esc(algo) + ":" + esc(hash) + "</div>";
1257
1257
  }
1258
- // Signing state, named explicitly. The hash-note below already says the
1259
- // hash does not prove provenance; without this row a reader is told what
1260
- // the receipt CANNOT do but never which state this particular receipt is
1261
- // in, nor how to change it. Signing is gated on LOKI_PROOF_GPG_KEY in
1262
- // proof-generator.py and is OFF by default -- deliberately, because a
1263
- // self-minted key would flip proof-verify.py's generator_trusted to false
1264
- // while the key came from the very process under audit.
1265
- var signed = !!g(p, "verification.gpg_signature", "");
1258
+ // Signing state, named explicitly: a reader must be told which state this
1259
+ // receipt is in, not just what the hash cannot prove. Signing is an Ed25519
1260
+ // attestation (verification.attestation), auto-generated on first run.
1261
+ var signed = !!g(p, "verification.attestation", "");
1266
1262
  kv += '<div class="k">Signature</div><div class="v">' +
1267
- (signed ? "SIGNED (detached GPG)" : "UNSIGNED") + "</div>";
1263
+ (signed ? "SIGNED (Ed25519 attestation)" : "UNSIGNED") + "</div>";
1268
1264
  var signNote = signed
1269
- ? '<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>'
1270
- : '<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>';
1265
+ ? '<p class="hash-note">This receipt carries an Ed25519 attestation over the integrity hash. <code>loki proof verify</code> checks it offline against the signer public key.</p>'
1266
+ : '<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>). Install the <code>cryptography</code> package so the next run signs automatically (docs/SIGNED-RECEIPTS.md).</p>';
1271
1267
  // Model provenance from the append-only decision trail
1272
1268
  // (.loki/decisions/decisions.jsonl, written once per dispatch).
1273
1269
  //