#!/usr/bin/env bash # fn-oracle.sh — A6 of the wake canon (EPIC #892, W5): the FN-oracle / # synthetic-canary. # # CONTRACT ANCHORS (docs/scratchpads/heartbeat-planning/CONVERGED-DESIGN.md): # §4 vector { synthetic-canary FN-rate = 0 } — the oracle injects a KNOWN # synthetic-canary delta into the wake pipeline and ASSERTS it # reaches CONSUMED within its per-class SLO; it measures the # FALSE-NEGATIVE rate. FN-rate MUST be 0 for the timer to retire. # §4/§7-res5 The FN-oracle is a real, NON-OPTIONAL operating cost — the price # of being allowed to retire the fixed timer, not a free extra. # §1.4 Framework component, operator-agnostic (~/.config/mosaic/tools/). # # OFF-DOMAIN / DETECTOR-INDEPENDENT (the §4/A8 false-negative-blindspot killer): # The injection and the CONSUMED-assertion do NOT depend on the detector's # own hashing / poll internals. The oracle injects a delta at the SOURCE # boundary (the bytes an operator adapter reports), drives the pipeline # through the detector's PUBLIC API only (poll-once — a black box), and then # renders its verdict SOLELY from the terminal store/ack state (did a CONSUMED # ack advance the cursor to cover the canary's observed_seq?). It never reads a # detector hash-file, poll counter, or self-report. CONSEQUENCE: a detector # that silently DROPS changes — including a FULLY-DISABLED detector at a # "perfect" no-op rate (0 wakes/day, which looks great on the wakes metric) — # still FAILS the oracle, because the canary never reaches CONSUMED. That is # the whole point: the success metric is a VECTOR, not a wake-count. # # ISOLATION: the probe runs in its OWN XDG state namespace (WAKE_ORACLE_HOME), # NOT the operator's live queue, so a synthetic canary never delivers a fake # wake to a real agent. It exercises the REAL detector.sh / store.sh / ack.sh # CODE paths against isolated synthetic data — the standard synthetic-canary # shape (synthetic transaction, real code, isolated data). # # SCOPE: calls the PUBLIC APIs of detector.sh (poll-once), store.sh # (cursors/drain), and ack.sh (received/consumed). It does NOT reimplement or # modify any of them. # # Operator-agnostic: all state via XDG/env; no operator paths/names/secrets. set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=./_wake-common.sh disable=SC1091 . "$SCRIPT_DIR/_wake-common.sh" DETECTOR_SH="$SCRIPT_DIR/detector.sh" STORE_SH="$SCRIPT_DIR/store.sh" ACK_SH="$SCRIPT_DIR/ack.sh" # Isolated oracle state root (XDG). NEVER the operator's live wake queue. ORACLE_HOME="${WAKE_ORACLE_HOME:-${XDG_STATE_HOME:-$HOME/.local/state}/mosaic/wake/fn-oracle}" # The canary's own store namespace lives UNDER the oracle root. CANARY_STATE_HOME="$ORACLE_HOME/canary-store" CANARY_AGENT="canary" CANARY_SRC="$ORACLE_HOME/canary.src" CANARY_ADAPTER="$ORACLE_HOME/canary-adapter.sh" CANARY_WATCHLIST="$ORACLE_HOME/canary-watch-list.json" METRICS="$ORACLE_HOME/metrics.jsonl" _need_jq() { command -v jq >/dev/null 2>&1 || { echo "fn-oracle.sh: jq is required" >&2 exit 3 } } usage() { cat >&2 <<'EOF' Usage: fn-oracle.sh run --slo-seconds N [options] Commands: run --slo-seconds N [--count K] [--class C] Run K synthetic-canary cycles (default 1). Each cycle injects a KNOWN delta at the source boundary, drives the pipeline through the detector's public poll-once, then asserts the canary reaches CONSUMED within the per-class SLO (N seconds). Emits per-canary PASS/FN-DETECTED, the FN-rate metric, and an overall VERDICT. Exit 0 IFF FN-rate == 0. status Print the tail of the recorded metrics ledger. Options: --slo-seconds N REQUIRED for run. The per-class SLO (operator-tuned; symbolic tiers per §4 — no default is invented). event->CONSUMED must land within N seconds or the canary counts as a false-negative. --count K Number of canary cycles this run (default 1). --class C Wake class the canary flows as (default digest — the primary machine-wake path §4 certifies). digest|actionable|human. Environment: WAKE_ORACLE_HOME Isolated oracle state root (XDG by default). NEVER the operator's live queue. WAKE_ORACLE_DETECTOR_CMD Command the oracle runs to advance ONE observe cycle of the pipeline (default: detector.sh poll-once). The canary env (watch-list/adapter/state) is exported to it. Pluggable so a disabled/dropping detector can be exercised (it MUST then be caught as FN-DETECTED). WAKE_ORACLE_SLO_SECONDS Fallback for --slo-seconds. EOF } # --- one-time synthetic-canary scaffold (idempotent) ----------------------- # The oracle SUPPLIES its own source adapter + watch-list so it needs no # operator config and stays fully self-contained. The adapter simply echoes the # current canary source bytes — the synthetic "source of truth" the oracle # controls end-to-end. _write_scaffold() { local class="$1" mkdir -p "$ORACLE_HOME" cat >"$CANARY_ADAPTER" < any enqueue is unambiguously the # canary. The class is carried so the canary flows on the class under test. jq -n --arg class "$class" '{ schema_version: 1, repos: [ { id: "fn-canary", remote: "synthetic/fn-oracle-canary", class: $class } ], watches: [ { lane: "fn-oracle", sources: [ { kind: "repo", id: "fn-canary" } ] } ] }' >"$CANARY_WATCHLIST" } # _canary_env — export the isolated canary namespace + adapter + watch-list for # a child detector/store/ack invocation. _canary_env() { export WAKE_STATE_HOME="$CANARY_STATE_HOME" export WAKE_AGENT="$CANARY_AGENT" export WAKE_WATCH_LIST="$CANARY_WATCHLIST" export WAKE_DETECTOR_SOURCE_CMD="$CANARY_ADAPTER" } # _store_observed / _store_consumed — read the terminal cursors (public API). _store_observed() { "$STORE_SH" cursors 2>/dev/null | sed -n 's/^observed_seq=//p'; } _store_consumed() { "$STORE_SH" cursors 2>/dev/null | sed -n 's/^consumed_seq=//p'; } # _drive_pipeline — advance ONE observe cycle. Black-box: whatever # WAKE_ORACLE_DETECTOR_CMD is (default: the real detector's public poll-once). # A source failure inside the detector is loud there; the oracle judges only the # terminal state, so a drop of ANY kind surfaces as an unmet CONSUMED. _drive_pipeline() { local cmd="${WAKE_ORACLE_DETECTOR_CMD:-"$DETECTOR_SH poll-once"}" sh -c "$cmd" >/dev/null 2>&1 || true } # _seat_baseline — seat the source's first-seen baseline ONCE (detector # first-seen is silent by design). Every subsequent canary token is then a # genuine DELTA off the prior token, so no spurious baseline wakes are produced. _seat_baseline() { _canary_env printf '%s' "FN-ORACLE-BASELINE-$$-$(date +%s)" >"$CANARY_SRC" _drive_pipeline } # --- one canary cycle ------------------------------------------------------- # Returns 0 = PASS (reached CONSUMED within SLO), 1 = FN-DETECTED. # Prints a one-line verdict for the cycle. _run_one() { local idx="$1" slo="$2" class="$3" _canary_env local nonce token inject_ts nonce="$$-$idx-$(date +%s%N 2>/dev/null || date +%s)" token="FN-ORACLE-CANARY-$nonce" local obs_before obs_before="$(_store_observed)" case "$obs_before" in '' | *[!0-9]*) obs_before=0 ;; esac # INJECT the known delta at the source boundary + stamp the clock. inject_ts="$(date +%s)" printf '%s' "$token" >"$CANARY_SRC" # Drive the pipeline (through the detector's public API — a black box). _drive_pipeline local obs_after obs_after="$(_store_observed)" case "$obs_after" in '' | *[!0-9]*) obs_after=0 ;; esac # ---- OFF-DOMAIN verdict, part 1: was the delta OBSERVED at all? ---------- # In an isolated single-source store, any observed_seq advance is the canary. # NO advance => the pipeline (a dropping / disabled detector) swallowed a KNOWN # real change => FALSE NEGATIVE. This is the killer: a perfect no-op rate does # not rescue a detector that fails to see a real delta. if [ "$obs_after" -le "$obs_before" ]; then printf ' canary %s: FN-DETECTED (injected delta never OBSERVED — pipeline dropped a known change; observed_seq %s unchanged)\n' \ "$idx" "$obs_before" >&2 return 1 fi # The delta was observed and durably enqueued. Simulate the consumer contract # (§2.2): deliver (drain), RECEIVED-ack, capture, then CONSUMED-ack the # contiguous prefix. All via the PUBLIC ack/store API. local delivered delivered="$("$STORE_SH" drain 2>/dev/null | grep -c '.' || echo 0)" if [ "${delivered:-0}" -lt 1 ]; then printf ' canary %s: FN-DETECTED (observed but NOT delivered — drain returned nothing to consume)\n' "$idx" >&2 return 1 fi "$ACK_SH" received --wake-id "fn-oracle-$nonce" >/dev/null 2>&1 || true # CONSUMED over the contiguous prefix up to the canary's observed_seq. "$ACK_SH" consumed --upto "$obs_after" --no-sync >/dev/null 2>&1 || true # ---- OFF-DOMAIN verdict, part 2: did it reach CONSUMED, within SLO? ------ local consumed elapsed now consumed="$(_store_consumed)" case "$consumed" in '' | *[!0-9]*) consumed=0 ;; esac now="$(date +%s)" elapsed=$((now - inject_ts)) if [ "$consumed" -lt "$obs_after" ]; then printf ' canary %s: FN-DETECTED (delivered but consumed_seq=%s never reached the canary observed_seq=%s)\n' \ "$idx" "$consumed" "$obs_after" >&2 return 1 fi if [ "$elapsed" -gt "$slo" ]; then printf ' canary %s: FN-DETECTED (reached CONSUMED but in %ss > per-class SLO %ss)\n' \ "$idx" "$elapsed" "$slo" >&2 return 1 fi printf ' canary %s: PASS (event->CONSUMED in %ss <= SLO %ss; observed_seq=%s consumed_seq=%s class=%s)\n' \ "$idx" "$elapsed" "$slo" "$obs_after" "$consumed" "$class" return 0 } cmd_run() { _need_jq local slo="${WAKE_ORACLE_SLO_SECONDS:-}" count=1 class="digest" while [ $# -gt 0 ]; do case "$1" in --slo-seconds) slo="${2:-}" shift 2 ;; --count) count="${2:-}" shift 2 ;; --class) class="${2:-}" shift 2 ;; *) echo "fn-oracle.sh run: unknown option '$1'" >&2 exit 2 ;; esac done # No invented numeric SLO (design law): the per-class SLO MUST be supplied. case "$slo" in '' | *[!0-9]*) echo "fn-oracle.sh run: --slo-seconds (or WAKE_ORACLE_SLO_SECONDS) is REQUIRED and must be a non-negative integer (per-class SLO is operator-tuned; no default is invented)" >&2 exit 2 ;; esac case "$count" in '' | *[!0-9]* | 0) echo "fn-oracle.sh run: --count must be a positive integer" >&2 exit 2 ;; esac case "$class" in digest | actionable | human) : ;; *) echo "fn-oracle.sh run: --class must be one of digest|actionable|human" >&2 exit 2 ;; esac # Fresh isolated probe state each run => a deterministic baseline. This is the # oracle's OWN synthetic store, never the operator's live queue. rm -rf "$CANARY_STATE_HOME" mkdir -p "$ORACLE_HOME" _write_scaffold "$class" echo "fn-oracle: synthetic-canary run (count=$count, class=$class, per-class SLO=${slo}s)" _seat_baseline # one-time first-seen baseline (silent by design) local fn=0 i i=1 while [ "$i" -le "$count" ]; do if _run_one "$i" "$slo" "$class"; then : else fn=$((fn + 1)) fi i=$((i + 1)) done # FN-rate metric. §4 requires FN-rate == 0. local rate verdict rc if [ "$fn" -eq 0 ]; then verdict="PASS" rc=0 else verdict="FN-DETECTED" rc=1 fi # Rational FN-rate rendered without bc (portable): integer numerator/denominator # plus a scaled decimal. rate="$(awk -v f="$fn" -v n="$count" 'BEGIN { printf "%.4f", (n>0? f/n : 0) }')" local rec rec="$(jq -cn \ --argjson ts "$(date +%s)" \ --arg class "$class" \ --argjson slo "$slo" \ --argjson count "$count" \ --argjson fn "$fn" \ --arg rate "$rate" \ --arg verdict "$verdict" \ '{ts:$ts, class:$class, slo_seconds:$slo, count:$count, fn:$fn, fn_rate:($rate|tonumber), verdict:$verdict}')" { cat "$METRICS" 2>/dev/null; printf '%s\n' "$rec"; } | grep -v '^[[:space:]]*$' >"$METRICS.tmp" 2>/dev/null || true mv -f "$METRICS.tmp" "$METRICS" 2>/dev/null || true echo "fn-oracle: FN-RATE = $fn/$count = $rate (class=$class, SLO=${slo}s)" echo "fn-oracle: VERDICT = $verdict" [ "$rc" -eq 0 ] || echo "fn-oracle: §4 vector FAILS — synthetic-canary FN-rate must be 0 to retire the timer." >&2 return "$rc" } cmd_status() { echo "# fn-oracle metrics (tail)" tail -n 10 "$METRICS" 2>/dev/null || echo "(no runs recorded)" } main() { [ $# -ge 1 ] || { usage exit 2 } local cmd="$1" shift case "$cmd" in run) cmd_run "$@" ;; status) cmd_status "$@" ;; -h | --help | help) usage ;; *) echo "fn-oracle.sh: unknown command '$cmd'" >&2 usage exit 2 ;; esac } main "$@"