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

4.5 KiB
Raw Permalink Blame History

ADR-0003 — SIP strategy: Rust-native trunk SIP, no SBC shield

  • Status: Accepted Superseded (2026-06)
  • Date: 2026-06
  • Supersedes: ADR-0001
  • Superseded by: ADR-0007 — rent the trunk transport; no first-party SIP stack
  • Origin: vision-revision §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.

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), 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.

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 — superseded (the layered Kamailio+rtpengine + Rust-core plan)
  • ADR-0002 — the fused-vertical reframe this SIP decision enables
  • ADR-0006 — WebRTC-first ingress; SIP endpoint deferred