loki-mode 8.37.0 → 8.38.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 +6 -2
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/tests/detect-invariant-violations.sh +359 -0
- package/tests/detect-mock-problems.sh +473 -0
- package/tests/detect-semantic-test-problems.sh +332 -0
- package/tests/detect-test-mutations.sh +455 -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.38.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.38.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.
|
|
1
|
+
8.38.0
|
package/dashboard/__init__.py
CHANGED
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var h_=Object.create;var{getPrototypeOf:v_,defineProperty:rK,getOwnPropertyNames:g_}=Object;var m_=Object.prototype.hasOwnProperty;function u_(Z){return this[Z]}var p_,d_,c_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?p_??=new WeakMap:d_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?h_(v_(Z)):{};let K=X||!Z||!Z.__esModule?rK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of g_(Z))if(!m_.call(K,$))rK(K,$,{get:u_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var qQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var l_=(Z)=>Z;function i_(Z,X){this[Z]=l_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)rK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:i_.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:()=>P4,findRepoRootForVersion:()=>eK,REPO_ROOT:()=>r0});import{resolve as n7,dirname as tK}from"path";import{fileURLToPath as a_}from"url";import{existsSync as GQ}from"fs";import{homedir as s_}from"os";function n_(){let Z=RO;for(let X=0;X<6;X++){if(GQ(n7(Z,"VERSION"))&&GQ(n7(Z,"autonomy/run.sh")))return Z;let Q=tK(Z);if(Q===Z)break;Z=Q}return n7(RO,"..","..","..")}function eK(Z){let X=Z;for(let Q=0;Q<6;Q++){if(GQ(n7(X,"VERSION"))&&GQ(n7(X,"autonomy/run.sh")))return X;let Y=tK(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function P4(){return n7(s_(),".loki")}var RO,r0;var G8=p(()=>{RO=tK(a_(import.meta.url));r0=n_()});import{readFileSync as o_}from"fs";import{resolve as r_,dirname as t_}from"path";import{fileURLToPath as e_}from"url";function f3(){if(h5!==null)return h5;let Z="8.
|
|
2
|
+
var h_=Object.create;var{getPrototypeOf:v_,defineProperty:rK,getOwnPropertyNames:g_}=Object;var m_=Object.prototype.hasOwnProperty;function u_(Z){return this[Z]}var p_,d_,c_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?p_??=new WeakMap:d_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?h_(v_(Z)):{};let K=X||!Z||!Z.__esModule?rK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of g_(Z))if(!m_.call(K,$))rK(K,$,{get:u_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var qQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var l_=(Z)=>Z;function i_(Z,X){this[Z]=l_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)rK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:i_.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:()=>P4,findRepoRootForVersion:()=>eK,REPO_ROOT:()=>r0});import{resolve as n7,dirname as tK}from"path";import{fileURLToPath as a_}from"url";import{existsSync as GQ}from"fs";import{homedir as s_}from"os";function n_(){let Z=RO;for(let X=0;X<6;X++){if(GQ(n7(Z,"VERSION"))&&GQ(n7(Z,"autonomy/run.sh")))return Z;let Q=tK(Z);if(Q===Z)break;Z=Q}return n7(RO,"..","..","..")}function eK(Z){let X=Z;for(let Q=0;Q<6;Q++){if(GQ(n7(X,"VERSION"))&&GQ(n7(X,"autonomy/run.sh")))return X;let Y=tK(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function P4(){return n7(s_(),".loki")}var RO,r0;var G8=p(()=>{RO=tK(a_(import.meta.url));r0=n_()});import{readFileSync as o_}from"fs";import{resolve as r_,dirname as t_}from"path";import{fileURLToPath as e_}from"url";function f3(){if(h5!==null)return h5;let Z="8.38.0";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=t_(e_(import.meta.url)),Q=eK(X);h5=o_(r_(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var HQ=p(()=>{G8()});var bO={};l0(bO,{runOrThrow:()=>Of,run:()=>E0,readStreamCapped:()=>UQ,commandVersion:()=>Af,commandExists:()=>X9,ShellError:()=>Z$,MAX_STDOUT_BYTES:()=>yO});async function UQ(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([UQ(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 Of(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Z$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=Lf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Lf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Af(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,Z$;var x9=p(()=>{Z$=class Z$ 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 jf?"":Z}var jf,L0,k8,p0,ZV0,i0,H8,Q9,v;var S6=p(()=>{jf=(process.env.NO_COLOR??"").length>0;L0=o7("\x1B[0;31m"),k8=o7("\x1B[0;32m"),p0=o7("\x1B[1;33m"),ZV0=o7("\x1B[0;34m"),i0=o7("\x1B[0;36m"),H8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),v=o7("\x1B[0m")});import{existsSync as xf}from"fs";async function E7(){if(k4!==void 0)return k4;let Z="/opt/homebrew/bin/python3.12";if(xf(Z))return k4=Z,Z;let X=await X9("python3.12");if(X)return k4=X,X;let Q=await X9("python3");return k4=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 k4;var r7=p(()=>{x9()});var ZL={};l0(ZL,{runStatus:()=>Yh});import{existsSync as Y9,readFileSync as v3,readdirSync as iO,statSync as aO}from"fs";import{resolve as h8,basename as sf}from"path";import{homedir as nf}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*NQ/X);if(J>NQ)J=NQ;let z=NQ-J,K=k8;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` ${H8}${Q}${v} ${K}[${$}]${v} ${Y}% (${W} / ${V})`}async function rf(){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)
|
|
@@ -1222,4 +1222,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
1222
1222
|
`),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (b_(),y_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
|
|
1223
1223
|
`),process.stderr.write(__),2}}cO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var fW0=await _W0(Bun.argv.slice(2));process.exit(fW0);
|
|
1224
1224
|
|
|
1225
|
-
//# debugId=
|
|
1225
|
+
//# debugId=D3A7B864D186CB7464756E2164756E21
|
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.38.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",
|
|
@@ -106,7 +106,11 @@
|
|
|
106
106
|
"web-app/migrations/",
|
|
107
107
|
"web-app/Dockerfile",
|
|
108
108
|
"web-app/docker-compose.purple-lab.yml",
|
|
109
|
-
"web-app/deploy/"
|
|
109
|
+
"web-app/deploy/",
|
|
110
|
+
"tests/detect-mock-problems.sh",
|
|
111
|
+
"tests/detect-test-mutations.sh",
|
|
112
|
+
"tests/detect-semantic-test-problems.sh",
|
|
113
|
+
"tests/detect-invariant-violations.sh"
|
|
110
114
|
],
|
|
111
115
|
"scripts": {
|
|
112
116
|
"prepack": "find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null; find . -name '*.pyc' -delete 2>/dev/null; if command -v bun >/dev/null 2>&1; then (cd loki-ts && bun install --production && bun run build) || echo 'WARN: loki-ts build failed, using existing dist if present'; else echo 'WARN: bun not on PATH, skipping loki-ts build (using committed dist if present)'; fi; true",
|
|
@@ -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.38.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,359 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Invariant Violation Detector - spec-independent invariant checks
|
|
3
|
+
#
|
|
4
|
+
# Usage: ./tests/detect-invariant-violations.sh [--strict]
|
|
5
|
+
# --strict: Exit 1 iff CRITICAL or HIGH findings exist (for CI). MEDIUM/LOW
|
|
6
|
+
# never block. This mirrors the mock-detector --strict contract.
|
|
7
|
+
#
|
|
8
|
+
# WHAT THIS IS
|
|
9
|
+
# This detector asserts a small set of invariants that are TRUE regardless of
|
|
10
|
+
# what the spec says. They catch the "spec was silent and the model guessed
|
|
11
|
+
# wrong" failure mode: code that ships a hardcoded secret, or logs PII, is
|
|
12
|
+
# wrong no matter what the PRD asked for. These are deterministic, near-zero
|
|
13
|
+
# false-positive pattern checks over the PRODUCED SOURCE (not test files).
|
|
14
|
+
#
|
|
15
|
+
# This is NOT a fast-check / hypothesis test generator (the "Kiro Pattern"
|
|
16
|
+
# documented in skills/testing.md). Generating property-based tests is a
|
|
17
|
+
# larger feature. This detector instead makes deterministic invariant
|
|
18
|
+
# ASSERTIONS over the produced code that hold regardless of spec.
|
|
19
|
+
#
|
|
20
|
+
# WHAT IT CATCHES (deterministic, blocking under --strict)
|
|
21
|
+
# 1. Committed secrets in source/logs: known-prefix high-signal credentials
|
|
22
|
+
# (AWS access keys, private-key PEM blocks, GitHub/Slack/Google/Stripe/
|
|
23
|
+
# Anthropic tokens). These have near-zero false positives. [CRITICAL]
|
|
24
|
+
# 2. PII in logs: an email-shaped literal passed to a log/print call. [HIGH]
|
|
25
|
+
#
|
|
26
|
+
# WHAT IT CATCHES (advisory only, never blocks)
|
|
27
|
+
# 3. Generic "secret-like" var assignment (a var named secret/token/password/
|
|
28
|
+
# apikey assigned a long opaque literal). FP-prone, so MEDIUM not HIGH.
|
|
29
|
+
# 4. Logging an interpolated email-bearing variable (e.g. user.email). [LOW]
|
|
30
|
+
#
|
|
31
|
+
# WHAT IT DEFERS (honest: NOT implemented, would be flaky as a static grep)
|
|
32
|
+
# - Unhandled-error path on the happy route. A grep cannot do control-flow
|
|
33
|
+
# analysis; a "no try/catch near await" heuristic is noise. Deferred to a
|
|
34
|
+
# real analysis pass (LSP diagnostics / typed exhaustiveness), not faked here.
|
|
35
|
+
# - Idempotency / round-trip invariants. Not statically detectable in any
|
|
36
|
+
# deterministic way worth shipping; it requires executing generated tests
|
|
37
|
+
# (the larger fast-check/metamorphic feature). Deferred, not faked.
|
|
38
|
+
#
|
|
39
|
+
# PLACEHOLDER SAFETY (false-positive avoidance)
|
|
40
|
+
# Generated code legitimately contains placeholders: AWS's own documented
|
|
41
|
+
# AKIAIOSFODNN7EXAMPLE, sk-test-..., your-api-key-here, .env.example samples.
|
|
42
|
+
# Lines matching the placeholder allowlist (EXAMPLE / your- / xxxx / placeholder
|
|
43
|
+
# / changeme / redacted / dummy / sample / <...>) are NOT flagged. Files named
|
|
44
|
+
# *.example / *.sample / *.template and *.md docs are skipped for the secret
|
|
45
|
+
# checks (they routinely show fake credentials for illustration).
|
|
46
|
+
#
|
|
47
|
+
# SCAN SURFACE
|
|
48
|
+
# Source files only. Extensions: ts js tsx jsx py go rb java rs php sh env yml
|
|
49
|
+
# yaml json plus *.log files (the "logs" in "no secrets in source/logs").
|
|
50
|
+
# node_modules, .git, dist, build, vendor, coverage, Python venvs (.venv /
|
|
51
|
+
# venv / __pycache__), and Loki's own .loki/ telemetry and .claude/ (agent
|
|
52
|
+
# worktrees) are excluded. These directories are pruned at the find level so
|
|
53
|
+
# the scan never descends into them. Test files are
|
|
54
|
+
# OUT OF SCOPE for all checks (the
|
|
55
|
+
# invariant is framed for source/logs): the exclusion covers every ecosystem's
|
|
56
|
+
# convention -- *.test.* / *.spec.*, test_*.py / *_test.py, test-*.sh,
|
|
57
|
+
# *_test.go, and anything under an anchored tests/ / __tests__/ / spec/ dir.
|
|
58
|
+
# Security/redaction test fixtures routinely embed fake credentials on purpose.
|
|
59
|
+
# Comprehensive secret scanning of generated TESTS is a separate feature (P3-4).
|
|
60
|
+
#
|
|
61
|
+
# ----------------------------------------------------------------------------
|
|
62
|
+
# HOW TO WIRE THIS AS A GATE (mirror enforce_mock_integrity in autonomy/run.sh)
|
|
63
|
+
# ----------------------------------------------------------------------------
|
|
64
|
+
# Add a wrapper alongside enforce_mock_integrity() (autonomy/run.sh:7932) and
|
|
65
|
+
# call it where enforce_mock_integrity is called (autonomy/run.sh:14676). The
|
|
66
|
+
# detector honors LOKI_SCAN_DIR (see below), so the wrapper exports it to the
|
|
67
|
+
# target project, exactly like the mock gate. Suggested wrapper:
|
|
68
|
+
#
|
|
69
|
+
# enforce_invariant_integrity() {
|
|
70
|
+
# local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
71
|
+
# local quality_dir="$loki_dir/quality"
|
|
72
|
+
# mkdir -p "$quality_dir"
|
|
73
|
+
# local findings_file="$quality_dir/invariant-findings.txt"
|
|
74
|
+
# local detector="$SCRIPT_DIR/../tests/detect-invariant-violations.sh"
|
|
75
|
+
# local gate_timeout="${LOKI_GATE_TIMEOUT:-300}"
|
|
76
|
+
# [ "${LOKI_GATE_INVARIANT:-true}" = "false" ] && return 0
|
|
77
|
+
# if [ ! -f "$detector" ]; then
|
|
78
|
+
# log_info "Invariant gate: detector not found, skipping (inconclusive)"
|
|
79
|
+
# rm -f "$findings_file" 2>/dev/null || true
|
|
80
|
+
# return 0
|
|
81
|
+
# fi
|
|
82
|
+
# local output rc
|
|
83
|
+
# output=$(cd "${TARGET_DIR:-.}" && LOKI_SCAN_DIR="${TARGET_DIR:-.}" \
|
|
84
|
+
# timeout "$gate_timeout" bash "$detector" --strict 2>&1)
|
|
85
|
+
# rc=$?
|
|
86
|
+
# if [ "$rc" -eq 124 ]; then
|
|
87
|
+
# log_warn "Invariant gate: detector timed out after ${gate_timeout}s -- inconclusive"
|
|
88
|
+
# rm -f "$findings_file" 2>/dev/null || true
|
|
89
|
+
# return 0
|
|
90
|
+
# fi
|
|
91
|
+
# if [ "$rc" -ne 0 ]; then
|
|
92
|
+
# { echo "# Invariant findings (CRITICAL/HIGH block this iteration)"
|
|
93
|
+
# echo "$output" | grep -E '\[(CRITICAL|HIGH|MEDIUM|LOW)\]' || true
|
|
94
|
+
# } > "$findings_file"
|
|
95
|
+
# log_warn "Invariant gate: CRITICAL/HIGH invariant violations detected -- BLOCK"
|
|
96
|
+
# return 1
|
|
97
|
+
# fi
|
|
98
|
+
# local med_low
|
|
99
|
+
# med_low=$(echo "$output" | grep -E '\[(MEDIUM|LOW)\]' || true)
|
|
100
|
+
# if [ -n "$med_low" ]; then
|
|
101
|
+
# { echo "# Invariant advisory findings (MED/LOW, non-blocking)"
|
|
102
|
+
# echo "$med_low"; } > "$findings_file"
|
|
103
|
+
# else
|
|
104
|
+
# rm -f "$findings_file" 2>/dev/null || true
|
|
105
|
+
# fi
|
|
106
|
+
# log_info "Invariant gate: PASS"
|
|
107
|
+
# return 0
|
|
108
|
+
# }
|
|
109
|
+
#
|
|
110
|
+
# Opt out with LOKI_GATE_INVARIANT=false. Document the new gate row in
|
|
111
|
+
# skills/quality-gates.md and cross-reference it from the Kiro Pattern section
|
|
112
|
+
# of skills/testing.md (which currently documents property testing with zero
|
|
113
|
+
# implementation). Do NOT edit run.sh from this script; the integrator wires it.
|
|
114
|
+
# ============================================================================
|
|
115
|
+
|
|
116
|
+
set -uo pipefail
|
|
117
|
+
|
|
118
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
119
|
+
# Directory to scan. Defaults to the repo containing this script (so
|
|
120
|
+
# run-all-tests.sh keeps scanning loki-mode unchanged). A run.sh gate wrapper
|
|
121
|
+
# MUST set LOKI_SCAN_DIR to the target project; cwd is NOT used by find here,
|
|
122
|
+
# so `cd TARGET_DIR` alone does not redirect the scan.
|
|
123
|
+
PROJECT_DIR="${LOKI_SCAN_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
|
124
|
+
STRICT="${1:-}"
|
|
125
|
+
|
|
126
|
+
RED='\033[0;31m'
|
|
127
|
+
YELLOW='\033[1;33m'
|
|
128
|
+
GREEN='\033[0;32m'
|
|
129
|
+
CYAN='\033[0;36m'
|
|
130
|
+
NC='\033[0m'
|
|
131
|
+
|
|
132
|
+
CRITICAL=0
|
|
133
|
+
HIGH=0
|
|
134
|
+
MEDIUM=0
|
|
135
|
+
LOW=0
|
|
136
|
+
|
|
137
|
+
echo "=========================================="
|
|
138
|
+
echo "Invariant Violation Detector"
|
|
139
|
+
echo "=========================================="
|
|
140
|
+
echo ""
|
|
141
|
+
|
|
142
|
+
report() {
|
|
143
|
+
local severity="$1"
|
|
144
|
+
local file="$2"
|
|
145
|
+
local line="$3"
|
|
146
|
+
local message="$4"
|
|
147
|
+
|
|
148
|
+
case "$severity" in
|
|
149
|
+
CRITICAL) echo -e "${RED}[CRITICAL]${NC} $file:$line - $message"; ((CRITICAL++)) ;;
|
|
150
|
+
HIGH) echo -e "${RED}[HIGH]${NC} $file:$line - $message"; ((HIGH++)) ;;
|
|
151
|
+
MEDIUM) echo -e "${YELLOW}[MEDIUM]${NC} $file:$line - $message"; ((MEDIUM++)) ;;
|
|
152
|
+
LOW) echo -e "${CYAN}[LOW]${NC} $file:$line - $message"; ((LOW++)) ;;
|
|
153
|
+
esac
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# Placeholder allowlist: lines matching these are NOT real secrets. Covers AWS's
|
|
157
|
+
# own documented example key, common scaffolding placeholders, and redaction.
|
|
158
|
+
PLACEHOLDER_RE='EXAMPLE|example\.com|your[-_]|xxxx|placeholder|changeme|change-me|REPLACE|<[^>]*>|dummy|sample|redact|sk-test-|test[-_]key|fake|FIXME|TODO|\*\*\*\*'
|
|
159
|
+
|
|
160
|
+
is_placeholder() {
|
|
161
|
+
# $1 = the matched line. Returns 0 (true) if it looks like a placeholder.
|
|
162
|
+
echo "$1" | grep -qiE "$PLACEHOLDER_RE"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Build the list of source files to scan once. Source extensions only; never
|
|
166
|
+
# *.test.* (that is the mock detector's surface). Exclude vendored/build dirs
|
|
167
|
+
# and Loki's own .loki/ telemetry.
|
|
168
|
+
collect_source_files() {
|
|
169
|
+
# Prune the heavy/vendored/build directories AT THE FIND LEVEL so find never
|
|
170
|
+
# descends into them (a post-filter `grep -v` still enumerates every file
|
|
171
|
+
# under node_modules first -- 100k+ files on a real repo -- and hangs). The
|
|
172
|
+
# pruned set mirrors the SCAN SURFACE comment: node_modules, .git, dist,
|
|
173
|
+
# build, vendor, .loki, .claude, coverage, .venv/venv, __pycache__.
|
|
174
|
+
# (.claude holds Loki's own agent worktrees -- full repo copies -- so it is
|
|
175
|
+
# excluded like .loki; .venv/venv/__pycache__ are Python vendored deps, the
|
|
176
|
+
# same category as node_modules.)
|
|
177
|
+
# Test-file / test-dir exclusion stays as a
|
|
178
|
+
# post-filter grep below: those are anchored file-name regexes (Test 7 covers
|
|
179
|
+
# *.test.js / test_*.py at a dir root, not just tests/ subdirs) that -prune
|
|
180
|
+
# cannot express, and keeping them in one place avoids two sources of truth.
|
|
181
|
+
find "$PROJECT_DIR" \
|
|
182
|
+
\( -type d \( -name node_modules -o -name .git -o -name dist \
|
|
183
|
+
-o -name build -o -name vendor -o -name .loki -o -name .claude \
|
|
184
|
+
-o -name coverage -o -name .venv -o -name venv \
|
|
185
|
+
-o -name __pycache__ \) \
|
|
186
|
+
-prune \) \
|
|
187
|
+
-o \( -type f \
|
|
188
|
+
\( -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.jsx' \
|
|
189
|
+
-o -name '*.py' -o -name '*.go' -o -name '*.rb' -o -name '*.java' \
|
|
190
|
+
-o -name '*.rs' -o -name '*.php' -o -name '*.sh' -o -name '*.env' \
|
|
191
|
+
-o -name '*.yml' -o -name '*.yaml' -o -name '*.json' -o -name '*.log' \) \
|
|
192
|
+
-print \) \
|
|
193
|
+
2>/dev/null \
|
|
194
|
+
| grep -vE '(^|/)(tests?|__tests__|spec|specs)(/|$)' \
|
|
195
|
+
| grep -vE '\.(test|spec)\.[a-z]+$' \
|
|
196
|
+
| grep -vE '(^|/)(test[_-][^/]*|[^/]*[_-]test)\.(py|sh|go|rb|js|ts)$' || true
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Invariant 1: committed secrets (known high-signal credential prefixes).
|
|
201
|
+
# Near-zero false positives. CRITICAL because a leaked live credential is the
|
|
202
|
+
# highest-severity spec-independent defect.
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
echo -e "${CYAN}Scanning for committed secrets (known credential formats)...${NC}"
|
|
205
|
+
|
|
206
|
+
# Each entry: a PCRE-ish ERE for a credential format with a distinctive prefix.
|
|
207
|
+
SECRET_PATTERNS=(
|
|
208
|
+
'AKIA[0-9A-Z]{16}' # AWS access key id
|
|
209
|
+
'ASIA[0-9A-Z]{16}' # AWS temporary access key id
|
|
210
|
+
'-----BEGIN[ A-Z]*PRIVATE KEY-----' # PEM private key block
|
|
211
|
+
'ghp_[A-Za-z0-9]{36}' # GitHub personal access token
|
|
212
|
+
'gho_[A-Za-z0-9]{36}' # GitHub OAuth token
|
|
213
|
+
'ghs_[A-Za-z0-9]{36}' # GitHub server-to-server token
|
|
214
|
+
'github_pat_[A-Za-z0-9_]{60,}' # GitHub fine-grained PAT
|
|
215
|
+
'xox[baprs]-[A-Za-z0-9-]{10,}' # Slack token
|
|
216
|
+
'AIza[0-9A-Za-z_-]{35}' # Google API key
|
|
217
|
+
'sk-ant-[A-Za-z0-9-]{20,}' # Anthropic API key
|
|
218
|
+
'sk_live_[A-Za-z0-9]{20,}' # Stripe live secret key
|
|
219
|
+
'rk_live_[A-Za-z0-9]{20,}' # Stripe live restricted key
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Combined alternation of every secret pattern, used as a single-grep prefilter.
|
|
223
|
+
# Most files match nothing, so one `grep -qE` per file lets us skip the 12-pattern
|
|
224
|
+
# inner loop entirely except on the rare file that contains a candidate. This is
|
|
225
|
+
# behavior-identical: the inner loop still runs all 12 patterns and reports each
|
|
226
|
+
# match with the exact same /$pat/ message; the prefilter only avoids spawning 12
|
|
227
|
+
# greps per clean file (the dominant cost on a large tree).
|
|
228
|
+
SECRET_COMBINED_RE="$(IFS='|'; printf '%s' "${SECRET_PATTERNS[*]}")"
|
|
229
|
+
|
|
230
|
+
while IFS= read -r src_file; do
|
|
231
|
+
[ -z "$src_file" ] && continue
|
|
232
|
+
rel_path="${src_file#"$PROJECT_DIR"/}"
|
|
233
|
+
# Skip illustration files for the secret check: docs and *.example/.sample/
|
|
234
|
+
# .template routinely show fake credentials on purpose.
|
|
235
|
+
case "$rel_path" in
|
|
236
|
+
*.md|*.markdown|*.example|*.sample|*.template|*.example.*|*.dist) continue ;;
|
|
237
|
+
esac
|
|
238
|
+
# Prefilter: skip files with no candidate at all (the common case).
|
|
239
|
+
grep -qE -- "$SECRET_COMBINED_RE" "$src_file" 2>/dev/null || continue
|
|
240
|
+
for pat in "${SECRET_PATTERNS[@]}"; do
|
|
241
|
+
while IFS=: read -r lineno line; do
|
|
242
|
+
[ -z "$lineno" ] && continue
|
|
243
|
+
if is_placeholder "$line"; then
|
|
244
|
+
continue
|
|
245
|
+
fi
|
|
246
|
+
report "CRITICAL" "$rel_path" "$lineno" "Hardcoded secret matching credential format /$pat/"
|
|
247
|
+
done < <(grep -nE -- "$pat" "$src_file" 2>/dev/null)
|
|
248
|
+
done
|
|
249
|
+
done < <(collect_source_files)
|
|
250
|
+
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
# Invariant 2: PII (email literal) passed to a log/print statement. HIGH.
|
|
253
|
+
# Scoped to log calls only -- an email in a string elsewhere may be legitimate
|
|
254
|
+
# (a sample, a mailto:), but writing one into a log statement is a leak.
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
echo -e "${CYAN}Scanning for PII (email literals) in log statements...${NC}"
|
|
257
|
+
|
|
258
|
+
# A log/print call on the line ...
|
|
259
|
+
LOG_CALL_RE='(console\.(log|info|warn|error|debug)|logger?\.(log|info|warn|error|debug|trace)|System\.out\.print|fmt\.(Print|Printf|Println)|\bprint\(|\bprintln!?|\becho\b|log\.(Print|Printf|Println|Info|Error|Warn|Debug))'
|
|
260
|
+
# ... an email-shaped token (local@domain.tld). The email need not be adjacent
|
|
261
|
+
# to the opening quote (it is commonly embedded in a larger message string, e.g.
|
|
262
|
+
# "user logged in: a@b.com"), so we match the email token anywhere on the line.
|
|
263
|
+
EMAIL_TOKEN_RE='[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}'
|
|
264
|
+
# ... and a string quote present on the line (so it is a literal, not a bare
|
|
265
|
+
# variable reference, which is handled by the LOW advisory check below).
|
|
266
|
+
QUOTE_RE='["'"'"'\`]'
|
|
267
|
+
|
|
268
|
+
while IFS= read -r src_file; do
|
|
269
|
+
[ -z "$src_file" ] && continue
|
|
270
|
+
rel_path="${src_file#"$PROJECT_DIR"/}"
|
|
271
|
+
while IFS=: read -r lineno line; do
|
|
272
|
+
[ -z "$lineno" ] && continue
|
|
273
|
+
# Must be a log call, contain an email token, and contain a string quote.
|
|
274
|
+
if echo "$line" | grep -qE "$LOG_CALL_RE" \
|
|
275
|
+
&& echo "$line" | grep -qE "$QUOTE_RE"; then
|
|
276
|
+
# Skip placeholder/example emails (your-, example.com via allowlist).
|
|
277
|
+
if is_placeholder "$line"; then
|
|
278
|
+
continue
|
|
279
|
+
fi
|
|
280
|
+
report "HIGH" "$rel_path" "$lineno" "Email literal written to a log/print statement (PII in logs)"
|
|
281
|
+
fi
|
|
282
|
+
done < <(grep -nE "$EMAIL_TOKEN_RE" "$src_file" 2>/dev/null)
|
|
283
|
+
done < <(collect_source_files)
|
|
284
|
+
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
# Invariant 3 (advisory, MEDIUM): generic secret-like assignment. A variable
|
|
287
|
+
# named secret/token/password/apikey assigned a long opaque string literal.
|
|
288
|
+
# FP-prone (could be a placeholder or a non-secret), so MEDIUM, never blocks.
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
echo -e "${CYAN}Scanning for generic secret-like assignments (advisory)...${NC}"
|
|
291
|
+
|
|
292
|
+
GENERIC_SECRET_RE='(secret|token|passwd|password|api[_-]?key|apikey|access[_-]?key|private[_-]?key|client[_-]?secret)["'"'"']?\s*[:=]\s*["'"'"'][A-Za-z0-9/+_=-]{16,}["'"'"']'
|
|
293
|
+
|
|
294
|
+
while IFS= read -r src_file; do
|
|
295
|
+
[ -z "$src_file" ] && continue
|
|
296
|
+
rel_path="${src_file#"$PROJECT_DIR"/}"
|
|
297
|
+
case "$rel_path" in
|
|
298
|
+
*.md|*.markdown|*.example|*.sample|*.template|*.example.*|*.dist) continue ;;
|
|
299
|
+
esac
|
|
300
|
+
while IFS=: read -r lineno line; do
|
|
301
|
+
[ -z "$lineno" ] && continue
|
|
302
|
+
if is_placeholder "$line"; then
|
|
303
|
+
continue
|
|
304
|
+
fi
|
|
305
|
+
# Skip obvious env-var indirection (not a hardcoded literal value).
|
|
306
|
+
if echo "$line" | grep -qE 'process\.env|os\.environ|getenv|ENV\[|System\.getenv'; then
|
|
307
|
+
continue
|
|
308
|
+
fi
|
|
309
|
+
report "MEDIUM" "$rel_path" "$lineno" "Secret-like variable assigned an opaque literal -- verify not a hardcoded credential"
|
|
310
|
+
done < <(grep -niE "$GENERIC_SECRET_RE" "$src_file" 2>/dev/null)
|
|
311
|
+
done < <(collect_source_files)
|
|
312
|
+
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
# Invariant 4 (advisory, LOW): logging an interpolated email-bearing variable
|
|
315
|
+
# (e.g. user.email, customerEmail). Cannot prove it is PII statically, so LOW.
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
echo -e "${CYAN}Scanning for logged email-bearing variables (advisory)...${NC}"
|
|
318
|
+
|
|
319
|
+
EMAIL_VAR_RE='(\.email\b|[Ee]mailAddress|user[Ee]mail|customer[Ee]mail)'
|
|
320
|
+
|
|
321
|
+
while IFS= read -r src_file; do
|
|
322
|
+
[ -z "$src_file" ] && continue
|
|
323
|
+
rel_path="${src_file#"$PROJECT_DIR"/}"
|
|
324
|
+
while IFS=: read -r lineno line; do
|
|
325
|
+
[ -z "$lineno" ] && continue
|
|
326
|
+
if echo "$line" | grep -qE "$LOG_CALL_RE" && echo "$line" | grep -qE "$EMAIL_VAR_RE"; then
|
|
327
|
+
report "LOW" "$rel_path" "$lineno" "Email-bearing variable referenced in a log/print statement -- review for PII leak"
|
|
328
|
+
fi
|
|
329
|
+
done < <(grep -nE "$EMAIL_VAR_RE" "$src_file" 2>/dev/null)
|
|
330
|
+
done < <(collect_source_files)
|
|
331
|
+
|
|
332
|
+
# Summary
|
|
333
|
+
echo ""
|
|
334
|
+
echo "=========================================="
|
|
335
|
+
TOTAL=$((CRITICAL + HIGH + MEDIUM + LOW))
|
|
336
|
+
echo "Results: $TOTAL finding(s)"
|
|
337
|
+
echo " CRITICAL: $CRITICAL"
|
|
338
|
+
echo " HIGH: $HIGH"
|
|
339
|
+
echo " MEDIUM: $MEDIUM"
|
|
340
|
+
echo " LOW: $LOW"
|
|
341
|
+
echo "=========================================="
|
|
342
|
+
echo ""
|
|
343
|
+
echo "Deferred (not implemented -- would be flaky as a static grep):"
|
|
344
|
+
echo " - Unhandled-error on happy path (needs control-flow / LSP diagnostics)"
|
|
345
|
+
echo " - Idempotency / round-trip (needs executing generated tests)"
|
|
346
|
+
|
|
347
|
+
if [ "$STRICT" = "--strict" ]; then
|
|
348
|
+
if [ $CRITICAL -gt 0 ] || [ $HIGH -gt 0 ]; then
|
|
349
|
+
echo ""
|
|
350
|
+
echo -e "${RED}GATE FAILED: $CRITICAL critical + $HIGH high findings${NC}"
|
|
351
|
+
exit 1
|
|
352
|
+
fi
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
if [ $TOTAL -eq 0 ]; then
|
|
356
|
+
echo -e "${GREEN}All scanned source passes invariant checks.${NC}"
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
exit 0
|