diff --git a/README.md b/README.md index cdf8547..1fdce9b 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,123 @@ # Rutster -A memory-safe, API-first, security-first telephony platform — a spiritual successor -to Asterisk for the WebRTC/microservices era, written in Rust. +The open-source engine for building the **AI-era contact center** — self-hostable, AI-native, +memory-safe Rust. A spiritual successor to Asterisk's *place in the world*, not its protocols +or its architecture. -> Not a port of Asterisk. A reimagining that keeps the *power* — era-bridging breadth, -> "wire anything to anything," a deep feature library — while dropping the legacy -> hardware/dead-protocol half and fixing Asterisk's two chronic wounds: -> memory-safety CVEs and toll fraud, at the design level. +> Not a port of Asterisk. rutster inherits the role Asterisk held — *the self-hostable engine a +> technical builder uses to stand up a contact center* — and re-aims it at a category AI is +> actively disrupting, instead of a PBX category UCaaS already ate. + +## Why it exists + +Asterisk won because contact centers were **built on it** (Vicidial, GOautodial, a thousand +integrator builds) — it never tried to *be* Five9. rutster inherits that position: it is a +**framework / engine, not a turnkey product**. + +The white space no incumbent fills: + +| Competitor | What they are | rutster's edge | +|---|---|---| +| **LiveKit** | Horizontal real-time media infra (Go) | rutster owns the **contact-center domain** (ACD, IVR, queues, recording, CDR, dialer, supervisor) LiveKit will never ship | +| **Cloud CCaaS** (Five9, Genesys, NICE, Amazon Connect, Twilio Flex) | Proprietary, AI bolted on | Self-hostable, AI-**native**, no per-seat/minute lock-in | +| **Cloud AI-voice** (Vapi, Retell, Bland) | Cheap managed voice bots | You own your **calls and training data**; it's a *contact center* (escalation, queues), not a single bot | +| **Dated OSS** (Vicidial, FreePBX) | Self-hostable, Asterisk-era | Modern, AI-native, memory-safe Rust | + +The wedge is a **coherent combination, not a silver bullet**: + +1. **No-GC real-time determinism** — tight turn-taking / barge-in / jitter in a no-GC loop. +2. **One secure auditable boundary** — trunk termination + media + spend/abuse control + audit in + a single memory-safe trust domain. One thing to certify (strongest for PCI / HIPAA / TCPA). +3. **Operational simplicity** — one binary, one bill, one deploy. + +**Honest caveat:** the agent *brain* (STT/LLM/TTS) is necessarily external — audio leaves the box +to reach it, same as LiveKit. The real-time edge therefore lives in the **local reflexes that don't +need the brain** (VAD killing TTS the instant the caller speaks, barge-in, jitter, pacing, DTMF), +not the brain round-trip. The moat is the *whole*. + +## Who it's for + +The modern equivalent of the 2006 Linux-nerd-who-stood-up-Asterisk-for-an-SMB: the +**CLI/IDE/AI-comfortable self-hosting technical builder** — runs Claude Code in a terminal, lives +in an editor with an AI pair, versions everything in git, self-hosts on principle. *Not* the no-code +admin clicking a flow-designer canvas. The builder serves the non-technical operator downstream, +exactly as integrators did on top of Asterisk. ## What it is / isn't -- **Is:** SIP (TLS-only) + WebRTC signaling, a real-time media plane (transcode / mix / - DSP), a programmable call model exposed as a REST/gRPC API + event stream, a - WASM-sandboxed plugin runtime, and a library of telephony features delivered as - isolated services. -- **Isn't:** A TDM/PSTN-hardware PBX. No DAHDI, no Sangoma/Digium cards, no ISDN/SS7, - no IAX2/H.323/SCCP/MGCP/Unistim. PSTN reach is via SIP trunks / SBC only. +- **Is:** a Rust media core owning the **per-call vertical** (trunk SIP termination + RTP/SRTP media + + local real-time reflexes + a clean audio tap to an external brain + in-boundary spend/abuse + control); a programmable call model exposed as a REST/gRPC API + event stream; WebRTC-first human + ingress; a library of contact-center capabilities (ACD, IVR, queues, recording, CDR, dialer, + supervisor) delivered as services around the core. +- **Isn't:** a TDM/PSTN-hardware PBX. No DAHDI, no Sangoma/Digium cards, no ISDN/SS7, no + IAX2/H.323/SCCP/MGCP/Unistim. PSTN reach is via SIP trunks only. Inbound SIP *endpoint* + registration (desk phones) is deferred to an out-of-tree edge adapter — not the browser/SSO UX + rutster targets. ## Core design pillars -1. **Memory-safe by construction** — Rust everywhere on the hot path; fuzzed, sans-IO - protocol parsers. Eliminates the buffer-overflow/RCE CVE class. -2. **Isolation over in-process modules** — extensions run in a **WASM sandbox**, not as - `.so` files in the daemon's address space. One bad plugin can't crash the box or - read another tenant's media. (The Asterisk `core show locks` era ends here.) -3. **Secure & deny-by-default** — mandatory TLS/SRTP, deny-by-default routing, - built-in toll-fraud controls, mTLS gRPC admin (no plaintext AMI), hard multi-tenancy. -4. **Three planes, scaled independently** — control / media / app. The media datapath - stays tight; business logic is microservices and plugins around it. -5. **Observable & declarative** — OpenTelemetry call traces across services; - config-as-data, not imperative `.conf` files edited on a box. +1. **Memory-safe by construction** — Rust everywhere on the hot path; fuzzed sans-IO protocol + parsers. Hostile bytes hit a fuzzed Rust parser first. Eliminates the buffer-overflow/RCE CVE + class — and, with the C SBC edge dropped, this is *literally true at the wire*. +2. **Security-as-product** — the single auditable boundary *is* the moat. TLS/SRTP mandatory, + deny-by-default routing, built-in toll-fraud controls, mTLS gRPC admin (no plaintext AMI), hard + multi-tenancy. Compliance is a buying criterion, not a row. +3. **In-boundary spend / abuse control** — spend caps and abuse/pacing control live **inside the + trust boundary**, co-located with trunk termination. A runaway brain can't exceed pacing or + spend because it doesn't hold the wire — structurally impossible for a 3-vendor stack. +4. **Data ownership** — calls and training data never leave the operator's infra. The self-host + wedge *and* the fuel for the ML self-improvement loop. +5. **Degradation, deterministic, observable** — no-GC real-time loop; OpenTelemetry traces that + follow a single call across the boundary; config-as-data, not `.conf` files edited on a box. +6. **Operational simplicity** — one binary, one bus, one deploy (`compose up`). ## Layout -- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — the three-plane model and pillars. -- [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md) — **the spine**: every Asterisk subsystem mapped - to a disposition (core / plugin / service / edge-FFI / dropped) with rationale. +- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — the fused per-call vertical + composable + horizontal platform; the agent tap as the central interface. +- [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md) — the capability *checklist* (what a telephony system + must handle), *not* an architecture template. Every Asterisk subsystem mapped to a disposition + with rationale. +- [`docs/adr/`](docs/adr/) — decisions. Highlights: + - [0002](docs/adr/0002-north-star-and-fused-core.md) — north star + fused vertical + - [0003](docs/adr/0003-sip-rust-native-trunk.md) — Rust-native trunk SIP (no SBC shield) + - [0004](docs/adr/0004-license.md) — GPL-3.0-or-later + - [0005](docs/adr/0005-event-bus.md) — Valkey (bus + state store) + - [0006](docs/adr/0006-ingress-posture.md) — WebRTC-first ingress; SIP endpoint deferred ## Status -Scoping. No code yet — the port plan defines the crate/service boundaries that the -workspace will be scaffolded from next. +Scoping. No code yet — the [vision revision](docs/superpowers/specs/2026-06-26-vision-revision-design.md) +and ADRs define the architecture; the workspace will be scaffolded from the capability ladder +and the thin-slice first proof. -## Open decisions +## First proof (the spearhead) -- **License** — undecided (Asterisk is GPLv2+commercial; rutster's model is TBD). -- **SIP stack** — FFI to pjproject vs. front with an SBC (Kamailio/drachtio) vs. a - long-term pure-Rust stack. See port plan §1. -- **WASM runtime** — `wasmtime` is the presumptive default (component model). +The full thin slice, sequenced so each step is its own proof — never a big bang: + +1. **WebRTC media loopback** (terminate RTP/SRTP, echo audio to a browser) — proves the media core +2. **Add the tap** (route audio to an external echo process and back) — proves the tap interface +3. **Swap echo for the brain** (ideally a single speech-to-speech API, e.g. OpenAI Realtime, to + collapse STT+LLM+TTS into one integration) — proves agent integration +4. **Add barge-in** (VAD-driven playout kill) — proves the reflex +5. **Replace WebRTC ingress with a real PSTN trunk call** — proves the trunk client +6. **Add the spend cap** (hard-stop at threshold) — proves the boundary + +Steps 1–4 *are* the reflex loop — the hard, most-differentiating part proves itself before trunk +integration piles on. "I called my Rust box and an AI answered the phone" is the momentum fuel a +solo multi-year build needs. + +## Capability ladder (the grand vision, incrementally) + +| Rung | Capability | Reuses | +|---|---|---| +| 1 | **Self-serve** — AI answers, contains the call | the thin-slice first proof | +| 2 | **Escalation** — human agent barges in / takes over when AI breaks down | the audiohook/barge primitive | +| 3 | **Measurement** — containment rate, where/why AI failed | CDR + analytics on calls *you own* | +| 4 | **Self-improvement** — every takeover → auto-labeled training data → loop | rungs 1–3 compounding | + +## License + +**GPL-3.0-or-later** ([ADR-0004](docs/adr/0004-license.md)). Strong copyleft in the Asterisk lineage, +modernized one notch. The license is the *floor*, not the moat — the wedge is. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d18d62b..d17e102 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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. diff --git a/docs/PORT_PLAN.md b/docs/PORT_PLAN.md index 5affe2f..0c96127 100644 --- a/docs/PORT_PLAN.md +++ b/docs/PORT_PLAN.md @@ -1,9 +1,12 @@ -# Rutster — Asterisk → Rust Capability Port Plan +# Rutster — Asterisk Capability Checklist -This is the spine of the project. It walks the Asterisk subsystem surface and assigns -every capability a **disposition** in the rutster architecture, with a one-line -rationale. The crate/service boundaries fall out of the "Core / Service" rows; the -threat model hangs off the "attack surface" of each row. +This is the **capability checklist**, not the architecture spine. It walks the Asterisk +subsystem surface as a completeness audit — *"what must a telephony system handle"* — and +assigns every capability a **disposition** in the rutster architecture, with a one-line +rationale. We keep the checklist; we **reject** the Asterisk channel/bridge/dialplan model +as the architecture (see [ADR-0002](adr/0002-north-star-and-fused-core.md)). The crate/service +boundaries fall out of the "Core / Service" rows; the threat model hangs off the "attack +surface" of each row. Grounded against the Asterisk 22.10.1 tree (~1.18M LOC C/H). @@ -20,12 +23,19 @@ Grounded against the Asterisk 22.10.1 tree (~1.18M LOC C/H). ## Design rules (the heuristics behind every row) -1. Hot media path & the call model → **Core**. Trusted, fast, always-on. -2. Anything that benefits from isolation or third-party extension → **Plugin**. -3. Stateful, independently-scaled, or business/billing logic → **Service**. -4. Mature C that's a tar pit to rewrite (SIP) → **Edge/FFI**, with a pure-Rust ambition. +1. Hot media path & the call model → **Core**. Trusted, fast, always-on. Inside the fused + per-call vertical. +2. Anything that benefits from isolation or third-party extension → **Plugin** *(now softened — + WASM demoted out of the core story per [ADR-0002](adr/0002-north-star-and-fused-core.md); the + agent **tap** is the primary extension point for in-call logic).* +3. Stateful, independently-scaled, or business/billing logic → **Service**. Around the core, + horizontal platform. +4. ~~Mature C that's a tar pit to rewrite (SIP) → **Edge/FFI**, with a pure-Rust ambition.~~ + *Reversed for SIP by [ADR-0003](adr/0003-sip-rust-native-trunk.md): Rust-native trunk SIP, no + C SBC shield. FFI retained only for mature codec/DSP/EQ libraries (libopus, speexdsp, WebRTC APM).* 5. Legacy hardware / dead protocols → **Drop**. -6. Security & multi-tenancy are cross-cutting — never a row, always a property. +6. Security & multi-tenancy are cross-cutting — never a row, always a property. **Spend / abuse + control is constitutive**, pulled into the boundary, not externalized. --- @@ -33,14 +43,15 @@ Grounded against the Asterisk 22.10.1 tree (~1.18M LOC C/H). | Asterisk subsystem | Module(s) | Disp. | Rationale | |---|---|---|---| -| SIP signaling | `chan_pjsip` + `res_pjsip*` (48 mods) | 🔌 Edge → 🦀 Core | **Decided (ADR-0001):** public SIP edge = **Kamailio + rtpengine** (proven SBC shield); native **Rust** parser + transaction/dialog core (`rsip`/`ezk-sip`) grows behind it. **No pjproject FFI** — it would put C at the most exposed seam and break the memory-safety thesis. The single biggest risk item. | -| ↳ registration (in/out) | `res_pjsip_registrar`, `_outbound_registration` | 🦀 Core | Registrar state is core control-plane; back it with the state store. | +| SIP signaling (trunk) | `chan_pjsip` + `res_pjsip*` (48 mods) | 🦀 Core | **Decided (ADR-0003):** **Rust-native trunk SIP**, no C SBC shield. `rsip` + `ezk` sans-IO transaction/dialog core. The *trunk* interop surface is bounded by carrier docs (Telnyx/Bandwidth/Twilio SIP, IP-allowlisted) — the device interop tail ADR-0001 rented a shield for *collapses* under the contact-center scope. Hostile bytes hit a fuzzed Rust parser first → memory-safety thesis literally true at the wire. | +| ↳ outbound registration (to carriers) | `res_pjsip_outbound_registration` | 🦀 Core | Trunk side; bounded register-to-carrier. State in the state store (Valkey, [ADR-0005](adr/0005-event-bus.md)). | +| ↳ **inbound endpoint registration (desk/soft phones)** | `res_pjsip_registrar` | ⛔ **Deferred — community edge** | **Split from trunk.** An inbound endpoint server re-imports the *unbounded device interop tail* ADR-0003 deliberately closed (Polycom/Yealink/Zoiper `REGISTER`, BLF/MWI, DTMF variants, per-device NAT/provisioning). In an SSO/browser-agent UX, the desk-phone demographic has collapsed. Out-of-tree SBC-style adapter (fronting the public API / B2B-SIP) for anyone who needs it — **outside the trust boundary**, so its tail can't compromise the wedge. See [ADR-0006](adr/0006-ingress-posture.md). | | ↳ SDP / media negotiation | `res_pjsip_session`, `_sdp_rtp` | 🦀 Core | Offer/answer drives the media plane directly. | | ↳ auth | `res_pjsip_outbound_authenticator_digest`, etc. | 🦀 Core | Digest + token auth, deny-by-default, rate-limited. Security-critical. | | ↳ NAT / ICE / rtcp | `res_pjsip_nat`, `res_rtp_asterisk` ICE | 🦀 Core | ICE/STUN/TURN via `str0m`/`webrtc-rs`. | | ↳ pub/sub (presence, MWI, BLF) | `res_pjsip_pubsub`, `_exten_state`, `_mwi` | 🦀 Core + ☁️ | Signaling in core; presence/MWI state aggregation as a service. | | ↳ T.38 fax | `res_pjsip_t38` | ☁️ Service | Isolate fax entirely (see §5). | -| WebRTC signaling | `chan_websocket`, `res_http_websocket` | 🦀 Core | First-class ingress. WSS + DTLS-SRTP via the media stack. | +| WebRTC signaling (agent ingress) | `chan_websocket`, `res_http_websocket` | 🦀 Core | **First-party human-participant ingress** ([ADR-0006](adr/0006-ingress-posture.md)). The browser is the softphone: SSO in, one peer-connection, DTLS-SRTP + ICE handled by the browser, zero device-provisioning tail. Also the modern hosted UX and the escalation path (rung 2): agent clicks *take this call* → WebRTC leg + audiohook/barge handoff. WSS + DTLS-SRTP via the media stack. | | External media | `chan_audiosocket` | 🦀 Core | Modern, relevant pattern (stream media to an external process); keep as a media tap/source API. | | Local console audio | `chan_console` | 🧩 Plugin | Dev/testing convenience; not core. | | RTP-only pseudo-channel | `chan_rtp`, `chan_bridge_media` | 🦀 Core | Media-plane primitives. | @@ -83,7 +94,7 @@ Grounded against the Asterisk 22.10.1 tree (~1.18M LOC C/H). | Internal message bus (Stasis) | `main/stasis*.c` | 🔁 Replace | In-core dispatcher for intra-core; external event bus (NATS/Kafka) for cross-service. | | Module loader | `main/loader.c` | 🔁 Replace | → WASM host + service registry. No in-process `.so`. | | ACL / named ACL | `main/acl.c`, `res_named_acl` | 🦀 Core | Security primitive; policy-driven. | -| astdb (internal KV) | `main/db.c` | 🔁 Replace | → state store (embedded KV or external). | +| astdb (internal KV) | `main/db.c` | 🔁 Replace | → state store (**Valkey** KV, [ADR-0005](adr/0005-event-bus.md)). | --- @@ -105,23 +116,30 @@ Grounded against the Asterisk 22.10.1 tree (~1.18M LOC C/H). --- -## 5. Applications (`apps/`) +## 5. Applications (`apps/`) — the contact-center domain + +> **Scope lift** ([vision-revision](superpowers/specs/2026-06-26-vision-revision-design.md) §4): the +> contact-center capabilities Asterisk kept as apps are **rutster's core domain**, not peripheral. +> This is the difference between "Asterisk successor" and "AI-era contact-center engine." Every row +> here is load-bearing for the wedge (the thing LiveKit will never ship). | Asterisk app | Module | Disp. | Rationale | |---|---|---|---| | Dial / originate | `app_dial`, `app_originate` | 🦀 Core | Core call origination, exposed via the API. | -| Queues / ACD | `app_queue` | ☁️ Service | Stateful, independently scaled, business logic. | +| Queues / ACD | `app_queue` | ☁️ Service (**domain core**) | Stateful, independently scaled. **First-class** — ACD is the beating heart of a contact center, not a peripheral app. The AI self-serve + human-escalation loop (capability ladder) is an ACD-shaped problem. | | Voicemail | `app_voicemail` | ☁️ Service | Stateful storage + business logic; isolate it. | | Conferencing | `app_confbridge` | 🦀 Core + ☁️ | Mixing in core (softmix); orchestration/features as a service. | | MeetMe | `app_meetme` | ⛔ Drop | DAHDI-timing legacy; ConfBridge supersedes. | -| Recording | `app_mixmonitor`, `app_monitor` | 🦀 Core + ☁️ | Audiohook tap in core; storage/lifecycle as a recording service. | -| ChanSpy / whisper / barge | `app_chanspy` | 🦀 Core | Audiohook primitive; gated by RBAC. | +| Recording | `app_mixmonitor`, `app_monitor` | 🦀 Core + ☁️ | Audiohook tap in core; storage/lifecycle as a recording service (object-storage-backed, retention policy, tenant-scoped — **durable, not on the bus**). | +| ChanSpy / whisper / barge | `app_chanspy` | 🦀 Core | Audiohook primitive; gated by RBAC. **The escalation primitive** (rung 2). | | Answering-machine detect | `app_amd` | 🦀 Core + 🧩 | DSP primitive in core; policy/thresholds as a plugin. | -| IVR primitives (playback/read/say) | `app_playback`, `app_read`, `app_sayunixtime`, `app_directory` | 🦀 Core | Media primitives (play/collect-digits/say) exposed to WASM apps. | +| IVR primitives (playback/read/say) | `app_playback`, `app_read`, `app_sayunixtime`, `app_directory` | 🦀 Core | Media primitives (play/collect-digits/say) exposed via the API/tap. The IVR *flow* dissolves into the agent; these are the primitives left over. | | Echo / test | `app_echo`, `app_milliwatt` | 🦀 Core | Trivial diagnostics. | -| FollowMe | `app_followme` | 🧩 Plugin | Routing policy → WASM/service. | +| FollowMe | `app_followme` | 🧩 Plugin | Routing policy → plugin/service. | | Speech recognition | `app_speech_utils`, `res_speech` | ☁️ Service | Modern ASR is external; expose as a service integration. | | Fax | `res_fax`, `res_fax_spandsp` (T.38) | ☁️ Service (FFI) | Niche, isolate fully; spandsp via FFI. Optional. | +| **Supervisor / agent-state** | (scattered manager/AMI surfaces) | ☁️ Service (**domain core**) | **First-class.** Agent presence, state, real-time monitoring, coaching/whisper — the contact-center operator surface. Pure API client (the dashboard). | +| **Dialer (predictive/progressive)** | (custom/`app_dial`-derived) | ☁️ Service (**domain core**) | **First-class.** Outbound campaign dialing with pacing — where the in-boundary spend/abuse gate (§10) *bites hardest*; structurally co-located. | --- @@ -184,28 +202,47 @@ These are the "modern" deltas — load-bearing, not optional polish. | Subsystem | Disp. | Purpose | |---|---|---| -| WASM plugin host | 🦀 Core | Sandboxed runtime (`wasmtime`) for extensions. The isolation boundary. | +| WASM plugin host | 🧩 **Demoted (candidate)** | **Out of the core story** ([ADR-0002](adr/0002-north-star-and-fused-core.md)). The agent **tap** is the extension point for in-call logic; ops-simplicity wants one binary. Softly retained as a *candidate* mechanism for community call-flow/routing plugins — undecided vs. webhooks/scripting. | | Multi-tenancy / isolation | 🦀 Core | Per-tenant keys, quotas, media separation, RBAC. | -| Toll-fraud / anomaly engine | ☁️ Service | Deny-by-default routing, spend caps, rate limits, pattern detection. The #1 real-world Asterisk wound. | +| Toll-fraud / spend / abuse engine | 🦀 Core (**promoted**) | **Constitutive, not a service** ([ADR-0002](adr/0002-north-star-and-fused-core.md)). Spend caps, pacing caps, deny-by-default routing, rate limits, pattern detection — **co-located with trunk termination inside the boundary** so a runaway brain structurally can't exceed spend/pacing (it doesn't hold the wire). This is what makes the boundary *auditable* on spend. | | Secrets / KMS | 🦀 Core | Vault/KMS integration; no plaintext credentials in config. | | Observability (OTel) | 🦀 Core | Distributed traces that follow a single call across services. | -| Event bus | ☁️ Service | NATS/Kafka/Redis Streams; replaces cross-service Stasis. | +| Event bus | ☁️ Service | **Valkey** ([ADR-0005](adr/0005-event-bus.md)): streams + consumer groups (CDR/CEL/analytics), pub/sub (presence/MWI/BLF), KV (state store). Pluggable to NATS later at a config seam. The 20ms media loop never rides the bus. | | SBOM / supply chain | — | cargo-deny, SBOM generation, reproducible builds. | | Fuzzing harness | — | Continuous fuzzing of every wire parser (SIP/SDP/RTP). | --- -## Phasing (maps to the roadmap) +## Phasing → the thin-slice first proof + capability ladder -- **Phase 0 — Media core.** RTP/SRTP endpoint, G.711+Opus, 2-party bridge, record+playback. (§2, §7). The riskiest/most valuable proof. -- **Phase 1 — Control plane + API.** ARI-modeled REST/gRPC + event stream, auth, event bus, multi-tenancy. (§4, §9, §10). -- **Phase 2 — Signaling.** WebRTC ingress first, then SIP via FFI/SBC. (§1). -- **Phase 3 — Conferencing + DSP + IVR primitives.** Softmix, transcoding matrix, DTMF/IVR. (§2, §5). -- **Phase 4 — Feature services.** Registrar, presence/MWI, queues, voicemail, recording, CDR, STIR/SHAKEN — as services/plugins. (§5, §6). +The old Phase 0–4 capability-rolldown is **replaced** (see [vision-revision §10–11](superpowers/specs/2026-06-26-vision-revision-design.md)) by a **brutally thin vertical slice** that proves the *combination* (the wedge) first, then a **capability ladder** that grows the contact center feature by feature. The spearhead proves the hardest, most-differentiating part before trunk integration piles on. + +### The thin-slice first proof (spearhead) + +1. **WebRTC media loopback** — terminate RTP/SRTP, echo audio to a browser. Proves the media core. +2. **Add the tap** — route audio to an external echo process and back. Proves the tap interface. +3. **Swap echo for the brain** — ideally a single speech-to-speech API (e.g. OpenAI Realtime) to collapse STT+LLM+TTS into one integration. Proves agent integration. +4. **Add barge-in** — VAD-driven playout kill. Proves the reflex. +5. **Replace WebRTC ingress with a real PSTN trunk call** — proves the trunk client ([ADR-0003](adr/0003-sip-rust-native-trunk.md)). +6. **Add the spend cap** — hard-stop at threshold. Proves the boundary. + +Steps 1–4 *are* the reflex loop. Keep the dev loop on WebRTC ingress until step 5. + +### The capability ladder (the grand vision, incrementally) + +| Rung | Capability | Reuses | +|---|---|---| +| 1 | **Self-serve** — AI answers, contains the call | the thin-slice first proof | +| 2 | **Escalation** — human agent barges in / takes over when AI breaks down | the audiohook/barge primitive (§5) | +| 3 | **Measurement** — containment rate, where/why AI failed | CDR + analytics on calls *you own* | +| 4 | **Self-improvement** — every takeover → auto-labeled training data → loop | rungs 1–3 compounding | ## Open decisions -- ~~**SIP:** FFI pjproject vs. SBC front vs. pure-Rust.~~ **Decided — ADR-0001:** native Rust core behind a **Kamailio + rtpengine** edge. -- **WASM runtime:** `wasmtime` component model (presumptive). -- **Event bus:** NATS vs. Kafka vs. Redis Streams. -- **License** (see README). +- ~~**SIP:** FFI pjproject vs. SBC front vs. pure-Rust.~~ **Decided — ADR-0003:** Rust-native trunk SIP, no SBC shield. +- ~~**License**~~ **Decided — ADR-0004:** GPL-3.0-or-later. +- ~~**Event bus:** NATS vs. Kafka vs. Redis Streams.~~ **Decided — ADR-0005:** Valkey. +- ~~**Ingress:** WebRTC vs. inbound SIP endpoint.~~ **Decided — ADR-0006:** WebRTC-first; SIP endpoint deferred. +- **WASM runtime** — `wasmtime` component model, *if* WASM is retained at all (demoted to candidate per [ADR-0002](adr/0002-north-star-and-fused-core.md)). Uncoupled from the critical path. +- **Plugin mechanism** — WASM vs. webhooks vs. scripting for community call-flow/routing extensions. Deferred. +- **Agent-tap protocol** — presumptively WSS + core-as-client + clean PCM + core-authoritative playout (see ARCHITECTURE.md, "Agent tap"). To harden against the thin-slice first proof, *not* a decided ADR yet. diff --git a/docs/adr/0001-sip-strategy.md b/docs/adr/0001-sip-strategy.md index e70eb87..9df4830 100644 --- a/docs/adr/0001-sip-strategy.md +++ b/docs/adr/0001-sip-strategy.md @@ -1,8 +1,13 @@ # ADR-0001 — SIP strategy: native Rust core behind a Kamailio + rtpengine edge -- **Status:** Accepted +- **Status:** ~~Accepted~~ **Superseded** (2026-06) - **Date:** 2026-06 -- **Supersedes:** the `🔌 Edge/FFI (pjproject)` disposition in PORT_PLAN.md §1 +- **Supersedes:** the `🔌 Edge/FFI (pjproject)` disposition in PORT_PLAN.md §1 (historical) +- **Superseded by:** [ADR-0003](0003-sip-rust-native-trunk.md) — Rust-native trunk SIP, no SBC shield + +> **Superseded.** Kept as the historical record of the layered "own the Rust parser, front with +> Kamailio + rtpengine" plan. Reversed under the AI-era contact-center scope (the device interop +> tail collapses to a few documented trunk providers). See [ADR-0003](0003-sip-rust-native-trunk.md). ## Context diff --git a/docs/adr/0002-north-star-and-fused-core.md b/docs/adr/0002-north-star-and-fused-core.md new file mode 100644 index 0000000..cce54cc --- /dev/null +++ b/docs/adr/0002-north-star-and-fused-core.md @@ -0,0 +1,93 @@ +# ADR-0002 — North star & fused per-call core + +- **Status:** Accepted +- **Date:** 2026-06 +- **Origin:** [vision-revision](../superpowers/specs/2026-06-26-vision-revision-design.md) §1–4, 6–7 +- **Amends:** `README.md`, `docs/ARCHITECTURE.md`, `docs/PORT_PLAN.md` (the founding identity framing) + +## Context + +The founding docs anchored rutster to "memory-safe successor to Asterisk." A pressure-test +of that anchor: + +1. The PBX category is **already consolidated/eaten** — by UCaaS + Teams Phone. Feature-parity + with a 1.18M-LOC C monolith is a solo-multi-year death-march into a shrinking market. +2. Asterisk's durable value is its **place in the world** — *the self-hostable engine technical + builders stand contact centers on* (Vicidial, GOautodial, a thousand integrator builds) — + *not* its protocols or its channel/bridge/dialplan architecture. +3. AI is actively disrupting telephony (voice agents), opening a self-hostable + AI-native slot + that no incumbent fills: LiveKit has no contact-center domain; cloud CCaaS isn't + self-hostable and bolts AI on; Vapi/Retell put your calls/data on their infra; Vicidial/FreePBX + are Asterisk-era PHP+C. + +"Spiritual successor to Asterisk" therefore means successor to its **role**, not its **form**. + +## Decision + +### 1. North star + +rutster is **the open-source engine for building the AI-era contact center** — AI-native, not AI +bolted-on; self-hostable; owning the contact-center domain; memory-safe Rust. It is a +**framework/engine, not a turnkey product** (Asterisk never tried to *be* Five9; it was the thing +people built on). The moment it chases CCaaS GUI-parity, it dies. Asterisk's capability map +(the PORT_PLAN) is a **completeness checklist**, explicitly **not** an architecture template. + +### 2. Persona + +The 2006 Linux-nerd-who-stood-up-Asterisk-for-an-SMB's modern equivalent: the **CLI/IDE/AI-comfortable +self-hosting technical builder** who runs Claude Code in a terminal, versions everything in git, +and self-hosts on principle. *Not* the no-code admin clicking a flow-designer canvas. This +narrowing is deliberate for an engine, and it liberates the authoring layer — the audience codes +and has an AI pair, so there is no "dumb it down" tax (see §8 of the vision-revision). + +### 3. Architecture — 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 (a `☁️ Service` and + the app plane) — 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). +- It is neither monolith nor pure microservices: **fused where fusion buys the wedge; composable + where independent scaling matters.** + +### 4. Pillars re-weighted + +| Pillar | Change | Why | +|---|---|---| +| Memory-safety | ▲ stronger | Now **literally true at the wire** once the C SBC edge is dropped ([ADR-0003](0003-sip-rust-native-trunk.md)) | +| Security-as-product | ▲ stronger | The single auditable boundary *is* the moat; compliance (PCI/HIPAA/TCPA) is a buying criterion | +| In-boundary spend / abuse control | ▲ promoted | From "table-stakes service" to **constitutive** — structurally impossible for a 3-vendor stack | +| **Data ownership** | ✦ new | Calls + training data never leave the operator's infra — the self-host wedge *and* the ML-loop fuel | +| WASM plugin sandbox | ▼ demoted | Out of the core story. The agent **tap** is the extension point; ops-simplicity wants one binary. Softly retained as a *candidate* for community call-flow/routing plugins | + +## Consequences + +- **Positive:** sharper target than a consolidating PBX category; the wedge is a *coherent + combination* (no-GC determinism + one auditable boundary + ops-simplicity) no one-competitor + matches; the authoring layer can be powerful (the audience codes + has an AI pair). +- **Negative:** narrows TAM deliberately (an engine, not a product); must hold the line against + scope creep toward Five9-parity (the GUI-as-pure-API-client discipline is a scope guard); the + agent brain is necessarily external, so the real-time edge lives in the **local reflexes** + (VAD/barge-in, jitter, pacing) + the *whole* boundary, not the brain round-trip. + +## References + +- [ADR-0003](0003-sip-rust-native-trunk.md) — Rust-native trunk SIP (removes the C edge this reframe assumes) +- [ADR-0005](0005-event-bus.md) — Valkey as bus + state store +- [ADR-0006](0006-ingress-posture.md) — WebRTC-first ingress diff --git a/docs/adr/0003-sip-rust-native-trunk.md b/docs/adr/0003-sip-rust-native-trunk.md new file mode 100644 index 0000000..cffa296 --- /dev/null +++ b/docs/adr/0003-sip-rust-native-trunk.md @@ -0,0 +1,76 @@ +# ADR-0003 — SIP strategy: Rust-native trunk SIP, no SBC shield + +- **Status:** Accepted +- **Date:** 2026-06 +- **Supersedes:** [ADR-0001](0001-sip-strategy.md) +- **Origin:** [vision-revision](../superpowers/specs/2026-06-26-vision-revision-design.md) §5 + +## Context + +ADR-0001 layered the SIP strategy: own the Rust parser, but front the public edge with a +proven **Kamailio + rtpengine** SBC to rent the "20-year device/carrier interop tail." That tail +is real for a generic PBX — thousands of desk-phone quirks, NAT behaviors, carrier glare. + +Reconsidered under the AI-era contact-center scope ([ADR-0002](0002-north-star-and-fused-core.md)), +the tail **collapses**: rutster talks to a few **documented, cooperative SIP-trunk providers +(Telnyx, Bandwidth, Twilio SIP)**, IP-allowlisted, not thousands of far-end desk-phone UAs. An +inbound/outbound SIP-trunk client against cooperative carriers is tractable in `rsip` / `ezk`, +bounded by carrier documentation, not device quirks. + +The C SBC shield, meanwhile, directly contradicts the wedge: + +- It puts **C at the most-exposed seam** (the public internet) — gutting the memory-safety + headline *at the one place it matters most*. +- It **terminates media twice** — rtpengine at the edge, rutster's media plane inside — + breaking "terminate media once" / "one auditable boundary." + +## Decision + +Own trunk SIP **and** media termination directly in the Rust core; IP-allowlist the handful of +trunk providers. **No Kamailio + rtpengine shield. No pjproject FFI.** + +- Parser: `rsip` (message types/parsing). Sans-IO transaction/dialog/core: the `ezk` family. + No hostile bytes ever hit a C parser. **Fuzzed.** +- The memory-safety thesis becomes **literally true at the wire**: hostile bytes hit a fuzzed + Rust parser first, at the edge of *our* trust boundary. +- Interop surface is **bounded by carrier docs** (a finite, knowable set), not the unbounded + device tail ADR-0001 rented the shield for. + +### Scope boundary — this ADR is about *trunk* SIP, not *endpoint* SIP + +Inbound SIP **endpoint** registration (desk/soft phones: `REGISTER`, BLF/MWI, DTMF variants, +per-device NAT/provisioning) is a **different axis** and is **deferred** — it re-imports the +unbounded device-interop tail this ADR deliberately closes. Human-participant ingress is +**WebRTC**. See [ADR-0006](0006-ingress-posture.md). + +## Topology + +``` + hostile internet trusted core + ───────────────► (carrier trunk) ──► rutster control + media plane + (SIP trunks, (native Rust SIP parser + + far-end UAs) transaction/dialog core, + owned end-to-end here) +``` + +WebRTC ingress ships **first** and is unaffected (WebRTC signaling is app-defined, not SIP), so +first-call never blocks on SIP. + +## Consequences + +- **Positive:** memory-safety headline *literally true* at the wire; one media-termination point; + no C operational dependency (no Kamailio/rtpengine config/deploy/expertise); no pjproject + license/threading/`unsafe` entanglement; the trunk client is bounded by carrier docs, not device + quirks. +- **Negative / cost:** we own a (bounded) SIP-trunk client early; carrier-specific interop must be + maintained as trunks are added. +- **Mitigation:** WebRTC-first ordering means first-call doesn't block on SIP; the thin-slice + steps 1–4 (media core → tap → brain → barge-in) all land before step 5 (real PSTN trunk). +- **Graduation:** unlike ADR-0001, there is no shield to retire — the question is only how the + trunk client matures in production. + +## References + +- [ADR-0001](0001-sip-strategy.md) — superseded (the layered Kamailio+rtpengine + Rust-core plan) +- [ADR-0002](0002-north-star-and-fused-core.md) — the fused-vertical reframe this SIP decision enables +- [ADR-0006](0006-ingress-posture.md) — WebRTC-first ingress; SIP endpoint deferred diff --git a/docs/adr/0004-license.md b/docs/adr/0004-license.md new file mode 100644 index 0000000..5ea3e44 --- /dev/null +++ b/docs/adr/0004-license.md @@ -0,0 +1,48 @@ +# ADR-0004 — License: GPL-3.0-or-later + +- **Status:** Accepted +- **Date:** 2026-06 +- **Closes:** the license open-decision carried in the founding docs + +## Context + +The founding docs (and the [vision-revision](../superpowers/specs/2026-06-26-vision-revision-design.md) +open-decisions list) left license open. rutster is a self-hosted engine whose competitor set +([ADR-0002](0002-north-star-and-fused-core.md)) includes cloud CCaaS that could *wrap-and-resell* +it as a managed service — the classic strong-copyleft concern. + +## Decision + +**GPL-3.0-or-later.** + +Strong copyleft (requires source-sharing on binary conveyance), OSI-clean, modernized one notch +over Asterisk's GPLv2+. `SPDX-License-Identifier: GPL-3.0-or-later`. + +## Alternatives considered + +- **AGPL-3.0-or-later** — *considered.* AGPL additionally closes the network-use / SaaS loophole: + GPLv3 §13 states network use is **not** conveyance (so a hosted service triggers no + source-sharing obligation); AGPL §13 fixes that, compelling source disclosure for network-served + use. Given that cloud CCaaS is a named competitor, AGPL would harder-prevent a vendor from + hosting rutster-as-managed-CCaaS without contributing back. +- **GPL-3.0-or-later was chosen** in favor of simplicity and lower downstream-embedding friction + (operators and tooling/library authors integrating rutster avoid AGPL's network-trigger), while + remaining strong copyleft against proprietary forking of the engine itself. + +## Trade-off accepted deliberately + +A cloud competitor *can* host GPL rutster and owe source only on binary distribution, which for a +hosted service may be nothing. The license is the **floor**, not the moat: the wedge — self-host ++ data-ownership + one auditable boundary + operational simplicity + data-ownership as a buying +criterion — is the actual reason operators self-host rather than wrap. + +If cloud-wrap becomes a demonstrated threat, **re-evaluate AGPL-3.0-or-later** (this ADR's +"or-later" clause permits that transition cleanly, since GPL-3.0-or-later is a strict subset of +AGPL-3.0-or-later for recipients). + +## Consequences + +- **Positive:** OSI-clean strong copyleft in the Asterisk lineage; compatible with the project's + dependencies (Valkey BSD-3-Clause per [ADR-0005](0005-event-bus.md); Rust-ecosystem MIT/Apache crates). +- **Negative:** the network-use loophole is open **by design choice** — mitigated by the wedge, not + the license. diff --git a/docs/adr/0005-event-bus.md b/docs/adr/0005-event-bus.md new file mode 100644 index 0000000..8f0d94a --- /dev/null +++ b/docs/adr/0005-event-bus.md @@ -0,0 +1,62 @@ +# ADR-0005 — Event bus & state store: Valkey + +- **Status:** Accepted +- **Date:** 2026-06 +- **Closes:** the event-bus open-decision carried in the founding docs (NATS vs. Kafka vs. Redis Streams) + +## Context + +The fused per-call core ([ADR-0002](0002-north-star-and-fused-core.md)) needs: + +- a **bus** for cross-service control events (replacing Asterisk's internal Stasis bus for + anything crossing the boundary), +- a **KV state store** replacing `astdb` + sorcery / realtime config state, +- **presence sets** for MWI/BLF signaling aggregation. + +The founding docs listed the choice as NATS vs. Kafka vs. Redis Streams. + +## Decision + +**Valkey** — the Linux-Foundation BSD-3-Clause fork of Redis 7.2.4 (wire-protocol-compatible drop-in). + +### Why not "Redis" + +Redis relicensed in March 2024 to **RSALv2 + SSPLv2** — **not OSI-open-source**, and license-incompatible with this project's posture (FOSS-clean + GPL-3.0-or-later per [ADR-0004](0004-license.md)). +Recommending "Redis" undercuts the data-ownership pillar ([ADR-0002](0002-north-star-and-fused-core.md)) and the clean-license story. Valkey is *Redis-the-thing* with a compatible license, maintained by the community (AWS, Google, Oracle, et al.). + +### Why Valkey fits the wedge specifically + +- **Collapses bus + state store + presence into one dependency.** A solo operator runs one process + with `compose up`. NATS is a cleaner pure-bus but doesn't also replace `astdb`/sorcery; Kafka is + structural overkill at self-hosting scale and violates the one-binary / operational-simplicity + pillar. +- One tool, three roles: + - **streams + consumer groups** → CDR/CEL/analytics pipeline fanout and replay, + - **pub/sub** → presence / MWI / BLF signaling, + - **KV** → state store (config + runtime state replacing `astdb`/sorcery). + +### Alternatives + +- **Kafka — rejected.** Structural overkill at self-hosting scale; operational weight violates + the operational-simplicity pillar. +- **NATS — retained as a config-pluggable alternative.** An operator outgrowing Valkey (notably + needing NATS JetStream's durable streaming model at larger scale) can swap it in. The bus + backend seam stays **pluggable** at the config boundary, not architecture-load-bearing. + +## Constraints (load-bearing, not preferences) + +1. **The 20ms media loop never rides the bus.** Media timing stays in-core on dedicated timing + threads ([ADR-0002](0002-north-star-and-fused-core.md) §7; vision-revision §7). The bus carries + control / cross-service events only — not latency-pinned media — so Valkey's sub-ms local + latency is plenty. +2. **The bus is NOT the source of truth for billing- or call-loss-critical state.** Valkey + persistence (RDB/AOF) is async-ish — fine for transport / replay / fleeting retention, **wrong** + for "the CDR that proves what we billed." CDR and recordings emit **durably to object storage** + in their own services; the bus only *flows* events into that pipeline and lets services *react*. + +## Consequences + +- **Positive:** one OSS dep for bus + state + presence; self-hostable and license-clean; pluggable + to NATS later without architecture changes; aligns with operational-simplicity. +- **Negative:** Valkey is "good enough" at each role, not best-of-breed for each — accepted + deliberately for the ops-simplicity pillar. diff --git a/docs/adr/0006-ingress-posture.md b/docs/adr/0006-ingress-posture.md new file mode 100644 index 0000000..454fc3e --- /dev/null +++ b/docs/adr/0006-ingress-posture.md @@ -0,0 +1,65 @@ +# ADR-0006 — Ingress posture: WebRTC-first; SIP endpoint deferred + +- **Status:** Accepted +- **Date:** 2026-06 +- **Related:** [ADR-0003](0003-sip-rust-native-trunk.md) (trunk SIP), [ADR-0002](0002-north-star-and-fused-core.md) (fused vertical) + +## Context + +The fused per-call vertical ([ADR-0002](0002-north-star-and-fused-core.md)) needs an answer for +**human-participant ingress** — agents and supervisors joining/taking calls. The thin-slice first +proof starts at WebRTC loopback (vision-revision §10). The question: which protocols carry humans +into calls, and is inbound **endpoint** SIP (desk/soft phones) in scope? + +## Decision + +### 1. WebRTC is the first-party human-participant ingress + +Agents and supervisors use the browser as the softphone: SSO in, one peer-connection, +DTLS-SRTP + ICE handled by the browser, **zero device-provisioning tail**. This is also the modern +hosted UX. Escalation (capability rung 2) is a UX gesture: an agent clicks *take this call* in the +dashboard → a WebRTC leg joins → audiohook/barge handoff. Writes itself onto the audiohook primitive +(PORT_PLAN) and shares step-1 infrastructure (WebRTC ships first regardless). + +### 2. SIP trunk client = core (per ADR-0003) + +Bounded: a few documented, IP-allowlisted, cooperative carriers. + +### 3. Inbound SIP *endpoint* registrar = deferred / community edge, NOT core + +`REGISTER` from Polycoms/Yealinks/Zoiper, BLF/MWI subscribe-notify, RFC2833-vs-SIP-INFO DTMF, +per-device NAT keepalives and provisioning quirks are **deferred out of the core**: + +- **The real reason** is not "SIP is hard" — it's that an inbound endpoint server **re-imports the + unbounded device-interop tail** [ADR-0003](0003-sip-rust-native-trunk.md) deliberately closed. + ADR-0003's "tail collapses" argument survives only because rutster talks to *documented carriers*, + not *thousands of desk-phone UAs*. Opening an endpoint server reopens that seam. +- In an SSO/browser-agent contact center the desk-phone demographic has collapsed to niche ops + floors already served by dated OSS (FreePBX/Vicidial). Wrong place to spend a solo multi-year + build — a pointlessly-and-diverting distraction in a modern hosted-UX/SSO world. +- **Don't burn the bridge:** if an operator genuinely needs desk phones, the right home is an + **optional, out-of-tree SBC-style adapter** (the Kamailio-flavored thing ADR-0003 chose not to put + in the core) fronting the public API or B2B-SIP'ing into rutster — community-owned, **outside the + trust boundary** so its tail can't compromise the wedge. + +### 4. Core interop perimeter is deliberately bounded + +**SIP trunk client + WebRTC ingress.** Nothing else inbound. + +## Security seam — tap ≠ ingress + +Two distinct extension points with **opposite** security postures; never unified into one model: + +- **Tap** (agent brain): **egress**, core-as-**client**, **no inbound tap port** on the core. The + boundary dials out to the brain. (See ARCHITECTURE.md, "Agent tap.") +- **Ingress** (humans): **inbound**, core-as-**server**, with SSO + RBAC + per-tenant scoping. + Legitimately a server surface — that's fine, it's auth'd; the "core-as-client" move was + specifically about the egress brain tap, where *egress* is the dangerous direction. + +## Consequences + +- **Positive:** first-call via WebRTC ships before any SIP; escalation is a UX click not a new + protocol; the core's interop surface stays bounded; no device-provisioning tail in the trust + boundary. +- **Negative:** desk-phone-only operators are not served by the core — accepted, the community + adapter path exists. diff --git a/docs/superpowers/specs/2026-06-26-vision-revision-design.md b/docs/superpowers/specs/2026-06-26-vision-revision-design.md index 9f37a51..aaa20c5 100644 --- a/docs/superpowers/specs/2026-06-26-vision-revision-design.md +++ b/docs/superpowers/specs/2026-06-26-vision-revision-design.md @@ -1,6 +1,7 @@ # Rutster — Vision Revision: the self-hosted AI-era contact-center engine -- **Status:** Proposed — for review +- **Status:** Ratified 2026-06-26 — folded into `README.md`, `docs/ARCHITECTURE.md`, + `docs/PORT_PLAN.md`, and ADRs 0002–0006. Kept as the design record of the pressure-test. - **Date:** 2026-06-26 - **Origin:** A pressure-test of the four founding docs (README, ARCHITECTURE, PORT_PLAN, ADR-0001). This document records the decisions that pressure-test produced. - **Amends:** `README.md`, `docs/ARCHITECTURE.md`, `docs/PORT_PLAN.md`