diff --git a/AGENTS.md b/AGENTS.md index a445421..31641e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ rutster/ │ ├── rutster/ # binary: axum signaling server + media driver + static page │ ├── rutster-media/ # str0m WebRTC + Opus<->PCM codec boundary │ ├── rutster-call-model/ # the Channel/Leg object embryo -│ ├── rutster-signaling-sip/ # stub until spearhead step 5 +│ ├── rutster-trunk/ # stub until spearhead step 5 │ ├── rutster-tap/ # stub until spearhead step 2 │ └── rutster-spend/ # stub until spearhead step 6 ├── fuzz/ # (planned) placeholder cargo-fuzz harness dir @@ -170,10 +170,12 @@ Before proposing changes to the architecture, read in this order: thin-slice phasing (20 min). 4. [`docs/adr/`](docs/adr/) — every ADR. Load-bearing decisions, not optional reading: - [ADR-0002](docs/adr/0002-north-star-and-fused-core.md) — north star + fused vertical - - [ADR-0003](docs/adr/0003-sip-rust-native-trunk.md) — Rust-native trunk SIP (no SBC) + - [ADR-0003](docs/adr/0003-sip-rust-native-trunk.md) — ~~Rust-native trunk SIP~~ **superseded by [ADR-0007](docs/adr/0007-trunk-rented-transport.md)** - [ADR-0004](docs/adr/0004-license.md) — GPL-3.0-or-later - [ADR-0005](docs/adr/0005-event-bus.md) — Valkey as bus + state store - [ADR-0006](docs/adr/0006-ingress-posture.md) — WebRTC-first ingress + - [ADR-0007](docs/adr/0007-trunk-rented-transport.md) — rent the trunk transport; no first-party SIP stack + - [ADR-0008](docs/adr/0008-fob-and-green-zone.md) — the FOB / green-zone build-vs-reuse doctrine 5. [`docs/superpowers/specs/`](docs/superpowers/specs/) — design specs in flight. Read the latest one to know what's currently being built and what's explicitly deferred. - [2026-06-26 vision-revision](docs/superpowers/specs/2026-06-26-vision-revision-design.md) — @@ -188,6 +190,12 @@ and "why isn't X here?" questions. Consult it before adding anything. ## Key decisions to respect +- **The FOB / green-zone doctrine** (ADR-0008) — *the* build-vs-reuse rule. Build it in the + memory-safe Rust **FOB** only if it's hot-path, security-constitutive, or differentiating; otherwise + reuse trusted, **actively-maintained** OSS in the **green zone**, at arm's length (its own process / + container / trust domain). When in doubt, default to green zone — the FOB earns its members, it + doesn't collect them. This is why the trunk is rented (ADR-0007) and Valkey reused (ADR-0005), not + rebuilt. Don't pull green-zone plumbing into the core. - **License:** `GPL-3.0-or-later` on every crate manifest (ADR-0004). Strong copyleft in the Asterisk lineage. Don't introduce deps that conflict (`cargo deny check licenses` enforces). - **WebRTC stack:** `str0m` (sans-IO). Not `webrtc-rs`. Chosen because the sans-IO design maps @@ -233,13 +241,13 @@ These are explicitly deferred per the slice-1 spec's out-of-scope table. Adding would break the sequencing that the spearhead depends on: - ❌ Dedicated timing thread for the media loop (deferred to step 4, barge-in) -- ❌ TLS on the HTTP signaling surface (deferred to step 5, PSTN trunk) +- ❌ TLS on the HTTP signaling surface (deferred to step 5, rented-transport PSTN) - ❌ Authn / authz / multi-tenancy on `/v1/sessions` (deferred to step 6, spend cap) - ❌ Trickle ICE (deferred until NATs demand it) - ❌ The tap itself (deferred to step 2 — slice 1 only *pre-paves* the seam) - ❌ The brain / STT / LLM / TTS (deferred to step 3) - ❌ Barge-in / VAD-driven playout kill (deferred to step 4) -- ❌ PSTN trunk / SIP client (deferred to step 5) +- ❌ PSTN via rented transport / CPaaS media-leg ingress (deferred to step 5; no first-party SIP — ADR-0007) - ❌ Spend cap / abuse gate (deferred to step 6) - ❌ Browser-based automated e2e tests / Selenium / Playwright (deferred post-slice-1) - ❌ Docker / compose (deferred to a later rung) diff --git a/README.md b/README.md index d4b4c15..e009220 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,10 @@ The white space no incumbent fills: 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). +2. **One secure auditable boundary** — media + local reflexes + spend/abuse control + the tap + + audit in a single memory-safe trust domain. One thing to certify (strongest for PCI / HIPAA / TCPA). + The carrier trunk is rented (or out-of-tree); bringing PSTN media *inside* the boundary is the + on-prem **graduation** ([ADR-0007](docs/adr/0007-trunk-rented-transport.md)), not a day-one claim. 3. **Operational simplicity** — one binary, one bill, one deploy. **Honest caveat:** the agent *brain* (STT/LLM/TTS) is necessarily external — audio leaves the box @@ -75,21 +77,24 @@ exactly as integrators did on top of Asterisk. ## What it is / isn't -- **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 +- **Is:** a Rust media core owning the **per-call vertical** (RTP/SRTP media termination + local real-time reflexes + a clean + audio tap to an external brain + in-boundary spend/abuse control; the carrier trunk is *rented + transport*, not first-party — [ADR-0007](docs/adr/0007-trunk-rented-transport.md)); 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. +- **Isn't:** a TDM/PSTN-hardware PBX — *and not a SIP stack.* No DAHDI, no Sangoma/Digium cards, no + ISDN/SS7, no IAX2/H.323/SCCP/MGCP/Unistim — and **no first-party SIP** + ([ADR-0007](docs/adr/0007-trunk-rented-transport.md)). PSTN reach is **rented transport** (a CPaaS + raw-media fork, or an out-of-tree SBC for on-prem media). Inbound SIP *endpoint* registration (desk + phones) likewise stays out-of-tree — 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. 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*. + parsers. rutster **parses no SIP at all** ([ADR-0007](docs/adr/0007-trunk-rented-transport.md)): its + entire first-party wire surface is WebRTC/RTP/SRTP + the WebSocket tap/ingress protocol — all + memory-safe Rust. The carrier-SIP interop tail lives *outside* the trust boundary (rented transport + or an out-of-tree SBC), so the buffer-overflow/RCE CVE class is designed out of rutster's own surface. 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. @@ -111,7 +116,7 @@ exactly as integrators did on top of Asterisk. 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) + - [0007](docs/adr/0007-trunk-rented-transport.md) — rent the trunk transport; no first-party SIP stack (supersedes [0003](docs/adr/0003-sip-rust-native-trunk.md)) - [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 @@ -133,7 +138,8 @@ The full thin slice, sequenced so each step is its own proof — never a big ban 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 +5. **Add a real phone number via rented transport** (a CPaaS raw-media fork, e.g. Twilio Media + Streams) — proves a PSTN call reaches the reflex loop, no first-party SIP ([ADR-0007](docs/adr/0007-trunk-rented-transport.md)) 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 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d17e102..5fec39c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -45,10 +45,33 @@ This **replaces the founding three-plane framing**: - 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 -- **Trunk SIP termination** — Rust-native; IP-allowlisted cooperative carriers; no C SBC. - ([ADR-0003](adr/0003-sip-rust-native-trunk.md)) +- **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 @@ -152,10 +175,13 @@ for call centers, not Squarespace.** ## Biggest technical risk -**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. +**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). -Everything else builds on the existing Rust media ecosystem; the agent **brain** is external by -necessity and reached via the tap. +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. diff --git a/docs/PORT_PLAN.md b/docs/PORT_PLAN.md index 0c96127..aeaab7f 100644 --- a/docs/PORT_PLAN.md +++ b/docs/PORT_PLAN.md @@ -30,9 +30,11 @@ Grounded against the Asterisk 22.10.1 tree (~1.18M LOC C/H). 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).* +4. Mature C that's a tar pit to rewrite (SIP) → **rent it, don't own it.** + *[ADR-0007](adr/0007-trunk-rented-transport.md) restores this rule's original instinct for the + trunk: rutster owns no SIP stack — carrier transport is a rented CPaaS raw-media fork (primary) or + an out-of-tree SBC (on-prem graduation); ADR-0003's from-scratch Rust trunk SIP is superseded. 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. **Spend / abuse control is constitutive**, pulled into the boundary, not externalized. @@ -43,9 +45,9 @@ Grounded against the Asterisk 22.10.1 tree (~1.18M LOC C/H). | Asterisk subsystem | Module(s) | Disp. | Rationale | |---|---|---|---| -| 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). | +| SIP signaling (trunk) | `chan_pjsip` + `res_pjsip*` (48 mods) | 🔌 Rented / out-of-tree | **Re-decided ([ADR-0007](adr/0007-trunk-rented-transport.md), supersedes ADR-0003):** rutster owns **no SIP stack.** PSTN audio arrives as a media-leg ingress from a rented CPaaS raw-media fork (Twilio Media Streams / Telnyx — primary), or an out-of-tree SBC (Kamailio/FreeSWITCH/drachtio + rtpengine) when media must stay on-prem. rutster parses no SIP → SIP is removed from the attack surface entirely, not rewritten. The from-scratch Rust trunk SIP of ADR-0003 was the highest-cost / lowest-differentiation square on the board, with no production-hardened Rust SIP stack to stand on. | +| ↳ outbound registration (to carriers) | `res_pjsip_outbound_registration` | 🔌 Rented / out-of-tree | Handled by the rented transport or the out-of-tree SBC ([ADR-0007](adr/0007-trunk-rented-transport.md)), not by rutster — the provider/SBC owns register-to-carrier. | +| ↳ **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* — it fails all three FOB tests ([ADR-0008](adr/0008-fob-and-green-zone.md)), so it's **green-zone, out-of-tree, never first-party** ([ADR-0007](adr/0007-trunk-rented-transport.md)): 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`. | @@ -204,7 +206,7 @@ These are the "modern" deltas — load-bearing, not optional polish. |---|---|---| | 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 / 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. | +| 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 call origination + the tap inside the boundary** so a runaway brain structurally can't exceed spend/pacing: rutster mediates both the provider call-control API and the brain tap, so the brain never holds the wire ([ADR-0007](adr/0007-trunk-rented-transport.md)). 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 | **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. | @@ -223,7 +225,7 @@ The old Phase 0–4 capability-rolldown is **replaced** (see [vision-revision § 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)). +5. **Add a real phone number via rented transport** — a CPaaS raw-media fork (e.g. Twilio Media Streams) lands a PSTN call on the reflex loop; no first-party SIP ([ADR-0007](adr/0007-trunk-rented-transport.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. @@ -239,7 +241,7 @@ Steps 1–4 *are* the reflex loop. Keep the dev loop on WebRTC ingress until ste ## Open decisions -- ~~**SIP:** FFI pjproject vs. SBC front vs. pure-Rust.~~ **Decided — ADR-0003:** Rust-native trunk SIP, no SBC shield. +- ~~**SIP:** FFI pjproject vs. SBC front vs. pure-Rust.~~ ~~Decided — ADR-0003: Rust-native trunk SIP.~~ **Re-decided — [ADR-0007](adr/0007-trunk-rented-transport.md):** rutster owns no SIP stack; the trunk is rented transport (CPaaS raw-media fork) or an out-of-tree SBC. - ~~**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.