@zanii/sentinel
v0.3.0
Published
Runtime behavioral monitoring for agents ('antivirus for agents'): score drift between what an agent actually does and what it was made to do, from the receipt stream — and the alerts themselves are tamper-evident receipts.
Readme
@zanii/sentinel
Runtime behavioral monitoring for agents — an "antivirus for agents." The behavioral
layer between @zanii/monitor (log integrity) and @zanii/policy (pre-action rules): it
consumes the receipt stream and scores drift — what an agent actually does vs what
it was made to do — then feeds back: escalate, prepare a freeze, page.
The Zanii twist: findings become sentinel.alert receipts, recorded by the
sentinel's own did:key (watcher ≠ watched). Detection itself is tamper-evident — and so
is silence (no alert history is a fact a buyer can check). No generic agent-EDR has that.
npm install @zanii/sentinel @zanii/coreimport { buildBaseline, createWatcher, escalationRule, buildAlertReceipt, baselineHash } from '@zanii/sentinel';
// 1. baseline: declared scopes (from delegation) + learned habits (from history)
const baseline = buildBaseline(agent.did, { receipts: history, delegation: [cert] });
// 2. watch the live stream; respond on drift (responses are injected — the sentinel holds no keys)
const w = createWatcher({
server: 'https://ledger.zanii.agency',
baselines: { [agent.did]: baseline },
cursor: loadCursor(), // resume — otherwise a restart re-alerts on old receipts
onFinding: async (f) => {
await sentinelAgent.record(buildAlertReceipt(f, { baselineHash: baselineHash(baseline) })); // alert = receipt
const rule = escalationRule(f); // null when the finding names no targets
if (f.severity === 'high' && rule) policy.upsert(rule); // → require_approval speed bump
},
});
setInterval(async () => { await w.tick(); saveCursor(w.cursor()); }, 60_000);The watcher keeps a rolling window (windowMs, default 2h) so windowed detectors work
across polls — a 3-step exfil arriving one step per 60s poll still fires. Findings are
de-duplicated, so an already-alerted receipt isn't re-alerted every tick.
Feed limit: /v1/recent is a global feed the server caps at 100. On a busy ledger,
receipts can be missed between polls (the cursor jumps). For production, drive scan from
receipt.recorded webhooks (@zanii/webhooks) rather than polling.
Two modes (forced by the metadata-only ledger)
- Ledger mode — anyone (auditor, customer, you) watches behavior shape over the
public stream: novel targets, rate spikes, exfil sequences, intent gaps, off-hours,
scope-probing (from
receipt.rejectedwebhooks). No operator cooperation needed. - Operator mode — inside the operator's pipeline, inject
payloadOf+checkContent(e.g.@zanii/wallscheckOutputover raw payloads, pre-hash) for content detection.
Detectors (v1, heuristic ladder — classifier upgrade is the documented path)
novel-target, scope-edge, rate-spike, intent-gap, sequence (default: the exfil
shape read → archive → external-send), off-hours, and wall-crossing (operator mode).
Every finding names the receipts behind it — evidence, not vibes.
Honest limits (read these)
- Detection, not prevention — a sentinel sees what's recorded; an agent acting entirely off-rails is the credential-custody problem, not a sentinel gap.
- False positives are structural — agents legitimately change. Default response is the
require_approvalspeed bump (escalationRule), never auto-revocation;freezeis always human-in-the-loop (revocation needs the owner's key, which the sentinel never holds).escalationRulereturnsnullwhen a finding names no specific targets (e.g.scope-edge) — it will not silently generate a*rule that gates the agent's whole surface. - In-scope compromise is the hard case — a poisoned agent doing plausible actions inside its scopes evades metadata detection; sequences/rates narrow the window, never to zero.
Changelog
- 0.1.0 — initial release:
buildBaseline,scan(six detectors + operator content hook),createWatcher(rolling window + finding de-dup + resumable cursor),escalationRule,buildAlertReceipt/verifyAlert.
License
Apache-2.0.
