Files
rutster/AGENTS.md
adlee-was-taken 504d0020f4 docs: add AGENTS.md for opencode
Project orientation for any agent (human/AI/hybrid) working in the repo.
Covers: project structure (current + planned), build/lint/test commands,
Rust code style (formatting, naming, error handling, async/concurrency,
learner-facing documentation comments), terminology policy (inclusive
language + ICE-as-protocol-convention exception), architecture
pre-reading order, key decisions to respect, git workflow, and the
slice-1 boundaries (what NOT to add yet).
2026-06-28 09:34:04 -04:00

13 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-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):

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 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<Mutex<T>> for short-held shared state; prefer Arc<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 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<Box<dyn Future>> instead of async fn, why Arc<Mutex<...>> vs Arc<RwLock<...>>, 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 — north star, persona, wedge, capability ladder (10 min).
  2. docs/ARCHITECTURE.md — fused per-call vertical + composable platform, the agent tap as central interface (15 min).
  3. docs/PORT_PLAN.md — capability checklist + disposition per subsystem + thin-slice phasing (20 min).
  4. docs/adr/ — every ADR. Load-bearing decisions, not optional reading:
  5. docs/superpowers/specs/ — design specs in flight. Read the latest one to know what's currently being built and what's explicitly deferred.

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. The brainstorming phase is complete; the next step is the implementation plan (via the writing-plans skill), then TDD execution.