loki-mode 8.97.0 → 8.98.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/verify-demo.sh +223 -0
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v8.
|
|
6
|
+
# Loki Mode v8.98.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -469,4 +469,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
469
469
|
|
|
470
470
|
---
|
|
471
471
|
|
|
472
|
-
**v8.
|
|
472
|
+
**v8.98.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.
|
|
1
|
+
8.98.0
|
package/dashboard/__init__.py
CHANGED
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var m_=Object.create;var{getPrototypeOf:u_,defineProperty:eK,getOwnPropertyNames:p_}=Object;var d_=Object.prototype.hasOwnProperty;function c_(Z){return this[Z]}var l_,i_,a_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?l_??=new WeakMap:i_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?m_(u_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of p_(Z))if(!d_.call(K,$))eK(K,$,{get:c_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var s_=(Z)=>Z;function n_(Z,X){this[Z]=s_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:n_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as o_}from"url";import{existsSync as UQ}from"fs";import{homedir as r_}from"os";function t_(){let Z=RO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(RO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(r_(),".loki")}var RO,i0;var H8=p(()=>{RO=Z$(o_(import.meta.url));i0=t_()});import{readFileSync as e_}from"fs";import{resolve as Zf,dirname as Xf}from"path";import{fileURLToPath as Qf}from"url";function h3(){if(h5!==null)return h5;let Z="8.
|
|
2
|
+
var m_=Object.create;var{getPrototypeOf:u_,defineProperty:eK,getOwnPropertyNames:p_}=Object;var d_=Object.prototype.hasOwnProperty;function c_(Z){return this[Z]}var l_,i_,a_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?l_??=new WeakMap:i_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?m_(u_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of p_(Z))if(!d_.call(K,$))eK(K,$,{get:c_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var s_=(Z)=>Z;function n_(Z,X){this[Z]=s_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:n_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as o_}from"url";import{existsSync as UQ}from"fs";import{homedir as r_}from"os";function t_(){let Z=RO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(RO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(r_(),".loki")}var RO,i0;var H8=p(()=>{RO=Z$(o_(import.meta.url));i0=t_()});import{readFileSync as e_}from"fs";import{resolve as Zf,dirname as Xf}from"path";import{fileURLToPath as Qf}from"url";function h3(){if(h5!==null)return h5;let Z="8.98.0";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Xf(Qf(import.meta.url)),Q=X$(X);h5=e_(Zf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var bO={};l0(bO,{runOrThrow:()=>jf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>Tf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>yO});async function NQ(Z,X=yO){let Q=Z.getReader(),Y=new TextDecoder,J="",z=0;try{while(z<X){let{done:K,value:$}=await Q.read();if(K)break;if(!$)continue;if(z+=$.byteLength,z>X){let W=$.byteLength-(z-X);J+=Y.decode($.subarray(0,W),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([NQ(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:z,stderr:K,exitCode:$}}finally{if(Y)clearTimeout(Y);if(J)clearTimeout(J)}}async function jf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Q$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=Mf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Mf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Tf(Z,X="--version"){if(!await X9(Z))return null;let Y=await E0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var yO=16777216,Q$;var x9=p(()=>{Q$=class Q$ extends Error{message;exitCode;stdout;stderr;constructor(Z,X,Q,Y){super(Z);this.message=Z;this.exitCode=X;this.stdout=Q;this.stderr=Y;this.name="ShellError"}}});function o7(Z){return wf?"":Z}var wf,L0,F8,p0,zV0,a0,W8,Q9,v;var S6=p(()=>{wf=(process.env.NO_COLOR??"").length>0;L0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),p0=o7("\x1B[1;33m"),zV0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),W8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),v=o7("\x1B[0m")});import{existsSync as bf}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(bf(Z))return x4=Z,Z;let X=await X9("python3.12");if(X)return x4=X,X;let Q=await X9("python3");return x4=Q,Q}async function Y7(Z,X={}){let Q=await E7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var x4;var r7=p(()=>{x9()});var ZL={};l0(ZL,{runStatus:()=>Kh});import{existsSync as Y9,readFileSync as g3,readdirSync as iO,statSync as aO}from"fs";import{resolve as h8,basename as rf}from"path";import{homedir as tf}from"os";function sO(Z){let X=Math.trunc(Z);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function nO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*LQ/X);if(J>LQ)J=LQ;let z=LQ-J,K=F8;if(Y>=80)K=L0;else if(Y>=50)K=p0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),W=sO(Z),V=sO(X);return` ${W8}${Q}${v} ${K}[${$}]${v} ${Y}% (${W} / ${V})`}async function Zh(){if(await X9("jq"))return!0;return process.stdout.write(`${L0}Error: jq is required but not installed.${v}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -1232,4 +1232,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
1232
1232
|
`),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (h_(),f_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
|
|
1233
1233
|
`),process.stderr.write(v_),2}}cO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var uW0=await mW0(Bun.argv.slice(2));process.exit(uW0);
|
|
1234
1234
|
|
|
1235
|
-
//# debugId=
|
|
1235
|
+
//# debugId=A8CE268C65C8280964756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "8.
|
|
4
|
+
"version": "8.98.0",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "8.
|
|
5
|
+
"version": "8.98.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Zero-cost demonstration of the verification chain, on receipts it builds
|
|
4
|
+
# itself. The point a prospective user cannot otherwise see without paying for
|
|
5
|
+
# a full build: this system re-checks its own output, and says so honestly when
|
|
6
|
+
# a receipt does not hold up.
|
|
7
|
+
#
|
|
8
|
+
# tools/verify-demo.sh [--keep]
|
|
9
|
+
#
|
|
10
|
+
# WHAT MAKES THIS HONEST, AND WHY IT IS BUILT THIS WAY.
|
|
11
|
+
#
|
|
12
|
+
# Every verdict below is printed by the REAL tool, verbatim. Nothing here
|
|
13
|
+
# echoes a "VERIFIED" of its own. A demo that printed its own verdicts would
|
|
14
|
+
# keep printing them after the verifier broke -- it would be a claim about the
|
|
15
|
+
# product rather than an exercise of it, which is the same defect the tools it
|
|
16
|
+
# demonstrates exist to prevent. So each step runs the actual script, shows the
|
|
17
|
+
# actual output, and asserts the actual exit code. If the chain regresses, this
|
|
18
|
+
# fails; that is deliberate, and makes it a smoke test as well as a demo.
|
|
19
|
+
#
|
|
20
|
+
# The receipts are SYNTHETIC. A scratch git repo with one commit and one
|
|
21
|
+
# uncommitted edit is created in a tempdir, and the real proof generator is run
|
|
22
|
+
# over it. Synthetic means no build ran, no provider was contacted and no money
|
|
23
|
+
# was spent -- it does NOT mean the receipt is fake. It is a genuine receipt
|
|
24
|
+
# about a trivially small piece of real work, which is exactly what lets the
|
|
25
|
+
# real verifier reach a real VERIFIED instead of a hedge.
|
|
26
|
+
#
|
|
27
|
+
# The scratch repo is a fresh mktemp -d. Nothing is written inside the user's
|
|
28
|
+
# own .loki, and the demo never touches global git config.
|
|
29
|
+
|
|
30
|
+
set -uo pipefail
|
|
31
|
+
|
|
32
|
+
KEEP=0
|
|
33
|
+
for arg in "$@"; do
|
|
34
|
+
case "$arg" in
|
|
35
|
+
--keep) KEEP=1 ;;
|
|
36
|
+
-h|--help) sed -n '2,28p' "$0" | sed 's/^#\{1,2\} \{0,1\}//'; exit 0 ;;
|
|
37
|
+
*) echo "verify-demo: unknown argument: $arg" >&2; exit 64 ;;
|
|
38
|
+
esac
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
42
|
+
ROOT="$(cd "$HERE/.." && pwd)"
|
|
43
|
+
|
|
44
|
+
GENERATOR="$ROOT/autonomy/lib/proof-generator.py"
|
|
45
|
+
ATTEST="$HERE/receipt-attest.py"
|
|
46
|
+
GATE="$HERE/ci-gate.py"
|
|
47
|
+
|
|
48
|
+
# Requirement: name the missing tool and stop. A demo that skipped a step and
|
|
49
|
+
# still reported success would be asserting a capability it never exercised.
|
|
50
|
+
missing=0
|
|
51
|
+
for tool in "$GENERATOR" "$ATTEST" "$GATE"; do
|
|
52
|
+
if [ ! -f "$tool" ]; then
|
|
53
|
+
echo "verify-demo: required tool missing: $tool" >&2
|
|
54
|
+
missing=1
|
|
55
|
+
fi
|
|
56
|
+
done
|
|
57
|
+
if ! command -v git >/dev/null 2>&1; then
|
|
58
|
+
echo "verify-demo: required tool missing: git" >&2
|
|
59
|
+
missing=1
|
|
60
|
+
fi
|
|
61
|
+
if [ "$missing" -ne 0 ]; then
|
|
62
|
+
echo "verify-demo: cannot demonstrate the verification chain with a tool absent" >&2
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/loki-verify-demo-XXXXXX")"
|
|
67
|
+
|
|
68
|
+
cleanup() {
|
|
69
|
+
if [ "$KEEP" -eq 1 ]; then
|
|
70
|
+
echo ""
|
|
71
|
+
echo "scratch kept at: $SCRATCH"
|
|
72
|
+
else
|
|
73
|
+
rm -rf "$SCRATCH"
|
|
74
|
+
fi
|
|
75
|
+
}
|
|
76
|
+
trap cleanup EXIT
|
|
77
|
+
|
|
78
|
+
rule() { printf '%s\n' "------------------------------------------------------------"; }
|
|
79
|
+
|
|
80
|
+
step_failed() {
|
|
81
|
+
echo ""
|
|
82
|
+
echo "verify-demo: FAILED -- $1" >&2
|
|
83
|
+
echo "verify-demo: the verification chain did not behave as this demo claims." >&2
|
|
84
|
+
exit 1
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
cat <<BANNER
|
|
88
|
+
============================================================
|
|
89
|
+
Loki Mode -- verification chain demo
|
|
90
|
+
============================================================
|
|
91
|
+
|
|
92
|
+
THESE RECEIPTS ARE SYNTHETIC.
|
|
93
|
+
|
|
94
|
+
No build was run. No provider was contacted. No money was
|
|
95
|
+
spent. This creates a scratch git repository, generates a
|
|
96
|
+
real receipt over it with the real generator, and then runs
|
|
97
|
+
the real verification tools against that receipt.
|
|
98
|
+
|
|
99
|
+
Every verdict below is printed by the tool named above it.
|
|
100
|
+
Nothing in this script prints a verdict of its own.
|
|
101
|
+
|
|
102
|
+
scratch workspace: $SCRATCH
|
|
103
|
+
BANNER
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Build the synthetic workspace: a real repo, a real commit, a real edit.
|
|
107
|
+
# Small, but genuinely present on disk, which is what the verifier re-derives
|
|
108
|
+
# the receipt's git facts from.
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
cd "$SCRATCH" || step_failed "could not enter the scratch workspace"
|
|
111
|
+
|
|
112
|
+
git init -q . 2>/dev/null || step_failed "could not create the scratch git repository"
|
|
113
|
+
# Repo-local only. The user's global git identity is never touched.
|
|
114
|
+
git config user.name "loki-verify-demo" || step_failed "could not set scratch git identity"
|
|
115
|
+
git config user.email "verify-demo@loki.invalid" || step_failed "could not set scratch git identity"
|
|
116
|
+
|
|
117
|
+
printf 'print("hello")\n' > app.py
|
|
118
|
+
git add app.py || step_failed "could not stage the scratch file"
|
|
119
|
+
git commit -qm "synthetic baseline" || step_failed "could not commit the scratch baseline"
|
|
120
|
+
printf 'print("goodbye")\n' >> app.py
|
|
121
|
+
|
|
122
|
+
mkdir -p .loki/proofs
|
|
123
|
+
|
|
124
|
+
echo ""
|
|
125
|
+
rule
|
|
126
|
+
echo "STEP 1 of 3 -- generate a receipt over the synthetic workspace"
|
|
127
|
+
echo " \$ autonomy/lib/proof-generator.py --out-dir .loki/proofs/good"
|
|
128
|
+
rule
|
|
129
|
+
if ! python3 "$GENERATOR" --loki-dir .loki --out-dir .loki/proofs/good \
|
|
130
|
+
--provider claude --quiet; then
|
|
131
|
+
step_failed "the real proof generator did not produce a receipt"
|
|
132
|
+
fi
|
|
133
|
+
GOOD="$SCRATCH/.loki/proofs/good/proof.json"
|
|
134
|
+
[ -f "$GOOD" ] || step_failed "the generator reported success but wrote no proof.json"
|
|
135
|
+
echo "wrote $(basename "$GOOD") ($(wc -c < "$GOOD" | tr -d ' ') bytes)"
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# 2. The honest receipt verifies.
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
echo ""
|
|
141
|
+
rule
|
|
142
|
+
echo "STEP 2 of 3 -- verify the untouched receipt"
|
|
143
|
+
echo " \$ tools/receipt-attest.py .loki/proofs/good/proof.json"
|
|
144
|
+
rule
|
|
145
|
+
python3 "$ATTEST" "$GOOD"
|
|
146
|
+
good_exit=$?
|
|
147
|
+
if [ "$good_exit" -ne 0 ]; then
|
|
148
|
+
step_failed "an untouched receipt did not attest cleanly (exit $good_exit, expected 0)"
|
|
149
|
+
fi
|
|
150
|
+
echo ""
|
|
151
|
+
echo " -> exit 0. Every scored axis was checked here, and passed."
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# 3. Tamper with a recorded FACT, not the formatting. The integrity hash is
|
|
155
|
+
# taken over canonicalized bytes, so re-indenting alone proves nothing;
|
|
156
|
+
# changing a claim the receipt makes is the case worth showing.
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
mkdir -p "$SCRATCH/.loki/proofs/tampered"
|
|
159
|
+
python3 - "$GOOD" "$SCRATCH/.loki/proofs/tampered/proof.json" <<'PY' || step_failed "could not write the tampered receipt"
|
|
160
|
+
import json, sys
|
|
161
|
+
src, dst = sys.argv[1], sys.argv[2]
|
|
162
|
+
proof = json.loads(open(src).read())
|
|
163
|
+
# Inflate the recorded file count: the receipt now claims more work than the
|
|
164
|
+
# repository contains. This is the realistic forgery, and it is a value inside
|
|
165
|
+
# the hashed body rather than a formatting change.
|
|
166
|
+
proof["facts"]["git"]["diff"]["count"] = 999
|
|
167
|
+
open(dst, "w").write(json.dumps(proof, indent=2))
|
|
168
|
+
PY
|
|
169
|
+
|
|
170
|
+
echo ""
|
|
171
|
+
rule
|
|
172
|
+
echo "STEP 3 of 3 -- tamper with the receipt, then re-verify"
|
|
173
|
+
echo " edited facts.git.diff.count: 1 -> 999 (claims work that is not there)"
|
|
174
|
+
echo " \$ tools/receipt-attest.py .loki/proofs/tampered/proof.json"
|
|
175
|
+
rule
|
|
176
|
+
python3 "$ATTEST" "$SCRATCH/.loki/proofs/tampered/proof.json"
|
|
177
|
+
bad_exit=$?
|
|
178
|
+
if [ "$bad_exit" -eq 0 ]; then
|
|
179
|
+
step_failed "a tampered receipt attested as clean (exit 0) -- the verifier did not catch it"
|
|
180
|
+
fi
|
|
181
|
+
echo ""
|
|
182
|
+
echo " -> exit $bad_exit. Caught, with the reason printed above."
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# 4. The gate refuses. A verdict a human reads is not a gate; an exit code CI
|
|
186
|
+
# branches on is. Only the tampered receipt is left in place.
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
# Moved aside rather than deleted: the gate must see ONLY the tampered receipt
|
|
189
|
+
# (it attests the newest one under .loki/proofs/*), while --keep still leaves
|
|
190
|
+
# the good receipt on disk for anyone -- or any test -- wanting to re-hash it
|
|
191
|
+
# and confirm the digest printed above came from these exact bytes.
|
|
192
|
+
mkdir -p "$SCRATCH/generated"
|
|
193
|
+
mv "$SCRATCH/.loki/proofs/good/proof.json" "$SCRATCH/generated/proof.json" \
|
|
194
|
+
|| step_failed "could not set aside the good receipt"
|
|
195
|
+
rm -rf "$SCRATCH/.loki/proofs/good"
|
|
196
|
+
|
|
197
|
+
echo ""
|
|
198
|
+
rule
|
|
199
|
+
echo "AND THE GATE REFUSES TO PASS"
|
|
200
|
+
echo " with only the tampered receipt present:"
|
|
201
|
+
echo " \$ tools/ci-gate.py . --require-receipt"
|
|
202
|
+
rule
|
|
203
|
+
python3 "$GATE" "$SCRATCH" --require-receipt
|
|
204
|
+
gate_exit=$?
|
|
205
|
+
if [ "$gate_exit" -eq 0 ]; then
|
|
206
|
+
step_failed "the CI gate passed (exit 0) on a tampered receipt"
|
|
207
|
+
fi
|
|
208
|
+
echo ""
|
|
209
|
+
echo " -> exit $gate_exit. A CI job branching on this exit code stops here."
|
|
210
|
+
|
|
211
|
+
cat <<'SUMMARY'
|
|
212
|
+
|
|
213
|
+
============================================================
|
|
214
|
+
Every verdict above was printed by the real tool, over
|
|
215
|
+
receipts generated on this machine seconds ago.
|
|
216
|
+
|
|
217
|
+
Reminder: SYNTHETIC receipts. No build ran, no provider was
|
|
218
|
+
contacted, no money was spent. This demonstrates the
|
|
219
|
+
verification chain; it does not certify any real work.
|
|
220
|
+
============================================================
|
|
221
|
+
SUMMARY
|
|
222
|
+
|
|
223
|
+
exit 0
|