027a573d89
Retires the defect class behind #129. The engine's state store returns "" for a key nothing writes — no error, no warning, no log. That is how the agentic path's crisis-escalation input scored 0 on every real conversation for two days afterff421d3moved conversation history behind conv_hist_key(session_id) and left one consumer reading the old "conv_history" bucket by hand. scripts/verify-state-keys.sh is the gate; scripts/state-key-audit.py is the El reader behind it. Two checks: DEAD-READ a state_get whose key resolves to something no state_set in the tree produces. HAND-ROLLED a literal that belongs to a namespace a helper owns, accessed without the helper. This is #129's actual shape, and DEAD-READ alone does NOT catch it: the dead handle_chat() still writes "conv_history" through conv_hist_key(""). Stating that plainly because a gate that only appears to work is worse than none. WHY IT DOES NOT CRY WOLF. Keys are usually computed, so a literal-matching script would flood and be switched off in a day. The resolver handles concatenation (matched on the static prefix), helper functions (resolved to their possible returns, with guard conditions folded so conv_hist_key("") does not falsely claim to produce the session_hist_ namespace), keys built into a local, and keys arriving as a parameter (resolved through the call sites). 278 of 278 sites on this tree resolve: UNRESOLVED 0, FINDINGS 0. Unresolvable keys would be listed and would NOT fail the build. TWO-LEG PROOF, one variable — agentic_safety_screen's single line: pre-fix scripts/verify-state-keys.sh --root <scratch> chat.el:2536 state_get("conv_history") conv_hist_key() owns this key namespace (EXACT 'conv_history') FAIL: 1 state-key finding(s) exit 1 as-is scripts/verify-state-keys.sh FINDINGS (0) ... PASS exit 0 INDEPENDENT CONFIRMATION: run read-only against origin/feat/soul-openai-tools-v2, which carries the same defect on its own, the gate reported chat.el:2937 — the exact line 43d0449's message had named by hand, with no prior knowledge. Against origin/fix/129-on-openai-tools: PASS. PRODUCER-MOVED CONTROLS: renaming the sole writer of an EXACT key (soul_model) orphans 3 readers across 3 files; renaming the sole writer of a PREFIX namespace (agent_workspace_root_*) orphans 3 readers, including when the producer moves to a NARROWER namespace — a case an earlier, more permissive prefix rule let through. That rule is now directional, with the reason written next to it. FOUND ON ITS FIRST RUN, unprompted: soul.el's state_set("soul_identity", ...) was deleted 2026-05-13 inb163fa6(a commit about awareness/ISE writes) and five readers in chat.el were left behind — build_system_prompt, the vision handler, the agentic system prompt and two council handlers have prefixed "" for ~3 months. studio.el:57 emits "principal":"" and never had a producer. Both are recorded in state-key-baseline.txt with dates and causes so the gate can be turned on today; they are DEBT, not false positives, and every run prints them. Baseline signatures carry no line number (an unrelated edit must not un-mute an accepted finding) but do carry a count, so a GROWTH in a baselined finding still fails the build. Engine behaviour unchanged: this commit adds scripts only, no .el is touched. CI is deliberately NOT wired here — .gitea/workflows/ci.yaml has changes in flight from someone else, and turning the gate on would immediately red feat/soul-openai-tools-v2 (correctly). That flip should be deliberate. Rung reached: RUNS — the gate executes (0.15s), discriminates on four independent test pairs, and its verdicts are quoted above. Not wired to CI, and no engine binary was built from this branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
119 lines
6.8 KiB
Bash
Executable File
119 lines
6.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# verify-state-keys.sh — the state-key gate. Retires a defect class at build time.
|
|
#
|
|
# ── WHY THIS EXISTS. DO NOT DELETE IT AS NOISE. ──────────────────────────────
|
|
#
|
|
# The engine keeps runtime values in a key-value store: state_set("k", v) writes,
|
|
# state_get("k") reads. A read of a key that NOTHING writes returns an empty
|
|
# string. Silently. No error, no warning, no log line. The El compiler cannot see
|
|
# it, no test sees it, and the product keeps running — just with a hole in it.
|
|
#
|
|
# That is how issue #129 happened. ff421d3 (2026-08-05) correctly moved
|
|
# conversation history to a per-session key behind conv_hist_key(session_id). One
|
|
# consumer did not move with it: the agentic path's L1 safety screen kept reading
|
|
# the old anonymous "conv_history" bucket. The desktop app always mints a session
|
|
# id, so history was always written under session_hist_<id> and that read always
|
|
# returned "". The half of the crisis score that receives history is the
|
|
# ESCALATION half — the one that exists for distress building across several
|
|
# turns, where no single message trips the bell on its own. It scored 0 on every
|
|
# real conversation for two days, and nothing failed.
|
|
#
|
|
# The line that broke carried a comment describing this exact bug being fixed
|
|
# once already, under issue #9. A comment is not a gate. This is the gate.
|
|
#
|
|
# ── WHAT IT CHECKS ──────────────────────────────────────────────────────────
|
|
#
|
|
# DEAD-READ a state_get whose key resolves to something no state_set in the
|
|
# tree produces. The direct form of the class.
|
|
#
|
|
# HAND-ROLLED a state_get/state_set that spells out a literal belonging to a
|
|
# key namespace a helper function owns (e.g. "conv_history", owned
|
|
# by conv_hist_key()). This is #129's actual shape: the producer
|
|
# moved behind the helper and one consumer kept the old spelling
|
|
# by hand. DEAD-READ alone does NOT catch #129, because the dead
|
|
# handle_chat() still writes that key through the helper — so this
|
|
# second check is the one that earns the gate its keep.
|
|
#
|
|
# ── WHY IT DOES NOT CRY WOLF ────────────────────────────────────────────────
|
|
#
|
|
# Keys are usually COMPUTED, not literal, so a naive grep would flood and get
|
|
# switched off within a day. scripts/state-key-audit.py resolves computed keys:
|
|
# string concatenation (matched on the static prefix), helper functions (resolved
|
|
# to their possible return values), keys built into a local variable, and keys
|
|
# arriving as a function parameter (resolved through the call sites). Where a key
|
|
# genuinely cannot be resolved it is printed under UNRESOLVED and does NOT fail
|
|
# the build — visible, never silently ignored. Keep that list short.
|
|
#
|
|
# On this tree it resolves 278 of 278 sites: UNRESOLVED is 0 and FINDINGS is 0.
|
|
#
|
|
# Two declaration files, both of which should only ever shrink:
|
|
# scripts/state-key-external.txt keys a host outside the El tree writes
|
|
# scripts/state-key-baseline.txt findings that predate the gate (real debt)
|
|
#
|
|
# ── PROVEN TO DISCRIMINATE (2026-08-07) ─────────────────────────────────────
|
|
#
|
|
# 1. Synthetic: a scratch copy of this tree with agentic_safety_screen reverted
|
|
# to the pre-fix state_get("conv_history") — ONE line, nothing else — FAILS
|
|
# with `chat.el:2536 ... conv_hist_key() owns this key namespace`. The tree
|
|
# as shipped PASSES. One variable, opposite verdicts.
|
|
# 2. Independent: run read-only against origin/feat/soul-openai-tools-v2, which
|
|
# carries the same defect on its own, the gate reported chat.el:2937 — the
|
|
# exact line 43d0449's commit message had named by hand. Against that
|
|
# branch's fix (origin/fix/129-on-openai-tools) it passes.
|
|
# 3. Producer-moved controls: renaming the sole writer of an EXACT key
|
|
# (soul_model) orphans 3 readers across 3 files; renaming the sole writer of
|
|
# a PREFIX namespace (agent_workspace_root_*) orphans 3 readers — including
|
|
# when the producer moves to a NARROWER namespace, which an earlier,
|
|
# sloppier prefix rule let through.
|
|
#
|
|
# It also found, on its first run, a defect nobody was looking for: soul.el's
|
|
# `state_set("soul_identity", ...)` was deleted on 2026-05-13 in b163fa6 (a
|
|
# commit about awareness/ISE writes) and five readers in chat.el were left
|
|
# behind — the system prompt, the vision handler, the agentic prompt and the
|
|
# council handler have been prefixing "" ever since. See state-key-baseline.txt.
|
|
#
|
|
# ── SAFETY ──────────────────────────────────────────────────────────────────
|
|
# Pure static read of .el sources. Starts nothing, opens no port, touches no
|
|
# daemon, and never reads or writes ~/.neuron.
|
|
#
|
|
# ── USAGE ───────────────────────────────────────────────────────────────────
|
|
# scripts/verify-state-keys.sh gate the repo (honours baseline)
|
|
# scripts/verify-state-keys.sh --strict ignore the baseline: show the debt
|
|
# scripts/verify-state-keys.sh --verbose also dump every write pattern
|
|
# scripts/verify-state-keys.sh --root DIR audit a different tree
|
|
# exit 0 = clean; 1 = finding(s); 2 = the gate itself could not run.
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
STRICT=0
|
|
PASS_THROUGH=()
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--strict) STRICT=1; shift ;;
|
|
--root) ROOT="${2:?--root needs a directory}"; shift 2 ;;
|
|
-h|--help) awk 'NR>1 && /^#/ {print; next} NR>1 {exit}' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
*) PASS_THROUGH+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
command -v python3 >/dev/null 2>&1 || {
|
|
echo "[state-keys] CANNOT RUN: python3 not found" >&2; exit 2; }
|
|
[ -d "$ROOT" ] || { echo "[state-keys] CANNOT RUN: no such tree: $ROOT" >&2; exit 2; }
|
|
|
|
AUDIT="$SCRIPT_DIR/state-key-audit.py"
|
|
[ -f "$AUDIT" ] || { echo "[state-keys] CANNOT RUN: missing $AUDIT" >&2; exit 2; }
|
|
|
|
ARGS=("$ROOT" "--external" "$SCRIPT_DIR/state-key-external.txt")
|
|
[ "$STRICT" -eq 0 ] && ARGS+=("--baseline" "$SCRIPT_DIR/state-key-baseline.txt")
|
|
[ ${#PASS_THROUGH[@]} -gt 0 ] && ARGS+=("${PASS_THROUGH[@]}")
|
|
|
|
python3 "$AUDIT" "${ARGS[@]}"
|
|
RC=$?
|
|
if [ "$RC" -gt 1 ]; then
|
|
echo "[state-keys] CANNOT RUN: the audit itself failed (exit $RC)" >&2
|
|
exit 2
|
|
fi
|
|
exit "$RC"
|