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.
- package/README.md +30 -962
- package/SKILL.md +3 -3
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +2 -1
- package/autonomy/completion-council.sh +40 -3
- package/autonomy/lib/doctor-fix.py +1 -1
- package/autonomy/lib/proof-generator.py +40 -84
- package/autonomy/lib/proof-template.html +7 -11
- package/autonomy/loki +118 -69
- package/autonomy/receipt_jwt.py +59 -14
- package/autonomy/run.sh +91 -124
- package/autonomy/trigger-server.py +2 -2
- package/autonomy/verify.sh +37 -2
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +2 -2
- package/docs/COMPETITIVE-NEXT-10.md +3 -4
- package/docs/INSTALLATION.md +1 -1
- package/docs/README-FULL.md +977 -0
- package/docs/SIGNED-RECEIPTS.md +40 -82
- package/docs/TOOLS.md +0 -23
- package/docs/VERIFICATION-COST.md +2 -2
- package/docs/v10/ADOPTION-PLAN.md +1 -1
- package/docs/v10/BOARD.md +42 -22
- package/docs/v10/DECISIONS.md +11 -0
- package/docs/v10/ENGINE.md +4 -4
- package/docs/v10/FOUNDER-QUEUE.md +1 -0
- package/docs/v10/GUIDE.md +0 -4
- package/docs/v10/PROGRESS.md +43 -0
- package/loki-ts/dist/loki.js +587 -563
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/tools/gate-status.py +3 -46
- package/tools/receipt-attest.py +1 -1
- package/web-app/dist/assets/{AdminPage-bi-XcWhD.js → AdminPage-C8zraQ0u.js} +1 -1
- package/web-app/dist/assets/{Avatar-CmeIKHo6.js → Avatar-DnjIA4Ap.js} +1 -1
- package/web-app/dist/assets/{Badge-Bc3RMd7Y.js → Badge-C487hf0b.js} +1 -1
- package/web-app/dist/assets/{Button-Dc7yzWmJ.js → Button-5AWAEksl.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-C_aRTgNm.js → CockpitPage-B6_1gTFO.js} +1 -1
- package/web-app/dist/assets/{ComparePage-B8eqE3GD.js → ComparePage-DDbC8-yi.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-BRVSyrME.js → ErrorBoundary-CWHKwjEp.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-DBkXGSAF.js → EvidenceReceiptPanel-MfJq92Ty.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-rRZ4rP-a.js → GitHubIssuesPanel-BBdijzdg.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-NMj2a_-3.js → GitHubPRsPanel-DjBHyymT.js} +1 -1
- package/web-app/dist/assets/{HomePage-YGPKiV9F.js → HomePage-B_6m7ITK.js} +2 -2
- package/web-app/dist/assets/{LoginPage-CVx7_jtD.js → LoginPage-OGgMvfjT.js} +1 -1
- package/web-app/dist/assets/{MagicPage-G9BuqW4T.js → MagicPage-m4MSfxWf.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-CrPo66D6.js → MetricsPage-D8Ea1ALe.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-CbjchFXV.js → NotFoundPage-ttvXL-7K.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CmPoA170.js → ProjectPage-CDX8tSPV.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-CfVMc4nw.js → ProjectsPage-BQ73nYWf.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-6nZ-vONY.js → SettingsPage-B6k71q2g.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-CxdZjDzY.js → ShowcasePage-CNbwtr7D.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-Bu0kwwMe.js → SystemSettingsPage-CD2qys43.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-Ck6xWgOM.js → TeamsPage-DAmsRIVD.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-BduvcCsH.js → TemplatesPage-CcRyyUhL.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-BmleKz7Q.js → TerminalOutput-DRquxtQ1.js} +1 -1
- package/web-app/dist/assets/{activity-xUOENXj1.js → activity-DUDUi3Qd.js} +1 -1
- package/web-app/dist/assets/{bell-Do1DnngS.js → bell-tDpCOcHx.js} +1 -1
- package/web-app/dist/assets/{bot-Csus-lhk.js → bot-87IR4aTF.js} +1 -1
- package/web-app/dist/assets/{check-CDMuF2fW.js → check-C2Y2z-Ge.js} +1 -1
- package/web-app/dist/assets/{chevron-left-1pxhei7a.js → chevron-left-CsPFT88x.js} +1 -1
- package/web-app/dist/assets/{circle-alert-HufOSfwc.js → circle-alert-Cp2UJikd.js} +1 -1
- package/web-app/dist/assets/{clock-BoCMViPQ.js → clock-DZ1cwNyf.js} +1 -1
- package/web-app/dist/assets/{cloud-B7JfKA_r.js → cloud-q5TrIOV-.js} +1 -1
- package/web-app/dist/assets/{code-xml-BduQ3MqM.js → code-xml-0TNGOS3c.js} +1 -1
- package/web-app/dist/assets/{database-CQ9s31cU.js → database-g73vgDVx.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-CVJu5-A0.js → dollar-sign-DAKkR_MO.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-BPu1BX6Z.js → file-code-corner-DTAvdxjl.js} +1 -1
- package/web-app/dist/assets/{file-plus-CGa7y-QG.js → file-plus-D5SB-1Uk.js} +1 -1
- package/web-app/dist/assets/{globe-ClS0c8Cw.js → globe-j5YaNmx9.js} +1 -1
- package/web-app/dist/assets/{hammer-CeljzUTd.js → hammer-C9yoXOC6.js} +1 -1
- package/web-app/dist/assets/{index-Dn35XhiL.js → index--EayJsJ-.js} +3 -3
- package/web-app/dist/assets/{layers-CzGZ-NNL.js → layers-B4mxkWdH.js} +1 -1
- package/web-app/dist/assets/{loader-circle-Dy1j6q2e.js → loader-circle-CP-qD1W2.js} +1 -1
- package/web-app/dist/assets/{lock-pmDyRsWL.js → lock-DwHNUSTU.js} +1 -1
- package/web-app/dist/assets/{package-C1dXDph_.js → package-BxLWioQ8.js} +1 -1
- package/web-app/dist/assets/{plus-CKPSHefh.js → plus-B-43Byzl.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-CRmwRgDw.js → refresh-cw-5s47zAFV.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-Br04gZR3.js → rotate-ccw-BewZ8_Rx.js} +1 -1
- package/web-app/dist/assets/{scroll-text-BHjpthNc.js → scroll-text-DPNpHL-Z.js} +1 -1
- package/web-app/dist/assets/{server-Db3IfTpl.js → server-hL8_-vRq.js} +1 -1
- package/web-app/dist/assets/{shield-alert-jJXWKLDZ.js → shield-alert-DAGc2aLO.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-BksBTkqT.js → thumbs-up-DRGzcWEH.js} +1 -1
- package/web-app/dist/assets/{trash-2-CzXuGa-4.js → trash-2-Bo_YPIqS.js} +1 -1
- package/web-app/dist/assets/{trending-down-c_JXJq0s.js → trending-down-BQ1cK9F3.js} +1 -1
- package/web-app/dist/assets/{trending-up-DtvURYKn.js → trending-up-BpnkTO1T.js} +1 -1
- package/web-app/dist/assets/{upload-BSsOPrmR.js → upload-DDdQNxmW.js} +1 -1
- package/web-app/dist/assets/{usePolling-CFWdgkmg.js → usePolling-z_FXsGz7.js} +1 -1
- package/web-app/dist/assets/{user-CL-HjV4R.js → user-obPH42HX.js} +1 -1
- package/web-app/dist/index.html +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
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 (
|
|
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 "
|
|
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
|
-
#
|
|
14096
|
-
#
|
|
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
|
-
|
|
14103
|
-
|
|
14104
|
-
|
|
14105
|
-
|
|
14106
|
-
|
|
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
|
|
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
|
|
36681
|
-
echo "
|
|
36682
|
-
echo "
|
|
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}.
|
|
37135
|
-
echo "
|
|
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"
|
package/autonomy/receipt_jwt.py
CHANGED
|
@@ -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
|
|
3
|
+
WHY THIS EXISTS, given old receipts could carry a gpg signature.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
|