loki-mode 9.50.4 → 9.52.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 +61 -20
- package/autonomy/loki +212 -35
- package/autonomy/receipt_jwt.py +7 -0
- package/dashboard/__init__.py +1 -1
- package/docs/ENTERPRISE-SCALE-RESEARCH-2026-09.md +816 -0
- 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 +85 -0
- package/docs/v10/DECISIONS.md +52 -0
- package/docs/v10/FOUNDER-QUEUE.md +13 -0
- package/docs/v10/METRICS.md +46 -0
- package/docs/v10/PROGRESS.md +35 -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/README.md
CHANGED
|
@@ -282,7 +282,8 @@ loki proof show <id> # the facts, the assessments, and the headline
|
|
|
282
282
|
loki proof verify <id> # re-hash the receipt and re-derive the diff
|
|
283
283
|
```
|
|
284
284
|
|
|
285
|
-
`loki proof verify` exits 0 clean, 1 on tamper or drift
|
|
285
|
+
`loki proof verify` exits 0 clean, 1 on tamper or drift, 2 when it could not check
|
|
286
|
+
(full table: [docs/exit-codes.md](docs/exit-codes.md)). Receipts are attached
|
|
286
287
|
to pull requests automatically (`LOKI_PROVEN_PR=0` to opt out), so a reviewer
|
|
287
288
|
sees the evidence next to the code.
|
|
288
289
|
|
|
@@ -368,7 +369,7 @@ Receipts are written to `.loki/proofs/<run_id>/` automatically at run completion
|
|
|
368
369
|
```bash
|
|
369
370
|
loki proof list # every receipt: run id, time, council verdict, cost, files
|
|
370
371
|
loki proof show <id> # the full proof.json (facts, assessments, honesty)
|
|
371
|
-
loki proof verify <id> # re-check the receipt against the repo (exit 0 clean, 1 tamper/drift)
|
|
372
|
+
loki proof verify <id> # re-check the receipt against the repo (exit 0 clean, 1 tamper/drift, 2 could not check)
|
|
372
373
|
```
|
|
373
374
|
|
|
374
375
|
`loki proof verify` does two independent checks and prints the result as JSON:
|
package/SKILL.md
CHANGED
|
@@ -3,13 +3,13 @@ name: loki-mode
|
|
|
3
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.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v9.
|
|
6
|
+
# Loki Mode v9.52.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
10
10
|
**Spec in, verified product out.** Spec-driven: a "spec" is whatever describes the work -- a Markdown PRD, a GitHub issue, an OpenAPI doc, a Jira ticket (a PRD is one form of spec). The differentiator is the trust layer: Loki does not call work done until it is verified. The RARV-C closure loop, 8 quality gates, the completion council, and the verified-completion evidence gate must all clear before completion is accepted. The evidence gate blocks on empty-diff, red tests, an unhealthy serveable app (runtime-boot axis, `LOKI_EVIDENCE_BOOT_GATE=0` to opt out), and a leaked credential in the changed files (secret-leak axis, `LOKI_EVIDENCE_SECRET_GATE=0` to opt out) -- v8.0.0.
|
|
11
11
|
|
|
12
|
-
**Evidence Receipt (verify it yourself).** Every run writes a receipt to `.loki/proofs/<run_id>/` (opt out with `LOKI_PROOF=0`) that separates deterministic FACTS (git diff with base/head SHAs and a `diff_sha256`, the test command + exit code, the build command + exit code, each gate verdict) from AI ASSESSMENTS (the council verdict, labeled judgment not proof). The headline is computed only from the facts: VERIFIED (tests ran a real command and exited 0, diff non-empty, nothing skipped), VERIFIED WITH GAPS (each gap listed by name), or NOT VERIFIED (a check ran and failed). Inspect and re-check with `loki proof list|show <id>|verify <id>` (aliased `loki receipt`); `loki proof verify` re-hashes the receipt (tamper) and re-derives the diff from the recorded base SHA against the live repo (drift), exiting 0 clean / 1 tamper-or-drift. This is honesty-of-done, not a claim that the code is bug-free.
|
|
12
|
+
**Evidence Receipt (verify it yourself).** Every run writes a receipt to `.loki/proofs/<run_id>/` (opt out with `LOKI_PROOF=0`) that separates deterministic FACTS (git diff with base/head SHAs and a `diff_sha256`, the test command + exit code, the build command + exit code, each gate verdict) from AI ASSESSMENTS (the council verdict, labeled judgment not proof). The headline is computed only from the facts: VERIFIED (tests ran a real command and exited 0, diff non-empty, nothing skipped), VERIFIED WITH GAPS (each gap listed by name), or NOT VERIFIED (a check ran and failed). Inspect and re-check with `loki proof list|show <id>|verify <id>` (aliased `loki receipt`); `loki proof verify` re-hashes the receipt (tamper) and re-derives the diff from the recorded base SHA against the live repo (drift), exiting 0 clean / 1 tamper-or-drift / 2 could not check (64 usage, 66 unknown id; see `docs/exit-codes.md`). This is honesty-of-done, not a claim that the code is bug-free.
|
|
13
13
|
|
|
14
14
|
**Provider-agnostic (stable since v5.0.0):** runs on Claude/Codex/Cline/Aider with abstract model tiers and degraded mode for non-Claude providers; no vendor lock-in. Gemini deprecated v7.5.18. See `skills/providers.md`. **Current track (v8.0.0):** the Anthropic Agent SDK route (see below), spec-mode expansion for OpenAPI/GraphQL/Postman contracts, the runtime-boot and secret-leak evidence axes, and `loki steer` / `loki why` for mid-run control. Earlier tracks: LSP grounding as a first-class agent tool (v7.7.x) and Phase 1 RARV-C closure (real provider judges, gate-failure flock, synthetic PRD e2e, status `--json`).
|
|
15
15
|
|
|
@@ -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
|
-
**v9.
|
|
473
|
+
**v9.52.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
9.
|
|
1
|
+
9.52.0
|
|
@@ -1937,7 +1937,9 @@ except (json.JSONDecodeError, IOError, KeyError, ValueError):
|
|
|
1937
1937
|
print('INCONCLUSIVE:none:true')
|
|
1938
1938
|
sys.exit(0)
|
|
1939
1939
|
runner = d.get('runner', 'none')
|
|
1940
|
-
|
|
1940
|
+
# No default of True: a results file with no "pass" key recorded no outcome, and
|
|
1941
|
+
# an unrecorded outcome is not a pass. None falls to INCONCLUSIVE below.
|
|
1942
|
+
passed = d.get('pass')
|
|
1941
1943
|
status = d.get('status', '')
|
|
1942
1944
|
#82 (zero-test-file hardening): a runner that ran but executed ZERO real tests
|
|
1943
1945
|
# (node --test on a *.test.js with no test() calls; jest --passWithNoTests with
|
|
@@ -3558,9 +3560,11 @@ council_evaluate_member() {
|
|
|
3558
3560
|
# the member stays CONTINUE.
|
|
3559
3561
|
#
|
|
3560
3562
|
# Parse verdict mirrors council_evidence_gate: runner=="none" => PASS,
|
|
3561
|
-
# pass is False => FAIL,
|
|
3563
|
+
# pass is False => FAIL, only a boolean True (and not status no_tests_run)
|
|
3564
|
+
# => PASS; a missing, null or non-boolean pass key recorded no outcome =>
|
|
3565
|
+
# INCONCLUSIVE (not red, not positive). Unparseable/missing file => absent.
|
|
3562
3566
|
local tr_file="$loki_dir/quality/test-results.json"
|
|
3563
|
-
local test_evidence="absent" # absent | pass | fail
|
|
3567
|
+
local test_evidence="absent" # absent | pass | fail | inconclusive
|
|
3564
3568
|
local test_runner_seen="none"
|
|
3565
3569
|
if [ -f "$tr_file" ]; then
|
|
3566
3570
|
local _tr_status
|
|
@@ -3573,11 +3577,13 @@ except (json.JSONDecodeError, IOError, KeyError, ValueError):
|
|
|
3573
3577
|
print('absent:none')
|
|
3574
3578
|
sys.exit(0)
|
|
3575
3579
|
runner = d.get('runner', 'none')
|
|
3576
|
-
passed = d.get('pass'
|
|
3580
|
+
passed = d.get('pass')
|
|
3577
3581
|
if runner == 'none':
|
|
3578
3582
|
print('pass:none')
|
|
3579
3583
|
elif passed is False:
|
|
3580
3584
|
print('fail:%s' % runner)
|
|
3585
|
+
elif passed is not True or d.get('status') == 'no_tests_run':
|
|
3586
|
+
print('inconclusive:%s' % runner)
|
|
3581
3587
|
else:
|
|
3582
3588
|
print('pass:%s' % runner)
|
|
3583
3589
|
" 2>/dev/null || echo "absent:none")
|
|
@@ -3595,19 +3601,42 @@ else:
|
|
|
3595
3601
|
local positive="false"
|
|
3596
3602
|
case "$role" in
|
|
3597
3603
|
requirements_verifier)
|
|
3598
|
-
# Positive: tests not red AND no
|
|
3599
|
-
#
|
|
3600
|
-
#
|
|
3601
|
-
#
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3604
|
+
# Positive: tests not red AND no UNFINISHED work in any queue.
|
|
3605
|
+
#
|
|
3606
|
+
# UNFINISHED WORK SPANS EVERY QUEUE THAT HOLDS IT, not just pending.
|
|
3607
|
+
#
|
|
3608
|
+
# This counted ONLY pending.json, which was safe purely by accident:
|
|
3609
|
+
# nothing in the runtime ever promoted a task out of pending, so the
|
|
3610
|
+
# count never dropped and this member stayed negative. The moment a
|
|
3611
|
+
# selector claims an item (pending -> in-progress), pending hits 0
|
|
3612
|
+
# and this member would vote COMPLETE with the work still running --
|
|
3613
|
+
# a gate weakened as a SIDE EFFECT of a throughput change, which is
|
|
3614
|
+
# the one thing the trust core must never do.
|
|
3615
|
+
#
|
|
3616
|
+
# in-progress: claimed and running. blocked: parked awaiting a fact.
|
|
3617
|
+
# Both are unfinished. An ABSENT queue file is still not
|
|
3618
|
+
# disqualifying (a legit run need not have one); only a present file
|
|
3619
|
+
# with a non-zero count blocks.
|
|
3620
|
+
#
|
|
3621
|
+
# Pinned by tests/test-completion-council-affirmative-evidence.sh
|
|
3622
|
+
# Cases 6 and 7, with Case 8 as the positive control proving this
|
|
3623
|
+
# can still reach COMPLETE when every queue really is empty.
|
|
3624
|
+
local unfinished=0
|
|
3625
|
+
local _q _qcount
|
|
3626
|
+
for _q in pending in-progress blocked; do
|
|
3627
|
+
[ -f "$loki_dir/queue/${_q}.json" ] || continue
|
|
3628
|
+
_qcount=$(_QUEUE_FILE="$loki_dir/queue/${_q}.json" python3 -c "import json, os
|
|
3629
|
+
d = json.load(open(os.environ['_QUEUE_FILE']))
|
|
3630
|
+
print(len(d.get('tasks', d) if isinstance(d, dict) else d))" 2>/dev/null || echo "0")
|
|
3631
|
+
# Guard against a non-numeric read (malformed file, python absent).
|
|
3632
|
+
case "$_qcount" in ''|*[!0-9]*) _qcount=0 ;; esac
|
|
3633
|
+
if [ "$_qcount" -gt 0 ]; then
|
|
3634
|
+
unfinished=$((unfinished + _qcount))
|
|
3606
3635
|
blocked="true"
|
|
3607
|
-
reasons="${reasons}$
|
|
3636
|
+
reasons="${reasons}$_qcount tasks still ${_q}; "
|
|
3608
3637
|
fi
|
|
3609
|
-
|
|
3610
|
-
if [ "$test_evidence" = "pass" ] && [ "$
|
|
3638
|
+
done
|
|
3639
|
+
if [ "$test_evidence" = "pass" ] && [ "$unfinished" -eq 0 ]; then
|
|
3611
3640
|
positive="true"
|
|
3612
3641
|
fi
|
|
3613
3642
|
;;
|
|
@@ -3619,6 +3648,8 @@ else:
|
|
|
3619
3648
|
# a no-tests / greenfield project leaves test_auditor at CONTINUE.
|
|
3620
3649
|
if [ "$test_evidence" = "absent" ]; then
|
|
3621
3650
|
reasons="${reasons}no structured test results found; "
|
|
3651
|
+
elif [ "$test_evidence" = "inconclusive" ]; then
|
|
3652
|
+
reasons="${reasons}structured test results inconclusive (runner '$test_runner_seen' recorded no boolean pass); "
|
|
3622
3653
|
elif [ "$test_runner_seen" = "none" ]; then
|
|
3623
3654
|
reasons="${reasons}no real test suite ran (runner none); "
|
|
3624
3655
|
elif [ "$test_evidence" = "pass" ]; then
|
|
@@ -3823,7 +3854,12 @@ council_devils_advocate_review() {
|
|
|
3823
3854
|
# (.loki/quality/test-results.json, written by run.sh:ensure_completion_test
|
|
3824
3855
|
# _evidence; parsed the same way as council_evaluate_member ~2414-2438 and the
|
|
3825
3856
|
# evidence gate). Parse verdict: runner=="none" => PASS (no real suite to
|
|
3826
|
-
# contradict completion), pass is False => FAIL,
|
|
3857
|
+
# contradict completion), pass is False => FAIL, a boolean True => PASS,
|
|
3858
|
+
# anything else (missing/null/non-boolean pass, status no_tests_run) =>
|
|
3859
|
+
# INCONCLUSIVE. Only FAIL is an issue here: inconclusive does not contradict
|
|
3860
|
+
# completion, exactly as council_evidence_gate lets it through, and vetoing
|
|
3861
|
+
# it would stall a zero-test run (#82) that is meant to reach this vote.
|
|
3862
|
+
# The legacy log
|
|
3827
3863
|
# glob is kept ONLY as an ADDITIONAL red signal -- its absence is NOT an issue
|
|
3828
3864
|
# (nothing writes .loki/logs/test-*.log, so an empty glob is the normal case
|
|
3829
3865
|
# and must never veto a unanimous COMPLETE on its own).
|
|
@@ -3839,11 +3875,13 @@ except (json.JSONDecodeError, IOError, KeyError, ValueError):
|
|
|
3839
3875
|
print('absent')
|
|
3840
3876
|
sys.exit(0)
|
|
3841
3877
|
runner = d.get('runner', 'none')
|
|
3842
|
-
passed = d.get('pass'
|
|
3878
|
+
passed = d.get('pass')
|
|
3843
3879
|
if runner == 'none':
|
|
3844
3880
|
print('pass')
|
|
3845
3881
|
elif passed is False:
|
|
3846
3882
|
print('fail')
|
|
3883
|
+
elif passed is not True or d.get('status') == 'no_tests_run':
|
|
3884
|
+
print('inconclusive')
|
|
3847
3885
|
else:
|
|
3848
3886
|
print('pass')
|
|
3849
3887
|
" 2>/dev/null || echo "absent")
|
|
@@ -4353,9 +4391,12 @@ try:
|
|
|
4353
4391
|
except (json.JSONDecodeError, IOError, KeyError, ValueError):
|
|
4354
4392
|
print('no'); sys.exit(0)
|
|
4355
4393
|
runner = d.get('runner', 'none')
|
|
4356
|
-
passed = d.get('pass'
|
|
4357
|
-
# Affirmative green requires a REAL suite (runner != none) that
|
|
4358
|
-
|
|
4394
|
+
passed = d.get('pass')
|
|
4395
|
+
# Affirmative green requires a REAL suite (runner != none) that recorded a
|
|
4396
|
+
# boolean True pass. A missing/null/non-boolean pass key or a zero-test run
|
|
4397
|
+
# (status no_tests_run) is inconclusive, which is not green.
|
|
4398
|
+
print('yes' if (runner != 'none' and passed is True
|
|
4399
|
+
and d.get('status') != 'no_tests_run') else 'no')
|
|
4359
4400
|
" 2>/dev/null || echo "no")
|
|
4360
4401
|
[ "$tr_state" = "yes" ] || return 1
|
|
4361
4402
|
|
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.
|