Files
rutster/docs/adr/0007-trunk-rented-transport.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

140 lines
9.7 KiB
Markdown
Raw Permalink 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-0007 — Trunk/PSTN strategy: rent the transport, no first-party SIP stack
- **Status:** Accepted
- **Date:** 2026-06
- **Supersedes:** [ADR-0003](0003-sip-rust-native-trunk.md) — Rust-native trunk SIP, no SBC shield
- **Origin:** 2026-06 strategic-relevance review (market + code sanity-check). Re-aims the solo build
at the AI-telephony frontier — the reflex/tap/eval loop and the contact-center domain — and off the
trunk-SIP rewrite.
- **Amends:** `README.md`, `docs/ARCHITECTURE.md`, `docs/PORT_PLAN.md`
- **Related:** [ADR-0002](0002-north-star-and-fused-core.md) (fused vertical), [ADR-0006](0006-ingress-posture.md)
(ingress posture), [ADR-0001](0001-sip-strategy.md) (the "rent the edge" instinct this re-converges with)
## Context
[ADR-0003](0003-sip-rust-native-trunk.md) decided to own trunk SIP **and** media termination directly
in the Rust core (`rsip` + `ezk`), dropping the Kamailio + rtpengine shield so the memory-safety thesis
would be "literally true at the wire." The argument was internally coherent: under the contact-center
scope the device-interop tail collapses to a few documented carriers, so a bounded Rust trunk client
looked tractable.
A strategic-relevance review — the goal is *staying at the AI-telephony frontier*, not shipping a
from-scratch protocol stack — stress-tested that bet against the 2026 market and the realities of a
solo build. It does not survive:
1. **A trunk SIP/media core is the highest-cost, lowest-differentiation square on the board.**
End-to-end voice-agent latency is ~1,100 ms mouth-to-ear; the media/transport layer is ~80100 ms
of it, and PSTN transport is ~$0.02/min — a rounding error. The perceived-quality battle
(turn-taking, barge-in, endpointing) and the buying criteria (integration, reliability, compliance,
outcomes) all live **above** the transport. No buyer in this market has ever purchased on
implementation language.
2. **There is no production-hardened pure-Rust SIP stack to stand on.** `ezk-sip` is the only credible
one and it is single-maintainer / pre-1.0 (bus-factor-1); `str0m` self-describes as "not for
production" for this purpose. Every serious player stands on mature C/Go and puts its own language
only at the orchestration edge — LiveKit on Go/Pion, Daily wrapping libwebrtc, jambonz on
FreeSWITCH + rtpengine. A solo founder re-fighting 25 years of carrier/NAT/DTMF hardening in a young
Rust stack spends the one resource it cannot spare (years) on the layer that pays back least.
3. **It steals those years from the actual white space.** The differentiation versus LiveKit / Vapi /
Pipecat is the **contact-center domain** (ACD, queues, escalation, CDR, recording, supervisor) plus
the **reflex loop** plus the **data-owned eval/containment loop** — none of which a trunk-SIP rewrite
gets you closer to.
ADR-0003's own sequencing already conceded the point: it put the reflex loop (spearhead steps 14)
*before* the trunk (step 5) precisely because the reflexes are "the hard, most-differentiating part."
Followed one step further, that logic says: don't make the trunk a first-party build at all.
## Decision
rutster **owns no SIP stack.** Carrier/PSTN reach is **rented transport**, in three layers — and
rutster owns only the top one:
1. **Primary (demo, frontier, most users) — rented CPaaS raw-media ingress. Zero SIP in rutster.**
A CPaaS raw-audio fork (Twilio **Media Streams**, Telnyx media streaming, or equivalent) delivers
the PSTN call's audio (µ-law/PCM) over a WebSocket; call control (answer/originate/hangup) is the
provider's REST/Call-Control API. This lands as a **media-leg *ingress*** — core-as-**server**,
exactly parallel to the WebRTC ingress in [ADR-0006](0006-ingress-posture.md) — resampled to the
canonical tap format and fed straight into the reflex loop. Use the **raw-audio** fork, **not**
managed Voice-AI products (Twilio ConversationRelay et al.) that perform STT/TTS for you and would
consume the reflex loop that is rutster's differentiation.
2. **Graduation (sovereignty / on-prem customers) — out-of-tree SBC adapter.**
When PSTN media must stay on the operator's infra, a mature C/Go SBC (Kamailio / FreeSWITCH /
drachtio + rtpengine) sits **outside the trust boundary** and B2BUAs carrier SIP into rutster as
clean RTP / tap-format media. The operator *operates* C; rutster *parses* none. This is
[ADR-0006](0006-ingress-posture.md)'s out-of-tree edge-adapter pattern, applied to trunks.
3. **Never — a first-party Rust trunk SIP stack.** `rsip` / `ezk` stay off the critical path. If a thin
Rust SIP UA is ever wanted, it is a discretionary, out-of-tree experiment — never a core dependency
or a blocker.
**The only things rutster owns in Rust are the call model, the reflex loop, and the agent tap.**
Everything that touches a carrier is rented (layer 1) or out-of-tree (layer 2).
## Topology
```
carrier / PSTN rented or out-of-tree rutster trust boundary
──────────────► ┌──────────────────────────┐ ┌──────────────────────────────┐
SIP trunks, │ layer 1: CPaaS raw-media │ │ media (RTP/SRTP, str0m) │
far-end UAs │ fork — NO SIP in rutster│──►│ + reflexes (VAD/barge-in, │
│ provider API = control │ │ jitter, pacing) │
│ layer 2: out-of-tree SBC │ │ + in-boundary spend/abuse │
│ (C/Go) → clean RTP/tap │ │ + clean audio tap ──► brain │
└──────────────────────────┘ └──────────────────────────────┘
SIP lives here — outside rutster, never first-party no SIP parser, ever
```
WebRTC ingress ([ADR-0006](0006-ingress-posture.md)) is unchanged and remains the primary
human-participant path; layer 1 adds a phone number without adding SIP.
## What this costs (stated so we don't kid ourselves)
- **Data-ownership / "terminate media once" dilutes for PSTN in layer 1.** A CPaaS-forked PSTN call's
audio traverses the provider's infra — the same property rutster criticizes in Vapi/Retell. What
survives even in layer 1: WebRTC legs stay on your infra; the **brain is self-hostable**, so
*training data and model* remain owned even when transport is rented; and layer 2 brings PSTN media
back on-prem for buyers who require it. The wedge becomes a **graduation**, not a day-one guarantee —
an honest narrowing the pitch must reflect (README competitor table + wedge bullet 2).
- **A CPaaS dependency** (billing, availability) for layer-1 PSTN. Mitigation: it is a config seam
(provider-agnostic media-leg ingress), and the frontier work (reflexes, tap, brain) is built and
demoed entirely on WebRTC, so nothing on the critical path blocks on any provider.
## Memory-safety, restated honestly
ADR-0003's headline was "own a fuzzed Rust SIP parser → safe at the wire." ADR-0007's is stronger in
one respect and weaker in another, stated plainly: **rutster parses no SIP at all.** Its entire
first-party wire surface is WebRTC/RTP/SRTP (`str0m`) + the WebSocket tap/ingress protocol — all
memory-safe Rust, fuzzed. The carrier-SIP interop tail lives **outside the trust boundary** (the
provider's, or an out-of-tree SBC). The concession vs. 0003: layer 2 reintroduces C — but it is *not
first-party*, *not on the per-call hot path inside the boundary*, and is widely-deployed, battle-tested
infra the operator runs at the edge. rutster removes SIP from its own attack surface entirely rather
than rewriting it safely.
## Consequences
- **Positive:** the solo-years redirect from a trunk-SIP rewrite to the white space (contact-center
domain + reflex/eval loop + the tap-as-open-protocol); no first-party SIP attack surface; no
bus-factor-1 dependency on a young Rust SIP stack; a real phone number becomes a small rented
integration, not the riskiest step; first-call still never blocks on SIP.
- **Negative:** PSTN data-ownership becomes a layer-2 graduation rather than a day-one property (the
pitch must be re-scoped honestly); a CPaaS dependency for layer-1 PSTN; the "memory-safe SIP parser"
talking point is retired.
- **Mitigation:** provider-agnostic media-leg ingress at a config seam; the sovereignty wedge is
preserved as the out-of-tree SBC path; the frontier is demoed end-to-end on WebRTC regardless of
trunk choice.
## Relationship to ADR-0001 and ADR-0003
This re-converges with [ADR-0001](0001-sip-strategy.md)'s correct instinct — *don't own the interop
tail; rent the hardened edge* — while keeping [ADR-0003](0003-sip-rust-native-trunk.md)'s correct
instinct — *don't front your own media plane with a double-terminating shield.* The synthesis: the
rented transport delivers **clean media** straight to the tap/ingress, so there is neither a
first-party SIP tail (0001's worry) nor a second in-boundary media termination (0003's worry). 0001
rented an SBC *in front of* rutster's own media plane; 0007 rents transport that *replaces* the need
for a first-party trunk entirely, with WebRTC + the tap as the media spine.
## References
- [ADR-0003](0003-sip-rust-native-trunk.md) — superseded (Rust-native trunk SIP)
- [ADR-0001](0001-sip-strategy.md) — the original "rent the edge" instinct this re-converges with
- [ADR-0002](0002-north-star-and-fused-core.md) — fused per-call vertical (the boundary this re-scopes)
- [ADR-0006](0006-ingress-posture.md) — WebRTC-first ingress; out-of-tree edge-adapter pattern (applied here to trunks)