Files
rutster/docs/PORT_PLAN.md
Aaron D. Lee da6b7c9ff0
All checks were successful
CI / fmt (push) Successful in 1m5s
CI / clippy (push) Successful in 1m53s
CI / test (1.85) (push) Successful in 5m54s
CI / test (stable) (push) Successful in 6m36s
CI / deny (push) Successful in 2m12s
adr: +0009 spend-gate rescope +0010 spearhead 4.5 benchmark/sim; amend 0004
Maintainer-ratified from the 2026-07-03 review (R1-R3):
- ADR-0009: retire "structurally impossible for a 3-vendor stack" post-0007;
  restate the gate's true guarantees (credential isolation, unskippable
  mediation, media-plane enforcement, audit co-location). Propagated to
  README pillar 3, ARCHITECTURE, PORT_PLAN s10; amendment note on ADR-0002.
- ADR-0010: insert step 4.5 (benchmark + simulation harness, rutster-sim
  seed, CI-regressed p50/p99 + kill-time) after barge-in; pull rung-2
  escalation ahead of steps 5-6. Spearhead lists updated.
- ADR-0004: delete the legally-broken AGPL escape hatch; GPL-3.0-or-later
  permanent, no CLA; tap protocol/SDKs intended permissive (future ADR).
- README: add brain-vendor-direct competitor row (review D2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vNe64BBDkgo5oQVkBa3XF
2026-07-04 00:00:57 -04:00

22 KiB
Raw Blame History

Rutster — Asterisk Capability Checklist

This is the capability checklist, not the architecture spine. It walks the Asterisk subsystem surface as a completeness audit — "what must a telephony system handle" — and assigns every capability a disposition in the rutster architecture, with a one-line rationale. We keep the checklist; we reject the Asterisk channel/bridge/dialplan model as the architecture (see ADR-0002). The crate/service boundaries fall out of the "Core / Service" rows; the threat model hangs off the "attack surface" of each row.

Grounded against the Asterisk 22.10.1 tree (~1.18M LOC C/H).

Disposition legend

Disposition Meaning
🦀 Core In the hardened, memory-safe Rust core. Reserved for the hot media path, call model, and signaling glue — fast, always-present, trusted.
🧩 Plugin (WASM) Sandboxed extension. Logic that benefits from isolation and third-party extensibility. Cannot crash the core or cross tenant boundaries.
☁️ Service Independent microservice. Stateful, independently-scaled, or business/billing logic. Talks over API + event bus.
🔌 Edge / FFI Wrap mature external C (pjproject/spandsp) or front with an SBC. Used where a pure-Rust rewrite is a tar pit.
Drop Legacy hardware or dead protocol. Gone.
🔁 Replace Capability survives but the mechanism is entirely redesigned (no 1:1 mapping).

Design rules (the heuristics behind every row)

  1. Hot media path & the call model → Core. Trusted, fast, always-on. Inside the fused per-call vertical.
  2. Anything that benefits from isolation or third-party extension → Plugin (now softened — WASM demoted out of the core story per ADR-0002; the 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) → rent it, don't own it. ADR-0007 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.

1. Signaling & channel drivers (channels/, res/res_pjsip*)

Asterisk subsystem Module(s) Disp. Rationale
SIP signaling (trunk) chan_pjsip + res_pjsip* (48 mods) 🔌 Rented / out-of-tree Re-decided (ADR-0007, 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), 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), so it's green-zone, out-of-tree, never first-party (ADR-0007): 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.
↳ 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.
↳ pub/sub (presence, MWI, BLF) res_pjsip_pubsub, _exten_state, _mwi 🦀 Core + ☁️ Signaling in core; presence/MWI state aggregation as a service.
↳ T.38 fax res_pjsip_t38 ☁️ Service Isolate fax entirely (see §5).
WebRTC signaling (agent ingress) chan_websocket, res_http_websocket 🦀 Core First-party human-participant ingress (ADR-0006). The browser is the softphone: SSO in, one peer-connection, DTLS-SRTP + ICE handled by the browser, zero device-provisioning tail. Also the modern hosted UX and the escalation path (rung 2): agent clicks take this call → WebRTC leg + audiohook/barge handoff. WSS + DTLS-SRTP via the media stack.
External media chan_audiosocket 🦀 Core Modern, relevant pattern (stream media to an external process); keep as a media tap/source API.
Local console audio chan_console 🧩 Plugin Dev/testing convenience; not core.
RTP-only pseudo-channel chan_rtp, chan_bridge_media 🦀 Core Media-plane primitives.
IAX2 chan_iax2 Drop Dead inter-Asterisk protocol. SIP trunks instead.
TDM / analog / ISDN chan_dahdi, sig_analog, sig_pri, sig_ss7 Drop No hardware era. PSTN via SIP trunk / SBC.
H.323 addons/chan_ooh323 Drop Dead protocol.
SCCP / Skinny, MGCP (Skinny/MGCP) Drop Dead protocols.
Unistim chan_unistim Drop Dead vendor protocol.
XMPP / Jingle chan_motif, res_xmpp Drop Dead path.

