ADR-0007 — Trunk/PSTN strategy: rent the transport, no first-party SIP stack.
Supersedes ADR-0003 (Rust-native trunk SIP). The 2026-06 strategic-relevance
review stress-tested ADR-0003 against the 2026 market + solo-build realities
and it did not survive: a trunk SIP/media core is the highest-cost,
lowest-differentiation square on the board (the perceived-quality battle and
the buying criteria both live above the transport), and no production-hardened
pure-Rust SIP stack exists to stand on (ezk is bus-factor-1 / pre-1.0;
str0m self-describes as 'not for production' for SIP).
rutster owns no SIP stack. PSTN reach is rented transport in three layers:
1. Primary (demo, most users) — CPaaS raw-media fork (Twilio Media Streams,
Telnyx). Media-leg ingress, core-as-server (parallel to WebRTC ingress
per ADR-0006). Use the raw-audio fork, *not* managed Voice-AI products
that would consume the reflex loop.
2. Graduation (on-prem) — out-of-tree SBC (Kamailio/FreeSWITCH/drachtio +
rtpengine) B2BUAs carrier SIP into rutster as clean RTP/tap media,
outside the trust boundary.
3. Never — a first-party Rust trunk SIP stack. rsip/ezk stay off the
critical path.
The only things rutster owns in Rust: the call model, the reflex loop, the
agent tap. Everything that touches a carrier is rented (layer 1) or
out-of-tree (layer 2).
ADR-0008 — The FOB and the green zone: the build-vs-reuse doctrine. Names
the boundary criterion implicit across ADRs 0002-0007 as one mechanical rule
every contributor (human or agent) applies the same way.
- FOB (build in Rust) — admitted only if it passes one of: hot path,
security-constitutive, differentiating. Current FOB: media termination
(rutster-media on str0m) · real-time reflexes (VAD/barge-in/jitter/pacing)
· call model (rutster-call-model) · agent tap (rutster-tap) · spend/abuse
gate (rutster-spend) · control API + state/bus trait (rutster).
- Green zone (reuse at arm's length) — its own process/container/trust
domain, never in the FOB's address space. Admitted when it fails all
three FOB tests AND a trusted, actively-maintained project already does
it well. Current green zone: Valkey (ADR-0005) · carrier trunk (ADR-0007)
· agent brain · reference GUI · object storage · KMS · OTel collector ·
container supervisor.
- 'Actively maintained' is the load-bearing gate: excludes a first-party
Rust SIP stack and webrtc-rs; admits Valkey and str0m-for-WebRTC.
- When in doubt, default to green zone. The FOB earns its members.
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.
ADR-0003's status is flipped to Superseded with a forward-pointer to ADR-0007.
Kept as the historical record of the 'own trunk SIP + media termination in
Rust, no SBC shield' plan; reversed under the strategic-relevance review.
84 lines
4.5 KiB
Markdown
84 lines
4.5 KiB
Markdown
# ADR-0003 — SIP strategy: Rust-native trunk SIP, no SBC shield
|
||
|
||
- **Status:** ~~Accepted~~ **Superseded** (2026-06)
|
||
- **Date:** 2026-06
|
||
- **Supersedes:** [ADR-0001](0001-sip-strategy.md)
|
||
- **Superseded by:** [ADR-0007](0007-trunk-rented-transport.md) — rent the trunk transport; no first-party SIP stack
|
||
- **Origin:** [vision-revision](../superpowers/specs/2026-06-26-vision-revision-design.md) §5
|
||
|
||
> **Superseded.** Kept as the historical record of the "own trunk SIP + media termination in Rust,
|
||
> no SBC shield" plan. Reversed under the 2026-06 strategic-relevance review: a from-scratch Rust
|
||
> trunk-SIP stack is the highest-cost / lowest-differentiation part of a solo build, and there is no
|
||
> production-hardened pure-Rust SIP stack to stand on. The trunk is **rented, not built** — see
|
||
> [ADR-0007](0007-trunk-rented-transport.md).
|
||
|
||
## 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
|