The 4-Engine Model
Warden’s codebase is organized into four engines, each with a distinct responsibility and personality:
| Engine | Role | Mnemonic |
|---|---|---|
| Reflex | Immediate safety decisions | ”Reflex acts” |
| Anchor | Session state and drift prevention | ”Anchor stabilizes” |
| Dream | Background learning and cross-session memory | ”Dream learns” |
| Harbor | Integration with assistants, tools, and protocols | ”Harbor connects” |
This is not a marketing taxonomy. The engine boundaries are enforced in the directory structure, the module system, and the API contracts between components.
Why Engines Exist
Early versions of Warden were a flat collection of hook scripts. A pretool_bash.sh script did pattern matching, loop detection, trust calculation, and output formatting in a single file. This worked until it didn’t — around 40 patterns and 3 signal types, the scripts became unmaintainable.
Engines solve three problems:
-
Separation of concerns. Reflex never touches session state. Anchor never matches patterns. Dream never formats output. Each engine owns its data and its logic.
-
Independent evolution. Adding a new safety pattern to Reflex requires no changes to Anchor. Adding a new phase to Compass requires no changes to Dream. Engines can be versioned and tested independently.
-
Budget isolation. Each engine has its own time and space budget. Reflex gets 50ms. Anchor gets 20ms. Dream runs on a background thread with no latency constraint. If one engine is slow, it does not steal budget from another.
How a Hook Call Flows Through Engines
When an assistant invokes a tool (say, Bash with command rm -rf node_modules), the following sequence occurs:
Assistant → Hook Script → Warden Binary → Router
│
├─→ Reflex (pattern match, loop check, injection scan)
│ └─→ Verdict: Approve / Deny / Modify
│
├─→ Anchor (phase detect, focus update, trust recalc)
│ └─→ Signals: [PhaseShift, FocusDrift, TrustDrop]
│
├─→ Dream (async log, no blocking)
│ └─→ (queued for background processing)
│
└─→ Harbor (format verdict + signals for assistant)
└─→ Hook Response (JSON)
The critical path is Reflex → Harbor. If Reflex denies, Harbor formats the denial and returns immediately. Anchor and Dream still process the call for state tracking, but they cannot override a Reflex denial.
If Reflex approves, Anchor’s signals are passed to Harbor for optional context injection. The decision to inject depends on the signal severity and the session’s current injection budget.
The Signal Abstraction
Engines communicate through Signals — typed structs that carry a category, severity, and payload:
pub struct Signal {
pub category: SignalCategory,
pub severity: Severity, // Info, Warning, Critical
pub message: String,
pub source: &'static str, // module that generated it
}
Signal categories include:
Safety— from Reflex: pattern matches, injection attemptsLoop— from Reflex: repetition detection, spiral detectionPhase— from Anchor: phase transitions, phase conflictsFocus— from Anchor: drift detection, file-set changesTrust— from Anchor: trust score changes, gate transitionsLearn— from Dream: new artifacts, pattern applicationsIntegration— from Harbor: assistant detection, protocol events
Signals flow one direction: from engines to Harbor. Engines do not read each other’s signals directly. If Anchor needs to know about a Reflex denial, it reads the Verdict, not a Signal.
The Verdict Enum
Every hook invocation produces exactly one Verdict:
pub enum Verdict {
Approve,
Deny { reason: String, pattern: Option<String> },
Modify { transformed_input: String, reason: String },
Skip, // module has nothing to say
}
Verdicts combine with a simple precedence rule: Deny > Modify > Approve > Skip. If any module returns Deny, the final verdict is Deny regardless of what other modules returned.
Modify is currently used only by the redirect module (rewriting tool calls to preferred alternatives). It cannot override a Deny.
The Budget Struct
Every engine receives a Budget that constrains its execution:
pub struct Budget {
pub max_duration_ms: u64,
pub max_output_bytes: usize,
pub max_injections: u8,
pub max_signals: u8,
}
Default budgets:
| Engine | Duration | Output | Injections | Signals |
|---|---|---|---|---|
| Reflex | 50ms | 512B | 0 | 10 |
| Anchor | 20ms | 1KB | 5 | 20 |
| Dream | unbounded | 4KB | 0 | 5 |
| Harbor | 10ms | 2KB | — | — |
If an engine exceeds its duration budget, its processing is interrupted and the last valid state is used. This has never triggered in production (Reflex typically completes in 2-4ms), but the guard exists for pathological inputs.
Engine Directory Tree
The engine model maps directly to the source tree:
src/engines/
├── reflex/
│ ├── mod.rs # engine entry point
│ ├── sentinel.rs # pattern matching (~300 patterns)
│ ├── loopbreaker.rs # repetition and spiral detection
│ ├── tripwire.rs # injection and bypass detection
│ └── gatekeeper.rs # central verdict aggregation (future)
├── anchor/
│ ├── mod.rs # engine entry point
│ ├── compass.rs # 5-phase session tracking
│ ├── focus.rs # file-set coherence (0-100)
│ ├── ledger.rs # turn counting and verification gaps
│ ├── debt.rs # verification debt tracking
│ └── trust.rs # trust score calculation
├── dream/
│ ├── mod.rs # engine entry point
│ ├── worker.rs # background thread (30s sleep cycle)
│ ├── artifacts.rs # V2 artifact types
│ └── budget.rs # cross-session storage limits
└── harbor/
├── mod.rs # engine entry point
├── assistant.rs # trait + adapter implementations
├── mcp.rs # MCP tool definitions
├── cli.rs # CLI command handlers
└── bridge.rs # future: external integrations
Each engine’s mod.rs exposes a single public function: pub fn process(call: &ToolCall, state: &mut SessionState, budget: &Budget) -> EngineResult. The router calls these in order and aggregates the results.
Further Reading
- Reflex Engine — pattern matching, loop detection, injection scanning
- Anchor Engine — phase tracking, trust scoring, focus management
- Dream Engine — background learning, cross-session artifacts
- Harbor Engine — assistant adapters, MCP tools, CLI commands