loki-mode 10.6.10 → 10.6.12
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 +20 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/backlog.py +19 -3
- package/autonomy/lib/fast_verify.py +19 -1
- package/autonomy/lib/proof-pr.sh +34 -3
- package/autonomy/lib/workspace.py +27 -1
- package/autonomy/lib/workspace_metrics.py +85 -0
- package/autonomy/lib/worktree_prep.py +45 -3
- package/autonomy/loki +62 -3
- package/autonomy/run.sh +95 -6
- package/autonomy/verify.sh +87 -20
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_operator.py +27 -0
- package/dashboard/api_runs.py +52 -0
- package/dashboard/api_workspaces.py +140 -0
- package/dashboard/audit.py +8 -2
- package/dashboard/server.py +42 -21
- package/dashboard/static/index.html +4 -3
- package/dashboard/static/proofs.html +5 -0
- package/dashboard/static/start.html +32 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/SIGNED-RECEIPTS.md +39 -0
- package/docs/UNREACHABLE-TESTS.md +1 -1
- package/docs/WORKSPACES.md +14 -0
- package/docs/audit-logging.md +13 -0
- package/docs/dev/release-checklist.md +5 -0
- package/docs/exit-codes.md +44 -9
- package/docs/v10/BOARD.md +78 -26
- package/docs/v10/DECISIONS.md +29 -0
- package/docs/v10/E2E-D65.md +8 -0
- package/docs/v10/GUIDE.md +1 -1
- package/docs/v10/PROGRESS.md +68 -0
- package/loki-ts/dist/loki.js +545 -536
- package/mcp/__init__.py +1 -1
- package/mcp/tests/test_v10_tools.py +41 -0
- package/mcp/v10_tools.py +13 -0
- package/package.json +1 -1
- package/packages/control-plane/dist/server.js +14 -1
- package/packages/control-plane/drizzle/meta/0001_snapshot.json +364 -0
- package/packages/control-plane/src/server/app.ts +7 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-OaSCF3M4.js → AdminPage-oc63I7OH.js} +1 -1
- package/web-app/dist/assets/{Avatar-xa3G7_vs.js → Avatar-BziJJn7b.js} +1 -1
- package/web-app/dist/assets/{Badge-Ci_UcHij.js → Badge-DeTNCD_k.js} +1 -1
- package/web-app/dist/assets/{Button-CFrrmbvL.js → Button-D9wjGlQI.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-6hyVl_QZ.js → CockpitPage-Bw4_akgH.js} +1 -1
- package/web-app/dist/assets/{ComparePage-B5H56KCG.js → ComparePage-XmdsH_wC.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-CBbRHsYr.js → ErrorBoundary-B0_IF9Ew.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-D-Zoneum.js → EvidenceReceiptPanel-Da3d60pX.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-D7XPRsic.js → GitHubIssuesPanel-D9Jiuoyc.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-CzHbBPAn.js → GitHubPRsPanel-DQCUsWAb.js} +1 -1
- package/web-app/dist/assets/{HomePage-ClOx5qF1.js → HomePage-rfyNY25A.js} +2 -2
- package/web-app/dist/assets/{LoginPage-3Yz_9j8E.js → LoginPage-BGHFcJj8.js} +1 -1
- package/web-app/dist/assets/{MagicPage-Dumn48gC.js → MagicPage-CdoeZ5aB.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-CYCYVG_8.js → MetricsPage-CwKr2759.js} +2 -2
- package/web-app/dist/assets/{NotFoundPage-CV-x_1qH.js → NotFoundPage-9qNm-fWi.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CeO7dnyM.js → ProjectPage-BbeRxI8t.js} +67 -67
- package/web-app/dist/assets/{ProjectsPage-BT9mz6iD.js → ProjectsPage-hw9XHZXR.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-BL8X9fAB.js → SettingsPage-Cq6duAHh.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-8f3lnSpc.js → ShowcasePage-DcpXgNKy.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-BUNAAzh2.js → SystemSettingsPage-BMWp2LCp.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-Qj-VEZJz.js → TeamsPage-Dh1rte-l.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-b8lKqe5q.js → TemplatesPage-BUIMiu9V.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-jOnfQKB4.js → TerminalOutput-g9_MPOQq.js} +1 -1
- package/web-app/dist/assets/{activity-w7GnyBNp.js → activity-BYGoe6e6.js} +1 -1
- package/web-app/dist/assets/{bell-2i4ejMOA.js → bell-CxCZpUMb.js} +1 -1
- package/web-app/dist/assets/{bot-jaPm4NIs.js → bot-Cpbkdqjb.js} +1 -1
- package/web-app/dist/assets/{check-CvNtxeKM.js → check-fO48BSWF.js} +1 -1
- package/web-app/dist/assets/{chevron-left-zzBjHAtX.js → chevron-left-C3J6cmqz.js} +1 -1
- package/web-app/dist/assets/{circle-alert-CCU0IU2m.js → circle-alert-DhdR0TJR.js} +1 -1
- package/web-app/dist/assets/{clock-BvZckMwN.js → clock-Crp889Lj.js} +1 -1
- package/web-app/dist/assets/{cloud-hrMWDdYp.js → cloud-CUoa_Aqa.js} +1 -1
- package/web-app/dist/assets/{code-xml-v6l40T-K.js → code-xml-BZSDiROr.js} +1 -1
- package/web-app/dist/assets/{database-l2TMUrQd.js → database-BC_OF6lp.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-C2V6OvpO.js → dollar-sign-DJUXGU2U.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-Bytb5JGA.js → file-code-corner-CwHaIaDe.js} +1 -1
- package/web-app/dist/assets/{file-plus-BynCYQB8.js → file-plus-DD8rC5Gt.js} +1 -1
- package/web-app/dist/assets/{globe-DifDdNMH.js → globe-CwDBjzLT.js} +1 -1
- package/web-app/dist/assets/{hammer-D7TsOJD6.js → hammer-CzeZBlk1.js} +1 -1
- package/web-app/dist/assets/{index-Zohfqaz4.js → index-BgFxf62u.js} +3 -3
- package/web-app/dist/assets/{layers-Bk8c2YLS.js → layers-DytzdbMj.js} +1 -1
- package/web-app/dist/assets/{loader-circle-BrLdbMwP.js → loader-circle-D8hG34Pb.js} +1 -1
- package/web-app/dist/assets/{lock-COpmjMyJ.js → lock-hk6B4D77.js} +1 -1
- package/web-app/dist/assets/{package-BMzYdY7F.js → package-DO_92_a-.js} +1 -1
- package/web-app/dist/assets/{plus-B9xpu1Pg.js → plus-BKQDzXcw.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-C2Lzh-AA.js → refresh-cw-DKTkN-7t.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-2Yyy-uQd.js → rotate-ccw-Yhmj5NtP.js} +1 -1
- package/web-app/dist/assets/{scroll-text-DiK2yhsT.js → scroll-text-Cc-NigJs.js} +1 -1
- package/web-app/dist/assets/{server-xlJBgleU.js → server-DYIet42E.js} +1 -1
- package/web-app/dist/assets/{shield-alert-D9w4yBCi.js → shield-alert-Cy_zkAp6.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-CcWfvcE9.js → thumbs-up-DtLybu1S.js} +1 -1
- package/web-app/dist/assets/{trash-2-_hk_pEAp.js → trash-2-n9tqmPW_.js} +1 -1
- package/web-app/dist/assets/{trending-down-CDZT99tC.js → trending-down-zHpd0XJs.js} +1 -1
- package/web-app/dist/assets/{trending-up-A0oreTXc.js → trending-up-d8bugNud.js} +1 -1
- package/web-app/dist/assets/{upload-DfLwhGG2.js → upload-CkiOu1GK.js} +1 -1
- package/web-app/dist/assets/{usePolling-B7gW2YY_.js → usePolling-B9aSXIDR.js} +1 -1
- package/web-app/dist/assets/{user-C-y7_Oon.js → user-DbfT_krW.js} +1 -1
- package/web-app/dist/index.html +1 -1
package/autonomy/run.sh
CHANGED
|
@@ -9884,12 +9884,19 @@ setup_agent_branch() {
|
|
|
9884
9884
|
# file is gone when the user switches back. On conflict the resume
|
|
9885
9885
|
# is refused and a fresh session branch (with a fresh snapshot) is
|
|
9886
9886
|
# minted below.
|
|
9887
|
-
|
|
9887
|
+
local resume_err="" resume_files=""
|
|
9888
|
+
if resume_err="$(git checkout --no-overwrite-ignore "$recorded" 2>&1 >/dev/null)"; then
|
|
9888
9889
|
log_info "Resuming on recorded agent branch: ${recorded}"
|
|
9889
9890
|
_loki_resume_snapshot
|
|
9890
9891
|
return 0
|
|
9891
9892
|
fi
|
|
9892
|
-
|
|
9893
|
+
# git lists the blocking paths as tab-indented lines.
|
|
9894
|
+
resume_files="$(printf '%s\n' "$resume_err" | sed -n 's/^[[:space:]][[:space:]]*//p' | head -5 | paste -sd, - | sed 's/,/, /g')"
|
|
9895
|
+
if [ -n "$resume_files" ]; then
|
|
9896
|
+
log_warn "Recorded agent branch ${recorded} could not be checked out because it would overwrite your ignored file(s): ${resume_files} - creating a new one; the earlier commits stay on ${recorded}"
|
|
9897
|
+
else
|
|
9898
|
+
log_warn "Recorded agent branch ${recorded} could not be checked out - creating a new one; the earlier commits stay on ${recorded}"
|
|
9899
|
+
fi
|
|
9893
9900
|
fi
|
|
9894
9901
|
fi
|
|
9895
9902
|
|
|
@@ -10640,10 +10647,70 @@ _loki_snapshot_preexisting() {
|
|
|
10640
10647
|
# entirely absent from the sealed snapshot this function ultimately feeds.
|
|
10641
10648
|
_loki_untracked_status() {
|
|
10642
10649
|
local top="" prefix="" gittool=""
|
|
10650
|
+
local genv=() fk="" fn="" n=0
|
|
10643
10651
|
gittool="$(_loki_snapshot_git_tool)" || return 1
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
"
|
|
10652
|
+
# Version floor: GIT_CONFIG_COUNT needs git >= 2.31 and GIT_NO_LAZY_FETCH
|
|
10653
|
+
# (no hook-running lazy fetch) needs git >= 2.44. Unparseable also refuses.
|
|
10654
|
+
local gver="" gmaj="" gmin=""
|
|
10655
|
+
gver="$(LC_ALL=C "$gittool" --version 2>/dev/null)" || gver=""
|
|
10656
|
+
gver="${gver#git version }"
|
|
10657
|
+
gmaj="${gver%%.*}"
|
|
10658
|
+
gmin="${gver#*.}"
|
|
10659
|
+
gmin="${gmin%%.*}"
|
|
10660
|
+
case "${gmaj}${gmin}" in
|
|
10661
|
+
'' | *[!0-9]*) gmaj="" ;;
|
|
10662
|
+
esac
|
|
10663
|
+
if [ -z "$gmaj" ] || [ -z "$gmin" ] || [ "$gmaj" -lt 2 ] || { [ "$gmaj" -eq 2 ] && [ "$gmin" -lt 44 ]; }; then
|
|
10664
|
+
echo "snapshot: git >= 2.44 required (got '${gver:-unknown}'); refusing to run status unprotected" >&2
|
|
10665
|
+
return 1
|
|
10666
|
+
fi
|
|
10667
|
+
# Inherited GIT_DIR / GIT_WORK_TREE would repoint every call below.
|
|
10668
|
+
top="$(env -u GIT_DIR -u GIT_WORK_TREE "$gittool" rev-parse --show-toplevel 2>/dev/null)" || return 1
|
|
10669
|
+
prefix="$(env -u GIT_DIR -u GIT_WORK_TREE "$gittool" rev-parse --show-prefix 2>/dev/null)" || return 1
|
|
10670
|
+
# BACKLOG 131c: a repo-local core.fsmonitor is a command git runs on
|
|
10671
|
+
# status; the agent can write .git/config, so disable it (and the
|
|
10672
|
+
# untracked cache it could have poisoned) for this one call.
|
|
10673
|
+
# A filter driver named by attributes (clean/smudge/process) is also a
|
|
10674
|
+
# command git runs when status compares a stat-dirty tracked file, so every
|
|
10675
|
+
# configured driver is blanked: clean/smudge/process become empty (never
|
|
10676
|
+
# `cat`, which resolves from PATH) and required is false.
|
|
10677
|
+
# The overrides travel as GIT_CONFIG_KEY_n / GIT_CONFIG_VALUE_n, never as
|
|
10678
|
+
# `-c key=value`: git splits -c at the first "=", so a driver named "x=y"
|
|
10679
|
+
# would be missed. GIT_NO_LAZY_FETCH stops a partial-clone status from
|
|
10680
|
+
# fetching a missing blob over an agent-written core.sshCommand. Inherited
|
|
10681
|
+
# GIT_DIR, GIT_WORK_TREE, GIT_CONFIG_PARAMETERS and GIT_CONFIG_GLOBAL are
|
|
10682
|
+
# dropped and GIT_CONFIG_COUNT is overwritten, never appended to. LC_ALL=C
|
|
10683
|
+
# keeps the name lookup byte-exact for non-UTF-8 driver names.
|
|
10684
|
+
genv=(env -u GIT_DIR -u GIT_WORK_TREE -u GIT_CONFIG_PARAMETERS -u GIT_CONFIG_GLOBAL
|
|
10685
|
+
LC_ALL=C GIT_NO_LAZY_FETCH=1)
|
|
10686
|
+
# The NUL-delimited listing goes to a file beside the output path, read by
|
|
10687
|
+
# a loop in this shell (it appends to genv and n); removed right after the
|
|
10688
|
+
# loop, before any later return path.
|
|
10689
|
+
: > "${1}.filters" || return 1
|
|
10690
|
+
"${genv[@]}" "$gittool" -C "$top" config -z --name-only --get-regexp \
|
|
10691
|
+
'^filter\..*\.(clean|smudge|process)$' > "${1}.filters" 2>/dev/null || true
|
|
10692
|
+
while IFS= read -r -d '' fk; do
|
|
10693
|
+
fn="${fk%.*}"
|
|
10694
|
+
genv+=("GIT_CONFIG_KEY_${n}=${fk}" "GIT_CONFIG_VALUE_${n}=")
|
|
10695
|
+
n=$((n + 1))
|
|
10696
|
+
genv+=("GIT_CONFIG_KEY_${n}=${fn}.required" "GIT_CONFIG_VALUE_${n}=false")
|
|
10697
|
+
n=$((n + 1))
|
|
10698
|
+
done < "${1}.filters"
|
|
10699
|
+
rm -f "${1}.filters"
|
|
10700
|
+
# Prove the overrides are visible with a per-call random nonce (a constant
|
|
10701
|
+
# could be planted in .git/config by the agent), or refuse to run.
|
|
10702
|
+
local nonce=""
|
|
10703
|
+
nonce="$(od -An -tx8 -N8 /dev/urandom 2>/dev/null | tr -d ' \n')"
|
|
10704
|
+
[ -n "$nonce" ] || return 1
|
|
10705
|
+
genv+=("GIT_CONFIG_KEY_${n}=loki.snapshot.sentinel" "GIT_CONFIG_VALUE_${n}=${nonce}")
|
|
10706
|
+
n=$((n + 1))
|
|
10707
|
+
genv+=("GIT_CONFIG_COUNT=${n}")
|
|
10708
|
+
if [ "$("${genv[@]}" "$gittool" -C "$top" config --get loki.snapshot.sentinel 2>/dev/null)" != "$nonce" ]; then
|
|
10709
|
+
echo "snapshot: git does not honor GIT_CONFIG_COUNT; refusing to run status unprotected" >&2
|
|
10710
|
+
return 1
|
|
10711
|
+
fi
|
|
10712
|
+
"${genv[@]}" "$gittool" -C "$top" -c core.fsmonitor=false -c core.untrackedCache=false \
|
|
10713
|
+
--no-optional-locks status --porcelain -z --no-renames -uall \
|
|
10647
10714
|
--ignored=matching --ignore-submodules=all -- ":(exclude,literal)${prefix}.loki" \
|
|
10648
10715
|
> "$1" 2>/dev/null && return 0
|
|
10649
10716
|
rm -f "$1"
|
|
@@ -11045,12 +11112,23 @@ _loki_untrack_agent_committed_user_files() {
|
|
|
11045
11112
|
fi
|
|
11046
11113
|
mv -f "$rec.new" "$rec"
|
|
11047
11114
|
names="$(_loki_nul_names "$rec")"
|
|
11115
|
+
# S-220: keep what the agent force-staged. Save the index as a tree, make the
|
|
11116
|
+
# removal commit from the branch tip alone, then restore the saved index minus
|
|
11117
|
+
# the removed paths. A global `git reset -q` here dropped those staged files
|
|
11118
|
+
# from the session commit that follows. No saved tree (unmerged index): the
|
|
11119
|
+
# old full reset still applies.
|
|
11120
|
+
local saved_tree=""
|
|
11121
|
+
saved_tree="$(git write-tree 2>/dev/null)" || saved_tree=""
|
|
11048
11122
|
git reset -q >/dev/null 2>&1 || true
|
|
11049
11123
|
if ! git -C "$top" update-index -z --force-remove --stdin < "$rec" >/dev/null 2>&1 \
|
|
11050
11124
|
|| ! git commit -q -m "Loki Mode: untrack pre-existing user files the agent committed" >/dev/null 2>&1; then
|
|
11051
11125
|
log_warn "The agent committed your pre-existing files on $1 and Loki could not remove them from the branch: ${names}. They are on disk. Before switching branches, run git rm --cached on each and commit, or a checkout of the base deletes them. Left uncommitted: no session commit."
|
|
11052
11126
|
return 1
|
|
11053
11127
|
fi
|
|
11128
|
+
if [ -n "$saved_tree" ]; then
|
|
11129
|
+
{ git read-tree "$saved_tree" \
|
|
11130
|
+
&& git -C "$top" update-index -z --force-remove --stdin < "$rec"; } >/dev/null 2>&1 || true
|
|
11131
|
+
fi
|
|
11054
11132
|
log_warn "The agent committed your pre-existing untracked or ignored files on $1: ${names}. Loki removed them from the branch tip; they stay on disk, untracked. The branch history still holds them: do not push $1 as-is. To drop them from its history: git reset --soft ${fork} && git commit"
|
|
11055
11133
|
audit_agent_action "git_untrack_user_files" "Removed pre-existing user files the agent committed" "files=${names}" || true
|
|
11056
11134
|
return 0
|
|
@@ -11156,11 +11234,22 @@ commit_session_changes() {
|
|
|
11156
11234
|
# (_commit_path_looks_secret + _commit_scan_secret_file over EVERY staged
|
|
11157
11235
|
# file) is the actual guarantee for nested/weak secrets; these excludes are a
|
|
11158
11236
|
# cheap first cut for the obvious top-level files only.
|
|
11237
|
+
# S-219: a failed add must not fall through to the "nothing staged" no-op,
|
|
11238
|
+
# which would silently commit nothing. Unstage any partial result and say so.
|
|
11239
|
+
# rc 1 is benign: git exits 1 after staging everything else when a pathspec
|
|
11240
|
+
# (':!.loki') names an ignored path, which is the normal case. A real
|
|
11241
|
+
# failure (unreadable file, lock) exits 128.
|
|
11242
|
+
local add_rc=0
|
|
11159
11243
|
git add -A \
|
|
11160
11244
|
':!.loki' ':!.loki/' \
|
|
11161
11245
|
':!.env' ':!.env.*' ':!*.env' \
|
|
11162
11246
|
':!*.key' ':!*.pem' ':!*.p12' ':!*.keystore' \
|
|
11163
|
-
':!id_rsa*' ':!*.token' ':!credentials*' 2>/dev/null ||
|
|
11247
|
+
':!id_rsa*' ':!*.token' ':!credentials*' 2>/dev/null || add_rc=$?
|
|
11248
|
+
if [ "$add_rc" -gt 1 ]; then
|
|
11249
|
+
git reset -q >/dev/null 2>&1 || true
|
|
11250
|
+
log_warn "Left uncommitted: could not stage the session's changes (git add failed). Review and commit manually."
|
|
11251
|
+
return 0
|
|
11252
|
+
fi
|
|
11164
11253
|
|
|
11165
11254
|
# Unstage exactly the paths recorded as untracked or gitignored when the
|
|
11166
11255
|
# session started (setup_agent_branch; a "dir/" entry covers its subtree):
|
package/autonomy/verify.sh
CHANGED
|
@@ -68,6 +68,46 @@ VERIFY_EXIT_BLOCKED=2
|
|
|
68
68
|
VERIFY_EXIT_ERROR=3
|
|
69
69
|
VERIFY_SCHEMA_VERSION="1.0"
|
|
70
70
|
|
|
71
|
+
# S-216: verify runs from the tree under review (tree="."), and a bare
|
|
72
|
+
# `python3 -c` / `python3 -` puts that cwd first on sys.path, so a planted
|
|
73
|
+
# json.py in the reviewed tree replaced the stdlib module and forged recorded
|
|
74
|
+
# gates (a dependency_audit with high CVEs read as pass). The inline readers
|
|
75
|
+
# run through _verify_py, which resolves the interpreter with
|
|
76
|
+
# _loki_snapshot_py_tool and runs it -I -S. When none resolves it exits 127
|
|
77
|
+
# with no output, exactly as a missing python3 did, so no reader reads as pass.
|
|
78
|
+
# This module does not source run.sh, so it carries a guarded copy; run.sh is
|
|
79
|
+
# the source of truth: keep it byte-identical. Pinned by
|
|
80
|
+
# tests/test-verify-no-cwd-shadow.sh and tests/test-council-py-tool-identity.sh.
|
|
81
|
+
# Only the unittest run stays on bare python3: it runs the reviewed tree's own
|
|
82
|
+
# tests on purpose. py_compile is a stdlib verdict, so it runs isolated too.
|
|
83
|
+
declare -F _loki_snapshot_py_tool >/dev/null 2>&1 || \
|
|
84
|
+
_loki_snapshot_py_tool() {
|
|
85
|
+
local c
|
|
86
|
+
for c in /usr/bin/python3 /bin/python3; do
|
|
87
|
+
[ -x "$c" ] && [ ! -d "$c" ] && "$c" -I -S -c '' >/dev/null 2>&1 && { printf '%s\n' "$c"; return 0; }
|
|
88
|
+
done
|
|
89
|
+
local dir
|
|
90
|
+
local IFS=:
|
|
91
|
+
for dir in $PATH; do
|
|
92
|
+
case "$dir" in
|
|
93
|
+
/*) ;;
|
|
94
|
+
*) continue ;;
|
|
95
|
+
esac
|
|
96
|
+
if [ -x "$dir/python3" ] && [ ! -d "$dir/python3" ] \
|
|
97
|
+
&& "$dir/python3" -I -S -c '' >/dev/null 2>&1; then
|
|
98
|
+
printf '%s\n' "$dir/python3"
|
|
99
|
+
return 0
|
|
100
|
+
fi
|
|
101
|
+
done
|
|
102
|
+
return 1
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_verify_py() {
|
|
106
|
+
local py
|
|
107
|
+
py="$(_loki_snapshot_py_tool)" || return 127
|
|
108
|
+
"$py" -I -S "$@"
|
|
109
|
+
}
|
|
110
|
+
|
|
71
111
|
# Resolve tool version from the VERSION file shipped alongside the repo.
|
|
72
112
|
# ---------------------------------------------------------------------------
|
|
73
113
|
# LLM review stage (v9.26.0). Phase 2 of the spec; the deterministic MVP shipped
|
|
@@ -187,7 +227,7 @@ SCHEMA
|
|
|
187
227
|
|
|
188
228
|
# Parse defensively: a malformed payload is "unavailable", never a pass.
|
|
189
229
|
local parsed
|
|
190
|
-
parsed="$(printf '%s' "$out" |
|
|
230
|
+
parsed="$(printf '%s' "$out" | _verify_py -c '
|
|
191
231
|
import json, sys
|
|
192
232
|
try:
|
|
193
233
|
d = json.load(sys.stdin)
|
|
@@ -588,7 +628,7 @@ ZT_MATCHES_EOF
|
|
|
588
628
|
_verify_pkg_test_script() {
|
|
589
629
|
local tree="$1"
|
|
590
630
|
[ -f "$tree/package.json" ] || return 0
|
|
591
|
-
|
|
631
|
+
_verify_py -c '
|
|
592
632
|
import json,sys
|
|
593
633
|
try:
|
|
594
634
|
with open(sys.argv[1]) as fh:
|
|
@@ -918,7 +958,7 @@ verify_gate_static() {
|
|
|
918
958
|
[ -z "$f" ] && continue
|
|
919
959
|
[ -f "$tree/$f" ] || continue
|
|
920
960
|
checked=$((checked + 1))
|
|
921
|
-
|
|
961
|
+
_verify_py -m py_compile "$tree/$f" >/dev/null 2>&1 || {
|
|
922
962
|
findings=$((findings + 1))
|
|
923
963
|
details="${details}Python syntax error: $f. "
|
|
924
964
|
_verify_add_finding "Medium" "lint" "deterministic:py_compile" "$f" "null" \
|
|
@@ -1266,9 +1306,9 @@ verify_gate_dependency_audit() {
|
|
|
1266
1306
|
out="$(cd "$tree" && npm audit --json 2>/dev/null)" || rc=$?
|
|
1267
1307
|
# npm audit exits nonzero when vulns are found; rc alone is not an
|
|
1268
1308
|
# error. Distinguish "ran and found vulns" from "could not run".
|
|
1269
|
-
if printf '%s' "$out" |
|
|
1309
|
+
if printf '%s' "$out" | _verify_py -c "import sys,json; json.load(sys.stdin)" >/dev/null 2>&1; then
|
|
1270
1310
|
local sev
|
|
1271
|
-
sev="$(printf '%s' "$out" |
|
|
1311
|
+
sev="$(printf '%s' "$out" | _verify_py -c '
|
|
1272
1312
|
import sys, json
|
|
1273
1313
|
d = json.load(sys.stdin)
|
|
1274
1314
|
v = d.get("metadata", {}).get("vulnerabilities", {})
|
|
@@ -1292,7 +1332,7 @@ print("%d %d %d %d" % (crit, high, mod, low))
|
|
|
1292
1332
|
# count different dependency graphs, so arithmetic on the
|
|
1293
1333
|
# totals would not be sound.
|
|
1294
1334
|
local _prod_hc=""
|
|
1295
|
-
_prod_hc="$(cd "$tree" && npm audit --omit=dev --json 2>/dev/null |
|
|
1335
|
+
_prod_hc="$(cd "$tree" && npm audit --omit=dev --json 2>/dev/null | _verify_py -c '
|
|
1296
1336
|
import sys, json
|
|
1297
1337
|
try:
|
|
1298
1338
|
v = json.load(sys.stdin).get("metadata", {}).get("vulnerabilities", {})
|
|
@@ -1334,9 +1374,9 @@ print(v.get("critical", 0) + v.get("high", 0))
|
|
|
1334
1374
|
if command -v pip-audit >/dev/null 2>&1; then
|
|
1335
1375
|
local out rc=0
|
|
1336
1376
|
out="$(cd "$tree" && pip-audit --format json 2>/dev/null)" || rc=$?
|
|
1337
|
-
if printf '%s' "$out" |
|
|
1377
|
+
if printf '%s' "$out" | _verify_py -c "import sys,json; json.load(sys.stdin)" >/dev/null 2>&1; then
|
|
1338
1378
|
local count
|
|
1339
|
-
count="$(printf '%s' "$out" |
|
|
1379
|
+
count="$(printf '%s' "$out" | _verify_py -c '
|
|
1340
1380
|
import sys, json
|
|
1341
1381
|
d = json.load(sys.stdin)
|
|
1342
1382
|
deps = d.get("dependencies", d if isinstance(d, list) else [])
|
|
@@ -1438,7 +1478,7 @@ _verify_runtime_detect() {
|
|
|
1438
1478
|
# 0b. Rank 9 setup recipe (opportunistic; NOT yet built -- absence is normal).
|
|
1439
1479
|
if [ -z "$method" ] && [ -f "$dir/.loki/setup-recipe.json" ] && command -v python3 >/dev/null 2>&1; then
|
|
1440
1480
|
local recipe
|
|
1441
|
-
recipe="$(
|
|
1481
|
+
recipe="$(_verify_py - "$dir/.loki/setup-recipe.json" <<'PYEOF' 2>/dev/null || true
|
|
1442
1482
|
import json, sys
|
|
1443
1483
|
try:
|
|
1444
1484
|
with open(sys.argv[1]) as f:
|
|
@@ -1611,7 +1651,7 @@ _verify_write_setup_recipe() {
|
|
|
1611
1651
|
_SR_DIR="$tree/.loki" _SR_INSTALL="$install" _SR_SEED="$seed" \
|
|
1612
1652
|
_SR_START="$start" _SR_HEALTH="$health_path" _SR_PORT="$port" \
|
|
1613
1653
|
_SR_ENVSRC="$env_src" \
|
|
1614
|
-
|
|
1654
|
+
_verify_py - <<'PYEOF' 2>/dev/null || return 0
|
|
1615
1655
|
import json, os, re
|
|
1616
1656
|
|
|
1617
1657
|
out_dir = os.environ["_SR_DIR"]
|
|
@@ -1815,7 +1855,7 @@ verify_gate_runtime() {
|
|
|
1815
1855
|
# reproducible (command + url + status + artifact are all captured).
|
|
1816
1856
|
_VR_DIR="$artifact_dir" _VR_METHOD="$method" _VR_URL="$url" \
|
|
1817
1857
|
_VR_STATUS="$http_status" _VR_ART="$artifact_field" _VR_TO="$boot_timeout" \
|
|
1818
|
-
|
|
1858
|
+
_verify_py - <<'PYEOF' 2>/dev/null || true
|
|
1819
1859
|
import json, os
|
|
1820
1860
|
rec = {
|
|
1821
1861
|
"start_command": os.environ.get("_VR_METHOD", ""),
|
|
@@ -2213,7 +2253,7 @@ verify_emit_evidence() {
|
|
|
2213
2253
|
_V_SCOPE_SOFT_NET="${VERIFY_SCOPE_SOFT_NET_LINES:-}" \
|
|
2214
2254
|
_V_SCOPE_MAX_FILES="${VERIFY_SCOPE_MAX_FILES:-}" \
|
|
2215
2255
|
_V_SCOPE_MAX_NET="${VERIFY_SCOPE_MAX_NET_LINES:-}" \
|
|
2216
|
-
|
|
2256
|
+
_verify_py - <<'PYEOF'
|
|
2217
2257
|
import json, os, hashlib, sys
|
|
2218
2258
|
|
|
2219
2259
|
out_dir = os.environ["_VERIFY_OUT_DIR"]
|
|
@@ -2671,7 +2711,7 @@ verify_expectation_ledger_gate() {
|
|
|
2671
2711
|
_LK_ITER="$iter" \
|
|
2672
2712
|
_LK_OBSERVED="$observed_file" \
|
|
2673
2713
|
_LK_MOD="$mod" \
|
|
2674
|
-
|
|
2714
|
+
_verify_py - <<'PYEOF' 2>/dev/null
|
|
2675
2715
|
import importlib.util, json, os, sys
|
|
2676
2716
|
|
|
2677
2717
|
mod_path = os.environ["_LK_MOD"]
|
|
@@ -2856,7 +2896,7 @@ verify_hosted_enrich() {
|
|
|
2856
2896
|
|
|
2857
2897
|
# Fold the engine payload into evidence.json under "hosted". A parse failure
|
|
2858
2898
|
# at any step leaves the file byte-for-byte unchanged.
|
|
2859
|
-
_V_EV="$ev_path" _V_ENGINE="$engine_out"
|
|
2899
|
+
_V_EV="$ev_path" _V_ENGINE="$engine_out" _verify_py - <<'PYEOF' || {
|
|
2860
2900
|
import json, os, sys
|
|
2861
2901
|
|
|
2862
2902
|
ev_path = os.environ["_V_EV"]
|
|
@@ -2906,7 +2946,7 @@ PYEOF
|
|
|
2906
2946
|
# Built from the engine payload via python3 (already a precondition above);
|
|
2907
2947
|
# any parse hiccup leaves the summary empty and the banner simply omits the
|
|
2908
2948
|
# line -- never a fabricated summary.
|
|
2909
|
-
VERIFY_HOSTED_SUMMARY="$(_V_ENGINE="$engine_out" _V_VERDICT="${VERIFY_VERDICT:-}"
|
|
2949
|
+
VERIFY_HOSTED_SUMMARY="$(_V_ENGINE="$engine_out" _V_VERDICT="${VERIFY_VERDICT:-}" _verify_py - <<'PYEOF' 2>/dev/null || true
|
|
2910
2950
|
import json, os
|
|
2911
2951
|
try:
|
|
2912
2952
|
e = json.loads(os.environ["_V_ENGINE"])
|
|
@@ -3002,7 +3042,7 @@ _verify_check_fresh() {
|
|
|
3002
3042
|
# Parse the graded head_sha out of the evidence document. python3 is the same
|
|
3003
3043
|
# dependency the emitter uses; a parse failure is fail-closed (ERROR).
|
|
3004
3044
|
local graded_head parse_rc
|
|
3005
|
-
graded_head="$(_VF_EV="$ev_path"
|
|
3045
|
+
graded_head="$(_VF_EV="$ev_path" _verify_py - <<'PYEOF' 2>/dev/null
|
|
3006
3046
|
import json, os, sys
|
|
3007
3047
|
try:
|
|
3008
3048
|
doc = json.load(open(os.environ["_VF_EV"]))
|
|
@@ -3053,9 +3093,26 @@ PYEOF
|
|
|
3053
3093
|
_verify_receipt_digest() {
|
|
3054
3094
|
local rid pj lib rc=0
|
|
3055
3095
|
rid="$(cat .loki/state/last-proof-id.txt 2>/dev/null || true)"
|
|
3056
|
-
|
|
3096
|
+
# D76: a missing or unusable run-id pointer must not silently skip the receipt check.
|
|
3097
|
+
# Proofs on disk with no pointer to one is NOT VERIFIED; a tree that never recorded a
|
|
3098
|
+
# proof (nothing to verify) says so aloud and does not claim a receipt check.
|
|
3099
|
+
if [ -z "$rid" ]; then
|
|
3100
|
+
if compgen -G ".loki/proofs/*/proof.json" >/dev/null 2>&1; then
|
|
3101
|
+
echo "receipt: NOT VERIFIED (no last-proof-id.txt pointer, but proofs exist under .loki/proofs; the receipt was not checked)"
|
|
3102
|
+
return 1
|
|
3103
|
+
fi
|
|
3104
|
+
echo "receipt: NONE (no proof was recorded in this tree; no receipt was checked)"
|
|
3105
|
+
return 0
|
|
3106
|
+
fi
|
|
3107
|
+
case "$rid" in *[!A-Za-z0-9._-]*)
|
|
3108
|
+
echo "receipt: NOT VERIFIED (last-proof-id.txt holds an unusable id; the receipt was not checked)"
|
|
3109
|
+
return 1 ;;
|
|
3110
|
+
esac
|
|
3057
3111
|
pj=".loki/proofs/$rid/proof.json"
|
|
3058
|
-
[ -f "$pj" ]
|
|
3112
|
+
if [ ! -f "$pj" ]; then
|
|
3113
|
+
echo "receipt: NOT VERIFIED (last-proof-id.txt names ${rid} but .loki/proofs/${rid}/proof.json is missing; the receipt was not checked)"
|
|
3114
|
+
return 1
|
|
3115
|
+
fi
|
|
3059
3116
|
lib="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib"
|
|
3060
3117
|
python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
3061
3118
|
import importlib.util, json
|
|
@@ -3069,9 +3126,9 @@ print('receipt_sha256: ' + h if ok else 'receipt: TAMPERED (integrity hash does
|
|
|
3069
3126
|
sys.exit(0 if ok else 3)" "$lib" "$pj" 2>/dev/null || rc=$?
|
|
3070
3127
|
[ "$rc" -ne 3 ] || return 1
|
|
3071
3128
|
# Provenance: the deploy gate's own verdict (Ed25519 attestation against the local
|
|
3072
|
-
# key plus LOKI_RECEIPT_RETIRED_PUBKEYS; unknown kid or no key is UNCHECKED).
|
|
3129
|
+
# key plus LOKI_RECEIPT_RETIRED_PUBKEYS; unknown kid or no key is UNCHECKED, which fails per D76).
|
|
3073
3130
|
# Defined in autonomy/loki, so it is absent when verify.sh runs standalone.
|
|
3074
|
-
declare -f _deploy_receipt_verdict >/dev/null 2>&1 || { echo "attestation: UNCHECKED (run via loki verify)"; return
|
|
3131
|
+
declare -f _deploy_receipt_verdict >/dev/null 2>&1 || { echo "attestation: UNCHECKED (run via loki verify)"; return 1; }
|
|
3075
3132
|
local att
|
|
3076
3133
|
att="$(_deploy_receipt_verdict "$pj")"
|
|
3077
3134
|
# D47: UNSIGNED is never a pass; --allow-unsigned / LOKI_VERIFY_ALLOW_UNSIGNED=1 accepts it, said aloud.
|
|
@@ -3084,6 +3141,16 @@ sys.exit(0 if ok else 3)" "$lib" "$pj" 2>/dev/null || rc=$?
|
|
|
3084
3141
|
return 1
|
|
3085
3142
|
fi
|
|
3086
3143
|
printf 'attestation: %s\n' "$att"
|
|
3144
|
+
# D76: UNCHECKED (a well-formed token whose kid no local key matches, or a check that
|
|
3145
|
+
# could not run) is not a pass: anyone can mint a foreign-kid token. rc 2, VERDICT not VERIFIED.
|
|
3146
|
+
if [ "$att" = "UNCHECKED" ]; then
|
|
3147
|
+
echo " Not verified: no local key matches this attestation, so who signed it is unproven."
|
|
3148
|
+
echo " Cross-machine path: loki verify --pubkey FILE <run-id> checks a Loki 10 run receipt"
|
|
3149
|
+
echo " (.loki/runs/<run-id>/receipt.json) against the signer's public key (JWK or PEM)."
|
|
3150
|
+
echo " It does not read a legacy .loki/proofs proof.json; for that, add the signer's public"
|
|
3151
|
+
echo " key to LOKI_RECEIPT_RETIRED_PUBKEYS on this machine."
|
|
3152
|
+
return 1
|
|
3153
|
+
fi
|
|
3087
3154
|
[ "$att" != "TAMPERED" ]
|
|
3088
3155
|
}
|
|
3089
3156
|
|
package/dashboard/__init__.py
CHANGED
|
@@ -294,3 +294,30 @@ def operator_phases():
|
|
|
294
294
|
return api_phases.phase_history(_loki_dir())
|
|
295
295
|
except Exception as exc:
|
|
296
296
|
_fail("phase history", exc)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@router.get("/workspaces/runs", dependencies=_READ)
|
|
300
|
+
def operator_workspace_runs():
|
|
301
|
+
"""Multi-repo workspace runs, read from each run's integration.json.
|
|
302
|
+
|
|
303
|
+
A corrupt file is a row with state "unreadable", never dropped.
|
|
304
|
+
"""
|
|
305
|
+
try:
|
|
306
|
+
from . import api_workspaces
|
|
307
|
+
return api_workspaces.list_runs(_loki_dir())
|
|
308
|
+
except Exception as exc:
|
|
309
|
+
_fail("workspace runs", exc)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@router.get("/workspaces/runs/{ws}/{run_id}", dependencies=_READ)
|
|
313
|
+
def operator_workspace_run(ws: str, run_id: str):
|
|
314
|
+
"""One workspace run with per-repo stale detection (recorded vs worktree head)."""
|
|
315
|
+
from . import api_workspaces
|
|
316
|
+
try:
|
|
317
|
+
return api_workspaces.get_run(_loki_dir(), ws, run_id)
|
|
318
|
+
except api_workspaces.BadName:
|
|
319
|
+
raise HTTPException(status_code=400, detail="invalid workspace or run id")
|
|
320
|
+
except api_workspaces.NotFound:
|
|
321
|
+
raise HTTPException(status_code=404, detail="unknown run")
|
|
322
|
+
except Exception as exc:
|
|
323
|
+
_fail("workspace run", exc)
|
package/dashboard/api_runs.py
CHANGED
|
@@ -158,6 +158,44 @@ def _efficiency_module():
|
|
|
158
158
|
# per-iteration cost, from the current run's efficiency records
|
|
159
159
|
# ---------------------------------------------------------------------------
|
|
160
160
|
|
|
161
|
+
def _errno_text(exc: OSError) -> str:
|
|
162
|
+
return "errno %s %s" % (exc.errno, exc.strerror or type(exc).__name__)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _events_read_error(loki_dir: str) -> Optional[str]:
|
|
166
|
+
"""errno text when trust-events.jsonl exists but cannot be read, else None.
|
|
167
|
+
|
|
168
|
+
A missing file (ENOENT) is not an error: it means nothing was recorded.
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
with open(_p(loki_dir, *_TRUST_EVENTS), "r", encoding="utf-8") as fh:
|
|
172
|
+
fh.read(1)
|
|
173
|
+
except FileNotFoundError:
|
|
174
|
+
return None
|
|
175
|
+
except OSError as exc:
|
|
176
|
+
return _errno_text(exc)
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _iterations_read_error(loki_dir: str) -> Optional[str]:
|
|
181
|
+
"""Why iteration records are unreadable or corrupt, else None.
|
|
182
|
+
|
|
183
|
+
None means the directory is absent or holds no iteration-*.json files at
|
|
184
|
+
all, i.e. genuinely nothing recorded.
|
|
185
|
+
"""
|
|
186
|
+
eff_dir = _p(loki_dir, *_EFFICIENCY_DIR)
|
|
187
|
+
try:
|
|
188
|
+
names = os.listdir(eff_dir)
|
|
189
|
+
except FileNotFoundError:
|
|
190
|
+
return None
|
|
191
|
+
except OSError as exc:
|
|
192
|
+
return _errno_text(exc)
|
|
193
|
+
cands = [n for n in names if n.startswith("iteration-") and n.endswith(".json")]
|
|
194
|
+
if cands and not _iteration_records(loki_dir):
|
|
195
|
+
return "%d iteration file(s) present but none parsed" % len(cands)
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
161
199
|
def _iteration_records(loki_dir: str) -> list:
|
|
162
200
|
"""[(iteration_int, record_dict, path)] sorted by iteration.
|
|
163
201
|
|
|
@@ -399,6 +437,13 @@ def list_runs(loki_dir: str, now: Optional[float] = None) -> dict:
|
|
|
399
437
|
if current_id and current_id not in ids:
|
|
400
438
|
ids.append(current_id)
|
|
401
439
|
|
|
440
|
+
if not ids and _events_read_error(loki_dir):
|
|
441
|
+
envelope["reason"] = (
|
|
442
|
+
"unreadable: .loki/metrics/trust-events.jsonl could not be read (%s)"
|
|
443
|
+
% _events_read_error(loki_dir)
|
|
444
|
+
)
|
|
445
|
+
return envelope
|
|
446
|
+
|
|
402
447
|
if not ids:
|
|
403
448
|
envelope["reason"] = (
|
|
404
449
|
"no run id found: .loki/state/trust-run-id is absent and "
|
|
@@ -463,6 +508,13 @@ def get_run(loki_dir: str, run_id: str, now: Optional[float] = None) -> dict:
|
|
|
463
508
|
|
|
464
509
|
mod = _efficiency_module()
|
|
465
510
|
recs = _iteration_records(loki_dir)
|
|
511
|
+
if not recs and _iterations_read_error(loki_dir):
|
|
512
|
+
envelope["reason"] = (
|
|
513
|
+
"per-iteration records unreadable or corrupt at "
|
|
514
|
+
".loki/metrics/efficiency/iteration-*.json (%s)"
|
|
515
|
+
% _iterations_read_error(loki_dir)
|
|
516
|
+
)
|
|
517
|
+
return envelope
|
|
466
518
|
if not recs:
|
|
467
519
|
envelope["reason"] = (
|
|
468
520
|
"no per-iteration records at .loki/metrics/efficiency/iteration-*.json"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Read-only view over multi-repo workspace runs (D51-B13r).
|
|
2
|
+
|
|
3
|
+
`loki workspace run` records one integration.json per run under
|
|
4
|
+
<loki_dir>/workspaces/<workspace>/<run_id>/. That file is the ONLY evidence a
|
|
5
|
+
run leaves (runs never produce group.json), so this reader lists and returns
|
|
6
|
+
exactly it.
|
|
7
|
+
|
|
8
|
+
Honesty rules:
|
|
9
|
+
- A missing workspaces directory is {runs: [], reason}, never a bare empty.
|
|
10
|
+
- An unreadable or corrupt integration.json is a row with state
|
|
11
|
+
"unreadable" and an error. It is never dropped.
|
|
12
|
+
- stale is True/False only when both the recorded head and the worktree head
|
|
13
|
+
are known. A missing worktree or recorded head reads None with a reason.
|
|
14
|
+
|
|
15
|
+
Nothing here mutates a run.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import subprocess
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
|
|
26
|
+
__all__ = ["list_runs", "get_run", "valid_name", "NotFound", "BadName"]
|
|
27
|
+
|
|
28
|
+
_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
29
|
+
_GIT_TIMEOUT_S = 5
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BadName(ValueError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NotFound(LookupError):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def valid_name(name: str) -> bool:
|
|
41
|
+
return bool(name) and ".." not in name and _NAME_RE.match(name) is not None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _slug(repo: str) -> str:
|
|
45
|
+
# Mirrors autonomy/lib/workspace.py _slug.
|
|
46
|
+
return repo.replace("/", "__")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _worktree_head(run_dir: str, repo: str) -> tuple:
|
|
50
|
+
wt = os.path.join(run_dir, "worktrees", _slug(repo))
|
|
51
|
+
if not os.path.isdir(wt):
|
|
52
|
+
return None, "worktree missing"
|
|
53
|
+
try:
|
|
54
|
+
r = subprocess.run(["git", "-C", wt, "rev-parse", "HEAD"],
|
|
55
|
+
capture_output=True, text=True, timeout=_GIT_TIMEOUT_S)
|
|
56
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
57
|
+
return None, "git failed: %s" % type(exc).__name__
|
|
58
|
+
if r.returncode != 0:
|
|
59
|
+
return None, "git rev-parse failed"
|
|
60
|
+
return r.stdout.strip(), None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _repo_rows(run_dir: str, ev: dict) -> list:
|
|
64
|
+
heads = ev.get("heads")
|
|
65
|
+
rows = []
|
|
66
|
+
if not isinstance(heads, dict):
|
|
67
|
+
return rows
|
|
68
|
+
for repo in sorted(heads):
|
|
69
|
+
recorded = heads[repo]
|
|
70
|
+
row: dict[str, Any] = {"repo": repo, "head": recorded,
|
|
71
|
+
"current_head": None, "stale": None, "reason": None}
|
|
72
|
+
if not recorded:
|
|
73
|
+
row["reason"] = "no recorded head"
|
|
74
|
+
else:
|
|
75
|
+
cur, why = _worktree_head(run_dir, repo)
|
|
76
|
+
row["current_head"] = cur
|
|
77
|
+
if cur is None:
|
|
78
|
+
row["reason"] = why
|
|
79
|
+
else:
|
|
80
|
+
row["stale"] = cur != recorded
|
|
81
|
+
rows.append(row)
|
|
82
|
+
return rows
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _row(root: str, ws: str, run_id: str, with_repos: bool = True) -> dict:
|
|
86
|
+
run_dir = os.path.join(root, ws, run_id)
|
|
87
|
+
path = os.path.join(run_dir, "integration.json")
|
|
88
|
+
row: dict[str, Any] = {"workspace": ws, "run_id": run_id, "state": "ok",
|
|
89
|
+
"status": None, "exit_code": None, "error": None,
|
|
90
|
+
"repos": []}
|
|
91
|
+
try:
|
|
92
|
+
with open(path) as fh:
|
|
93
|
+
ev = json.load(fh)
|
|
94
|
+
if not isinstance(ev, dict):
|
|
95
|
+
raise ValueError("integration.json is not an object")
|
|
96
|
+
except (OSError, ValueError) as exc:
|
|
97
|
+
row["state"] = "unreadable"
|
|
98
|
+
row["error"] = "%s: %s" % (type(exc).__name__, exc)
|
|
99
|
+
return row
|
|
100
|
+
row["status"] = ev.get("status")
|
|
101
|
+
row["exit_code"] = ev.get("exit_code")
|
|
102
|
+
row["outcomes"] = ev.get("outcomes") if isinstance(ev.get("outcomes"), dict) else {}
|
|
103
|
+
if with_repos:
|
|
104
|
+
row["repos"] = _repo_rows(run_dir, ev)
|
|
105
|
+
return row
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def list_runs(loki_dir: str) -> dict:
|
|
109
|
+
root = os.path.join(loki_dir, "workspaces")
|
|
110
|
+
if not os.path.isdir(root):
|
|
111
|
+
return {"runs": [], "reason": "no workspaces directory at %s" % root,
|
|
112
|
+
"source": root}
|
|
113
|
+
runs = []
|
|
114
|
+
try:
|
|
115
|
+
names = sorted(os.listdir(root))
|
|
116
|
+
except OSError as exc:
|
|
117
|
+
return {"runs": [], "reason": "workspaces directory unreadable: %s" % exc,
|
|
118
|
+
"source": root}
|
|
119
|
+
for ws in names:
|
|
120
|
+
wdir = os.path.join(root, ws)
|
|
121
|
+
if not valid_name(ws) or not os.path.isdir(wdir):
|
|
122
|
+
continue
|
|
123
|
+
try:
|
|
124
|
+
ids = sorted(os.listdir(wdir))
|
|
125
|
+
except OSError:
|
|
126
|
+
continue
|
|
127
|
+
for run_id in ids:
|
|
128
|
+
if valid_name(run_id) and os.path.isdir(os.path.join(wdir, run_id)):
|
|
129
|
+
runs.append(_row(root, ws, run_id))
|
|
130
|
+
reason: Optional[str] = None if runs else "no workspace runs recorded"
|
|
131
|
+
return {"runs": runs, "reason": reason, "source": root}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_run(loki_dir: str, ws: str, run_id: str) -> dict:
|
|
135
|
+
if not valid_name(ws) or not valid_name(run_id):
|
|
136
|
+
raise BadName("invalid workspace or run id")
|
|
137
|
+
root = os.path.join(loki_dir, "workspaces")
|
|
138
|
+
if not os.path.isdir(os.path.join(root, ws, run_id)):
|
|
139
|
+
raise NotFound("unknown run")
|
|
140
|
+
return _row(root, ws, run_id)
|
package/dashboard/audit.py
CHANGED
|
@@ -862,7 +862,10 @@ def _unified_cli() -> int:
|
|
|
862
862
|
python3 dashboard/audit.py verify <audit_dir>
|
|
863
863
|
|
|
864
864
|
Prints a single JSON object to stdout. Returns process exit code 0
|
|
865
|
-
on a valid chain, 1 on an invalid chain, 2 on usage error.
|
|
865
|
+
on a valid chain, 1 on an invalid chain, 2 on usage error. `verify`
|
|
866
|
+
also exits 2 when it checked zero files (missing dir, empty dir, or
|
|
867
|
+
only pre-integrity files): nothing checked is not a verified chain.
|
|
868
|
+
`tip` keeps exiting 0 on an empty chain (src/audit/crosslink.js reads it).
|
|
866
869
|
"""
|
|
867
870
|
argv = sys.argv[1:]
|
|
868
871
|
if len(argv) < 2 or argv[0] not in ("tip", "verify", "prefix"):
|
|
@@ -889,7 +892,10 @@ def _unified_cli() -> int:
|
|
|
889
892
|
return 0 if out.get("found", False) else 1
|
|
890
893
|
out = verify_all_logs_in_dir(audit_dir)
|
|
891
894
|
print(json.dumps(out))
|
|
892
|
-
|
|
895
|
+
if not out.get("valid", False):
|
|
896
|
+
return 1
|
|
897
|
+
# `valid` stays True for an empty chain; checking nothing is not a pass.
|
|
898
|
+
return 0 if out.get("files_checked", 0) > 0 else 2
|
|
893
899
|
|
|
894
900
|
|
|
895
901
|
if __name__ == "__main__":
|