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/autonomy/loki CHANGED
@@ -1280,7 +1280,7 @@ loki_remote_verify_receipt() {
1280
1280
  echo " Proven: the receipt is internally consistent with its own hash."
1281
1281
  echo " NOT proven: who produced it. An unsigned receipt is forgeable -- whoever"
1282
1282
  echo " controls the builder can rewrite the facts and recompute the hash."
1283
- echo " For provenance, the build must set LOKI_PROOF_GPG_KEY (docs/SIGNED-RECEIPTS.md)."
1283
+ echo " For provenance, sign the build (docs/SIGNED-RECEIPTS.md)."
1284
1284
  return 0
1285
1285
  }
1286
1286
 
@@ -9282,14 +9282,14 @@ _deploy_receipt_verdict() {
9282
9282
  printf '%s' "UNCHECKED"; return 0
9283
9283
  fi
9284
9284
 
9285
- local out hash_ok gpg_ok signed=0
9285
+ local out hash_ok
9286
9286
  out="$(python3 -E "$verifier" "$pj" "${TARGET_DIR:-.}" 2>/dev/null || true)"
9287
9287
 
9288
9288
  # "The verifier did not RUN" and "the verifier says the hash is bad" are
9289
9289
  # different facts and must not collapse. If they do, a broken python3
9290
9290
  # produces the verdict TAMPERED -- an accusation of forgery for a check that
9291
9291
  # never executed. That is the same error the signed path is careful to avoid
9292
- # with NO_PUBKEY (see loki:852): it sends a user hunting a forgery that never
9292
+ # with a missing key: it sends a user hunting a forgery that never
9293
9293
  # happened, and trains them to shrug at a real TAMPERED. Unparseable output
9294
9294
  # is UNCHECKED, which still REFUSES -- fail-closed, honestly labelled.
9295
9295
  if [ -z "$out" ] || ! printf '%s' "$out" | python3 -c 'import json,sys;json.load(sys.stdin)' >/dev/null 2>&1; then
@@ -9297,28 +9297,39 @@ _deploy_receipt_verdict() {
9297
9297
  fi
9298
9298
 
9299
9299
  hash_ok="$(printf '%s' "$out" | python3 -c 'import json,sys;print(str(json.load(sys.stdin).get("hash_ok",False)).lower())' 2>/dev/null || printf 'false')"
9300
- gpg_ok="$(printf '%s' "$out" | python3 -c 'import json,sys;print(str(json.load(sys.stdin).get("gpg_ok","n/a")).lower())' 2>/dev/null || printf 'n/a')"
9301
-
9302
- # Whether the receipt CARRIES a signature is read from the receipt, never
9303
- # inferred from gpg_ok -- gpg_ok is "n/a" both for "no signature" and for
9304
- # "gpg missing here", and conflating those mislabels a signed receipt.
9305
- if python3 -c 'import json,sys;sys.exit(0 if (json.load(open(sys.argv[1])).get("verification") or {}).get("gpg_signature") else 1)' "$pj" 2>/dev/null; then
9306
- signed=1
9307
- fi
9308
-
9309
9300
  if [ "$hash_ok" != "true" ]; then printf '%s' "TAMPERED"; return 0; fi
9310
- if [ "$signed" -eq 0 ]; then printf '%s' "UNSIGNED"; return 0; fi
9311
- case "$gpg_ok" in
9312
- true) printf '%s' "VERIFIED" ;;
9313
- # Signed but the signature does not verify: contents or signature were
9314
- # altered. A missing PUBLIC KEY is a different fact (uncheckable, not an
9315
- # accusation), so it reports UNCHECKED -- which refuses here anyway.
9316
- false) if command -v gpg >/dev/null 2>&1 && [ "$(loki_remote_gpg_status "$pj")" = "nopubkey" ]; then
9317
- printf '%s' "UNCHECKED"
9318
- else
9319
- printf '%s' "TAMPERED"
9320
- fi ;;
9321
- *) printf '%s' "UNCHECKED" ;;
9301
+
9302
+ # Provenance is the Ed25519 attestation, checked against the PUBLIC keys we
9303
+ # hold: the local signing key plus LOKI_RECEIPT_RETIRED_PUBKEYS. A legacy
9304
+ # gpg_signature is ignored: old receipts read UNSIGNED here. A token whose
9305
+ # kid is not in that set (another machine, a rotated-away key) cannot be
9306
+ # evaluated, so no key set is written and the verdict is UNCHECKED, never
9307
+ # TAMPERED: the hash is fine, we just cannot vouch for who signed it. A
9308
+ # local key only proves "a holder of this key", not an independent build.
9309
+ local jwks att
9310
+ jwks="$(mktemp "${TMPDIR:-/tmp}/loki-jwks.XXXXXX")" || { printf '%s' "UNCHECKED"; return 0; }
9311
+ python3 -E - "${_LOKI_SCRIPT_DIR}" "$pj" >"$jwks" 2>/dev/null <<'PY' || true
9312
+ import sys, json
9313
+ sys.path[:] = [p for p in sys.path if p not in ("", ".")]
9314
+ sys.path.insert(0, sys.argv[1])
9315
+ from receipt_jwt import load_signing_key, load_retired_public_keys, build_jwks, _b64url_decode
9316
+ k, _ = load_signing_key(auto_generate=False)
9317
+ jwks = build_jwks(k, load_retired_public_keys())
9318
+ try:
9319
+ tok = json.load(open(sys.argv[2]))["verification"]["attestation"]
9320
+ kid = json.loads(_b64url_decode(tok.split(".")[0])).get("kid", "")
9321
+ except Exception:
9322
+ kid = "" # malformed or absent: let the checker decide (none / bad)
9323
+ if not kid or kid in [j.get("kid") for j in jwks["keys"]]:
9324
+ print(json.dumps(jwks))
9325
+ PY
9326
+ att="$(loki_proof_attestation_check "$pj" "$jwks")"
9327
+ rm -f "$jwks"
9328
+ case "$att" in
9329
+ ok) printf '%s' "VERIFIED" ;;
9330
+ none) printf '%s' "UNSIGNED" ;;
9331
+ bad) printf '%s' "TAMPERED" ;;
9332
+ *) printf '%s' "UNCHECKED" ;;
9322
9333
  esac
