Merge remote-tracking branch 'origin/main' into slice-1-webrtc-loopback
# Conflicts: # README.md
This commit is contained in:
195
docs/DEVELOPMENT.md
Normal file
195
docs/DEVELOPMENT.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Development
|
||||
|
||||
The dev loop: how to iterate on the Rust workspace, run tests, and find
|
||||
your way around the codebase.
|
||||
|
||||
> **Status:** Slice 1 is the active build target. The workspace is
|
||||
> landing task-by-task on the `slice-1-webrtc-loopback` branch. Once it
|
||||
> merges to `main`, everything below applies directly. Until then,
|
||||
> `git checkout slice-1-webrtc-loopback` to follow along.
|
||||
> Implementation plan: [`docs/superpowers/plans/2026-06-28-slice-1-webrtc-loopback.md`](superpowers/plans/2026-06-28-slice-1-webrtc-loopback.md).
|
||||
|
||||
---
|
||||
|
||||
## Workspace layout
|
||||
|
||||
Cargo workspace at the repo root, shaped to ADR-0002's fused per-call
|
||||
vertical. One binary crate + five library crates.
|
||||
|
||||
```
|
||||
rutster/
|
||||
├── Cargo.toml # [workspace] + [workspace.dependencies]
|
||||
├── deny.toml # cargo-deny config (licenses/bans/sources)
|
||||
├── rust-toolchain.toml # pinned stable
|
||||
├── LEARNING.md # index of "to learn concept X, read file Y"
|
||||
├── crates/
|
||||
│ ├── rutster/ # binary: axum signaling server + media driver
|
||||
│ │ ├── src/main.rs
|
||||
│ │ ├── src/session_map.rs # DashMap<ChannelId, RtcSession>
|
||||
│ │ ├── src/routes.rs # HTTP routes
|
||||
│ │ └── static/index.html # browser test client
|
||||
│ ├── rutster-media/ # str0m WebRTC + Opus⇄PCM codec boundary
|
||||
│ │ ├── src/pcm.rs # PcmFrame, AudioSource/Sink traits, EchoAudioPipe
|
||||
│ │ ├── src/opus_codec.rs # OpusDecoder / OpusEncoder wrappers
|
||||
│ │ ├── src/rtc_session.rs # RtcSession (per-peer owner)
|
||||
│ │ └── src/loop_driver.rs # str0m poll loop
|
||||
│ ├── 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/ # placeholder cargo-fuzz dir (real harnesses: step 5)
|
||||
```
|
||||
|
||||
The three stub crates (`rutster-signaling-sip`, `rutster-tap`,
|
||||
`rutster-spend`) exist to lock the ADR-0002 boundary shape without
|
||||
anticipating code. Each is a `lib.rs` with a `//!` module doc comment
|
||||
describing what will land there and when. Don't fill them in early —
|
||||
see [`AGENTS.md`](../AGENTS.md) "Slice-1 boundaries — what NOT to add."
|
||||
|
||||
### Dependency direction
|
||||
|
||||
- `rutster` (binary) → `rutster-media`, `rutster-call-model`
|
||||
- `rutster-media` → `rutster-call-model`
|
||||
- `rutster-call-model` is a leaf (depends on nothing in the workspace)
|
||||
- The three stubs depend on nothing in slice 1
|
||||
|
||||
---
|
||||
|
||||
## Build / test / lint commands
|
||||
|
||||
The four CI gates — all must pass before merge. Run them locally before
|
||||
pushing:
|
||||
|
||||
```bash
|
||||
cargo fmt --check # formatting check
|
||||
cargo clippy -- -D warnings # lints (warnings = failures)
|
||||
cargo test --all # all unit + integration tests
|
||||
cargo deny check # licenses, advisories, bans, sources
|
||||
```
|
||||
|
||||
To auto-fix formatting + lint warnings:
|
||||
|
||||
```bash
|
||||
cargo fmt # apply formatting
|
||||
cargo clippy --fix # apply lint suggestions
|
||||
cargo clippy --fix --all-features # apply across all features
|
||||
```
|
||||
|
||||
### Per-crate iteration
|
||||
|
||||
When working on one crate, skip the rest for faster cycles:
|
||||
|
||||
```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
|
||||
cargo build -p rutster-call-model # type-check one crate
|
||||
cargo doc -p rutster-media --open # render one crate's docs
|
||||
```
|
||||
|
||||
### Render full API docs
|
||||
|
||||
```bash
|
||||
cargo doc --no-deps --open
|
||||
```
|
||||
|
||||
Every module has `//!` module docs explaining what it does + why it
|
||||
exists in the architecture. `cargo doc` is genuinely useful here, not
|
||||
just ceremony — the user is learning Rust from this codebase and slice 1
|
||||
carries thorough educational comments per spec §7.
|
||||
|
||||
---
|
||||
|
||||
## Running the binary
|
||||
|
||||
```bash
|
||||
cargo run -p rutster
|
||||
# listening on http://0.0.0.0:8080
|
||||
```
|
||||
|
||||
Open <http://localhost:8080/> in a browser → click "Start call" → grant
|
||||
mic → hear yourself echo. See [`QUICKSTART.md`](QUICKSTART.md) for
|
||||
the full walkthrough + troubleshooting.
|
||||
|
||||
Verbose tracing:
|
||||
|
||||
```bash
|
||||
RUST_LOG=rutster=debug cargo run -p rutster
|
||||
```
|
||||
|
||||
Filter to one module:
|
||||
|
||||
```bash
|
||||
RUST_LOG=rutster_media::loop_driver=trace,rutster=info cargo run -p rutster
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The 20 ms media loop & the "drop + observe" rule
|
||||
|
||||
The hot path is the str0m poll loop in
|
||||
`crates/rutster-media/src/loop_driver.rs`. It runs every ~10 ms (tokio
|
||||
interval, slice-1 deviation per spec §3.4 — step 4 lands a dedicated
|
||||
timing thread).
|
||||
|
||||
Error policy on the hot path: **never** `?`-propagate. Match-and-continue.
|
||||
A dropped packet must not terminate the peer. Policy: "drop + observe
|
||||
(log + counter), don't crash." The eventual fuzz harness (step 5) will
|
||||
test against this exact posture.
|
||||
|
||||
Cold path (signaling, setup, request handlers) uses `thiserror`-derived
|
||||
error enums + `?` propagation, converted to HTTP status codes at the
|
||||
axum boundary.
|
||||
|
||||
See [`AGENTS.md`](../AGENTS.md) "Error handling" for the full policy.
|
||||
|
||||
---
|
||||
|
||||
## Shipped vs. deferred
|
||||
|
||||
Slice 1 proves the **media core** only: WebRTC termination + the
|
||||
Opus⇄PCM codec boundary. The tap, the brain, barge-in, the trunk, and
|
||||
the spend cap are all explicitly deferred — each lands in a subsequent
|
||||
spearhead step.
|
||||
|
||||
The full "what's deferred + when it returns" table is in
|
||||
[`docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md` §1.2](superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md).
|
||||
|
||||
The short version — **don't add these in slice 1:**
|
||||
|
||||
- ❌ Dedicated timing thread (step 4)
|
||||
- ❌ TLS on the HTTP signaling surface (step 5)
|
||||
- ❌ Authn / authz / multi-tenancy (step 6)
|
||||
- ❌ Trickle ICE (when NATs demand it)
|
||||
- ❌ The tap itself (step 2 — slice 1 only *pre-paves* the seam)
|
||||
- ❌ The brain / STT / LLM / TTS (step 3)
|
||||
- ❌ Barge-in / VAD (step 4)
|
||||
- ❌ PSTN trunk / SIP (step 5)
|
||||
- ❌ Spend cap (step 6)
|
||||
- ❌ Event bus / Valkey / CDR emission (step 5)
|
||||
- ❌ Transfer / park / pickup / barge features (escalation rung 2)
|
||||
- ❌ Browser automation e2e tests (post-slice-1)
|
||||
- ❌ Docker / compose (later rung)
|
||||
|
||||
If you find yourself reaching for any of these, the right answer is
|
||||
"no, see slice-1 spec §1.2."
|
||||
|
||||
---
|
||||
|
||||
## Dev loop gotchas
|
||||
|
||||
- **libopus system dependency.** The `opus` crate links system libopus
|
||||
via FFI. If you see `error: linking with cc failed: exit code`, install
|
||||
`libopus-dev` (Debian/Ubuntu) / `opus-devel` (Fedora) / `brew install opus`
|
||||
(macOS). See [`QUICKSTART.md`](QUICKSTART.md) for platform-specific
|
||||
commands.
|
||||
- **`cargo deny` first-time setup.** Run `cargo install cargo-deny --locked`
|
||||
once. `cargo deny check` then validates licenses, advisories, and
|
||||
duplicate-version bans.
|
||||
- **Editor IDE.** `rust-analyzer` is recommended — it's what the plan was
|
||||
written against. Open the repo root in your editor; `rust-analyzer`
|
||||
picks up `rust-toolchain.toml` and the workspace manifest
|
||||
automatically.
|
||||
- **Slow first build.** str0m + axum + tokio compile fresh on first
|
||||
build (~2 minutes). Incremental builds are fast. Use
|
||||
`cargo check -p <crate>` instead of `cargo build` for type-check-only.
|
||||
115
docs/QUICKSTART.md
Normal file
115
docs/QUICKSTART.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Quickstart
|
||||
|
||||
Get Rutster running and hear your own voice echoed back in under 5 minutes.
|
||||
|
||||
> **Status:** Slice 1 (WebRTC media loopback) is the active build target.
|
||||
> If the workspace isn't on `main` yet, check the `slice-1-webrtc-loopback`
|
||||
> branch — that's where the implementation is landing task-by-task.
|
||||
> See [`docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md`](superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md)
|
||||
> for the full design.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### 1. Rust toolchain
|
||||
|
||||
Install via [rustup](https://rustup.rs/):
|
||||
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
```
|
||||
|
||||
The repo pins a specific stable channel in `rust-toolchain.toml` — `rustup`
|
||||
will pick it up automatically on first `cargo` invocation. No manual
|
||||
toolchain selection needed.
|
||||
|
||||
### 2. libopus (FFI dependency)
|
||||
|
||||
The `opus` crate links system libopus via FFI (per PORT_PLAN §7's
|
||||
"🦀 Core (FFI)" disposition — Opus is the codec surface Rust doesn't need
|
||||
to re-implement). Install the dev headers:
|
||||
|
||||
| Platform | Command |
|
||||
|---|---|
|
||||
| Debian/Ubuntu | `sudo apt-get install -y libopus-dev` |
|
||||
| Fedora | `sudo dnf install -y opus-devel` |
|
||||
| Arch | `sudo pacman -S opus` |
|
||||
| macOS (Homebrew) | `brew install opus` |
|
||||
|
||||
Verify: `pkg-config --cflags opus` should print a path with no error.
|
||||
|
||||
That's the only system dependency in slice 1. Everything else is pure
|
||||
Rust from crates.io.
|
||||
|
||||
---
|
||||
|
||||
## Run the server
|
||||
|
||||
```bash
|
||||
cargo run
|
||||
# listening on http://0.0.0.0:8080
|
||||
```
|
||||
|
||||
First build takes ~2 minutes (str0m + axum + tokio compile fresh).
|
||||
Subsequent builds are incremental.
|
||||
|
||||
---
|
||||
|
||||
## Hear the echo
|
||||
|
||||
1. Open a browser to <http://localhost:8080/>.
|
||||
2. Click **Start call**.
|
||||
3. Grant microphone permission when the browser prompts.
|
||||
4. Speak — you should hear yourself back within ~200 ms
|
||||
(no perceptible delay).
|
||||
5. Click **Hang up** to tear down. The server logs
|
||||
`Closing → Closed` for the session.
|
||||
|
||||
Verbose tracing for debugging:
|
||||
|
||||
```bash
|
||||
RUST_LOG=rutster=debug cargo run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause / fix |
|
||||
|---|---|
|
||||
| `error: linking with cc failed` / `could not find opus` | libopus dev headers not installed. Re-run the install command above. |
|
||||
| Browser shows no mic prompt | Another tab/app holding the mic, or mic permissions disabled for `localhost`. Check browser settings. |
|
||||
| `ICE connection failed` in the browser | Shouldn't happen on loopback (host candidates only). If it does, check the server console for the str0m error. |
|
||||
| Click Start call, nothing happens | Open the browser console (F12). The page logs ICE state + connection state to a `<pre>` element. Look for the failure there. |
|
||||
| Port 8080 already in use | Another process holding the port. Either stop it or edit `crates/rutster/src/main.rs` to bind a different port. |
|
||||
|
||||
The browser test page at `GET /` is a single self-contained HTML file
|
||||
with inline JS — no build step. View source to see exactly what the
|
||||
client side is doing.
|
||||
|
||||
---
|
||||
|
||||
## What's happening
|
||||
|
||||
When you click "Start call":
|
||||
|
||||
1. Browser captures microphone audio via `getUserMedia`.
|
||||
2. Browser creates an `RTCPeerConnection` and generates an SDP offer
|
||||
(audio-only, Opus codec).
|
||||
3. Browser POSTs the offer to `POST /v1/sessions/:id/offer`.
|
||||
4. The Rutster core (built on [`str0m`](https://docs.rs/str0m), a sans-IO
|
||||
WebRTC implementation) accepts the offer, generates an SDP answer with
|
||||
its DTLS fingerprint + ICE credentials.
|
||||
5. Browser sets the answer as remote description; ICE + DTLS handshake
|
||||
completes.
|
||||
6. RTP starts flowing: browser → core terminates DTLS-SRTP → decodes
|
||||
Opus to 16-bit PCM @ 24 kHz mono → echoes PCM back → re-encodes to
|
||||
Opus → DTLS-SRTP → browser plays it.
|
||||
|
||||
The "codec-to-PCM boundary" is the canonical point where, in a future
|
||||
slice, the audio tap for an external AI brain splices in. Slice 1 just
|
||||
echoes; step 2 of the spearhead swaps the echo for a real tap.
|
||||
|
||||
For the why, see [`ARCHITECTURE.md`](ARCHITECTURE.md). For the dev loop,
|
||||
see [`DEVELOPMENT.md`](DEVELOPMENT.md).
|
||||
Reference in New Issue
Block a user