Files
rutster/docs/adr/0003-sip-rust-native-trunk.md
opencode controller 272aa07acd adr: +0007 rent trunk transport +0008 FOB/green-zone doctrine; supersede 0003
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.
2026-06-29 20:26:24 -04:00

84 lines
4.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 14 (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