9323
9334
  return 0
9324
9335
  }
@@ -9680,7 +9691,7 @@ _deploy_gate() {
9680
9691
  verdict="$(_deploy_receipt_verdict "$pj")"
9681
9692
  case "$verdict" in
9682
9693
  VERIFIED) : ;;
9683
- UNSIGNED) reasons="${reasons}receipt is UNSIGNED: integrity holds but provenance is unproven (set LOKI_PROOF_GPG_KEY; docs/SIGNED-RECEIPTS.md)
9694
+ UNSIGNED) reasons="${reasons}receipt is UNSIGNED: integrity holds but provenance is unproven (docs/SIGNED-RECEIPTS.md)
9684
9695
  " ;;
9685
9696
  UNCHECKED) reasons="${reasons}receipt is UNCHECKED: its signature could not be evaluated here, so nothing was proven
9686
9697
  " ;;
@@ -9784,7 +9795,7 @@ cmd_deploy() {
9784
9795
  echo " file alone can opt in."
9785
9796
  echo ""
9786
9797
  echo "It FAILS CLOSED: if a check cannot be evaluated at all (no receipt,"
9787
- echo "gpg unavailable, anchor unresolvable, not a git repo), that is a"
9798
+ echo "signing key or keys unavailable, anchor unresolvable, not a git repo), that is a"
9788
9799
  echo "REFUSAL, not a pass. A refusal still prints the advisory command."
9789
9800
  echo ""
9790
9801
  echo "Hard limits, with or without --execute:"
@@ -13635,7 +13646,7 @@ cmd_doctor() {
13635
13646
  if ! command -v "$cmd" &> /dev/null; then
13636
13647
  if [ "$required" = "required" ]; then
13637
13648
  echo -e " ${RED}FAIL${NC} $name - not found"
13638
- _doctor_block "$name is not installed"
13649
+ _doctor_block "${name%% (*} is not installed"
13639
13650
  fail_count=$((fail_count + 1))
13640
13651
  elif [ "$required" = "recommended" ]; then
13641
13652
  echo -e " ${YELLOW}WARN${NC} $name - not found (recommended)"
@@ -13704,7 +13715,7 @@ cmd_doctor() {
13704
13715
  { [ "$cur_major" -eq "$min_major" ] 2>/dev/null && [ "$cur_minor" -lt "$min_minor" ] 2>/dev/null; }; then
13705
13716
  if [ "$required" = "required" ]; then
13706
13717
  echo -e " ${RED}FAIL${NC} $name$version_display - requires >= $min_version"
13707
- _doctor_block "$name must be >= $min_version"
13718
+ _doctor_block "${name%% (*} must be >= $min_version"
13708
13719
  fail_count=$((fail_count + 1))
13709
13720
  else
13710
13721
  echo -e " ${YELLOW}WARN${NC} $name$version_display - recommended >= $min_version"
@@ -13806,6 +13817,19 @@ cmd_doctor() {
13806
13817
  fi
13807
13818
  echo ""
13808
13819
 
13820
+ # A-123: an EXPLICIT LOKI_PROVIDER names the CLI a build will launch, so that
13821
+ # CLI is required even when another provider is installed. Auto-detection
13822
+ # never reaches here: it only picks an installed provider. Bundled-SDK
13823
+ # claude stays allowed. Mirrored in loki-ts/src/commands/doctor.ts.
13824
+ if [ -n "${LOKI_PROVIDER:-}" ] && [ "$fail_count" -eq 0 ] \
13825
+ && ! command -v "$LOKI_PROVIDER" >/dev/null 2>&1 \
13826
+ && ! { [ "$LOKI_PROVIDER" = "claude" ] && [ "$_doctor_sdk_only" = "1" ]; }; then
13827
+ _sel_install=$(doctor_provider_install_cmd "$LOKI_PROVIDER")
13828
+ echo -e " ${RED}FAIL${NC} Selected provider '${LOKI_PROVIDER}' CLI not found"
13829
+ _doctor_block "Selected provider ${LOKI_PROVIDER} CLI not found. Fix: ${_sel_install:-install ${LOKI_PROVIDER}, or unset LOKI_PROVIDER}"
13830
+ fail_count=$((fail_count + 1))
13831
+ fi
13832
+
13809
13833
  # Provider Availability. Which provider a build would ACTUALLY pick, which
13810
13834
  # is decided by providers/loader.sh auto_detect_provider, not by PATH order
13811
13835
  # and not by the section above. Rendered by the shared helper in
@@ -14092,22 +14116,14 @@ except Exception:
14092
14116
  echo -e " ${YELLOW}WARN${NC} sentrux - not installed (optional, brew install sentrux/tap/sentrux)"
14093
14117
  warn_count=$((warn_count + 1))
14094
14118
  fi
14095
- # Evidence Receipt signing state (optional). Unsigned receipts are the
14096
- # default and are still checkable (hash + diff re-derivation); what they
14097
- # cannot prove is PROVENANCE, so proof-verify.py reports
14098
- # generator_trusted: true. Surface the state here because it is otherwise
14099
- # invisible until someone reads a receipt. WARN (never FAIL): unsigned is
14100
- # a supported, documented mode, not a broken install.
14119
+ # Receipt signing: Ed25519, auto-generated on first run. WARN only, and
14120
+ # only when something needs attention. Byte-mirrored in doctor.ts.
14101
14121
  if [ -n "${LOKI_PROOF_GPG_KEY:-}" ]; then
14102
- if command -v gpg &>/dev/null; then
14103
- echo -e " ${GREEN}PASS${NC} Receipt signing: LOKI_PROOF_GPG_KEY set and gpg available"
14104
- pass_count=$((pass_count + 1))
14105
- else
14106
- echo -e " ${YELLOW}WARN${NC} Receipt signing: LOKI_PROOF_GPG_KEY set but gpg NOT on PATH - receipts will be UNSIGNED"
14107
- warn_count=$((warn_count + 1))
14108
- fi
14109
- else
14110
- echo -e " ${YELLOW}WARN${NC} Receipt signing: UNSIGNED (set LOKI_PROOF_GPG_KEY to a gpg key id; see docs/SIGNED-RECEIPTS.md)"
14122
+ echo -e " ${YELLOW}WARN${NC} Receipt signing: LOKI_PROOF_GPG_KEY is no longer supported and is ignored; use LOKI_RECEIPT_SIGNING_KEY or LOKI_RECEIPT_SIGNING_KEY_FILE (docs/SIGNED-RECEIPTS.md)"
14123
+ warn_count=$((warn_count + 1))
14124
+ fi
14125
+ if ! python3 -c 'import cryptography' >/dev/null 2>&1; then
14126
+ echo -e " ${YELLOW}WARN${NC} Receipt signing: python3 'cryptography' package missing, receipts will be UNSIGNED (pip install cryptography)"
14111
14127
  warn_count=$((warn_count + 1))
14112
14128
  fi
14113
14129
  echo ""
@@ -14394,6 +14410,8 @@ else:
14394
14410
  esac
14395
14411
  ( loki_emit_first_run_blocked "$_blk_key" >/dev/null 2>&1 </dev/null & ) 2>/dev/null || true
14396
14412
  fi
14413
+ # A-123: the LAST line is the one blocking reason, exactly.
14414
+ printf '%b' "$_doctor_blockers" | awk '/^ - /{sub(/^ - /,""); print; exit}'
14397
14415
  return 1
14398
14416
  elif [ "$warn_count" -gt 0 ]; then
14399
14417
  echo -e "${YELLOW}All required checks passed with some warnings.${NC}"
@@ -14416,6 +14434,46 @@ else:
14416
14434
  echo "Next: loki quickstart (guided first build from your idea, no PRD needed)"
14417
14435
  echo " or loki demo (builds a sample todo app end to end) or loki start ./prd.md"
14418
14436
  fi
14437
+ # A-123: the last stdout line. Provider and model come from the selected
14438
+ # provider; the key state is read WITHOUT creating a key (auto_generate=False).
14439
+ # Byte-mirrored in loki-ts/src/commands/doctor.ts.
14440
+ local _rd_id _rd_model _rd_key _rd_state
14441
+ _rd_id="${LOKI_PROVIDER:-}"
14442
+ [ -n "$_rd_id" ] || _rd_id="$(effective_provider_id 2>/dev/null || true)"
14443
+ [ -n "$_rd_id" ] || _rd_id="claude"
14444
+ _rd_model="$(bash -c 'source "$1" >/dev/null 2>&1; printf %s "${PROVIDER_MODEL_DEVELOPMENT:-}"' _ "${_LOKI_SCRIPT_DIR}/../providers/${_rd_id}.sh" 2>/dev/null || true)"
14445
+ [ -n "$_rd_model" ] || _rd_model="default"
14446
+ _rd_key="$(python3 -E - "${_LOKI_SCRIPT_DIR}" 2>/dev/null <<'PY' || true
14447
+ import os, sys
14448
+ sys.path[:] = [p for p in sys.path if p not in ("", ".")]
14449
+ sys.path.insert(0, sys.argv[1])
14450
+ from receipt_jwt import load_signing_key, _CRYPTO_AVAILABLE
14451
+ if not _CRYPTO_AVAILABLE:
14452
+ print("nocrypto")
14453
+ else:
14454
+ k, kid = load_signing_key(auto_generate=False)
14455
+ if kid:
14456
+ print("kid " + kid[:8])
14457
+ elif os.environ.get("LOKI_RECEIPT_SIGNING_KEY", "").strip():
14458
+ print("bad")
14459
+ else:
14460
+ # A-122: a missing key FILE is auto-generated on first use when its nearest
14461
+ # existing ancestor directory is writable; an existing file that did not
14462
+ # load (corrupt/unreadable) or an unwritable location is unusable.
14463
+ f = os.environ.get("LOKI_RECEIPT_SIGNING_KEY_FILE", "").strip() or os.path.expanduser("~/.loki/keys/receipt-ed25519.pem")
14464
+ d = os.path.dirname(os.path.abspath(f))
14465
+ while not os.path.isdir(d) and os.path.dirname(d) != d:
14466
+ d = os.path.dirname(d)
14467
+ print("bad" if os.path.lexists(f) or not os.access(d, os.W_OK | os.X_OK) else "none")
14468
+ PY
14469
+ )"
14470
+ case "$_rd_key" in
14471
+ "kid "*) _rd_state="receipts signed (${_rd_key})" ;;
14472
+ none) _rd_state="receipts will be signed on first run" ;;
14473
+ bad) _rd_state="receipts unsigned: signing key could not be loaded" ;;
14474
+ *) _rd_state="receipts unsigned: python3 cryptography package missing" ;;
14475
+ esac
14476
+ echo "Ready: ${_rd_id} (${_rd_model}), ${_rd_state}"
14419
14477
  # Best-effort stale-install nudge (stderr only; never blocks; off on
