loki-mode 9.3.0 → 9.5.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/dashboard/__init__.py +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/tools/cost-attribute.py +394 -0
- package/tools/gate-badge.py +144 -0
- package/tools/gate-explain.py +290 -0
- package/tools/gate-log.py +422 -0
- package/tools/gate-status.py +361 -0
- package/tools/receipt-export.py +348 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Is this repo's merge gate actually set up and working? One screen.
|
|
3
|
+
|
|
4
|
+
WHY THIS EXISTS. The gate line ships as eight separate tools, each honest on
|
|
5
|
+
its own axis: policy-load.py validates the policy file, baseline-pin.py holds
|
|
6
|
+
the cost reference, signing-status.py proves the keyring can sign,
|
|
7
|
+
cost-history.py holds the measured trend, ci-gate.py enforces the lot. Every
|
|
8
|
+
one of them answers a question an operator did not ask. The question they
|
|
9
|
+
actually ask, at the moment they need it -- before a release, after inheriting
|
|
10
|
+
a repo, when a green check stops being believable -- is one question:
|
|
11
|
+
|
|
12
|
+
is the gate ON, and would it do anything right now?
|
|
13
|
+
|
|
14
|
+
Nobody could answer it without running five commands and knowing how to read
|
|
15
|
+
five different exit conventions. So the honest answer to "is the gate working"
|
|
16
|
+
was, in practice, "somebody said it was". That is the exact shape this repo has
|
|
17
|
+
paid for fifteen times over: an assurance nobody re-derived.
|
|
18
|
+
|
|
19
|
+
NOTHING HERE RE-IMPLEMENTS A CHECK. Each line is produced by invoking the tool
|
|
20
|
+
that owns that rule as a subprocess (sys.executable + the tool path) and
|
|
21
|
+
mapping its exit code. A second copy of a rule is how the rule drifts, and this
|
|
22
|
+
repo proved that five times over with provider lists alone. Concretely: this
|
|
23
|
+
file does NOT know what makes a policy valid, what counts as baseline drift,
|
|
24
|
+
what proves a keyring can sign, or what makes a cost record measured. It knows
|
|
25
|
+
only which tool owns each question.
|
|
26
|
+
|
|
27
|
+
THREE STATES, NEVER A BOOLEAN, on every line:
|
|
28
|
+
|
|
29
|
+
OK the owning tool checked, and the answer is yes
|
|
30
|
+
PROBLEM the owning tool checked, and the answer is no
|
|
31
|
+
UNKNOWN the owning tool could not check, or is not on disk
|
|
32
|
+
|
|
33
|
+
UNKNOWN is the state this whole file exists to keep. "We checked and it is
|
|
34
|
+
fine" and "we could not check" are opposite facts about the world, and a status
|
|
35
|
+
screen is exactly where the second silently becomes the first -- a dash, a
|
|
36
|
+
blank cell, a skipped row. Absent is not zero, and unmeasured is not OK.
|
|
37
|
+
|
|
38
|
+
A MISSING COMPOSED TOOL READS "unavailable" AND POISONS THE VERDICT. If
|
|
39
|
+
signing-status.py is not on disk, this cannot report on signing, so it says so
|
|
40
|
+
and the overall verdict is not OK. A status screen that drops a line it could
|
|
41
|
+
not produce and still says "healthy" is reporting on a smaller repo than the
|
|
42
|
+
one it was pointed at. That is the tarball-assertion defect again: a check
|
|
43
|
+
reporting a pass without having checked.
|
|
44
|
+
|
|
45
|
+
THE VERDICT IS WEAKEST-LINK. Never a count, never a percentage, never "4 of 5".
|
|
46
|
+
A score cannot answer "is the gate working", because the one axis that is
|
|
47
|
+
blind is the one that matters. UNKNOWN outranks PROBLEM in the verdict for the
|
|
48
|
+
same reason ci-gate.py ranks them that way: told PROBLEM, an operator fixes the
|
|
49
|
+
named thing, re-runs, sees green, and is still blind on the dead axis.
|
|
50
|
+
|
|
51
|
+
READ-ONLY, AND IT SAYS SO. Every composed tool is invoked in a reporting mode
|
|
52
|
+
that starts no run, spends nothing, and contacts no provider: policy-load
|
|
53
|
+
reads a file, baseline-pin `show` reads a pin, signing-status round-trips
|
|
54
|
+
against the local keyring only, cost-history `report` reads recorded history,
|
|
55
|
+
and ci-gate is NOT executed at all -- the "would the gate run" line is answered
|
|
56
|
+
from whether a policy implies flags, because running the real gate would be the
|
|
57
|
+
one composed call that is not free.
|
|
58
|
+
|
|
59
|
+
Usage:
|
|
60
|
+
tools/gate-status.py [workspace] [--json] [--policy .loki-policy.json]
|
|
61
|
+
|
|
62
|
+
Exit: 0 every component checked and healthy, 1 a component checked and is not,
|
|
63
|
+
2 a component could not be checked (including a missing tool), 66 the
|
|
64
|
+
workspace path does not exist.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
import argparse
|
|
68
|
+
import json
|
|
69
|
+
import os
|
|
70
|
+
import subprocess
|
|
71
|
+
import sys
|
|
72
|
+
|
|
73
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
74
|
+
|
|
75
|
+
OK, PROBLEM, UNKNOWN = "OK", "PROBLEM", "UNKNOWN"
|
|
76
|
+
|
|
77
|
+
# Exit codes, per the repo-wide tools/ convention.
|
|
78
|
+
EXIT_OK, EXIT_PROBLEM, EXIT_UNKNOWN, EXIT_USAGE, EXIT_MISSING = 0, 1, 2, 64, 66
|
|
79
|
+
|
|
80
|
+
_VERDICT_EXIT = {OK: EXIT_OK, PROBLEM: EXIT_PROBLEM, UNKNOWN: EXIT_UNKNOWN}
|
|
81
|
+
|
|
82
|
+
# Module-level so a test can point one at a nonexistent path and assert that a
|
|
83
|
+
# missing component reads unavailable AND drags the verdict off OK.
|
|
84
|
+
POLICY_LOAD = os.path.join(_HERE, "policy-load.py")
|
|
85
|
+
BASELINE_PIN = os.path.join(_HERE, "baseline-pin.py")
|
|
86
|
+
SIGNING_STATUS = os.path.join(_HERE, "signing-status.py")
|
|
87
|
+
COST_HISTORY = os.path.join(_HERE, "cost-history.py")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class _Usage(argparse.ArgumentParser):
|
|
91
|
+
"""argparse defaults a usage error to exit 2, which here means "could not
|
|
92
|
+
be checked" -- a typo in a flag would read as a blind gate rather than as
|
|
93
|
+
the operator error it is. 64 keeps those two facts distinct."""
|
|
94
|
+
|
|
95
|
+
def error(self, message):
|
|
96
|
+
self.print_usage(sys.stderr)
|
|
97
|
+
sys.stderr.write("%s: error: %s\n" % (self.prog, message))
|
|
98
|
+
raise SystemExit(EXIT_USAGE)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _line(component, state, detail):
|
|
102
|
+
return {"component": component, "state": state, "detail": detail}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _invoke(tool, argv):
|
|
106
|
+
"""Run a component tool read-only. Returns (returncode, stdout, stderr) or
|
|
107
|
+
None when the tool is not on disk. Never recomputes the tool's rule."""
|
|
108
|
+
if not os.path.isfile(tool):
|
|
109
|
+
return None
|
|
110
|
+
try:
|
|
111
|
+
proc = subprocess.run([sys.executable, tool] + argv,
|
|
112
|
+
capture_output=True, text=True)
|
|
113
|
+
except OSError as exc:
|
|
114
|
+
return (None, "", str(exc))
|
|
115
|
+
return (proc.returncode, proc.stdout, proc.stderr)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _unavailable(component, tool):
|
|
119
|
+
return _line(component, UNKNOWN,
|
|
120
|
+
"unavailable: %s is not on disk, so this axis was never "
|
|
121
|
+
"checked -- absent is not OK" % os.path.basename(tool))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _first_line(*texts):
|
|
125
|
+
for text in texts:
|
|
126
|
+
for raw in (text or "").splitlines():
|
|
127
|
+
if raw.strip():
|
|
128
|
+
return raw.strip()
|
|
129
|
+
return "no detail reported"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def check_policy(policy_file):
|
|
133
|
+
"""Is a merge policy file present and valid? policy-load.py owns the rule."""
|
|
134
|
+
got = _invoke(POLICY_LOAD, ["--file", policy_file, "--as-args"])
|
|
135
|
+
if got is None:
|
|
136
|
+
return _unavailable("policy", POLICY_LOAD)
|
|
137
|
+
rc, out, err = got
|
|
138
|
+
if rc is None:
|
|
139
|
+
return _line("policy", UNKNOWN, "could not run policy-load.py: %s" % err)
|
|
140
|
+
if rc == 0:
|
|
141
|
+
return _line("policy", OK,
|
|
142
|
+
"%s is valid and enforces: %s" % (policy_file, out.strip()))
|
|
143
|
+
if rc == 1:
|
|
144
|
+
# policy-load's own words: missing file, unknown key, bad value, or a
|
|
145
|
+
# policy that enforces nothing. All are "checked, and it is not set up".
|
|
146
|
+
return _line("policy", PROBLEM, _first_line(err, out))
|
|
147
|
+
return _line("policy", UNKNOWN,
|
|
148
|
+
"policy-load.py exited %d, which is not a verdict: %s"
|
|
149
|
+
% (rc, _first_line(err, out)))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def check_baseline(pin_file):
|
|
153
|
+
"""Is a cost baseline pinned and undrifted? baseline-pin.py owns the rule,
|
|
154
|
+
including what counts as drift (raw-bytes sha256, not the receipt's own
|
|
155
|
+
canonical digest). That distinction is not restated here."""
|
|
156
|
+
got = _invoke(BASELINE_PIN, ["show", "--file", pin_file, "--json"])
|
|
157
|
+
if got is None:
|
|
158
|
+
return _unavailable("baseline", BASELINE_PIN)
|
|
159
|
+
rc, out, err = got
|
|
160
|
+
if rc is None:
|
|
161
|
+
return _line("baseline", UNKNOWN,
|
|
162
|
+
"could not run baseline-pin.py: %s" % err)
|
|
163
|
+
if rc == 0:
|
|
164
|
+
return _line("baseline", OK, _first_line(err))
|
|
165
|
+
if rc == 1:
|
|
166
|
+
# No pin at all, or a pin whose receipt drifted since. Both are
|
|
167
|
+
# "checked, and there is no trustworthy baseline".
|
|
168
|
+
return _line("baseline", PROBLEM, _first_line(err, out))
|
|
169
|
+
return _line("baseline", UNKNOWN,
|
|
170
|
+
"baseline-pin.py exited %d, which is not a verdict: %s"
|
|
171
|
+
% (rc, _first_line(err, out)))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def check_signing():
|
|
175
|
+
"""Can this machine sign receipts? signing-status.py owns the rule, and it
|
|
176
|
+
already refuses to collapse its four states into a boolean. Its
|
|
177
|
+
not_configured (2) and gpg_absent (3) are NOT problems -- signing is opt-in
|
|
178
|
+
and nothing is broken -- but neither are they proof of origin, so they read
|
|
179
|
+
UNKNOWN here rather than OK. Only a completed sign+verify round trip is OK."""
|
|
180
|
+
got = _invoke(SIGNING_STATUS, ["--json"])
|
|
181
|
+
if got is None:
|
|
182
|
+
return _unavailable("signing", SIGNING_STATUS)
|
|
183
|
+
rc, out, err = got
|
|
184
|
+
if rc is None:
|
|
185
|
+
return _line("signing", UNKNOWN,
|
|
186
|
+
"could not run signing-status.py: %s" % err)
|
|
187
|
+
try:
|
|
188
|
+
detail = json.loads(out)
|
|
189
|
+
except ValueError:
|
|
190
|
+
detail = None
|
|
191
|
+
status = detail.get("status") if isinstance(detail, dict) else None
|
|
192
|
+
reason = (detail or {}).get("reason") if isinstance(detail, dict) else None
|
|
193
|
+
|
|
194
|
+
if rc == 0 and status == "ok":
|
|
195
|
+
return _line("signing", OK,
|
|
196
|
+
"sign+verify round trip completed: receipts carry origin")
|
|
197
|
+
if rc == 1 and status == "broken":
|
|
198
|
+
return _line("signing", PROBLEM,
|
|
199
|
+
"a key is configured but cannot sign, so receipts emit "
|
|
200
|
+
"UNSIGNED silently: %s" % (reason or "no reason reported"))
|
|
201
|
+
if rc == 2 and status == "not_configured":
|
|
202
|
+
return _line("signing", UNKNOWN,
|
|
203
|
+
"signing is opt-in and off (LOKI_PROOF_GPG_KEY unset): "
|
|
204
|
+
"receipts prove integrity but NOT origin")
|
|
205
|
+
if rc == 3 and status == "gpg_absent":
|
|
206
|
+
return _line("signing", UNKNOWN,
|
|
207
|
+
"no gpg on PATH, so origin cannot be proven here")
|
|
208
|
+
return _line("signing", UNKNOWN,
|
|
209
|
+
"signing-status.py exited %d with status %r, which is not a "
|
|
210
|
+
"verdict: %s" % (rc, status, _first_line(err, out)))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def check_cost_history(history_file):
|
|
214
|
+
"""Is there measured cost history? cost-history.py owns the rule, including
|
|
215
|
+
what counts as measured (it imports record_is_measured; this file does not
|
|
216
|
+
restate that predicate, and must not)."""
|
|
217
|
+
got = _invoke(COST_HISTORY, ["report", "--file", history_file, "--json"])
|
|
218
|
+
if got is None:
|
|
219
|
+
return _unavailable("cost_history", COST_HISTORY)
|
|
220
|
+
rc, out, err = got
|
|
221
|
+
if rc is None:
|
|
222
|
+
return _line("cost_history", UNKNOWN,
|
|
223
|
+
"could not run cost-history.py: %s" % err)
|
|
224
|
+
try:
|
|
225
|
+
detail = json.loads(out)
|
|
226
|
+
except ValueError:
|
|
227
|
+
detail = None
|
|
228
|
+
if rc == 0 and isinstance(detail, dict):
|
|
229
|
+
runs = detail.get("measured")
|
|
230
|
+
median = detail.get("median_usd")
|
|
231
|
+
# A measured ZERO must survive as 0, so both of these ask `is None`,
|
|
232
|
+
# never falsy. `if not runs` would report a real count of zero as an
|
|
233
|
+
# absent count, and `median or "unknown"` would render a genuine,
|
|
234
|
+
# measured $0.00 median as unknown -- the exact collapse this repo has
|
|
235
|
+
# paid for on fifteen surfaces, run in the opposite direction.
|
|
236
|
+
if runs is None:
|
|
237
|
+
return _line("cost_history", UNKNOWN,
|
|
238
|
+
"cost-history reported no measured-run count")
|
|
239
|
+
return _line("cost_history", OK,
|
|
240
|
+
"%d measured run(s) in %s; median $%s; direction: %s"
|
|
241
|
+
% (runs, history_file,
|
|
242
|
+
"UNKNOWN" if median is None else "%.4f" % median,
|
|
243
|
+
detail.get("direction", "UNKNOWN")))
|
|
244
|
+
if rc == 1:
|
|
245
|
+
# Empty history, or history with no measured run. Zero runs is an
|
|
246
|
+
# absent measurement, which is a blind axis, not a healthy one.
|
|
247
|
+
why = detail.get("why") if isinstance(detail, dict) else None
|
|
248
|
+
return _line("cost_history", UNKNOWN,
|
|
249
|
+
"no measured cost history: %s"
|
|
250
|
+
% (why or _first_line(err) or history_file))
|
|
251
|
+
return _line("cost_history", UNKNOWN,
|
|
252
|
+
"cost-history.py exited %d, which is not a verdict: %s"
|
|
253
|
+
% (rc, _first_line(err, out)))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def check_would_run(policy_line):
|
|
257
|
+
"""Would the gate do anything right now?
|
|
258
|
+
|
|
259
|
+
Answered from the policy line, NOT by executing ci-gate.py: running the
|
|
260
|
+
real gate is the one composed call that is not free, and this tool promises
|
|
261
|
+
to spend nothing. A gate invoked with no flags enforces nothing while
|
|
262
|
+
looking configured -- ci-gate.py's own docstring calls that vacuously
|
|
263
|
+
green -- so an unusable policy means the gate would not run.
|
|
264
|
+
"""
|
|
265
|
+
if policy_line["state"] == OK:
|
|
266
|
+
return _line("would_run", OK,
|
|
267
|
+
"yes: ci-gate would enforce the loaded policy on the next "
|
|
268
|
+
"run (not executed here -- this tool spends nothing)")
|
|
269
|
+
if policy_line["state"] == PROBLEM:
|
|
270
|
+
return _line("would_run", PROBLEM,
|
|
271
|
+
"no: the policy does not load, so ci-gate would run with "
|
|
272
|
+
"nothing to enforce, which is green without checking")
|
|
273
|
+
return _line("would_run", UNKNOWN,
|
|
274
|
+
"cannot say: the policy itself could not be checked")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def evaluate(workspace=".", policy_file=None, history_file=None):
|
|
278
|
+
"""Every component, then the weakest-link verdict."""
|
|
279
|
+
root = os.path.normpath(workspace)
|
|
280
|
+
if os.path.basename(root) == ".loki":
|
|
281
|
+
root = os.path.dirname(root) or "."
|
|
282
|
+
if policy_file is None:
|
|
283
|
+
policy_file = os.path.join(root, ".loki-policy.json")
|
|
284
|
+
if history_file is None:
|
|
285
|
+
history_file = os.path.join(root, ".loki", "cost-history.jsonl")
|
|
286
|
+
pin_file = os.path.join(root, ".loki", "baseline.json")
|
|
287
|
+
|
|
288
|
+
policy = check_policy(policy_file)
|
|
289
|
+
lines = [policy,
|
|
290
|
+
check_baseline(pin_file),
|
|
291
|
+
check_signing(),
|
|
292
|
+
check_cost_history(history_file),
|
|
293
|
+
check_would_run(policy)]
|
|
294
|
+
|
|
295
|
+
# THE DECISION. Weakest link, UNKNOWN outranking PROBLEM. Written once,
|
|
296
|
+
# here, so a mutation of it has nowhere to hide. Never a count: "4 of 5
|
|
297
|
+
# healthy" cannot answer "is the gate working", because the blind axis is
|
|
298
|
+
# exactly the one that matters.
|
|
299
|
+
states = [line["state"] for line in lines]
|
|
300
|
+
verdict = UNKNOWN if UNKNOWN in states else (
|
|
301
|
+
PROBLEM if PROBLEM in states else OK)
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
"workspace": root,
|
|
305
|
+
"components": lines,
|
|
306
|
+
"verdict": verdict,
|
|
307
|
+
"exit_code": _VERDICT_EXIT[verdict],
|
|
308
|
+
"read_only": True,
|
|
309
|
+
"headline": _HEADLINE[verdict],
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
_HEADLINE = {
|
|
314
|
+
OK: "the merge gate is configured and would enforce the policy",
|
|
315
|
+
PROBLEM: "the merge gate is configured but something it checked is wrong",
|
|
316
|
+
UNKNOWN: "the merge gate cannot be fully verified from here, so it is not "
|
|
317
|
+
"known to be working",
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def render(result):
|
|
322
|
+
lines = ["Merge gate status for %s" % result["workspace"],
|
|
323
|
+
" (read-only: starts nothing, spends nothing, contacts no "
|
|
324
|
+
"provider)", ""]
|
|
325
|
+
lines.append("%-14s %-9s %s" % ("COMPONENT", "STATE", "DETAIL"))
|
|
326
|
+
for row in result["components"]:
|
|
327
|
+
lines.append("%-14s %-9s %s"
|
|
328
|
+
% (row["component"], row["state"], row["detail"]))
|
|
329
|
+
lines += ["", "GATE: %s -- %s" % (result["verdict"], result["headline"])]
|
|
330
|
+
return "\n".join(lines)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def main(argv=None):
|
|
334
|
+
ap = _Usage(
|
|
335
|
+
description="One-screen answer to whether this repo's merge gate is "
|
|
336
|
+
"set up and working. Read-only: starts nothing, spends "
|
|
337
|
+
"nothing, contacts no provider.")
|
|
338
|
+
ap.add_argument("workspace", nargs="?", default=".",
|
|
339
|
+
help="workspace root (or its .loki dir); default .")
|
|
340
|
+
ap.add_argument("--policy", default=None,
|
|
341
|
+
help="policy file; default <workspace>/.loki-policy.json")
|
|
342
|
+
ap.add_argument("--history", default=None,
|
|
343
|
+
help="cost history JSONL; default "
|
|
344
|
+
"<workspace>/.loki/cost-history.jsonl")
|
|
345
|
+
ap.add_argument("--json", action="store_true", dest="as_json",
|
|
346
|
+
help="emit the status as JSON")
|
|
347
|
+
args = ap.parse_args(argv)
|
|
348
|
+
|
|
349
|
+
if not os.path.isdir(args.workspace):
|
|
350
|
+
sys.stderr.write("gate-status: workspace does not exist: %s\n"
|
|
351
|
+
% args.workspace)
|
|
352
|
+
return EXIT_MISSING
|
|
353
|
+
|
|
354
|
+
result = evaluate(args.workspace, args.policy, args.history)
|
|
355
|
+
print(json.dumps(result, indent=2, sort_keys=True) if args.as_json
|
|
356
|
+
else render(result))
|
|
357
|
+
return result["exit_code"]
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
if __name__ == "__main__":
|
|
361
|
+
sys.exit(main())
|