Compiling formal invariants (TLA+, Alloy, temporal logic) into runtime monitors that continuously audit agent coordination daemons. The Arbiter pattern: a verification agent that smoke-tests live sessions against proven properties and triggers salvage on violation. NOT FOR application performance monitoring (use observability skills), log aggregation, static analysis, or unit testing.
Scanned 9/11/2026
Install to Claude Code
npx -y skills add curiositech/windags-skills --skill runtime-verification-for-agents --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Runtime Verification For Agents?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/curiositech-runtime-verification-for-agents)More formats (shields.io, HTML) on the badges page.
---
license: Apache-2.0
name: runtime-verification-for-agents
description: |
Compiling formal invariants (TLA+, Alloy, temporal logic) into runtime monitors that continuously audit agent coordination daemons. The Arbiter pattern: a verification agent that smoke-tests live sessions against proven properties and triggers salvage on violation. NOT FOR application performance monitoring (use observability skills), log aggregation, static analysis, or unit testing.
metadata:
category: Formal Methods & Verification
tags:
- runtime-verification
- formal-methods
- arbiter
- agent-coordination
- monitors
- invariants
---
# Runtime Verification for Agents
**Version:** 1.0
**Domain:** Formal Methods, Agent Coordination, Runtime Monitoring
**Lineage:** Port Daddy FORMAL_VERIFICATION_PLAN.md, Section 3 (The Arbiter)
## When to Use This Skill
Load this skill when:
- You have formal invariants (TLA+, Alloy, temporal logic) and need to enforce them at runtime
- You are building an agent coordination daemon and need continuous audit
- You are implementing the Arbiter pattern: an independent agent that monitors others
- You need to decide between synchronous checking and sampled checking
- You are debugging false positive cascades in your monitoring layer
- You need to handle the "who watches the watchman" recursion
Do NOT load this skill for:
- **Application performance monitoring** -- use observability/APM skills
- **Log aggregation** -- use ELK/Loki/Datadog skills
- **Static analysis** -- use linting/type-checking skills
- **Unit testing** -- use TDD skills; runtime verification is complementary, not a substitute
## Core Concept: The Arbiter
The Arbiter is an independent agent that continuously audits a coordination
daemon's state transitions against formal invariants. On violation, it
triggers remediation (salvage, flag, or halt).
```
Formal Spec (TLA+)
|
[Compilation]
|
Runtime Monitor
/ \
Synchronous Sampled
(per-call) (periodic)
| |
PASS / VIOLATE PASS / VIOLATE
| |
+-------+-------+
|
[Decision Tree]
/ | \
Alert Auto-Remediate Halt
```
## Decision Tree 1: Choosing Your Checking Strategy
```
Is the invariant safety-critical (data loss, security breach)?
|
+-- YES --> Is the operation latency-sensitive (<5ms budget)?
| |
| +-- YES --> Can you check a weaker precondition synchronously?
| | |
| | +-- YES --> HYBRID: sync precondition + async full check
| | +-- NO --> SYNCHRONOUS (accept the latency hit)
| |
| +-- NO --> SYNCHRONOUS: check on every state transition
|
+-- NO --> Is the state space small enough to snapshot cheaply?
|
+-- YES --> SAMPLED: poll every 1-5 seconds
+-- NO --> EVENT-DRIVEN: subscribe to state-change events
```
**Synchronous:** Runs in the hot path. Budget 0.1-2ms per check. Use for
safety-critical invariants where a single violation is unacceptable.
**Sampled:** Runs on a timer. Violations may persist for one interval before
detection. Use for liveness properties or when sync overhead is unacceptable.
**Hybrid:** Cheap precondition sync, full check async. Use when the full
invariant is expensive but a necessary precondition is cheap.
## Decision Tree 2: Alert vs Auto-Remediate vs Halt
```
Violation detected. What is the blast radius?
|
+-- SINGLE SESSION (one agent affected)
| |
| +-- Self-healing? (e.g., stale heartbeat -> reaper fixes it)
| | +-- YES --> LOG + ALERT (info). Let existing machinery fix it.
| | +-- NO --> AUTO-REMEDIATE: trigger salvage for affected session.
| |
+-- MULTI-SESSION (systemic violation)
| |
| +-- Could this be a false positive from clock skew or snapshot lag?
| +-- YES --> RETRY with fresh snapshot. If still violated --> escalate.
| +-- NO --> HALT new session creation. Alert operator.
|
+-- DAEMON INTEGRITY (monitor detected corruption)
--> HALT EVERYTHING. Write to append-only audit log.
Signal operator via out-of-band channel.
```
## Decision Tree 3: Clock Skew False Positives
```
Monitor reports a temporal ordering violation.
|
+-- Using wall-clock time?
| +-- YES --> Delta within NTP skew tolerance (<100ms)?
| | +-- YES --> FALSE POSITIVE. Suppress. Debug log.
| | +-- NO --> Check for NTP step adjustment.
| | NTP step? Suppress + warn. Else: REAL VIOLATION.
| +-- NO (HLC / Lamport clocks)
| +-- Logical component monotonic even if physical is not?
| +-- YES --> FALSE POSITIVE. Suppress.
| +-- NO --> REAL VIOLATION. HLC is broken. HALT.
```
## Trace-Checking Algorithms
| Approach | Latency | Memory | Use Case |
|----------|---------|--------|----------|
| **Online (streaming)** | Per-event | O(formula size) | Synchronous monitors |
| **Offline (batch)** | Post-hoc | O(trace length) | Audit, forensics |
| **Bounded online** | Per-event | O(window size) | Sliding-window properties |
For agent coordination, use **bounded online**:
- **Safety invariants** (NoteMonotonicity): O(1) per session, check every transition
- **Liveness invariants** (CrashRecovery): O(active dead agents), bounded window
- **Temporal ordering**: Ring buffer of last N events, O(N)
**Complexity budget** (100 req/s daemon, 10ms p99 target):
- Per-request monitor: 0.5ms (5% of latency budget)
- Background sweep: 10ms every 1s (1% CPU)
- Total overhead target: < 2%
## Worked Example: Compiling NoteMonotonicity
### The Formal Invariant (TLA+)
From the Port Daddy whitepaper:
```tla+
\* SAFETY: Notes never shrink for active sessions
NoteMonotonicity ==
\A a \in registered :
sessions[a] # NULL /\ sessions[a].status = "active"
=> Len(sessions'[a].notes) >= Len(sessions[a].notes)
```
For every registered agent with an active session, the note count in the
next state must be >= the current state. Notes are append-only.
### Step 1: Identify Violating Transitions
- `addNote()` -- safe by construction (only increases count)
- `deleteSession()` -- CASCADE destroys notes (violates if session active)
- Direct SQL `DELETE/UPDATE FROM session_notes` -- bypasses API (corruption)
### Step 2: Choose Strategy
Safety-critical + cheap check (integer comparison) = **synchronous**.
### Step 3: Compile to Runtime Monitor
```typescript
function createNoteMonotonicityMonitor(db: Database) {
const noteCounts = new Map<string, number>();
function checkAfterAddNote(sessionId: string): Violation | null {
const row = db.prepare(
'SELECT COUNT(*) as count FROM session_notes WHERE sessionId = ?'
).get(sessionId) as { count: number };
const previous = noteCounts.get(sessionId) ?? 0;
noteCounts.set(sessionId, row.count);
if (row.count < previous) {
return { invariant: 'NoteMonotonicity', sessionId,
previousCount: previous, currentCount: row.count,
timestamp: Date.now(), trigger: 'addNote' };
}
return null;
}
function sweep(): Violation[] {
const violations: Violation[] = [];
const rows = db.prepare(`
SELECT s.id as sessionId, COUNT(n.id) as noteCount
FROM sessions s LEFT JOIN session_notes n ON n.sessionId = s.id
WHERE s.status = 'active' GROUP BY s.id
`).all() as Array<{ sessionId: string; noteCount: number }>;
for (const row of rows) {
const previous = noteCounts.get(row.sessionId) ?? 0;
if (row.noteCount < previous)
violations.push({ invariant: 'NoteMonotonicity', sessionId: row.sessionId,
previousCount: previous, currentCount: row.noteCount,
timestamp: Date.now(), trigger: 'sweep' });
noteCounts.set(row.sessionId, row.noteCount);
}
return violations;
}
function forget(sessionId: string) { noteCounts.delete(sessionId); }
return { checkAfterAddNote, sweep, forget };
}
```
### Step 4: Wire Into the Daemon
```typescript
// In sessions.ts -- after INSERT, before return:
const violation = monitor.checkAfterAddNote(sessionId);
if (violation) arbiter.onViolation(violation);
// In server.ts -- background sweep every 5s:
setInterval(() => {
for (const v of monitor.sweep()) arbiter.onViolation(v);
}, 5000);
```
### Step 5: Remediation Handler
```typescript
function onViolation(v: Violation) {
auditLog.append({ type: 'INVARIANT_VIOLATION', ...v });
const session = sessions.get(v.sessionId);
if (session?.session?.agentId)
resurrection.enqueue(session.session.agentId, {
reason: `Arbiter: ${v.invariant} violated`, evidence: v });
messaging.publish('arbiter-violations', JSON.stringify(v));
webhooks.fire('arbiter.violation', v);
}
```
### Step 6: Verify the Monitor
**Quality gate:** 100% of injected violations caught, < 2% CPU overhead.
```typescript
// Injection test: direct DELETE bypassing API
db.prepare('DELETE FROM session_notes WHERE sessionId = ?').run(sessionId);
const violations = monitor.sweep();
expect(violations).toHaveLength(1);
expect(violations[0].currentCount).toBeLessThan(violations[0].previousCount);
// Overhead test: <0.5ms per check
const start = performance.now();
for (let i = 0; i < 10_000; i++) monitor.checkAfterAddNote(sessionId);
expect((performance.now() - start) / 10_000).toBeLessThan(0.5);
```
## Failure Modes
### Failure Mode 1: Monitoring Overhead Kills Throughput
**Symptom:** p99 latency spikes after enabling synchronous monitors.
**Root cause:** Checks too expensive for the hot path -- multi-table JOINs,
full table scans, or checking all-session invariants per single-session op.
**Detection:** Track `check_duration_ms` per invariant. Alert if >1ms average.
**Remediation:**
```
Is the slow check safety-critical?
+-- YES --> Cache/precompute state (e.g., in-memory count map, not COUNT(*))
| If still slow --> HYBRID: cheap sync + expensive async
+-- NO --> Move to SAMPLED. Accept detection delay.
```
**Prevention:** Budget 0.5ms/sync check. Precompute state. No full scans in sync path.
### Failure Mode 2: False Positive Cascades
**Symptom:** Arbiter triggers salvage on healthy sessions. Death spiral where
remediation triggers further false violations.
**Root causes:**
1. **Clock skew:** Wall-clock goes backward after NTP adjustment
2. **Snapshot inconsistency:** Monitor reads mid-transaction
3. **Stale monitor state:** Cache diverges from DB after crash recovery
**Detection:** Track false positive rate. If >5% of violations not reproducible
on re-check, your monitor has a consistency bug.
**Remediation:**
```
+-- Clock skew --> Use HLC. Suppress violations where delta < NTP_TOLERANCE.
+-- Snapshot --> Check after transaction commits, not during.
+-- Stale state --> Resync from DB on startup, after crashes, and every 60s.
```
**Prevention:** Always double-check before remediating. Single-check triggers
re-check; only double-confirmed triggers salvage.
### Failure Mode 3: The Watchman Crashes (Quis Custodiet)
**Symptom:** Arbiter dies/hangs. No invariants checked. Violations accumulate
silently. System believes it is monitored when it is not.
**Root causes:**
1. Unhandled exception in monitor code (malformed input -> SQL error)
2. Unbounded state growth (tracking every session ever, not just active)
3. Deadlock (monitor lock conflicts with daemon write path)
**Detection:**
- **Heartbeat canary:** Arbiter publishes heartbeat every N seconds; daemon
alerts if no heartbeat within 2N seconds
- **Self-check invariant:** "I am running" is always the first check
**Remediation:**
```
Heartbeat missed.
+-- PID alive?
| +-- YES --> Hung. SIGTERM, wait 5s, SIGKILL. Restart + resync.
| +-- NO --> Crashed. Restart immediately. Check crash log.
+-- After restart: full sweep immediately.
+-- 3+ crashes in 10 min: HALT new sessions. Alert operator.
Do NOT keep restarting -- fix the bug first.
```
**Prevention:** try/catch every check. Bound state with LRU. Separate DB
connection (WAL mode). Meta-monitor must be trivially simple -- just a
`setInterval` checking a timestamp. Complex meta-monitors create infinite regression.
## Anti-Patterns
**1. Checking Everything Synchronously.** Partition by criticality: safety-critical
gets sync, liveness gets sampled, statistical gets offline batch.
**2. Monitors That Mutate State.** Monitors observe and report. Remediation is
separate. A monitor that "fixes" violations is a participant, not an observer --
it introduces its own bugs and triggers other monitors.
**3. Unbounded Monitor State.** Track only active sessions. `forget()` on end.
Hard cap with LRU eviction. Cold-start from DB if needed.
**4. Alert Fatigue.** Severity tiers + suppression policies. Low-severity
accumulates into hourly digests. Only high-severity triggers immediate alerts.
**5. Happy-Path-Only Testing.** Every monitor needs injection tests: direct SQL
bypassing API, concurrent races, clock manipulation, simulated crash recovery.
## Quality Gates
| Gate | Criterion | Verification |
|------|-----------|-------------|
| Correctness | 100% of injected violations caught | >= 10 injection scenarios |
| No false positives | Zero on 24h clean run | Load test against healthy daemon |
| Overhead | < 2% CPU, < 0.5ms/sync check | Benchmark with/without monitor |
| Crash resilience | Recovers from own crash | Kill mid-sweep, verify resync |
| State bounded | Memory O(active sessions) | 24h churn test, measure RSS |
| Watchman health | Canary detects death within 2x interval | Kill monitor, verify alert |
## Compiling Any TLA+ Safety Invariant
1. **Identify state variables** referenced by the invariant
2. **Identify actions** that can modify those variables
3. **Per action**, determine if sync check is feasible (< 0.5ms)
4. **Compile** into `(previous, current) -> Violation | null`
5. **Wire** into action path (sync) or background sweep (sampled)
6. **Define remediation**: log, alert, salvage, or halt
7. **Write injection tests** that bypass the API
8. **Measure overhead** against quality gates
### Port Daddy Invariant Reference
| Invariant | Strategy | State | Remediation |
|-----------|----------|-------|-------------|
| NoteMonotonicity | Sync + sweep | noteCount/session | Salvage |
| EscrowInvariant | Sync (per Begin) | escrow/session | Reject Begin |
| CrashRecovery | Sampled (5s) | dead agent times | Trigger reaper |
| HeartbeatFreshness | Sampled (10s) | heartbeat/agent | Mark stale/dead |
| FileClaimConsistency | Sync (per claim) | claims/file | Alert on conflict |
## The Arbiter as a Port Daddy Agent
The Arbiter registers as an agent, sends heartbeats, and writes violation
notes. If it crashes, the reaper detects it and enqueues it for salvage:
```bash
pd agent register --agent arbiter-001 \
--identity myproject:arbiter:main --purpose "Invariant monitoring"
pd begin --agent arbiter-001 --purpose "Monitoring against formal invariants"
```
The recursion terminates because the reaper is a simple timer with no
invariant checks of its own -- the ground truth that must be correct by
inspection, not by monitoring.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!