14420
14478
  # non-TTY/CI). Reached only on the human-readable path (--json returns above).
14421
14479
  maybe_print_update_hint
@@ -14523,23 +14581,6 @@ sentrux = {
14523
14581
  'required': 'optional'
14524
14582
  }
14525
14583
 
14526
- # Evidence Receipt signing state. Unsigned is the DEFAULT and is a supported
14527
- # mode, so this is never 'fail'. Exposed because signing state is otherwise
14528
- # invisible until someone reads a receipt, and it is exactly what decides
14529
- # whether proof-verify.py reports generator_trusted: true. Sibling of
14530
- # checks/disk/sentrux -- deliberately NOT counted in the summary tally so
14531
- # existing consumers see unchanged numbers.
14532
- signing_key_set = bool(os.environ.get('LOKI_PROOF_GPG_KEY', '').strip())
14533
- signing_gpg_available = shutil.which('gpg') is not None
14534
- signing_enabled = signing_key_set and signing_gpg_available
14535
- receipt_signing = {
14536
- 'enabled': signing_enabled,
14537
- 'key_configured': signing_key_set,
14538
- 'gpg_available': signing_gpg_available,
14539
- 'status': 'pass' if signing_enabled else 'warn',
14540
- 'required': 'optional'
14541
- }
14542
-
14543
14584
  # v7.7.17: memory subsystem health surface. Reports the latest entries
