# 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](adr/0002-north-star-and-fused-core.md)) ## 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](adr/0008-fob-and-green-zone.md). - **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](adr/0007-trunk-rented-transport.md)). 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 (`str0m` sans-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](adr/0009-spend-gate-honest-rescope.md) — 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 (`AudioOut` frames, 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. **Make `AudioOut` advisory / 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](adr/0006-ingress-posture.md). 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](adr/0005-event-bus.md)): 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.conf` and 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](adr/0007-trunk-rented-transport.md)). 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.