docs: ratify vision revision across README/ARCHITECTURE/PORT_PLAN + ADRs 0002-0006

Folds the 2026-06-26 vision-revision pressure-test into the canonical
docs. The spec moves from Proposed to Ratified; the decisions it produced
land in the docs it said it would amend, with ADRs recording each
load-bearing reversal.

- README.md: reframe north star (AI-era contact-center engine, not
  Asterisk successor); persona; revised pillars (add data-ownership,
  demote WASM, promote spend-control); update 'what it is/isn't'.
- ARCHITECTURE.md: replace three-plane framing with fused per-call
  vertical + composable horizontal platform; remove control<->media
  hot-path gRPC hop; make the agent tap the central interface; add
  DX spine + GUI-as-API-client + k8s declarative/operational model.
  Also: 'too slow to police' -> 'too slow to enforce' (terminology).
- PORT_PLAN.md: recharacterize as capability checklist (not template);
  graduate contact-center capabilities to first-class domain; Rust-native
  trunk SIP rows; WASM demoted; thin-slice + capability ladder phasing.
- ADR-0001: marked Superseded by ADR-0003.
- ADR-0002 (new): north star + fused per-call core.
- ADR-0003 (new): Rust-native trunk SIP, no SBC shield.
- ADR-0004 (new): GPL-3.0-or-later license.
- ADR-0005 (new): Valkey as event bus + state store.
- ADR-0006 (new): WebRTC-first ingress; SIP endpoint deferred.
- vision-revision spec: status -> Ratified 2026-06-26.
This commit is contained in:
adlee-was-taken
2026-06-28 09:33:29 -04:00
parent 23cec14977
commit 075e984fb5
10 changed files with 674 additions and 109 deletions

View File

@@ -2,55 +2,160 @@
## The reframe
Asterisk's power was: *one process, load any `.so`, wire anything to anything in the
dialplan.* That composability is the thing to match — but it does **not** require a
1.2M-LOC monolith. Rutster delivers the same "build anything" through a different
substrate:
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** (media + signaling glue + call model),
- a **WASM plugin runtime** for safe, third-party-extensible logic,
- 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.
- 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.
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.
## Three planes
## The fused per-call vertical + composable horizontal platform
### Control plane (stateless-ish, horizontally scalable)
The ARI-style resource API (channels / bridges / endpoints / recordings / playbacks)
over REST + gRPC + a WebSocket/SSE event stream. Registrar, routing, auth. This is
where "the dialplan" disappears — replaced by declarative routing + external services
reacting to call events (the Twilio / ARI-Stasis model). Asterisk's
`rest-api/api-docs/*.json` is a reusable spec for the resource model.
The core owns the **per-call vertical** end-to-end as one deterministic, auditable trust domain:
### Media plane (stateful, latency-pinned, scaled separately)
RTP/SRTP termination, mixing/bridging (softmix), transcoding, record/playback. A
controllable media node driven over gRPC by the control plane. Built on the Rust
WebRTC media ecosystem (`str0m` sans-IO design, `webrtc-rs`). **The media datapath
stays tight** — do not over-decompose it across service hops; latency and failure
modes compound.
```
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
```
### App plane (your services + plugins, outside the core)
IVR, queues, voicemail, dialers, custom routing — driven via the API, deployed
independently. WASM plugins for in-call logic that needs to run close to the core;
microservices for stateful/business/billing logic.
**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.
## Cross-cutting
This **replaces the founding three-plane framing**:
- **Event bus** (NATS / Redis Streams / Kafka) replaces Asterisk's internal Stasis bus
for cross-service events; a lightweight in-core dispatcher handles intra-core.
- **State store** replaces `astdb` + realtime/sorcery.
- **Security is load-bearing, not a row:** memory-safe fuzzed parsers, TLS/SRTP
mandatory, deny-by-default routing + toll-fraud engine, mTLS gRPC admin (no AMI),
WASM tenant isolation, SBOM + KMS/Vault for secrets.
- **Observability:** OpenTelemetry traces that follow a single call across
signaling → media → app services.
- 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))
## Inside the boundary
- **Trunk SIP termination** — Rust-native; IP-allowlisted cooperative carriers; no C SBC.
([ADR-0003](adr/0003-sip-rust-native-trunk.md))
- **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 trunk termination** so a runaway
brain structurally cannot exceed spend or pacing — it doesn't hold the wire.
- **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
The **SIP stack****decided in [ADR-0001](adr/0001-sip-strategy.md)**: own the Rust
parser from day one (the security thesis depends on it), front the public edge with a
proven **Kamailio + rtpengine** SBC to absorb the interop tail, and grow the native Rust
transaction/dialog core behind that shield. No pjproject FFI. Everything else builds on
the existing Rust media ecosystem.
**The SIP stack****decided: Rust-native trunk SIP** ([ADR-0003](adr/0003-sip-rust-native-trunk.md)).
Own the parser from day one (the security thesis depends on it); the *trunk* interop surface is
bounded by carrier docs (a few IP-allowlisted providers), so no C SBC shield is rented. The single
biggest risk item, de-risked by maturing behind WebRTC-first ordering — first-call never blocks on SIP.
Everything else builds on the existing Rust media ecosystem; the agent **brain** is external by
necessity and reached via the tap.