loki-mode 7.129.5 → 8.0.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 +81 -5
- package/SKILL.md +66 -5
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +37 -0
- package/autonomy/completion-council.sh +862 -28
- package/autonomy/council-v2.sh +39 -0
- package/autonomy/crash.sh +3 -2
- package/autonomy/grill.sh +42 -0
- package/autonomy/hooks/validate-bash.sh +301 -4
- package/autonomy/lib/cockpit-render.sh +8 -2
- package/autonomy/lib/cr-rematerialize.py +101 -4
- package/autonomy/lib/deadline.py +957 -0
- package/autonomy/lib/dependency-setup.sh +205 -0
- package/autonomy/lib/done-recognition.sh +45 -0
- package/autonomy/lib/efficiency_cost.py +13 -6
- package/autonomy/lib/no_mock_scan.py +793 -0
- package/autonomy/lib/prd-enrich.sh +42 -0
- package/autonomy/lib/proof-analytics-props.py +69 -0
- package/autonomy/lib/proof-generator.py +282 -95
- package/autonomy/lib/proof-template.html +15 -12
- package/autonomy/lib/proof-verify.py +151 -102
- package/autonomy/lib/requirements_contract.py +848 -0
- package/autonomy/lib/sdk-mode.sh +103 -0
- package/autonomy/lib/secret-scan.sh +139 -0
- package/autonomy/lib/spec-expand.sh +208 -0
- package/autonomy/lib/tree_digest.py +266 -0
- package/autonomy/lib/voter-agents.sh +58 -8
- package/autonomy/lib/workspace_diff.py +124 -0
- package/autonomy/loki +189 -17
- package/autonomy/playwright-verify.sh +501 -0
- package/autonomy/prd-checklist.sh +17 -8
- package/autonomy/provider-offer.sh +111 -0
- package/autonomy/run.sh +4417 -676
- package/autonomy/sandbox.sh +42 -0
- package/autonomy/spec-interrogation.sh +148 -5
- package/autonomy/spec.sh +118 -1
- package/autonomy/telemetry.sh +133 -7
- package/autonomy/verify.sh +107 -0
- package/bin/loki +110 -3
- package/completions/_loki +10 -0
- package/completions/loki.bash +1 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +96 -7
- package/dashboard/build_supervisor.py +1619 -0
- package/dashboard/control.py +8 -0
- package/dashboard/prompt_optimizer.py +7 -0
- package/dashboard/server.py +577 -22
- package/dashboard/static/trust.html +1 -1
- package/docs/ARCHITECTURE-OVERVIEW.md +207 -0
- package/docs/AUTONOMI-ECOSYSTEM.md +107 -0
- package/docs/INSTALLATION.md +19 -2
- package/docs/MODEL-EQUIVALENCE-HARNESS-PLAN.md +70 -0
- package/docs/PLANS-INDEX.md +33 -0
- package/docs/PRIVACY.md +29 -1
- package/docs/SIGNED-RECEIPTS.md +102 -0
- package/docs/SONNET5-DEFAULT-PLAN.md +1 -1
- package/docs/V8-ACCEPTANCE-TRIAGE-2026-07-24.md +114 -0
- package/docs/V8-AGENT-SDK-PLAN.md +692 -0
- package/docs/V8-COMPLEXITY-AUDIT-2026-07-24.md +45 -0
- package/docs/V8-MAJOR-RELEASE-PLAN.md +137 -0
- package/docs/V8-OVERNIGHT-PLAN-2026-07-25.md +82 -0
- package/docs/V8-RUNTIME-TRUTH-2026-07-25.md +232 -0
- package/docs/V8-SDK-RESEARCH-RAW.md +129 -0
- package/docs/alternative-installations.md +4 -4
- package/loki-ts/dist/loki.js +674 -285
- package/loki-ts/package.json +2 -0
- package/mcp/__init__.py +1 -1
- package/package.json +7 -6
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +75 -0
- package/providers/codex.sh +85 -13
- package/references/sdk-mode.md +106 -0
- package/skills/model-selection.md +1 -1
- package/skills/quality-gates.md +22 -0
|
@@ -47,6 +47,25 @@
|
|
|
47
47
|
#
|
|
48
48
|
#===============================================================================
|
|
49
49
|
|
|
50
|
+
# RUN-25 iter 21 (Wave D #2): share the ONE secret matcher with the commit gate.
|
|
51
|
+
# The completion evidence gate uses _commit_scan_secret_file / _commit_path_looks_secret
|
|
52
|
+
# to add a secret-leak axis, so a credential can no longer ship in a "done"
|
|
53
|
+
# deliverable via the completion-promise / dirty-tree route. Resolve this file's
|
|
54
|
+
# dir robustly: BASH_SOURCE[0] can be EMPTY when the file is sourced at top level,
|
|
55
|
+
# so fall back to run.sh's exported SCRIPT_DIR and finally to the on-disk location.
|
|
56
|
+
_LOKI_CC_DIR="$(dirname "${BASH_SOURCE[0]:-}" 2>/dev/null)"
|
|
57
|
+
if [ -z "$_LOKI_CC_DIR" ] || [ "$_LOKI_CC_DIR" = "." ]; then
|
|
58
|
+
_LOKI_CC_DIR="${SCRIPT_DIR:-}"
|
|
59
|
+
fi
|
|
60
|
+
for _cc_cand in "$_LOKI_CC_DIR/lib/secret-scan.sh" "${SCRIPT_DIR:-}/lib/secret-scan.sh" "autonomy/lib/secret-scan.sh"; do
|
|
61
|
+
if [ -n "$_cc_cand" ] && [ -f "$_cc_cand" ]; then
|
|
62
|
+
# shellcheck source=lib/secret-scan.sh
|
|
63
|
+
source "$_cc_cand"
|
|
64
|
+
break
|
|
65
|
+
fi
|
|
66
|
+
done
|
|
67
|
+
unset _LOKI_CC_DIR _cc_cand
|
|
68
|
+
|
|
50
69
|
# Council configuration
|
|
51
70
|
COUNCIL_ENABLED=${LOKI_COUNCIL_ENABLED:-true}
|
|
52
71
|
COUNCIL_SIZE=${LOKI_COUNCIL_SIZE:-3}
|
|
@@ -128,6 +147,31 @@ _council_effective_min_iter() {
|
|
|
128
147
|
fi
|
|
129
148
|
printf '%s' "${COUNCIL_MIN_ITERATIONS}"
|
|
130
149
|
}
|
|
150
|
+
|
|
151
|
+
# _loki_auto_tune_interval: OPT-IN auto-tune of the council CHECK interval by
|
|
152
|
+
# complexity. Env: LOKI_AUTO_TUNE (default 0 = OFF; stock runs are unchanged).
|
|
153
|
+
# Pure function of $1 (complexity tier) so it is unit-testable in isolation.
|
|
154
|
+
# When LOKI_AUTO_TUNE!=1 -> echoes the current COUNCIL_CHECK_INTERVAL verbatim
|
|
155
|
+
# (no behavior change). When ON: simple -> 3 (converge faster on trivial builds,
|
|
156
|
+
# fewer idle iterations before the council MAY evaluate); standard/complex/unknown
|
|
157
|
+
# -> COUNCIL_CHECK_INTERVAL (unchanged, default 5).
|
|
158
|
+
#
|
|
159
|
+
# This only changes WHEN the council is allowed to evaluate, never WHETHER a gate
|
|
160
|
+
# passes: MIN_ITERATIONS floor, hard gate, evidence gate and the aggregate vote
|
|
161
|
+
# are all unchanged. Explicit LOKI_COUNCIL_CHECK_INTERVAL still wins because
|
|
162
|
+
# COUNCIL_CHECK_INTERVAL already carries it as the base value.
|
|
163
|
+
_loki_auto_tune_interval() {
|
|
164
|
+
local complexity="${1:-}"
|
|
165
|
+
local base="${COUNCIL_CHECK_INTERVAL:-5}"
|
|
166
|
+
if [ "${LOKI_AUTO_TUNE:-0}" != "1" ]; then
|
|
167
|
+
printf '%s' "$base"
|
|
168
|
+
return 0
|
|
169
|
+
fi
|
|
170
|
+
case "$complexity" in
|
|
171
|
+
simple) printf '%s' 3 ;;
|
|
172
|
+
*) printf '%s' "$base" ;;
|
|
173
|
+
esac
|
|
174
|
+
}
|
|
131
175
|
COUNCIL_CONVERGENCE_WINDOW=${LOKI_COUNCIL_CONVERGENCE_WINDOW:-3}
|
|
132
176
|
COUNCIL_STAGNATION_LIMIT=${LOKI_COUNCIL_STAGNATION_LIMIT:-5}
|
|
133
177
|
COUNCIL_DONE_SIGNAL_LIMIT=${LOKI_COUNCIL_DONE_SIGNAL_LIMIT:-10}
|
|
@@ -263,8 +307,30 @@ council_track_iteration() {
|
|
|
263
307
|
fi
|
|
264
308
|
COUNCIL_LAST_DIFF_HASH="$combined_hash"
|
|
265
309
|
|
|
266
|
-
# Track "done" signals from agent output
|
|
267
|
-
|
|
310
|
+
# Track "done" signals from agent output.
|
|
311
|
+
#
|
|
312
|
+
# A "done signal" is any claim by the agent that the work is complete this
|
|
313
|
+
# iteration. There are TWO ways it can claim: (1) the STRUCTURED path -- the
|
|
314
|
+
# loki_complete_task MCP tool writing .loki/signals/COMPLETION_REQUESTED (or
|
|
315
|
+
# the runner's TASK_COMPLETION_CLAIMED), which is the DEFAULT since v6.82.0;
|
|
316
|
+
# and (2) fuzzy completion-like language in the iteration log. Historically
|
|
317
|
+
# only (2) was counted -- so a build that claimed done every iteration via the
|
|
318
|
+
# structured signal (medium confidence, thin evidence, council correctly
|
|
319
|
+
# rejects) never incremented COUNCIL_TOTAL_DONE_SIGNALS, the done-signal safety
|
|
320
|
+
# valve (council_should_stop, "Safety valve 2") never armed, and the loop ran
|
|
321
|
+
# to the wall spending real dollars. Observed on real builds: 11 identical
|
|
322
|
+
# structured claims, 0 counted, 60-min timeout at ~$34. We now count a
|
|
323
|
+
# structured claim as a done signal too, PEEKING the signal files
|
|
324
|
+
# non-destructively (the council owns consumption; the same two paths the loop
|
|
325
|
+
# tests at the council_should_stop call site).
|
|
326
|
+
local _done_this_iter=0
|
|
327
|
+
|
|
328
|
+
if [ -f "${TARGET_DIR:-.}/.loki/signals/COMPLETION_REQUESTED" ] \
|
|
329
|
+
|| [ -f "${TARGET_DIR:-.}/.loki/signals/TASK_COMPLETION_CLAIMED" ]; then
|
|
330
|
+
_done_this_iter=1
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
if [ "$_done_this_iter" = "0" ] && [ -n "$log_file" ] && [ -f "$log_file" ]; then
|
|
268
334
|
# Check last 200 lines for completion-like language
|
|
269
335
|
local done_indicators
|
|
270
336
|
done_indicators=$(tail -200 "$log_file" 2>/dev/null | grep -ciE \
|
|
@@ -273,14 +339,15 @@ council_track_iteration() {
|
|
|
273
339
|
# Ensure we have a clean integer (strip any whitespace/newlines)
|
|
274
340
|
done_indicators=$(echo "$done_indicators" | tr -dc '0-9')
|
|
275
341
|
done_indicators="${done_indicators:-0}"
|
|
342
|
+
[ "$done_indicators" -gt 0 ] && _done_this_iter=1
|
|
343
|
+
fi
|
|
276
344
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
fi
|
|
345
|
+
if [ "$_done_this_iter" = "1" ]; then
|
|
346
|
+
((COUNCIL_DONE_SIGNALS++))
|
|
347
|
+
((COUNCIL_TOTAL_DONE_SIGNALS++))
|
|
348
|
+
else
|
|
349
|
+
# Reset consecutive count if agent stopped claiming done
|
|
350
|
+
COUNCIL_DONE_SIGNALS=0
|
|
284
351
|
fi
|
|
285
352
|
|
|
286
353
|
# Store convergence data point
|
|
@@ -1252,7 +1319,25 @@ except: print('Results unavailable')
|
|
|
1252
1319
|
council_reverify_checklist() {
|
|
1253
1320
|
if type checklist_verify &>/dev/null && [ -f ".loki/checklist/checklist.json" ]; then
|
|
1254
1321
|
log_info "[Council] Re-verifying checklist before evaluation..."
|
|
1255
|
-
|
|
1322
|
+
# RUN-25 iter 15 (Wave B #10): capture the REAL exit status. The old
|
|
1323
|
+
# `checklist_verify 2>/dev/null || true` swallowed a failure, leaving LAST
|
|
1324
|
+
# iteration's (possibly-green) verification-results.json in place -- so a
|
|
1325
|
+
# build that regressed since the last checklist run would sail through the
|
|
1326
|
+
# gates on STALE green evidence. On a re-verify failure we cannot trust the
|
|
1327
|
+
# existing results, so we mark them unverifiable: overwrite
|
|
1328
|
+
# verification-results.json with an explicit critical-failure sentinel that
|
|
1329
|
+
# council_checklist_gate reads as BLOCK (fail-closed). The next successful
|
|
1330
|
+
# re-verify overwrites the sentinel with fresh data.
|
|
1331
|
+
local _rv_rc=0
|
|
1332
|
+
checklist_verify 2>/dev/null || _rv_rc=$?
|
|
1333
|
+
if [ "$_rv_rc" -ne 0 ]; then
|
|
1334
|
+
log_warn "[Council] Checklist re-verify FAILED (rc=$_rv_rc); invalidating stale results (fail-closed)."
|
|
1335
|
+
local _rf=".loki/checklist/verification-results.json"
|
|
1336
|
+
local _tmp="${_rf}.tmp.$$"
|
|
1337
|
+
if printf '%s\n' '{"categories":[{"items":[{"id":"reverify_failed","title":"checklist re-verify failed","priority":"critical","status":"failing"}]}]}' > "$_tmp" 2>/dev/null; then
|
|
1338
|
+
mv -f "$_tmp" "$_rf" 2>/dev/null || rm -f "$_tmp" 2>/dev/null
|
|
1339
|
+
fi
|
|
1340
|
+
fi
|
|
1256
1341
|
fi
|
|
1257
1342
|
}
|
|
1258
1343
|
|
|
@@ -1267,11 +1352,24 @@ council_checklist_gate() {
|
|
|
1267
1352
|
local waivers_file=".loki/checklist/waivers.json"
|
|
1268
1353
|
local heldout_file=".loki/checklist/held-out.json"
|
|
1269
1354
|
|
|
1270
|
-
# No checklist = no gate (backwards compatible)
|
|
1355
|
+
# No checklist = no gate (backwards compatible). Absent is genuinely "nothing
|
|
1356
|
+
# to verify"; present-but-corrupt is handled fail-closed below.
|
|
1271
1357
|
if [ ! -f "$results_file" ]; then
|
|
1272
1358
|
return 0
|
|
1273
1359
|
fi
|
|
1274
1360
|
|
|
1361
|
+
# RUN-25 iter 13 (Wave B #4): the results file EXISTS (a checklist was
|
|
1362
|
+
# generated, possibly with a failing critical item), so if we cannot read it
|
|
1363
|
+
# we must fail CLOSED -- a checklist we cannot parse is NOT proof of a clean
|
|
1364
|
+
# build. python3 absent -> we cannot evaluate the gate at all -> BLOCK rather
|
|
1365
|
+
# than the old `|| echo PASS`, which cleared the first hard gate on a broken
|
|
1366
|
+
# host. (Absent python is a real deployment problem; failing open here is
|
|
1367
|
+
# exactly the fake-green this gate exists to prevent.)
|
|
1368
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
1369
|
+
log_warn "[Council] Hard gate BLOCKED: python3 unavailable, cannot verify checklist (fail-closed)."
|
|
1370
|
+
return 1
|
|
1371
|
+
fi
|
|
1372
|
+
|
|
1275
1373
|
# Check for critical failures, excluding waived AND held-out items. Held-out
|
|
1276
1374
|
# items (v7.28.0) must NOT block here: they are evaluated separately by
|
|
1277
1375
|
# council_heldout_gate at the ship gate, and surfacing them in this gate's
|
|
@@ -1287,8 +1385,11 @@ heldout_file = os.environ.get('_HELDOUT_FILE', '')
|
|
|
1287
1385
|
try:
|
|
1288
1386
|
with open(results_file) as f:
|
|
1289
1387
|
results = json.load(f)
|
|
1290
|
-
except (json.JSONDecodeError, IOError, KeyError):
|
|
1291
|
-
|
|
1388
|
+
except (json.JSONDecodeError, IOError, KeyError, ValueError):
|
|
1389
|
+
# RUN-25 iter 13 (Wave B #4): the file EXISTS but is unreadable/corrupt. A
|
|
1390
|
+
# checklist we cannot parse is NOT proof of a clean build -- a failing
|
|
1391
|
+
# critical item could be hiding in the unparseable bytes. Fail CLOSED.
|
|
1392
|
+
print('BLOCK:unreadable_checklist_results')
|
|
1292
1393
|
sys.exit(0)
|
|
1293
1394
|
|
|
1294
1395
|
# Load waivers
|
|
@@ -1325,7 +1426,7 @@ if critical_failures:
|
|
|
1325
1426
|
else:
|
|
1326
1427
|
print('PASS')
|
|
1327
1428
|
sys.exit(0)
|
|
1328
|
-
" 2>/dev/null || echo "
|
|
1429
|
+
" 2>/dev/null || echo "BLOCK:checklist_gate_error")
|
|
1329
1430
|
|
|
1330
1431
|
if [[ "$gate_result" == BLOCK:* ]]; then
|
|
1331
1432
|
local failures="${gate_result#BLOCK:}"
|
|
@@ -1425,8 +1526,14 @@ try:
|
|
|
1425
1526
|
results = json.load(f)
|
|
1426
1527
|
with open(heldout_file) as f:
|
|
1427
1528
|
heldout_ids = set(json.load(f).get('held_out', []))
|
|
1428
|
-
except (json.JSONDecodeError, IOError, KeyError):
|
|
1429
|
-
|
|
1529
|
+
except (json.JSONDecodeError, IOError, KeyError, ValueError):
|
|
1530
|
+
# RUN-25 iter 13 (Wave B #4): BOTH files are known to EXIST (guarded above),
|
|
1531
|
+
# so an error here is a present-but-corrupt results/held-out file, NOT an
|
|
1532
|
+
# inert no-items-reserved state. A held-out gate we cannot evaluate must fail
|
|
1533
|
+
# CLOSED (BLOCK), never emit NONE (which the caller treats as nothing-to-check).
|
|
1534
|
+
# The caller reads verdict pass fail, so emit BLOCK as the first field with a
|
|
1535
|
+
# synthetic 1 fail. A hidden failing reserved item could ship otherwise.
|
|
1536
|
+
print('BLOCK 0 1')
|
|
1430
1537
|
sys.exit(0)
|
|
1431
1538
|
|
|
1432
1539
|
# No held-out items reserved (e.g. N<4): gate is inert. Emit NONE so the caller
|
|
@@ -1476,7 +1583,7 @@ if matched == 0:
|
|
|
1476
1583
|
|
|
1477
1584
|
verdict = 'BLOCK' if failed > 0 else 'PASS'
|
|
1478
1585
|
print('%s %d %d' % (verdict, passed, failed))
|
|
1479
|
-
" 2>/dev/null || echo "
|
|
1586
|
+
" 2>/dev/null || echo "BLOCK 0 1")
|
|
1480
1587
|
|
|
1481
1588
|
local verdict pass_count fail_count
|
|
1482
1589
|
read -r verdict pass_count fail_count <<< "$gate_result"
|
|
@@ -1705,6 +1812,12 @@ council_evidence_gate() {
|
|
|
1705
1812
|
# Knob first: opt-out is exact-as-today, before any file read or write.
|
|
1706
1813
|
[ "${LOKI_EVIDENCE_GATE:-1}" = "0" ] && return 0
|
|
1707
1814
|
|
|
1815
|
+
# Every Python helper in this trust boundary inherits a sanitized import
|
|
1816
|
+
# environment. A project-controlled sitecustomize.py must never execute
|
|
1817
|
+
# while the engine decides whether completion evidence is credible.
|
|
1818
|
+
local PYTHONPATH="" PYTHONNOUSERSITE=1
|
|
1819
|
+
export PYTHONPATH PYTHONNOUSERSITE
|
|
1820
|
+
|
|
1708
1821
|
# The gate may run even when the completion council is disabled
|
|
1709
1822
|
# (LOKI_COUNCIL_ENABLED=false leaves COUNCIL_STATE_DIR unset by council_init),
|
|
1710
1823
|
# because it now also guards the default completion-promise route. Default
|
|
@@ -1984,6 +2097,14 @@ INCONCLUSIVE_EOF
|
|
|
1984
2097
|
local _diff_ok _tests_ok
|
|
1985
2098
|
if [ "$diff_fails" = "true" ]; then _diff_ok="false"; else _diff_ok="true"; fi
|
|
1986
2099
|
if [ "$test_fails" = "true" ]; then _tests_ok="false"; else _tests_ok="true"; fi
|
|
2100
|
+
# Proof-of-Function axes (nomock/persistence/auth) are declared later in
|
|
2101
|
+
# the gate; default to "true"/"" when this helper runs before they are
|
|
2102
|
+
# set (it never does in practice, but keep the write robust).
|
|
2103
|
+
local _nomock_ok _persist_ok _auth_ok _authz_ok
|
|
2104
|
+
if [ "${nomock_fails:-false}" = "true" ]; then _nomock_ok="false"; else _nomock_ok="true"; fi
|
|
2105
|
+
if [ "${persist_fails:-false}" = "true" ]; then _persist_ok="false"; else _persist_ok="true"; fi
|
|
2106
|
+
if [ "${auth_fails:-false}" = "true" ]; then _auth_ok="false"; else _auth_ok="true"; fi
|
|
2107
|
+
if [ "${authz_fails:-false}" = "true" ]; then _authz_ok="false"; else _authz_ok="true"; fi
|
|
1987
2108
|
cat > "$_det_tmp" << DETAILS_EOF
|
|
1988
2109
|
{
|
|
1989
2110
|
"recorded_at": "$_det_ts",
|
|
@@ -2002,14 +2123,456 @@ INCONCLUSIVE_EOF
|
|
|
2002
2123
|
"pass": $test_pass,
|
|
2003
2124
|
"inconclusive": $test_inconclusive,
|
|
2004
2125
|
"inconclusive_reason": "$test_inconclusive_reason"
|
|
2126
|
+
},
|
|
2127
|
+
"nomock": {
|
|
2128
|
+
"ok": $_nomock_ok,
|
|
2129
|
+
"inconclusive": ${nomock_inconclusive:-false},
|
|
2130
|
+
"reason": "${nomock_inconclusive_reason:-}"
|
|
2131
|
+
},
|
|
2132
|
+
"persistence": {
|
|
2133
|
+
"ok": $_persist_ok,
|
|
2134
|
+
"inconclusive": ${persist_inconclusive:-false},
|
|
2135
|
+
"reason": "${persist_inconclusive_reason:-}"
|
|
2136
|
+
},
|
|
2137
|
+
"auth": {
|
|
2138
|
+
"ok": $_auth_ok,
|
|
2139
|
+
"inconclusive": ${auth_inconclusive:-false},
|
|
2140
|
+
"reason": "${auth_inconclusive_reason:-}"
|
|
2141
|
+
},
|
|
2142
|
+
"authorization": {
|
|
2143
|
+
"ok": $_authz_ok,
|
|
2144
|
+
"inconclusive": ${authz_inconclusive:-false},
|
|
2145
|
+
"reason": "${authz_inconclusive_reason:-}"
|
|
2005
2146
|
}
|
|
2006
2147
|
}
|
|
2007
2148
|
DETAILS_EOF
|
|
2008
2149
|
mv "$_det_tmp" "$_det_file" 2>/dev/null || rm -f "$_det_tmp" 2>/dev/null || true
|
|
2009
2150
|
}
|
|
2010
2151
|
|
|
2011
|
-
# ---
|
|
2012
|
-
|
|
2152
|
+
# --- Evidence check (c): RUNTIME BOOT -- does the built app actually run? ---
|
|
2153
|
+
# RUN-25 iter 20 (Wave D #1): the single most load-bearing claim of a
|
|
2154
|
+
# spec-to-product builder is "the app runs", and until now the evidence gate
|
|
2155
|
+
# NEVER checked it -- app_runner_health_check already writes
|
|
2156
|
+
# .loki/app-runner/health.json but nothing read it here. Mirror the test axis:
|
|
2157
|
+
# a SERVEABLE app confirmed unhealthy -> boot_fails (BLOCK); no serveable
|
|
2158
|
+
# runner / probe never attempted (a CLI tool or library has no server to boot)
|
|
2159
|
+
# -> boot_inconclusive, pass-through (must NOT deadlock a non-web project).
|
|
2160
|
+
# Opt out with LOKI_EVIDENCE_BOOT_GATE=0.
|
|
2161
|
+
local boot_fails="false"
|
|
2162
|
+
local boot_inconclusive="false"
|
|
2163
|
+
local boot_inconclusive_reason=""
|
|
2164
|
+
local _health_file=".loki/app-runner/health.json"
|
|
2165
|
+
local _state_file=".loki/app-runner/state.json"
|
|
2166
|
+
if [ "${LOKI_EVIDENCE_BOOT_GATE:-1}" = "0" ]; then
|
|
2167
|
+
boot_inconclusive="true"
|
|
2168
|
+
boot_inconclusive_reason="boot_gate_disabled"
|
|
2169
|
+
elif [ ! -f "$_health_file" ] && [ ! -f "$_state_file" ]; then
|
|
2170
|
+
# No app-runner artifacts at all -> no serveable app was ever launched
|
|
2171
|
+
# (CLI/library project, or the runner never ran). Inconclusive, not a block.
|
|
2172
|
+
boot_inconclusive="true"
|
|
2173
|
+
boot_inconclusive_reason="no_app_runner"
|
|
2174
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
2175
|
+
local _boot_status
|
|
2176
|
+
_boot_status=$(_HEALTH="$_health_file" _STATE="$_state_file" python3 <<'PYEOF' 2>/dev/null || echo "INCONCLUSIVE:parse_error"
|
|
2177
|
+
import json, os
|
|
2178
|
+
health_file = os.environ.get('_HEALTH', '')
|
|
2179
|
+
state_file = os.environ.get('_STATE', '')
|
|
2180
|
+
|
|
2181
|
+
def load(p):
|
|
2182
|
+
try:
|
|
2183
|
+
with open(p) as f:
|
|
2184
|
+
return json.load(f)
|
|
2185
|
+
except Exception:
|
|
2186
|
+
return None
|
|
2187
|
+
|
|
2188
|
+
state = load(state_file) or {}
|
|
2189
|
+
health = load(health_file) or {}
|
|
2190
|
+
# A "serveable" app is one the runner classified with a real service (status not
|
|
2191
|
+
# 'none'/'unknown' and a primary_service or url present). Only then is an
|
|
2192
|
+
# unhealthy probe a genuine BLOCK; otherwise there is nothing to boot.
|
|
2193
|
+
status = str(state.get('status', 'unknown')).lower()
|
|
2194
|
+
svc = state.get('primary_service') or ''
|
|
2195
|
+
url = state.get('url') or ''
|
|
2196
|
+
serveable = status not in ('none', 'unknown', '') and (bool(svc) or bool(url))
|
|
2197
|
+
# health 'ok' is the last health-check verdict (True healthy / False failing).
|
|
2198
|
+
ok = health.get('ok', None)
|
|
2199
|
+
if not serveable:
|
|
2200
|
+
print('INCONCLUSIVE:not_serveable')
|
|
2201
|
+
elif ok is True:
|
|
2202
|
+
print('PASS')
|
|
2203
|
+
elif ok is False:
|
|
2204
|
+
print('FAIL')
|
|
2205
|
+
else:
|
|
2206
|
+
# serveable but no health verdict recorded -> probe never completed.
|
|
2207
|
+
print('INCONCLUSIVE:no_health_probe')
|
|
2208
|
+
PYEOF
|
|
2209
|
+
)
|
|
2210
|
+
case "$_boot_status" in
|
|
2211
|
+
FAIL)
|
|
2212
|
+
boot_fails="true" ;;
|
|
2213
|
+
PASS)
|
|
2214
|
+
: ;; # affirmative: the app boots and serves.
|
|
2215
|
+
INCONCLUSIVE:*)
|
|
2216
|
+
boot_inconclusive="true"
|
|
2217
|
+
boot_inconclusive_reason="${_boot_status#INCONCLUSIVE:}" ;;
|
|
2218
|
+
*)
|
|
2219
|
+
boot_inconclusive="true"
|
|
2220
|
+
boot_inconclusive_reason="unknown" ;;
|
|
2221
|
+
esac
|
|
2222
|
+
else
|
|
2223
|
+
boot_inconclusive="true"
|
|
2224
|
+
boot_inconclusive_reason="no_python3"
|
|
2225
|
+
fi
|
|
2226
|
+
|
|
2227
|
+
# === Proof-of-Function (PoF) axes: NO-MOCK, PERSISTENCE, AUTH ==============
|
|
2228
|
+
# Three functionality-proving axes. Each is proven by an OBSERVED artifact
|
|
2229
|
+
# (static source scan for no-mock; a real browser drive + DB/API read-back
|
|
2230
|
+
# for persistence + auth), never a self-report. Each fails CLOSED on POSITIVE
|
|
2231
|
+
# disproof and passes through ONLY when genuinely inconclusive (non-web build,
|
|
2232
|
+
# browser unavailable, no create path, no auth signal). Web-app-only: the
|
|
2233
|
+
# dynamic axes reuse the SAME serveable gate as boot, so a CLI/API/library
|
|
2234
|
+
# build is NEVER blocked. Master knob LOKI_PROOF_GATE=0 disables all three;
|
|
2235
|
+
# per-axis LOKI_PROOF_NOMOCK / LOKI_PROOF_PERSIST / LOKI_PROOF_AUTH=0.
|
|
2236
|
+
local _proof_gate="${LOKI_PROOF_GATE:-1}"
|
|
2237
|
+
local nomock_fails="false" nomock_inconclusive="false" nomock_inconclusive_reason=""
|
|
2238
|
+
local persist_fails="false" persist_inconclusive="false" persist_inconclusive_reason=""
|
|
2239
|
+
local auth_fails="false" auth_inconclusive="false" auth_inconclusive_reason=""
|
|
2240
|
+
local authz_fails="false" authz_inconclusive="false" authz_inconclusive_reason=""
|
|
2241
|
+
local _proof_file=".loki/verification/functional-proof.json"
|
|
2242
|
+
|
|
2243
|
+
# --- Evidence check (e): NO-MOCK (static, cheap, runs first) ---
|
|
2244
|
+
# A list/table/dashboard whose backing data traces to an inline mock array /
|
|
2245
|
+
# faker / placeholder literal instead of a real fetch/query is a fake app.
|
|
2246
|
+
# This is the ONLY axis that can run without the app up. Consumer of
|
|
2247
|
+
# verify.sh writes nomock-scan.json as an audit artifact. Completion never
|
|
2248
|
+
# trusts that mutable cache, even when its base SHA matches: the run base is
|
|
2249
|
+
# constant across iterations while source bytes can keep changing. Re-run
|
|
2250
|
+
# the engine-owned scanner over the current changed-file union every time.
|
|
2251
|
+
if [ "$_proof_gate" = "0" ] || [ "${LOKI_PROOF_NOMOCK:-1}" = "0" ]; then
|
|
2252
|
+
nomock_inconclusive="true"
|
|
2253
|
+
nomock_inconclusive_reason="nomock_gate_disabled"
|
|
2254
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
2255
|
+
local _nm_files
|
|
2256
|
+
_nm_files=$(
|
|
2257
|
+
{
|
|
2258
|
+
[ -n "$base_sha" ] && git diff --name-only "$base_sha" 2>/dev/null
|
|
2259
|
+
git diff --name-only 2>/dev/null
|
|
2260
|
+
git diff --name-only --cached 2>/dev/null
|
|
2261
|
+
git ls-files --others --exclude-standard 2>/dev/null
|
|
2262
|
+
} | grep -v '^$' | grep -vE '^\.loki/' | sort -u
|
|
2263
|
+
)
|
|
2264
|
+
local _nm_status _nm_scanner _nm_cc_dir
|
|
2265
|
+
_nm_cc_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-}")" 2>/dev/null && pwd || true)"
|
|
2266
|
+
_nm_scanner="$_nm_cc_dir/lib/no_mock_scan.py"
|
|
2267
|
+
if [ -z "$_nm_cc_dir" ] || [ ! -f "$_nm_scanner" ]; then
|
|
2268
|
+
_nm_status="INCONCLUSIVE:scanner_unavailable"
|
|
2269
|
+
else
|
|
2270
|
+
_nm_status=$(
|
|
2271
|
+
_NM_FILES="$_nm_files" \
|
|
2272
|
+
_NM_TREE="." \
|
|
2273
|
+
python3 -I "$_nm_scanner" 2>/dev/null \
|
|
2274
|
+
|| echo "INCONCLUSIVE:detector_error"
|
|
2275
|
+
)
|
|
2276
|
+
fi
|
|
2277
|
+
case "$_nm_status" in
|
|
2278
|
+
FAIL:*) nomock_fails="true" ;;
|
|
2279
|
+
PASS:*) : ;;
|
|
2280
|
+
SKIP:*) nomock_inconclusive="true"; nomock_inconclusive_reason="no_ui_files" ;;
|
|
2281
|
+
INCONCLUSIVE:*) nomock_inconclusive="true"; nomock_inconclusive_reason="${_nm_status#INCONCLUSIVE:}" ;;
|
|
2282
|
+
*) nomock_inconclusive="true"; nomock_inconclusive_reason="unknown" ;;
|
|
2283
|
+
esac
|
|
2284
|
+
else
|
|
2285
|
+
nomock_inconclusive="true"
|
|
2286
|
+
nomock_inconclusive_reason="no_python3"
|
|
2287
|
+
fi
|
|
2288
|
+
|
|
2289
|
+
# --- Evidence checks (f) PERSISTENCE + (g) AUTH (dynamic, driven) ---
|
|
2290
|
+
# Both are read from .loki/verification/functional-proof.json, produced by
|
|
2291
|
+
# playwright_prove_functional (run.sh interval-gated, serveable-only). The
|
|
2292
|
+
# gate reads ONLY this artifact -- never a screenshot, never an LLM opinion.
|
|
2293
|
+
# Tri-state per property: attempted / proven / reason. Fail-closed:
|
|
2294
|
+
# PERSISTENCE: proven==true -> PASS. attempted && !proven -> BLOCK (a
|
|
2295
|
+
# create path was exercised and the record did NOT survive reload; a
|
|
2296
|
+
# submit that errors is the #1 churn bug and must not green-wash).
|
|
2297
|
+
# attempted==false with reason no_create_path/not_serveable/driver_
|
|
2298
|
+
# unavailable -> logged pass-through. Missing/unparseable/STALE (a stamp
|
|
2299
|
+
# whose iteration != the current one) file -> treated as NOT PRESENT ->
|
|
2300
|
+
# inconclusive pass-through, NEVER a block. Only a FRESH attempted &&
|
|
2301
|
+
# !proven disproof blocks: absence and staleness must never false-block a
|
|
2302
|
+
# legitimate build (the fail-closed rule is disproven-blocks, not
|
|
2303
|
+
# absent-blocks).
|
|
2304
|
+
# AUTH: proven==true (observed 401/403/redirect) -> PASS. attempted &&
|
|
2305
|
+
# !proven (protected route served 200 logged-out, or only a login screen
|
|
2306
|
+
# rendered) -> BLOCK. no_auth/not_serveable -> pass-through. Auth
|
|
2307
|
+
# detected but unprovable under LOKI_PROOF_AUTH_STRICT=1 -> BLOCK
|
|
2308
|
+
# (security property never assumed).
|
|
2309
|
+
# Web-app-only: only evaluate the dynamic axes when the app is SERVEABLE
|
|
2310
|
+
# (same signal the boot axis uses). A non-web build never launches a browser,
|
|
2311
|
+
# writes no proof file, and passes through by construction.
|
|
2312
|
+
local _proof_persist_disabled="false" _proof_auth_disabled="false" _proof_authz_disabled="false"
|
|
2313
|
+
[ "$_proof_gate" = "0" ] || [ "${LOKI_PROOF_PERSIST:-1}" = "0" ] && _proof_persist_disabled="true"
|
|
2314
|
+
[ "$_proof_gate" = "0" ] || [ "${LOKI_PROOF_AUTH:-1}" = "0" ] && _proof_auth_disabled="true"
|
|
2315
|
+
[ "$_proof_gate" = "0" ] || [ "${LOKI_PROOF_AUTHZ:-1}" = "0" ] && _proof_authz_disabled="true"
|
|
2316
|
+
|
|
2317
|
+
# Serveable? Reuse the boot axis's own classification: boot_fails or an
|
|
2318
|
+
# affirmative boot pass both imply a serveable app was launched. Only a
|
|
2319
|
+
# not_serveable / no_app_runner boot-inconclusive means non-web.
|
|
2320
|
+
local _proof_serveable="false"
|
|
2321
|
+
if [ "$boot_fails" = "true" ]; then
|
|
2322
|
+
_proof_serveable="true"
|
|
2323
|
+
elif [ "$boot_inconclusive" != "true" ]; then
|
|
2324
|
+
_proof_serveable="true"
|
|
2325
|
+
fi
|
|
2326
|
+
|
|
2327
|
+
if [ "$_proof_persist_disabled" = "true" ]; then
|
|
2328
|
+
persist_inconclusive="true"; persist_inconclusive_reason="persist_gate_disabled"
|
|
2329
|
+
elif [ "$_proof_serveable" != "true" ]; then
|
|
2330
|
+
persist_inconclusive="true"; persist_inconclusive_reason="not_serveable"
|
|
2331
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
2332
|
+
local _p_status
|
|
2333
|
+
_p_status=$(_PF="$_proof_file" _ITER="${ITERATION_COUNT:-0}" python3 <<'PYEOF' 2>/dev/null || echo "MISSING"
|
|
2334
|
+
import json, os
|
|
2335
|
+
p = os.environ['_PF']
|
|
2336
|
+
if not os.path.isfile(p):
|
|
2337
|
+
print("MISSING"); raise SystemExit
|
|
2338
|
+
try:
|
|
2339
|
+
d = json.load(open(p))
|
|
2340
|
+
except Exception:
|
|
2341
|
+
print("UNPARSEABLE"); raise SystemExit
|
|
2342
|
+
# BUG 1b: freshness. A proof stamped for a different iteration is a STALE artifact
|
|
2343
|
+
# (a timeout/hang left a prior iteration's file, or the producer did not run this
|
|
2344
|
+
# iteration). Treat it as NOT PRESENT -- never let a stale proven:true pass.
|
|
2345
|
+
stamp = d.get("stamp") or {}
|
|
2346
|
+
try:
|
|
2347
|
+
cur = int(os.environ.get("_ITER", "0"))
|
|
2348
|
+
except Exception:
|
|
2349
|
+
cur = 0
|
|
2350
|
+
if "iteration" in stamp:
|
|
2351
|
+
try:
|
|
2352
|
+
if int(stamp.get("iteration")) != cur:
|
|
2353
|
+
print("MISSING"); raise SystemExit
|
|
2354
|
+
except (TypeError, ValueError):
|
|
2355
|
+
print("MISSING"); raise SystemExit
|
|
2356
|
+
per = d.get("persistence") or {}
|
|
2357
|
+
attempted = per.get("attempted")
|
|
2358
|
+
proven = per.get("proven")
|
|
2359
|
+
reason = str(per.get("reason", "") or "")
|
|
2360
|
+
if attempted is True and proven is True:
|
|
2361
|
+
print("PASS")
|
|
2362
|
+
elif attempted is False and reason in ("no_create_path", "not_serveable", "driver_unavailable"):
|
|
2363
|
+
print("INCONCLUSIVE:" + reason)
|
|
2364
|
+
elif attempted is True and proven is False:
|
|
2365
|
+
# A create path WAS exercised and the record did not survive -> disproven.
|
|
2366
|
+
print("FAIL")
|
|
2367
|
+
else:
|
|
2368
|
+
# attempted True/proven unknown, or malformed record on a serveable app.
|
|
2369
|
+
# We cannot prove persistence, but "not proven" is not "disproven": pass
|
|
2370
|
+
# through inconclusive rather than block a legitimate build (BUG 2).
|
|
2371
|
+
print("INCONCLUSIVE:proof_indeterminate")
|
|
2372
|
+
PYEOF
|
|
2373
|
+
)
|
|
2374
|
+
case "$_p_status" in
|
|
2375
|
+
PASS) : ;;
|
|
2376
|
+
INCONCLUSIVE:*) persist_inconclusive="true"; persist_inconclusive_reason="${_p_status#INCONCLUSIVE:}" ;;
|
|
2377
|
+
# BUG 2: absence / staleness / unparseable is NOT a disproof. A proof
|
|
2378
|
+
# that was never freshly attempted this iteration (interval not fired,
|
|
2379
|
+
# driver unavailable, timeout left nothing) is INCONCLUSIVE pass-through,
|
|
2380
|
+
# never a hard block. ONLY a FRESHLY-attempted proven:false blocks.
|
|
2381
|
+
MISSING) persist_inconclusive="true"; persist_inconclusive_reason="proof_not_attempted" ;;
|
|
2382
|
+
UNPARSEABLE) persist_inconclusive="true"; persist_inconclusive_reason="proof_file_unparseable" ;;
|
|
2383
|
+
FAIL) persist_fails="true" ;;
|
|
2384
|
+
*) persist_inconclusive="true"; persist_inconclusive_reason="proof_indeterminate" ;;
|
|
2385
|
+
esac
|
|
2386
|
+
else
|
|
2387
|
+
persist_inconclusive="true"; persist_inconclusive_reason="no_python3"
|
|
2388
|
+
fi
|
|
2389
|
+
|
|
2390
|
+
if [ "$_proof_auth_disabled" = "true" ]; then
|
|
2391
|
+
auth_inconclusive="true"; auth_inconclusive_reason="auth_gate_disabled"
|
|
2392
|
+
elif [ "$_proof_serveable" != "true" ]; then
|
|
2393
|
+
auth_inconclusive="true"; auth_inconclusive_reason="not_serveable"
|
|
2394
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
2395
|
+
local _a_status
|
|
2396
|
+
_a_status=$(_PF="$_proof_file" _STRICT="${LOKI_PROOF_AUTH_STRICT:-1}" _ITER="${ITERATION_COUNT:-0}" python3 <<'PYEOF' 2>/dev/null || echo "MISSING"
|
|
2397
|
+
import json, os
|
|
2398
|
+
p = os.environ['_PF']
|
|
2399
|
+
strict = os.environ.get('_STRICT', '1') != '0'
|
|
2400
|
+
if not os.path.isfile(p):
|
|
2401
|
+
# No proof file on a serveable app: cannot prove auth. If auth was never
|
|
2402
|
+
# detected we would expect no_auth; a missing file is ambiguous. Under
|
|
2403
|
+
# STRICT this is not a pass, but we cannot know auth was detected, so treat
|
|
2404
|
+
# missing as inconclusive (the boot/persist axes already catch a dead app).
|
|
2405
|
+
print("INCONCLUSIVE:proof_file_missing"); raise SystemExit
|
|
2406
|
+
try:
|
|
2407
|
+
d = json.load(open(p))
|
|
2408
|
+
except Exception:
|
|
2409
|
+
print("INCONCLUSIVE:proof_file_unparseable"); raise SystemExit
|
|
2410
|
+
# BUG 1b: freshness. A proof stamped for a different iteration is stale -> treat
|
|
2411
|
+
# as not present (inconclusive), never let a stale auth proven:true pass.
|
|
2412
|
+
stamp = d.get("stamp") or {}
|
|
2413
|
+
try:
|
|
2414
|
+
cur = int(os.environ.get("_ITER", "0"))
|
|
2415
|
+
except Exception:
|
|
2416
|
+
cur = 0
|
|
2417
|
+
if "iteration" in stamp:
|
|
2418
|
+
try:
|
|
2419
|
+
if int(stamp.get("iteration")) != cur:
|
|
2420
|
+
print("INCONCLUSIVE:proof_stale"); raise SystemExit
|
|
2421
|
+
except (TypeError, ValueError):
|
|
2422
|
+
print("INCONCLUSIVE:proof_stale"); raise SystemExit
|
|
2423
|
+
a = d.get("auth") or {}
|
|
2424
|
+
attempted = a.get("attempted")
|
|
2425
|
+
proven = a.get("proven")
|
|
2426
|
+
reason = str(a.get("reason", "") or "")
|
|
2427
|
+
if attempted is True and proven is True:
|
|
2428
|
+
print("PASS")
|
|
2429
|
+
elif attempted is True and proven is False:
|
|
2430
|
+
# observed a protected route served logged-out, or only a login screen
|
|
2431
|
+
# rendered -> auth NOT enforced -> BLOCK.
|
|
2432
|
+
print("FAIL")
|
|
2433
|
+
elif attempted is False and reason in ("no_auth", "not_serveable", "driver_unavailable"):
|
|
2434
|
+
print("INCONCLUSIVE:" + reason)
|
|
2435
|
+
elif reason in ("auth_detected_untestable", "auth_detected_timeout"):
|
|
2436
|
+
# auth signal detected but enforcement could not be proven. Security
|
|
2437
|
+
# property: STRICT fails closed to BLOCK; otherwise inconclusive-but-logged.
|
|
2438
|
+
print("FAIL" if strict else "INCONCLUSIVE:" + reason)
|
|
2439
|
+
else:
|
|
2440
|
+
print("INCONCLUSIVE:" + (reason or "unknown"))
|
|
2441
|
+
PYEOF
|
|
2442
|
+
)
|
|
2443
|
+
case "$_a_status" in
|
|
2444
|
+
PASS) : ;;
|
|
2445
|
+
INCONCLUSIVE:*) auth_inconclusive="true"; auth_inconclusive_reason="${_a_status#INCONCLUSIVE:}" ;;
|
|
2446
|
+
FAIL) auth_fails="true" ;;
|
|
2447
|
+
*) auth_inconclusive="true"; auth_inconclusive_reason="unknown" ;;
|
|
2448
|
+
esac
|
|
2449
|
+
else
|
|
2450
|
+
auth_inconclusive="true"; auth_inconclusive_reason="no_python3"
|
|
2451
|
+
fi
|
|
2452
|
+
|
|
2453
|
+
# --- Evidence check (h): AUTHORIZATION / TENANT ISOLATION (dynamic, driven) ---
|
|
2454
|
+
# The Lovable-breach class: two LOGGED-IN users where user A can read user B's
|
|
2455
|
+
# owned rows (ownership/RLS check inverted or missing). "auth" proves anon->401
|
|
2456
|
+
# (authentication); "authorization" proves user-A-data-denied-to-user-B (tenant
|
|
2457
|
+
# isolation). Read from the SAME functional-proof.json, key "authorization".
|
|
2458
|
+
#
|
|
2459
|
+
# CRITICAL ASYMMETRY vs persistence: a security property must NEVER be declared
|
|
2460
|
+
# VIOLATED by our own tooling's inability to observe. So attempted && !proven
|
|
2461
|
+
# blocks ONLY when the reason is the explicit positive-leak prefix
|
|
2462
|
+
# 'user_b_read_user_a_' (B's real session actually received A's sentinel on a
|
|
2463
|
+
# path). Every other outcome -- no multi-user auth, no owned data, could not
|
|
2464
|
+
# create a second user, no read path for B, a drive error, missing/stale --
|
|
2465
|
+
# is INCONCLUSIVE pass-through, never a block. Absence != insecure. This is the
|
|
2466
|
+
# discipline that prevents the false-block: ONLY a fresh positive disproof of
|
|
2467
|
+
# isolation blocks.
|
|
2468
|
+
if [ "$_proof_authz_disabled" = "true" ]; then
|
|
2469
|
+
authz_inconclusive="true"; authz_inconclusive_reason="authz_gate_disabled"
|
|
2470
|
+
elif [ "$_proof_serveable" != "true" ]; then
|
|
2471
|
+
authz_inconclusive="true"; authz_inconclusive_reason="not_serveable"
|
|
2472
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
2473
|
+
local _authz_status
|
|
2474
|
+
_authz_status=$(_PF="$_proof_file" _ITER="${ITERATION_COUNT:-0}" python3 <<'PYEOF' 2>/dev/null || echo "MISSING"
|
|
2475
|
+
import json, os
|
|
2476
|
+
p = os.environ['_PF']
|
|
2477
|
+
if not os.path.isfile(p):
|
|
2478
|
+
print("MISSING"); raise SystemExit
|
|
2479
|
+
try:
|
|
2480
|
+
d = json.load(open(p))
|
|
2481
|
+
except Exception:
|
|
2482
|
+
print("UNPARSEABLE"); raise SystemExit
|
|
2483
|
+
# Freshness: a proof stamped for a different iteration is stale -> treat as NOT
|
|
2484
|
+
# PRESENT. A stale proven:false leak reason must never linger-block; a stale
|
|
2485
|
+
# proven:true must never be a stale green.
|
|
2486
|
+
stamp = d.get("stamp") or {}
|
|
2487
|
+
try:
|
|
2488
|
+
cur = int(os.environ.get("_ITER", "0"))
|
|
2489
|
+
except Exception:
|
|
2490
|
+
cur = 0
|
|
2491
|
+
if "iteration" in stamp:
|
|
2492
|
+
try:
|
|
2493
|
+
if int(stamp.get("iteration")) != cur:
|
|
2494
|
+
print("STALE"); raise SystemExit
|
|
2495
|
+
except (TypeError, ValueError):
|
|
2496
|
+
print("STALE"); raise SystemExit
|
|
2497
|
+
az = d.get("authorization") or {}
|
|
2498
|
+
attempted = az.get("attempted")
|
|
2499
|
+
proven = az.get("proven")
|
|
2500
|
+
reason = str(az.get("reason", "") or "")
|
|
2501
|
+
if attempted is True and proven is True:
|
|
2502
|
+
# Isolation observed to HOLD on every path tried.
|
|
2503
|
+
print("PASS")
|
|
2504
|
+
elif attempted is True and proven is False and reason.startswith("user_b_read_user_a_"):
|
|
2505
|
+
# The ONLY blocking outcome: B's real session received A's sentinel on a path.
|
|
2506
|
+
print("FAIL")
|
|
2507
|
+
else:
|
|
2508
|
+
# EVERY other case is inconclusive pass-through (never a block):
|
|
2509
|
+
# attempted:false -> no_multiuser_auth / no_owned_data /
|
|
2510
|
+
# could_not_create_second_user / not_serveable / driver_unavailable
|
|
2511
|
+
# attempted:true/proven:false -> no_read_path_for_b / authz_drive_error:*
|
|
2512
|
+
# A generic "could not read as B" or a driver error is OUR failure to observe,
|
|
2513
|
+
# not a proven leak. Never block on it.
|
|
2514
|
+
print("INCONCLUSIVE:" + (reason or "unknown"))
|
|
2515
|
+
PYEOF
|
|
2516
|
+
)
|
|
2517
|
+
case "$_authz_status" in
|
|
2518
|
+
PASS) : ;;
|
|
2519
|
+
INCONCLUSIVE:*) authz_inconclusive="true"; authz_inconclusive_reason="${_authz_status#INCONCLUSIVE:}" ;;
|
|
2520
|
+
MISSING) authz_inconclusive="true"; authz_inconclusive_reason="proof_not_attempted" ;;
|
|
2521
|
+
UNPARSEABLE) authz_inconclusive="true"; authz_inconclusive_reason="proof_file_unparseable" ;;
|
|
2522
|
+
STALE) authz_inconclusive="true"; authz_inconclusive_reason="proof_stale" ;;
|
|
2523
|
+
FAIL) authz_fails="true" ;;
|
|
2524
|
+
*) authz_inconclusive="true"; authz_inconclusive_reason="unknown" ;;
|
|
2525
|
+
esac
|
|
2526
|
+
else
|
|
2527
|
+
authz_inconclusive="true"; authz_inconclusive_reason="no_python3"
|
|
2528
|
+
fi
|
|
2529
|
+
|
|
2530
|
+
# --- Evidence check (d): SECRET LEAK -- did the build ship a credential? ---
|
|
2531
|
+
# RUN-25 iter 21 (Wave D #2): the secret matcher ran ONLY on the auto-commit
|
|
2532
|
+
# path, so a completion via the promise / dirty-tree route could ship a leaked
|
|
2533
|
+
# credential in a "done" deliverable -- a hole in the exact trust moat. Run the
|
|
2534
|
+
# SAME matcher (shared lib) over the changed files. A high-confidence hit ->
|
|
2535
|
+
# secret_fails (BLOCK); no matcher available or no scannable files ->
|
|
2536
|
+
# inconclusive pass-through. Opt out LOKI_EVIDENCE_SECRET_GATE=0.
|
|
2537
|
+
local secret_fails="false"
|
|
2538
|
+
if [ "${LOKI_EVIDENCE_SECRET_GATE:-1}" != "0" ] \
|
|
2539
|
+
&& type _commit_scan_secret_file >/dev/null 2>&1; then
|
|
2540
|
+
# Changed files vs run-start SHA (committed) + working-tree (staged/unstaged/
|
|
2541
|
+
# untracked), excluding .loki/ (Loki's own state is never project work).
|
|
2542
|
+
local _sec_files
|
|
2543
|
+
_sec_files=$(
|
|
2544
|
+
{
|
|
2545
|
+
[ -n "$base_sha" ] && git diff --name-only "$base_sha" 2>/dev/null
|
|
2546
|
+
git diff --name-only 2>/dev/null
|
|
2547
|
+
git diff --name-only --cached 2>/dev/null
|
|
2548
|
+
git ls-files --others --exclude-standard 2>/dev/null
|
|
2549
|
+
} | grep -v '^$' | grep -vE '^\.loki/' | sort -u
|
|
2550
|
+
)
|
|
2551
|
+
local _sf
|
|
2552
|
+
while IFS= read -r _sf; do
|
|
2553
|
+
[ -n "$_sf" ] || continue
|
|
2554
|
+
# CONTENT ONLY. The filename heuristic (_commit_path_looks_secret) is
|
|
2555
|
+
# correct for the AUTO-COMMIT path -- refusing to stage a file called
|
|
2556
|
+
# `.env` is cheap and right. It is WRONG as a completion gate, because
|
|
2557
|
+
# it blocks on names alone with no look at what is inside:
|
|
2558
|
+
# `src/auth/token.ts` is a routine file in any auth-enabled app, which
|
|
2559
|
+
# is exactly what this engine is asked to build, and v7.129.5 shipped
|
|
2560
|
+
# such builds without complaint (v7's council had no secret logic at
|
|
2561
|
+
# all). Blocking there tells a user their finished, correct app is a
|
|
2562
|
+
# security leak because of how they named a file.
|
|
2563
|
+
# A real leak is still caught: _commit_scan_secret_file reads the
|
|
2564
|
+
# bytes, and it carries the example/dummy/placeholder deny-filter that
|
|
2565
|
+
# keeps fixtures and .env.example from tripping it.
|
|
2566
|
+
if [ -f "$_sf" ] && _commit_scan_secret_file "$_sf"; then
|
|
2567
|
+
secret_fails="true"
|
|
2568
|
+
log_warn "[Council] Evidence gate: secret-leak match in changed file '${_sf}'"
|
|
2569
|
+
break
|
|
2570
|
+
fi
|
|
2571
|
+
done <<< "$_sec_files"
|
|
2572
|
+
fi
|
|
2573
|
+
|
|
2574
|
+
# --- Block decision: block iff any axis FAILS (diff/test/boot/secret/nomock/persist/auth/authz) ---
|
|
2575
|
+
if [ "$diff_fails" != "true" ] && [ "$test_fails" != "true" ] && [ "$boot_fails" != "true" ] && [ "$secret_fails" != "true" ] && [ "$nomock_fails" != "true" ] && [ "$persist_fails" != "true" ] && [ "$auth_fails" != "true" ] && [ "$authz_fails" != "true" ]; then
|
|
2013
2576
|
# Gate passes: remove any stale block report.
|
|
2014
2577
|
if [ -f "$COUNCIL_STATE_DIR/evidence-block.json" ]; then
|
|
2015
2578
|
rm -f "$COUNCIL_STATE_DIR/evidence-block.json"
|
|
@@ -2022,6 +2585,26 @@ DETAILS_EOF
|
|
|
2022
2585
|
if [ "$test_inconclusive" = "true" ]; then
|
|
2023
2586
|
log_warn "[Council] Evidence gate: completion not backed by test evidence (${test_inconclusive_reason}). Pass-through; set LOKI_EVIDENCE_NO_TESTS_AFFIRMATIVE=1 to treat no-tests as affirmative."
|
|
2024
2587
|
fi
|
|
2588
|
+
# Same honesty for the runtime-boot axis: a pass that could not confirm the
|
|
2589
|
+
# app boots (CLI/library, probe never ran, gate disabled) says so out loud
|
|
2590
|
+
# rather than implying the app was verified to run.
|
|
2591
|
+
if [ "$boot_inconclusive" = "true" ]; then
|
|
2592
|
+
log_warn "[Council] Evidence gate: app-boot not confirmed (${boot_inconclusive_reason}). Pass-through; set LOKI_EVIDENCE_BOOT_GATE=0 to silence, or run the app so its health probe records a verdict."
|
|
2593
|
+
fi
|
|
2594
|
+
# Proof-of-Function honesty: a pass-through that could not PROVE a
|
|
2595
|
+
# functionality property says so out loud (never imply it was proven).
|
|
2596
|
+
if [ "$nomock_inconclusive" = "true" ] && [ "$nomock_inconclusive_reason" != "nomock_gate_disabled" ]; then
|
|
2597
|
+
log_warn "[Council] Evidence gate: no-mock not confirmed (${nomock_inconclusive_reason}). Pass-through; set LOKI_PROOF_NOMOCK=0 to silence."
|
|
2598
|
+
fi
|
|
2599
|
+
if [ "$persist_inconclusive" = "true" ] && [ "$persist_inconclusive_reason" != "persist_gate_disabled" ]; then
|
|
2600
|
+
log_warn "[Council] Evidence gate: persistence not proven (${persist_inconclusive_reason}). Pass-through; set LOKI_PROOF_PERSIST=0 to silence, or expose a create form the driver can exercise (LOKI_PROOF_CREATE_SELECTOR)."
|
|
2601
|
+
fi
|
|
2602
|
+
if [ "$auth_inconclusive" = "true" ] && [ "$auth_inconclusive_reason" != "auth_gate_disabled" ]; then
|
|
2603
|
+
log_warn "[Council] Evidence gate: auth enforcement not proven (${auth_inconclusive_reason}). Pass-through; set LOKI_PROOF_AUTH=0 to silence, or set LOKI_PROOF_PROTECTED_PATH so the driver can test a logged-out request."
|
|
2604
|
+
fi
|
|
2605
|
+
if [ "$authz_inconclusive" = "true" ] && [ "$authz_inconclusive_reason" != "authz_gate_disabled" ]; then
|
|
2606
|
+
log_warn "[Council] Evidence gate: tenant isolation not proven (${authz_inconclusive_reason}). Pass-through; set LOKI_PROOF_AUTHZ=0 to silence, or configure LOKI_PROOF_AUTHZ_* selectors so the driver can drive two distinct sessions."
|
|
2607
|
+
fi
|
|
2025
2608
|
_write_evidence_details "pass"
|
|
2026
2609
|
return 0
|
|
2027
2610
|
fi
|
|
@@ -2034,6 +2617,18 @@ DETAILS_EOF
|
|
|
2034
2617
|
reason="empty_diff"
|
|
2035
2618
|
elif [ "$test_fails" = "true" ]; then
|
|
2036
2619
|
reason="tests_red"
|
|
2620
|
+
elif [ "$boot_fails" = "true" ]; then
|
|
2621
|
+
reason="app_boot_failed"
|
|
2622
|
+
elif [ "$secret_fails" = "true" ]; then
|
|
2623
|
+
reason="secret_leak_in_changed_files"
|
|
2624
|
+
elif [ "$nomock_fails" = "true" ]; then
|
|
2625
|
+
reason="mock_backed_data"
|
|
2626
|
+
elif [ "$persist_fails" = "true" ]; then
|
|
2627
|
+
reason="persistence_unproven"
|
|
2628
|
+
elif [ "$auth_fails" = "true" ]; then
|
|
2629
|
+
reason="auth_not_enforced"
|
|
2630
|
+
elif [ "$authz_fails" = "true" ]; then
|
|
2631
|
+
reason="tenant_isolation_broken"
|
|
2037
2632
|
fi
|
|
2038
2633
|
|
|
2039
2634
|
local failures=""
|
|
@@ -2049,6 +2644,75 @@ DETAILS_EOF
|
|
|
2049
2644
|
fi
|
|
2050
2645
|
log_warn "[Council] Evidence gate BLOCKED: test runner '${test_runner}' was red"
|
|
2051
2646
|
fi
|
|
2647
|
+
if [ "$boot_fails" = "true" ]; then
|
|
2648
|
+
# Wave D #1: the built app was launched and its health probe reported
|
|
2649
|
+
# unhealthy -- a spec-to-product build cannot be "done" if its app does not
|
|
2650
|
+
# run. Pass-through for CLI/library projects (boot_inconclusive above).
|
|
2651
|
+
if [ -n "$failures" ]; then
|
|
2652
|
+
failures="${failures}|app runner started but the app is not healthy (does not serve)"
|
|
2653
|
+
else
|
|
2654
|
+
failures="app runner started but the app is not healthy (does not serve)"
|
|
2655
|
+
fi
|
|
2656
|
+
log_warn "[Council] Evidence gate BLOCKED: the built app is not running/serving (app-runner health FAIL)"
|
|
2657
|
+
fi
|
|
2658
|
+
if [ "$secret_fails" = "true" ]; then
|
|
2659
|
+
# Wave D #2: a credential-shaped secret is present in the changed files. A
|
|
2660
|
+
# trust builder cannot call a build "done" while it would leak a secret.
|
|
2661
|
+
if [ -n "$failures" ]; then
|
|
2662
|
+
failures="${failures}|a secret/credential was detected in the changed files"
|
|
2663
|
+
else
|
|
2664
|
+
failures="a secret/credential was detected in the changed files"
|
|
2665
|
+
fi
|
|
2666
|
+
log_warn "[Council] Evidence gate BLOCKED: a secret/credential was detected in the changed files"
|
|
2667
|
+
fi
|
|
2668
|
+
if [ "$nomock_fails" = "true" ]; then
|
|
2669
|
+
# PoF #1: a list, table, or dashboard resolves to an inline mock array,
|
|
2670
|
+
# faker value, or placeholder literal. Opt out: LOKI_PROOF_NOMOCK=0.
|
|
2671
|
+
if [ -n "$failures" ]; then
|
|
2672
|
+
failures="${failures}|a rendered operational collection resolves to inline, faker, or placeholder-backed data (set LOKI_PROOF_NOMOCK=0 to opt out)"
|
|
2673
|
+
else
|
|
2674
|
+
failures="a rendered operational collection resolves to inline, faker, or placeholder-backed data (set LOKI_PROOF_NOMOCK=0 to opt out)"
|
|
2675
|
+
fi
|
|
2676
|
+
log_warn "[Council] Evidence gate BLOCKED: a rendered operational collection resolves to inline, faker, or placeholder-backed data. Opt out: LOKI_PROOF_NOMOCK=0"
|
|
2677
|
+
fi
|
|
2678
|
+
if [ "$persist_fails" = "true" ]; then
|
|
2679
|
+
# PoF #2: a create path was FRESHLY driven this iteration and the record
|
|
2680
|
+
# did NOT survive a fresh-context read-back (or the submit errored).
|
|
2681
|
+
# "Submit does nothing" is the #1 churn bug. Absence/staleness of a proof
|
|
2682
|
+
# is inconclusive pass-through, not this block. Opt out: LOKI_PROOF_PERSIST=0.
|
|
2683
|
+
if [ -n "$failures" ]; then
|
|
2684
|
+
failures="${failures}|a create/submit was exercised but the record did not persist across reload (set LOKI_PROOF_PERSIST=0 to opt out)"
|
|
2685
|
+
else
|
|
2686
|
+
failures="a create/submit was exercised but the record did not persist across reload (set LOKI_PROOF_PERSIST=0 to opt out)"
|
|
2687
|
+
fi
|
|
2688
|
+
log_warn "[Council] Evidence gate BLOCKED: persistence not proven -- a submit did not survive reload (${persist_inconclusive_reason:-not_persisted}). Opt out: LOKI_PROOF_PERSIST=0"
|
|
2689
|
+
fi
|
|
2690
|
+
if [ "$auth_fails" = "true" ]; then
|
|
2691
|
+
# PoF #3: a protected route served logged-out (200), only a login screen
|
|
2692
|
+
# rendered, or auth was detected but enforcement could not be proven
|
|
2693
|
+
# under STRICT. A rendered login screen is not "auth done". Opt out:
|
|
2694
|
+
# LOKI_PROOF_AUTH=0 (or LOKI_PROOF_AUTH_STRICT=0 to relax the detected-but-
|
|
2695
|
+
# untestable case, or LOKI_PROOF_PROTECTED_PATH to point the driver).
|
|
2696
|
+
if [ -n "$failures" ]; then
|
|
2697
|
+
failures="${failures}|auth enforcement was not proven -- a protected route was reachable logged-out or could not be tested (set LOKI_PROOF_AUTH=0 to opt out)"
|
|
2698
|
+
else
|
|
2699
|
+
failures="auth enforcement was not proven -- a protected route was reachable logged-out or could not be tested (set LOKI_PROOF_AUTH=0 to opt out)"
|
|
2700
|
+
fi
|
|
2701
|
+
log_warn "[Council] Evidence gate BLOCKED: auth enforcement not proven (${auth_inconclusive_reason:-not_enforced}). Opt out: LOKI_PROOF_AUTH=0 (or LOKI_PROOF_PROTECTED_PATH / LOKI_PROOF_AUTH_STRICT=0)"
|
|
2702
|
+
fi
|
|
2703
|
+
if [ "$authz_fails" = "true" ]; then
|
|
2704
|
+
# PoF #4 (the Lovable-breach class): user B's real second session READ user
|
|
2705
|
+
# A's owned sentinel via a list/detail/API path -- tenant isolation is
|
|
2706
|
+
# BROKEN (ownership/RLS check inverted or missing). This is the ONLY authz
|
|
2707
|
+
# block outcome: absence/undetectable/driver-error all pass through. Opt
|
|
2708
|
+
# out: LOKI_PROOF_AUTHZ=0.
|
|
2709
|
+
if [ -n "$failures" ]; then
|
|
2710
|
+
failures="${failures}|tenant isolation is broken -- a second user could read another user's owned data (set LOKI_PROOF_AUTHZ=0 to opt out)"
|
|
2711
|
+
else
|
|
2712
|
+
failures="tenant isolation is broken -- a second user could read another user's owned data (set LOKI_PROOF_AUTHZ=0 to opt out)"
|
|
2713
|
+
fi
|
|
2714
|
+
log_warn "[Council] Evidence gate BLOCKED: tenant isolation broken -- user B read user A's owned data (${authz_inconclusive_reason:-user_b_read_user_a}). Opt out: LOKI_PROOF_AUTHZ=0"
|
|
2715
|
+
fi
|
|
2052
2716
|
|
|
2053
2717
|
# Rail 3 (one-step self-rescue): the terminal user (no dashboard open) must
|
|
2054
2718
|
# be told, right at the block site, how to opt out of the gate. A false
|
|
@@ -2069,8 +2733,23 @@ import json, os
|
|
|
2069
2733
|
items = [s for s in os.environ['_FAILURES'].split('|') if s]
|
|
2070
2734
|
print(json.dumps(items[:5]))
|
|
2071
2735
|
" 2>/dev/null || echo '[]')
|
|
2736
|
+
local boot_ok secret_ok boot_reason_json
|
|
2737
|
+
local nomock_ok persist_ok auth_ok authz_ok nomock_reason_json persist_reason_json auth_reason_json authz_reason_json
|
|
2072
2738
|
if [ "$diff_fails" = "true" ]; then diff_ok="false"; else diff_ok="true"; fi
|
|
2073
2739
|
if [ "$test_fails" = "true" ]; then tests_ok="false"; else tests_ok="true"; fi
|
|
2740
|
+
if [ "$boot_fails" = "true" ]; then boot_ok="false"; else boot_ok="true"; fi
|
|
2741
|
+
if [ "$secret_fails" = "true" ]; then secret_ok="false"; else secret_ok="true"; fi
|
|
2742
|
+
if [ "$nomock_fails" = "true" ]; then nomock_ok="false"; else nomock_ok="true"; fi
|
|
2743
|
+
if [ "$persist_fails" = "true" ]; then persist_ok="false"; else persist_ok="true"; fi
|
|
2744
|
+
if [ "$auth_fails" = "true" ]; then auth_ok="false"; else auth_ok="true"; fi
|
|
2745
|
+
if [ "$authz_fails" = "true" ]; then authz_ok="false"; else authz_ok="true"; fi
|
|
2746
|
+
# Record WHY boot was inconclusive (no_app_runner / not_serveable / etc.) so a
|
|
2747
|
+
# consumer of the block report can tell a genuine boot pass from a pass-through.
|
|
2748
|
+
boot_reason_json=$(_R="${boot_inconclusive_reason:-}" python3 -c "import json,os; print(json.dumps(os.environ['_R']))" 2>/dev/null || echo '""')
|
|
2749
|
+
nomock_reason_json=$(_R="${nomock_inconclusive_reason:-}" python3 -c "import json,os; print(json.dumps(os.environ['_R']))" 2>/dev/null || echo '""')
|
|
2750
|
+
persist_reason_json=$(_R="${persist_inconclusive_reason:-}" python3 -c "import json,os; print(json.dumps(os.environ['_R']))" 2>/dev/null || echo '""')
|
|
2751
|
+
auth_reason_json=$(_R="${auth_inconclusive_reason:-}" python3 -c "import json,os; print(json.dumps(os.environ['_R']))" 2>/dev/null || echo '""')
|
|
2752
|
+
authz_reason_json=$(_R="${authz_inconclusive_reason:-}" python3 -c "import json,os; print(json.dumps(os.environ['_R']))" 2>/dev/null || echo '""')
|
|
2074
2753
|
base_for_json="${base_sha:-}"
|
|
2075
2754
|
cat > "$ev_tmp" << EVIDENCE_EOF
|
|
2076
2755
|
{
|
|
@@ -2081,7 +2760,13 @@ print(json.dumps(items[:5]))
|
|
|
2081
2760
|
"reason": "$reason",
|
|
2082
2761
|
"checks": {
|
|
2083
2762
|
"diff": {"ok": $diff_ok, "base_sha": "$base_for_json", "files_changed": $diff_files, "sources": "committed|unstaged|staged|untracked union"},
|
|
2084
|
-
"tests": {"ok": $tests_ok, "runner": "$test_runner", "pass": $test_pass}
|
|
2763
|
+
"tests": {"ok": $tests_ok, "runner": "$test_runner", "pass": $test_pass},
|
|
2764
|
+
"boot": {"ok": $boot_ok, "inconclusive": $boot_inconclusive, "reason": $boot_reason_json},
|
|
2765
|
+
"secret": {"ok": $secret_ok},
|
|
2766
|
+
"nomock": {"ok": $nomock_ok, "inconclusive": $nomock_inconclusive, "reason": $nomock_reason_json},
|
|
2767
|
+
"persistence": {"ok": $persist_ok, "inconclusive": $persist_inconclusive, "reason": $persist_reason_json},
|
|
2768
|
+
"auth": {"ok": $auth_ok, "inconclusive": $auth_inconclusive, "reason": $auth_reason_json},
|
|
2769
|
+
"authorization": {"ok": $authz_ok, "inconclusive": $authz_inconclusive, "reason": $authz_reason_json}
|
|
2085
2770
|
},
|
|
2086
2771
|
"failures": $failures_json
|
|
2087
2772
|
}
|
|
@@ -2093,10 +2778,19 @@ EVIDENCE_EOF
|
|
|
2093
2778
|
# be the cross-run corpus for the block rate. Append an event here, where a
|
|
2094
2779
|
# block is definitely happening. Additive, best-effort, stdout-silent.
|
|
2095
2780
|
if type record_trust_event_bash &>/dev/null; then
|
|
2781
|
+
# proof_axis attributes a Proof-of-Function block to its axis (nomock|
|
|
2782
|
+
# persistence|auth) so the TS trust metrics can attribute it without a
|
|
2783
|
+
# schema-breaking change; empty for the diff/tests/boot/secret axes.
|
|
2784
|
+
local _proof_axis=""
|
|
2785
|
+
if [ "$nomock_fails" = "true" ]; then _proof_axis="nomock"
|
|
2786
|
+
elif [ "$persist_fails" = "true" ]; then _proof_axis="persistence"
|
|
2787
|
+
elif [ "$auth_fails" = "true" ]; then _proof_axis="auth"
|
|
2788
|
+
elif [ "$authz_fails" = "true" ]; then _proof_axis="authorization"; fi
|
|
2096
2789
|
record_trust_event_bash "evidence_block" \
|
|
2097
2790
|
"reason=$reason" \
|
|
2098
2791
|
"diff_ok=$diff_ok" \
|
|
2099
2792
|
"tests_ok=$tests_ok" \
|
|
2793
|
+
"proof_axis=$_proof_axis" \
|
|
2100
2794
|
>/dev/null 2>&1 || true
|
|
2101
2795
|
fi
|
|
2102
2796
|
|
|
@@ -2223,7 +2917,19 @@ council_member_review() {
|
|
|
2223
2917
|
|
|
2224
2918
|
# Validate provider CLI is available
|
|
2225
2919
|
case "${PROVIDER_NAME:-claude}" in
|
|
2226
|
-
claude)
|
|
2920
|
+
claude)
|
|
2921
|
+
# v8: the raw-SDK vote path (LOKI_SDK_COUNCIL_VOTE=1) needs no claude
|
|
2922
|
+
# binary, so the precondition passes when that path is viable (bridge
|
|
2923
|
+
# + bun). The vote body still falls closed to claude on an SDK miss.
|
|
2924
|
+
if [ "${LOKI_SDK_COUNCIL_VOTE:-0}" = "1" ] \
|
|
2925
|
+
&& { [ -x "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/bin/loki" ] \
|
|
2926
|
+
|| [ -x "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../bin/loki" ]; } \
|
|
2927
|
+
&& command -v bun >/dev/null 2>&1; then
|
|
2928
|
+
:
|
|
2929
|
+
else
|
|
2930
|
+
command -v claude >/dev/null 2>&1 || { log_error "Claude CLI not found"; return 1; }
|
|
2931
|
+
fi
|
|
2932
|
+
;;
|
|
2227
2933
|
codex) command -v codex >/dev/null 2>&1 || { log_error "Codex CLI not found"; return 1; } ;;
|
|
2228
2934
|
gemini) command -v gemini >/dev/null 2>&1 || { log_error "Gemini CLI not found"; return 1; } ;;
|
|
2229
2935
|
cline) command -v cline >/dev/null 2>&1 || { log_error "Cline CLI not found"; return 1; } ;;
|
|
@@ -2306,7 +3012,59 @@ ISSUES: CRITICAL:description (optional, one per line per issue)"
|
|
|
2306
3012
|
# Use the configured provider for review
|
|
2307
3013
|
case "${PROVIDER_NAME:-claude}" in
|
|
2308
3014
|
claude)
|
|
2309
|
-
|
|
3015
|
+
# v8 RAW-SDK VOTE PATH (opt-in LOKI_SDK_COUNCIL_VOTE=1). Run the member
|
|
3016
|
+
# completion vote via the pure-HTTPS @anthropic-ai/sdk text bridge (no
|
|
3017
|
+
# claude binary) through `internal sdk-text`. The reviewer emits the
|
|
3018
|
+
# SAME free-form VOTE/REASON/ISSUES text the claude path does, so the
|
|
3019
|
+
# elaborate downstream parser (grep VOTE:/REASON:/ISSUES:) is UNCHANGED
|
|
3020
|
+
# -- text bridge, not a schema change, to keep the trust-core parse
|
|
3021
|
+
# byte-identical. Runs BEFORE the claude-binary guard so the no-binary
|
|
3022
|
+
# deploy win holds. Fail-closed: on ANY miss (flag off, no key, bun/
|
|
3023
|
+
# entrypoint absent, non-zero, empty) fall through to the claude path;
|
|
3024
|
+
# a truly empty verdict then hits the conservative REJECT default. The
|
|
3025
|
+
# captured text is written to $verdict (same var the claude arm sets)
|
|
3026
|
+
# and _provider_rc is set so a timeout still routes conservatively.
|
|
3027
|
+
if [ "${LOKI_SDK_COUNCIL_VOTE:-0}" = "1" ]; then
|
|
3028
|
+
local _cv_root _cv_loki _cv_pf _cv_out _cv_rc
|
|
3029
|
+
_cv_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
3030
|
+
_cv_loki="${_cv_root}/bin/loki"
|
|
3031
|
+
[ -x "$_cv_loki" ] || _cv_loki="${_cv_root}/../bin/loki"
|
|
3032
|
+
if [ -x "$_cv_loki" ] && command -v bun >/dev/null 2>&1; then
|
|
3033
|
+
_cv_pf="$(mktemp 2>/dev/null)" || _cv_pf=""
|
|
3034
|
+
if [ -n "$_cv_pf" ]; then
|
|
3035
|
+
printf '%s' "$prompt" > "$_cv_pf"
|
|
3036
|
+
_cv_rc=0
|
|
3037
|
+
local _cv_to_s="${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}"
|
|
3038
|
+
local _cv_wrap
|
|
3039
|
+
if command -v timeout >/dev/null 2>&1; then _cv_wrap="timeout $(( _cv_to_s + 15 ))"
|
|
3040
|
+
elif command -v gtimeout >/dev/null 2>&1; then _cv_wrap="gtimeout $(( _cv_to_s + 15 ))"
|
|
3041
|
+
else _cv_wrap=""; fi
|
|
3042
|
+
_cv_out="$($_cv_wrap "$_cv_loki" internal sdk-text \
|
|
3043
|
+
--prompt-file "$_cv_pf" \
|
|
3044
|
+
--model "${LOKI_SDK_COUNCIL_MODEL:-claude-haiku-4-5}" --effort medium \
|
|
3045
|
+
--timeout-ms "$(( _cv_to_s * 1000 ))" 2>/dev/null)" || _cv_rc=$?
|
|
3046
|
+
rm -f "$_cv_pf" 2>/dev/null || true
|
|
3047
|
+
if [ "$_cv_rc" -eq 0 ] && [ -n "$_cv_out" ]; then
|
|
3048
|
+
verdict="$_cv_out"
|
|
3049
|
+
_provider_rc=0
|
|
3050
|
+
elif [ "$_cv_rc" -eq 124 ] || [ "$_cv_rc" -eq 137 ] || [ "$_cv_rc" -eq 143 ]; then
|
|
3051
|
+
# TRUST-CORE SAFE DEFAULT (council review, bash-F4 parity):
|
|
3052
|
+
# an SDK-subprocess TIMEOUT must propagate to _provider_rc
|
|
3053
|
+
# so the post-case guard forces a conservative REJECT.
|
|
3054
|
+
# Without this, a no-claude-binary deploy would fall to
|
|
3055
|
+
# council_heuristic_review (which can APPROVE on benign
|
|
3056
|
+
# evidence) on a hung/rate-limited endpoint -- correlated
|
|
3057
|
+
# member timeouts could then fake-APPROVE the council.
|
|
3058
|
+
# Only set on a genuine timeout kill; a normal SDK miss
|
|
3059
|
+
# (rc 1: no key / transport / refusal) still falls through
|
|
3060
|
+
# to the claude arm exactly as before.
|
|
3061
|
+
_provider_rc=$_cv_rc
|
|
3062
|
+
fi
|
|
3063
|
+
fi
|
|
3064
|
+
fi
|
|
3065
|
+
# if verdict is still unset, fall through to the claude arm below
|
|
3066
|
+
fi
|
|
3067
|
+
if [ -z "${verdict:-}" ] && command -v claude &>/dev/null; then
|
|
2310
3068
|
local council_model="${PROVIDER_MODEL_FAST:-haiku}"
|
|
2311
3069
|
# EMBED 2 + 3 (v7.33.0). Council member completion vote. The
|
|
2312
3070
|
# $prompt is fully self-contained (evidence + instructions +
|
|
@@ -2415,7 +3173,19 @@ council_devils_advocate() {
|
|
|
2415
3173
|
|
|
2416
3174
|
# Validate provider CLI is available
|
|
2417
3175
|
case "${PROVIDER_NAME:-claude}" in
|
|
2418
|
-
claude)
|
|
3176
|
+
claude)
|
|
3177
|
+
# v8: the raw-SDK vote path (LOKI_SDK_COUNCIL_VOTE=1) needs no claude
|
|
3178
|
+
# binary, so the precondition passes when that path is viable (bridge
|
|
3179
|
+
# + bun). The vote body still falls closed to claude on an SDK miss.
|
|
3180
|
+
if [ "${LOKI_SDK_COUNCIL_VOTE:-0}" = "1" ] \
|
|
3181
|
+
&& { [ -x "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/bin/loki" ] \
|
|
3182
|
+
|| [ -x "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../bin/loki" ]; } \
|
|
3183
|
+
&& command -v bun >/dev/null 2>&1; then
|
|
3184
|
+
:
|
|
3185
|
+
else
|
|
3186
|
+
command -v claude >/dev/null 2>&1 || { log_error "Claude CLI not found"; return 1; }
|
|
3187
|
+
fi
|
|
3188
|
+
;;
|
|
2419
3189
|
codex) command -v codex >/dev/null 2>&1 || { log_error "Codex CLI not found"; return 1; } ;;
|
|
2420
3190
|
gemini) command -v gemini >/dev/null 2>&1 || { log_error "Gemini CLI not found"; return 1; } ;;
|
|
2421
3191
|
cline) command -v cline >/dev/null 2>&1 || { log_error "Cline CLI not found"; return 1; } ;;
|
|
@@ -2452,7 +3222,42 @@ REASON: your reasoning"
|
|
|
2452
3222
|
local verdict=""
|
|
2453
3223
|
case "${PROVIDER_NAME:-claude}" in
|
|
2454
3224
|
claude)
|
|
2455
|
-
|
|
3225
|
+
# v8 RAW-SDK CONTRARIAN VOTE (opt-in LOKI_SDK_COUNCIL_VOTE=1). Same
|
|
3226
|
+
# text-bridge pattern as the member vote: emit the SAME free-form
|
|
3227
|
+
# VOTE/REASON text via the pure-HTTPS SDK (no claude binary), leaving
|
|
3228
|
+
# the downstream VOTE: parser untouched. Runs BEFORE the claude guard.
|
|
3229
|
+
# Fail-closed: on ANY miss fall through to the claude arm; a truly
|
|
3230
|
+
# empty verdict then hits the conservative REJECT default. (The
|
|
3231
|
+
# contrarian path tracks no _provider_rc -- an empty verdict already
|
|
3232
|
+
# routes conservatively -- so we only set $verdict on success.)
|
|
3233
|
+
if [ "${LOKI_SDK_COUNCIL_VOTE:-0}" = "1" ]; then
|
|
3234
|
+
local _dv_root _dv_loki _dv_pf _dv_out _dv_rc
|
|
3235
|
+
_dv_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
3236
|
+
_dv_loki="${_dv_root}/bin/loki"
|
|
3237
|
+
[ -x "$_dv_loki" ] || _dv_loki="${_dv_root}/../bin/loki"
|
|
3238
|
+
if [ -x "$_dv_loki" ] && command -v bun >/dev/null 2>&1; then
|
|
3239
|
+
_dv_pf="$(mktemp 2>/dev/null)" || _dv_pf=""
|
|
3240
|
+
if [ -n "$_dv_pf" ]; then
|
|
3241
|
+
printf '%s' "$prompt" > "$_dv_pf"
|
|
3242
|
+
_dv_rc=0
|
|
3243
|
+
local _dv_to_s="${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}"
|
|
3244
|
+
local _dv_wrap
|
|
3245
|
+
if command -v timeout >/dev/null 2>&1; then _dv_wrap="timeout $(( _dv_to_s + 15 ))"
|
|
3246
|
+
elif command -v gtimeout >/dev/null 2>&1; then _dv_wrap="gtimeout $(( _dv_to_s + 15 ))"
|
|
3247
|
+
else _dv_wrap=""; fi
|
|
3248
|
+
_dv_out="$($_dv_wrap "$_dv_loki" internal sdk-text \
|
|
3249
|
+
--prompt-file "$_dv_pf" \
|
|
3250
|
+
--model "${LOKI_SDK_COUNCIL_MODEL:-claude-haiku-4-5}" --effort medium \
|
|
3251
|
+
--timeout-ms "$(( _dv_to_s * 1000 ))" 2>/dev/null)" || _dv_rc=$?
|
|
3252
|
+
rm -f "$_dv_pf" 2>/dev/null || true
|
|
3253
|
+
if [ "$_dv_rc" -eq 0 ] && [ -n "$_dv_out" ]; then
|
|
3254
|
+
verdict="$_dv_out"
|
|
3255
|
+
fi
|
|
3256
|
+
fi
|
|
3257
|
+
fi
|
|
3258
|
+
# if verdict is still unset, fall through to the claude arm below
|
|
3259
|
+
fi
|
|
3260
|
+
if [ -z "${verdict:-}" ] && command -v claude &>/dev/null; then
|
|
2456
3261
|
local council_model="${PROVIDER_MODEL_FAST:-haiku}"
|
|
2457
3262
|
# EMBED 2 + 3 (v7.33.0). Contrarian (devil's-advocate) vote --
|
|
2458
3263
|
# an adversarial reviewer. Self-contained $prompt via stdin,
|
|
@@ -2536,11 +3341,35 @@ council_heuristic_review() {
|
|
|
2536
3341
|
fi
|
|
2537
3342
|
;;
|
|
2538
3343
|
test_auditor)
|
|
2539
|
-
#
|
|
2540
|
-
|
|
3344
|
+
# RUN-25 iter 14 (Wave B #9): require AFFIRMATIVE evidence that tests
|
|
3345
|
+
# actually ran AND passed before approving -- the old logic approved on
|
|
3346
|
+
# the ABSENCE of a negative signal (evidence merely MENTIONS "test" and
|
|
3347
|
+
# lacks "fail"), which is a fake-green: a build where the suite never
|
|
3348
|
+
# ran has no failure text either. Mirror council_evaluate_member: a real
|
|
3349
|
+
# .loki/quality/test-results.json with a runner (not "none") AND pass
|
|
3350
|
+
# true is the only thing that clears this auditor. No such affirmative
|
|
3351
|
+
# evidence -> REJECT (keep iterating), never a heuristic APPROVE.
|
|
3352
|
+
local _tr_file=".loki/quality/test-results.json"
|
|
3353
|
+
local _tests_ok=0
|
|
3354
|
+
if [ -f "$_tr_file" ] && command -v python3 >/dev/null 2>&1; then
|
|
3355
|
+
_tests_ok=$(_TR="$_tr_file" python3 -c "
|
|
3356
|
+
import json, os
|
|
3357
|
+
try:
|
|
3358
|
+
with open(os.environ['_TR']) as f:
|
|
3359
|
+
d = json.load(f)
|
|
3360
|
+
except Exception:
|
|
3361
|
+
print(0); raise SystemExit
|
|
3362
|
+
runner = str(d.get('runner', 'none'))
|
|
3363
|
+
p = d.get('pass', None)
|
|
3364
|
+
# affirmative: a real runner ran AND reported pass true (not inconclusive/false).
|
|
3365
|
+
print(1 if runner not in ('none', '', 'None') and p is True else 0)
|
|
3366
|
+
" 2>/dev/null || echo 0)
|
|
3367
|
+
fi
|
|
3368
|
+
if [ "${_tests_ok:-0}" != "1" ]; then
|
|
3369
|
+
# No affirmative pass evidence -> this auditor cannot approve.
|
|
2541
3370
|
((issues++))
|
|
2542
3371
|
fi
|
|
2543
|
-
#
|
|
3372
|
+
# A negative signal in the evidence is still an independent issue.
|
|
2544
3373
|
if echo "$evidence" | grep -qiE "(fail|error|FAIL)"; then
|
|
2545
3374
|
((issues++))
|
|
2546
3375
|
fi
|
|
@@ -3530,7 +4359,12 @@ _council_should_check_now() {
|
|
|
3530
4359
|
local circuit_triggered="${1:-false}"
|
|
3531
4360
|
local completion_claimed="${2:-false}"
|
|
3532
4361
|
local iter="${ITERATION_COUNT:-0}"
|
|
3533
|
-
|
|
4362
|
+
# LOKI_AUTO_TUNE (default 0): when on, converge faster on simple builds by
|
|
4363
|
+
# lowering the check interval (simple -> 3). No-op when off. Resolved here (not
|
|
4364
|
+
# at source time) because DETECTED_COMPLEXITY is populated after this file is
|
|
4365
|
+
# sourced -- same reason _council_effective_min_iter is resolved at run time.
|
|
4366
|
+
local interval
|
|
4367
|
+
interval="$(_loki_auto_tune_interval "${DETECTED_COMPLEXITY:-}")"
|
|
3534
4368
|
# SaaS #122: tier-aware effective floor (simple->1) so the no-claim early
|
|
3535
4369
|
# check can also fire at iteration 1 on a genuinely-done simple app.
|
|
3536
4370
|
local min_iter
|