2. Media plane (main/rtp_engine.c, res_rtp_asterisk, bridges/, main/dsp.c)

Asterisk subsystem Module(s) Disp. Rationale
RTP / RTCP engine rtp_engine.c, res_rtp_asterisk 🦀 Core Crown jewel. Built on str0m/webrtc-rs, sans-IO, dedicated timing (not the shared tokio pool).
SRTP / DTLS res_srtp 🦀 Core Mandatory-by-default encryption.
Bridging framework main/bridge.c 🦀 Core The 2-party + N-party call topology engine.
↳ softmix conference mixer bridges/bridge_softmix 🦀 Core N-party mixing on the hot path.
↳ simple / native-RTP bridge bridge_simple, bridge_native_rtp 🦀 Core Direct media flow (re-INVITE peers together) when no mixing needed.
↳ holding bridge bridge_holding 🦀 Core Parking/hold media state.
↳ native DAHDI bridge bridge_native_dahdi Drop Hardware.
DSP (DTMF/MF detect, silence, tone) main/dsp.c 🦀 Core Media-plane primitive; exposed to plugins as events.
Echo cancellation (hardware / sw) 🔌 Edge/FFI No mature pure-Rust EC; FFI speexdsp / WebRTC APM.
Audio hooks (tap/inject/volume) main/audiohook.c 🦀 Core Primitive behind recording, ChanSpy, whisper/barge.
Music on hold res_musiconhold 🦀 Core Just a media source; small, keep in core (asset from object storage).
Jitter buffer func_jitterbuffer, abstract jb 🦀 Core Quality-critical; part of the RTP path.

3. Call model & PBX core (main/)

Asterisk subsystem Module(s) Disp. Rationale
Channel abstraction main/channel.c (ast_channel) 🦀 Core The unifying "leg" object (signaling + media state). Reimagined as a Channel/leg.
PBX / dialplan engine main/pbx.c 🔁 Replace → declarative routing (data) + event-driven apps. No extensions.conf.
Call features (transfer/park/pickup) res_features, res_parking 🦀 Core Call-control primitives, exposed via the API.
Scheduler / taskprocessor / threadpool main/sched.c, taskprocessor.c, threadpool.c 🦀 Core tokio for control; dedicated timing threads + timer wheel for the 20ms media loop.
Internal message bus (Stasis) main/stasis*.c 🔁 Replace In-core dispatcher for intra-core; external event bus (NATS/Kafka) for cross-service.
Module loader main/loader.c 🔁 Replace → WASM host + service registry. No in-process .so.
ACL / named ACL main/acl.c, res_named_acl 🦀 Core Security primitive; policy-driven.
astdb (internal KV) main/db.c 🔁 Replace → state store (Valkey KV, ADR-0005).

4. Dialplan & programmability (pbx/, res_agi, res_ari, manager)

