feat(binary): wire TapEngine into session lifecycle (spec §5.1, §7)

- tap_engine.rs: spawn_tap_engine(session_id, tap_url) — dials the brain,
  runs TapClient with bounded exponential backoff (250ms→5s cap, infinite
  retries). Cold-path network I/O on tokio's pool, NOT a timing thread
  (spec §4.3 deviation boundary).
- session_map.rs: +tap_registry (DashMap<ChannelId, TapConn>); +default_tap_url.
  drive_all_sessions observes Connected+tap.is_none() → spawn TapEngine, set
  channel.tap = Some(TapHandle). Observes Closing+tap.is_some() → fire close
  oneshot, abort task, clear tap BEFORE state advances to Closed.
- routes.rs: POST /v1/sessions accepts optional {"tap_url":...} body;
  resolve_tap_url validates ws:// 127.0.0.1/localhost, rejects wss:// + non-
  loopback ws:// + bad schemes with 400 Bad Request (fail-fast per spec §4.4).
- main.rs: reads RUTSTER_TAP_URL env (default ws://127.0.0.1:8081/echo).
- rtc_session.rs (rutster-media): pipe field changed from EchoAudioPipe to
  Box<dyn AudioPipe + Send> — the seam test (§8.5 #6):
  loop_driver's call sites (sink.on_pcm_frame / source.next_pcm_frame)
  are byte-identical; only the field type widens. RtcSession::new keeps
  EchoAudioPipe default (slice-1 tests unchanged); binary uses
  RtcSession::set_pipe(tap_audio_pipe) to swap on the Connected transition.
- 4 unit tests for tap_url validation (loopback accepted, non-loopback
  rejected, wss rejected fail-fast, default fallback).

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §4.3, §4.4, §5.1, §7.
This commit is contained in:
opencode controller
2026-06-28 17:39:52 -04:00
parent e0616fe42d
commit 6bc6f6426a
13 changed files with 691 additions and 30 deletions

5
Cargo.lock generated
View File

@@ -1165,15 +1165,20 @@ version = "0.0.0"
dependencies = [
"axum",
"dashmap",
"futures-util",
"rutster-call-model",
"rutster-media",
"rutster-tap",
"rutster-tap-echo",
"serde",
"serde_json",
"thiserror 1.0.69",
"tokio",
"tokio-tungstenite",
"tower",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]