diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a445421 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,259 @@ +# 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`](README.md) +for the full vision and [`docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md`](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-signaling-sip/ # 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):** + +```bash +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:** + +```bash +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):** + +```bash +# 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 fmt` is the single source of truth for whitespace/indentation. Don't hand-format. +- `clippy -D warnings` is the lint bar. CI fails on any warning. Fix the code, don't suppress + with `#[allow]` unless the rationale is documented inline. + +### Naming + +- `snake_case` for functions, methods, variables, modules, crates. +- `PascalCase` for types (struct, enum, trait). +- `UPPER_SNAKE_CASE` for constants. +- `newtype` wrappers over primitives for type-safety (e.g. `ChannelId(Uuid)`, not bare `Uuid`) — + see `rutster-call-model`. The pattern prevents mixing up a `ChannelId` with a `SessionId` at + 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>` for short-held shared state; prefer `Arc>` 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 `Live` polling). 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 every `lib.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 in `cargo doc`. +- `//` inline comments on the *mechanism*, not the what — why `Pin>` instead of + `async fn`, why `Arc>` vs `Arc>`, what `PhantomData` is doing, why an + `enum` was chosen over a `struct` with a `kind` field. 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: + +1. [`README.md`](README.md) — north star, persona, wedge, capability ladder (10 min). +2. [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — fused per-call vertical + composable platform, + the agent tap as central interface (15 min). +3. [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md) — capability checklist + disposition per subsystem + + thin-slice phasing (20 min). +4. [`docs/adr/`](docs/adr/) — every ADR. Load-bearing decisions, not optional reading: + - [ADR-0002](docs/adr/0002-north-star-and-fused-core.md) — north star + fused vertical + - [ADR-0003](docs/adr/0003-sip-rust-native-trunk.md) — Rust-native trunk SIP (no SBC) + - [ADR-0004](docs/adr/0004-license.md) — GPL-3.0-or-later + - [ADR-0005](docs/adr/0005-event-bus.md) — Valkey as bus + state store + - [ADR-0006](docs/adr/0006-ingress-posture.md) — WebRTC-first ingress +5. [`docs/superpowers/specs/`](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](docs/superpowers/specs/2026-06-26-vision-revision-design.md) — + the pressure-test that produced the current architecture. + - [2026-06-28 slice-1 WebRTC loopback](docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md) — + 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 + +- **License:** `GPL-3.0-or-later` on every crate manifest (ADR-0004). Strong copyleft in the + Asterisk lineage. Don't introduce deps that conflict (`cargo deny check licenses` enforces). +- **WebRTC stack:** `str0m` (sans-IO). Not `webrtc-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.rs` with doc comment + a `crate_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): + 1. Branch from `main` for any change. + 2. Open a PR targeting `main`. + 3. CI gates: `cargo fmt --check`, `cargo clippy -- -D warnings`, `cargo test --all`, + `cargo deny check`. All must pass before merge. + 4. Squash-merge to keep `main` linear. +- **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 `.gitignore` already covers `.env*`, `*.pem`, `*.key`. If + a new secret pattern appears, extend `.gitignore` in 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, PSTN trunk) +- ❌ 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 trunk / SIP client (deferred to step 5) +- ❌ 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`](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.