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.'
14 KiB
Agent Guide: rutster
Rutster is the open-source engine for building the AI-era contact center — self-hostable,
AI-native, memory-safe Rust. A framework/engine (not a turnkey product); a spiritual successor to
Asterisk's place in the world, not its protocols or architecture. See README.md
for the full vision and docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md
for the active build target.
This file orients any agent (human, AI, hybrid) working in the repo.
Project structure (current + planned)
rutster/
├── README.md # vision, persona, wedge, capability ladder
├── AGENTS.md # this file
├── LEARNING.md # (planned) index of "to learn concept X, read file Y"
├── Cargo.toml # (planned) [workspace] manifest
├── deny.toml # (planned) cargo-deny config
├── rust-toolchain.toml # (planned) pinned stable
├── crates/ # (planned) workspace members, ADR-0002-fused-vertical shape
│ ├── rutster/ # binary: axum signaling server + media driver + static page
│ ├── rutster-media/ # str0m WebRTC + Opus<->PCM codec boundary
│ ├── rutster-call-model/ # the Channel/Leg object embryo
│ ├── rutster-trunk/ # stub until spearhead step 5
│ ├── rutster-tap/ # stub until spearhead step 2
│ └── rutster-spend/ # stub until spearhead step 6
├── fuzz/ # (planned) placeholder cargo-fuzz harness dir
├── docs/
│ ├── ARCHITECTURE.md # fused per-call vertical + composable platform
│ ├── PORT_PLAN.md # capability checklist + thin-slice phasing
│ ├── adr/ # Architecture Decision Records (read before design work)
│ └── superpowers/specs/ # design specs (brainstorming → plan → implementation)
└── .github/workflows/ci.yml # (planned) fmt, clippy -D warnings, test --all, cargo deny check
Items marked (planned) are not yet on disk; they land with slice-1 implementation. Until then the repo is docs-only.
Build / lint / test commands
Rust (when the workspace exists):
cargo fmt --check # formatting check (CI gate)
cargo clippy -- -D warnings # lints (CI gate; warnings = failures)
cargo test --all # all unit + integration tests across the workspace
cargo deny check # licenses, advisories, bans, sources (CI gate)
cargo doc --no-deps --open # render the API docs (slice 1 heavily commented for learners)
Per-crate iteration:
cargo test -p rutster-media # one crate's tests
cargo test -p rutster-media -- --nocapture # see println! output
cargo run -p rutster # run the binary (axum on 0.0.0.0:8080)
RUST_LOG=rutster=debug cargo run # verbose tracing
Docs-only iterations (current state):
# validate markdown links + structure
ls docs/adr/ docs/superpowers/specs/
There is no Python, no Node, no Docker in the dev loop for slice 1. The batteries-included
compose up is a later-rung concern (lands with Valkey + trunk).
Code style (Rust)
Formatting & linting
cargo fmtis the single source of truth for whitespace/indentation. Don't hand-format.clippy -D warningsis the lint bar. CI fails on any warning. Fix the code, don't suppress with#[allow]unless the rationale is documented inline.
Naming
snake_casefor functions, methods, variables, modules, crates.PascalCasefor types (struct, enum, trait).UPPER_SNAKE_CASEfor constants.newtypewrappers over primitives for type-safety (e.g.ChannelId(Uuid), not bareUuid) — seerutster-call-model. The pattern prevents mixing up aChannelIdwith aSessionIdat the type system level.
Error handling
- Cold path (signaling, setup, request handlers):
thiserror-derived error enums,?propagation, converted to HTTP status codes at the axum boundary. - Hot path (the 20 ms media loop): never
?-propagate. Match-and-continue. A dropped packet must not terminate the peer. Policy: "drop + observe (log + counter), don't crash." This is the posture the eventual fuzz harness will test against. - Never
unwrap()/expect()outside tests or const-initialization contexts. Use?or explicit match.
Async & concurrency
- tokio for the control plane and for slice-1 media polling (acknowledged deviation from ARCHITECTURE.md, which mandates dedicated timing threads — see slice-1 spec §3.4).
Arc<Mutex<T>>for short-held shared state; preferArc<RwLock<T>>only when reads dominate writes. Comment the choice inline (it's a learner-facing item).- Sans-IO design where the slice-1 spec calls for it (str0m
Livepolling). The code comments explain why: a sans-IO component is one that takes input via method calls and produces output via return values, never touching IO directly — making it fully testable without a network.
Documentation comments (learner-facing — important)
This project overrides the default "no comments" convention. The user is learning Rust from this codebase. Slice 1 carries thorough educational comments:
//!module docs at the top of everylib.rs/main.rs/ sub-module: what the module does, why it exists in the architecture (cross-ref the relevant ADR / PORT_PLAN row), key types.///item docs on every public struct / enum / fn / trait: purpose + short example where non-obvious. Must render correctly incargo doc.//inline comments on the mechanism, not the what — whyPin<Box<dyn Future>>instead ofasync fn, whyArc<Mutex<...>>vsArc<RwLock<...>>, whatPhantomDatais doing, why anenumwas chosen over astructwith akindfield. Aim: a Rust learner reads the comment and learns a specific Rust concept they wouldn't have inferred from the code alone.- str0m-specifics flagged: every str0m interaction gets a comment explaining what str0m is doing and why we drive it that way.
- Ownership / borrowing decisions called out the first time each non-obvious pattern appears.
This verbosity is a deliberate trade-off: more tokens to skim now, compound educational value later. Once a pattern is established and the reader has learned it, later slices can be sparser on the well-trodden patterns.
Terminology policy (inclusive language)
Avoid authoritarian / exclusionary terms in our own code, prose, identifiers, and endpoint names. Use equally-descriptive alternatives:
| Avoid | Use instead |
|---|---|
| police / policing (the verb) | enforce / gate / guard |
| master / slave | primary / replica, leader / follower, controller / worker |
| blacklist / whitelist | denylist / allowlist, blocklist / safelist |
| officer | operator / handler / controller |
| censor | suppress / filter |
Exception: protocol-convention names are kept verbatim when they come from upstream
specs or libraries we depend on — replacing them would hurt the educational mapping to
upstream documentation. Concretely, ICE (Interactive Connectivity Establishment,
RFC 8445) stays: it's the protocol name in str0m::ice, RTCIceCandidate, and the cargo
crate ecosystem. Our prose around it can say "NAT traversal" / "connectivity candidates"
where that reads better, but identifiers and protocol-level references keep ICE.
Same logic for any future RFC-defined acronym.
Architecture pre-reading (required before design work)
Before proposing changes to the architecture, read in this order:
README.md— north star, persona, wedge, capability ladder (10 min).docs/ARCHITECTURE.md— fused per-call vertical + composable platform, the agent tap as central interface (15 min).docs/PORT_PLAN.md— capability checklist + disposition per subsystem + thin-slice phasing (20 min).docs/adr/— every ADR. Load-bearing decisions, not optional reading:- ADR-0002 — north star + fused vertical
- ADR-0003 —
Rust-native trunk SIPsuperseded by ADR-0007 - ADR-0004 — GPL-3.0-or-later
- ADR-0005 — Valkey as bus + state store
- ADR-0006 — WebRTC-first ingress
- ADR-0007 — rent the trunk transport; no first-party SIP stack
- ADR-0008 — the FOB / green-zone build-vs-reuse doctrine
docs/superpowers/specs/— design specs in flight. Read the latest one to know what's currently being built and what's explicitly deferred.- 2026-06-26 vision-revision — the pressure-test that produced the current architecture.
- 2026-06-28 slice-1 WebRTC loopback — the active build target.
The slice-1 spec's §1.2 out-of-scope table is the single source of truth for "is X done?" and "why isn't X here?" questions. Consult it before adding anything.
Key decisions to respect
- The FOB / green-zone doctrine (ADR-0008) — the build-vs-reuse rule. Build it in the memory-safe Rust FOB only if it's hot-path, security-constitutive, or differentiating; otherwise reuse trusted, actively-maintained OSS in the green zone, at arm's length (its own process / container / trust domain). 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.
- License:
GPL-3.0-or-lateron every crate manifest (ADR-0004). Strong copyleft in the Asterisk lineage. Don't introduce deps that conflict (cargo deny check licensesenforces). - WebRTC stack:
str0m(sans-IO). Notwebrtc-rs. Chosen because the sans-IO design maps directly onto ARCHITECTURE.md's "dedicated timing threads, not the shared tokio pool." - Workspace shape: full ADR-0002-fused-vertical layout. Stub crates are explicitly
permissibly empty (
lib.rswith doc comment + acrate_compiles()test). They lock boundaries, not anticipate code. - Agent tap posture: core-as-client, brain-as-server. No inbound tap port on the core. Tap = egress; ingress = inbound (WebRTC) — opposite security postures, never unified (ADR-0006). Don't blur this line.
- In-boundary spend/abuse control is constitutive of the wedge (ADR-0002). Pulling it out into a service re-introduces the 3-vendor structural hole. Don't externalize it.
- Fused per-call vertical — the control↔media gRPC hop on the per-call hot path is removed by design (ADR-0002). Don't re-introduce it.
- No WASM in the core story (ADR-0002 demoted it). The agent tap is the extension point for in-call logic.
Git workflow
- Trunk-based development (target, once branch protection is in place):
- Branch from
mainfor any change. - Open a PR targeting
main. - CI gates:
cargo fmt --check,cargo clippy -- -D warnings,cargo test --all,cargo deny check. All must pass before merge. - Squash-merge to keep
mainlinear.
- Branch from
- Never push directly to
main. Branch protection (planned) will enforce; until then, self-discipline. - Commit messages: imperative mood, subject ≤ 72 chars, body wraps at 72, blank line
between subject and body. Reference ADRs / specs by number when relevant. Match the style
of recent commits (
git log --oneline -10). - Atomic commits: one logical change per commit. Doc ratifications, code, and tests each land as separate commits when practical. Don't bundle unrelated work.
- Never commit secrets. The
.gitignorealready covers.env*,*.pem,*.key. If a new secret pattern appears, extend.gitignorein the same commit.
Slice-1 boundaries — what NOT to add (yet)
These are explicitly deferred per the slice-1 spec's out-of-scope table. Adding them NOW would break the sequencing that the spearhead depends on:
- ❌ Dedicated timing thread for the media loop (deferred to step 4, barge-in)
- ❌ TLS on the HTTP signaling surface (deferred to step 5, rented-transport PSTN)
- ❌ Authn / authz / multi-tenancy on
/v1/sessions(deferred to step 6, spend cap) - ❌ Trickle ICE (deferred until NATs demand it)
- ❌ The tap itself (deferred to step 2 — slice 1 only pre-paves the seam)
- ❌ The brain / STT / LLM / TTS (deferred to step 3)
- ❌ Barge-in / VAD-driven playout kill (deferred to step 4)
- ❌ PSTN via rented transport / CPaaS media-leg ingress (deferred to step 5; no first-party SIP — ADR-0007)
- ❌ Spend cap / abuse gate (deferred to step 6)
- ❌ Browser-based automated e2e tests / Selenium / Playwright (deferred post-slice-1)
- ❌ Docker / compose (deferred to a later rung)
- ❌ Event bus / Valkey / CDR emission (deferred to step 5)
- ❌ Transfer / park / pickup / barge features (deferred to escalation rung 2)
If an agent proposes adding any of these in slice 1, the right answer is "no, see the slice-1 spec §1.2."
What's next
The active task is implementing slice 1 per
docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md.
The brainstorming phase is complete; the next step is the implementation plan (via the
writing-plans skill), then TDD execution.