Watch
1
0
Fork
You've already forked SouveraineOS
0

device-state: §11 — where the trail lives, what bounds it, what chains it

This commit is contained in:
Fimeg 2026-07-26 11:25:18 -04:00
commit eaa5e2923d

View file

@ -60,12 +60,12 @@ positive debounce and **250 ms negative** — near suppresses immediately, far
waits before being believed. Every flap writes a forensic entry, so this is
also most of the log volume below.
**OPEN — the forensic trail is neither durable nor bounded.**
`$XDG_RUNTIME_DIR/souveraine/forensic.jsonl` is on tmpfs: it is RAM on a 3.5 GB
phone and it is erased on every reboot. 104 KB accumulated in 40 minutes
(~3.6 MB/day) with no rotation. §5 calls this trail "tamper-evident" and pairs
it with the SessionAudit hash chain, which a file that evaporates on reboot
cannot be. Two separate decisions are owed: where it lives, and what bounds it.
**FIXED 2026-07-26 — the trail was on tmpfs, unbounded, and chained to
nothing.** `$XDG_RUNTIME_DIR/souveraine/forensic.jsonl` was RAM on a 3.5 GB
phone, erased every reboot, growing without limit — 104 KB in this audit's
first 40 minutes. §5 called it tamper-evident; it carried a sequence number,
which catches a deleted line and not an edited one. Both decisions the audit
owed are taken, and the third it implied. See §11.
Two divergences to settle when wiring, code-first per doc precedence:
- The `Locked → Observed` transition keys on **raw `proximity_near`**, not on
@ -712,3 +712,53 @@ Health answers "is the source there". It does not answer "is the source telling
the truth" — a proximity sensor stuck reporting `near` heartbeats perfectly and
reads as `Live`. §4's cross-sensor disagreement machinery is where that would
live, and it is still computed and logged without being branched on.
---
## 11. The trail itself (built 2026-07-26)
§5 said what goes in the trail. §10 made the entries diagnostic. This is about
the file.
**Where.** `$XDG_STATE_HOME/souveraine/forensic.jsonl``~/.local/state`, next
to `crashes.log`. Durable, and inside the encrypted home. No fallback path: with
no home the trail is memory-only and says so at startup. The old code fell
through to `/tmp`, which nobody would look in. Nothing is migrated; there was
never anything in the tmpfs file to migrate.
**Bounds.** 4 MiB a file, two rotated generations, 12 MiB in all. At §0's
3.6 MB/day that is three days; at the 0.5 MB/day measured since §10's heartbeat
dedupe, three weeks. Proximity debounce (§9.5) cuts it again — the flap is most
of what is left.
**The chain.** Each entry carries `prev`, the SHA-256 of the one before it, and
each written line ends with its own `hash`. Same contract as
`SessionAudit.qml`, so one verifier reads both: strip the trailing
`,"hash":"<hex>"`, close the object, hash it, compare.
Three properties, each a test:
- Rotation does not break it. The new file's first entry carries the old file's
last hash. A file read alone shows a non-empty `prev` on line 1 — correct, its
predecessor is the next file along.
- Restart does not break it. The daemon reads the tail and continues the seq and
the hash. Every run opens with `trail-opened`, which names the path, says
whether the chain resumed, and proves the file is writable before anything
needs it to be.
- A tail we cannot parse is rotated aside, never appended to. Chaining onto a
torn line makes the file fail verification forever, which reads as tampering.
The damaged file is kept — it is evidence — and a clean chain starts, loudly.
Seq, chain head and write now sit under one lock. Two locks and a later write
was survivable for a counter; two threads taking the same `prev` verifies as
tampered.
**Failed writes are loud.** Every file call was `let _ = ...`. Now the first
failure warns with path and error, and recovery says so — one line per edge, as
§10 does for source-down. The chain head advances only on a line that landed.
Advance it on a failed write and the next entry points at a `prev` no file
holds, which looks exactly like deletion.
**Owed:** nothing verifies the chain yet. The contract above is four lines of
shell, and the same verifier answers `SessionAudit.qml`, which has had this gap
since it was written.