Files
rutster/docs/PORT_PLAN.md
adlee-was-taken 075e984fb5 docs: ratify vision revision across README/ARCHITECTURE/PORT_PLAN + ADRs 0002-0006
Folds the 2026-06-26 vision-revision pressure-test into the canonical
docs. The spec moves from Proposed to Ratified; the decisions it produced
land in the docs it said it would amend, with ADRs recording each
load-bearing reversal.

- README.md: reframe north star (AI-era contact-center engine, not
  Asterisk successor); persona; revised pillars (add data-ownership,
  demote WASM, promote spend-control); update 'what it is/isn't'.
- ARCHITECTURE.md: replace three-plane framing with fused per-call
  vertical + composable horizontal platform; remove control<->media
  hot-path gRPC hop; make the agent tap the central interface; add
  DX spine + GUI-as-API-client + k8s declarative/operational model.
  Also: 'too slow to police' -> 'too slow to enforce' (terminology).
- PORT_PLAN.md: recharacterize as capability checklist (not template);
  graduate contact-center capabilities to first-class domain; Rust-native
  trunk SIP rows; WASM demoted; thin-slice + capability ladder phasing.
- ADR-0001: marked Superseded by ADR-0003.
- ADR-0002 (new): north star + fused per-call core.
- ADR-0003 (new): Rust-native trunk SIP, no SBC shield.
- ADR-0004 (new): GPL-3.0-or-later license.
- ADR-0005 (new): Valkey as event bus + state store.
- ADR-0006 (new): WebRTC-first ingress; SIP endpoint deferred.
- vision-revision spec: status -> Ratified 2026-06-26.
2026-06-28 09:33:29 -04:00

20 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) → Edge/FFI, with a pure-Rust ambition. Reversed for SIP by ADR-0003: Rust-native trunk SIP, no C SBC shield. 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) 🦀 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).
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.
↳ 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). 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.
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.
  5. Replace WebRTC ingress with a real PSTN trunk call — proves the trunk client (ADR-0003).
  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.

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, no SBC shield.
  • 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.