loki-mode 7.54.0 → 7.55.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/run.sh +97 -0
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +2 -2
- 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/src/audit/crosslink.js +186 -0
- package/src/audit/index.js +23 -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 v7.
|
|
6
|
+
# Loki Mode v7.55.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -406,4 +406,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
406
406
|
|
|
407
407
|
---
|
|
408
408
|
|
|
409
|
-
**v7.
|
|
409
|
+
**v7.55.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.55.0
|
package/autonomy/run.sh
CHANGED
|
@@ -8402,6 +8402,87 @@ _semantic_gate_and_surface() {
|
|
|
8402
8402
|
return "$_rc"
|
|
8403
8403
|
}
|
|
8404
8404
|
|
|
8405
|
+
# P1-4 invariant/property gate (bash-route parity, v7.51.0). The Bun route
|
|
8406
|
+
# ships an invariant toggle (loki-ts/src/runner/quality_gates.ts:2057,
|
|
8407
|
+
# `invariants: flag("LOKI_GATE_INVARIANTS", false)`) running
|
|
8408
|
+
# tests/detect-invariant-violations.sh --strict; the bash route had NO
|
|
8409
|
+
# counterpart, so the bash/Bun parity test reported "Only in bun:
|
|
8410
|
+
# LOKI_GATE_INVARIANTS". This wires the bash side, mirroring
|
|
8411
|
+
# enforce_semantic_integrity's structure byte-for-byte:
|
|
8412
|
+
# - detector --strict exit-code contract (tests/detect-invariant-violations.sh
|
|
8413
|
+
# :347-353): rc 1 iff CRITICAL/HIGH present, rc 0 otherwise.
|
|
8414
|
+
# - detector honors LOKI_SCAN_DIR (tests/detect-invariant-violations.sh:123),
|
|
8415
|
+
# so the wrapper exports it to the TARGET project (cwd alone does NOT
|
|
8416
|
+
# redirect the scan).
|
|
8417
|
+
# The PLURAL token LOKI_GATE_INVARIANTS is used deliberately to match the Bun
|
|
8418
|
+
# readToggles flag name; the detector's own reference comment suggests a
|
|
8419
|
+
# singular default-on variant (no trailing S), which is NOT used here (parity
|
|
8420
|
+
# needs the plural, and this gate is OPT-IN / default OFF like the semantic
|
|
8421
|
+
# gate).
|
|
8422
|
+
enforce_invariant_integrity() {
|
|
8423
|
+
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
8424
|
+
local quality_dir="$loki_dir/quality"
|
|
8425
|
+
mkdir -p "$quality_dir"
|
|
8426
|
+
local findings_file="$quality_dir/invariant-findings.txt"
|
|
8427
|
+
local detector="$SCRIPT_DIR/../tests/detect-invariant-violations.sh"
|
|
8428
|
+
local gate_timeout="${LOKI_GATE_TIMEOUT:-300}"
|
|
8429
|
+
|
|
8430
|
+
if [ ! -f "$detector" ]; then
|
|
8431
|
+
log_info "Invariant gate: detector not found, skipping (inconclusive)"
|
|
8432
|
+
rm -f "$findings_file" 2>/dev/null || true
|
|
8433
|
+
return 0
|
|
8434
|
+
fi
|
|
8435
|
+
|
|
8436
|
+
local output rc
|
|
8437
|
+
# --strict exits 1 iff CRITICAL/HIGH present; 0 otherwise (clean wrapper).
|
|
8438
|
+
output=$(cd "${TARGET_DIR:-.}" && LOKI_SCAN_DIR="${TARGET_DIR:-.}" \
|
|
8439
|
+
timeout "$gate_timeout" bash "$detector" --strict 2>&1)
|
|
8440
|
+
rc=$?
|
|
8441
|
+
|
|
8442
|
+
# timeout exit 124 -- inconclusive, never block on a hang (deny-filter)
|
|
8443
|
+
if [ "$rc" -eq 124 ]; then
|
|
8444
|
+
log_warn "Invariant gate: detector timed out after ${gate_timeout}s -- inconclusive"
|
|
8445
|
+
rm -f "$findings_file" 2>/dev/null || true
|
|
8446
|
+
return 0
|
|
8447
|
+
fi
|
|
8448
|
+
|
|
8449
|
+
if [ "$rc" -eq 1 ]; then
|
|
8450
|
+
# rc 1 == one or more CRITICAL/HIGH findings. Persist per-finding text.
|
|
8451
|
+
{
|
|
8452
|
+
echo "# Invariant findings (CRITICAL/HIGH block this completion)"
|
|
8453
|
+
echo "$output" | grep -E '\[(CRITICAL|HIGH|MEDIUM|LOW)\]' || true
|
|
8454
|
+
} > "$findings_file"
|
|
8455
|
+
log_warn "Invariant gate: CRITICAL/HIGH invariant violations detected -- BLOCK"
|
|
8456
|
+
return 1
|
|
8457
|
+
fi
|
|
8458
|
+
|
|
8459
|
+
# rc 0 (and any other non-1, non-124 code, e.g. a malformed run) -> PASS.
|
|
8460
|
+
# Route any MED/LOW advisory findings to the injection file, else clear it.
|
|
8461
|
+
local med_low
|
|
8462
|
+
med_low=$(echo "$output" | grep -E '\[(MEDIUM|LOW)\]' || true)
|
|
8463
|
+
if [ -n "$med_low" ]; then
|
|
8464
|
+
{
|
|
8465
|
+
echo "# Invariant advisory findings (MED/LOW, non-blocking)"
|
|
8466
|
+
echo "$med_low"
|
|
8467
|
+
} > "$findings_file"
|
|
8468
|
+
else
|
|
8469
|
+
rm -f "$findings_file" 2>/dev/null || true
|
|
8470
|
+
fi
|
|
8471
|
+
log_info "Invariant gate: PASS"
|
|
8472
|
+
return 0
|
|
8473
|
+
}
|
|
8474
|
+
|
|
8475
|
+
# Thin wrapper mirroring _semantic_gate_and_surface so the completion-promise
|
|
8476
|
+
# elif arm reads cleanly (`! _invariant_gate_and_surface`). Returns nonzero
|
|
8477
|
+
# ONLY when enforce_invariant_integrity saw an rc-1 (CRITICAL/HIGH) result; all
|
|
8478
|
+
# deny-filter cases already collapse to 0 inside enforce_invariant_integrity,
|
|
8479
|
+
# so this never blocks a clean run.
|
|
8480
|
+
_invariant_gate_and_surface() {
|
|
8481
|
+
local _rc=0
|
|
8482
|
+
enforce_invariant_integrity || _rc=$?
|
|
8483
|
+
return "$_rc"
|
|
8484
|
+
}
|
|
8485
|
+
|
|
8405
8486
|
# ============================================================================
|
|
8406
8487
|
# 3-Reviewer Parallel Code Review (v5.35.0)
|
|
8407
8488
|
# Specialist pool from skills/quality-gates.md with blind review
|
|
@@ -15467,6 +15548,22 @@ else:
|
|
|
15467
15548
|
log_warn "Completion claim rejected: semantic test-authenticity gate found CRITICAL/HIGH fake-test problem(s)."
|
|
15468
15549
|
log_warn " Details under .loki/quality/semantic-findings.txt ; opt-in gate -- disable with LOKI_GATE_SEMANTIC_TESTS=false"
|
|
15469
15550
|
# Fall through; keep iterating until the fake tests are fixed.
|
|
15551
|
+
# P1-4: invariant/property gate (OPT-IN, default OFF). Mirrors the
|
|
15552
|
+
# semantic arm above and the Bun route's invariants toggle
|
|
15553
|
+
# (loki-ts/src/runner/quality_gates.ts:2057). Guarded by
|
|
15554
|
+
# LOKI_GATE_INVARIANTS (PLURAL, to match the Bun readToggles flag
|
|
15555
|
+
# name; the detector's reference-comment singular default-on variant
|
|
15556
|
+
# without the trailing S is deliberately NOT used). Accepts "true" or
|
|
15557
|
+
# "1"; default OFF means the arm never runs (zero cost, never
|
|
15558
|
+
# blocks). When enabled it runs detect-invariant-violations.sh
|
|
15559
|
+
# --strict and rejects completion ONLY on a CRITICAL/HIGH (rc 1)
|
|
15560
|
+
# finding; clean / detector-absent / timeout / malformed all
|
|
15561
|
+
# collapse to a pass inside _invariant_gate_and_surface, so the
|
|
15562
|
+
# autonomous loop can never deadlock on a clean run.
|
|
15563
|
+
elif [ "$_completion_claimed" = 1 ] && { [ "${LOKI_GATE_INVARIANTS:-false}" = "true" ] || [ "${LOKI_GATE_INVARIANTS:-false}" = "1" ]; } && type _invariant_gate_and_surface &>/dev/null && ! _invariant_gate_and_surface; then
|
|
15564
|
+
log_warn "Completion claim rejected: invariant gate found CRITICAL/HIGH invariant/property violation(s)."
|
|
15565
|
+
log_warn " Details under .loki/quality/invariant-findings.txt ; opt-in gate -- disable with LOKI_GATE_INVARIANTS=false"
|
|
15566
|
+
# Fall through; keep iterating until the invariant violations are fixed.
|
|
15470
15567
|
elif [ "$_completion_claimed" = 1 ]; then
|
|
15471
15568
|
echo ""
|
|
15472
15569
|
if [ -n "$COMPLETION_PROMISE" ]; then
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.
|
|
5
|
+
**Version:** v7.55.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
395
395
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
396
396
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
397
397
|
-v $(pwd):/workspace -w /workspace \
|
|
398
|
-
asklokesh/loki-mode:7.
|
|
398
|
+
asklokesh/loki-mode:7.55.0 start ./my-spec.md
|
|
399
399
|
```
|
|
400
400
|
|
|
401
401
|
##### docker compose + .env (no host install)
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var r6=Object.defineProperty;var t6=($)=>$;function i6($,Q){this[$]=t6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)r6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:i6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var D1={};h(D1,{lokiDir:()=>P,homeLokiDir:()=>n$,findRepoRootForVersion:()=>o$,REPO_ROOT:()=>g});import{resolve as n,dirname as d$}from"path";import{fileURLToPath as e6}from"url";import{existsSync as P$}from"fs";import{homedir as $Q}from"os";function QQ(){let $=S1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=d$($);if(Z===$)break;$=Z}return n(S1,"..","..","..")}function o$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=d$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function n$(){return n($Q(),".loki")}var S1,g;var b=L(()=>{S1=d$(e6(import.meta.url));g=QQ()});import{readFileSync as ZQ}from"fs";import{resolve as zQ,dirname as XQ}from"path";import{fileURLToPath as KQ}from"url";function j$(){if($$!==null)return $$;let $="7.
|
|
2
|
+
var r6=Object.defineProperty;var t6=($)=>$;function i6($,Q){this[$]=t6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)r6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:i6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var D1={};h(D1,{lokiDir:()=>P,homeLokiDir:()=>n$,findRepoRootForVersion:()=>o$,REPO_ROOT:()=>g});import{resolve as n,dirname as d$}from"path";import{fileURLToPath as e6}from"url";import{existsSync as P$}from"fs";import{homedir as $Q}from"os";function QQ(){let $=S1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=d$($);if(Z===$)break;$=Z}return n(S1,"..","..","..")}function o$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=d$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function n$(){return n($Q(),".loki")}var S1,g;var b=L(()=>{S1=d$(e6(import.meta.url));g=QQ()});import{readFileSync as ZQ}from"fs";import{resolve as zQ,dirname as XQ}from"path";import{fileURLToPath as KQ}from"url";function j$(){if($$!==null)return $$;let $="7.55.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=XQ(KQ(import.meta.url)),Z=o$(Q);$$=ZQ(zQ(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var a$=L(()=>{b()});var b1={};h(b1,{runOrThrow:()=>qQ,run:()=>k,commandVersion:()=>WQ,commandExists:()=>f,ShellError:()=>s$});async function k($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function qQ($,Q={}){let Z=await k($,Q);if(Z.exitCode!==0)throw new s$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=VQ($),Z=await k(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function VQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function WQ($,Q="--version"){if(!await f($))return null;let z=await k([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var s$;var d=L(()=>{s$=class s$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return JQ?"":$}var JQ,T,S,_,wZ,I,R,y,V;var c=L(()=>{JQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),_=a("\x1B[1;33m"),wZ=a("\x1B[0;34m"),I=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),V=a("\x1B[0m")});import{existsSync as wQ}from"fs";async function Q$(){if(G$!==void 0)return G$;let $="/opt/homebrew/bin/python3.12";if(wQ($))return G$=$,$;let Q=await f("python3.12");if(Q)return G$=Q,Q;let Z=await f("python3");return G$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Z,"-c",$],Q)}var G$;var q$=L(()=>{d()});var e1={};h(e1,{runStatus:()=>uQ});import{existsSync as v,readFileSync as W$,readdirSync as d1,statSync as o1}from"fs";import{resolve as C,basename as DQ}from"path";import{homedir as CQ}from"os";function n1($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function a1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*k$/Q);if(X>k$)X=k$;let q=k$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=n1($),U=n1(Q);return` ${R}${Z}${V} ${K}[${W}]${V} ${z}% (${J} / ${U})`}async function hQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}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)
|
|
@@ -790,4 +790,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
790
790
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
791
791
|
`),process.stderr.write(s6),2}}l1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var KZ=await XZ(Bun.argv.slice(2));process.exit(KZ);
|
|
792
792
|
|
|
793
|
-
//# debugId=
|
|
793
|
+
//# debugId=75540993435DE5C864756E2164756E21
|
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": "7.
|
|
4
|
+
"version": "7.55.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": "7.
|
|
5
|
+
"version": "7.55.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",
|
package/src/audit/crosslink.js
CHANGED
|
@@ -42,6 +42,8 @@ var { execFileSync } = require('child_process');
|
|
|
42
42
|
var { AuditLog } = require('./log');
|
|
43
43
|
|
|
44
44
|
var CROSSLINK_ACTION = 'audit_crosslink';
|
|
45
|
+
var MANIFEST_LINK_ACTION = 'audit_manifest_link';
|
|
46
|
+
var MANIFEST_FILE = 'loki-run.json';
|
|
45
47
|
var WITNESS_FILE = 'witness.jsonl';
|
|
46
48
|
var PY_GENESIS = '0'.repeat(64);
|
|
47
49
|
|
|
@@ -400,6 +402,185 @@ function verifyUnified(opts) {
|
|
|
400
402
|
};
|
|
401
403
|
}
|
|
402
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Resolve the path to the run manifest (bill-of-materials) written by
|
|
407
|
+
* autonomy/run.sh at <project>/.loki/loki-run.json. Override via
|
|
408
|
+
* opts.manifestPath for tests / non-standard layouts (mirrors the
|
|
409
|
+
* explicit-override idiom used by witnessFile / dashboardAuditDir).
|
|
410
|
+
*/
|
|
411
|
+
function defaultManifestPath(opts) {
|
|
412
|
+
opts = opts || {};
|
|
413
|
+
if (opts.manifestPath) return opts.manifestPath;
|
|
414
|
+
return path.join((opts.projectDir || process.cwd()), '.loki', MANIFEST_FILE);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Hash the manifest exactly as run.sh's _loki_sha256 does: sha256 over the
|
|
419
|
+
* raw file BYTES. We deliberately do NOT JSON.parse-then-re-stringify
|
|
420
|
+
* (that would diverge from the on-disk bytes run.sh hashes and be fragile
|
|
421
|
+
* to formatting). We additionally best-effort parse the bytes only to lift
|
|
422
|
+
* the manifest `schema` field into anchor metadata; a malformed manifest
|
|
423
|
+
* still hashes its bytes and records schema:null rather than aborting.
|
|
424
|
+
*
|
|
425
|
+
* @returns {object} { present, sha256, schema } -- present:false when the
|
|
426
|
+
* file is absent (no fabricated hash).
|
|
427
|
+
*/
|
|
428
|
+
function hashManifest(manifestPath) {
|
|
429
|
+
if (!manifestPath || !fs.existsSync(manifestPath)) {
|
|
430
|
+
return { present: false, sha256: null, schema: null };
|
|
431
|
+
}
|
|
432
|
+
var buf = fs.readFileSync(manifestPath);
|
|
433
|
+
var sha = crypto.createHash('sha256').update(buf).digest('hex');
|
|
434
|
+
var schema = null;
|
|
435
|
+
try {
|
|
436
|
+
var parsed = JSON.parse(buf.toString('utf8'));
|
|
437
|
+
if (parsed && typeof parsed.schema === 'string') schema = parsed.schema;
|
|
438
|
+
} catch (_) { /* malformed manifest: hash bytes anyway, schema stays null */ }
|
|
439
|
+
return { present: true, sha256: sha, schema: schema };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Link the run manifest (loki-run.json, the build bill-of-materials) into
|
|
444
|
+
* the agent audit chain so the manifest becomes tamper-evident and
|
|
445
|
+
* verifiable against the evidence chain.
|
|
446
|
+
*
|
|
447
|
+
* The manifest already embeds sha256 hashes of the evidence files
|
|
448
|
+
* (test_results, coverage, ...) computed by run.sh. By recording the
|
|
449
|
+
* manifest's OWN byte-hash as an `audit_manifest_link` entry, the anchor
|
|
450
|
+
* is protected by the agent chain's hash linkage, and the evidence hashes
|
|
451
|
+
* inside the manifest become transitively tamper-evident (mutating the
|
|
452
|
+
* manifest to point at different evidence changes its byte-hash, which no
|
|
453
|
+
* longer matches the anchored hash; mutating the anchor breaks chain
|
|
454
|
+
* verification). We hash the manifest itself only -- we do NOT re-hash the
|
|
455
|
+
* evidence files here (run.sh already did, and the manifest pins them).
|
|
456
|
+
*
|
|
457
|
+
* HONEST behavior:
|
|
458
|
+
* - Manifest absent -> no-op, returns { linked:false, present:false }.
|
|
459
|
+
* No fabricated link is recorded.
|
|
460
|
+
* - Manifest present -> hash recorded as an anchor; returns
|
|
461
|
+
* { linked:true, present:true, anchor, manifestSha256, ... }.
|
|
462
|
+
*
|
|
463
|
+
* Note: this records tamper-EVIDENCE (in-place edits are detected), not
|
|
464
|
+
* tamper-PROOF against a full downstream chain rewrite -- that is what
|
|
465
|
+
* writeWitness (external witness) is for.
|
|
466
|
+
*
|
|
467
|
+
* NOT auto-invoked from run.sh in this wave (integration is the run.sh
|
|
468
|
+
* owner's territory). Intended call site: after run.sh writes
|
|
469
|
+
* .loki/loki-run.json in build_completion_summary (autonomy/run.sh ~2895,
|
|
470
|
+
* just after os.replace(tmp, out)), call
|
|
471
|
+
* node -e "require('./src/audit').linkManifest({projectDir:'<dir>'})"
|
|
472
|
+
* (or the JS API audit.linkManifest()) on every terminal path.
|
|
473
|
+
*
|
|
474
|
+
* @param {object} [opts]
|
|
475
|
+
* @param {string} [opts.projectDir] project dir for the agent log + manifest
|
|
476
|
+
* @param {string} [opts.logDir] explicit agent log dir (tests)
|
|
477
|
+
* @param {string} [opts.manifestPath] explicit manifest path (tests)
|
|
478
|
+
* @param {string} [opts.who] actor recorded on the anchor
|
|
479
|
+
* @returns {object}
|
|
480
|
+
*/
|
|
481
|
+
function linkManifest(opts) {
|
|
482
|
+
opts = opts || {};
|
|
483
|
+
var manifestPath = defaultManifestPath(opts);
|
|
484
|
+
var h = hashManifest(manifestPath);
|
|
485
|
+
if (!h.present) {
|
|
486
|
+
return {
|
|
487
|
+
linked: false, present: false, manifestPath: manifestPath,
|
|
488
|
+
reason: 'run manifest absent (no-op)',
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
var log = new AuditLog(opts);
|
|
492
|
+
var anchor = log.record({
|
|
493
|
+
who: opts.who || 'audit-manifest-link',
|
|
494
|
+
what: MANIFEST_LINK_ACTION,
|
|
495
|
+
where: manifestPath,
|
|
496
|
+
why: 'link run manifest (bill-of-materials) into agent audit chain',
|
|
497
|
+
metadata: {
|
|
498
|
+
manifestPath: manifestPath,
|
|
499
|
+
manifestSha256: h.sha256,
|
|
500
|
+
manifestSchema: h.schema,
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
log.flush();
|
|
504
|
+
log.destroy();
|
|
505
|
+
return {
|
|
506
|
+
linked: true, present: true, manifestPath: manifestPath,
|
|
507
|
+
manifestSha256: h.sha256, manifestSchema: h.schema, anchor: anchor,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Verify the run-manifest link against the evidence chain.
|
|
513
|
+
*
|
|
514
|
+
* Composes TWO checks (mirroring verifyUnified rather than a bare disk-vs
|
|
515
|
+
* -recorded compare):
|
|
516
|
+
* 1. Agent chain integrity (AuditLog.verifyChain()). This catches an
|
|
517
|
+
* edit to the ANCHOR entry itself (e.g. someone rewrites the recorded
|
|
518
|
+
* manifestSha256), not just an edit to the manifest file.
|
|
519
|
+
* 2. Manifest reconciliation: re-hash the on-disk manifest and require
|
|
520
|
+
* it to equal the hash recorded by the MOST RECENT manifest-link
|
|
521
|
+
* anchor. A mutated manifest no longer matches -> tamper detected.
|
|
522
|
+
*
|
|
523
|
+
* HONEST empty cases (distinguishable from a real pass via `present`):
|
|
524
|
+
* - No anchor recorded yet -> { present:false, valid:true, reason:... }.
|
|
525
|
+
* - Anchor exists but the manifest file is now gone -> manifest.valid
|
|
526
|
+
* is false (the pinned manifest is missing/cannot be reconciled).
|
|
527
|
+
*
|
|
528
|
+
* @param {object} [opts] projectDir / logDir / manifestPath as linkManifest.
|
|
529
|
+
* @returns {object} { valid, present, chain, manifest }
|
|
530
|
+
*/
|
|
531
|
+
function verifyManifestLink(opts) {
|
|
532
|
+
opts = opts || {};
|
|
533
|
+
var manifestPath = defaultManifestPath(opts);
|
|
534
|
+
|
|
535
|
+
var log = new AuditLog(opts);
|
|
536
|
+
var chain = log.verifyChain();
|
|
537
|
+
var entries = log.readEntries();
|
|
538
|
+
log.destroy();
|
|
539
|
+
|
|
540
|
+
var anchors = entries.filter(function (e) {
|
|
541
|
+
return e.what === MANIFEST_LINK_ACTION;
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
if (anchors.length === 0) {
|
|
545
|
+
return {
|
|
546
|
+
valid: !!chain.valid, present: false, chain: chain,
|
|
547
|
+
manifest: { present: false, valid: true, reason: 'no manifest-link anchor recorded' },
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Most recent anchor pins the current manifest state.
|
|
552
|
+
var anchor = anchors[anchors.length - 1];
|
|
553
|
+
var pinned = (anchor.metadata && anchor.metadata.manifestSha256) || null;
|
|
554
|
+
|
|
555
|
+
var current = hashManifest(manifestPath);
|
|
556
|
+
var manifest = {
|
|
557
|
+
present: true,
|
|
558
|
+
valid: true,
|
|
559
|
+
manifestPath: manifestPath,
|
|
560
|
+
pinnedSha256: pinned,
|
|
561
|
+
currentSha256: current.sha256,
|
|
562
|
+
anchorSeq: anchor.seq,
|
|
563
|
+
error: null,
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
if (!current.present) {
|
|
567
|
+
manifest.valid = false;
|
|
568
|
+
manifest.error = 'manifest pinned by anchor seq ' + anchor.seq +
|
|
569
|
+
' is missing on disk';
|
|
570
|
+
} else if (current.sha256 !== pinned) {
|
|
571
|
+
manifest.valid = false;
|
|
572
|
+
manifest.error = 'manifest hash mismatch at anchor seq ' + anchor.seq +
|
|
573
|
+
' (manifest tampered after linking)';
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
valid: !!chain.valid && manifest.valid,
|
|
578
|
+
present: true,
|
|
579
|
+
chain: chain,
|
|
580
|
+
manifest: manifest,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
403
584
|
module.exports = {
|
|
404
585
|
crossLink: crossLink,
|
|
405
586
|
verifyUnified: verifyUnified,
|
|
@@ -409,5 +590,10 @@ module.exports = {
|
|
|
409
590
|
agentChainTip: agentChainTip,
|
|
410
591
|
unifiedRoot: unifiedRoot,
|
|
411
592
|
defaultDashboardAuditDir: defaultDashboardAuditDir,
|
|
593
|
+
linkManifest: linkManifest,
|
|
594
|
+
verifyManifestLink: verifyManifestLink,
|
|
595
|
+
hashManifest: hashManifest,
|
|
596
|
+
defaultManifestPath: defaultManifestPath,
|
|
412
597
|
CROSSLINK_ACTION: CROSSLINK_ACTION,
|
|
598
|
+
MANIFEST_LINK_ACTION: MANIFEST_LINK_ACTION,
|
|
413
599
|
};
|
package/src/audit/index.js
CHANGED
|
@@ -228,6 +228,27 @@ function writeWitness(opts) {
|
|
|
228
228
|
return crosslink.writeWitness(Object.assign({ projectDir: _projectDir }, opts || {}));
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Link the run manifest (loki-run.json bill-of-materials) into the agent
|
|
233
|
+
* audit chain so it becomes tamper-evident and verifiable against the
|
|
234
|
+
* evidence chain. No-op (honest) when the manifest is absent.
|
|
235
|
+
* See src/audit/crosslink.js (linkManifest).
|
|
236
|
+
*/
|
|
237
|
+
function linkManifest(opts) {
|
|
238
|
+
if (!_initialized) init();
|
|
239
|
+
return crosslink.linkManifest(Object.assign({ projectDir: _projectDir }, opts || {}));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Verify the run-manifest link: agent chain integrity AND the on-disk
|
|
244
|
+
* manifest still matching the hash pinned by the most recent
|
|
245
|
+
* manifest-link anchor. See src/audit/crosslink.js (verifyManifestLink).
|
|
246
|
+
*/
|
|
247
|
+
function verifyManifestLink(opts) {
|
|
248
|
+
if (!_initialized) init();
|
|
249
|
+
return crosslink.verifyManifestLink(Object.assign({ projectDir: _projectDir }, opts || {}));
|
|
250
|
+
}
|
|
251
|
+
|
|
231
252
|
/**
|
|
232
253
|
* Destroy audit trail (for testing).
|
|
233
254
|
*/
|
|
@@ -255,6 +276,8 @@ module.exports = {
|
|
|
255
276
|
crossLink: crossLink,
|
|
256
277
|
verifyUnified: verifyUnified,
|
|
257
278
|
writeWitness: writeWitness,
|
|
279
|
+
linkManifest: linkManifest,
|
|
280
|
+
verifyManifestLink: verifyManifestLink,
|
|
258
281
|
};
|
|
259
282
|
|
|
260
283
|
// CLI entry point: `node src/audit/index.js report <type> <projectDir>`.
|