loki-mode 7.85.0 → 7.87.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/README.md +76 -0
- package/SKILL.md +4 -2
- package/VERSION +1 -1
- package/autonomy/lib/proof-generator.py +64 -0
- package/autonomy/lib/secure-scan.py +652 -0
- package/autonomy/loki +108 -0
- package/autonomy/run.sh +163 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +56 -0
- package/dashboard/static/index.html +93 -0
- 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/autonomy/loki
CHANGED
|
@@ -16500,6 +16500,10 @@ main() {
|
|
|
16500
16500
|
# Receipt surface): same subcommands (list/show/verify/open/share).
|
|
16501
16501
|
cmd_proof "$@"
|
|
16502
16502
|
;;
|
|
16503
|
+
secure)
|
|
16504
|
+
# Secure-by-default gate surface: inspect findings + manage waivers.
|
|
16505
|
+
cmd_secure "$@"
|
|
16506
|
+
;;
|
|
16503
16507
|
bench)
|
|
16504
16508
|
cmd_bench "$@"
|
|
16505
16509
|
;;
|
|
@@ -30434,6 +30438,110 @@ cmd_bench() {
|
|
|
30434
30438
|
bash "$bench_sh" "$@"
|
|
30435
30439
|
}
|
|
30436
30440
|
|
|
30441
|
+
# loki secure - the secure-by-default gate surface (v7.87.0).
|
|
30442
|
+
# Subcommands: list (show findings) | waive <rule> <file> [reason] | unwaive.
|
|
30443
|
+
# Waivers are written to .loki/quality/security-waivers.json, which the gate
|
|
30444
|
+
# (run.sh run_secure_scan) and the Evidence Receipt both READ + honor. The gate
|
|
30445
|
+
# is advisory by default; LOKI_SECURE_GATE=block makes un-waived HIGH findings
|
|
30446
|
+
# block. Honest: a waiver is RECORDED in the receipt (accepted with intent), never
|
|
30447
|
+
# silently hides a finding.
|
|
30448
|
+
cmd_secure() {
|
|
30449
|
+
local loki_dir="${LOKI_DIR:-.loki}"
|
|
30450
|
+
local quality_dir="${loki_dir}/quality"
|
|
30451
|
+
local findings_file="${quality_dir}/security-findings.json"
|
|
30452
|
+
local waivers_file="${quality_dir}/security-waivers.json"
|
|
30453
|
+
local sub="${1:-}"
|
|
30454
|
+
[ $# -gt 0 ] && shift
|
|
30455
|
+
case "$sub" in
|
|
30456
|
+
""|--help|-h|help)
|
|
30457
|
+
echo -e "${BOLD}loki secure${NC} - secure-by-default gate: findings + waivers"
|
|
30458
|
+
echo ""
|
|
30459
|
+
echo "Usage: loki secure <subcommand> [args]"
|
|
30460
|
+
echo ""
|
|
30461
|
+
echo "Subcommands:"
|
|
30462
|
+
echo " list Show security findings from the last scan"
|
|
30463
|
+
echo " waive <rule> <file> [reason] Waive a finding (accepted with intent)"
|
|
30464
|
+
echo " unwaive <rule> <file> Remove a waiver"
|
|
30465
|
+
echo ""
|
|
30466
|
+
echo "The gate is advisory by default; set LOKI_SECURE_GATE=block to make"
|
|
30467
|
+
echo "un-waived HIGH findings block completion. Waivers are recorded in the"
|
|
30468
|
+
echo "Evidence Receipt (loki proof show) -- they are never hidden."
|
|
30469
|
+
[ "$sub" = "" ] && exit 1
|
|
30470
|
+
exit 0
|
|
30471
|
+
;;
|
|
30472
|
+
list)
|
|
30473
|
+
if [ ! -f "$findings_file" ]; then
|
|
30474
|
+
echo -e "${YELLOW}No security scan results yet.${NC} Run 'loki start' (the gate runs in the review phase)."
|
|
30475
|
+
exit 0
|
|
30476
|
+
fi
|
|
30477
|
+
if command -v jq &>/dev/null; then
|
|
30478
|
+
jq '.findings' "$findings_file" 2>/dev/null || cat "$findings_file"
|
|
30479
|
+
else
|
|
30480
|
+
LOKI_SEC_F="$findings_file" python3 -c "import json,os; d=json.load(open(os.environ['LOKI_SEC_F'])); [print('%s [%s] %s%s -- %s' % (f.get('severity','?'), f.get('rule','?'), f.get('file','?'), (':'+str(f['line'])) if f.get('line') else '', f.get('fix',''))) for f in d.get('findings',[])] or print('No findings.')"
|
|
30481
|
+
fi
|
|
30482
|
+
exit 0
|
|
30483
|
+
;;
|
|
30484
|
+
waive)
|
|
30485
|
+
local rule="${1:-}" file="${2:-}" reason="${3:-waived via loki secure}"
|
|
30486
|
+
if [ -z "$rule" ] || [ -z "$file" ]; then
|
|
30487
|
+
echo -e "${RED}Usage: loki secure waive <rule> <file> [reason]${NC}" >&2
|
|
30488
|
+
exit 2
|
|
30489
|
+
fi
|
|
30490
|
+
mkdir -p "$quality_dir"
|
|
30491
|
+
LOKI_SEC_W="$waivers_file" LOKI_SEC_RULE="$rule" LOKI_SEC_FILE="$file" LOKI_SEC_REASON="$reason" python3 - <<'PYW'
|
|
30492
|
+
import json, os
|
|
30493
|
+
p = os.environ["LOKI_SEC_W"]
|
|
30494
|
+
try:
|
|
30495
|
+
with open(p) as f: data = json.load(f)
|
|
30496
|
+
if not isinstance(data, dict): data = {}
|
|
30497
|
+
except Exception:
|
|
30498
|
+
data = {}
|
|
30499
|
+
waivers = data.get("waivers")
|
|
30500
|
+
if not isinstance(waivers, list): waivers = []
|
|
30501
|
+
rule, fl, reason = os.environ["LOKI_SEC_RULE"], os.environ["LOKI_SEC_FILE"], os.environ["LOKI_SEC_REASON"]
|
|
30502
|
+
if not any(w.get("rule") == rule and w.get("file") == fl for w in waivers if isinstance(w, dict)):
|
|
30503
|
+
waivers.append({"rule": rule, "file": fl, "reason": reason})
|
|
30504
|
+
data["waivers"] = waivers
|
|
30505
|
+
tmp = p + ".tmp"
|
|
30506
|
+
with open(tmp, "w") as f: json.dump(data, f, indent=2)
|
|
30507
|
+
os.replace(tmp, p)
|
|
30508
|
+
print("Waived %s on %s (recorded in the Evidence Receipt)." % (rule, fl))
|
|
30509
|
+
PYW
|
|
30510
|
+
exit $?
|
|
30511
|
+
;;
|
|
30512
|
+
unwaive)
|
|
30513
|
+
local rule="${1:-}" file="${2:-}"
|
|
30514
|
+
if [ -z "$rule" ] || [ -z "$file" ]; then
|
|
30515
|
+
echo -e "${RED}Usage: loki secure unwaive <rule> <file>${NC}" >&2
|
|
30516
|
+
exit 2
|
|
30517
|
+
fi
|
|
30518
|
+
[ -f "$waivers_file" ] || { echo "No waivers to remove."; exit 0; }
|
|
30519
|
+
LOKI_SEC_W="$waivers_file" LOKI_SEC_RULE="$rule" LOKI_SEC_FILE="$file" python3 - <<'PYU'
|
|
30520
|
+
import json, os
|
|
30521
|
+
p = os.environ["LOKI_SEC_W"]
|
|
30522
|
+
try:
|
|
30523
|
+
with open(p) as f: data = json.load(f)
|
|
30524
|
+
except Exception:
|
|
30525
|
+
data = {}
|
|
30526
|
+
waivers = [w for w in (data.get("waivers") or [])
|
|
30527
|
+
if not (isinstance(w, dict) and w.get("rule") == os.environ["LOKI_SEC_RULE"]
|
|
30528
|
+
and w.get("file") == os.environ["LOKI_SEC_FILE"])]
|
|
30529
|
+
data["waivers"] = waivers
|
|
30530
|
+
tmp = p + ".tmp"
|
|
30531
|
+
with open(tmp, "w") as f: json.dump(data, f, indent=2)
|
|
30532
|
+
os.replace(tmp, p)
|
|
30533
|
+
print("Removed waiver for %s on %s." % (os.environ["LOKI_SEC_RULE"], os.environ["LOKI_SEC_FILE"]))
|
|
30534
|
+
PYU
|
|
30535
|
+
exit $?
|
|
30536
|
+
;;
|
|
30537
|
+
*)
|
|
30538
|
+
echo -e "${RED}Unknown subcommand: secure $sub${NC}" >&2
|
|
30539
|
+
echo "Try: loki secure --help"
|
|
30540
|
+
exit 2
|
|
30541
|
+
;;
|
|
30542
|
+
esac
|
|
30543
|
+
}
|
|
30544
|
+
|
|
30437
30545
|
# loki proof - inspect and share proof-of-run artifacts (.loki/proofs/<id>/).
|
|
30438
30546
|
# Subcommands: list | show <id> | open <id> | share <id>.
|
|
30439
30547
|
# The proof.json schema is frozen (R1 spec). Reads are tolerant of missing
|
package/autonomy/run.sh
CHANGED
|
@@ -7309,6 +7309,157 @@ SAFEOF
|
|
|
7309
7309
|
fi
|
|
7310
7310
|
}
|
|
7311
7311
|
|
|
7312
|
+
# ============================================================================
|
|
7313
|
+
# Secure-by-default scan (v7.87.0 - Loop 4)
|
|
7314
|
+
# Runs the high-precision rule engine (autonomy/lib/secure-scan.py) over the
|
|
7315
|
+
# generated app and reports known-bad security patterns.
|
|
7316
|
+
#
|
|
7317
|
+
# ADVISORY BY DEFAULT (mirrors the ktlint/detekt advisory linters above):
|
|
7318
|
+
# findings are reported via log_warn + the receipt json, but do NOT block. This
|
|
7319
|
+
# guarantees no existing build starts blocking on this new gate.
|
|
7320
|
+
#
|
|
7321
|
+
# OPT-IN BLOCK: only when LOKI_SECURE_GATE=block do un-waived HIGH findings
|
|
7322
|
+
# cause a blocking gate failure (return 1, same mechanism the other gates use).
|
|
7323
|
+
#
|
|
7324
|
+
# Waivers: .loki/quality/security-waivers.json ({"waivers":[{rule,file},...]})
|
|
7325
|
+
# is READ here and honored (matched findings recorded as waived, never counted
|
|
7326
|
+
# active). The waiver-write surface is a separate slice.
|
|
7327
|
+
#
|
|
7328
|
+
# Honest degrade: if python3 or secure-scan.py is absent, pass through cleanly
|
|
7329
|
+
# (no crash, no block), exactly like the optional linters.
|
|
7330
|
+
# ============================================================================
|
|
7331
|
+
run_secure_scan() {
|
|
7332
|
+
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
7333
|
+
local quality_dir="$loki_dir/quality"
|
|
7334
|
+
mkdir -p "$quality_dir"
|
|
7335
|
+
|
|
7336
|
+
local out_file="$quality_dir/security-findings.json"
|
|
7337
|
+
local waivers_file="$quality_dir/security-waivers.json"
|
|
7338
|
+
local scanner="$SCRIPT_DIR/lib/secure-scan.py"
|
|
7339
|
+
|
|
7340
|
+
# Honest pass-through if the engine or python3 is unavailable. Still write a
|
|
7341
|
+
# valid (empty) receipt so downstream consumers never read malformed JSON.
|
|
7342
|
+
if ! command -v python3 >/dev/null 2>&1 || [ ! -f "$scanner" ]; then
|
|
7343
|
+
cat > "$out_file" << 'SECEMPTY'
|
|
7344
|
+
{"rules_version":null,"findings":[],"summary":{"total":0,"by_severity":{}},"skipped":"scanner-unavailable"}
|
|
7345
|
+
SECEMPTY
|
|
7346
|
+
log_info "Security scan: secure-scan.py or python3 not available, skipping (pass-through)"
|
|
7347
|
+
return 0
|
|
7348
|
+
fi
|
|
7349
|
+
|
|
7350
|
+
# Run the scanner. exit 0 = no findings, 1 = findings, 2 = bad input.
|
|
7351
|
+
local raw rc=0
|
|
7352
|
+
raw=$(python3 "$scanner" "${TARGET_DIR:-.}" --json 2>/dev/null) || rc=$?
|
|
7353
|
+
if [ "$rc" -eq 2 ] || [ -z "$raw" ]; then
|
|
7354
|
+
cat > "$out_file" << 'SECEMPTY'
|
|
7355
|
+
{"rules_version":null,"findings":[],"summary":{"total":0,"by_severity":{}},"skipped":"scanner-error"}
|
|
7356
|
+
SECEMPTY
|
|
7357
|
+
log_info "Security scan: scanner returned no parseable output, skipping (pass-through)"
|
|
7358
|
+
return 0
|
|
7359
|
+
fi
|
|
7360
|
+
|
|
7361
|
+
# Apply waivers, build the receipt json, and emit a machine-readable verdict.
|
|
7362
|
+
# All policy lives in this one python pass so the bash stays bash-3.2 safe.
|
|
7363
|
+
# It prints a final line: ACTIVE_HIGH=<n>\tACTIVE_TOTAL=<n>\tWAIVED=<n>
|
|
7364
|
+
# and writes the enriched receipt (findings carry a "waived" bool).
|
|
7365
|
+
local verdict
|
|
7366
|
+
verdict=$(_SEC_RAW="$raw" _SEC_WAIVERS="$waivers_file" _SEC_OUT="$out_file" python3 -c '
|
|
7367
|
+
import json, os, sys
|
|
7368
|
+
raw = os.environ.get("_SEC_RAW", "")
|
|
7369
|
+
waivers_file = os.environ.get("_SEC_WAIVERS", "")
|
|
7370
|
+
out_file = os.environ.get("_SEC_OUT", "")
|
|
7371
|
+
|
|
7372
|
+
try:
|
|
7373
|
+
data = json.loads(raw)
|
|
7374
|
+
except Exception:
|
|
7375
|
+
data = {"rules_version": None, "findings": [], "summary": {"total": 0, "by_severity": {}}}
|
|
7376
|
+
|
|
7377
|
+
# Load waivers: {"waivers":[{"rule":..,"file":..}, ...]}. Match on rule+file.
|
|
7378
|
+
waived_set = set()
|
|
7379
|
+
try:
|
|
7380
|
+
with open(waivers_file) as f:
|
|
7381
|
+
wdoc = json.load(f)
|
|
7382
|
+
for w in wdoc.get("waivers", []):
|
|
7383
|
+
r = w.get("rule"); fl = w.get("file")
|
|
7384
|
+
if r is not None and fl is not None:
|
|
7385
|
+
waived_set.add((r, fl))
|
|
7386
|
+
except (OSError, json.JSONDecodeError, AttributeError):
|
|
7387
|
+
pass
|
|
7388
|
+
|
|
7389
|
+
findings = data.get("findings", []) or []
|
|
7390
|
+
active_high = 0
|
|
7391
|
+
active_total = 0
|
|
7392
|
+
waived_count = 0
|
|
7393
|
+
for fnd in findings:
|
|
7394
|
+
key = (fnd.get("rule"), fnd.get("file"))
|
|
7395
|
+
is_waived = key in waived_set
|
|
7396
|
+
fnd["waived"] = is_waived
|
|
7397
|
+
if is_waived:
|
|
7398
|
+
waived_count += 1
|
|
7399
|
+
else:
|
|
7400
|
+
active_total += 1
|
|
7401
|
+
if str(fnd.get("severity", "")).upper() == "HIGH":
|
|
7402
|
+
active_high += 1
|
|
7403
|
+
|
|
7404
|
+
data["waived"] = waived_count
|
|
7405
|
+
data["active"] = active_total
|
|
7406
|
+
try:
|
|
7407
|
+
with open(out_file, "w") as f:
|
|
7408
|
+
json.dump(data, f, indent=2)
|
|
7409
|
+
except OSError:
|
|
7410
|
+
pass
|
|
7411
|
+
|
|
7412
|
+
sys.stdout.write("ACTIVE_HIGH=%d\tACTIVE_TOTAL=%d\tWAIVED=%d" % (active_high, active_total, waived_count))
|
|
7413
|
+
' 2>/dev/null) || verdict=""
|
|
7414
|
+
|
|
7415
|
+
if [ -z "$verdict" ]; then
|
|
7416
|
+
# python policy pass failed unexpectedly; preserve the raw scan as the
|
|
7417
|
+
# receipt so nothing is lost, and pass through (never crash the gate).
|
|
7418
|
+
printf '%s\n' "$raw" > "$out_file" 2>/dev/null || true
|
|
7419
|
+
log_info "Security scan: result recorded (policy pass unavailable, advisory)"
|
|
7420
|
+
return 0
|
|
7421
|
+
fi
|
|
7422
|
+
|
|
7423
|
+
local active_high active_total waived
|
|
7424
|
+
active_high=$(printf '%s' "$verdict" | sed -n 's/.*ACTIVE_HIGH=\([0-9]*\).*/\1/p')
|
|
7425
|
+
active_total=$(printf '%s' "$verdict" | sed -n 's/.*ACTIVE_TOTAL=\([0-9]*\).*/\1/p')
|
|
7426
|
+
waived=$(printf '%s' "$verdict" | sed -n 's/.*WAIVED=\([0-9]*\).*/\1/p')
|
|
7427
|
+
active_high=${active_high:-0}
|
|
7428
|
+
active_total=${active_total:-0}
|
|
7429
|
+
waived=${waived:-0}
|
|
7430
|
+
|
|
7431
|
+
if [ "$active_total" -eq 0 ]; then
|
|
7432
|
+
log_info "Security scan: no active findings (waived: $waived)"
|
|
7433
|
+
return 0
|
|
7434
|
+
fi
|
|
7435
|
+
|
|
7436
|
+
# Actionable advisory summary: rule + file + fix, from the receipt json.
|
|
7437
|
+
log_warn "Security scan: $active_total active finding(s) (HIGH: $active_high, waived: $waived)"
|
|
7438
|
+
_SEC_OUT="$out_file" python3 -c '
|
|
7439
|
+
import json, os
|
|
7440
|
+
try:
|
|
7441
|
+
with open(os.environ["_SEC_OUT"]) as f:
|
|
7442
|
+
data = json.load(f)
|
|
7443
|
+
except Exception:
|
|
7444
|
+
data = {"findings": []}
|
|
7445
|
+
for fnd in data.get("findings", []):
|
|
7446
|
+
if fnd.get("waived"):
|
|
7447
|
+
continue
|
|
7448
|
+
print(" [%s] %s %s:%s -- %s | fix: %s" % (
|
|
7449
|
+
fnd.get("severity", "?"), fnd.get("rule", "?"),
|
|
7450
|
+
fnd.get("file", "?"), fnd.get("line", "?"),
|
|
7451
|
+
fnd.get("message", ""), fnd.get("fix", "")))
|
|
7452
|
+
' 2>/dev/null | while IFS= read -r line; do log_warn "$line"; done
|
|
7453
|
+
|
|
7454
|
+
# OPT-IN BLOCK: only un-waived HIGH findings block, and only when explicitly
|
|
7455
|
+
# enabled. Advisory default returns 0 (never surprise-blocks).
|
|
7456
|
+
if [ "${LOKI_SECURE_GATE:-advisory}" = "block" ] && [ "$active_high" -gt 0 ]; then
|
|
7457
|
+
log_warn "Security gate: $active_high un-waived HIGH finding(s) - BLOCK (LOKI_SECURE_GATE=block)"
|
|
7458
|
+
return 1
|
|
7459
|
+
fi
|
|
7460
|
+
return 0
|
|
7461
|
+
}
|
|
7462
|
+
|
|
7312
7463
|
#===============================================================================
|
|
7313
7464
|
# Gate Failure Tracking (v6.10.0)
|
|
7314
7465
|
#===============================================================================
|
|
@@ -15262,6 +15413,18 @@ if __name__ == "__main__":
|
|
|
15262
15413
|
log_warn "Static analysis FAILED ($sa_count consecutive) - findings injected into next iteration"
|
|
15263
15414
|
fi
|
|
15264
15415
|
fi
|
|
15416
|
+
# Secure-by-default scan (v7.87.0). Advisory by default (never
|
|
15417
|
+
# blocks); records .loki/quality/security-findings.json each
|
|
15418
|
+
# iteration. Blocks only on un-waived HIGH when LOKI_SECURE_GATE=block.
|
|
15419
|
+
log_info "Quality gate: security scan (advisory)..."
|
|
15420
|
+
if run_secure_scan; then
|
|
15421
|
+
clear_gate_failure "security_scan"
|
|
15422
|
+
else
|
|
15423
|
+
local sec_count
|
|
15424
|
+
sec_count=$(track_gate_failure "security_scan")
|
|
15425
|
+
gate_failures="${gate_failures}security_scan,"
|
|
15426
|
+
log_warn "Security gate BLOCKED ($sec_count consecutive) - un-waived HIGH findings (LOKI_SECURE_GATE=block)"
|
|
15427
|
+
fi
|
|
15265
15428
|
# BUG-ST-002: Check pause signal between quality gates
|
|
15266
15429
|
if [ -f "${TARGET_DIR:-.}/.loki/PAUSE" ] || [ -f "${TARGET_DIR:-.}/.loki/STOP" ]; then
|
|
15267
15430
|
log_warn "Pause/stop signal detected between quality gates - deferring remaining gates"
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -10110,6 +10110,62 @@ async def list_proofs():
|
|
|
10110
10110
|
return {"proofs": items}
|
|
10111
10111
|
|
|
10112
10112
|
|
|
10113
|
+
@app.get("/api/proofs/summary",
|
|
10114
|
+
dependencies=[Depends(auth.require_scope("read"))])
|
|
10115
|
+
async def proofs_summary():
|
|
10116
|
+
"""Honest aggregate over the active project's Evidence Receipts.
|
|
10117
|
+
|
|
10118
|
+
Counts are computed ONLY from real proof.json files; nothing is invented.
|
|
10119
|
+
The single source of truth for "verified" is the v1.1 deterministic
|
|
10120
|
+
honesty.headline (proof-generator.py::_compute_headline), which a forger
|
|
10121
|
+
cannot turn green without real exit_code:0 evidence. Buckets:
|
|
10122
|
+
|
|
10123
|
+
verified -> honesty.headline == "VERIFIED"
|
|
10124
|
+
with_gaps -> honesty.headline == "VERIFIED WITH GAPS"
|
|
10125
|
+
not_verified -> honesty.headline == "NOT VERIFIED"
|
|
10126
|
+
unknown -> no honesty block (schema v1.0 proofs) or any other/
|
|
10127
|
+
missing headline. We refuse to count these as verified
|
|
10128
|
+
because we cannot prove they were.
|
|
10129
|
+
|
|
10130
|
+
Empty or missing proofs dir -> all zeros (200), an honest empty state.
|
|
10131
|
+
Mirrors list_proofs' iteration + _safe_json_read so the counts can never
|
|
10132
|
+
drift from what the list endpoint shows.
|
|
10133
|
+
"""
|
|
10134
|
+
proofs_dir = _proofs_dir()
|
|
10135
|
+
total = verified = with_gaps = not_verified = unknown = 0
|
|
10136
|
+
try:
|
|
10137
|
+
entries = sorted(proofs_dir.iterdir())
|
|
10138
|
+
except (OSError, FileNotFoundError):
|
|
10139
|
+
entries = []
|
|
10140
|
+
for entry in entries:
|
|
10141
|
+
if not entry.is_dir():
|
|
10142
|
+
continue
|
|
10143
|
+
proof_json = entry / "proof.json"
|
|
10144
|
+
if not proof_json.is_file():
|
|
10145
|
+
continue
|
|
10146
|
+
data = _safe_json_read(proof_json, default=None)
|
|
10147
|
+
if not isinstance(data, dict):
|
|
10148
|
+
continue
|
|
10149
|
+
total += 1
|
|
10150
|
+
honesty = data.get("honesty")
|
|
10151
|
+
headline = honesty.get("headline") if isinstance(honesty, dict) else None
|
|
10152
|
+
if headline == "VERIFIED":
|
|
10153
|
+
verified += 1
|
|
10154
|
+
elif headline == "VERIFIED WITH GAPS":
|
|
10155
|
+
with_gaps += 1
|
|
10156
|
+
elif headline == "NOT VERIFIED":
|
|
10157
|
+
not_verified += 1
|
|
10158
|
+
else:
|
|
10159
|
+
unknown += 1
|
|
10160
|
+
return {
|
|
10161
|
+
"total_receipts": total,
|
|
10162
|
+
"verified": verified,
|
|
10163
|
+
"with_gaps": with_gaps,
|
|
10164
|
+
"not_verified": not_verified,
|
|
10165
|
+
"unknown": unknown,
|
|
10166
|
+
}
|
|
10167
|
+
|
|
10168
|
+
|
|
10113
10169
|
@app.get("/api/proofs/{run_id}", dependencies=[Depends(auth.require_scope("read"))])
|
|
10114
10170
|
async def get_proof(run_id: str):
|
|
10115
10171
|
"""Return the redacted proof.json for one run."""
|
|
@@ -204,6 +204,41 @@
|
|
|
204
204
|
font-weight: 500;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/* Verified-receipts badge: an honest trust signal next to the brand.
|
|
208
|
+
Counts are pulled live from /api/proofs/summary and reflect only real,
|
|
209
|
+
deterministic Evidence Receipts. Hidden until we have data; shows a muted
|
|
210
|
+
"No receipts yet" at zero; never a fabricated number. */
|
|
211
|
+
.receipts-badge {
|
|
212
|
+
display: none;
|
|
213
|
+
align-items: center;
|
|
214
|
+
gap: 5px;
|
|
215
|
+
margin-top: 6px;
|
|
216
|
+
padding: 3px 8px;
|
|
217
|
+
width: fit-content;
|
|
218
|
+
max-width: 100%;
|
|
219
|
+
border: 1px solid var(--loki-border);
|
|
220
|
+
border-radius: 999px;
|
|
221
|
+
background: var(--loki-bg-card);
|
|
222
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
223
|
+
font-size: 10px;
|
|
224
|
+
font-weight: 500;
|
|
225
|
+
line-height: 1.2;
|
|
226
|
+
color: var(--loki-text-secondary);
|
|
227
|
+
cursor: default;
|
|
228
|
+
}
|
|
229
|
+
.receipts-badge.show { display: inline-flex; }
|
|
230
|
+
.receipts-badge.empty { color: var(--loki-text-muted); }
|
|
231
|
+
.receipts-badge .receipts-dot {
|
|
232
|
+
width: 6px;
|
|
233
|
+
height: 6px;
|
|
234
|
+
border-radius: 50%;
|
|
235
|
+
background: var(--loki-success);
|
|
236
|
+
flex: 0 0 auto;
|
|
237
|
+
}
|
|
238
|
+
.receipts-badge.empty .receipts-dot { background: var(--loki-text-muted); }
|
|
239
|
+
.receipts-badge .receipts-verified { color: var(--loki-success); font-weight: 600; }
|
|
240
|
+
.receipts-badge.empty .receipts-verified { color: var(--loki-text-muted); font-weight: 500; }
|
|
241
|
+
|
|
207
242
|
/* Navigation: the only scrolling region. min-height:0 + overflow-y:auto so
|
|
208
243
|
a long grouped nav scrolls within the sidebar while the header + footer
|
|
209
244
|
stay pinned. */
|
|
@@ -851,6 +886,15 @@
|
|
|
851
886
|
</button>
|
|
852
887
|
<span class="logo-brand">Loki Mode</span>
|
|
853
888
|
<span class="logo-subtitle">powered by Autonomi</span>
|
|
889
|
+
<!-- Verified-receipts badge: honest trust signal. Populated at runtime
|
|
890
|
+
from /api/proofs/summary; hidden until data arrives, shows a muted
|
|
891
|
+
empty state at zero, and degrades silently if the endpoint is
|
|
892
|
+
unavailable. Every receipt is a deterministic, re-verifiable
|
|
893
|
+
Evidence Receipt (loki proof verify). -->
|
|
894
|
+
<span class="receipts-badge" id="receipts-badge" role="status">
|
|
895
|
+
<span class="receipts-dot" aria-hidden="true"></span>
|
|
896
|
+
<span id="receipts-badge-text"></span>
|
|
897
|
+
</span>
|
|
854
898
|
<!-- v7.84 single project switcher: ONE searchable <select> with two
|
|
855
899
|
<optgroup>s ("Running" and "All projects"), built at runtime from
|
|
856
900
|
/api/running-projects. A running-app count pill sits beside it; the
|
|
@@ -15250,6 +15294,55 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
15250
15294
|
} catch (err) { /* polling fallback still covers it */ }
|
|
15251
15295
|
})();
|
|
15252
15296
|
|
|
15297
|
+
// Verified-receipts badge: honest trust signal beside the brand. Fetches the
|
|
15298
|
+
// real aggregate from /api/proofs/summary and shows "N receipts - M
|
|
15299
|
+
// verified". At zero it shows a muted "No receipts yet"; if the endpoint is
|
|
15300
|
+
// unavailable it stays hidden (no error spew). We never display a number the
|
|
15301
|
+
// data does not support: "verified" here is the deterministic
|
|
15302
|
+
// honesty.headline == VERIFIED count, re-verifiable via loki proof verify.
|
|
15303
|
+
(function initReceiptsBadge() {
|
|
15304
|
+
var badge = document.getElementById('receipts-badge');
|
|
15305
|
+
var textEl = document.getElementById('receipts-badge-text');
|
|
15306
|
+
if (!badge || !textEl) return;
|
|
15307
|
+
|
|
15308
|
+
function plural(n, word) { return n + ' ' + word + (n === 1 ? '' : 's'); }
|
|
15309
|
+
|
|
15310
|
+
function render(s) {
|
|
15311
|
+
var total = (s && typeof s.total_receipts === 'number') ? s.total_receipts : 0;
|
|
15312
|
+
var verified = (s && typeof s.verified === 'number') ? s.verified : 0;
|
|
15313
|
+
if (total <= 0) {
|
|
15314
|
+
// Honest empty state: no fabricated number.
|
|
15315
|
+
badge.classList.add('empty');
|
|
15316
|
+
textEl.textContent = 'No receipts yet';
|
|
15317
|
+
badge.title = 'Evidence Receipts appear here once Loki completes a '
|
|
15318
|
+
+ 'verified run. Each is deterministic and re-verifiable with '
|
|
15319
|
+
+ '"loki proof verify".';
|
|
15320
|
+
badge.classList.add('show');
|
|
15321
|
+
return;
|
|
15322
|
+
}
|
|
15323
|
+
badge.classList.remove('empty');
|
|
15324
|
+
// "N receipts - M verified" with the verified count emphasized.
|
|
15325
|
+
textEl.innerHTML = plural(total, 'receipt') + ' - '
|
|
15326
|
+
+ '<span class="receipts-verified"></span> verified';
|
|
15327
|
+
var vEl = textEl.querySelector('.receipts-verified');
|
|
15328
|
+
if (vEl) vEl.textContent = String(verified);
|
|
15329
|
+
badge.title = plural(verified, 'receipt') + ' of ' + total
|
|
15330
|
+
+ ' verified by a deterministic Evidence Receipt (re-verifiable with '
|
|
15331
|
+
+ '"loki proof verify"). "Verified" means tests passed with real '
|
|
15332
|
+
+ 'exit-code evidence, not an LLM opinion.';
|
|
15333
|
+
badge.classList.add('show');
|
|
15334
|
+
}
|
|
15335
|
+
|
|
15336
|
+
function poll() {
|
|
15337
|
+
fetch('/api/proofs/summary', { headers: { 'Accept': 'application/json' } })
|
|
15338
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
15339
|
+
.then(function (d) { if (d) render(d); })
|
|
15340
|
+
.catch(function () { /* endpoint unavailable: leave badge hidden */ });
|
|
15341
|
+
}
|
|
15342
|
+
poll();
|
|
15343
|
+
setInterval(poll, 30000);
|
|
15344
|
+
})();
|
|
15345
|
+
|
|
15253
15346
|
// Mobile menu toggle
|
|
15254
15347
|
var mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
|
15255
15348
|
var sidebar = document.getElementById('sidebar');
|
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.87.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.87.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 QQ=Object.defineProperty;var ZQ=($)=>$;function zQ($,Q){this[$]=ZQ.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)QQ($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:zQ.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var h1={};b(h1,{lokiDir:()=>P,homeLokiDir:()=>i$,findRepoRootForVersion:()=>t$,REPO_ROOT:()=>g});import{resolve as a,dirname as r$}from"path";import{fileURLToPath as XQ}from"url";import{existsSync as R$}from"fs";import{homedir as KQ}from"os";function qQ(){let $=b1;for(let Q=0;Q<6;Q++){if(R$(a($,"VERSION"))&&R$(a($,"autonomy/run.sh")))return $;let Z=r$($);if(Z===$)break;$=Z}return a(b1,"..","..","..")}function t$($){let Q=$;for(let Z=0;Z<6;Z++){if(R$(a(Q,"VERSION"))&&R$(a(Q,"autonomy/run.sh")))return Q;let z=r$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function P(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function i$(){return a(KQ(),".loki")}var b1,g;var C=L(()=>{b1=r$(XQ(import.meta.url));g=qQ()});import{readFileSync as VQ}from"fs";import{resolve as JQ,dirname as UQ}from"path";import{fileURLToPath as WQ}from"url";function E$(){if(Q$!==null)return Q$;let $="7.
|
|
2
|
+
var QQ=Object.defineProperty;var ZQ=($)=>$;function zQ($,Q){this[$]=ZQ.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)QQ($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:zQ.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var h1={};b(h1,{lokiDir:()=>P,homeLokiDir:()=>i$,findRepoRootForVersion:()=>t$,REPO_ROOT:()=>g});import{resolve as a,dirname as r$}from"path";import{fileURLToPath as XQ}from"url";import{existsSync as R$}from"fs";import{homedir as KQ}from"os";function qQ(){let $=b1;for(let Q=0;Q<6;Q++){if(R$(a($,"VERSION"))&&R$(a($,"autonomy/run.sh")))return $;let Z=r$($);if(Z===$)break;$=Z}return a(b1,"..","..","..")}function t$($){let Q=$;for(let Z=0;Z<6;Z++){if(R$(a(Q,"VERSION"))&&R$(a(Q,"autonomy/run.sh")))return Q;let z=r$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function P(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function i$(){return a(KQ(),".loki")}var b1,g;var C=L(()=>{b1=r$(XQ(import.meta.url));g=qQ()});import{readFileSync as VQ}from"fs";import{resolve as JQ,dirname as UQ}from"path";import{fileURLToPath as WQ}from"url";function E$(){if(Q$!==null)return Q$;let $="7.87.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=UQ(WQ(import.meta.url)),Z=t$(Q);Q$=VQ(JQ(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var e$=L(()=>{C()});var g1={};b(g1,{runOrThrow:()=>HQ,run:()=>F,readStreamCapped:()=>m1,commandVersion:()=>BQ,commandExists:()=>f,ShellError:()=>$1,MAX_STDOUT_BYTES:()=>v1});async function m1($,Q=v1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:U}=await Z.read();if(K)break;if(!U)continue;if(q+=U.byteLength,q>Q){let J=U.byteLength-(q-Q);X+=z.decode(U.subarray(0,J),{stream:!0});break}X+=z.decode(U,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,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,U]=await Promise.all([m1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function HQ($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new $1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=GQ($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function GQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function BQ($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var v1=16777216,$1;var d=L(()=>{$1=class $1 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 s($){return YQ?"":$}var YQ,O,S,_,_Z,I,k,h,V;var c=L(()=>{YQ=(process.env.NO_COLOR??"").length>0;O=s("\x1B[0;31m"),S=s("\x1B[0;32m"),_=s("\x1B[1;33m"),_Z=s("\x1B[0;34m"),I=s("\x1B[0;36m"),k=s("\x1B[1m"),h=s("\x1B[2m"),V=s("\x1B[0m")});import{existsSync as jQ}from"fs";async function Z$(){if(Y$!==void 0)return Y$;let $="/opt/homebrew/bin/python3.12";if(jQ($))return Y$=$,$;let Q=await f("python3.12");if(Q)return Y$=Q,Q;let Z=await f("python3");return Y$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var Y$;var V$=L(()=>{d()});var X0={};b(X0,{runStatus:()=>oQ});import{existsSync as y,readFileSync as U$,readdirSync as r1,statSync as t1}from"fs";import{resolve as D,basename as vQ}from"path";import{homedir as mQ}from"os";function i1($){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 e1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*N$/Q);if(X>N$)X=N$;let q=N$-X,K=S;if(z>=80)K=O;else if(z>=50)K=_;let U="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=i1($),W=i1(Q);return` ${k}${Z}${V} ${K}[${U}]${V} ${z}% (${J} / ${W})`}async function fQ(){if(await f("jq"))return!0;return process.stdout.write(`${O}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)
|
|
@@ -796,4 +796,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
796
796
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
797
797
|
`),process.stderr.write($Q),2}}s1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var qZ=await KZ(Bun.argv.slice(2));process.exit(qZ);
|
|
798
798
|
|
|
799
|
-
//# debugId=
|
|
799
|
+
//# debugId=7982E696DB64940A64756E2164756E21
|
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.87.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.87.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",
|