loki-mode 9.54.2 → 9.55.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 +1 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +110 -34
- package/autonomy/crash.sh +6 -6
- package/autonomy/lib/done-recognition.sh +7 -0
- package/autonomy/lib/prd-enrich.sh +7 -0
- package/autonomy/lib/proof-verify.py +93 -21
- package/autonomy/lib/voter-agents.sh +7 -0
- package/autonomy/loki +404 -119
- package/autonomy/quickstart.sh +7 -0
- package/autonomy/run.sh +605 -30
- package/autonomy/verify.sh +114 -8
- package/bin/loki +12 -0
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +6 -5
- package/docs/air-gapped.md +39 -17
- package/docs/enterprise/security.md +10 -13
- package/docs/environment-variables.md +1 -0
- package/docs/exit-codes.md +76 -2
- package/docs/v10/BACKLOG.md +23 -0
- package/docs/v10/BOARD.md +61 -32
- package/docs/v10/CONTROL.md +36 -0
- package/docs/v10/DECISIONS.md +31 -0
- package/docs/v10/METRICS.md +78 -1
- package/docs/v10/PROGRESS.md +197 -0
- package/loki-ts/dist/loki.js +7 -4
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/loader.sh +10 -0
- package/tools/receipt-attest.py +1 -1
package/README.md
CHANGED
|
@@ -748,7 +748,7 @@ env vars. See [Enterprise Identity Roadmap](docs/ENTERPRISE-IDENTITY-ROADMAP.md)
|
|
|
748
748
|
| 8 quality gates | Yes | No | No | No |
|
|
749
749
|
| Blind code review | Yes | No | No | No |
|
|
750
750
|
| Enterprise auth (OIDC token + scoped RBAC) | Yes | No | Yes | No |
|
|
751
|
-
| Air-gapped deployment |
|
|
751
|
+
| Air-gapped deployment | Verification and the build pipeline, yes; builds need a model you host ([details](docs/air-gapped.md)) | No | No | No |
|
|
752
752
|
| Docker + CI/CD generation | Yes | No | Yes | No |
|
|
753
753
|
| Source-available (BUSL-1.1) | Yes | No | No | No |
|
|
754
754
|
| Free tier | Source-available | Yes | Yes | Yes |
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ 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.55.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
470
470
|
|
|
471
471
|
---
|
|
472
472
|
|
|
473
|
-
**v9.
|
|
473
|
+
**v9.55.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
9.
|
|
1
|
+
9.55.0
|
|
@@ -219,13 +219,22 @@ council_augment_from_managed_memory() {
|
|
|
219
219
|
return 0
|
|
220
220
|
fi
|
|
221
221
|
local target_dir="${TARGET_DIR:-.}"
|
|
222
|
-
|
|
222
|
+
# D7: never fall back to the cwd. The council runs with cwd inside the
|
|
223
|
+
# agent's own repo, which can ship its own memory/managed_memory package
|
|
224
|
+
# that shadows the real one -- `${PROJECT_DIR:-$(pwd)}` would `cd` into
|
|
225
|
+
# that repo and `python3 -m memory.managed_memory.retrieve` would import
|
|
226
|
+
# the agent's module (BACKLOG 63). PROJECT_DIR must be explicit; unset ->
|
|
227
|
+
# skip augmentation (silent no-op, same as the flags-off path above).
|
|
228
|
+
if [ -z "${PROJECT_DIR:-}" ]; then
|
|
229
|
+
return 0
|
|
230
|
+
fi
|
|
231
|
+
local project_dir="$PROJECT_DIR"
|
|
223
232
|
local out_file="$target_dir/.loki/managed/council-augment.txt"
|
|
224
233
|
mkdir -p "$target_dir/.loki/managed" 2>/dev/null || true
|
|
225
234
|
(
|
|
226
235
|
cd "$project_dir" 2>/dev/null && \
|
|
227
236
|
LOKI_TARGET_DIR="$target_dir" \
|
|
228
|
-
timeout 5 python3 -m memory.managed_memory.retrieve \
|
|
237
|
+
timeout 5 python3 -E -m memory.managed_memory.retrieve \
|
|
229
238
|
--query "completion-council verdict context" --top-k 3 \
|
|
230
239
|
> "$out_file" 2>/dev/null || true
|
|
231
240
|
) || true
|
|
@@ -1961,19 +1970,44 @@ if failed_n is None:
|
|
|
1961
1970
|
# (node --test on a *.test.js with no test() calls; jest --passWithNoTests with
|
|
1962
1971
|
# no suites) records pass:"inconclusive" + status:"no_tests_run" -- a mini
|
|
1963
1972
|
# fake-green when read as affirmative. The pass value is then the STRING
|
|
1964
|
-
# "inconclusive" (not the bool True/False), so
|
|
1965
|
-
# have counted it as green.
|
|
1966
|
-
#
|
|
1967
|
-
#
|
|
1968
|
-
|
|
1973
|
+
# "inconclusive" (not the bool True/False), so an else-branch of plain PASS
|
|
1974
|
+
# would have counted it as green.
|
|
1975
|
+
#
|
|
1976
|
+
# BACKLOG 33 rework (S-07, take 2): status=='no_tests_run' MUST be checked
|
|
1977
|
+
# BEFORE the runner=='none' sentinel below. A zero-test record can carry
|
|
1978
|
+
# runner=='none' (explicit or omitted/defaulted), and the old order let
|
|
1979
|
+
# runner=='none' short-circuit first, printing PASS -- a real runner having
|
|
1980
|
+
# run and found nothing is a WORSE signal than no test tooling applying here
|
|
1981
|
+
# at all, and must never read as equal-or-better. ZEROTESTS is a distinct
|
|
1982
|
+
# verdict token (not reused INCONCLUSIVE) so the shell dispatch below can
|
|
1983
|
+
# route it to reason=no_tests_executed for EVERY runner value, without
|
|
1984
|
+
# colliding with the parse-failure sentinel INCONCLUSIVE:none:true above or
|
|
1985
|
+
# the generic NO_PASS case.
|
|
1986
|
+
#
|
|
1987
|
+
# runner=='none' below is now UNCONDITIONAL again (this is the fix for the
|
|
1988
|
+
# prior rework's regression): the ONLY real writer of runner:"none" is
|
|
1989
|
+
# run.sh's enforce_test_coverage's no-test-tooling branch, which always
|
|
1990
|
+
# writes pass:"inconclusive" (the STRING) with status:"not_run" -- never a
|
|
1991
|
+
# boolean pass:true. Narrowing this branch to require passed is True (as the
|
|
1992
|
+
# prior, unmerged rework did) therefore caught the ENTIRE real no-test-tooling
|
|
1993
|
+
# population and routed it to NO_PASS/no_pass_recorded instead of
|
|
1994
|
+
# PASS/no_test_runner, silently defeating LOKI_EVIDENCE_NO_TESTS_AFFIRMATIVE=1
|
|
1995
|
+
# for the real shape and dropping the heuristic council's vote on genuine
|
|
1996
|
+
# no-test-tooling projects. runner=='none' is a distinct dimension (no
|
|
1997
|
+
# suite applies here) from status=='no_tests_run' (a suite ran and executed
|
|
1998
|
+
# nothing); checking status first is what keeps the two separate, not a
|
|
1999
|
+
# pass-value narrowing on top of it.
|
|
2000
|
+
if status == 'no_tests_run':
|
|
2001
|
+
print('ZEROTESTS:%s:true' % runner)
|
|
2002
|
+
elif runner == 'none':
|
|
1969
2003
|
print('PASS:none:true')
|
|
1970
2004
|
elif passed is False or (failed_n or 0) > 0:
|
|
1971
2005
|
print('FAIL:%s:false' % runner)
|
|
1972
|
-
elif
|
|
1973
|
-
#
|
|
1974
|
-
#
|
|
1975
|
-
#
|
|
1976
|
-
print('
|
|
2006
|
+
elif passed is not True:
|
|
2007
|
+
# No boolean pass was recorded at all (missing, null, or a non-boolean
|
|
2008
|
+
# value): not a zero-test run (status check above already excluded that),
|
|
2009
|
+
# just an unrecorded outcome.
|
|
2010
|
+
print('NO_PASS:%s:true' % runner)
|
|
1977
2011
|
else:
|
|
1978
2012
|
print('PASS:%s:true' % runner)
|
|
1979
2013
|
PYEOF
|
|
@@ -1987,20 +2021,25 @@ PYEOF
|
|
|
1987
2021
|
fi
|
|
1988
2022
|
# INCONCLUSIVE => test_fails stays "false" => pass-through.
|
|
1989
2023
|
# No test suite ran: a present results file that records runner=="none"
|
|
1990
|
-
# is not affirmative evidence
|
|
1991
|
-
#
|
|
1992
|
-
#
|
|
1993
|
-
|
|
2024
|
+
# is not affirmative evidence UNLESS it is also the ZEROTESTS shape
|
|
2025
|
+
# (excluded below; that reason is more specific: a suite ran and found
|
|
2026
|
+
# nothing, distinct from no test tooling existing at all). Route to
|
|
2027
|
+
# council (inconclusive), not a silent diff-alone pass. Default-on;
|
|
2028
|
+
# LOKI_EVIDENCE_NO_TESTS_AFFIRMATIVE=1 restores the old affirmative-PASS
|
|
2029
|
+
# behavior for this branch only -- never for ZEROTESTS below.
|
|
2030
|
+
if [ "$test_runner" = "none" ] && [ "$_verdict" != "ZEROTESTS" ] && [ "${LOKI_EVIDENCE_NO_TESTS_AFFIRMATIVE:-0}" != "1" ]; then
|
|
1994
2031
|
test_inconclusive="true"
|
|
1995
2032
|
test_inconclusive_reason="no_test_runner"
|
|
1996
2033
|
fi
|
|
1997
|
-
# #82:
|
|
1998
|
-
#
|
|
1999
|
-
#
|
|
2000
|
-
#
|
|
2034
|
+
# #82: ZERO tests executed (pass:"inconclusive" + status:"no_tests_run")
|
|
2035
|
+
# is INCONCLUSIVE, not affirmative, for EVERY runner value including
|
|
2036
|
+
# none/omitted (BACKLOG 33 rework, take 2: the ZEROTESTS token from the
|
|
2037
|
+
# parser above already isolates this case from the runner=="none"
|
|
2038
|
+
# no-test-tooling sentinel, so this branch no longer needs to exclude
|
|
2039
|
+
# runner=="none" -- doing so was the take-1 bug this rework fixes).
|
|
2001
2040
|
# Not gated by LOKI_EVIDENCE_NO_TESTS_AFFIRMATIVE -- a zero-test run is
|
|
2002
2041
|
# never affirmative evidence regardless of that opt-out.
|
|
2003
|
-
if [ "$_verdict" = "
|
|
2042
|
+
if [ "$_verdict" = "ZEROTESTS" ]; then
|
|
2004
2043
|
test_inconclusive="true"
|
|
2005
2044
|
test_inconclusive_reason="no_tests_executed"
|
|
2006
2045
|
fi
|
|
@@ -2791,6 +2830,17 @@ print(json.dumps(items[:5]))
|
|
|
2791
2830
|
if [ "$persist_fails" = "true" ]; then persist_ok="false"; else persist_ok="true"; fi
|
|
2792
2831
|
if [ "$auth_fails" = "true" ]; then auth_ok="false"; else auth_ok="true"; fi
|
|
2793
2832
|
if [ "$authz_fails" = "true" ]; then authz_ok="false"; else authz_ok="true"; fi
|
|
2833
|
+
# BACKLOG 61: mirror the _write_evidence_details (BACKLOG 55) fix here.
|
|
2834
|
+
# $test_pass defaults to "true" and stays "true" for every inconclusive
|
|
2835
|
+
# outcome (no runner, no results file, zero tests, no pass recorded), so
|
|
2836
|
+
# writing it raw lets tests.pass read true when this block was triggered
|
|
2837
|
+
# by a DIFFERENT axis (e.g. empty_diff) while tests never actually ran.
|
|
2838
|
+
local tests_pass_json='"inconclusive"'
|
|
2839
|
+
if [ "$test_fails" = "true" ]; then
|
|
2840
|
+
tests_pass_json="false"
|
|
2841
|
+
elif [ "$test_inconclusive" != "true" ] && [ "$test_runner" != "none" ] && [ "$test_pass" = "true" ]; then
|
|
2842
|
+
tests_pass_json="true"
|
|
2843
|
+
fi
|
|
2794
2844
|
# Record WHY boot was inconclusive (no_app_runner / not_serveable / etc.) so a
|
|
2795
2845
|
# consumer of the block report can tell a genuine boot pass from a pass-through.
|
|
2796
2846
|
boot_reason_json=$(_R="${boot_inconclusive_reason:-}" python3 -E -s -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json,os; print(json.dumps(os.environ['_R']))" 2>/dev/null || echo '""')
|
|
@@ -2808,7 +2858,7 @@ print(json.dumps(items[:5]))
|
|
|
2808
2858
|
"reason": "$reason",
|
|
2809
2859
|
"checks": {
|
|
2810
2860
|
"diff": {"ok": $diff_ok, "base_sha": "$base_for_json", "files_changed": $diff_files, "sources": "committed|unstaged|staged|untracked union"},
|
|
2811
|
-
"tests": {"ok": $tests_ok, "runner": "$test_runner", "pass": $
|
|
2861
|
+
"tests": {"ok": $tests_ok, "runner": "$test_runner", "pass": $tests_pass_json},
|
|
2812
2862
|
"boot": {"ok": $boot_ok, "inconclusive": $boot_inconclusive, "reason": $boot_reason_json},
|
|
2813
2863
|
"secret": {"ok": $secret_ok},
|
|
2814
2864
|
"nomock": {"ok": $nomock_ok, "inconclusive": $nomock_inconclusive, "reason": $nomock_reason_json},
|
|
@@ -3595,14 +3645,26 @@ council_evaluate_member() {
|
|
|
3595
3645
|
# fragile log grep. The completion route guarantees this file is written
|
|
3596
3646
|
# before the council votes via ensure_completion_test_evidence()
|
|
3597
3647
|
# (autonomy/run.sh): a project with a real runner records its true PASS/FAIL,
|
|
3598
|
-
# and a project with no
|
|
3599
|
-
#
|
|
3600
|
-
#
|
|
3648
|
+
# and a project with no test tooling records the shape run.sh's
|
|
3649
|
+
# enforce_test_coverage's no-runner branch actually writes:
|
|
3650
|
+
# {"runner":"none","pass":"inconclusive","status":"not_run",...} -- the pass
|
|
3651
|
+
# value is the STRING "inconclusive", never the boolean true. A greenfield
|
|
3652
|
+
# run with an empty .loki/ has NO such file -> no positive base -> the
|
|
3653
|
+
# member stays CONTINUE.
|
|
3601
3654
|
#
|
|
3602
|
-
# Parse verdict mirrors council_evidence_gate
|
|
3603
|
-
#
|
|
3604
|
-
#
|
|
3605
|
-
#
|
|
3655
|
+
# Parse verdict mirrors council_evidence_gate (BACKLOG 33 rework, take 2):
|
|
3656
|
+
# status=='no_tests_run' is checked FIRST (a real runner ran and executed
|
|
3657
|
+
# ZERO tests, #82) and is always inconclusive, whatever the runner field --
|
|
3658
|
+
# this must be decided before runner=='none' below, or a zero-test record
|
|
3659
|
+
# with runner omitted/'none' would short-circuit into the no-tooling
|
|
3660
|
+
# sentinel and print an affirmative pass, exactly the ordering bug already
|
|
3661
|
+
# fixed in council_evidence_gate. runner=='none' is UNCONDITIONALLY pass
|
|
3662
|
+
# (the no-test-tooling sentinel: no suite applies here at all, a distinct
|
|
3663
|
+
# and BETTER signal than a suite that ran and found nothing) -- it is not
|
|
3664
|
+
# narrowed to a boolean pass:true, because the real writer never produces
|
|
3665
|
+
# that shape. passed is False => FAIL; a missing, null or non-boolean pass
|
|
3666
|
+
# key recorded no outcome => INCONCLUSIVE (not red, not positive).
|
|
3667
|
+
# Unparseable/missing file => absent.
|
|
3606
3668
|
local tr_file="$loki_dir/quality/test-results.json"
|
|
3607
3669
|
local test_evidence="absent" # absent | pass | fail | inconclusive
|
|
3608
3670
|
local test_runner_seen="none"
|
|
@@ -3618,11 +3680,14 @@ except (json.JSONDecodeError, IOError, KeyError, ValueError):
|
|
|
3618
3680
|
sys.exit(0)
|
|
3619
3681
|
runner = d.get('runner', 'none')
|
|
3620
3682
|
passed = d.get('pass')
|
|
3621
|
-
|
|
3683
|
+
status = d.get('status', '')
|
|
3684
|
+
if status == 'no_tests_run':
|
|
3685
|
+
print('inconclusive:%s' % runner)
|
|
3686
|
+
elif runner == 'none':
|
|
3622
3687
|
print('pass:none')
|
|
3623
3688
|
elif passed is False:
|
|
3624
3689
|
print('fail:%s' % runner)
|
|
3625
|
-
elif passed is not True
|
|
3690
|
+
elif passed is not True:
|
|
3626
3691
|
print('inconclusive:%s' % runner)
|
|
3627
3692
|
else:
|
|
3628
3693
|
print('pass:%s' % runner)
|
|
@@ -4623,8 +4688,19 @@ council_should_stop() {
|
|
|
4623
4688
|
# v6.83.0 Phase 1: shadow-write the final council verdict to the
|
|
4624
4689
|
# managed memory store. Backgrounded + silent; flags gate the work
|
|
4625
4690
|
# inside the Python module so no-op when off.
|
|
4691
|
+
#
|
|
4692
|
+
# D7: never fall back to the cwd. Identical threat model to
|
|
4693
|
+
# council_augment_from_managed_memory (BACKLOG 63) but more severe:
|
|
4694
|
+
# this call EXECUTES memory.managed_memory.shadow_write as a module,
|
|
4695
|
+
# not just imports one for reading, so `cd "${PROJECT_DIR:-$(pwd)}"`
|
|
4696
|
+
# with PROJECT_DIR unset runs the agent's own repo's shadow_write.py
|
|
4697
|
+
# (if it ships one) with the council's privileges -- arbitrary code
|
|
4698
|
+
# execution, not just data poisoning. PROJECT_DIR must be explicit;
|
|
4699
|
+
# unset -> skip the shadow-write entirely (silent no-op, same as the
|
|
4700
|
+
# flags-off path above; STOP still returns 0 unchanged).
|
|
4626
4701
|
if [ "${LOKI_MANAGED_AGENTS:-false}" = "true" ] && \
|
|
4627
|
-
[ "${LOKI_MANAGED_MEMORY:-false}" = "true" ]
|
|
4702
|
+
[ "${LOKI_MANAGED_MEMORY:-false}" = "true" ] && \
|
|
4703
|
+
[ -n "${PROJECT_DIR:-}" ]; then
|
|
4628
4704
|
local _verdict_file="$loki_dir/council/verdicts/iteration-$ITERATION_COUNT.json"
|
|
4629
4705
|
if [ ! -f "$_verdict_file" ]; then
|
|
4630
4706
|
# Fall back to the round vote file as the verdict payload.
|
|
@@ -4632,9 +4708,9 @@ council_should_stop() {
|
|
|
4632
4708
|
fi
|
|
4633
4709
|
if [ -f "$_verdict_file" ]; then
|
|
4634
4710
|
(
|
|
4635
|
-
cd "$
|
|
4711
|
+
cd "$PROJECT_DIR" 2>/dev/null && \
|
|
4636
4712
|
LOKI_TARGET_DIR="$loki_dir/.." \
|
|
4637
|
-
timeout 15 python3 -m memory.managed_memory.shadow_write \
|
|
4713
|
+
timeout 15 python3 -E -m memory.managed_memory.shadow_write \
|
|
4638
4714
|
--verdict "$_verdict_file" >/dev/null 2>&1 || true
|
|
4639
4715
|
) &
|
|
4640
4716
|
disown 2>/dev/null || true
|
package/autonomy/crash.sh
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
# controls BOTH PostHog usage telemetry and local crash capture. It is
|
|
6
6
|
# sourceable for helpers only; it executes nothing on source.
|
|
7
7
|
#
|
|
8
|
-
# Collection is
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# Collection is ON by default for an individual interactive install and
|
|
9
|
+
# auto-off in CI, non-interactive, LOKI_ENTERPRISE=true and LOKI_AIRGAP=true
|
|
10
|
+
# contexts (see loki_collection_enabled below and docs/PRIVACY.md).
|
|
11
11
|
#
|
|
12
|
-
#
|
|
12
|
+
# Explicit opt-in (forces collection on, even where it would auto-off):
|
|
13
13
|
# - LOKI_TELEMETRY=on (case-insensitive, exact word "on")
|
|
14
14
|
# - ~/.loki/config line: TELEMETRY_ENABLED=true (written by: loki telemetry on)
|
|
15
15
|
#
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
# - ~/.loki/config line: TELEMETRY_DISABLED=true
|
|
21
21
|
#
|
|
22
22
|
# All capture is best-effort: it never blocks the parent and always returns 0.
|
|
23
|
-
# Phase 0 has zero network egress; local capture
|
|
24
|
-
#
|
|
23
|
+
# Phase 0 crash capture has zero network egress; local capture uses the same
|
|
24
|
+
# gate as telemetry.
|
|
25
25
|
|
|
26
26
|
# Double-source guard.
|
|
27
27
|
if [ -n "${_LOKI_CRASH_SH_SOURCED:-}" ]; then
|
|
@@ -100,6 +100,13 @@ _loki_done_recog_invoke() {
|
|
|
100
100
|
# fall through to the claude/deterministic paths (fail-closed)
|
|
101
101
|
fi
|
|
102
102
|
|
|
103
|
+
# Only the provider the operator chose may see a prompt (V10 P5). The gate
|
|
104
|
+
# above admits any provider with an argv seam, but this call is claude-only.
|
|
105
|
+
# Policy (and the LOKI_ALLOW_CLAUDE_SIDECALLS=1 opt-in) lives in loader.sh.
|
|
106
|
+
# ponytail: upgrade path is routing this call through provider_invoke_argv.
|
|
107
|
+
type loki_claude_sidecall_allowed >/dev/null 2>&1 \
|
|
108
|
+
|| . "$(dirname "${BASH_SOURCE[0]}")/../../providers/loader.sh" 2>/dev/null
|
|
109
|
+
loki_claude_sidecall_allowed 2>/dev/null || return 1
|
|
103
110
|
command -v claude >/dev/null 2>&1 || return 1
|
|
104
111
|
|
|
105
112
|
# STRUCTURED VERDICT (v8.x): when the CLI supports --json-schema, force valid
|
|
@@ -76,6 +76,13 @@ _loki_prd_enrich_invoke() {
|
|
|
76
76
|
fi
|
|
77
77
|
# fall through to the claude path (fail-closed)
|
|
78
78
|
fi
|
|
79
|
+
# Only the provider the operator chose may see a prompt (V10 P5). The gate
|
|
80
|
+
# above admits any provider with an argv seam, but this call is claude-only.
|
|
81
|
+
# Policy (and the LOKI_ALLOW_CLAUDE_SIDECALLS=1 opt-in) lives in loader.sh.
|
|
82
|
+
# ponytail: upgrade path is routing this call through provider_invoke_argv.
|
|
83
|
+
type loki_claude_sidecall_allowed >/dev/null 2>&1 \
|
|
84
|
+
|| . "$(dirname "${BASH_SOURCE[0]}")/../../providers/loader.sh" 2>/dev/null
|
|
85
|
+
loki_claude_sidecall_allowed 2>/dev/null || return 1
|
|
79
86
|
command -v claude >/dev/null 2>&1 || return 1
|
|
80
87
|
local rc=0
|
|
81
88
|
local out=""
|
|
@@ -59,7 +59,10 @@ Schema compatibility:
|
|
|
59
59
|
CLI:
|
|
60
60
|
python3 autonomy/lib/proof-verify.py <proof.json> [repo_dir]
|
|
61
61
|
Prints the JSON result. Exit 0 if ok, 1 on tamper / drift / bad signature,
|
|
62
|
-
2 on a usage / load error (missing file, malformed JSON)
|
|
62
|
+
2 on a usage / load error (missing file, malformed JSON) OR when every check
|
|
63
|
+
that ran passed but drift could not be re-derived (not a git tree, no
|
|
64
|
+
base_sha, an unresolvable base ref) -- an inability to measure drift is not
|
|
65
|
+
evidence of tampering, so it must not share exit 1 with a genuine failure.
|
|
63
66
|
"""
|
|
64
67
|
|
|
65
68
|
import hashlib
|
|
@@ -791,9 +794,10 @@ def verify(proof_path, repo_dir="."):
|
|
|
791
794
|
if not _is_git_repo(repo_dir):
|
|
792
795
|
result["diff_drift"] = None
|
|
793
796
|
if not result["reason"]:
|
|
794
|
-
result["reason"] = "repo_dir is not a git
|
|
797
|
+
result["reason"] = ("could not check drift: repo_dir is not a git "
|
|
798
|
+
"work tree; drift unverifiable")
|
|
795
799
|
result["reasons"].append(
|
|
796
|
-
"drift
|
|
800
|
+
"could not check drift: %r is not a git work tree, so the recorded "
|
|
797
801
|
"diff cannot be re-derived (re-run from the repository the receipt "
|
|
798
802
|
"was generated in)" % repo_dir)
|
|
799
803
|
elif not base_sha:
|
|
@@ -801,19 +805,22 @@ def verify(proof_path, repo_dir="."):
|
|
|
801
805
|
# so the diff cannot be re-derived. Report honestly, do NOT pass.
|
|
802
806
|
result["diff_drift"] = None
|
|
803
807
|
if not result["reason"]:
|
|
804
|
-
result["reason"] = "
|
|
808
|
+
result["reason"] = ("could not check drift: base ref unresolvable "
|
|
809
|
+
"(no recorded base_sha; drift unverifiable)")
|
|
805
810
|
result["reasons"].append(
|
|
806
|
-
"drift
|
|
807
|
-
"no starting point to re-derive the diff from (schema v1.0
|
|
811
|
+
"could not check drift: the receipt records no base_sha, so there "
|
|
812
|
+
"is no starting point to re-derive the diff from (schema v1.0 "
|
|
813
|
+
"receipt)")
|
|
808
814
|
elif not _rev_resolvable(repo_dir, base_sha):
|
|
809
815
|
result["diff_drift"] = None
|
|
810
816
|
if not result["reason"]:
|
|
811
|
-
result["reason"] = ("
|
|
812
|
-
"drift unverifiable)"
|
|
817
|
+
result["reason"] = ("could not check drift: base ref unresolvable "
|
|
818
|
+
"(%s not found in repo; drift unverifiable)"
|
|
819
|
+
% base_sha)
|
|
813
820
|
result["reasons"].append(
|
|
814
|
-
"drift
|
|
815
|
-
"repository (fetch the branch, or verify against the repo
|
|
816
|
-
"receipt was generated in)" % base_sha)
|
|
821
|
+
"could not check drift: recorded base ref %s is not present in "
|
|
822
|
+
"this repository (fetch the branch, or verify against the repo "
|
|
823
|
+
"the receipt was generated in)" % base_sha)
|
|
817
824
|
else:
|
|
818
825
|
# Drift answers "does this receipt still describe the CURRENT branch
|
|
819
826
|
# state". A receipt is for verifying the work as it stands now, so we
|
|
@@ -829,10 +836,11 @@ def verify(proof_path, repo_dir="."):
|
|
|
829
836
|
if current_stat is None:
|
|
830
837
|
result["diff_drift"] = None
|
|
831
838
|
if not result["reason"]:
|
|
832
|
-
result["reason"] = "git diff could not
|
|
839
|
+
result["reason"] = ("could not check drift: git diff could not "
|
|
840
|
+
"be computed; drift unverifiable")
|
|
833
841
|
result["reasons"].append(
|
|
834
|
-
"drift
|
|
835
|
-
% base_sha)
|
|
842
|
+
"could not check drift: git diff %s..HEAD could not be "
|
|
843
|
+
"computed" % base_sha)
|
|
836
844
|
else:
|
|
837
845
|
drift = False
|
|
838
846
|
if recorded_stat is not None:
|
|
@@ -846,12 +854,13 @@ def verify(proof_path, repo_dir="."):
|
|
|
846
854
|
# compare against -- cannot confirm the facts match.
|
|
847
855
|
result["diff_drift"] = None
|
|
848
856
|
if not result["reason"]:
|
|
849
|
-
result["reason"] = ("
|
|
850
|
-
"drift
|
|
857
|
+
result["reason"] = ("could not check drift: no recorded "
|
|
858
|
+
"diff stat to compare; drift "
|
|
859
|
+
"unverifiable")
|
|
851
860
|
result["reasons"].append(
|
|
852
|
-
"drift
|
|
853
|
-
"but the receipt recorded no diff stat to
|
|
854
|
-
"against")
|
|
861
|
+
"could not check drift: the repository diff was "
|
|
862
|
+
"re-derived, but the receipt recorded no diff stat to "
|
|
863
|
+
"compare it against")
|
|
855
864
|
|
|
856
865
|
# diff_sha256: a stronger content check than the counts. Only when
|
|
857
866
|
# the receipt recorded one (v1.1).
|
|
@@ -945,13 +954,70 @@ def verify(proof_path, repo_dir="."):
|
|
|
945
954
|
# CLI shim (mirrors dashboard/audit.py _unified_cli style)
|
|
946
955
|
# ---------------------------------------------------------------------------
|
|
947
956
|
|
|
957
|
+
def drift_unverifiable(result):
|
|
958
|
+
"""True iff every check that ran passed and drift alone could not be
|
|
959
|
+
re-derived (not a git tree, no base_sha, an unresolvable base ref, or the
|
|
960
|
+
diff/tree could not be recomputed) -- the exit-2 "could not check" case
|
|
961
|
+
from docs/exit-codes.md, distinct from a genuine tamper/drift failure.
|
|
962
|
+
|
|
963
|
+
Fail-safe direction: this is an ALLOWLIST, not a check on `diff_drift is
|
|
964
|
+
None` alone. `diff_drift` (and `tree_drift`) can be None for reasons OTHER
|
|
965
|
+
than "unmeasurable" -- e.g. an early return on a missing/mismatched
|
|
966
|
+
integrity hash never reaches the drift block at all, leaving diff_drift at
|
|
967
|
+
its initial None while the receipt is genuinely tampered. Requiring every
|
|
968
|
+
OTHER check to have passed before honoring a None here keeps a real
|
|
969
|
+
failure from being laundered into "could not check". Any check added
|
|
970
|
+
later that is not explicitly listed here defaults to exit 1, never 2.
|
|
971
|
+
|
|
972
|
+
Known ceiling (not a new regression): this allowlist only covers a
|
|
973
|
+
genuinely unmeasurable check. A forger who re-hashes the receipt after
|
|
974
|
+
stripping base_sha, tree_sha256, and the diff stat produces a receipt
|
|
975
|
+
that passes hash_ok and every other listed check, then reaches this
|
|
976
|
+
function with the same unresolvable-drift signature as an honest
|
|
977
|
+
outside-the-repo verification -- so it also exits 2 ("could not check"),
|
|
978
|
+
not 1 ("failed"). `ok` stays False either way, so nothing here reports
|
|
979
|
+
VERIFIED; the ceiling is only that a deliberately stripped receipt and an
|
|
980
|
+
honestly unverifiable one are not distinguished from each other.
|
|
981
|
+
"""
|
|
982
|
+
if result.get("ok"):
|
|
983
|
+
return False
|
|
984
|
+
if not result.get("hash_ok"):
|
|
985
|
+
return False
|
|
986
|
+
if result.get("gpg_ok") not in (True, "n/a"):
|
|
987
|
+
return False
|
|
988
|
+
if result.get("headline_consistent") is False:
|
|
989
|
+
return False
|
|
990
|
+
if result.get("cost_coherent") is False:
|
|
991
|
+
return False
|
|
992
|
+
if result.get("schema_supported") is not True:
|
|
993
|
+
return False
|
|
994
|
+
if result.get("diff_drift") is True:
|
|
995
|
+
return False
|
|
996
|
+
if result.get("tree_drift") is True:
|
|
997
|
+
return False
|
|
998
|
+
# tree_drift is None also means "no tree was recorded to check" (not
|
|
999
|
+
# applicable), which is not itself a reason to report exit 2 -- only count
|
|
1000
|
+
# it as unmeasured when a tree digest was actually recorded.
|
|
1001
|
+
tree_unmeasured = (
|
|
1002
|
+
bool(result.get("tree_recheck", {}).get("recorded"))
|
|
1003
|
+
and result.get("tree_drift") is None
|
|
1004
|
+
)
|
|
1005
|
+
return result.get("diff_drift") is None or tree_unmeasured
|
|
1006
|
+
|
|
1007
|
+
|
|
948
1008
|
def render_reasons(result):
|
|
949
1009
|
"""Render a verdict as human-readable lines.
|
|
950
1010
|
|
|
951
1011
|
The JSON report is the machine surface; this is the one a person reads.
|
|
952
1012
|
A passing receipt renders the verdict alone -- never a fabricated reason.
|
|
953
1013
|
"""
|
|
954
|
-
|
|
1014
|
+
if result.get("ok"):
|
|
1015
|
+
header = "VERIFIED"
|
|
1016
|
+
elif drift_unverifiable(result):
|
|
1017
|
+
header = "COULD NOT CHECK"
|
|
1018
|
+
else:
|
|
1019
|
+
header = "FAILED"
|
|
1020
|
+
lines = [header]
|
|
955
1021
|
for reason in (result.get("reasons") or []):
|
|
956
1022
|
lines.append(" - %s" % reason)
|
|
957
1023
|
return "\n".join(lines)
|
|
@@ -984,7 +1050,13 @@ def _cli(argv=None):
|
|
|
984
1050
|
print(json.dumps({"ok": False, "error": "verify failed: %s" % exc}))
|
|
985
1051
|
return 2
|
|
986
1052
|
print(render_reasons(result) if human else json.dumps(result, indent=2))
|
|
987
|
-
|
|
1053
|
+
if result.get("ok"):
|
|
1054
|
+
return 0
|
|
1055
|
+
if drift_unverifiable(result):
|
|
1056
|
+
# An inability to measure drift is not evidence of tampering (BACKLOG
|
|
1057
|
+
# 31 / docs/exit-codes.md: 2 = could not check, never 1 = failed).
|
|
1058
|
+
return 2
|
|
1059
|
+
return 1
|
|
988
1060
|
|
|
989
1061
|
|
|
990
1062
|
if __name__ == "__main__":
|
|
@@ -309,6 +309,13 @@ loki_council_dispatch_agents() {
|
|
|
309
309
|
# 3. Invoke claude (only when the SDK path did not already produce a response).
|
|
310
310
|
# Guard against absent binary or non-zero exit.
|
|
311
311
|
if [ "$_va_sdk_done" != "1" ]; then
|
|
312
|
+
# Only the provider the operator chose may see a prompt (V10 P5): with any
|
|
313
|
+
# other provider this falls back to the heuristic council. Policy (and the
|
|
314
|
+
# LOKI_ALLOW_CLAUDE_SIDECALLS=1 opt-in) lives in providers/loader.sh.
|
|
315
|
+
# ponytail: upgrade path is a provider-neutral structured dispatch.
|
|
316
|
+
type loki_claude_sidecall_allowed >/dev/null 2>&1 \
|
|
317
|
+
|| . "${__LOKI_VA_REPO_ROOT}/providers/loader.sh" 2>/dev/null
|
|
318
|
+
loki_claude_sidecall_allowed 2>/dev/null || return 1
|
|
312
319
|
command -v claude >/dev/null 2>&1 || return 1
|
|
313
320
|
|
|
314
321
|
# caveman HARD-SUPPRESS (parsed output, v7.41.0): the response is parsed for
|