Merge remote-tracking branch 'origin/main' into slice-1-webrtc-loopback
# Conflicts: # README.md
This commit is contained in:
215
CONTRIBUTING.md
Normal file
215
CONTRIBUTING.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Thanks for considering a contribution to Rutster. This file is the short
|
||||||
|
form — read [`AGENTS.md`](AGENTS.md) for the full orientation any agent
|
||||||
|
(human, AI, hybrid) working in the repo should have.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trunk-based development
|
||||||
|
|
||||||
|
1. Branch from `main` for any change.
|
||||||
|
2. Open a PR targeting `main`.
|
||||||
|
3. CI gates (below) must pass before merge.
|
||||||
|
4. Squash-merge to keep `main` linear.
|
||||||
|
|
||||||
|
**Never push directly to `main`.** Branch protection is planned to
|
||||||
|
enforce this; until then, self-discipline.
|
||||||
|
|
||||||
|
### Branch naming
|
||||||
|
|
||||||
|
Single-purpose branches named after the change:
|
||||||
|
|
||||||
|
- `slice-1-webrtc-loopback` — long-running build-target branch
|
||||||
|
- `docs/quickstart` — documentation additions
|
||||||
|
- `media/opus-roundtrip-fix` — bug fix with crate scope
|
||||||
|
- `ci/deny-toml` — CI config
|
||||||
|
|
||||||
|
No strict scheme — short, descriptive, hyphen-separated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CI gates
|
||||||
|
|
||||||
|
All four must pass before merge to `main`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --check # formatting
|
||||||
|
cargo clippy -- -D warnings # lints (warnings = failures)
|
||||||
|
cargo test --all # all unit + integration tests
|
||||||
|
cargo deny check # licenses, advisories, bans, sources
|
||||||
|
```
|
||||||
|
|
||||||
|
CI runs on push + PR to `main`, matrix: latest stable + the MSRV pinned
|
||||||
|
in `rust-toolchain.toml`. The `clippy` + `test` jobs install `libopus-dev`
|
||||||
|
(the one system dependency, per PORT_PLAN §7).
|
||||||
|
|
||||||
|
If your change adds a new dependency, run `cargo deny check` locally
|
||||||
|
before pushing — a license conflict or duplicate-version ban will fail
|
||||||
|
CI, and it's faster to catch locally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commit messages
|
||||||
|
|
||||||
|
- **Imperative mood**: "Add X" / "Fix Y", not "Added X" / "Fixes Y".
|
||||||
|
- **Subject ≤ 72 chars**.
|
||||||
|
- **Body wraps at 72**, blank line between subject and body.
|
||||||
|
- **Reference ADRs / specs** by number when relevant: `ADR-0002`,
|
||||||
|
`slice-1 spec §3.4`.
|
||||||
|
- Match the style of recent commits:
|
||||||
|
```bash
|
||||||
|
git log --oneline -10
|
||||||
|
```
|
||||||
|
|
||||||
|
Example shape (from the repo history):
|
||||||
|
|
||||||
|
```
|
||||||
|
media: PcmFrame + AudioSource/Sink + Opus codec pair
|
||||||
|
|
||||||
|
PcmFrame is the canonical tap format (16-bit mono @ 24 kHz, 480 samples
|
||||||
|
per 20 ms frame — ARCHITECTURE.md). AudioSource/AudioSink are the seam
|
||||||
|
step 2 splices the tap client into (spec §3.3); EchoAudioPipe is the
|
||||||
|
slice-1 wiring of that seam. OpusDecoder/OpusEncoder wrap the opus
|
||||||
|
crate's libopus FFI with hot-path match-and-continue.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Atomic commits
|
||||||
|
|
||||||
|
One logical change per commit. Doc ratifications, code, and tests each
|
||||||
|
land as separate commits when practical. Don't bundle unrelated work.
|
||||||
|
|
||||||
|
A new feature + its tests can land in one commit if the test is part of
|
||||||
|
the feature's correctness story (TDD). A refactor + a new feature should
|
||||||
|
be two commits.
|
||||||
|
|
||||||
|
### Never commit secrets
|
||||||
|
|
||||||
|
`.gitignore` covers `.env*`, `*.pem`, `*.key`. If a new secret pattern
|
||||||
|
appears in your work, extend `.gitignore` in the same commit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code style
|
||||||
|
|
||||||
|
See [`AGENTS.md`](AGENTS.md) "Code style" for the full guide. Highlights:
|
||||||
|
|
||||||
|
- `cargo fmt` is the source of truth for formatting. Don't hand-format.
|
||||||
|
- `clippy -D warnings` is the lint bar. Fix the code, don't suppress
|
||||||
|
with `#[allow]` unless the rationale is documented inline.
|
||||||
|
- `snake_case` (functions, variables, modules, crates), `PascalCase`
|
||||||
|
(types), `UPPER_SNAKE_CASE` (constants).
|
||||||
|
- Newtype wrappers over primitives for type-safety — e.g. `ChannelId(Uuid)`,
|
||||||
|
not bare `Uuid`.
|
||||||
|
- Hot path (the 20 ms media loop): **never** `?`-propagate.
|
||||||
|
Match-and-continue. Dropped packet must not terminate the peer.
|
||||||
|
- Cold path: `thiserror`-derived error enums + `?`, converted to HTTP
|
||||||
|
status codes at the axum boundary.
|
||||||
|
- Never `unwrap()` / `expect()` outside tests or const-init contexts.
|
||||||
|
|
||||||
|
### Documentation comments (learner-facing — important)
|
||||||
|
|
||||||
|
**This project overrides the default "no comments" convention.** The
|
||||||
|
user is learning Rust from this codebase. Code in slice 1 (and the
|
||||||
|
spearhead steps) carries thorough educational comments:
|
||||||
|
|
||||||
|
- `//!` module docs on every `lib.rs` / `main.rs` / sub-module
|
||||||
|
- `///` item docs on every public struct / enum / fn / trait
|
||||||
|
- `//` inline comments on the *mechanism*, not the what — why this
|
||||||
|
ownership pattern, why `Arc<Mutex<>>` vs `Arc<RwLock<>>`, why an
|
||||||
|
`enum` over a struct with a `kind` field, etc. Aim: a Rust learner
|
||||||
|
reads the comment and learns a specific Rust concept they wouldn't
|
||||||
|
have inferred from the code alone.
|
||||||
|
|
||||||
|
`LEARNING.md` at the repo root indexes "to learn concept X, read file Y"
|
||||||
|
pointers. Add to it when you introduce a new pattern worth surfacing.
|
||||||
|
|
||||||
|
This verbosity is a deliberate trade-off: more tokens now, compound
|
||||||
|
educational value later. Once a pattern is established, later slices can
|
||||||
|
be sparser on the well-trodden patterns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slice-1 boundaries — what NOT to add (yet)
|
||||||
|
|
||||||
|
If an agent (you, an AI pair, a contributor) proposes adding any of these
|
||||||
|
in slice 1, the right answer is "no, see slice-1 spec §1.2":
|
||||||
|
|
||||||
|
- Dedicated timing thread for the media loop (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-driven playout kill (step 4)
|
||||||
|
- PSTN trunk / SIP client (step 5)
|
||||||
|
- Spend cap / abuse gate (step 6)
|
||||||
|
- Browser-based automated e2e tests / Selenium / Playwright (post-slice-1)
|
||||||
|
- Docker / compose (later rung)
|
||||||
|
- Event bus / Valkey / CDR emission (step 5)
|
||||||
|
- Transfer / park / pickup / barge features (escalation rung 2)
|
||||||
|
|
||||||
|
The spearhead depends on this sequencing. Adding things early breaks
|
||||||
|
the ordering each step is its own proof.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terminology policy
|
||||||
|
|
||||||
|
**Avoid authoritarian / exclusionary terms** in 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 — replacing them would hurt the
|
||||||
|
educational mapping to upstream docs. **ICE** (Interactive Connectivity
|
||||||
|
Establishment, RFC 8445) stays: it's the protocol name in `str0m::ice`,
|
||||||
|
`RTCIceCandidate`, and the cargo ecosystem.
|
||||||
|
|
||||||
|
See [`AGENTS.md`](AGENTS.md) "Terminology policy" for the full table.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pull request workflow
|
||||||
|
|
||||||
|
1. Branch from `main`.
|
||||||
|
2. Make atomic commits per the guidance above.
|
||||||
|
3. Push the branch + open a PR targeting `main`.
|
||||||
|
4. CI runs: fmt, clippy, test, deny. All four must be green.
|
||||||
|
5. Reviewer checks the diff against the spec / ADRs cited in the commit
|
||||||
|
message. New dependencies require scrutiny — `cargo deny check`
|
||||||
|
enforces license + source bans, but reviewers should also sanity-check
|
||||||
|
the dependency choice against the architecture.
|
||||||
|
6. Squash-merge once approved + green.
|
||||||
|
7. Delete the branch post-merge (keeps the branch list tidy).
|
||||||
|
|
||||||
|
### Reviewing
|
||||||
|
|
||||||
|
When reviewing a PR:
|
||||||
|
|
||||||
|
- Does it cite the relevant ADR / spec section? (For substantial changes.)
|
||||||
|
- Does it add anything from the slice-1 "what NOT to add" list? (Reject
|
||||||
|
if so — refer to slice-1 spec §1.2.)
|
||||||
|
- Are educational comments present where a new pattern is introduced?
|
||||||
|
- Any `unwrap()` / `expect()` outside tests? (Reject unless justified.)
|
||||||
|
- Does the hot-path code use `?`? (Reject — it must match-and-continue.)
|
||||||
|
- Does the diff bundle unrelated work? (Ask for split commits.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing, you agree your contributions are licensed under
|
||||||
|
**GPL-3.0-or-later** (ADR-0004). Strong copyleft in the Asterisk lineage.
|
||||||
|
|
||||||
|
If you contribute code with a different license header / SPDX expression
|
||||||
|
in a Cargo manifest, CI will reject it (`cargo deny check licenses`).
|
||||||
|
Don't introduce dependencies whose licenses conflict — check `deny.toml`
|
||||||
|
for the allow-list.
|
||||||
52
README.md
52
README.md
@@ -8,47 +8,35 @@ or its architecture.
|
|||||||
> technical builder uses to stand up a contact center* — and re-aims it at a category AI is
|
> technical builder uses to stand up a contact center* — and re-aims it at a category AI is
|
||||||
> actively disrupting, instead of a PBX category UCaaS already ate.
|
> actively disrupting, instead of a PBX category UCaaS already ate.
|
||||||
|
|
||||||
## Slice 1 dev loop (WebRTC media loopback)
|
## Quickstart
|
||||||
|
|
||||||
> Build prerequisite: install libopus (the `opus` crate links it via FFI):
|
|
||||||
> ```bash
|
|
||||||
> sudo apt-get install -y libopus-dev # Debian/Ubuntu
|
|
||||||
> # Fedora: sudo dnf install -y opus-devel
|
|
||||||
> # macOS: brew install opus
|
|
||||||
> ```
|
|
||||||
> This is the one system dependency in slice 1. Opus is FFI per PORT_PLAN
|
|
||||||
> §7's "🦀 Core (FFI)" disposition — the codec surface Rust doesn't need
|
|
||||||
> to re-implement.
|
|
||||||
|
|
||||||
Run the server:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Prereqs: Rust (rustup), libopus dev headers (libopus-dev / opus-devel / brew install opus)
|
||||||
cargo run
|
cargo run
|
||||||
# listening on http://0.0.0.0:8080
|
# listening on http://0.0.0.0:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Open a browser to `http://localhost:8080/`, click "Start call", grant
|
Open <http://localhost:8080/> → click "Start call" → grant mic → hear yourself echo.
|
||||||
microphone permission. Speak — you should hear yourself back within
|
Full walkthrough + troubleshooting: **[`docs/QUICKSTART.md`](docs/QUICKSTART.md)**.
|
||||||
~200 ms (no perceptible delay). Click "Hang up" to tear down; server
|
|
||||||
logs `Closing → Closed`.
|
|
||||||
|
|
||||||
Verbose tracing:
|
> **Status:** Slice 1 (WebRTC media loopback) is the active build target. The workspace is
|
||||||
|
> landing task-by-task on the `slice-1-webrtc-loopback` branch. Design:
|
||||||
|
> [`docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md`](docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md).
|
||||||
|
> Implementation plan:
|
||||||
|
> [`docs/superpowers/plans/2026-06-28-slice-1-webrtc-loopback.md`](docs/superpowers/plans/2026-06-28-slice-1-webrtc-loopback.md).
|
||||||
|
|
||||||
```bash
|
## Documentation
|
||||||
RUST_LOG=rutster=debug cargo run
|
|
||||||
```
|
|
||||||
|
|
||||||
### Slice 1 "done" checklist (spec §6.5)
|
| Doc | For when you want to… |
|
||||||
|
|---|---|
|
||||||
On a clean checkout:
|
| [`docs/QUICKSTART.md`](docs/QUICKSTART.md) | Run it in 5 minutes |
|
||||||
1. `cargo test --all` passes.
|
| [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) | Iterate on the codebase (workspace layout, per-crate testing, dev loop) |
|
||||||
2. `cargo fmt --check` passes.
|
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Understand the fused per-call vertical + composable platform + agent tap |
|
||||||
3. `cargo clippy -- -D warnings` passes.
|
| [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md) | See every Asterisk subsystem mapped to a disposition (capability checklist, not template) |
|
||||||
4. `cargo deny check` passes.
|
| [`docs/adr/`](docs/adr/) | Load-bearing architecture decisions |
|
||||||
5. `cargo run` + browser manual e2e: speak → hear echo within ~200 ms.
|
| [`AGENTS.md`](AGENTS.md) | Project orientation for any agent (human/AI/hybrid) working in the repo |
|
||||||
6. Hang-up button triggers `Closing → Closed` in server logs.
|
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Trunk-based dev workflow, CI gates, commit style, review checklist |
|
||||||
7. Every stub crate compiles; its doc-comment names its scheduled step.
|
| [`LEARNING.md`](LEARNING.md) | Index of "to learn concept X, read file Y" (learner-facing codebase) |
|
||||||
8. `LEARNING.md` indexes at least 5 "to learn X, read Y" pointers.
|
|
||||||
|
|
||||||
## Why it exists
|
## Why it exists
|
||||||
|
|
||||||
|
|||||||
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