#Purpose
Replaces scripts/agent_runner.py and the agentic loop in infra/console/server.py with a single Rust daemon implementing OpenClaw's Agent Client Protocol (ACP) wire interface, extended with HAAK's constitutional governance layer and designed for the Merkle-CRDT ledger specified in [[33-constitutional-ledger]].
OpenClaw agents connect by changing one config line. All agents — native or OpenClaw — pass through the constitutional layer. The ledger schema carries proof and envelope fields from day one (null in v0), leaving no migration debt for Filix v1.0 cryptographic enforcement.
#Layer Stack
OpenClaw Agent HAAK Native Agent CLI
│ ACP/WebSocket │ ACP/WebSocket │
└─────────────┬────────┘ │
▼ │
┌─────────────────┐ │
│ Gateway Server │◄─────────────────┘
│ Axum, :18789 │
│ ACP wire compat│
└────────┬────────┘
│
┌────────▼────────┐
│ SessionManager │ serial queues · TTL eviction
│ │ identity reconciliation
│ │ session store (SQLite)
└────────┬────────┘
│
┌────────▼────────┐
│ Constitutional │ mandate injection · policy gates
│ Runtime<R> │ mailbox · board · roster heartbeat
│ │ ledger append (BLAKE3 + parents)
└────────┬────────┘
│
┌───────────┴───────────┐
│ │
┌───────▼─────────┐ ┌─────────▼───────────┐
│AnthropicBackend │ │ ClaudeCodeBackend │
│reqwest + SSE │ │ subprocess │
│reasoning blocks │ │ stream-json parse │
└─────────────────┘ └─────────────────────┘
#AgentRuntime Trait
ACP-compatible interface. Any backend implementing this trait is governable by ConstitutionalRuntime and manageable by SessionManager.
#[async_trait]
pub trait AgentRuntime: Send + Sync {
async fn ensure_session(&self, input: SessionInput) -> Result<Handle>;
fn run_turn(
&self,
handle: &Handle,
input: TurnInput,
) -> BoxStream<'static, Result<RuntimeEvent>>;
async fn cancel(&self, handle: &Handle) -> Result<()>;
async fn close(&self, handle: &Handle, reason: CloseReason) -> Result<()>;
async fn status(&self, handle: &Handle) -> Result<SessionStatus>;
}
#RuntimeEvent
ACP wire-compatible core. HAAK extensions are additive — unknown type values are ignored by OpenClaw clients. seq is mandatory on all events from day one (prerequisite for stream signing in v1.0).
pub enum RuntimeEvent {
// ACP-compatible — same JSON field names as OpenClaw
TextDelta { seq: u64, text: String },
ToolCall { seq: u64, id: String, name: String, input: Value },
ToolCallUpdate { seq: u64, id: String, input_delta: String },
ToolResult { seq: u64, id: String, content: String, is_error: bool },
UsageUpdate { seq: u64, input_tokens: u32, output_tokens: u32 },
Done { seq: u64, stop_reason: StopReason },
Error { seq: u64, code: String, message: String },
// HAAK extensions
ReasoningDelta { seq: u64, text: String },
PolicyGate { seq: u64, entry: LedgerEntry },
MailboxInjected { seq: u64, entry: LedgerEntry },
LedgerAppend { seq: u64, entry: LedgerEntry },
}
#Ledger Entry
Every turn, tool call, and policy verdict produces a ledger entry. Schema matches [[33-constitutional-ledger]] exactly. proof and envelope are null in v0; their fields are present and typed from day one.
pub struct LedgerEntry {
pub cid: Cid, // BLAKE3 over JCS-canonical form (RFC 8785)
pub entity_id: String, // agent session key
pub target: String, // session id or tool id
pub quality: EntryQuality, // Turn | ToolCall | PolicyVerdict | MailboxInject | SessionLifecycle
pub timestamp: DateTime<Utc>,
pub source: String, // session key of producing agent
pub actor: String, // agent roster id
pub parents: Vec<Cid>, // prev turn CID, tool call CID, etc.
pub tags: Vec<String>,
pub payload: Value, // JCS canonical; skill trace format for Turn entries
pub proof: Option<Proof>, // null (v0) → attestation (v1.0) → zk-stark (v2.0)
pub envelope: Option<Envelope>, // null (v0/v1.0) → KDF-encrypted (v1.5)
}
Skill trace payload format (for Turn entries), as specified in [[strategy/19-filix-v1-plan]]:
{
"skill_name": "agent_runner",
"inputs_hash": "<blake3 of canonical turn input>",
"outputs_hash": "<blake3 of canonical response>",
"timestamp": "<iso-8601-utc>",
"actor": "<roster id>"
}
#Session Store
SQLite. Designed so that the Filix v1.0 transition (session store becomes a materialized view over the ledger) requires no schema migration — prev_cid and proof fields are present from v0.
CREATE TABLE sessions (
id TEXT PRIMARY KEY, -- blake3(agent_id || session_key || created_at)
agent_id TEXT NOT NULL,
session_key TEXT NOT NULL UNIQUE,
backend TEXT NOT NULL, -- "anthropic" | "claude_code"
model TEXT,
mode TEXT NOT NULL, -- "persistent" | "domain" | "oneshot"
state TEXT NOT NULL, -- "idle" | "running" | "cancelled" | "closed"
pubkey TEXT, -- null (v0); ed25519 pubkey (v1.0)
last_activity TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE turns (
id TEXT PRIMARY KEY, -- cid of ledger entry
session_id TEXT NOT NULL REFERENCES sessions(id),
seq INTEGER NOT NULL,
prev_cid TEXT, -- null for first turn; hash-chained thereafter
input_hash TEXT NOT NULL, -- blake3 of canonical turn input
output_hash TEXT, -- blake3 of canonical response; null until complete
stop_reason TEXT,
usage TEXT, -- json: {input_tokens, output_tokens}
started_at TEXT NOT NULL,
completed_at TEXT,
proof TEXT -- null (v0); json attestation (v1.0)
);
CREATE TABLE history (
id TEXT PRIMARY KEY, -- cid of this message entry
session_id TEXT NOT NULL REFERENCES sessions(id),
turn_id TEXT REFERENCES turns(id),
seq INTEGER NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL, -- JCS canonical json
created_at TEXT NOT NULL
);
CREATE TABLE ledger (
cid TEXT PRIMARY KEY,
quality TEXT NOT NULL,
entity_id TEXT NOT NULL,
target TEXT,
actor TEXT NOT NULL,
parents TEXT NOT NULL, -- json array of cids
tags TEXT NOT NULL, -- json array
payload TEXT NOT NULL, -- JCS canonical
proof TEXT, -- null (v0)
envelope TEXT, -- null (v0/v1.0)
timestamp TEXT NOT NULL
);
Session IDs are BLAKE3-derived, not random UUIDs. This makes them re-derivable from agent identity — prerequisite for keypair-anchored session identity in v1.0.
#SessionManager
Matches OpenClaw's AcpSessionManager. Per-session serial queue: no concurrent turns within one session. TTL eviction applies only to domain mode sessions; persistent sessions (standing roles) are never evicted.
Three session modes:
| Mode | TTL eviction | Use |
|---|---|---|
persistent | never | Standing roles (Naga, Veda, Reed) |
domain | configurable (default 30 min) | Project-scoped agents |
oneshot | after each turn | Console web sessions |
On startup, SessionManager reads data/agent-roster.jsonl for standing: true agents and calls ensure_session for each, preloading them before the gateway accepts connections.
#ConstitutionalRuntime<R>
A decorator implementing AgentRuntime by wrapping any other AgentRuntime. The session manager sees only another AgentRuntime — the constitutional layer is transparent to it and to the inner backend.
Per-turn sequence:
- Session open — append
SessionLifecycleledger entry; register/heartbeat roster - Pre-turn — read unread mailbox entries; append
MailboxInjectledger entry for each; prepend to history as[mailbox from X: ...]user turn - System prompt — assemble: constitutional preamble + mandate text + board excerpt +
constitution_hash: blake3(constitution.md)in prompt footer (non-enforcing in v0; read by policy engine in v1.0) - Tool gating — pass tool list through
policy.gate(tools); emitPolicyGateledger entry per tool with verdict andconstitution_hash;proof: nullin v0 - Run inner — stream
RuntimeEvents frominner.run_turn() - On
ToolCall— re-evaluate policy with known input; appendToolCallledger entry; ifBlocked, short-circuit: emitPolicyGateevent, injecttool_resulterror, continue - On
ToolResult— appendToolResultledger entry,parents: [toolcallcid] - On
Done— computeoutputhash(BLAKE3 of accumulated response); completeTurnledger entry withprevcid= previous turn's CID; board post if relevant
#OpenClaw Compatibility
An OpenClaw user migrates by adding one backend entry to their config:
// ~/.openclaw/config.json
{
"acp": {
"backends": {
"haak": { "url": "ws://localhost:18789" }
}
}
}
And pointing their agent's runtime at it:
{ "runtime": { "type": "acp", "acp": { "backend": "haak", "mode": "persistent" } } }
Wire protocol — our gateway implements OpenClaw's full RPC surface:
| OpenClaw RPC | Handler |
|---|---|
session.init | SessionManager::ensure_session |
turn.run (streaming) | SessionManager::run_turn |
session.cancel | SessionManager::cancel |
session.close | SessionManager::close |
session.status | SessionManager::status |
Constitutional bind for OpenClaw agents. Unknown connecting agents receive the default mandate: sandboxed workspace, restricted tool allowlist (read/search/send_message only — no shell, no unconstrained file write, no direct network), board check-in on session open, all tool calls logged to ledger. A registered agent (known pubkey in roster) receives its specific mandate. The agent cannot negotiate or opt out — the constitutional layer is gateway-enforced.
#Crate Layout
New crate at infra/daemons/agent-runner/, sibling to infra/daemons/gateway/.
infra/daemons/agent-runner/
├── Cargo.toml
└── src/
├── main.rs
├── lib.rs
├── runtime/
│ ├── mod.rs # AgentRuntime trait, RuntimeEvent, Handle, TurnInput, CloseReason
│ ├── anthropic.rs # AnthropicBackend — reqwest + SSE, reasoning blocks, tool loop
│ └── claude_code.rs # ClaudeCodeBackend — subprocess + stream-json parse
├── constitution/
│ ├── mod.rs # ConstitutionalRuntime<R>
│ ├── manifest.rs # AgentManifest — parse mandate markdown, assemble system prompt
│ ├── policy.rs # PolicyEngine — predicate eval, gate(), constitution_hash()
│ ├── mailbox.rs # MailboxClient — read/ack agent-mailbox.jsonl
│ └── board.rs # BoardClient — read/append board.md
├── ledger/
│ ├── mod.rs # LedgerEntry, LedgerWriter, Cid
│ ├── hash.rs # BLAKE3 + JCS canonical serialization (RFC 8785)
│ └── proof.rs # Proof enum: Null | Attestation | ZkStark
├── session/
│ ├── mod.rs # SessionManager — public API
│ ├── queue.rs # KeyedQueue — per-session tokio mpsc + worker tasks
│ ├── store.rs # SessionStore — SQLite, hash-chained turns
│ └── eviction.rs # TTL loop (domain mode only)
└── server/
├── mod.rs # Axum, WebSocket upgrade, :18789
└── routes.rs # RPC handlers, event serialization, ACP wire compat
ledger/ is a standalone module. In Filix v1.0, the session store becomes a materialized view over the ledger — that refactor is trivial if LedgerWriter has a clean interface. If ledger logic were entangled with the constitutional wrapper, it would be painful.
#Crypto Readiness
| Phase | Requirement | Status |
|---|---|---|
| v0 (now) | BLAKE3 CIDs, proof: null, envelope: null | in schema from day one |
| v1.0 (Aug 2026) | Populate proof with attestation + agent signature; verify prev_cid chain | proof field in schema; prev_cid in turns; PolicyEngine evaluates predicates |
| v1.5 | KDF audience keys, populate envelope, rotate on agent departure | envelope field in schema; tags on entries track audience |
| v2.0 | ZK-STARK proof generation and verification | proof accepts any Proof variant; verifier plugs in without touching event or session types |
#First Build Target
Start with ledger/ — LedgerEntry, Cid, BLAKE3 + JCS canonical hash. Everything else builds on it: session store uses CIDs as primary keys, constitutional wrapper emits ledger entries, event stream carries them. Get the ledger module right first.
#Supersedes
Replaces the minimal spec in [[35-agent-runner]]. The Python implementation (scripts/agent_runner.py) and console agentic loop (infra/console/server.py) remain active until migration is complete.
#Related
- [[33-constitutional-ledger]] — ledger entry schema, BLAKE3/JCS spec, crypto phase roadmap
- [[36-agent-router]] — session routing and room assignment
filix-v1-plan— Filix v1.0 scope and design-for-crypto specontology-governance-situation— governance proofs and authority model
Architecture 38 — Agent Runner v2 — Constitutional Runtime over ACP — 2026 — Zachary F. Mainen / HAAK