Drift Sentinel — Windu's Silent-Failure Early-Warning

For two nights in a row the curfew said the kids were asleep. For two nights
in a row they were watching Netflix. The logs were clean, the enforcement
loop ran on schedule, and nothing — not one alert, not one red line — hinted
that the block() path had quietly stopped firing. That is the only kind of
failure Windu genuinely fears. Enforcement that silently isn’t enforcing looks,
from the attacker’s side of the glass, exactly like no enforcement at all.
So the response to the April 18–19 screen-time incident
was not a bigger dashboard. It was a watcher that would have caught both bugs
before a human noticed either one. That watcher is drift-sentinel.sh. It runs
under Windu’s sigil — because a detector for silent enforcement failure is a
security problem, not a monitoring nicety — and it has run on manoir at a
five-minute cadence, regression-gated, since 2026-04-19.
The two bugs this catches
Section titled “The two bugs this catches”1. Firewalla SDK stale-presence
Section titled “1. Firewalla SDK stale-presence”Firewalla’s /hosts endpoint returns online: false for every host when its
SDK cache drifts out of sync with the control plane. On April 18 that meant
every kid phone read as “offline,” so Force Flow’s
curfew loop never reached the block() path — the log said asleep; reality said
Netflix. The bridge itself looks healthy. Individual /host/<mac> calls answer
correctly. Only the fleet-wide online flag has rotted, which is precisely why
nobody caught it by staring at the service.
2. Duplicate-file import shadowing
Section titled “2. Duplicate-file import shadowing”When we patched ~/.sanctum/screen-time/screen_time.py, the running Force Flow
process kept importing a stale copy that lived in
~/.sanctum/force-flow/screen_time.py. Python’s module resolution found the
local file first, so every patch landed in a file nobody was executing. The
file-drift check is exactly this shape: two basenames, two different SHAs, in
two different top-level sanctum subdirectories, where one would shadow the other
under a plausible import path.
What it checks, every 5 minutes
Section titled “What it checks, every 5 minutes”The sentinel does three cheap reads and compares them against a working baseline. Nothing here is exotic — that is the point.
| Check | Signal | Threshold |
|---|---|---|
| presence drift | arp -an MAC-count vs Firewalla /hosts online-count | Warn when ARP ≥ 15 and Firewalla < ARP/3 |
| file drift | Same basename, different SHA256, in ≥2 top-level sanctum subdirs, outside known-noise zones | Any match |
| stale-import | ~/.sanctum/force-flow/screen_time.py diverges from ~/.sanctum/screen-time/screen_time.py (not symlinked) | Any divergence |
Known-noise zones (expected to contain divergent-basename files, skipped):
staging/, archive/, backups/, logs/, memory-vault/, parity/,
bench-results/, test-results/, proposals/, plus standard
.venv/ node_modules/ __pycache__/ .git/ dist/ build/.
Where the signal lives
Section titled “Where the signal lives”| Artefact | Path |
|---|---|
| Script | /Users/neo/.sanctum/scripts/drift-sentinel.sh |
| LaunchAgent | /Users/neo/Library/LaunchAgents/com.sanctum.drift-sentinel.plist |
| State (read by morning briefing) | /Users/neo/.sanctum/state/drift-sentinel.json |
| Structured log | /Users/neo/.openclaw/logs/drift-sentinel.log |
| Alert pipe | POST http://127.0.0.1:4077/notify (Force Flow, 30-min cooldown) |
The exit code tells you which world you are in — and the third one is the one to respect.
| Code | Meaning |
|---|---|
0 | Clean: zero findings |
1 | One or more drift findings — state file carries specifics |
2 | Sentinel itself broken (dependency missing, parse error) — investigate first, this isn’t a drift report, it’s a broken detector |
Reading the state
Section titled “Reading the state”python3 -c "import json; d=json.load(open('/Users/neo/.sanctum/state/drift-sentinel.json')); \ print(f\"last_run={d['last_run']}, findings={len(d['findings'])}\"); \ [print(f' - {f}') for f in d['findings']]"Every finding line is self-descriptive. It reads like a person telling you what they saw:
presence: Firewalla claims 0 online but ARP sees 56 — SDK likely stalestale-import: /Users/neo/.sanctum/force-flow/screen_time.py diverges from canonical (a1b2c3... vs d4e5f6...)file-drift: devices.yaml: divergent copies [/Users/neo/.sanctum/screen-time/devices.yaml vs /Users/neo/.sanctum/force-flow/devices.yaml]When drift fires — what to do
Section titled “When drift fires — what to do”presence drift (Firewalla stale).
The screen-time stack is already safe — the ARP fallback in _poll_presence
compensates. What needs clearing is the SDK state, and the bridge wants a bounce:
launchctl kickstart -k gui/$(id -u)/com.sanctum.firewallaIf Firewalla drifts twice in one day, read /Users/neo/.openclaw/logs/firewalla-bridge.stderr
for an upstream SDK upgrade hint.
stale-import drift. There is exactly one right fix: replace the shadow copy with a symlink to the canonical file, so the divergence can never reopen.
mv /Users/neo/.sanctum/force-flow/screen_time.py \ /Users/neo/.sanctum/force-flow/screen_time.py.stale-$(date +%Y%m%d-%H%M%S)ln -s /Users/neo/.sanctum/screen-time/screen_time.py \ /Users/neo/.sanctum/force-flow/screen_time.pylaunchctl kickstart -k gui/$(id -u)/com.sanctum.force-flowfile drift, generic. Open the two paths side by side, decide which is canonical, symlink or delete the other. The sentinel clears on the next tick.
Regression gate
Section titled “Regression gate”A detector you cannot trust is worse than no detector, so the sentinel earns its
place the same way everything else on manoir does — a test that runs the whole
loop. tests/test-drift-sentinel.sh in the Claude_Code workspace exercises it
end to end in under 10 seconds:
- Script and plist deployed, 5-min cadence
- State JSON well-formed, log emitting
run_startevents - Inject a known file-drift (replace the symlink with a real divergent file),
sentinel flags
stale-import, exit code is 1 - Clean up the injection, next run reports clean
- Exit code contract (0/1/2) respected
- Alert cooldown respected: two back-to-back drift runs don’t double-page
Currently 10 pass / 0 fail.
Why Windu
Section titled “Why Windu”Windu is the security agent, and this is the failure mode he is built to trust
nobody about. The sentinel is tier-0 infrastructure: it fires every five minutes
whether or not anyone is watching, because the whole premise of silent failure
is that nobody is. The morning briefing reads drift-sentinel.json at first
light. A non-empty findings array on that read is Windu’s way of proving the
previous night’s silence was real — not another “say it’s blocked; watch Netflix
anyway.”
Rollback
Section titled “Rollback”One command, fully reversible — it is purely observational, so removing it can break nothing downstream:
launchctl unload -w ~/Library/LaunchAgents/com.sanctum.drift-sentinel.plistrm ~/Library/LaunchAgents/com.sanctum.drift-sentinel.plistrm ~/.sanctum/scripts/drift-sentinel.shrm ~/.sanctum/state/drift-sentinel.json ~/.openclaw/logs/drift-sentinel.*No state it wrote is consumed by any service except the morning briefing, which tolerates the files being gone. The runtime drift audit covers the slower, deeper cousin of this check.
The sentinel does not make the haus safer. It makes the haus’s silences honest — and after two nights of a curfew that lied, an honest silence is the only kind Windu will sign off on.