loki-mode 11.4.2 → 11.6.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/chat.sh +6 -3
- package/autonomy/lib/engine10-push.sh +19 -1
- package/autonomy/lib/help-core.sh +1 -1
- package/autonomy/lib/proof-generator.py +23 -5
- package/autonomy/lib/proof-verify.py +607 -5
- package/autonomy/lib/run-summary.sh +17 -1
- package/autonomy/lib/setup-requirements.json +35 -0
- package/autonomy/loki +244 -237
- package/autonomy/notification-checker.py +1 -1
- package/autonomy/provider-offer.sh +95 -47
- package/autonomy/route.sh +38 -6
- package/autonomy/run.sh +42 -14
- package/autonomy/setup-wizard.sh +19 -10
- package/autonomy/verify.sh +40 -1
- package/completions/_loki +3 -1
- package/completions/loki.bash +9 -3
- package/dashboard/__init__.py +3 -10
- package/dashboard/control.py +5 -435
- package/docs/ARCHITECTURE.md +1 -2
- package/docs/CLI-CORE-VERBS.md +2 -6
- package/docs/CLI-REFERENCE.md +3 -1
- package/docs/COMPONENTS.md +0 -2
- package/docs/EVALUATING.md +27 -0
- package/docs/HARNESS-INTELLIGENCE-PLAN.md +20 -0
- package/docs/INSTALLATION.md +1 -1
- package/docs/KNOWN-ISSUES.md +10 -1
- package/docs/PROOF-PASSPORT.md +114 -0
- package/docs/alternative-installations.md +103 -0
- package/docs/authentication.md +19 -1
- package/docs/cost-controls.md +139 -0
- package/docs/dev/release-checklist.md +9 -8
- package/docs/enterprise/security.md +3 -234
- package/docs/environment-variables.md +36 -0
- package/docs/exit-codes.md +102 -2
- package/docs/spec-first-intent.md +29 -0
- package/docs/v10/ARCHITECT-CUTS.md +304 -0
- package/docs/v10/CLI-MODERN.md +2 -2
- package/docs/v10/DECISIONS.md +5 -5
- package/docs/v10/ENGINE.md +735 -267
- package/docs/v10/GUARDS.md +16 -14
- package/docs/v10/GUIDE.md +5 -7
- package/docs/v10/PROGRESS.md +12 -1
- package/docs/v11/MARKET-SLICES.md +1 -5
- package/integrations/openclaw/README.md +20 -0
- package/integrations/vibe-kanban.md +12 -50
- package/loki-ts/data/model-pricing.json +1 -0
- package/loki-ts/dist/loki.js +1156 -1103
- package/mcp/__init__.py +1 -1
- package/mcp/_sdk_loader.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +2063 -1032
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/plugins/loki-mode/README.md +41 -1
- package/plugins/loki-mode/commands/handoff.md +12 -0
- package/plugins/loki-mode/commands/loki-verify.md +9 -2
- package/plugins/loki-mode/commands/start.md +20 -0
- package/plugins/loki-mode/hooks/hooks.json +11 -0
- package/plugins/loki-mode/scripts/loki-stop-verify.sh +84 -0
- package/references/quality-control.md +1 -116
- package/skills/00-index.md +2 -2
- package/skills/artifacts.md +0 -22
- package/skills/production.md +2 -38
- package/web-app/dist/assets/{AdminPage-CYJ2BWRC.js → AdminPage-DhXdJNQk.js} +1 -1
- package/web-app/dist/assets/{Avatar-DdK4wynI.js → Avatar-ixVj76fM.js} +1 -1
- package/web-app/dist/assets/{Badge-CrX3MI7J.js → Badge-BGMSp22s.js} +1 -1
- package/web-app/dist/assets/{Button-709-wwGM.js → Button-pYJc32tG.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-BapdTInO.js → CockpitPage-C1JBBouA.js} +1 -1
- package/web-app/dist/assets/{ComparePage-DT99pGvA.js → ComparePage-qD0NV4RK.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-B0uJSys8.js → ErrorBoundary-C-yxO58j.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-BtMFgElP.js → EvidenceReceiptPanel-ByPzuE1S.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-DjJuU941.js → GitHubIssuesPanel-BNv_2BEJ.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-HXfEX1BC.js → GitHubPRsPanel-CuLZQ9CS.js} +1 -1
- package/web-app/dist/assets/{HomePage-Cqyves-S.js → HomePage-CS_FW8Je.js} +2 -2
- package/web-app/dist/assets/{LoginPage-jUbtC7_Y.js → LoginPage-DwiNf1Wm.js} +1 -1
- package/web-app/dist/assets/{MagicPage-DHRsqjgR.js → MagicPage-CmQybLPZ.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-B-pED05R.js → MetricsPage-D9ERdpv8.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-CsbUs21R.js → NotFoundPage-D9ccgdbF.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-BtlLEQwB.js → ProjectPage-B9Jslmgv.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-DaRbRjSp.js → ProjectsPage-DnU99Go2.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-DALrtP3F.js → SettingsPage-DcTyx6wo.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-CoPHSNP0.js → ShowcasePage-jEY9ghSr.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-kOuSN50O.js → SystemSettingsPage-CSGltW-8.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-DGWK0LMp.js → TeamsPage-gUuJXb9l.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-C-_10CwA.js → TemplatesPage-37Zhe--v.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-Cz_vPbXL.js → TerminalOutput-B09hRwaR.js} +1 -1
- package/web-app/dist/assets/{activity-icIsXJVa.js → activity-q_4MY9J0.js} +1 -1
- package/web-app/dist/assets/{bell-DbPR2_4r.js → bell-X8OC_PFP.js} +1 -1
- package/web-app/dist/assets/{bot-DvjP3Yh5.js → bot-C-N1PUOD.js} +1 -1
- package/web-app/dist/assets/{check-D1OBxHeU.js → check-B2c76Jmp.js} +1 -1
- package/web-app/dist/assets/{chevron-left-D09jDZ29.js → chevron-left-BF76YLNw.js} +1 -1
- package/web-app/dist/assets/{circle-alert-DyZMLOcC.js → circle-alert-AeFKBD8h.js} +1 -1
- package/web-app/dist/assets/{clock-CdpdJTId.js → clock-CiXRf-FH.js} +1 -1
- package/web-app/dist/assets/{cloud-CoLTlmoj.js → cloud-DKQ8SRJ5.js} +1 -1
- package/web-app/dist/assets/{code-xml-DrkE2ZZY.js → code-xml-DEb6jXWP.js} +1 -1
- package/web-app/dist/assets/{database-CMQQuM2R.js → database-6W7e-E9c.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-CuioDaOG.js → dollar-sign-Ds2GOuyW.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-Cx9j9K1S.js → file-code-corner-CZU0jg52.js} +1 -1
- package/web-app/dist/assets/{file-plus-CYD2zaaI.js → file-plus-DTVYnXCN.js} +1 -1
- package/web-app/dist/assets/{globe-6w-hnQkL.js → globe-0mUzKYDe.js} +1 -1
- package/web-app/dist/assets/{hammer-BpqtautL.js → hammer-BMHZg3YX.js} +1 -1
- package/web-app/dist/assets/{index-Dzs-9EgJ.js → index-BvOTvjax.js} +3 -3
- package/web-app/dist/assets/{layers-D5hd7jb7.js → layers-Df7TdcWB.js} +1 -1
- package/web-app/dist/assets/{loader-circle-C6Y9x_7H.js → loader-circle-kPsVfrSY.js} +1 -1
- package/web-app/dist/assets/{lock-D51L7rYh.js → lock-CXYcbEc5.js} +1 -1
- package/web-app/dist/assets/{package-BJF8hkP8.js → package-Cqhua1ID.js} +1 -1
- package/web-app/dist/assets/{plus-D5kUQ79n.js → plus-lseXhsn-.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-BKdlCa7F.js → refresh-cw-OZriI6gD.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-_bd_YI1e.js → rotate-ccw-CKCfjPg8.js} +1 -1
- package/web-app/dist/assets/{scroll-text-DbkegIxF.js → scroll-text-DoVyyfKb.js} +1 -1
- package/web-app/dist/assets/{server-DAwqFIsq.js → server-DGy7TflW.js} +1 -1
- package/web-app/dist/assets/{shield-alert-BuR1tPHu.js → shield-alert-BCzRcMXi.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-BF6p3mRd.js → thumbs-up-C_znJd5g.js} +1 -1
- package/web-app/dist/assets/{trash-2-BVpnkqqz.js → trash-2-BosCbeeA.js} +1 -1
- package/web-app/dist/assets/{trending-down-DpsDvRwW.js → trending-down-CkR5zDkV.js} +1 -1
- package/web-app/dist/assets/{trending-up-mFogTjt3.js → trending-up-DpUIfOH4.js} +1 -1
- package/web-app/dist/assets/{upload-4T8zpBsi.js → upload-CH9aKc8L.js} +1 -1
- package/web-app/dist/assets/{usePolling-DPXQv8WO.js → usePolling-pzxrHJeg.js} +1 -1
- package/web-app/dist/assets/{user-BYGIFaTm.js → user-B7OHa-VQ.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/dashboard/run.py +0 -63
- package/docs/openclaw-integration.md +0 -52
- package/docs/v10/HANDOFF-NEXT-WEEK.md +0 -40
- package/docs/v10/MORNING-BRIEF-1003.md +0 -25
- package/docs/v10/MORNING-BRIEF.md +0 -52
- package/docs/v10/TODO.md +0 -35
- package/mcp/resources.py +0 -351
- package/mcp/tools.py +0 -200
|
@@ -68,6 +68,7 @@ CLI:
|
|
|
68
68
|
import hashlib
|
|
69
69
|
import json
|
|
70
70
|
import os
|
|
71
|
+
import platform
|
|
71
72
|
import re
|
|
72
73
|
import subprocess
|
|
73
74
|
import sys
|
|
@@ -138,12 +139,18 @@ def _canonical(obj):
|
|
|
138
139
|
# git helpers
|
|
139
140
|
# ---------------------------------------------------------------------------
|
|
140
141
|
|
|
141
|
-
def _git(repo_dir, args, timeout=30):
|
|
142
|
-
"""Run git in repo_dir. Returns stdout string, or None on any failure.
|
|
142
|
+
def _git(repo_dir, args, timeout=30, env=None, config=()):
|
|
143
|
+
"""Run git in repo_dir. Returns stdout string, or None on any failure.
|
|
144
|
+
|
|
145
|
+
env replaces the child environment; config entries become `-c key=value`.
|
|
146
|
+
"""
|
|
147
|
+
cfg = []
|
|
148
|
+
for c in config:
|
|
149
|
+
cfg += ["-c", c]
|
|
143
150
|
try:
|
|
144
151
|
out = subprocess.run(
|
|
145
|
-
["git", "-C", repo_dir] + args,
|
|
146
|
-
capture_output=True, text=True, timeout=timeout,
|
|
152
|
+
["git", "-C", repo_dir] + cfg + list(args),
|
|
153
|
+
capture_output=True, text=True, timeout=timeout, env=env,
|
|
147
154
|
)
|
|
148
155
|
if out.returncode != 0:
|
|
149
156
|
return None
|
|
@@ -1037,6 +1044,8 @@ def _cli(argv=None):
|
|
|
1037
1044
|
return 2
|
|
1038
1045
|
# Flags are stripped BEFORE positional parsing: proof.ts pipes this
|
|
1039
1046
|
# command's stdout through verbatim, so --human must not shift repo_dir.
|
|
1047
|
+
if argv[0] in ("label", "summary", "json"):
|
|
1048
|
+
return _vr_cli(argv)
|
|
1040
1049
|
human = "--human" in argv
|
|
1041
1050
|
argv = [a for a in argv if a != "--human"]
|
|
1042
1051
|
if not argv:
|
|
@@ -1054,7 +1063,25 @@ def _cli(argv=None):
|
|
|
1054
1063
|
except Exception as exc: # defensive: never a traceback-as-UX
|
|
1055
1064
|
print(json.dumps({"ok": False, "error": "verify failed: %s" % exc}))
|
|
1056
1065
|
return 2
|
|
1057
|
-
|
|
1066
|
+
# Additive: the three-property record (integrity / execution validity /
|
|
1067
|
+
# requirement coverage). Existing fields are untouched.
|
|
1068
|
+
vrec = None
|
|
1069
|
+
try:
|
|
1070
|
+
proof_dir = os.path.dirname(os.path.abspath(proof_path))
|
|
1071
|
+
vrec = build_for_proof(
|
|
1072
|
+
_load_proof(proof_path), repo_dir=repo_dir,
|
|
1073
|
+
loki_dir=os.path.dirname(os.path.dirname(proof_dir)),
|
|
1074
|
+
record_path=proof_path)
|
|
1075
|
+
result["verification"] = vrec
|
|
1076
|
+
except Exception as exc: # never a traceback-as-UX
|
|
1077
|
+
result["verification"] = {"error": "verification record unavailable: %s" % exc}
|
|
1078
|
+
if human:
|
|
1079
|
+
print(render_reasons(result))
|
|
1080
|
+
if vrec is not None:
|
|
1081
|
+
print("")
|
|
1082
|
+
print(render_compact(vrec))
|
|
1083
|
+
else:
|
|
1084
|
+
print(json.dumps(result, indent=2))
|
|
1058
1085
|
if result.get("ok"):
|
|
1059
1086
|
return 0
|
|
1060
1087
|
if drift_unverifiable(result):
|
|
@@ -1064,5 +1091,580 @@ def _cli(argv=None):
|
|
|
1064
1091
|
return 1
|
|
1065
1092
|
|
|
1066
1093
|
|
|
1094
|
+
# ===========================================================================
|
|
1095
|
+
# Verification record: what an outcome record proves, as three separate
|
|
1096
|
+
# properties (formerly autonomy/lib/verification_record.py; contract in
|
|
1097
|
+
# docs/PROOF-PASSPORT.md, "Verification record").
|
|
1098
|
+
#
|
|
1099
|
+
# (a) integrity: the record's hash (and signature, when checkable here).
|
|
1100
|
+
# (b) execution validity: the git revision (sha plus dirty flag) and
|
|
1101
|
+
# environment the checks ran against; a HEAD/tree mismatch is STALE.
|
|
1102
|
+
# (c) requirement coverage: <loki_dir>/state/requirements-map.json when
|
|
1103
|
+
# present. Absence is "not recorded", never "covered".
|
|
1104
|
+
# Per-check status is passed, failed, skipped, not_run or stale. The label never
|
|
1105
|
+
# reads as a bare pass when any check did not run. Used by verify.sh (evidence
|
|
1106
|
+
# "verification" block), `loki proof show`, `loki proof verify`, proof.ts and
|
|
1107
|
+
# proof-generator.py. CLI: proof-verify.py <label|summary|json> <file>.
|
|
1108
|
+
# ===========================================================================
|
|
1109
|
+
|
|
1110
|
+
SCHEMA_VERSION = "1.0"
|
|
1111
|
+
|
|
1112
|
+
PASSED, FAILED, SKIPPED, NOT_RUN, STALE = "passed", "failed", "skipped", "not_run", "stale"
|
|
1113
|
+
CHECK_STATUSES = (PASSED, FAILED, SKIPPED, NOT_RUN, STALE)
|
|
1114
|
+
|
|
1115
|
+
_PASS_WORDS = {"pass", "passed", "verified", "ok", "proven", "success", "completed"}
|
|
1116
|
+
_FAIL_WORDS = {"fail", "failed", "blocked", "weakened", "error", "findings"}
|
|
1117
|
+
_SKIP_WORDS = {"skipped", "disabled", "not_applicable", "n/a"}
|
|
1118
|
+
|
|
1119
|
+
# Single definition of "a test file"; proof-generator reads it via _PV.
|
|
1120
|
+
_TEST_FILE = re.compile(
|
|
1121
|
+
r"(^|/)(tests?|__tests__|spec|specs)/|\.(test|spec)\.[\w]+$"
|
|
1122
|
+
r"|(^|/)test_[^/]*\.py$|_test\.(py|go|rs)$|(^|/)conftest\.py$")
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def normalize_status(raw):
|
|
1126
|
+
"""Map any recorded status word to one of CHECK_STATUSES.
|
|
1127
|
+
|
|
1128
|
+
Unknown, empty and inconclusive all read not_run: a check that did not
|
|
1129
|
+
reach a conclusion has not run to completion, and must not read as passed.
|
|
1130
|
+
"""
|
|
1131
|
+
s = str(raw if raw is not None else "").strip().lower().replace("-", "_")
|
|
1132
|
+
if s in _PASS_WORDS:
|
|
1133
|
+
return PASSED
|
|
1134
|
+
if s in _FAIL_WORDS:
|
|
1135
|
+
return FAILED
|
|
1136
|
+
if s in _SKIP_WORDS:
|
|
1137
|
+
return SKIPPED
|
|
1138
|
+
if s == STALE:
|
|
1139
|
+
return STALE
|
|
1140
|
+
return NOT_RUN
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
# ---------------------------------------------------------------------------
|
|
1144
|
+
# git and environment helpers (explicit minimal env, bounded time)
|
|
1145
|
+
# ---------------------------------------------------------------------------
|
|
1146
|
+
|
|
1147
|
+
def _scoped_env():
|
|
1148
|
+
keep = ("PATH", "HOME", "LANG", "LC_ALL", "TMPDIR", "SYSTEMROOT")
|
|
1149
|
+
env = {k: os.environ[k] for k in keep if k in os.environ}
|
|
1150
|
+
env["GIT_TERMINAL_PROMPT"] = "0"
|
|
1151
|
+
return env
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def _run(argv, cwd=None, timeout=10):
|
|
1155
|
+
try:
|
|
1156
|
+
r = subprocess.run(argv, cwd=cwd, capture_output=True, text=True,
|
|
1157
|
+
timeout=timeout, env=_scoped_env())
|
|
1158
|
+
except (OSError, subprocess.SubprocessError, ValueError):
|
|
1159
|
+
return None
|
|
1160
|
+
if r.returncode != 0:
|
|
1161
|
+
return None
|
|
1162
|
+
return r.stdout
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def _vr_git_args(repo, args):
|
|
1166
|
+
"""Verification-record git call: scoped env, short timeout, raw paths."""
|
|
1167
|
+
return _git(repo or ".", args, timeout=10, env=_scoped_env(),
|
|
1168
|
+
config=("core.quotePath=false",))
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def git_head(repo):
|
|
1172
|
+
out = _vr_git_args(repo, ["rev-parse", "HEAD"])
|
|
1173
|
+
return out.strip() if out and out.strip() else ""
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def git_dirty(repo):
|
|
1177
|
+
"""True/False, or None when git cannot answer. .loki/ is excluded: the
|
|
1178
|
+
verifier writes its own artifacts there."""
|
|
1179
|
+
out = _vr_git_args(repo, ["status", "--porcelain"])
|
|
1180
|
+
if out is None:
|
|
1181
|
+
return None
|
|
1182
|
+
for line in out.splitlines():
|
|
1183
|
+
path = line[3:] if len(line) > 3 else line
|
|
1184
|
+
if path.startswith('"'):
|
|
1185
|
+
path = path.strip('"')
|
|
1186
|
+
if not (path.startswith(".loki/") or path == ".loki"):
|
|
1187
|
+
return True
|
|
1188
|
+
return False
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
def _tool_version(argv):
|
|
1192
|
+
out = _run(argv, timeout=3)
|
|
1193
|
+
if not out:
|
|
1194
|
+
return None
|
|
1195
|
+
line = out.strip().splitlines()[0] if out.strip() else ""
|
|
1196
|
+
return line[:60] or None
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def capture_environment():
|
|
1200
|
+
"""Measured, not assumed: only what this process can read right now."""
|
|
1201
|
+
env = {
|
|
1202
|
+
"os": "%s %s" % (platform.system(), platform.release()),
|
|
1203
|
+
"machine": platform.machine(),
|
|
1204
|
+
"python": platform.python_version(),
|
|
1205
|
+
}
|
|
1206
|
+
for key, argv in (("node", ["node", "--version"]),
|
|
1207
|
+
("bun", ["bun", "--version"]),
|
|
1208
|
+
("git", ["git", "--version"])):
|
|
1209
|
+
v = _tool_version(argv)
|
|
1210
|
+
if v:
|
|
1211
|
+
env[key] = v
|
|
1212
|
+
return env
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
def _tree_digest(repo):
|
|
1216
|
+
try:
|
|
1217
|
+
return compute_tree_digest(repo) or None
|
|
1218
|
+
except Exception:
|
|
1219
|
+
return None
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
# ---------------------------------------------------------------------------
|
|
1223
|
+
# (a) integrity
|
|
1224
|
+
# ---------------------------------------------------------------------------
|
|
1225
|
+
|
|
1226
|
+
def integrity_for_proof(proof):
|
|
1227
|
+
try:
|
|
1228
|
+
res = verify_integrity(proof)
|
|
1229
|
+
except Exception as exc: # never a traceback-as-UX
|
|
1230
|
+
return {"status": "unchecked", "hash_ok": None, "signature": "unchecked",
|
|
1231
|
+
"detail": "integrity check could not run: %s" % exc}
|
|
1232
|
+
verification = proof.get("verification") if isinstance(proof, dict) else None
|
|
1233
|
+
verification = verification if isinstance(verification, dict) else {}
|
|
1234
|
+
if not verification.get("hash"):
|
|
1235
|
+
status = "not_recorded"
|
|
1236
|
+
elif res.get("hash_ok") and res.get("gpg_ok") in (True, "n/a") \
|
|
1237
|
+
and res.get("headline_consistent") is not False \
|
|
1238
|
+
and res.get("cost_coherent") is not False:
|
|
1239
|
+
status = "valid"
|
|
1240
|
+
else:
|
|
1241
|
+
status = "invalid"
|
|
1242
|
+
gpg = res.get("gpg_ok")
|
|
1243
|
+
sig = "gpg verified" if gpg is True else "gpg FAILED" if gpg is False else None
|
|
1244
|
+
if sig is None:
|
|
1245
|
+
sig = ("ed25519 attestation present (not checked here; use loki proof "
|
|
1246
|
+
"verify --jwks)" if verification.get("attestation")
|
|
1247
|
+
or verification.get("seal") else "none")
|
|
1248
|
+
return {
|
|
1249
|
+
"status": status,
|
|
1250
|
+
"hash_ok": bool(res.get("hash_ok")),
|
|
1251
|
+
"signature": sig,
|
|
1252
|
+
"detail": "; ".join(res.get("reasons") or []) or "sha256 of canonical bytes matches",
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
def integrity_for_evidence(doc):
|
|
1257
|
+
return {"status": "not_recorded", "hash_ok": None, "signature": "none",
|
|
1258
|
+
"detail": "evidence.json carries no hash or signature"}
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
# ---------------------------------------------------------------------------
|
|
1262
|
+
# per-check extraction
|
|
1263
|
+
# ---------------------------------------------------------------------------
|
|
1264
|
+
|
|
1265
|
+
def checks_from_proof(proof):
|
|
1266
|
+
facts = proof.get("facts") if isinstance(proof, dict) else None
|
|
1267
|
+
if not isinstance(facts, dict):
|
|
1268
|
+
return []
|
|
1269
|
+
out = []
|
|
1270
|
+
|
|
1271
|
+
def add(cid, status, detail=""):
|
|
1272
|
+
out.append({"id": cid, "status": status, "detail": detail})
|
|
1273
|
+
|
|
1274
|
+
tests = facts.get("tests") or {}
|
|
1275
|
+
add("tests", normalize_status(tests.get("status")),
|
|
1276
|
+
"exit_code=%s" % tests.get("exit_code") if tests.get("command") else "no test command recorded")
|
|
1277
|
+
build = facts.get("build") or {}
|
|
1278
|
+
add("build", normalize_status(build.get("status")),
|
|
1279
|
+
"" if build.get("ran") else "build not run")
|
|
1280
|
+
for g in facts.get("quality_gates") or []:
|
|
1281
|
+
if isinstance(g, dict) and g.get("name"):
|
|
1282
|
+
add(str(g["name"]), normalize_status(g.get("status")),
|
|
1283
|
+
"provenance=%s" % g.get("provenance") if g.get("provenance") else "")
|
|
1284
|
+
sec = facts.get("security") or {}
|
|
1285
|
+
if sec.get("ran"):
|
|
1286
|
+
add("security", FAILED if (sec.get("high_active") or 0) > 0 else PASSED,
|
|
1287
|
+
"%s active HIGH finding(s)" % (sec.get("high_active") or 0))
|
|
1288
|
+
else:
|
|
1289
|
+
add("security", NOT_RUN, "security scan did not run")
|
|
1290
|
+
fn = facts.get("functional") or {}
|
|
1291
|
+
if isinstance(fn, dict) and fn:
|
|
1292
|
+
add("functional", normalize_status(fn.get("functional_status")) if fn.get("ran") else NOT_RUN, "")
|
|
1293
|
+
ti = facts.get("tests_integrity") or {}
|
|
1294
|
+
if ti.get("weakened"):
|
|
1295
|
+
add("tests_integrity", FAILED, "; ".join(ti["weakened"])[:200])
|
|
1296
|
+
return out
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
def checks_from_evidence(doc):
|
|
1300
|
+
out = []
|
|
1301
|
+
for g in (doc.get("deterministic_gates") or []) if isinstance(doc, dict) else []:
|
|
1302
|
+
if isinstance(g, dict) and g.get("gate"):
|
|
1303
|
+
out.append({"id": str(g["gate"]), "status": normalize_status(g.get("status")),
|
|
1304
|
+
"detail": str(g.get("summary") or "")})
|
|
1305
|
+
return out
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
# ---------------------------------------------------------------------------
|
|
1309
|
+
# (c) requirement coverage
|
|
1310
|
+
# ---------------------------------------------------------------------------
|
|
1311
|
+
|
|
1312
|
+
def read_requirements(loki_dir, checks):
|
|
1313
|
+
path = os.path.join(loki_dir or ".loki", "state", "requirements-map.json")
|
|
1314
|
+
if not os.path.isfile(path):
|
|
1315
|
+
return {"status": "not_recorded", "total": None, "covered": None,
|
|
1316
|
+
"uncovered": None, "uncovered_ids": [], "covered_but_check_not_passed": [],
|
|
1317
|
+
"detail": "no requirements map recorded; coverage is unknown, not zero"}
|
|
1318
|
+
try:
|
|
1319
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
1320
|
+
doc = json.load(fh)
|
|
1321
|
+
reqs = doc["requirements"]
|
|
1322
|
+
if not isinstance(reqs, list):
|
|
1323
|
+
raise ValueError("requirements is not a list")
|
|
1324
|
+
except (OSError, ValueError, KeyError, TypeError) as exc:
|
|
1325
|
+
return {"status": "unreadable", "total": None, "covered": None,
|
|
1326
|
+
"uncovered": None, "uncovered_ids": [], "covered_but_check_not_passed": [],
|
|
1327
|
+
"detail": "requirements map unreadable: %s" % exc}
|
|
1328
|
+
by_id = {c["id"]: c["status"] for c in checks}
|
|
1329
|
+
covered, uncovered_ids, weak = 0, [], []
|
|
1330
|
+
for r in reqs:
|
|
1331
|
+
r = r if isinstance(r, dict) else {}
|
|
1332
|
+
rid = str(r.get("id") or "?")
|
|
1333
|
+
if str(r.get("status") or "").lower() == "covered":
|
|
1334
|
+
covered += 1
|
|
1335
|
+
known = [by_id[c] for c in (r.get("checks") or []) if c in by_id]
|
|
1336
|
+
if known and any(s != PASSED for s in known):
|
|
1337
|
+
weak.append(rid)
|
|
1338
|
+
else:
|
|
1339
|
+
uncovered_ids.append(rid)
|
|
1340
|
+
return {"status": "recorded", "total": len(reqs), "covered": covered,
|
|
1341
|
+
"uncovered": len(uncovered_ids), "uncovered_ids": uncovered_ids,
|
|
1342
|
+
"covered_but_check_not_passed": weak, "detail": ""}
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
# ---------------------------------------------------------------------------
|
|
1346
|
+
# implementer-modified tests
|
|
1347
|
+
# ---------------------------------------------------------------------------
|
|
1348
|
+
|
|
1349
|
+
def modified_tests(repo, base, head=None):
|
|
1350
|
+
"""Existing test files the change modified, deleted or renamed.
|
|
1351
|
+
|
|
1352
|
+
Added test files are not listed: adding a test does not weaken one. Status
|
|
1353
|
+
is "unknown" when the diff cannot be derived, never an empty "none".
|
|
1354
|
+
"""
|
|
1355
|
+
if not base:
|
|
1356
|
+
return {"status": "unknown", "files": [], "detail": "no base revision recorded"}
|
|
1357
|
+
args = ["diff", "--name-status", "-M", "-z", base]
|
|
1358
|
+
if head:
|
|
1359
|
+
args.append(head)
|
|
1360
|
+
out = _vr_git_args(repo, args + ["--"])
|
|
1361
|
+
if out is None:
|
|
1362
|
+
return {"status": "unknown", "files": [], "detail": "git diff could not be computed"}
|
|
1363
|
+
toks = out.split("\0")
|
|
1364
|
+
files, i = [], 0
|
|
1365
|
+
while i < len(toks) and toks[i]:
|
|
1366
|
+
st = toks[i][:1]
|
|
1367
|
+
if st in "RC" and i + 2 < len(toks):
|
|
1368
|
+
old, new, i = toks[i + 1], toks[i + 2], i + 3
|
|
1369
|
+
else:
|
|
1370
|
+
old = new = toks[i + 1] if i + 1 < len(toks) else ""
|
|
1371
|
+
i += 2
|
|
1372
|
+
if st in ("M", "D", "R", "T") and _TEST_FILE.search(old):
|
|
1373
|
+
files.append({"path": old, "change": {"M": "modified", "D": "deleted",
|
|
1374
|
+
"R": "renamed", "T": "type-changed"}[st]})
|
|
1375
|
+
return {"status": "measured", "files": files, "detail": ""}
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
# ---------------------------------------------------------------------------
|
|
1379
|
+
# (b) execution validity
|
|
1380
|
+
# ---------------------------------------------------------------------------
|
|
1381
|
+
|
|
1382
|
+
def _short(sha):
|
|
1383
|
+
return (sha or "")[:7] or "none"
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
def execution_validity(recorded, repo_dir, environment=None):
|
|
1387
|
+
"""Compare the recorded revision with the live one.
|
|
1388
|
+
|
|
1389
|
+
recorded: {"head_sha", "dirty", "tree_sha256"} as stored with the record.
|
|
1390
|
+
status: fresh | stale | unknown. Unknown is never promoted to fresh.
|
|
1391
|
+
"""
|
|
1392
|
+
cur_head = git_head(repo_dir)
|
|
1393
|
+
cur_dirty = git_dirty(repo_dir) if cur_head else None
|
|
1394
|
+
rec_head = recorded.get("head_sha") or ""
|
|
1395
|
+
rec_tree = recorded.get("tree_sha256") or ""
|
|
1396
|
+
result = {"status": "unknown", "reason": "", "recorded": dict(recorded),
|
|
1397
|
+
"current": {"head_sha": cur_head, "dirty": cur_dirty},
|
|
1398
|
+
"environment": environment or {}}
|
|
1399
|
+
if not rec_head:
|
|
1400
|
+
result["reason"] = "record stores no git revision, so freshness cannot be judged"
|
|
1401
|
+
elif not cur_head:
|
|
1402
|
+
result["reason"] = "current directory is not a git work tree, so freshness cannot be judged"
|
|
1403
|
+
elif rec_head != cur_head:
|
|
1404
|
+
result["status"] = "stale"
|
|
1405
|
+
result["reason"] = "HEAD moved: recorded %s, now %s" % (_short(rec_head), _short(cur_head))
|
|
1406
|
+
elif rec_tree:
|
|
1407
|
+
now_tree = _tree_digest(repo_dir)
|
|
1408
|
+
result["current"]["tree_sha256"] = now_tree
|
|
1409
|
+
if not now_tree:
|
|
1410
|
+
result["reason"] = "tree digest could not be recomputed"
|
|
1411
|
+
elif now_tree != rec_tree:
|
|
1412
|
+
result["status"] = "stale"
|
|
1413
|
+
result["reason"] = "working tree differs from the tree the checks ran against"
|
|
1414
|
+
else:
|
|
1415
|
+
result["status"] = "fresh"
|
|
1416
|
+
elif recorded.get("dirty") is False and cur_dirty is False:
|
|
1417
|
+
result["status"] = "fresh"
|
|
1418
|
+
elif recorded.get("dirty") is False and cur_dirty is True:
|
|
1419
|
+
result["status"] = "stale"
|
|
1420
|
+
result["reason"] = "working tree modified since the checks ran"
|
|
1421
|
+
else:
|
|
1422
|
+
result["reason"] = ("same HEAD, but the checks ran on a dirty tree and no tree "
|
|
1423
|
+
"digest was recorded, so the tree cannot be compared")
|
|
1424
|
+
return result
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
# ---------------------------------------------------------------------------
|
|
1428
|
+
# assembly and label
|
|
1429
|
+
# ---------------------------------------------------------------------------
|
|
1430
|
+
|
|
1431
|
+
def _counts(checks):
|
|
1432
|
+
c = {s: 0 for s in CHECK_STATUSES}
|
|
1433
|
+
for ch in checks:
|
|
1434
|
+
c[ch["status"]] += 1
|
|
1435
|
+
c["total"] = len(checks)
|
|
1436
|
+
c["ran"] = c[PASSED] + c[FAILED]
|
|
1437
|
+
return c
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
def _names(checks, *statuses):
|
|
1441
|
+
return [c["id"] for c in checks if c["status"] in statuses]
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
def compute_label(base, integrity, execution, checks, counts, requirements, mod_tests, complete,
|
|
1445
|
+
kind="proof"):
|
|
1446
|
+
base = (base or "").strip().upper()
|
|
1447
|
+
failed = _names(checks, FAILED)
|
|
1448
|
+
if integrity["status"] == "invalid":
|
|
1449
|
+
return "NOT VERIFIED (integrity failed: record was altered or is inconsistent)"
|
|
1450
|
+
if failed:
|
|
1451
|
+
return "NOT VERIFIED (failed: %s)" % ", ".join(failed)
|
|
1452
|
+
if not complete:
|
|
1453
|
+
return "INCOMPLETE (record has no usable check results)"
|
|
1454
|
+
if kind == "proof" and integrity["status"] != "valid":
|
|
1455
|
+
if integrity["status"] == "unchecked":
|
|
1456
|
+
return "NOT VERIFIED (integrity could not be checked)"
|
|
1457
|
+
return "NOT VERIFIED (integrity not provable: no hash recorded)"
|
|
1458
|
+
if base not in ("VERIFIED", "VERIFIED WITH GAPS"):
|
|
1459
|
+
return base or "NOT VERIFIED"
|
|
1460
|
+
if execution["status"] == "stale":
|
|
1461
|
+
return "STALE (%s; re-run the checks)" % execution["reason"]
|
|
1462
|
+
not_run = _names(checks, NOT_RUN, SKIPPED, STALE)
|
|
1463
|
+
parts = []
|
|
1464
|
+
if kind != "proof" and integrity["status"] != "valid":
|
|
1465
|
+
parts.append("integrity not recorded")
|
|
1466
|
+
if not_run:
|
|
1467
|
+
parts.append("%d of %d checks ran; not run: %s" % (
|
|
1468
|
+
counts["ran"], counts["total"], ", ".join(not_run)))
|
|
1469
|
+
if base == "VERIFIED WITH GAPS" and not not_run:
|
|
1470
|
+
parts.append("recorded gaps")
|
|
1471
|
+
if requirements["status"] == "recorded" and requirements["uncovered"]:
|
|
1472
|
+
parts.append("%d requirement(s) uncovered" % requirements["uncovered"])
|
|
1473
|
+
if requirements.get("covered_but_check_not_passed"):
|
|
1474
|
+
parts.append("covered by a check that did not pass: %s" %
|
|
1475
|
+
", ".join(requirements["covered_but_check_not_passed"]))
|
|
1476
|
+
if mod_tests["files"]:
|
|
1477
|
+
parts.append("%d existing test file(s) modified" % len(mod_tests["files"]))
|
|
1478
|
+
if execution["status"] == "unknown":
|
|
1479
|
+
parts.append("revision freshness unknown")
|
|
1480
|
+
if parts:
|
|
1481
|
+
return "VERIFIED (scoped: %s)" % "; ".join(parts)
|
|
1482
|
+
return "VERIFIED"
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
def _assemble(kind, base, integrity, execution, checks, requirements, mod_tests,
|
|
1486
|
+
complete, record_path):
|
|
1487
|
+
if execution["status"] == "stale":
|
|
1488
|
+
for c in checks:
|
|
1489
|
+
if c["status"] == PASSED:
|
|
1490
|
+
c["status"] = STALE
|
|
1491
|
+
counts = _counts(checks)
|
|
1492
|
+
# Label uses pre-stale statuses for "not run" names: recompute from the
|
|
1493
|
+
# current list, where stale checks are named as not run on a stale record.
|
|
1494
|
+
label = compute_label(base, integrity, execution, checks, counts,
|
|
1495
|
+
requirements, mod_tests, complete, kind)
|
|
1496
|
+
return {
|
|
1497
|
+
"schema_version": SCHEMA_VERSION,
|
|
1498
|
+
"kind": kind,
|
|
1499
|
+
"label": label,
|
|
1500
|
+
"base_verdict": base,
|
|
1501
|
+
"integrity": integrity,
|
|
1502
|
+
"execution": execution,
|
|
1503
|
+
"requirements": requirements,
|
|
1504
|
+
"checks": checks,
|
|
1505
|
+
"counts": counts,
|
|
1506
|
+
"modified_tests": mod_tests,
|
|
1507
|
+
"complete": complete,
|
|
1508
|
+
"record_path": record_path,
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
def build_for_proof(proof, repo_dir=".", loki_dir=".loki", record_path=""):
|
|
1513
|
+
proof = proof if isinstance(proof, dict) else {}
|
|
1514
|
+
facts = proof.get("facts") if isinstance(proof.get("facts"), dict) else {}
|
|
1515
|
+
git = facts.get("git") if isinstance(facts.get("git"), dict) else {}
|
|
1516
|
+
recorded = {"head_sha": git.get("head_sha") or "", "dirty": git.get("dirty"),
|
|
1517
|
+
"tree_sha256": git.get("tree_sha256") or ""}
|
|
1518
|
+
env = facts.get("environment") if isinstance(facts.get("environment"), dict) else {}
|
|
1519
|
+
checks = checks_from_proof(proof)
|
|
1520
|
+
base = str((proof.get("honesty") or {}).get("headline") or "")
|
|
1521
|
+
complete = bool(facts) and bool(base)
|
|
1522
|
+
mod = modified_tests(repo_dir, git.get("base_sha") or "")
|
|
1523
|
+
if mod["status"] == "unknown" and isinstance(facts.get("tests_integrity"), dict):
|
|
1524
|
+
weak = [w for w in facts["tests_integrity"].get("weakened") or []
|
|
1525
|
+
if str(w).startswith("test file")]
|
|
1526
|
+
if weak:
|
|
1527
|
+
mod = {"status": "measured", "files": [{"path": w, "change": "recorded"} for w in weak],
|
|
1528
|
+
"detail": "from the receipt's tests_integrity"}
|
|
1529
|
+
return _assemble("proof", base, integrity_for_proof(proof),
|
|
1530
|
+
execution_validity(recorded, repo_dir, env), checks,
|
|
1531
|
+
read_requirements(loki_dir, checks), mod, complete, record_path)
|
|
1532
|
+
|
|
1533
|
+
|
|
1534
|
+
def build_for_evidence(doc, repo_dir=".", loki_dir=".loki", record_path="", fresh_capture=False):
|
|
1535
|
+
"""Build the record for an evidence.json document.
|
|
1536
|
+
|
|
1537
|
+
fresh_capture=True is the emit-time call: the checks just ran here, so the
|
|
1538
|
+
revision and environment are captured from the live tree. Otherwise the
|
|
1539
|
+
revision recorded in doc["verification"] (or doc["subject"]) is compared
|
|
1540
|
+
with the live tree.
|
|
1541
|
+
"""
|
|
1542
|
+
doc = doc if isinstance(doc, dict) else {}
|
|
1543
|
+
subject = doc.get("subject") if isinstance(doc.get("subject"), dict) else {}
|
|
1544
|
+
prior = doc.get("verification") if isinstance(doc.get("verification"), dict) else {}
|
|
1545
|
+
checks = checks_from_evidence(doc)
|
|
1546
|
+
base = str(doc.get("verdict") or "")
|
|
1547
|
+
complete = bool(checks) and bool(base)
|
|
1548
|
+
if fresh_capture:
|
|
1549
|
+
recorded = {"head_sha": subject.get("head_sha") or git_head(repo_dir),
|
|
1550
|
+
"dirty": not subject.get("worktree_clean_at_grade", False),
|
|
1551
|
+
"tree_sha256": _tree_digest(repo_dir) or ""}
|
|
1552
|
+
env = capture_environment()
|
|
1553
|
+
else:
|
|
1554
|
+
rec = (prior.get("execution") or {}).get("recorded") or {}
|
|
1555
|
+
recorded = {"head_sha": rec.get("head_sha") or subject.get("head_sha") or "",
|
|
1556
|
+
"dirty": rec.get("dirty") if "dirty" in rec
|
|
1557
|
+
else (not subject.get("worktree_clean_at_grade", False)),
|
|
1558
|
+
"tree_sha256": rec.get("tree_sha256") or ""}
|
|
1559
|
+
env = (prior.get("execution") or {}).get("environment") or {}
|
|
1560
|
+
mod = modified_tests(repo_dir, subject.get("merge_base_sha") or "",
|
|
1561
|
+
subject.get("head_sha") or None)
|
|
1562
|
+
return _assemble("evidence", base, integrity_for_evidence(doc),
|
|
1563
|
+
execution_validity(recorded, repo_dir, env), checks,
|
|
1564
|
+
read_requirements(loki_dir, checks), mod, complete, record_path)
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
# ---------------------------------------------------------------------------
|
|
1568
|
+
# compact reviewer summary
|
|
1569
|
+
# ---------------------------------------------------------------------------
|
|
1570
|
+
|
|
1571
|
+
def _list(items, limit=4):
|
|
1572
|
+
items = list(items)
|
|
1573
|
+
shown = ", ".join(items[:limit])
|
|
1574
|
+
return shown + (" (+%d more)" % (len(items) - limit) if len(items) > limit else "")
|
|
1575
|
+
|
|
1576
|
+
|
|
1577
|
+
def render_compact(rec):
|
|
1578
|
+
ex, integ, req, cnt = rec["execution"], rec["integrity"], rec["requirements"], rec["counts"]
|
|
1579
|
+
cur, recd = ex["current"], ex["recorded"]
|
|
1580
|
+
dirty = {True: "dirty", False: "clean", None: "dirty flag unknown"}[recd.get("dirty")]
|
|
1581
|
+
lines = [rec["label"]]
|
|
1582
|
+
if ex["status"] == "fresh":
|
|
1583
|
+
lines.append("revision: %s (%s), fresh: HEAD and tree match" % (_short(recd.get("head_sha")), dirty))
|
|
1584
|
+
elif ex["status"] == "stale":
|
|
1585
|
+
lines.append("revision: %s (%s), STALE: %s" % (_short(recd.get("head_sha")), dirty, ex["reason"]))
|
|
1586
|
+
else:
|
|
1587
|
+
lines.append("revision: %s (%s), freshness unknown: %s" % (
|
|
1588
|
+
_short(recd.get("head_sha")), dirty, ex["reason"]))
|
|
1589
|
+
env = ex.get("environment") or {}
|
|
1590
|
+
lines.append("environment: %s" % (", ".join("%s %s" % (k, v) for k, v in env.items())
|
|
1591
|
+
if env else "not recorded"))
|
|
1592
|
+
lines.append("integrity: %s (%s)" % (integ["status"], integ["signature"]))
|
|
1593
|
+
lines.append("checks: %d passed, %d failed, %d skipped, %d not run, %d stale (of %d)" % (
|
|
1594
|
+
cnt[PASSED], cnt[FAILED], cnt[SKIPPED], cnt[NOT_RUN], cnt[STALE], cnt["total"]))
|
|
1595
|
+
if rec["checks"]:
|
|
1596
|
+
bad = _names(rec["checks"], FAILED, NOT_RUN, SKIPPED, STALE)
|
|
1597
|
+
if bad:
|
|
1598
|
+
lines.append(" not passed: %s" % _list(bad, 6))
|
|
1599
|
+
if req["status"] == "recorded":
|
|
1600
|
+
extra = " (uncovered: %s)" % _list(req["uncovered_ids"]) if req["uncovered"] else ""
|
|
1601
|
+
lines.append("requirements: %d covered, %d uncovered%s" % (req["covered"], req["uncovered"], extra))
|
|
1602
|
+
else:
|
|
1603
|
+
lines.append("requirements: %s" % req["status"].replace("_", " "))
|
|
1604
|
+
mt = rec["modified_tests"]
|
|
1605
|
+
if mt["status"] == "unknown":
|
|
1606
|
+
lines.append("modified tests: unknown (%s)" % mt["detail"])
|
|
1607
|
+
elif mt["files"]:
|
|
1608
|
+
lines.append("modified tests: %d existing test file(s): %s" % (
|
|
1609
|
+
len(mt["files"]), _list("%s %s" % (f["change"], f["path"]) for f in mt["files"])))
|
|
1610
|
+
else:
|
|
1611
|
+
lines.append("modified tests: none")
|
|
1612
|
+
lines.append("A signature proves integrity only, not execution validity or requirement coverage.")
|
|
1613
|
+
if rec.get("record_path"):
|
|
1614
|
+
lines.append("full record: %s" % rec["record_path"])
|
|
1615
|
+
return "\n".join(lines)
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
# ---------------------------------------------------------------------------
|
|
1619
|
+
# CLI
|
|
1620
|
+
# ---------------------------------------------------------------------------
|
|
1621
|
+
|
|
1622
|
+
def _vr_cli(argv):
|
|
1623
|
+
usage = ("usage: proof-verify.py <label|summary|json> <proof.json|evidence.json> "
|
|
1624
|
+
"[--repo DIR] [--loki-dir DIR] [--embedded]")
|
|
1625
|
+
args = [a for a in argv]
|
|
1626
|
+
# --embedded: render the record already stored in evidence.json instead of
|
|
1627
|
+
# re-deriving it (used right after emit, when re-deriving adds nothing).
|
|
1628
|
+
embedded = "--embedded" in args
|
|
1629
|
+
args = [a for a in args if a != "--embedded"]
|
|
1630
|
+
if len(args) < 2 or args[0] not in ("label", "summary", "json"):
|
|
1631
|
+
sys.stderr.write(usage + "\n")
|
|
1632
|
+
return 2
|
|
1633
|
+
mode, path = args[0], args[1]
|
|
1634
|
+
repo, loki_dir = ".", None
|
|
1635
|
+
rest = args[2:]
|
|
1636
|
+
while rest:
|
|
1637
|
+
flag = rest.pop(0)
|
|
1638
|
+
if flag == "--repo" and rest:
|
|
1639
|
+
repo = rest.pop(0)
|
|
1640
|
+
elif flag == "--loki-dir" and rest:
|
|
1641
|
+
loki_dir = rest.pop(0)
|
|
1642
|
+
else:
|
|
1643
|
+
sys.stderr.write(usage + "\n")
|
|
1644
|
+
return 2
|
|
1645
|
+
try:
|
|
1646
|
+
doc = _load_proof(path)
|
|
1647
|
+
except ProofLoadError as exc:
|
|
1648
|
+
sys.stderr.write("cannot read %s: %s\n" % (path, exc))
|
|
1649
|
+
return 2
|
|
1650
|
+
if loki_dir is None:
|
|
1651
|
+
loki_dir = os.environ.get("LOKI_DIR") or os.path.join(repo, ".loki")
|
|
1652
|
+
is_evidence = isinstance(doc, dict) and "deterministic_gates" in doc
|
|
1653
|
+
if embedded and is_evidence and isinstance(doc.get("verification"), dict) \
|
|
1654
|
+
and doc["verification"].get("checks") is not None:
|
|
1655
|
+
rec = dict(doc["verification"])
|
|
1656
|
+
rec["record_path"] = path
|
|
1657
|
+
else:
|
|
1658
|
+
rec = (build_for_evidence(doc, repo, loki_dir, path) if is_evidence
|
|
1659
|
+
else build_for_proof(doc, repo, loki_dir, path))
|
|
1660
|
+
if mode == "label":
|
|
1661
|
+
print(rec["label"])
|
|
1662
|
+
elif mode == "json":
|
|
1663
|
+
print(json.dumps(rec, indent=2))
|
|
1664
|
+
else:
|
|
1665
|
+
print(render_compact(rec))
|
|
1666
|
+
return 0
|
|
1667
|
+
|
|
1668
|
+
|
|
1067
1669
|
if __name__ == "__main__":
|
|
1068
1670
|
sys.exit(_cli())
|
|
@@ -66,6 +66,22 @@ except Exception:
|
|
|
66
66
|
*) if [ -n "$files" ] && [ "$files" != "0" ]; then next="loki undo"; else next="loki doctor"; fi ;;
|
|
67
67
|
esac
|
|
68
68
|
|
|
69
|
+
# REQTRACE: requirements with no passing check, from .loki/state/requirements-map.json (written by the Bun runner and engine10).
|
|
70
|
+
local unc=""
|
|
71
|
+
if [ -s "$loki_dir/state/requirements-map.json" ]; then
|
|
72
|
+
unc="$(LOKI_RM="$loki_dir/state/requirements-map.json" python3 -I -c "
|
|
73
|
+
import json, os
|
|
74
|
+
try:
|
|
75
|
+
m = json.load(open(os.environ['LOKI_RM']))
|
|
76
|
+
n = sum(1 for r in m.get('requirements', []) if r.get('status') == 'uncovered')
|
|
77
|
+
print(n if n else '')
|
|
78
|
+
except Exception:
|
|
79
|
+
print('')
|
|
80
|
+
" 2>/dev/null || true)"
|
|
81
|
+
fi
|
|
82
|
+
local next_h="$next"
|
|
83
|
+
[ -n "$unc" ] && next_h="${next}; ${unc} requirement(s) uncovered, see: loki plan --requirements"
|
|
84
|
+
|
|
69
85
|
local mm=$((elapsed / 60)) ss=$((elapsed % 60)) elapsed_h
|
|
70
86
|
if [ "$elapsed" -lt 60 ]; then elapsed_h="${elapsed}s"; else elapsed_h="${mm}m$(printf '%02d' "$ss")s"; fi
|
|
71
87
|
|
|
@@ -92,5 +108,5 @@ except Exception:
|
|
|
92
108
|
printf 'Cost: %s\n' "$cost_h"
|
|
93
109
|
printf 'Elapsed: %s\n' "$elapsed_h"
|
|
94
110
|
[ -n "$receipt" ] && printf 'Receipt: %s\n' "$receipt"
|
|
95
|
-
printf 'next: %s (or: loki chat --attach)\n' "$
|
|
111
|
+
printf 'next: %s (or: loki chat --attach)\n' "$next_h"
|
|
96
112
|
}
|