Asterisk subsystem Module(s) Disp. Rationale
extensions.conf dialplan pbx_config 🔁 Replace Declarative routing as data (GitOps) for the 90% case.
AEL / Lua dialplan pbx_ael, pbx_lua Drop Replaced by WASM call-flow + routing config.
Realtime dialplan pbx_realtime 🔁 Replace Routing in the config/state layer.
Call files / spool pbx_spool 🔁 Replace → originate via API.
Macro / Gosub / Stack app_stack, app_macro 🔁 Replace → WASM call-flow logic.
AGI / FastAGI res_agi 🔁 Replace → WASM plugins + gRPC. Same "external program controls a call," sandboxed and typed.
ARI (REST + stasis apps) res_ari*, res_stasis* 🦀 Core Becomes THE API — the control-plane resource model. api-docs/*.json is the reusable spec.
AMI (manager) main/manager*.c 🔁 Replace Drop the plaintext TCP protocol → mTLS gRPC + RBAC + audit log.
CLI main/cli.c 🔁 Replace Thin CLI that's an API client, not an in-process console.
DUNDi (distributed routing) pbx_dundi Drop Dead/niche.
ENUM lookup func_enum 🧩 Plugin Routing-time lookup; pluggable.

5. Applications (apps/) — the contact-center domain

Scope lift (vision-revision §4): the contact-center capabilities Asterisk kept as apps are rutster's core domain, not peripheral. This is the difference between "Asterisk successor" and "AI-era contact-center engine." Every row here is load-bearing for the wedge (the thing LiveKit will never ship).

Asterisk app Module Disp. Rationale
Dial / originate app_dial, app_originate 🦀 Core Core call origination, exposed via the API.
Queues / ACD app_queue ☁️ Service (domain core) Stateful, independently scaled. First-class — ACD is the beating heart of a contact center, not a peripheral app. The AI self-serve + human-escalation loop (capability ladder) is an ACD-shaped problem.
Voicemail app_voicemail ☁️ Service Stateful storage + business logic; isolate it.
Conferencing app_confbridge 🦀 Core + ☁️ Mixing in core (softmix); orchestration/features as a service.
MeetMe app_meetme Drop DAHDI-timing legacy; ConfBridge supersedes.
Recording app_mixmonitor, app_monitor 🦀 Core + ☁️ Audiohook tap in core; storage/lifecycle as a recording service (object-storage-backed, retention policy, tenant-scoped — durable, not on the bus).
ChanSpy / whisper / barge app_chanspy 🦀 Core Audiohook primitive; gated by RBAC. The escalation primitive (rung 2).
Answering-machine detect app_amd 🦀 Core + 🧩 DSP primitive in core; policy/thresholds as a plugin.
IVR primitives (playback/read/say) app_playback, app_read, app_sayunixtime, app_directory 🦀 Core Media primitives (play/collect-digits/say) exposed via the API/tap. The IVR flow dissolves into the agent; these are the primitives left over.
Echo / test app_echo, app_milliwatt 🦀 Core Trivial diagnostics.
FollowMe app_followme 🧩 Plugin Routing policy → plugin/service.
Speech recognition app_speech_utils, res_speech ☁️ Service Modern ASR is external; expose as a service integration.
Fax res_fax, res_fax_spandsp (T.38) ☁️ Service (FFI) Niche, isolate fully; spandsp via FFI. Optional.
Supervisor / agent-state (scattered manager/AMI surfaces) ☁️ Service (domain core) First-class. Agent presence, state, real-time monitoring, coaching/whisper — the contact-center operator surface. Pure API client (the dashboard).
Dialer (predictive/progressive) (custom/app_dial-derived) ☁️ Service (domain core) First-class. Outbound campaign dialing with pacing — where the in-boundary spend/abuse gate (§10) bites hardest; structurally co-located.

6. Telephony features / subscriptions

Asterisk subsystem Module(s) Disp. Rationale
Presence / BLF res_pjsip_pubsub, res_pjsip_exten_state 🦀 Core + ☁️ Signaling in core; state aggregation service.
MWI (message-waiting) res_mwi_external, app_voicemail MWI ☁️ Service Tied to the voicemail service.
STIR/SHAKEN (attestation) res_stir_shaken ☁️ Service Keep — modern, anti-robocall/fraud. Core to the security story.
Call recording lifecycle app_mixmonitor + storage ☁️ Service Object-storage backed, retention policy, tenant-scoped.
CDR (call detail records) cdr/, cdr_* ☁️ Service Event stream → billing/analytics pipeline.
CEL (channel event logging) cel/, cel_* ☁️ Service Folded into the unified event bus + analytics.

7. Codecs & formats (codecs/, formats/)

Codec / format Module Disp. Rationale
G.711 a-law / µ-law codec_alaw, codec_ulaw, codec_a_mu 🦀 Core Trivial, ubiquitous. Native Rust.
G.722 (HD) codec_g722 🦀 Core Common wideband. Native Rust.
Opus (external codec_opus) 🦀 Core (FFI) WebRTC default; libopus via FFI.
Sample-rate resample codec_resample 🦀 Core Needed for any transcoding/mixing.
GSM codec_gsm 🧩 Plugin (FFI) Optional; FFI libgsm.
G.726 / G.729 codec_g726 / (external) 🧩 Plugin (FFI) Optional; G.729 now patent-free.
Speex / iLBC / LPC10 / ADPCM / codec2 codec_speex, codec_ilbc, … Drop Obsolete; drop or community plugin.
Hardware transcode codec_dahdi Drop Hardware.
File formats (wav/sln/ogg-opus) format_wav, format_sln, … 🦀 Core Small set for prompts/recordings in core; rest as plugins.
Legacy file formats (gsm/g729/g726 files) format_* 🧩 Plugin Optional.

8. Config, state & data (res_config_*, res_sorcery*)

Asterisk subsystem Module(s) Disp. Rationale
.conf files main/config.c 🔁 Replace Config-as-data (declarative, validated, GitOps).
Sorcery (object config abstraction) res_sorcery* 🔁 Replace Typed config + state-store layer.
Realtime backends (ODBC/PgSQL/SQLite/cURL/LDAP) res_config_* 🔁 Replace One config/state service with pluggable backends.
HTTP media cache res_http_media_cache 🦀 Core Prompt/asset fetch → object storage + cache.
Security event logging res_security_log 🦀 Core Structured audit log; security-critical.

9. Management & APIs

Asterisk surface Module(s) Disp. Rationale
ARI (REST resource model + event WS) res_ari* 🦀 Core The control-plane API. Reuse the resource model.
AMI (plaintext TCP admin) manager*.c 🔁 Replace → mTLS gRPC + RBAC + audit. No plaintext admin channel.
HTTP server res_http* 🦀 Core TLS-only; axum/hyper.
CLI cli.c 🔁 Replace API-client CLI.

10. New subsystems (no Asterisk equivalent)

These are the "modern" deltas — load-bearing, not optional polish.

Subsystem Disp. Purpose
WASM plugin host 🧩 Demoted (candidate) Out of the core story (ADR-0002). 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, re-scoped by ADR-0009). Spend caps, pacing caps, deny-by-default routing, rate limits, pattern detection — co-located with the tap + the provider call-control client inside the boundary: the brain never holds provider credentials, the gate is unskippable on everything rutster mediates (which is everything the brain can do), and pacing/playout are enforced over media the core terminates (ADR-0007). Not a physical wire monopoly — deployments pair it with provider-side caps. 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): 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.
SBOM / supply chain cargo-deny, SBOM generation, reproducible builds.
Fuzzing harness Continuous fuzzing of every wire parser (SIP/SDP/RTP).

Phasing → the thin-slice first proof + capability ladder

The old Phase 04 capability-rolldown is replaced (see vision-revision §1011) by a brutally thin vertical slice that proves the combination (the wedge) first, then a capability ladder that grows the contact center feature by feature. The spearhead proves the hardest, most-differentiating part before trunk integration piles on.

The thin-slice first proof (spearhead)

  1. WebRTC media loopback — terminate RTP/SRTP, echo audio to a browser. Proves the media core.
  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.
    • 4½ — Benchmark + simulation harness (ADR-0010): synthetic-caller ingress drives p50/p99 mouth-to-ear + barge-in kill-time under concurrent load, CI-regressed. Proves the reflex measurably; seeds rutster-sim.
  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).
  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. Post-review re-sequencing (ADR-0010): after step 4½, rung-2 escalation is pulled forward ahead of steps 56.

The capability ladder (the grand vision, incrementally)

Rung Capability Reuses
1 Self-serve — AI answers, contains the call the thin-slice first proof
2 Escalation — human agent barges in / takes over when AI breaks down the audiohook/barge primitive (§5)
3 Measurement — containment rate, where/why AI failed CDR + analytics on calls you own
4 Self-improvement — every takeover → auto-labeled training data → loop rungs 13 compounding

Open decisions

  • SIP: FFI pjproject vs. SBC front vs. pure-Rust. Decided — ADR-0003: Rust-native trunk SIP. Re-decided — ADR-0007: 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.
  • WASM runtimewasmtime component model, if WASM is retained at all (demoted to candidate per ADR-0002). Uncoupled from the critical path.
  • Plugin mechanism — WASM vs. webhooks vs. scripting for community call-flow/routing extensions. Deferred.
  • Agent-tap protocol — presumptively WSS + core-as-client + clean PCM + core-authoritative playout (see ARCHITECTURE.md, "Agent tap"). To harden against the thin-slice first proof, not a decided ADR yet.