loki-mode 7.49.0 → 7.51.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 +2 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/prd-analyzer.py +215 -1
- package/autonomy/prd-checklist.sh +315 -0
- package/autonomy/run.sh +350 -3
- package/autonomy/spec-interrogation.sh +224 -4
- package/autonomy/spec.sh +25 -16
- package/autonomy/verify.sh +108 -26
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +202 -21
- package/docs/INSTALLATION.md +2 -2
- package/docs/siem-integration.md +102 -0
- package/loki-ts/dist/loki.js +231 -230
- package/mcp/__init__.py +1 -1
- package/mcp/lsp_proxy.py +203 -0
- package/mcp/tests/test_lsp_proxy.py +169 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/references/invariant-checks.md +109 -0
- package/src/audit/crosslink.js +413 -0
- package/src/audit/index.js +32 -0
- package/src/observability/siem-export.js +424 -0
- package/src/policies/cost.js +270 -1
|
@@ -29,6 +29,28 @@
|
|
|
29
29
|
# prompt-injected, and surfaced in proof-of-done. LOKI_ASSUMPTIONS_REQUIRE_CONFIRM=1
|
|
30
30
|
# disables auto-ack for a human-in-the-loop path (only confirmed=true clears).
|
|
31
31
|
#
|
|
32
|
+
# Design note (P2-4 contradictions -- the exception to the lifecycle above):
|
|
33
|
+
# A GAP can become a recorded assumption: "the spec is silent, so I took the
|
|
34
|
+
# implementer default." A CONTRADICTION cannot -- there is no default that
|
|
35
|
+
# satisfies "X and not-X". So a contradiction (class=contradictory, forced to
|
|
36
|
+
# severity=high) is NEVER auto-acknowledged, even in default autonomous mode
|
|
37
|
+
# (spec_ledger_acknowledge_all skips it). It stays acknowledged=false and keeps
|
|
38
|
+
# the completion gate BLOCKED until a human sets confirmed=true. We chose this
|
|
39
|
+
# "block, do not assume away" behavior over the softer "auto-ack but surface
|
|
40
|
+
# loudly" option because a silently-acked contradiction lets "done" be declared
|
|
41
|
+
# over a spec we know is internally inconsistent, which is exactly the
|
|
42
|
+
# accuracy failure this feature exists to prevent. Honest cost: in a fully
|
|
43
|
+
# autonomous run an unresolved contradiction grinds the loop to max-iterations
|
|
44
|
+
# (no human to confirm), wasting budget. It is STILL surfaced in proof-of-done
|
|
45
|
+
# on every terminal path. Escape hatches: a human sets confirmed=true in
|
|
46
|
+
# .loki/assumptions/, or the operator sets LOKI_ASSUMPTION_GATE=0. A follow-up
|
|
47
|
+
# in run.sh (out of this module's scope) should add an early hard-stop on an
|
|
48
|
+
# unresolved contradiction so the run fails fast instead of grinding.
|
|
49
|
+
# Contradictions come from two sources: spec-INTERNAL (grill finds them, the
|
|
50
|
+
# classifier tags them) and spec-EXTERNAL (spec_interrogation_external_check
|
|
51
|
+
# compares the spec to the repo's declared dependencies; narrow + high-
|
|
52
|
+
# confidence by design).
|
|
53
|
+
#
|
|
32
54
|
# Provider-aware + clean degrade: grill needs a provider CLI; when absent we log
|
|
33
55
|
# an honest message, skip the grill subcall (NO fabricated questions), but STILL
|
|
34
56
|
# fold prd-analyzer's deterministic missing-dimension assumptions into the ledger
|
|
@@ -38,6 +60,7 @@
|
|
|
38
60
|
# LOKI_SPEC_GRILL=0 skip interrogation entirely
|
|
39
61
|
# LOKI_ASSUMPTION_GATE=0 completion gate is pass-through (gate file)
|
|
40
62
|
# LOKI_ASSUMPTIONS_REQUIRE_CONFIRM=1 require human confirmed=true (no auto-ack)
|
|
63
|
+
# LOKI_SPEC_EXTERNAL_CHECK=0 skip the spec-vs-repo external contradiction check
|
|
41
64
|
|
|
42
65
|
set -uo pipefail
|
|
43
66
|
|
|
@@ -81,6 +104,17 @@ spec_interrogation_severity_for() {
|
|
|
81
104
|
printf 'high'; return 0 ;;
|
|
82
105
|
esac
|
|
83
106
|
|
|
107
|
+
# P2-4: a "Contradictions" SECTION escalates to high even when the line has
|
|
108
|
+
# no contradiction keyword. Keyword-only detection would silently miss a
|
|
109
|
+
# contradiction phrased plainly ("section 2 mandates immutable records;
|
|
110
|
+
# section 5 specifies an edit endpoint."), which is exactly the failure P2-4
|
|
111
|
+
# exists to prevent -- and the grill-prompt follow-up that adds a
|
|
112
|
+
# "Contradictions" section would emit such keyword-free findings.
|
|
113
|
+
case "$lc_section" in
|
|
114
|
+
*contradiction*|*contradictor*)
|
|
115
|
+
printf 'high'; return 0 ;;
|
|
116
|
+
esac
|
|
117
|
+
|
|
84
118
|
# Section-driven severity.
|
|
85
119
|
case "$lc_section" in
|
|
86
120
|
*security*|*scale*|*reliability*)
|
|
@@ -113,6 +147,13 @@ spec_interrogation_class_for() {
|
|
|
113
147
|
printf 'contradictory'; return 0 ;;
|
|
114
148
|
esac
|
|
115
149
|
|
|
150
|
+
# P2-4: a "Contradictions" SECTION tags its findings contradictory even when
|
|
151
|
+
# the line carries no contradiction keyword (see severity_for for rationale).
|
|
152
|
+
case "$lc_section" in
|
|
153
|
+
*contradiction*|*contradictor*)
|
|
154
|
+
printf 'contradictory'; return 0 ;;
|
|
155
|
+
esac
|
|
156
|
+
|
|
116
157
|
case "$lc_section" in
|
|
117
158
|
*security*|*scale*|*reliability*) printf 'missing'; return 0 ;;
|
|
118
159
|
*unstated\ assumption*) printf 'underspecified'; return 0 ;;
|
|
@@ -249,9 +290,19 @@ spec_interrogation_classify_report() {
|
|
|
249
290
|
# Trim leading/trailing whitespace.
|
|
250
291
|
stripped="$(printf '%s' "$q" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
|
251
292
|
[ -z "$stripped" ] && continue
|
|
252
|
-
# Skip
|
|
253
|
-
|
|
254
|
-
|
|
293
|
+
# Skip negative-result / clean-spec lines so a grill that honestly
|
|
294
|
+
# reports "nothing found" never becomes a persisted finding (and, under
|
|
295
|
+
# "### Contradictions", never deadlocks a clean spec to max-iterations).
|
|
296
|
+
# Match a lowercased copy (bash 3.2 has no ${var,,}); write the original.
|
|
297
|
+
# Patterns are START-anchored to whole-line negative phrasings so a real
|
|
298
|
+
# finding that merely contains "no" (e.g. "no input validation on the
|
|
299
|
+
# login endpoint") is NOT skipped.
|
|
300
|
+
local stripped_lc
|
|
301
|
+
stripped_lc="$(printf '%s' "$stripped" | tr '[:upper:]' '[:lower:]')"
|
|
302
|
+
case "$stripped_lc" in
|
|
303
|
+
"none"|"none."*|"none found"*|"none identified"*|\
|
|
304
|
+
"no contradiction"*|"no issues"*|"no conflicts"*|"no problems"*|\
|
|
305
|
+
"no concerns"*|"no gaps"*|"not applicable"*|"n/a"*) continue ;;
|
|
255
306
|
esac
|
|
256
307
|
|
|
257
308
|
local sev class affects assumption
|
|
@@ -260,7 +311,20 @@ spec_interrogation_classify_report() {
|
|
|
260
311
|
affects="$(_spec_affects_for "$section")"
|
|
261
312
|
# No-fabrication: the finding is a QUESTION; the honest assumption is a
|
|
262
313
|
# stated default, NOT an invented resolution the build will not follow.
|
|
263
|
-
|
|
314
|
+
# P2-4: a CONTRADICTION is special. A gap can become a recorded
|
|
315
|
+
# assumption (a stated default), but a contradiction CANNOT be assumed
|
|
316
|
+
# away -- there is no consistent default for "X and not-X". So a
|
|
317
|
+
# contradictory finding carries a contradiction-specific message instead
|
|
318
|
+
# of the implementer-default text. This keeps spec_ledger_prompt_block
|
|
319
|
+
# honest (it would otherwise instruct the build agent to "proceed with a
|
|
320
|
+
# default" for something that has no consistent default) and makes the
|
|
321
|
+
# ledger rollup state the truth: unresolvable by assumption, needs a
|
|
322
|
+
# human.
|
|
323
|
+
if [ "$class" = "contradictory" ]; then
|
|
324
|
+
assumption="UNRESOLVED CONTRADICTION: the spec is internally inconsistent here and cannot be assumed away; a human must resolve it before this can be built correctly."
|
|
325
|
+
else
|
|
326
|
+
assumption="Spec gives no answer; proceeding with the implementer default for ${affects}."
|
|
327
|
+
fi
|
|
264
328
|
|
|
265
329
|
spec_ledger_write \
|
|
266
330
|
"$stripped" \
|
|
@@ -274,6 +338,144 @@ spec_interrogation_classify_report() {
|
|
|
274
338
|
return 0
|
|
275
339
|
}
|
|
276
340
|
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
# P2-4 EXTERNAL contradiction check: spec vs existing code/config.
|
|
343
|
+
#
|
|
344
|
+
# A spec-internal contradiction is found by the grill (LLM) and recognized by
|
|
345
|
+
# the classifier above. An EXTERNAL contradiction is the spec disagreeing with
|
|
346
|
+
# the repo it is being built into (spec says Postgres, repo wired to Mongo).
|
|
347
|
+
#
|
|
348
|
+
# Design constraint -- PRECISION OVER RECALL (deliberate):
|
|
349
|
+
# A contradiction is tagged high + contradictory and (per the auto-ack skip
|
|
350
|
+
# above) BLOCKS completion until a human resolves it. In an autonomous run no
|
|
351
|
+
# human is present, so a false positive grinds a GOOD spec to max-iterations.
|
|
352
|
+
# That is a severe failure mode. A grep heuristic is far lower-confidence than
|
|
353
|
+
# the LLM-identified internal contradictions feeding the same blocking path, so
|
|
354
|
+
# this check fires ONLY on UNAMBIGUOUS POSITIVE-CONFLICT evidence and is happy
|
|
355
|
+
# to miss real conflicts (low recall) rather than ever block a clean spec.
|
|
356
|
+
#
|
|
357
|
+
# Scope of THIS slice: database-engine conflict only -- the single signal where
|
|
358
|
+
# "what the spec names" and "what the repo is wired to" are both concretely
|
|
359
|
+
# detectable from declared dependencies. The trigger requires ALL FOUR:
|
|
360
|
+
# 1. the spec explicitly names database engine X, AND
|
|
361
|
+
# 2. a manifest (package.json / requirements.txt / go.mod / pyproject.toml /
|
|
362
|
+
# Gemfile) declares a concrete driver for a DIFFERENT engine Y, AND
|
|
363
|
+
# 3. that same manifest declares NO driver for engine X, AND
|
|
364
|
+
# 4. the spec does NOT also name engine Y (if it names BOTH, the spec is
|
|
365
|
+
# discussing the choice -- "we chose Mongo over Postgres" -- not in
|
|
366
|
+
# conflict; skip rather than false-fire on a good spec).
|
|
367
|
+
# Bare prose substring matches in source files are intentionally NOT used (specs
|
|
368
|
+
# mention databases in passing; comments and migrations carry both drivers).
|
|
369
|
+
#
|
|
370
|
+
# Honest deferral: other external conflicts (REST-vs-gRPC, language/runtime,
|
|
371
|
+
# cloud provider) are NOT implemented here. They need a higher-confidence
|
|
372
|
+
# extractor than a single grep and are documented as the harder follow-up.
|
|
373
|
+
#
|
|
374
|
+
# Usage: spec_interrogation_external_check <spec_path>
|
|
375
|
+
# Best-effort; writes at most one contradiction ledger entry; never fails a run.
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
spec_interrogation_external_check() {
|
|
378
|
+
local spec_path="${1:-}"
|
|
379
|
+
[ -n "$spec_path" ] && [ -f "$spec_path" ] || return 0
|
|
380
|
+
[ "${LOKI_SPEC_EXTERNAL_CHECK:-1}" = "0" ] && return 0
|
|
381
|
+
|
|
382
|
+
local repo_root="${TARGET_DIR:-.}"
|
|
383
|
+
|
|
384
|
+
# Collect declared-dependency manifest text (declarations only, not prose).
|
|
385
|
+
local manifests="" m
|
|
386
|
+
for m in \
|
|
387
|
+
"$repo_root/package.json" \
|
|
388
|
+
"$repo_root/requirements.txt" \
|
|
389
|
+
"$repo_root/pyproject.toml" \
|
|
390
|
+
"$repo_root/go.mod" \
|
|
391
|
+
"$repo_root/Gemfile"; do
|
|
392
|
+
[ -f "$m" ] && manifests="$manifests $m"
|
|
393
|
+
done
|
|
394
|
+
# No declared dependencies => no concrete repo signal => nothing to conflict.
|
|
395
|
+
[ -n "$manifests" ] || return 0
|
|
396
|
+
|
|
397
|
+
# Lowercase the spec body and the manifest text once.
|
|
398
|
+
local spec_lc deps_lc
|
|
399
|
+
spec_lc="$(tr '[:upper:]' '[:lower:]' < "$spec_path" 2>/dev/null)"
|
|
400
|
+
# shellcheck disable=SC2086 # word-split of the manifest path list is intended
|
|
401
|
+
deps_lc="$(cat $manifests 2>/dev/null | tr '[:upper:]' '[:lower:]')"
|
|
402
|
+
[ -n "$spec_lc" ] && [ -n "$deps_lc" ] || return 0
|
|
403
|
+
|
|
404
|
+
# Engine -> concrete driver-dependency token (a dependency name, not prose).
|
|
405
|
+
# Keys are the engine names we look for in the SPEC; values are the package
|
|
406
|
+
# tokens that prove the repo is wired to that engine.
|
|
407
|
+
_spec_db_driver_token() {
|
|
408
|
+
case "$1" in
|
|
409
|
+
postgres) printf '%s' 'pg|psycopg|postgresql|asyncpg|node-postgres|sequelize-postgres|gorm.io/driver/postgres' ;;
|
|
410
|
+
mongodb) printf '%s' 'mongoose|pymongo|mongodb|motor|go.mongodb.org/mongo-driver' ;;
|
|
411
|
+
mysql) printf '%s' 'mysql|mysql2|pymysql|mysqlclient|gorm.io/driver/mysql' ;;
|
|
412
|
+
*) printf '' ;;
|
|
413
|
+
esac
|
|
414
|
+
}
|
|
415
|
+
# Does the spec name engine $1? Match unambiguous engine names only.
|
|
416
|
+
_spec_names_engine() {
|
|
417
|
+
case "$1" in
|
|
418
|
+
postgres) printf '%s' 'postgres\|postgresql' ;;
|
|
419
|
+
mongodb) printf '%s' 'mongodb\|mongo db\|mongo database' ;;
|
|
420
|
+
mysql) printf '%s' 'mysql' ;;
|
|
421
|
+
*) printf '' ;;
|
|
422
|
+
esac
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
local engines="postgres mongodb mysql"
|
|
426
|
+
local spec_engine other_engine
|
|
427
|
+
for spec_engine in $engines; do
|
|
428
|
+
local spec_pat
|
|
429
|
+
spec_pat="$(_spec_names_engine "$spec_engine")"
|
|
430
|
+
[ -n "$spec_pat" ] || continue
|
|
431
|
+
printf '%s' "$spec_lc" | grep -q -e "$spec_pat" || continue
|
|
432
|
+
|
|
433
|
+
# Spec names this engine. Is the repo wired to a DIFFERENT one, with no
|
|
434
|
+
# driver for the spec's engine?
|
|
435
|
+
local spec_engine_token
|
|
436
|
+
spec_engine_token="$(_spec_db_driver_token "$spec_engine")"
|
|
437
|
+
# If the repo DOES declare a driver for the spec's engine, there is no
|
|
438
|
+
# conflict (they agree) -- skip.
|
|
439
|
+
if printf '%s' "$deps_lc" | grep -E -q -- "$spec_engine_token"; then
|
|
440
|
+
continue
|
|
441
|
+
fi
|
|
442
|
+
|
|
443
|
+
for other_engine in $engines; do
|
|
444
|
+
[ "$other_engine" = "$spec_engine" ] && continue
|
|
445
|
+
# PRECISION guard: if the spec ALSO names other_engine, this is not an
|
|
446
|
+
# unambiguous conflict -- the spec is discussing both engines (e.g.
|
|
447
|
+
# "we chose MongoDB over PostgreSQL"). Skip rather than false-fire on
|
|
448
|
+
# a good spec, since a false contradiction would block it to max-iter.
|
|
449
|
+
local other_spec_pat
|
|
450
|
+
other_spec_pat="$(_spec_names_engine "$other_engine")"
|
|
451
|
+
if [ -n "$other_spec_pat" ] && printf '%s' "$spec_lc" | grep -q -e "$other_spec_pat"; then
|
|
452
|
+
continue
|
|
453
|
+
fi
|
|
454
|
+
local other_token
|
|
455
|
+
other_token="$(_spec_db_driver_token "$other_engine")"
|
|
456
|
+
[ -n "$other_token" ] || continue
|
|
457
|
+
if printf '%s' "$deps_lc" | grep -E -q -- "$other_token"; then
|
|
458
|
+
# UNAMBIGUOUS: spec names X, repo declares a Y driver, repo has
|
|
459
|
+
# no X driver. Record one high/contradictory external finding.
|
|
460
|
+
local gap assumption
|
|
461
|
+
gap="External contradiction: the spec specifies the ${spec_engine} database, but this repository declares a ${other_engine} driver dependency and no ${spec_engine} driver."
|
|
462
|
+
assumption="UNRESOLVED CONTRADICTION: the spec and the existing code disagree on the database engine and this cannot be assumed away; a human must reconcile the spec with the repo before building."
|
|
463
|
+
spec_ledger_write \
|
|
464
|
+
"$gap" \
|
|
465
|
+
"$assumption" \
|
|
466
|
+
"external: spec vs repo dependencies" \
|
|
467
|
+
"high" \
|
|
468
|
+
"contradictory" \
|
|
469
|
+
"data-store" \
|
|
470
|
+
"external-check"
|
|
471
|
+
# One database-engine conflict is enough to block; stop scanning.
|
|
472
|
+
return 0
|
|
473
|
+
fi
|
|
474
|
+
done
|
|
475
|
+
done
|
|
476
|
+
return 0
|
|
477
|
+
}
|
|
478
|
+
|
|
277
479
|
# ---------------------------------------------------------------------------
|
|
278
480
|
# Fold prd-analyzer's deterministic missing-dimension assumptions into the
|
|
279
481
|
# ledger as medium (non-blocking). Reads .loki/prd-observations.md "Assumptions
|
|
@@ -371,6 +573,16 @@ print("%d %d" % (total, high))
|
|
|
371
573
|
# Auto-acknowledgment lifecycle helper: set acknowledged=true on every ledger
|
|
372
574
|
# entry. run.sh calls this once an iteration AFTER assumptions are injected into
|
|
373
575
|
# the build prompt (unless LOKI_ASSUMPTIONS_REQUIRE_CONFIRM=1). Best-effort.
|
|
576
|
+
#
|
|
577
|
+
# P2-4 EXCEPTION: class=contradictory entries are NEVER auto-acknowledged. A gap
|
|
578
|
+
# can be assumed away (auto-ack records that the implementer-default was taken),
|
|
579
|
+
# but a contradiction is unresolvable by assumption -- there is no default that
|
|
580
|
+
# satisfies "X and not-X". Auto-acknowledging it would silently clear the
|
|
581
|
+
# completion gate and let "done" be declared over an internally inconsistent
|
|
582
|
+
# spec. So a contradiction stays acknowledged=false until a human confirms a
|
|
583
|
+
# resolution (sets confirmed=true). This is deliberately the same teeth the
|
|
584
|
+
# LOKI_ASSUMPTIONS_REQUIRE_CONFIRM=1 path applies to ALL entries, scoped here to
|
|
585
|
+
# just contradictions in default autonomous mode. See the design note below.
|
|
374
586
|
# ---------------------------------------------------------------------------
|
|
375
587
|
spec_ledger_acknowledge_all() {
|
|
376
588
|
[ "${LOKI_ASSUMPTIONS_REQUIRE_CONFIRM:-0}" = "1" ] && return 0
|
|
@@ -388,6 +600,9 @@ for p in glob.glob(os.path.join(d, "a-*.json")):
|
|
|
388
600
|
continue
|
|
389
601
|
if r.get("acknowledged"):
|
|
390
602
|
continue
|
|
603
|
+
# P2-4: a contradiction cannot be assumed away, so it is never auto-acked.
|
|
604
|
+
if r.get("class") == "contradictory":
|
|
605
|
+
continue
|
|
391
606
|
r["acknowledged"] = True
|
|
392
607
|
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(p), suffix=".tmp")
|
|
393
608
|
try:
|
|
@@ -531,6 +746,11 @@ spec_interrogation_run() {
|
|
|
531
746
|
# (works with no provider) so degrade still surfaces something.
|
|
532
747
|
spec_ledger_fold_prd_observations || true
|
|
533
748
|
|
|
749
|
+
# P2-4: best-effort EXTERNAL contradiction check (spec vs repo deps). Runs
|
|
750
|
+
# with no provider (it is pure file inspection) and is intentionally narrow
|
|
751
|
+
# (high-confidence DB-engine conflict only) so it never blocks a clean spec.
|
|
752
|
+
spec_interrogation_external_check "$spec_path" || true
|
|
753
|
+
|
|
534
754
|
spec_ledger_rebuild_md || true
|
|
535
755
|
|
|
536
756
|
local counts total high
|
package/autonomy/spec.sh
CHANGED
|
@@ -470,12 +470,17 @@ PYEOF
|
|
|
470
470
|
# drift check quietly and, on drift, emits ONE SPEC_DRIFT record to stdout in
|
|
471
471
|
# the verify finding TSV shape:
|
|
472
472
|
# severity \t category \t source \t file \t line \t message
|
|
473
|
-
# Severity is
|
|
474
|
-
#
|
|
473
|
+
# Severity is High (-> BLOCKED under verify's default --block-on critical,high).
|
|
474
|
+
# A spec lock is an explicit human declaration that "this spec is the contract"
|
|
475
|
+
# (loki spec lock / sync), so once it exists, real drift is a blocking gate, not
|
|
476
|
+
# a soft concern. Graceful no-op (prints nothing, returns 0) when there is no
|
|
477
|
+
# lock or the spec cannot be resolved, so an unlocked / first-run workflow is
|
|
478
|
+
# never blocked.
|
|
475
479
|
#
|
|
476
480
|
# This function is intentionally side-effect-light for the verify caller: it
|
|
477
|
-
# still writes the drift-report.json (a useful artifact)
|
|
478
|
-
#
|
|
481
|
+
# still writes the drift-report.json (a useful artifact) and never prints to the
|
|
482
|
+
# verify human channel; the BLOCK is delivered purely via the High finding the
|
|
483
|
+
# verify verdict logic consumes.
|
|
479
484
|
# ---------------------------------------------------------------------------
|
|
480
485
|
spec_verify_hook() {
|
|
481
486
|
local out_dir="${1:-$SPEC_DIR_DEFAULT}"
|
|
@@ -485,16 +490,17 @@ spec_verify_hook() {
|
|
|
485
490
|
local spec_path
|
|
486
491
|
# Prefer the spec path recorded in the lock; fall back to resolution.
|
|
487
492
|
spec_path="$(python3 -c 'import sys,json; print(json.load(open(sys.argv[1])).get("spec_path",""))' "$lock_file" 2>/dev/null || echo "")"
|
|
488
|
-
#
|
|
489
|
-
#
|
|
490
|
-
#
|
|
491
|
-
#
|
|
492
|
-
#
|
|
493
|
-
#
|
|
494
|
-
#
|
|
493
|
+
# The lock recorded a spec path but that file is now MISSING (the locked spec
|
|
494
|
+
# was deleted). That is real drift -- the contract the lock binds no longer
|
|
495
|
+
# exists -- so emit a High spec_drift finding (blocking, consistent with the
|
|
496
|
+
# content-drift finding below) instead of silently returning 0. NEVER fall
|
|
497
|
+
# back to spec_resolve_source here: comparing against a different candidate
|
|
498
|
+
# file would mask the deletion and attest a spec that is not the locked one.
|
|
499
|
+
# The empty-spec_path case below is a SEPARATE, legitimate fallback (legacy
|
|
500
|
+
# locks that never recorded a path).
|
|
495
501
|
if [ -n "$spec_path" ] && [ ! -f "$spec_path" ]; then
|
|
496
502
|
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
497
|
-
"
|
|
503
|
+
"High" "spec_drift" "deterministic:loki-spec" "$spec_path" "null" \
|
|
498
504
|
"locked spec file missing: $spec_path (the spec is the contract; restore it or run 'loki spec sync' after review to re-lock against the current spec)"
|
|
499
505
|
return 0
|
|
500
506
|
fi
|
|
@@ -523,9 +529,10 @@ PYEOF
|
|
|
523
529
|
)"
|
|
524
530
|
[ -n "$summary" ] || return 0
|
|
525
531
|
|
|
526
|
-
# Emit one
|
|
532
|
+
# Emit one High SPEC_DRIFT finding in the verify TSV shape (blocking under
|
|
533
|
+
# verify's default --block-on critical,high; only reachable when a lock exists).
|
|
527
534
|
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
528
|
-
"
|
|
535
|
+
"High" "spec_drift" "deterministic:loki-spec" "$spec_path" "null" "$summary"
|
|
529
536
|
return 0
|
|
530
537
|
}
|
|
531
538
|
|
|
@@ -572,8 +579,10 @@ EXIT CODES:
|
|
|
572
579
|
|
|
573
580
|
VERIFY INTEGRATION:
|
|
574
581
|
When .loki/spec/spec.lock exists, `loki verify` runs the drift check and
|
|
575
|
-
adds a
|
|
576
|
-
|
|
582
|
+
adds a High-severity SPEC_DRIFT finding on drift, which BLOCKS verify under
|
|
583
|
+
its default --block-on critical,high. Locking is an explicit declaration
|
|
584
|
+
that the spec is the contract, so post-lock drift is a hard gate. No lock =
|
|
585
|
+
graceful no-op (gate skipped, never blocks an unlocked / first-run workflow).
|
|
577
586
|
|
|
578
587
|
EOF
|
|
579
588
|
}
|
package/autonomy/verify.sh
CHANGED
|
@@ -453,11 +453,104 @@ verify_gate_static() {
|
|
|
453
453
|
}
|
|
454
454
|
|
|
455
455
|
# ---------------------------------------------------------------------------
|
|
456
|
-
# Gate: secret scan (NET-NEW).
|
|
457
|
-
#
|
|
456
|
+
# Gate: secret scan (NET-NEW).
|
|
457
|
+
#
|
|
458
|
+
# WHAT IS SCANNED:
|
|
459
|
+
# Only the files in the PR diff (merge-base(base,HEAD)..HEAD), i.e. the net
|
|
460
|
+
# change being verified. Pre-existing secrets in untouched files are NOT
|
|
461
|
+
# scanned by design: a verifier attests that THE CHANGE is safe, and blocking
|
|
462
|
+
# on legacy secrets is the #1 false-positive-fatigue failure (spec 7.2).
|
|
463
|
+
# Binary files are skipped.
|
|
464
|
+
#
|
|
465
|
+
# WHEN IT RUNS:
|
|
466
|
+
# As a gate inside `loki verify`, after generation/edits and BEFORE the change
|
|
467
|
+
# is treated as VERIFIED. A real secret here forces a non-VERIFIED verdict.
|
|
468
|
+
#
|
|
469
|
+
# GITLEAKS vs FALLBACK:
|
|
470
|
+
# - If `gitleaks` is on PATH, it scans each changed file (filesystem mode,
|
|
471
|
+
# `detect --no-git --source`). Any hit -> Critical finding for that file.
|
|
472
|
+
# - If gitleaks is NOT installed, a documented two-tier regex fallback runs
|
|
473
|
+
# (see verify_secret_scan_file). The fallback is a deterministic safety net,
|
|
474
|
+
# not a replacement for a full scanner; gitleaks is recommended for depth.
|
|
475
|
+
#
|
|
476
|
+
# BLOCKING GUARANTEE:
|
|
477
|
+
# Every detected secret is emitted as a Critical finding. The default
|
|
478
|
+
# --block-on is "critical,high" (verify_main), so verify_compute_verdict sets
|
|
479
|
+
# verdict=BLOCKED and exit=2 (VERIFY_EXIT_BLOCKED). The scan BLOCKS, it does
|
|
480
|
+
# not merely warn. (Operators who pass a narrower --block-on accept the risk.)
|
|
481
|
+
#
|
|
482
|
+
# FOLLOW-UP (deferred, honest): a true pre-WRITE hook that scans generated bytes
|
|
483
|
+
# BEFORE they touch disk would be stronger still, but it must hook every write
|
|
484
|
+
# path in run.sh (out of scope for this verify.sh slice). The gate here is the
|
|
485
|
+
# strong post-generation / pre-completion scan; the pre-write hook is tracked
|
|
486
|
+
# as a follow-up and is NOT claimed to exist.
|
|
458
487
|
#
|
|
459
488
|
# skipped = no changed files to scan.
|
|
460
489
|
# ---------------------------------------------------------------------------
|
|
490
|
+
|
|
491
|
+
# Two-tier regex secret matcher for a single file. Echoes nothing; returns 0 if
|
|
492
|
+
# a high-confidence secret is found, 1 otherwise. Used ONLY by the fallback path
|
|
493
|
+
# (gitleaks absent). Kept as a standalone function so the test suite can drive it
|
|
494
|
+
# and so the two tiers are documented in one place.
|
|
495
|
+
#
|
|
496
|
+
# TIER 1 -- specific credential FORMATS. A match is conclusive on its own: the
|
|
497
|
+
# string already looks exactly like a real credential, so we do NOT apply the
|
|
498
|
+
# placeholder filter (a leaked key is a leak even if a comment says EXAMPLE).
|
|
499
|
+
#
|
|
500
|
+
# TIER 2 -- generic long quoted-value assignments (api_key="...", bearer
|
|
501
|
+
# tokens). This is a length + charset heuristic (>=16 contiguous secret-charset
|
|
502
|
+
# chars), NOT a true Shannon-entropy measure. These are false-positive magnets,
|
|
503
|
+
# so a match is only counted if the matched LINE survives the placeholder /
|
|
504
|
+
# env-reference deny filter. That keeps obvious non-secrets (your-api-key-here,
|
|
505
|
+
# REDACTED, ${API_KEY}, process.env.X) clean.
|
|
506
|
+
verify_secret_scan_file() {
|
|
507
|
+
local file="$1"
|
|
508
|
+
|
|
509
|
+
# TIER 1: specific formats. No deny filter -- a format match is a finding.
|
|
510
|
+
local tier1=(
|
|
511
|
+
'AKIA[0-9A-Z]{16}' # AWS access key id
|
|
512
|
+
'ASIA[0-9A-Z]{16}' # AWS temporary (STS) key id
|
|
513
|
+
'-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----' # PEM private key block
|
|
514
|
+
'gh[pousr]_[A-Za-z0-9]{36,}' # GitHub token (ghp_/gho_/...)
|
|
515
|
+
'github_pat_[A-Za-z0-9_]{60,}' # GitHub fine-grained PAT
|
|
516
|
+
'xox[baprs]-[A-Za-z0-9-]{10,}' # Slack token (xoxb-/xoxp-/...)
|
|
517
|
+
'sk-[A-Za-z0-9]{20,}' # OpenAI-style secret key
|
|
518
|
+
'AIza[0-9A-Za-z_-]{35}' # Google API key
|
|
519
|
+
'glpat-[A-Za-z0-9_-]{20,}' # GitLab personal access token
|
|
520
|
+
)
|
|
521
|
+
local p
|
|
522
|
+
for p in "${tier1[@]}"; do
|
|
523
|
+
# -e terminates option parsing so patterns beginning with '-' (the PEM
|
|
524
|
+
# "-----BEGIN ... PRIVATE KEY-----" block) are not mistaken for a flag.
|
|
525
|
+
if LC_ALL=C grep -Eq -e "$p" "$file" 2>/dev/null; then
|
|
526
|
+
return 0
|
|
527
|
+
fi
|
|
528
|
+
done
|
|
529
|
+
|
|
530
|
+
# Deny filter for TIER 2: a matched line is IGNORED if it is plainly a
|
|
531
|
+
# placeholder or an environment-variable reference rather than a literal.
|
|
532
|
+
# - env refs: ${VAR}, $VAR, process.env.X, os.environ/os.getenv, %VAR%
|
|
533
|
+
# - placeholders: your-/your_..., redacted, changeme, placeholder, example,
|
|
534
|
+
# dummy/sample/fake, xxxx+, <...>, runs of 4+ asterisks
|
|
535
|
+
# ("test" is deliberately NOT denied -- too common, would mask real keys)
|
|
536
|
+
local deny='(\$\{|\$[A-Za-z_]|process\.env|os\.(environ|getenv)|%[A-Za-z_]+%|your[-_]|redacted|changeme|change[-_]me|placeholder|example|dummy|sample|fake|<[^>]*>|x{4,}|\*{4,})'
|
|
537
|
+
|
|
538
|
+
# TIER 2: generic assignments. Grab matching lines, drop denied ones, count.
|
|
539
|
+
# api_key / apikey / secret / token / password / passwd / access_key, an
|
|
540
|
+
# assignment operator (= or :), then a quoted >=16-char high-entropy value.
|
|
541
|
+
local tier2='(api[_-]?key|secret|token|password|passwd|access[_-]?key|client[_-]?secret|auth)[A-Za-z0-9_]*[[:space:]]*[:=][[:space:]]*["'"'"']?[A-Za-z0-9_/+.=-]{16,}'
|
|
542
|
+
# Bearer tokens: "Bearer <>=20 high-entropy chars>".
|
|
543
|
+
local bearer='[Bb]earer[[:space:]]+[A-Za-z0-9_.\-]{20,}'
|
|
544
|
+
|
|
545
|
+
local surviving
|
|
546
|
+
surviving="$(LC_ALL=C grep -EiI "$tier2|$bearer" "$file" 2>/dev/null \
|
|
547
|
+
| LC_ALL=C grep -Eiv "$deny" 2>/dev/null)"
|
|
548
|
+
if [ -n "$surviving" ]; then
|
|
549
|
+
return 0
|
|
550
|
+
fi
|
|
551
|
+
return 1
|
|
552
|
+
}
|
|
553
|
+
|
|
461
554
|
verify_gate_secret_scan() {
|
|
462
555
|
local tree="$1"
|
|
463
556
|
local changed="$VERIFY_DIFF_NAMES"
|
|
@@ -504,17 +597,10 @@ verify_gate_secret_scan() {
|
|
|
504
597
|
return 0
|
|
505
598
|
fi
|
|
506
599
|
|
|
507
|
-
# High-confidence
|
|
508
|
-
#
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
'-----BEGIN [A-Z ]*PRIVATE KEY-----' # PEM private key
|
|
512
|
-
'gh[pousr]_[A-Za-z0-9]{36,}' # GitHub token
|
|
513
|
-
'xox[baprs]-[A-Za-z0-9-]{10,}' # Slack token
|
|
514
|
-
'sk-[A-Za-z0-9]{20,}' # OpenAI-style key
|
|
515
|
-
'AIza[0-9A-Za-z_-]{35}' # Google API key
|
|
516
|
-
)
|
|
517
|
-
|
|
600
|
+
# High-confidence two-tier matcher (verify_secret_scan_file): tier 1 specific
|
|
601
|
+
# credential formats flag unconditionally; tier 2 generic assignments flag
|
|
602
|
+
# only when the line survives the placeholder/env-ref deny filter. Conservative
|
|
603
|
+
# on purpose to limit false positives (spec Section 7.2).
|
|
518
604
|
local hits=0
|
|
519
605
|
local detail=""
|
|
520
606
|
local f
|
|
@@ -523,16 +609,12 @@ verify_gate_secret_scan() {
|
|
|
523
609
|
[ -f "$tree/$f" ] || continue
|
|
524
610
|
# Skip obvious binaries.
|
|
525
611
|
if LC_ALL=C grep -Iq . "$tree/$f" 2>/dev/null; then : ; else continue; fi
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
"Potential hardcoded secret detected in $f (matched a high-confidence credential pattern)."
|
|
533
|
-
break
|
|
534
|
-
fi
|
|
535
|
-
done
|
|
612
|
+
if verify_secret_scan_file "$tree/$f"; then
|
|
613
|
+
hits=$((hits + 1))
|
|
614
|
+
detail="${detail}${f} "
|
|
615
|
+
_verify_add_finding "Critical" "security" "deterministic:regex-secret-scan" "$f" "null" \
|
|
616
|
+
"Potential hardcoded secret detected in $f (matched a high-confidence credential pattern)."
|
|
617
|
+
fi
|
|
536
618
|
done <<<"$changed"
|
|
537
619
|
|
|
538
620
|
if [ "$hits" -eq 0 ]; then
|
|
@@ -975,9 +1057,9 @@ EOF
|
|
|
975
1057
|
# Living-spec drift gate (integration with autonomy/spec.sh).
|
|
976
1058
|
#
|
|
977
1059
|
# When .loki/spec/spec.lock exists, source the spec module and run its
|
|
978
|
-
# verify hook, which emits at most one SPEC_DRIFT finding (
|
|
979
|
-
# in the verify finding TSV shape. Records a gate row either
|
|
980
|
-
# evidence document shows the spec was checked. Fully graceful: no lock, no
|
|
1060
|
+
# verify hook, which emits at most one SPEC_DRIFT finding (High -> BLOCKED when
|
|
1061
|
+
# the spec is locked) in the verify finding TSV shape. Records a gate row either
|
|
1062
|
+
# way so the evidence document shows the spec was checked. Fully graceful: no lock, no
|
|
981
1063
|
# module, or a hook error all degrade to a skipped gate and never abort verify.
|
|
982
1064
|
# ---------------------------------------------------------------------------
|
|
983
1065
|
verify_spec_drift_gate() {
|