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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user