What the channel layer does
Wirken routes messages from named chat surfaces to one agent that answers to you. Each channel runs in its own adapter process with its own platform credentials, and the switchboard authenticates the channel identity on every IPC frame. You decide which channels exist; the channels never decide on your behalf.
The adapters
Each adapter is an independent Cargo crate that converts platform messages to and from the same Cap'n Proto frame the switchboard expects. The set:
Adding a channel means writing one more crate against the same trait set. The switchboard does not care which channel a frame arrived on, as long as it can prove the frame arrived on the channel the connection authenticated as.
Cross-channel isolation
The IPC crate names the property the channel layer is supposed to have, and the names are types. AuthenticatedChannel is the channel a connection resolved to during the Ed25519 handshake; the gateway sets it once and treats it as the trusted value for the lifetime of the connection. SessionHandle<C: Channel> is a session reference parameterized by a sealed channel marker. The phantom type makes SessionHandle<Telegram> and SessionHandle<Discord> distinct Rust types; any function that takes one of them refuses to accept the other at compile time. Crossing channels is not a check, it is a type error.
pub trait Channel: Send + Sync + 'static {
fn id() -> &'static str;
}
pub struct AuthenticatedChannel(String);
pub struct SessionHandle<C: Channel> {
id: SessionId,
_channel: PhantomData<C>,
}
The IPC crate enforces this at the type level: any function that holds a SessionHandle<Telegram> cannot pass it to anything that expects a SessionHandle<Discord>. The gateway pairs the type-level layer with a runtime AuthenticatedChannel::require_match on every inbound frame, which fails closed and audits both sides of a mismatch, so cross-channel attempts are observable in the chain. Credentials handed to an adapter stay in that adapter.
What lands in the audit log
Every frame, decision, and call writes a typed event into the per-session hash chain before the action runs. A representative slice from one session, with hashes truncated for the page:
{
"ts": "2026-05-14T10:23:18Z",
"actor": "adapter",
"action": "inbound.message",
"channel": "telegram",
"session": { "full": "assistant/telegram/12af9c" },
"detail": { "sender_id": "tg:user:74821", "message_hash": "8b1a..." },
"hash": "3f72..."
}
{
"ts": "2026-05-14T10:23:21Z",
"actor": "gateway.permissions",
"action": "permission.decision",
"session": { "full": "assistant/telegram/12af9c" },
"detail": {
"action_variant": "ShellExec",
"tier": "tier2",
"decision": "allow",
"remembered": true
},
"hash": "a04e..."
}
{
"ts": "2026-05-14T10:23:22Z",
"actor": "agent.llm",
"action": "llm.request",
"session": { "full": "assistant/telegram/12af9c" },
"detail": {
"provider": "anthropic",
"model": "claude-sonnet-4-6",
"messages_hash": "c1d2...",
"tools_hash": "5e9f..."
},
"hash": "917b..."
}
{
"ts": "2026-05-14T10:23:24Z",
"actor": "adapter",
"action": "outbound.delivered",
"channel": "telegram",
"session": { "full": "assistant/telegram/12af9c" },
"detail": { "recipient_id": "tg:user:74821", "result_hash": "2c30..." },
"hash": "e6a1..."
}
Every event to the left of a decision is pinned by the hashes to its right, so changing any single event changes every chain hash that follows it. You can prove the order these rows ran in, and prove nothing has been removed or rewritten between them, without trusting wirken to tell you so.
SIEM forwarding
The same typed events feed a SIEM forwarder when one is configured. Datadog, Splunk HEC, Microsoft Sentinel, and any HTTPS webhook are first-class targets. Events ship after they land in the chain, so the SIEM stream and your local audit log are independent views of the same record.
If you want structured ingestion, choose typed events. If you already have a normalizer pointed at a generic webhook, keep using it.
Verify a session offline
The chain is replay-verifiable without the live switchboard. wirken session verify reads the per-session rows, recomputes message hashes, re-runs deterministic tool calls against their recorded inputs, and reports any divergence.
$ wirken session verify assistant/telegram/12af9c
wirken session verify assistant/telegram/12af9c
──────────────────────
agent: assistant
Note: deterministic tools (read_file, list_files) are re-executed against the
CURRENT workspace, not the workspace state at the time of execution.
events_total: 1247
events_verified: 1245
events_unverifiable: 2
events_divergent: 0
chain: OK (1247 rows)
Chain-head Ed25519 signatures are checked at audit-wide scope with wirken audit verify, which walks every per-session chain in the database and verifies the signature on each head against the agent's published key. An intact chain plus a verified head means the session ran in the order recorded, and the head was attested by the agent that produced it.