Maintainer-ratified from the 2026-07-03 review (R1-R3): - ADR-0009: retire "structurally impossible for a 3-vendor stack" post-0007; restate the gate's true guarantees (credential isolation, unskippable mediation, media-plane enforcement, audit co-location). Propagated to README pillar 3, ARCHITECTURE, PORT_PLAN s10; amendment note on ADR-0002. - ADR-0010: insert step 4.5 (benchmark + simulation harness, rutster-sim seed, CI-regressed p50/p99 + kill-time) after barge-in; pull rung-2 escalation ahead of steps 5-6. Spearhead lists updated. - ADR-0004: delete the legally-broken AGPL escape hatch; GPL-3.0-or-later permanent, no CLA; tap protocol/SDKs intended permissive (future ADR). - README: add brain-vendor-direct competitor row (review D2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018vNe64BBDkgo5oQVkBa3XF
12 KiB
Rutster Architecture
The reframe
Asterisk's power was: one process, load any .so, wire anything to anything in the dialplan.
rutster delivers the same "build anything" through a different substrate — but the engine is
the goal, not the PBX. Where Asterisk matched composability through a 1.2M-LOC monolith + in-process
modules, rutster matches it through:
- a small hardened core owning the per-call vertical end-to-end (media + signaling + call model
- reflexes + spend gate) as one deterministic, auditable trust domain,
- a clean audio tap as the external brain's extension point (safely extensible by people you don't fully trust, because the brain is out-of-process and the core authoritatively controls playout),
- declarative routing as data for the common path,
- a programmable API (REST/gRPC + event stream) modeled on Asterisk's ARI — and that ARI lineage is where the dialplan goes: external services reacting to call events.
More extensible than Asterisk, because extensions are safe to run from people you don't fully trust
— and they can be AI brains, not just .so files.
The fused per-call vertical + composable horizontal platform
The core owns the per-call vertical end-to-end as one deterministic, auditable trust domain:
carrier SIP trunk ─► media termination (RTP/SRTP + local real-time reflexes)
│
├─► clean audio tap ──► external agent brain (STT/LLM/TTS)
│
└─► in-boundary spend / abuse gate
Horizontal platform concerns are services around the core, independently scaled: number inventory, billing rollup, analytics, multi-region orchestration, the management API, and the agent brain itself.
This replaces the founding three-plane framing:
- The control↔media gRPC hop on the per-call hot path is removed. Fusion where fusion buys determinism + security + simplicity.
- The spend gate and the agent tap — which founding docs externalized — are pulled into the boundary, because they are constitutive of the wedge (a runtime can't structurally enforce spend/abuse control or barge-in if the media is elsewhere).
- Neither monolith nor pure microservices: fused where fusion buys the wedge; composable where independent scaling matters. (ADR-0002)
The FOB and the green zone — the build-vs-reuse doctrine
The rule that decides what lives inside that boundary: stop building a fortress; build a secure base of operations. Harden and own the core — the FOB (forward operating base); operate through a trusted, actively-maintained OSS perimeter — the green zone. Leverage, not autarky — the antidote to the not-invented-here death-march (a from-scratch SIP stack was exactly that). The full doctrine and the mechanical test an agent applies live in ADR-0008.
- FOB — memory-safe Rust, owned, fuzzed, inside the trust boundary; a capability is admitted only
if it is hot-path, security-constitutive, or genuinely differentiating: media termination, the
real-time reflexes, the call model, the agent tap, the spend/abuse gate, the control API + the
state/bus trait. (It may link mature OSS for internals — libopus,
str0m— as trusted vendored deps; the capability is still owned, the wire parser still Rust.) - Green zone — trusted, actively-maintained OSS/services reused at arm's length (own process / container / trust domain), admitted when they fail all three FOB tests: Valkey (state+bus), the trunk transport (CPaaS / out-of-tree SBC), the agent brain, the reference GUI, object storage, KMS, the supervisor.
This restates the memory-safety pillar precisely: the FOB is 100% memory-safe Rust; the green zone is trusted, battle-tested OSS kept outside the boundary — not an over-claim that every byte is Rust.
Inside the boundary
- Carrier trunk — rented transport, not first-party (ADR-0007). rutster owns no SIP stack: PSTN audio arrives as a media-leg ingress from a rented CPaaS raw-media fork (primary), or from an out-of-tree SBC when media must stay on-prem (sovereignty graduation). Inside the boundary it's clean RTP/SRTP like any other leg.
- Media plane — RTP/SRTP termination, mixing/bridging (softmix), transcoding, record/playback.
Built on the Rust WebRTC ecosystem (
str0msans-IO design,webrtc-rs). The media datapath stays tight — do not over-decompose it across hops or into the bus; latency and failure modes compound. Dedicated timing threads for the 20ms loop, never the shared tokio pool. - Local real-time reflexes — VAD-driven barge-in / playout kill, half-duplex gating, jitter buffer, pacing. These live in-core because the brain round-trip is too slow to enforce them. The tap carries the results of reflexes to the brain, not the responsibility.
- Call model — the unifying
Channel/leg object (signaling + media state); call-control primitives (transfer/park/pickup, answer/originate); the ARI-modeled resource API surfaced as REST/gRPC + a WebSocket/SSE event stream. "The dialplan" disappears — replaced by declarative routing + external services reacting to call events (the ARI/Stasis model). - In-boundary spend / abuse gate — spend caps, pacing caps, deny-by-default routing, rate-limits, toll-fraud pattern detection. Co-located with the tap + the provider call-control client: the brain never holds provider credentials, so every brain-initiated action passes the gate; pacing/half-duplex/playout are enforced over media the core terminates (ADR-0009 — pair with provider-side caps for defense in depth).
- Audiohook primitive — tap/inject/volume behind recording, ChanSpy/whisper/barge, and human-agent escalation (rung 2: an agent takes over a call via WebRTC + audiohook handoff).
Agent tap (the central interface)
The single most important interface in the system — where audio leaves the boundary to an external brain. Presumptive shape (to harden against the thin-slice first proof, not a decided ADR yet):
- Bidirectional WSS stream per call, small versioned typed framing protocol. WSS (not gRPC) because the consumer is a Python script / a browser / an OpenAI-Realtime-style speech-to-speech API for which event-framed WSS is already the de-facto protocol — making the OpenAI adapter a thin shim, not a gRPC-bridge project.
- Core-as-client; brain-as-server. Always. The audited boundary dials out to the brain (to OpenAI, to a self-hosted brain process on localhost). No inbound tap port on the core. One design choice deletes a whole attack class ("something connected to my tap port and read audio"), simplifies firewall posture, and gives clean symmetry — every connection is initiated by the thing you trust.
- One canonical PCM format at the tap — 16-bit, mono, a speech-model-friendly rate (24kHz default, 16kHz fallback). The core terminates the codec soup on the PSTN side and exposes one clean format. Brains never touch codecs.
- Core-authoritative playout. The brain proposes audio (
AudioOutframes, advisory); the core disposes — owning the playout buffer + the VAD that kills TTS on caller speech + the half-duplex/pacing caps. A brain that can push audio straight to the PSTN wire is a brain that can overlap, flood, or exfiltrate-via-timing. MakeAudioOutadvisory / core-authoritative.
Tap ≠ ingress — opposite security postures; never unified. The tap is egress, core-as-client, no inbound port. Human ingress is inbound, core-as-server, with SSO + RBAC + per-tenant scoping — legitimately a server surface, fine because auth'd. The "core-as-client" move is specifically about egress, where the dangerous direction is. See ADR-0006.
For a brain that wants to be a full media peer (an existing SIP IVR, another media server, a WebRTC client), the path is media-leg ingress (a real participant that speaks RTP/SRTP itself), not the tap. Forcing a clean-audio brain into a media re-termination re-introduces a second media termination, contradicting "terminate media once."
Cross-cutting (outside the per-call boundary)
- Event bus — Valkey (ADR-0005): streams + consumer groups for
CDR/CEL/analytics fanout; pub/sub for presence/MWI/BLF; KV for the state store (replacing
astdb- sorcery). The 20ms media loop never rides the bus. The bus is not the source of truth for billing- or call-loss-critical state — CDR/recordings emit durably to object storage; the bus only flows events into that pipeline and lets services react. Pluggable to NATS later at a config seam.
- State store — Valkey KV, replacing
astdb+ realtime/sorcery. - Observability — OpenTelemetry traces that follow a single call across the boundary and out to services.
- Secrets / KMS — Vault/KMS; no plaintext credentials in config.
- Supply chain —
cargo-deny, SBOM generation, reproducible builds; continuous fuzzing of every wire parser (SIP/SDP/RTP).
DX spine — developer-first authoring
Headless and API-complete. The persona authors via code + config-as-text: git-versioned, CLI-driven, IDE-native, AI-assistant-friendly (typed, schema'd, LSP-friendly). Terraform/Rails for call centers, not Squarespace.
- The AEL lesson (DCAP author's): better isn't enough. AEL was superior to
extensions.confand still lost — it arrived after the muscle memory had set. An authoring layer must win on contact and meet people where their muscle memory is. In 2026 that's code + an AI pair, not a config syntax or a visual canvas. - "Boom" + swiss-army-knife, reconciled: an opinionated batteries-included reference distro
(
compose up, point a trunk at it, start taking calls) on top of a composable framework (build anything in code). Home Assistant model, not raw dialplan. - Call-flow authoring is a first-class design surface. The AI-era twist: part of the old dialplan dissolves into the agent (the model improvises the conversation). The authoring layer becomes the routing, escalation, and business scaffolding around an AI that writes half the flow at runtime — genuinely new design space, not "dialplan 2.0."
GUI & extension architecture
The GUI is a pure API client, never an insider — FreePBX↔Asterisk, modernized.
- Not a plugin in the core. A web GUI must not share an address space / lifecycle / attack surface with the latency-pinned media engine inside the one secure boundary.
- A separate application. The official reference GUI ships in the batteries-included distro (so "boom" includes a usable UI) but holds no privilege a third party couldn't get. The ecosystem can build rival GUIs.
- Discipline: the official GUI is built only on the public API — no backdoors. That guarantees API completeness and prevents FreePBX's leaky "don't hand-edit config or it'll clobber you" coupling.
- Config-as-code vs. GUI-mutation tension → the Kubernetes model. Declarative desired-state (config-as-code, git, reconciled by the engine) plus an API/CLI/GUI for live operational state and ad-hoc actions, all through one API, single source of truth. The GUI is the dashboard, not a side-channel. Manifests + kubectl + dashboard, for call centers. Bonus scope guard: the GUI can only surface what the API already does, so it can't drag the engine toward Five9-parity.
Biggest technical risk
No longer the SIP stack — because rutster no longer builds one (ADR-0007). The trunk is rented transport (a CPaaS raw-media fork) or an out-of-tree SBC; rutster parses no SIP. That retires what ADR-0003 called the single biggest risk item and redirects the solo-years to the white space (the contact-center domain + the reflex/eval loop + the tap-as-open-protocol).
The risk that remains is the reflex loop itself — turn-taking, VAD-driven barge-in, jitter and
pacing in a no-GC real-time loop (spearhead steps 1–4). That is also the most-differentiating work,
which is the point. Everything else builds on the existing Rust media ecosystem (str0m); the agent
brain is external by necessity and reached via the tap.