The Sentinel
I3's detections judge when asked. Somebody still has to ask, and an investigation that begins with "did anyone look at the ledger today?" has already lost. The sentinel asks on a cadence — and, crucially, speaks only when something changes.
by Mansour Ayouni · in the repository since 2026-07-31 · series · the security series · area Security & governance
Alarms on transitions, not on states — and a case photographed the moment one fires
Every code block below is real, and every output block is its actual output (the run lives in
base/test/system/_secsentinel_narration_demo.ring; the guard suite isbase/test/system/security_sentinel_narrated.ring, 33 assertions). Incident analysis I4 —doc/design/SOFTANZA_INCIDENT_ANALYSIS.md.
From asking to watching
StzOpenSecurityLedger(512)
oSent = StzSecuritySentinel(StzDefaultDetectionSet())
oSent.OnDetection(func aFinding { ... })
oSent.OnClear(func cWhere { ... })illustration
quiet system : 0 alert(s)
after five guesses:
>> ALERT [error] credential-stuffing/victim
5 x auth.login.failed within 60000ms by 'victim' -- repeated authentication failures against one account
still under attack:
0 new alert(s) -- the story did not restartRead the last line twice: the attack is still happening — another failed login just arrived — and the sentinel says nothing new. The alarm fired when the story began, and it will speak again only when the story genuinely ends and returns. This is the same edge-triggering the perf system settled on, and it is the difference between an alert an operator reads and an alert an operator filters.
What counts as "a story"
The identity of a firing is the finding's :where —
credential-stuffing/victim, not merely credential-stuffing. So:
- one account under sustained attack is one story, no matter how long it lasts;
- a second account under attack is a second story, fired separately (the guard proves both);
- and when the evidence ages out of the ledger's window, the story clears — with its own callback and its own channel — so a returning attack is honestly a new alarm rather than a duplicate.
That last property is why the sentinel keeps an alert log of both
kinds: the guard asserts the sequence fire, fire, clear, clear, fire
end to end.
The case, photographed at the moment of firing
An investigation that starts hours later inherits a window that has moved on. So when something fires, the sentinel takes a picture:
aCase = oSent.LastCase()
illustration
the case, photographed when it fired:
story : credential-stuffing/victim
severity : error
ledger held : 6 event(s)
head digest : 462edef3afcc78cdc713472d...
nearest events:
auth.login.failed by victim (x5)
auth.lockout.engaged by victimThe head digest matters more than it looks: it is I1's chain head, a commitment to the entire history as it stood at that instant. If the ledger is later edited, the case's digest and the ledger's no longer agree — the snapshot is evidence about evidence. And the nearest events are the seed of the timeline I5 will build, so the investigation will not have to re-derive the moment of detection.
Notice something the demo did not have to arrange: the sixth event in
that picture is auth.lockout.engaged. The library's own defence
fired during the attack, and the case records both the attack and the
response — because both went through seams in I2.
Three ways to run it, one of them hands-free
Check() when you like; Every(ms) + Tick() from a loop you
already run; or — the one that matters — Name_() + Cycle(), the
house agent contract, so any stzAgentHost supervises it:
oHost.Supervise(oSentinel, 50) oHost.RunFor(260)
illustration
the host ticked the sentinel 6 time(s)
The guard runs exactly that, with a replayed-nonce event planted in
the watched ledger, and asserts the hosted copy fired on it. Since
stzAppServer hosts agents on this same contract, a served
application can watch its own security while it serves — no separate
process, no cron, no forgetting.
Every transition also fans out on the process-global event bus
(sec.detection / sec.clear), so an agent supervised on that
channel wakes exactly when something fires.
One thing deliberately not done
The sentinel does not write its own firings into the ledger as events. Detections over detection-events invite feedback loops, and the ledger is a record of what the system did, not of what the watcher thought about it. The alert log is the watcher's own memory, kept separately and bounded — 256 alerts, 16 cases, the house bounded-store law.
Honest limits
A detection stays "active" as long as its evidence remains in the
ledger's retained window, so window size and eviction determine when a
story clears — bounded means forgetting, and here forgetting is also
what makes a re-alarm possible. The cadence is monotonic-clocked, but
the detections it runs judge wall-time windows. And the sentinel's
transition state is Haro-side: one face drives Check(), every face
reads the same ledger.
I5 turns a case into an incident: the timeline, the actors, the attack path from the security graph, the blast radius of whatever secret is implicated — the file a person actually reads at 3 a.m.
Proved by
The article names these scenario guards of the library:
Related reading
Cite
Mansour Ayouni. "The Sentinel". Softanza narrations, 2026. https://mayouni.github.io/stzsite/en/narrations/stz-security-sentinel-narration.html
Not run: its code does not load as written. The article is published as written; its blocks are illustrations. The code of the articles is Haro code. Where an article was written before the language took its present name, the name was changed and nothing else. Genre, area and series read from the file's name, the year from its first commit: derived, until the article carries its own header. The file on GitHub(opens in a new tab)

