docs: propagate ADR-0007/0008 to README, ARCHITECTURE, PORT_PLAN, AGENTS

ADR-0007 (rent the trunk) + ADR-0008 (FOB/green-zone doctrine) propagation
into the narrative docs that orient contributors + readers. No code changes.

README:
- Wedge bullet 2: 'one secure auditable boundary' now lists media + local
  reflexes + spend + tap + audit (was: trunk termination + media + spend).
  Adds the honest caveat that PSTN media inside the boundary is the on-prem
  *graduation* (ADR-0007), not a day-one claim.
- 'Is / isn't': 'isn't a TDM/PSTN-hardware PBX — *and not a SIP stack.*'
  No first-party SIP (ADR-0007). PSTN reach is rented transport.
- Memory-safety pillar: 'rutster parses no SIP at all' — entire first-party
  wire surface is WebRTC/RTP/SRTP + WebSocket tap/ingress, all memory-safe
  Rust. The carrier-SIP interop tail lives outside the trust boundary.
- Spearhead step 5: 'Add a real phone number via rented transport' (was:
  'Replace WebRTC ingress with a real PSTN trunk call'). Re-aims at the
  AI-telephony frontier; no first-party SIP stack.
- References: 0007 supersedes 0003 in the highlighted ADR list.

ARCHITECTURE:
- New 'FOB and the green zone' section after the fused-vertical framing —
  names the build-vs-reuse doctrine with the FOB member list + green-zone
  member list, and restates memory-safety precisely ('FOB is 100%
  memory-safe Rust; the green zone is trusted OSS kept outside the
  boundary — not an over-claim that every byte is Rust').
- 'Inside the boundary': 'Carrier trunk — rented transport, not first-party'
  replaces 'Trunk SIP termination — Rust-native.' PSTN audio arrives as
  a media-leg ingress from a rented CPaaS raw-media fork or an out-of-tree
  SBC for on-prem sovereignty.
- Biggest technical risk: 'No longer the SIP stack — because rutster no
  longer builds one.' Retires ADR-0003's named schedule risk; redirects
  to 'the reflex loop itself' (turn-taking, VAD-driven barge-in, jitter,
  pacing) — which is also the differentiator.

PORT_PLAN:
- Design rule 4: 'rent it, don't own it' replaces ADR-0003's 'Rust-native
  trunk SIP, no SBC shield.' ADR-0007 restores the rule's original instinct.
- SIP signaling (trunk) row: disposition flipped to 🔌 Rented / out-of-tree.
- Outbound registration row: disposition flipped to 🔌 Rented / out-of-tree
  (handled by the rented transport or out-of-tree SBC; rutster parses no SIP).
- Spend/abuse engine row: 'co-located with call origination + the tap inside
  the boundary' (was: 'co-located with trunk termination'). rutster mediates
  both the provider call-control API and the brain tap — the brain never
  holds the wire.
- Spearhead step 5: 'Add a real phone number via rented transport.'
- Open decisions: SIP line updated to 'Re-decided — ADR-0007.'

AGENTS.md:
- ADR list: adds 0007 and 0008 with annotations; marks 0003 as superseded
  by 0007.
- Key decisions to respect: prepends the FOB/green-zone doctrine as the
  *the* build-vs-reuse rule. '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.'
This commit is contained in:
opencode controller
2026-06-29 20:26:51 -04:00
parent 3c3197e57a
commit 7616878bb1
4 changed files with 76 additions and 34 deletions

View File

@@ -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 14). 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.

View File

@@ -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 04 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 14 *are* the reflex loop. Keep the dev loop on WebRTC ingress until step 5.
@@ -239,7 +241,7 @@ Steps 14 *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.