loki-mode 9.51.1 → 9.53.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 +3 -2
- package/SKILL.md +3 -3
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +93 -53
- package/autonomy/lib/workspace_diff.py +18 -1
- package/autonomy/loki +212 -35
- package/autonomy/receipt_jwt.py +7 -0
- package/autonomy/run.sh +79 -30
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +2 -2
- package/docs/LOKI-10-BUILD-PROMPT.md +350 -0
- package/docs/SIGNED-RECEIPTS.md +9 -6
- package/docs/V10-RESEARCH.md +81 -0
- package/docs/V10-VISION.md +161 -0
- package/docs/exit-codes.md +41 -0
- package/docs/v10/BACKLOG.md +95 -0
- package/docs/v10/DECISIONS.md +52 -0
- package/docs/v10/FOUNDER-QUEUE.md +13 -0
- package/docs/v10/METRICS.md +47 -0
- package/docs/v10/PROGRESS.md +49 -0
- package/loki-ts/dist/loki.js +411 -404
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/tools/verify-chain.py +4 -1
package/autonomy/loki
CHANGED
|
@@ -786,7 +786,13 @@ loki_remote_gpg_status() {
|
|
|
786
786
|
local pj="$1"
|
|
787
787
|
local d="${TMPDIR:-/tmp}/loki-sigcheck.$$"
|
|
788
788
|
mkdir -p "$d" || return 0
|
|
789
|
-
python3 - "$pj" "$d" <<'PY' 2>/dev/null || { rm -rf "$d"; return 0; }
|
|
789
|
+
python3 -E - "$pj" "$d" <<'PY' 2>/dev/null || { rm -rf "$d"; return 0; }
|
|
790
|
+
# The cwd is the checkout being verified: drop it from sys.path before any
|
|
791
|
+
# other import, or a committed hashlib.py/json.py could print the verdict.
|
|
792
|
+
# The interpreter runs with -E so PYTHONPATH (an empty component would add
|
|
793
|
+
# the cwd as an absolute path) and a committed sitecustomize.py cannot act
|
|
794
|
+
# before this line.
|
|
795
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
790
796
|
import json, sys, os
|
|
791
797
|
proof = json.load(open(sys.argv[1]))
|
|
792
798
|
d = sys.argv[2]
|
|
@@ -825,16 +831,22 @@ loki_remote_attestation_status() {
|
|
|
825
831
|
local tok
|
|
826
832
|
tok="$(jq -r '.verification.attestation // empty' "$pj" 2>/dev/null)"
|
|
827
833
|
[ -n "$tok" ] || { printf ''; return 0; }
|
|
828
|
-
python3 - "$pj" "$url" "${_LOKI_SCRIPT_DIR}" <<'PY' 2>/dev/null || printf ''
|
|
834
|
+
python3 -E - "$pj" "$url" "${_LOKI_SCRIPT_DIR}" <<'PY' 2>/dev/null || printf ''
|
|
835
|
+
# The cwd is the checkout being verified: drop it from sys.path before any
|
|
836
|
+
# other import, or a committed hashlib.py/json.py could print the verdict.
|
|
837
|
+
# The interpreter runs with -E so PYTHONPATH (an empty component would add
|
|
838
|
+
# the cwd as an absolute path) and a committed sitecustomize.py cannot act
|
|
839
|
+
# before this line.
|
|
840
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
829
841
|
import json, sys, hashlib, urllib.request
|
|
830
842
|
sys.path.insert(0, sys.argv[3])
|
|
831
843
|
try:
|
|
832
|
-
from receipt_jwt import verify_attestation
|
|
844
|
+
from receipt_jwt import verify_attestation, _CRYPTO_AVAILABLE
|
|
833
845
|
except ImportError:
|
|
834
846
|
sys.exit(1) # -> "" : cannot check, never a verdict
|
|
835
847
|
proof = json.load(open(sys.argv[1]))
|
|
836
|
-
tok = (proof.get("verification") or {}).get("attestation")
|
|
837
|
-
if
|
|
848
|
+
tok = (proof.get("verification") or {}).get("attestation")
|
|
849
|
+
if tok is None or tok == "":
|
|
838
850
|
sys.exit(1)
|
|
839
851
|
try:
|
|
840
852
|
with urllib.request.urlopen(
|
|
@@ -842,7 +854,28 @@ try:
|
|
|
842
854
|
jwks = json.loads(r.read())
|
|
843
855
|
except Exception:
|
|
844
856
|
sys.exit(1) # unreachable JWKS is UNCHECKED, not a failure
|
|
845
|
-
|
|
857
|
+
# A key set that is not {"keys": [{...}, ...]} is a key-set problem: UNCHECKED,
|
|
858
|
+
# never an accusation against the receipt. Checked BEFORE anything below can
|
|
859
|
+
# print "bad", so a bad key set can never read as TAMPERED.
|
|
860
|
+
_keys = jwks.get("keys") if isinstance(jwks, dict) else None
|
|
861
|
+
if not isinstance(_keys, list) or not all(isinstance(k, dict) for k in _keys):
|
|
862
|
+
sys.exit(1)
|
|
863
|
+
# The receipt's builder controls the token. An attestation that is not a JWT
|
|
864
|
+
# string is a malformed receipt: TAMPERED, decided without any crypto.
|
|
865
|
+
if not isinstance(tok, str):
|
|
866
|
+
print("bad"); sys.exit(0)
|
|
867
|
+
# Without `cryptography`, verify_attestation returns False and this would print
|
|
868
|
+
# "bad" -- a TAMPERED accusation for a check that never ran. A missing verifier
|
|
869
|
+
# dependency is UNCHECKED. Same guard in loki_proof_attestation_check below.
|
|
870
|
+
if not _CRYPTO_AVAILABLE:
|
|
871
|
+
sys.exit(1)
|
|
872
|
+
# The key set is known-good here, so any crash inside verification comes from
|
|
873
|
+
# the token: refuse it. Letting it propagate read as UNCHECKED, which let a
|
|
874
|
+
# forger turn TAMPERED into "install python3 cryptography".
|
|
875
|
+
try:
|
|
876
|
+
ok, claims = verify_attestation(tok, jwks)
|
|
877
|
+
except Exception:
|
|
878
|
+
print("bad"); sys.exit(0)
|
|
846
879
|
if not ok:
|
|
847
880
|
print("bad"); sys.exit(0)
|
|
848
881
|
# The signature verifying is not enough on its own: it must attest to THIS
|
|
@@ -880,19 +913,25 @@ loki_proof_attestation_check() {
|
|
|
880
913
|
local pj="$1" src="$2"
|
|
881
914
|
[ -n "$src" ] || { printf ''; return 0; }
|
|
882
915
|
command -v python3 >/dev/null 2>&1 || { printf ''; return 0; }
|
|
883
|
-
python3 - "$pj" "$src" "${_LOKI_SCRIPT_DIR}" <<'PY' 2>/dev/null || printf ''
|
|
916
|
+
python3 -E - "$pj" "$src" "${_LOKI_SCRIPT_DIR}" <<'PY' 2>/dev/null || printf ''
|
|
917
|
+
# The cwd is the checkout being verified: drop it from sys.path before any
|
|
918
|
+
# other import, or a committed hashlib.py/json.py could print the verdict.
|
|
919
|
+
# The interpreter runs with -E so PYTHONPATH (an empty component would add
|
|
920
|
+
# the cwd as an absolute path) and a committed sitecustomize.py cannot act
|
|
921
|
+
# before this line.
|
|
922
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
884
923
|
import json, sys, os, hashlib, urllib.request
|
|
885
924
|
sys.path.insert(0, sys.argv[3])
|
|
886
925
|
try:
|
|
887
|
-
from receipt_jwt import verify_attestation
|
|
926
|
+
from receipt_jwt import verify_attestation, _CRYPTO_AVAILABLE
|
|
888
927
|
except ImportError:
|
|
889
928
|
sys.exit(1) # "" : cannot check
|
|
890
929
|
try:
|
|
891
930
|
proof = json.load(open(sys.argv[1]))
|
|
892
931
|
except Exception:
|
|
893
932
|
sys.exit(1)
|
|
894
|
-
tok = (proof.get("verification") or {}).get("attestation")
|
|
895
|
-
if
|
|
933
|
+
tok = (proof.get("verification") or {}).get("attestation")
|
|
934
|
+
if tok is None or tok == "":
|
|
896
935
|
print("none"); sys.exit(0) # a FACT about the receipt, not an error
|
|
897
936
|
src = sys.argv[2]
|
|
898
937
|
try:
|
|
@@ -907,7 +946,30 @@ try:
|
|
|
907
946
|
jwks = json.loads(r.read())
|
|
908
947
|
except Exception:
|
|
909
948
|
sys.exit(1) # "" : unreachable/unreadable, no verdict
|
|
910
|
-
|
|
949
|
+
# A key set that is not {"keys": [{...}, ...]} is a key-set problem: NOT
|
|
950
|
+
# CHECKED, never an accusation. Checked BEFORE anything below can print "bad",
|
|
951
|
+
# so a bad key set can never read as FAILED. Same order in
|
|
952
|
+
# loki_remote_attestation_status above -- change both.
|
|
953
|
+
_keys = jwks.get("keys") if isinstance(jwks, dict) else None
|
|
954
|
+
if not isinstance(_keys, list) or not all(isinstance(k, dict) for k in _keys):
|
|
955
|
+
sys.exit(1) # "" : cannot check
|
|
956
|
+
# The receipt's builder controls the token. An attestation that is not a JWT
|
|
957
|
+
# string is a malformed receipt: FAILED, decided without any crypto.
|
|
958
|
+
if not isinstance(tok, str):
|
|
959
|
+
print("bad"); sys.exit(0)
|
|
960
|
+
# receipt_jwt imports without `cryptography`, and verify_attestation then
|
|
961
|
+
# returns False, which would print "bad": a FAILED verdict for a check that
|
|
962
|
+
# never ran. A missing verifier dependency is NOT CHECKED. Same guard in
|
|
963
|
+
# loki_remote_attestation_status above -- change both.
|
|
964
|
+
if not _CRYPTO_AVAILABLE:
|
|
965
|
+
sys.exit(1) # "" : cannot check
|
|
966
|
+
# The key set is known-good here, so any crash inside verification comes from
|
|
967
|
+
# the token: refuse it. Letting it propagate read as NOT CHECKED (exit 2), which
|
|
968
|
+
# let a forger turn FAILED into "could not check".
|
|
969
|
+
try:
|
|
970
|
+
ok, claims = verify_attestation(tok, jwks)
|
|
971
|
+
except Exception:
|
|
972
|
+
print("bad"); sys.exit(0)
|
|
911
973
|
if not ok:
|
|
912
974
|
print("bad"); sys.exit(0)
|
|
913
975
|
# A verifying signature is not enough: it must attest to THESE bytes. Recompute
|
|
@@ -951,7 +1013,7 @@ loki_remote_verify_receipt() {
|
|
|
951
1013
|
# empty for a local receipt, which simply means no attestation is checked.
|
|
952
1014
|
local _attest_url="${2:-}" _attest=""
|
|
953
1015
|
_attest="$(loki_remote_attestation_status "$pj" "$_attest_url")"
|
|
954
|
-
out="$(python3 "$verifier" "$pj" "${TARGET_DIR:-.}" 2>/dev/null)"
|
|
1016
|
+
out="$(python3 -E "$verifier" "$pj" "${TARGET_DIR:-.}" 2>/dev/null)"
|
|
955
1017
|
hash_ok="$(printf '%s' "$out" | jq -r '.hash_ok // false' 2>/dev/null)"
|
|
956
1018
|
gpg_ok="$(printf '%s' "$out" | jq -r '.gpg_ok | tostring' 2>/dev/null)"
|
|
957
1019
|
|
|
@@ -1057,6 +1119,18 @@ loki_remote_verify_receipt() {
|
|
|
1057
1119
|
return 0
|
|
1058
1120
|
fi
|
|
1059
1121
|
|
|
1122
|
+
# The receipt carries an attestation that could not be checked here (JWKS
|
|
1123
|
+
# unreachable, or python cryptography missing). Calling it UNSIGNED would
|
|
1124
|
+
# mislabel a signed receipt and point at the wrong fix. Same policy as the
|
|
1125
|
+
# gpg-unavailable branch above: NOT CHECKED, return 0, provenance unproven.
|
|
1126
|
+
if [ -z "$_attest" ] && jq -e '.verification.attestation // empty' "$pj" >/dev/null 2>&1; then
|
|
1127
|
+
echo -e " ${YELLOW}NOT CHECKED${NC}: the receipt carries an attestation, but it could not be checked"
|
|
1128
|
+
echo " against ${_attest_url:-<no url>} (JWKS unreachable, or python cryptography missing)."
|
|
1129
|
+
echo " Its integrity hash matches. Provenance is unproven here, not disproven."
|
|
1130
|
+
echo " Install python3 cryptography, or re-run where the build server is reachable."
|
|
1131
|
+
return 0
|
|
1132
|
+
fi
|
|
1133
|
+
|
|
1060
1134
|
echo -e " ${YELLOW}UNSIGNED${NC}: integrity hash matches. This is NOT proof of provenance."
|
|
1061
1135
|
echo " Proven: the receipt is internally consistent with its own hash."
|
|
1062
1136
|
echo " NOT proven: who produced it. An unsigned receipt is forgeable -- whoever"
|
|
@@ -9019,7 +9093,7 @@ _deploy_receipt_verdict() {
|
|
|
9019
9093
|
fi
|
|
9020
9094
|
|
|
9021
9095
|
local out hash_ok gpg_ok signed=0
|
|
9022
|
-
out="$(python3 "$verifier" "$pj" "${TARGET_DIR:-.}" 2>/dev/null || true)"
|
|
9096
|
+
out="$(python3 -E "$verifier" "$pj" "${TARGET_DIR:-.}" 2>/dev/null || true)"
|
|
9023
9097
|
|
|
9024
9098
|
# "The verifier did not RUN" and "the verifier says the hash is bad" are
|
|
9025
9099
|
# different facts and must not collapse. If they do, a broken python3
|
|
@@ -13097,12 +13171,25 @@ _loki_airgap_audit() {
|
|
|
13097
13171
|
;;
|
|
13098
13172
|
opencode|cline|aider)
|
|
13099
13173
|
model_host="${OPENAI_BASE_URL:-${OPENROUTER_API_KEY:+https://openrouter.ai}}"
|
|
13100
|
-
|
|
13174
|
+
# Local ONLY on positive evidence: a model id naming a local runtime.
|
|
13175
|
+
# An unset endpoint is not that evidence -- the opencode catalog
|
|
13176
|
+
# default is an openrouter/ model, so inference still leaves the host.
|
|
13177
|
+
# Judge only the ACTIVE provider's model variable (the one
|
|
13178
|
+
# providers/<provider>.sh reads): another provider's local model
|
|
13179
|
+
# says nothing about where this provider sends inference.
|
|
13180
|
+
local model_var model_id
|
|
13181
|
+
case "$provider" in
|
|
13182
|
+
opencode) model_var="LOKI_OPENCODE_MODEL" model_id="${LOKI_OPENCODE_MODEL:-}" ;;
|
|
13183
|
+
cline) model_var="LOKI_CLINE_MODEL" model_id="${LOKI_CLINE_MODEL:-}" ;;
|
|
13184
|
+
aider) model_var="LOKI_AIDER_MODEL" model_id="${LOKI_AIDER_MODEL:-}" ;;
|
|
13185
|
+
esac
|
|
13186
|
+
if printf '%s' "$model_id" | grep -qiE 'ollama|localhost|127\.0\.0\.1|lmstudio'; then
|
|
13101
13187
|
model_host="local weights"
|
|
13102
13188
|
model_required="no"
|
|
13103
13189
|
model_note="Local model runtime (Ollama / LM Studio / llama.cpp). No inference egress."
|
|
13104
13190
|
else
|
|
13105
|
-
|
|
13191
|
+
[ -n "$model_host" ] || model_host="remote model provider (no local model configured)"
|
|
13192
|
+
model_note="Remote model gateway. Set $model_var to an ollama/ model id for zero-egress inference."
|
|
13106
13193
|
fi
|
|
13107
13194
|
;;
|
|
13108
13195
|
*)
|
|
@@ -36193,8 +36280,11 @@ cmd_proof() {
|
|
|
36193
36280
|
echo " phases [--json] Measured phase history from real phase_change"
|
|
36194
36281
|
echo " events; unrecorded boundaries read UNKNOWN"
|
|
36195
36282
|
echo " show <id> Pretty-print .loki/proofs/<id>/proof.json"
|
|
36196
|
-
echo " verify <id> Re-check a receipt against the repo (tamper + drift)
|
|
36197
|
-
echo "
|
|
36283
|
+
echo " verify <id> Re-check a receipt against the repo (tamper + drift)."
|
|
36284
|
+
echo " Exit 0 clean, 1 tamper/drift, 2 could not check,"
|
|
36285
|
+
echo " 64 usage (no id, more than one id, unknown option,"
|
|
36286
|
+
echo " -h/--help: verify never exits 0 without a verdict),"
|
|
36287
|
+
echo " 66 unknown id. Verify it yourself."
|
|
36198
36288
|
echo " passport <contract> <id|proof.json> <output.json>"
|
|
36199
36289
|
echo " Bind work from any agent to a portable Outcome Contract"
|
|
36200
36290
|
echo " and independently rechecked Evidence Receipt"
|
|
@@ -36204,9 +36294,15 @@ cmd_proof() {
|
|
|
36204
36294
|
echo " --jwks <url|file> Also check the receipt's attestation against a published"
|
|
36205
36295
|
echo " key set. Accepts a server URL or a local jwks.json, so a"
|
|
36206
36296
|
echo " third party can check WHO produced a receipt with no API"
|
|
36207
|
-
echo " token, no gpg key import, and no network."
|
|
36208
|
-
echo "
|
|
36209
|
-
|
|
36297
|
+
echo " token, no gpg key import, and no network. Exit 1 if the"
|
|
36298
|
+
echo " token FAILED or the receipt is unsigned (ABSENT); 2 if"
|
|
36299
|
+
echo " NOT CHECKED (key set unreadable or verifier dependency"
|
|
36300
|
+
echo " missing); 64 if --jwks has no value or an empty one."
|
|
36301
|
+
echo " A 1 is never softened to 2."
|
|
36302
|
+
echo ""
|
|
36303
|
+
echo "More subcommands:"
|
|
36304
|
+
echo " md <id> Paste-able Markdown for a PR comment or Slack"
|
|
36305
|
+
echo " open <id> Open .loki/proofs/<id>/index.html in a browser"
|
|
36210
36306
|
echo " share <id> Publish the proof page as a GitHub Gist (opt-in)"
|
|
36211
36307
|
echo ""
|
|
36212
36308
|
echo "Options for 'share':"
|
|
@@ -36412,7 +36508,7 @@ PYEOF
|
|
|
36412
36508
|
echo -e "${RED}Proof Passport tool not found: ${passport_tool}${NC}" >&2
|
|
36413
36509
|
exit 3
|
|
36414
36510
|
fi
|
|
36415
|
-
python3 "$passport_tool" "$contract" "$receipt_path" "$output" "$@"
|
|
36511
|
+
python3 -E "$passport_tool" "$contract" "$receipt_path" "$output" "$@"
|
|
36416
36512
|
exit $?
|
|
36417
36513
|
;;
|
|
36418
36514
|
chain)
|
|
@@ -36435,10 +36531,27 @@ PYEOF
|
|
|
36435
36531
|
echo -e "${RED}python3 unavailable: the chain could not be verified (UNAVAILABLE, not a pass)${NC}" >&2
|
|
36436
36532
|
exit 2
|
|
36437
36533
|
fi
|
|
36534
|
+
# A verifier never exits 0 without a verdict: -h/--help (before any
|
|
36535
|
+
# "--") shows usage on stderr and exits 64, where argparse would
|
|
36536
|
+
# exit 0 with nothing checked. The tool itself keeps its exit-0
|
|
36537
|
+
# --help for direct use (tests/test_tool_exit_contract.py).
|
|
36538
|
+
local _ca
|
|
36539
|
+
for _ca in "$@"; do
|
|
36540
|
+
[ "$_ca" = "--" ] && break
|
|
36541
|
+
# argparse also treats --h/--he/--hel and any short-option
|
|
36542
|
+
# cluster containing h (-hq) as help.
|
|
36543
|
+
case "$_ca" in
|
|
36544
|
+
-h*|--h|--he|--hel|--help|-[!-]*h*)
|
|
36545
|
+
python3 -E "$chain_tool" --help >&2 || true
|
|
36546
|
+
echo "Nothing was checked (exit 64). Full help: loki proof help" >&2
|
|
36547
|
+
exit 64
|
|
36548
|
+
;;
|
|
36549
|
+
esac
|
|
36550
|
+
done
|
|
36438
36551
|
# `set -e` is active (loki:22) and a FAILED verdict is a NORMAL
|
|
36439
36552
|
# result here, so a bare call would abort before the status is read.
|
|
36440
36553
|
local _chain_rc=0
|
|
36441
|
-
python3 "$chain_tool" "$@" || _chain_rc=$?
|
|
36554
|
+
python3 -E "$chain_tool" "$@" || _chain_rc=$?
|
|
36442
36555
|
exit $_chain_rc
|
|
36443
36556
|
;;
|
|
36444
36557
|
md)
|
|
@@ -36495,35 +36608,84 @@ PYEOF
|
|
|
36495
36608
|
# so an air-gapped reviewer, handed the receipt and the key set on
|
|
36496
36609
|
# disk, can still verify -- requiring the network would exclude
|
|
36497
36610
|
# exactly the reviewer who most needs this.
|
|
36498
|
-
|
|
36499
|
-
|
|
36611
|
+
# An EMPTY --jwks value ("--jwks ''", "--jwks=") is a usage error,
|
|
36612
|
+
# sticky across every occurrence. It used to skip the attestation
|
|
36613
|
+
# check and exit 0 on an unsigned receipt, and a later empty
|
|
36614
|
+
# --jwks cancelled an earlier real one.
|
|
36615
|
+
# An UNKNOWN option, or a second positional, is a usage error too:
|
|
36616
|
+
# "--jwk keys.json" and "-jwks keys.json" used to be kept as extra
|
|
36617
|
+
# positionals and ignored, so an unsigned receipt exited 0 as if a
|
|
36618
|
+
# signature had been checked.
|
|
36619
|
+
local _pv_human=0 _pv_jwks="" _pv_jwks_empty=0 _pv_args=()
|
|
36620
|
+
local _pv_a _pv_want_jwks=0 _pv_npos=0 _pv_end_opts=0
|
|
36500
36621
|
for _pv_a in "$@"; do
|
|
36622
|
+
# After "--" everything is a proof id, so a caller can pass an
|
|
36623
|
+
# id that begins with "-" without it being read as an option.
|
|
36624
|
+
if [ "$_pv_end_opts" -eq 1 ]; then
|
|
36625
|
+
_pv_args+=("$_pv_a"); _pv_npos=$((_pv_npos + 1)); continue
|
|
36626
|
+
fi
|
|
36501
36627
|
if [ "$_pv_want_jwks" -eq 1 ]; then
|
|
36502
|
-
_pv_jwks="$_pv_a"; _pv_want_jwks=0
|
|
36628
|
+
_pv_jwks="$_pv_a"; _pv_want_jwks=0
|
|
36629
|
+
[ -n "$_pv_jwks" ] || _pv_jwks_empty=1
|
|
36630
|
+
continue
|
|
36503
36631
|
fi
|
|
36504
36632
|
case "$_pv_a" in
|
|
36505
36633
|
--human) _pv_human=1 ;;
|
|
36634
|
+
--) _pv_end_opts=1 ;;
|
|
36635
|
+
-h|--help)
|
|
36636
|
+
# verify never exits 0 without a verdict. The documented
|
|
36637
|
+
# call is "loki proof verify <id>", so an id taken from
|
|
36638
|
+
# untrusted content could be "-h"; an exit-0 help would
|
|
36639
|
+
# turn a provenance gate into a pass. Usage goes to
|
|
36640
|
+
# stderr with 64; full exit-0 help is "loki proof help".
|
|
36641
|
+
{
|
|
36642
|
+
echo "Usage: loki proof verify [--human] [--jwks <url|file>] [--] <id>"
|
|
36643
|
+
echo " Exit 0 clean, 1 tamper/drift (or --jwks FAILED/ABSENT), 2 could not check,"
|
|
36644
|
+
echo " 64 usage (no id, more than one id, unknown option, empty --jwks, -h/--help),"
|
|
36645
|
+
echo " 66 unknown id. '--' ends options, for an id that starts with '-'."
|
|
36646
|
+
echo " Nothing was checked. Full help: loki proof help"
|
|
36647
|
+
} >&2
|
|
36648
|
+
exit 64 # usage (docs/exit-codes.md)
|
|
36649
|
+
;;
|
|
36506
36650
|
--jwks) _pv_want_jwks=1 ;;
|
|
36507
|
-
--jwks=*)
|
|
36508
|
-
|
|
36651
|
+
--jwks=*)
|
|
36652
|
+
_pv_jwks="${_pv_a#--jwks=}"
|
|
36653
|
+
[ -n "$_pv_jwks" ] || _pv_jwks_empty=1
|
|
36654
|
+
;;
|
|
36655
|
+
-*)
|
|
36656
|
+
echo -e "${RED}Unknown option for 'loki proof verify': '${_pv_a}'.${NC}" >&2
|
|
36657
|
+
echo " Valid options: --human, --jwks <url|file>. Nothing was checked." >&2
|
|
36658
|
+
exit 64 # usage (docs/exit-codes.md)
|
|
36659
|
+
;;
|
|
36660
|
+
*) _pv_args+=("$_pv_a"); _pv_npos=$((_pv_npos + 1)) ;;
|
|
36509
36661
|
esac
|
|
36510
36662
|
done
|
|
36663
|
+
if [ "$_pv_npos" -gt 1 ]; then
|
|
36664
|
+
echo -e "${RED}'loki proof verify' takes one proof id; got ${_pv_npos}: ${_pv_args[*]}${NC}" >&2
|
|
36665
|
+
echo " Nothing was checked." >&2
|
|
36666
|
+
exit 64 # usage (docs/exit-codes.md)
|
|
36667
|
+
fi
|
|
36511
36668
|
if [ "$_pv_want_jwks" -eq 1 ]; then
|
|
36512
36669
|
echo -e "${RED}--jwks needs a URL or file path.${NC}" >&2
|
|
36513
|
-
exit
|
|
36670
|
+
exit 64 # usage (docs/exit-codes.md)
|
|
36671
|
+
fi
|
|
36672
|
+
if [ "$_pv_jwks_empty" -eq 1 ]; then
|
|
36673
|
+
echo -e "${RED}--jwks was given an empty value; pass a URL or file path.${NC}" >&2
|
|
36674
|
+
echo " Nothing was checked: an empty key set must not read as a skipped check." >&2
|
|
36675
|
+
exit 64 # usage (docs/exit-codes.md)
|
|
36514
36676
|
fi
|
|
36515
36677
|
set -- "${_pv_args[@]+"${_pv_args[@]}"}"
|
|
36516
36678
|
|
|
36517
36679
|
local id="${1:-}"
|
|
36518
36680
|
if [ -z "$id" ]; then
|
|
36519
36681
|
echo -e "${RED}Missing proof id.${NC} Use 'loki proof list'."
|
|
36520
|
-
exit 2
|
|
36682
|
+
exit 64 # usage (docs/exit-codes.md); 2 means "could not check"
|
|
36521
36683
|
fi
|
|
36522
36684
|
local pj="${proofs_dir}/${id}/proof.json"
|
|
36523
36685
|
if [ ! -f "$pj" ]; then
|
|
36524
36686
|
echo -e "${RED}Proof not found: ${id}${NC}"
|
|
36525
36687
|
echo "Use 'loki proof list' to see available proofs."
|
|
36526
|
-
exit 1
|
|
36688
|
+
exit 66 # input missing; 1 is reserved for tamper/drift
|
|
36527
36689
|
fi
|
|
36528
36690
|
# The verifier ships beside the generator under autonomy/lib/ (same
|
|
36529
36691
|
# dir resolution run.sh uses for proof-generator.py).
|
|
@@ -36537,11 +36699,17 @@ PYEOF
|
|
|
36537
36699
|
# its NORMAL result -- aborts the whole command before the exit
|
|
36538
36700
|
# status can be read. Without the guard the attestation block below
|
|
36539
36701
|
# is unreachable on exactly the receipts it exists to check.
|
|
36702
|
+
# No python3 means the verifier cannot run: could not check (2),
|
|
36703
|
+
# never a shell's 127 and never a pass.
|
|
36704
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
36705
|
+
echo -e "${YELLOW}NOT CHECKED${NC}: python3 is not on PATH, so the verifier could not run (exit 2)." >&2
|
|
36706
|
+
exit 2
|
|
36707
|
+
fi
|
|
36540
36708
|
local _pv_rc=0
|
|
36541
36709
|
if [ "$_pv_human" -eq 1 ]; then
|
|
36542
|
-
python3 "$verifier" --human "$pj" "${TARGET_DIR:-.}" || _pv_rc=$?
|
|
36710
|
+
python3 -E "$verifier" --human "$pj" "${TARGET_DIR:-.}" || _pv_rc=$?
|
|
36543
36711
|
else
|
|
36544
|
-
python3 "$verifier" "$pj" "${TARGET_DIR:-.}" || _pv_rc=$?
|
|
36712
|
+
python3 -E "$verifier" "$pj" "${TARGET_DIR:-.}" || _pv_rc=$?
|
|
36545
36713
|
fi
|
|
36546
36714
|
|
|
36547
36715
|
# The attestation check runs AFTER, and can only make the verdict
|
|
@@ -36565,16 +36733,25 @@ PYEOF
|
|
|
36565
36733
|
_pv_rc=1
|
|
36566
36734
|
;;
|
|
36567
36735
|
none)
|
|
36736
|
+
# Exit 1: the caller supplied a key set and asked who
|
|
36737
|
+
# signed this receipt; it is unsigned, so it is NOT
|
|
36738
|
+
# verified. Exiting 0 here let a stripped signature pass
|
|
36739
|
+
# any CI step that asked for provenance.
|
|
36568
36740
|
echo -e "${YELLOW}attestation: ABSENT -- this receipt carries no attestation.${NC}" >&2
|
|
36569
36741
|
echo " Nothing was checked against ${_pv_jwks}. The receipt may still be" >&2
|
|
36570
36742
|
echo " gpg-signed; this says only that it has no JWKS attestation." >&2
|
|
36743
|
+
echo " Not verified: a key set was supplied, but the receipt is unsigned (exit 1)." >&2
|
|
36744
|
+
_pv_rc=1
|
|
36571
36745
|
;;
|
|
36572
36746
|
*)
|
|
36573
36747
|
# Unreachable key set, unreadable file, missing python
|
|
36574
36748
|
# deps. NOT a verdict: an absent measurement must never
|
|
36575
|
-
# read as either a pass or an accusation
|
|
36576
|
-
|
|
36577
|
-
|
|
36749
|
+
# read as either a pass or an accusation, so it exits 2
|
|
36750
|
+
# (could not check). A tamper/drift 1 from the base
|
|
36751
|
+
# verifier is a finding and is never softened to 2.
|
|
36752
|
+
echo -e "${YELLOW}attestation: NOT CHECKED -- could not check against ${_pv_jwks} (key set unreadable, or verifier dependency missing).${NC}" >&2
|
|
36753
|
+
echo " This is not a failure of the receipt. Nothing was proven either way (exit 2)." >&2
|
|
36754
|
+
if [ "$_pv_rc" -eq 0 ]; then _pv_rc=2; fi
|
|
36578
36755
|
;;
|
|
36579
36756
|
esac
|
|
36580
36757
|
fi
|
package/autonomy/receipt_jwt.py
CHANGED
|
@@ -277,6 +277,11 @@ def verify_attestation(token: str, jwks: dict):
|
|
|
277
277
|
"""
|
|
278
278
|
if not _CRYPTO_AVAILABLE:
|
|
279
279
|
return False, "cryptography is not installed"
|
|
280
|
+
# The token comes from the receipt, which its builder controls. Anything
|
|
281
|
+
# that is not the expected shape is REFUSED here rather than raising, so a
|
|
282
|
+
# caller can never mistake a crash for "could not check".
|
|
283
|
+
if not isinstance(token, str):
|
|
284
|
+
return False, "token is not a string"
|
|
280
285
|
parts = token.split(".")
|
|
281
286
|
if len(parts) != 3:
|
|
282
287
|
return False, "malformed token"
|
|
@@ -286,6 +291,8 @@ def verify_attestation(token: str, jwks: dict):
|
|
|
286
291
|
sig = _b64url_decode(parts[2])
|
|
287
292
|
except (ValueError, json.JSONDecodeError):
|
|
288
293
|
return False, "malformed token"
|
|
294
|
+
if not isinstance(header, dict) or not isinstance(payload, dict):
|
|
295
|
+
return False, "malformed token: header and payload must be JSON objects"
|
|
289
296
|
if header.get("alg") != "EdDSA":
|
|
290
297
|
# Refusing an unexpected alg is what blocks the classic "alg: none" and
|
|
291
298
|
# algorithm-substitution attacks.
|