loki-mode 9.2.0 → 9.4.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.
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env python3
2
+ """Write ONE self-describing evidence file a third party can check.
3
+
4
+ WHY THIS EXISTS. Every piece is already here and none of them leaves the
5
+ machine. `receipt-bundle.py` rolls a workspace up into a verdict, but it prints
6
+ to a terminal and its output means nothing an hour later. `receipt-attest.py`
7
+ emits a portable per-receipt attestation, but ONE receipt at a time, and the
8
+ caller has to know which files to feed it. Handing a reviewer "run these two
9
+ tools in this order against a directory you do not have" is not evidence; it is
10
+ homework.
11
+
12
+ So this produces a single FILE: every receipt in the workspace, each with the
13
+ verdict THIS MACHINE computed, plus a statement of what the file does not
14
+ prove. One artifact, self-describing, checkable by someone who was never here.
15
+
16
+ THE RULE THAT MAKES IT EVIDENCE RATHER THAN MARKETING:
17
+
18
+ NEVER EXPORT A VERDICT THIS MACHINE DID NOT COMPUTE.
19
+
20
+ Every per-receipt verdict and every per-axis state comes from attest(), which
21
+ calls verify() in autonomy/lib/proof-verify.py. Nothing here decides whether a
22
+ receipt is good. An axis that could not be checked is exported as UNVERIFIABLE
23
+ carrying the verifier's own reason -- never omitted, never rounded up to
24
+ VERIFIED. An export claiming more than was checked is not an optimistic report,
25
+ it is a FORGED CREDENTIAL: the reader's entire reason for trusting the file is
26
+ that it reports only what was measured, and one upgraded axis makes every other
27
+ line in it worthless too.
28
+
29
+ The specific way that forgery happens by accident is OMISSION. Dropping the
30
+ receipts that failed or could not be read leaves a file where every line says
31
+ VERIFIED, and the reader cannot tell whether that is because the run was clean
32
+ or because the bad ones were filtered out. Absent is not clean. So the export
33
+ carries every receipt discovered, and the counts are of what was DISCOVERED,
34
+ not of what survived.
35
+
36
+ WHAT THIS FILE DOES NOT PROVE, and it is said in the export body rather than in
37
+ documentation the reader will never see:
38
+
39
+ An UNSIGNED receipt proves INTEGRITY, not ORIGIN.
40
+
41
+ Integrity means the recorded bytes were not edited after they were hashed.
42
+ Origin means a particular machine produced them. Without a gpg signature the
43
+ generator is trusted, so a forger who rewrites the facts AND the headline
44
+ consistently and recomputes the hash passes every check in here. proof-verify's
45
+ own docstring says so outright. A reader who takes an unsigned VERIFIED as
46
+ proof of provenance has been misled, and the only place that caveat reliably
47
+ reaches them is inside the artifact.
48
+
49
+ Reuse, not reimplementation:
50
+ - verify() autonomy/lib/proof-verify.py, via attest() below
51
+ - attest() tools/receipt-attest.py -- the per-axis three-state
52
+ projection, already written and already tested
53
+ - find_receipts() tools/receipt-bundle.py -- the discovery convention, so an
54
+ export covers exactly what an audit covers
55
+ - rollup() tools/receipt-bundle.py -- weakest-link, never an average
56
+
57
+ Usage:
58
+ tools/receipt-export.py [workspace] [--out evidence.json] [--force] [--json]
59
+
60
+ Exit codes (tests/test_tool_exit_contract.py enforces the convention):
61
+ 0 every receipt exported VERIFIED
62
+ 1 a receipt was checked and FAILED
63
+ 2 nothing failed, but a receipt was UNVERIFIABLE
64
+ 3 no receipts found -- nothing exported, and an empty export is not a
65
+ passing audit
66
+ 64 usage error, including refusing to overwrite without --force
67
+ 66 the workspace does not exist
68
+ """
69
+
70
+ import argparse
71
+ import importlib.util
72
+ import json
73
+ import os
74
+ import pathlib
75
+ import sys
76
+
77
+ # A stale .pyc for a hyphenated module loaded by path makes mutation probes
78
+ # report FALSE failures (the probe edits the source, the loader serves the old
79
+ # bytecode). Must be set before any loader below runs.
80
+ sys.dont_write_bytecode = True
81
+
82
+ _ROOT = pathlib.Path(__file__).resolve().parents[1]
83
+ _TOOLS = _ROOT / "tools"
84
+
85
+
86
+ def _load(name, path):
87
+ spec = importlib.util.spec_from_file_location(name, path)
88
+ mod = importlib.util.module_from_spec(spec)
89
+ spec.loader.exec_module(mod)
90
+ return mod
91
+
92
+
93
+ _attest = _load("receipt_attest", _TOOLS / "receipt-attest.py")
94
+ _bundle = _load("receipt_bundle", _TOOLS / "receipt-bundle.py")
95
+
96
+ # The single source of every verdict in the export. attest() calls verify().
97
+ attest = _attest.attest
98
+ # The discovery convention, imported so an export covers exactly what an audit
99
+ # covers. A second glob here is how the two drift apart and a receipt becomes
100
+ # auditable but not exportable.
101
+ find_receipts = _bundle.find_receipts
102
+ # Weakest-link, imported rather than restated. One FAILED receipt sinks the
103
+ # export no matter how many VERIFIED ones surround it.
104
+ rollup = _bundle.rollup
105
+ measured_cost = _bundle.measured_cost
106
+ _pv = _bundle._pv
107
+
108
+ VERIFIED = "VERIFIED"
109
+ FAILED = "FAILED"
110
+ UNVERIFIABLE = "UNVERIFIABLE"
111
+ EMPTY = "EMPTY"
112
+
113
+ EXIT_OK = 0
114
+ EXIT_FAILED = 1
115
+ EXIT_UNVERIFIABLE = 2
116
+ EXIT_EMPTY = 3
117
+ EXIT_USAGE = 64
118
+ EXIT_INPUT_MISSING = 66
119
+
120
+ EXIT = {VERIFIED: EXIT_OK, FAILED: EXIT_FAILED,
121
+ UNVERIFIABLE: EXIT_UNVERIFIABLE, EMPTY: EXIT_EMPTY}
122
+
123
+ # Stated in the artifact, not in docs the reader never opens. Each line is a
124
+ # claim the export does NOT support, phrased as the reader would be tempted to
125
+ # over-read it.
126
+ LIMITS = [
127
+ "An UNSIGNED receipt proves INTEGRITY, not ORIGIN: the recorded bytes were "
128
+ "not edited after they were hashed, but nothing here shows WHICH machine "
129
+ "produced them. A forger who rewrites the facts and the headline "
130
+ "consistently and recomputes the hash passes every check in this file. "
131
+ "Only a gpg signature from a key you already trust proves origin.",
132
+
133
+ "UNVERIFIABLE is not a soft pass. It means this machine could NOT check "
134
+ "that axis, most often because the recorded diff cannot be re-derived "
135
+ "outside the original repository. Read it as unchecked, never as clean.",
136
+
137
+ "This export covers only the receipts DISCOVERED under the workspace. It "
138
+ "cannot show a run that never wrote a receipt, so it is evidence about "
139
+ "what was recorded, not proof that nothing else happened.",
140
+
141
+ "Every verdict here was computed by the exporting machine at export time. "
142
+ "Re-run the verifier yourself if you do not trust that machine; this file "
143
+ "reports what was checked, it does not replace checking.",
144
+ ]
145
+
146
+
147
+ def _cost_of(path):
148
+ """The receipt's cost, or None when it was not MEASURED.
149
+
150
+ measured_cost() reaches record_is_measured() in autonomy/lib/
151
+ efficiency_cost.py, which is the single definition of "measured" in this
152
+ repo. Restating it here is how the honesty rule drifts.
153
+ """
154
+ try:
155
+ cost = measured_cost(_pv._load_proof(str(path)))
156
+ except Exception:
157
+ return None
158
+ if cost is None:
159
+ return None
160
+ # `is None`, never falsy: a run that genuinely measured $0.00 is a real
161
+ # observation and must survive as 0, not be erased into "unmeasured".
162
+ return cost.get("cost_usd")
163
+
164
+
165
+ def _receipt_entry(path, repo_dir):
166
+ """One receipt, verdict and axes straight from attest(). No judgement here.
167
+
168
+ attest() already keeps VERIFIED / FAILED / UNVERIFIABLE apart per axis and
169
+ refuses to collapse the third into either neighbour. Re-deriving any of it
170
+ would be a second opinion this file is not entitled to hold.
171
+ """
172
+ record = attest(str(path), repo_dir)
173
+
174
+ axes = {}
175
+ for name, axis in (record.get("axes") or {}).items():
176
+ # Carried whole, including the reason. An UNVERIFIABLE axis with its
177
+ # reason stripped is indistinguishable from one nobody thought about.
178
+ axes[name] = {"state": axis["state"], "reason": axis.get("reason", "")}
179
+
180
+ signature = record.get("signature") or {}
181
+
182
+ return {
183
+ "path": str(path),
184
+ "receipt_sha256": record.get("receipt_sha256"),
185
+ "verdict": record.get("verdict"),
186
+ "summary": record.get("summary"),
187
+ "axes": axes,
188
+ "signature": {
189
+ "state": signature.get("state"),
190
+ "status": signature.get("status"),
191
+ "reason": signature.get("reason", ""),
192
+ },
193
+ # True means no valid signature vouches for the facts, so they are
194
+ # taken at face value. Carried from verify() rather than inferred.
195
+ "generator_trusted": bool(record.get("generator_trusted")),
196
+ "cost_usd": _cost_of(path),
197
+ }
198
+
199
+
200
+ def export(workspace, repo_dir="."):
201
+ """Build the evidence record. Pure: reads only, writes nothing."""
202
+ paths = find_receipts(workspace)
203
+ receipts = [_receipt_entry(p, repo_dir) for p in paths]
204
+
205
+ verdict = rollup([r["verdict"] for r in receipts])
206
+
207
+ measured = [r["cost_usd"] for r in receipts if r["cost_usd"] is not None]
208
+ cost = {
209
+ "measured_receipts": len(measured),
210
+ "total_receipts": len(receipts),
211
+ # UNKNOWN when nothing measured, never 0.0. Unmeasured and free are
212
+ # different claims and only one of them is honest. The COUNT decides,
213
+ # not the sum, so a genuine total of $0.00 still reports as a number.
214
+ "total_usd": sum(measured) if measured else None,
215
+ }
216
+
217
+ unsigned_n = sum(1 for r in receipts
218
+ if r["signature"].get("status") == "unsigned")
219
+
220
+ return {
221
+ "export": "loki-evidence-export/v1",
222
+ "workspace": os.path.abspath(str(workspace)),
223
+ "checked_from": os.path.abspath(repo_dir),
224
+ # Counts of what was DISCOVERED. If these disagree with len(receipts),
225
+ # something was dropped, and a reader can see it without trusting us.
226
+ "counts": {
227
+ state: sum(1 for r in receipts if r["verdict"] == state)
228
+ for state in (FAILED, UNVERIFIABLE, VERIFIED)
229
+ },
230
+ "discovered": len(receipts),
231
+ "receipts": receipts,
232
+ "cost": cost,
233
+ "unsigned_receipts": unsigned_n,
234
+ "verdict": verdict,
235
+ "does_not_prove": LIMITS,
236
+ "summary": _summary(verdict, receipts, cost, unsigned_n),
237
+ }
238
+
239
+
240
+ def _cost_line(cost):
241
+ if cost["total_usd"] is None:
242
+ return "total cost UNKNOWN (0 of %d receipts measured cost)" % (
243
+ cost["total_receipts"],)
244
+ return "total cost $%.4f across %d of %d receipts measured" % (
245
+ cost["total_usd"], cost["measured_receipts"], cost["total_receipts"])
246
+
247
+
248
+ def _summary(verdict, receipts, cost, unsigned_n):
249
+ if verdict == EMPTY:
250
+ return ("EMPTY -- no receipts found under this workspace. Nothing was "
251
+ "exported, because an export with no evidence in it is not a "
252
+ "passing audit.")
253
+ n = len(receipts)
254
+ bad = sum(1 for r in receipts if r["verdict"] == FAILED)
255
+ unv = sum(1 for r in receipts if r["verdict"] == UNVERIFIABLE)
256
+ if verdict == VERIFIED:
257
+ head = "VERIFIED -- all %d receipts verified here" % n
258
+ elif verdict == FAILED:
259
+ head = ("FAILED -- %d of %d receipts FAILED verification; this export "
260
+ "is only as good as its weakest receipt" % (bad, n))
261
+ else:
262
+ head = ("UNVERIFIABLE -- %d of %d receipts could not be checked here; "
263
+ "nothing failed, but nothing is proven either" % (unv, n))
264
+ caveat = ""
265
+ if unsigned_n:
266
+ caveat = ("; %d of %d receipts are UNSIGNED, so their ORIGIN is not "
267
+ "proven -- only that their bytes were not edited after "
268
+ "hashing" % (unsigned_n, n))
269
+ return head + ". " + _cost_line(cost) + caveat
270
+
271
+
272
+ def _render(record):
273
+ lines = ["Evidence export: %s" % record["workspace"], ""]
274
+ for r in record["receipts"]:
275
+ lines.append(" %-13s %s" % (r["verdict"], r["path"]))
276
+ for name, axis in sorted(r["axes"].items()):
277
+ if axis["state"] != VERIFIED:
278
+ lines.append(" %s %s: %s" % (
279
+ axis["state"], name, axis["reason"]))
280
+ lines.append("")
281
+ lines.append(record["summary"])
282
+ lines.append("")
283
+ lines.append("What this export does NOT prove:")
284
+ for limit in record["does_not_prove"]:
285
+ lines.append(" - %s" % limit)
286
+ return "\n".join(lines)
287
+
288
+
289
+ class _Parser(argparse.ArgumentParser):
290
+ # argparse exits 2 on a usage error, and 2 means "could not be checked" in
291
+ # this repo -- a mistyped flag would read as a finding about the evidence.
292
+ # Overriding error() rather than parse_args leaves --help exiting 0.
293
+ def error(self, message):
294
+ self.print_usage(sys.stderr)
295
+ print("%s: error: %s" % (self.prog, message), file=sys.stderr)
296
+ raise SystemExit(EXIT_USAGE)
297
+
298
+
299
+ def main(argv=None):
300
+ ap = _Parser(
301
+ description="Export every receipt under a workspace as ONE evidence "
302
+ "file a third party can check.")
303
+ ap.add_argument("workspace", nargs="?", default=".",
304
+ help="workspace to scan for receipts (default: .)")
305
+ ap.add_argument("--out", default=None,
306
+ help="write the evidence file here (default: stdout only)")
307
+ ap.add_argument("--force", action="store_true",
308
+ help="overwrite an existing --out file")
309
+ ap.add_argument("--json", action="store_true", dest="as_json",
310
+ help="print the raw record instead of the report")
311
+ ap.add_argument("--repo-dir", default=".",
312
+ help="repository the receipts are re-checked against")
313
+ args = ap.parse_args(argv)
314
+
315
+ if not os.path.isdir(args.workspace):
316
+ print("receipt-export: workspace does not exist: %s" % args.workspace,
317
+ file=sys.stderr)
318
+ return EXIT_INPUT_MISSING
319
+
320
+ # Refuse BEFORE doing the work. Discovering the clobber after verifying
321
+ # everything wastes the run, and worse, tempts a caller to pass --force
322
+ # reflexively next time.
323
+ if args.out and os.path.exists(args.out) and not args.force:
324
+ print("receipt-export: refusing to overwrite %s without --force"
325
+ % args.out, file=sys.stderr)
326
+ return EXIT_USAGE
327
+
328
+ record = export(args.workspace, args.repo_dir)
329
+
330
+ # An empty workspace exports NOTHING. Writing a file whose only content is
331
+ # "no evidence" makes an audit passable by deleting the receipts, and the
332
+ # resulting file looks like evidence in a directory listing.
333
+ if record["verdict"] == EMPTY:
334
+ print(record["summary"], file=sys.stderr)
335
+ return EXIT_EMPTY
336
+
337
+ if args.out:
338
+ with open(args.out, "w", encoding="utf-8") as f:
339
+ json.dump(record, f, indent=2, sort_keys=True)
340
+ f.write("\n")
341
+
342
+ print(json.dumps(record, indent=2, sort_keys=True) if args.as_json
343
+ else _render(record))
344
+ return EXIT[record["verdict"]]
345
+
346
+
347
+ if __name__ == "__main__":
348
+ sys.exit(main())