14544
14585
  # from .loki/memory/.errors.log (rotated by memory/error_log.py) so
14545
14586
  # developers see regressions in the previously-silent-fail call sites
@@ -14731,7 +14772,6 @@ result = {
14731
14772
  'ai_provider': ai_provider,
14732
14773
  'skills': skills,
14733
14774
  'sentrux': sentrux,
14734
- 'receipt_signing': receipt_signing,
14735
14775
  'memory': memory,
14736
14776
  'model_catalog': model_catalog,
14737
14777
  'summary': {
@@ -16280,10 +16320,12 @@ cmd_quick() {
16280
16320
  echo " loki quick \"write unit tests for the API endpoints\""
16281
16321
  echo ""
16282
16322
  echo "Options:"
16283
- echo " --help, -h Show this help and exit"
16323
+ echo " --help, -h Show this help and exit"
16324
+ echo " --verbose, -v Show setup and progress output (same as LOKI_VERBOSE=1)"
16284
16325
  echo ""
16285
16326
  echo "Environment:"
16286
16327
  echo " LOKI_MAX_ITERATIONS Override the default 3-iteration cap"
16328
+ echo " LOKI_VERBOSE=1 Show setup and progress output (default is quiet)"
16287
16329
  echo ""
16288
16330
  echo "See also: loki start (full RARV cycle), loki run (alias for start)"
16289
16331
  return 0
@@ -16316,19 +16358,27 @@ cmd_quick() {
16316
16358
  # `--config f.env` would be swept into task_desc (cmd_quick has no arg loop).
16317
16359
  local _quick_args=()
16318
16360
  local _skip_next=false
16319
- local _qa
16361
+ local _qa _quick_task_seen=false
16320
16362
  for _qa in "$@"; do
16321
16363
  if [ "$_skip_next" = "true" ]; then
16322
16364
  _skip_next=false
16323
16365
  continue
16324
16366
  fi
16325
16367
  case "$_qa" in
16368
+ # A-134: --verbose/-v before the task text is a flag, never task text.
16369
+ --verbose|-v) if [ "$_quick_task_seen" = "false" ]; then export LOKI_VERBOSE=1; continue; fi ;;
16326
16370
  --config|--vars|--env-file) _skip_next=true; continue ;;
16327
16371
  --config=*|--vars=*|--env-file=*) continue ;;
16328
16372
  esac
16373
+ _quick_task_seen=true
16329
16374
  _quick_args+=("$_qa")
16330
16375
  done
16331
16376
  set -- "${_quick_args[@]+"${_quick_args[@]}"}"
16377
+ if [ $# -eq 0 ]; then
16378
+ echo -e "${RED}Error: No task description provided${NC}"
16379
+ echo "Usage: loki quick [--verbose] \"description of what to do\""
16380
+ exit 2
16381
+ fi
16332
16382
 
16333
16383
  local task_desc="$*"
16334
16384
  local version=$(get_version)
@@ -36677,10 +36727,9 @@ cmd_proof() {
36677
36727
  echo ""
36678
36728
  echo "Proofs are generated automatically at run completion (LOKI_PROOF=0 to opt out)."
36679
36729
  echo ""
36680
- echo "Signing (off by default): receipts are UNSIGNED unless LOKI_PROOF_GPG_KEY is"
36681
- echo "set to a gpg key id. Unsigned, the integrity hash proves the bytes were not"
36682
- echo "edited after hashing but NOT who produced them, so verify reports"
36683
- echo "generator_trusted: true. Export LOKI_PROOF_GPG_KEY to close that gap."
36730
+ echo "Signing: receipts are Ed25519-signed automatically (key auto-generated at"
36731
+ echo "~/.loki/keys/receipt-ed25519.pem). To use your own key set"
36732
+ echo "LOKI_RECEIPT_SIGNING_KEY or LOKI_RECEIPT_SIGNING_KEY_FILE."
36684
36733
  echo "See docs/SIGNED-RECEIPTS.md."
36685
36734
  [ "$sub" = "" ] && exit 1
36686
36735
  exit 0
@@ -37131,8 +37180,8 @@ print(json.dumps(json.load(open(os.environ['LOKI_PROOF_JSON'])), indent=2))
37131
37180
  # verified. Exiting 0 here let a stripped signature pass
37132
37181
  # any CI step that asked for provenance.
37133
37182
  echo -e "${YELLOW}attestation: ABSENT -- this receipt carries no attestation.${NC}" >&2
37134
- echo " Nothing was checked against ${_pv_jwks}. The receipt may still be" >&2
37135
- echo " gpg-signed; this says only that it has no JWKS attestation." >&2
37183
+ echo " Nothing was checked against ${_pv_jwks}. This says only that the" >&2
37184
+ echo " receipt has no JWKS attestation." >&2
37136
37185
  echo " Not verified: a key set was supplied, but the receipt is unsigned (exit 1)." >&2
37137
37186
  _pv_rc=1
37138
37187
  _pv_attestation_field="ABSENT"
@@ -1,8 +1,8 @@
1
1
  """Per-job receipt attestation: a signed JWT plus a public JWKS.
2
2
 
3
- WHY THIS EXISTS, given receipts are ALREADY gpg-signable.
3
+ WHY THIS EXISTS, given old receipts could carry a gpg signature.
4
4
 
5
- `LOKI_PROOF_GPG_KEY` proves a receipt was produced by a holder of that gpg key,
5
+ A gpg signature proves a receipt was produced by a holder of that gpg key,
6
6
  and for a LOCAL build that is the whole story: the operator already has the
7
7
  keyring. It does not survive the remote path. A submitter who ran
8
8
  `loki start --remote https://loki.corp` never had local access to the cluster,
@@ -114,13 +114,14 @@ def _load_private_key(pem_bytes: bytes):
114
114
  return key
115
115
 
116
116
 
117
- def load_signing_key():
117
+ def load_signing_key(auto_generate=True):
118
118
  """Return (private_key, kid) from env or a mounted file, or (None, "").
119
119
 
120
- Mirrors load_api_token's precedence deliberately: LOKI_RECEIPT_SIGNING_KEY
121
- then LOKI_RECEIPT_SIGNING_KEY_FILE, the normal Kubernetes mounted-secret
122
- path. Absence is NOT an error -- it is the default, and it means receipts
123
- stay UNSIGNED exactly as they were before this module existed.
120
+ Precedence: LOKI_RECEIPT_SIGNING_KEY (inline PEM), then
121
+ LOKI_RECEIPT_SIGNING_KEY_FILE, then ~/.loki/keys/receipt-ed25519.pem. A
122
+ missing key FILE is generated (PKCS8, 0600, O_EXCL) so a first run signs
123
+ without setup unless auto_generate=False (the server path: an ephemeral
124
+ container key would orphan old receipts). The private key is never logged or printed.
124
125
  """
125
126
  if not _CRYPTO_AVAILABLE:
126
127
  return None, ""
@@ -129,21 +130,65 @@ def load_signing_key():
129
130
  key = _load_private_key(pem.encode("utf-8"))
130
131
  else:
131
132
  key_file = os.environ.get("LOKI_RECEIPT_SIGNING_KEY_FILE", "").strip()
132
- if not key_file:
133
- return None, ""
133
+ is_default = not key_file
134
+ if is_default:
135
+ key_file = str(Path.home() / ".loki" / "keys" / "receipt-ed25519.pem")
134
136
  try:
135
- key = _load_private_key(Path(key_file).read_bytes())
137
+ try:
138
+ data = Path(key_file).read_bytes()
139
+ if is_default: # never chmod an operator's key: it may be a :ro mount
140
+ _tighten(key_file)
141
+ except FileNotFoundError:
142
+ if not auto_generate:
143
+ return None, ""
144
+ data = _create_key_file(key_file)
145
+ logging.info("receipt signing: auto-generated local key at %s", key_file)
146
+ key = _load_private_key(data)
136
147
  except OSError as e:
137
- logging.error(
138
- "receipt signing: cannot read LOKI_RECEIPT_SIGNING_KEY_FILE %s: %s",
139
- key_file, e,
140
- )
148
+ logging.error("receipt signing: cannot use key file %s: %s", key_file, e)
141
149
  return None, ""
142
150
  if key is None:
143
151
  return None, ""
144
152
  return key, compute_kid(key.public_key())
145
153
 
146
154
 
155
+ def _tighten(path: str) -> None:
156
+ """Drop group/other access on an existing key file and its directory."""
157
+ # best effort: a failed chmod must never downgrade signing
158
+ try:
159
+ if os.stat(path).st_mode & 0o077:
160
+ os.chmod(path, 0o600)
161
+ d = os.path.dirname(path)
162
+ if os.stat(d).st_mode & 0o077:
163
+ os.chmod(d, 0o700)
164
+ except OSError as e:
165
+ logging.warning("receipt signing: could not tighten %s: %s", path, e)
166
+
167
+
168
+ def _create_key_file(path: str) -> bytes:
169
+ """Create the key 0600, published atomically via link(); a concurrent first run reads the winner's file."""
170
+ pem = Ed25519PrivateKey.generate().private_bytes(
171
+ encoding=serialization.Encoding.PEM,
172
+ format=serialization.PrivateFormat.PKCS8,
173
+ encryption_algorithm=serialization.NoEncryption(),
174
+ )
175
+ d = os.path.dirname(path) or "."
176
+ if d.endswith(os.path.join(".loki", "keys")):
177
+ os.makedirs(os.path.dirname(d), mode=0o700, exist_ok=True)
178
+ os.makedirs(d, mode=0o700, exist_ok=True)
179
+ tmp = "%s.%d.tmp" % (path, os.getpid())
180
+ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
181
+ try:
182
+ with os.fdopen(fd, "wb") as f:
183
+ f.write(pem)
184
+ os.link(tmp, path) # atomic and fails if a concurrent run won
185
+ except FileExistsError:
186
+ return Path(path).read_bytes()
187
+ finally:
188
+ os.unlink(tmp)
189
+ return pem
190
+
191
+
147
192
  def load_retired_public_keys():
148
193
  """Public keys that must stay in JWKS so OLD receipts still verify.
149
194