Commit Graph

7 Commits

Author SHA1 Message Date
1bc36c21b2 fix(smoke): enable Plan A assertions (ASSERT_ENGINE_REPLY + ASSERT_ZERO_DROPS)
Plan A (PR #24, merged as 7e2ae6d) landed the TCP_NODELAY +
TCP_QUICKACK-suppression + WS ping fixes that the smoke harness's
ASSERT_ENGINE_REPLY + ASSERT_ZERO_DROPS flags gate on. The flags
were left False during deploy-B's Task 6 (commit 4c62222) pending
Plan A's integration. Plan A is now integrated in docs/deployment-topology-spec
at 838ecdf, so the flags flip True — the smoke + smoke-reload CI jobs
now have teeth.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-06 15:42:43 -04:00
abb7e1428d feat(deploy/smoke): all-in-one + compose + reload-during-call smoke harness (deploy-B §9)
Three Python-stdlib-only smoke scripts (no pip install required in CI;
the runner image ships Python 3 + ssl/socket/json/base64/time):

* allinone_smoke.py — boots rutster-allinone with RUTSTER_LOCAL_CERTS=true
  (Caddy internal CA, no ACME per spec §9), extracts the root cert from
  /data, opens wss:// WS to /twilio/media-stream, sends Twilio's
  connected+start handshake (the same JSON sequence the in-tree
  ws_ping.rs/nodelay.rs tests use), streams 200 PCM frames at 20ms cadence,
  asserts the engine echoes/replies at least one text frame. Reuses the
  T8 stub's handshake pattern (crates/rutster/tests/trunk_sim_e2e.rs TODO
  body) against a real containerized binary rather than an in-process
  MockTwilioMediaStreamsServer.
* compose_smoke.sh — T2 four-service boots + health checks via Caddy TLS.
  The # TODO slice-C: assert lifecycle event in Valkey stream named hook
  documents the contract slice C will assert; the valkey service is dark
  on day one (no consumers) so the stream length is always 0 today.
* reload_during_call.py — holds a live WS streaming frames at 20ms while a
  concurrent `caddy reload` fires mid-call; asserts zero frames dropped
  + WS survives across the reload. Regresses the stream_close_delay 24h
  mitigation against caddy #6420/#7222 (TLS brief §5 risk 1; spec §9
  reload-during-call smoke).

WS frame encode/decode is hand-rolled (RFC 6455 §5 — ten lines on top of
socket+ssl) so the smoke needs no websocket-client/websockets pip
dependency + no network round-trip in CI.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-06 15:30:44 -04:00
4714ca4da7 feat(deploy): compose.yaml — T2 reference deployment (deploy-B §2.2)
Four services per spec §2.2:

* caddy    — image rutster-edge; ports 80/443; volume caddy-data:/data
             (Caddy ACME state — loss risks LE 5/week duplicate-cert lockout
             per TLS brief §5 risk 5; mounts ./Caddyfile for edit-without-
             rebuild).
* engine   — image rutster-engine; ports 8080 + 49152-49407/udp (media
             UDP direct, never proxied — spec §2.1); stop_grace_period 660s
             paired with RUTSTER_DRAIN_DEADLINE_SECS=600 (grace exceeds drain
             invariant, spec §2.2/§8).
* brain    — image rutster-brain; network_mode: "service:engine" so the
             loopback-only tap posture survives unchanged (resolve_tap_url
             rejects non-loopback until step 6 per spec §2.2). Quoted in
             YAML — `service:engine` would parse as a mapping otherwise.
* valkey   — upstream valkey/valkey:8.0 (BSD-3); volume valkey-data:/data;
             dark on day one except slice-E ValkeyEventSink consumer.

Bring-your-own-proxy: comment out the caddy service; the engine keeps
plaintext :8080; tuned-timeout snippets land in slice-G reverse-proxies.md.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-06 15:30:44 -04:00
1d007d99c6 docs(deploy): .env.example — all RUTSTER_* vars documented (deploy-B §6.2)
The operator documentation surface. Lists every RUTSTER_* var with its
default + the slice that introduced it + the fail-fast behavior. Includes:

* Artifact-level (this slice): RUTSTER_DOMAIN, RUTSTER_ACME_EMAIL,
  RUTSTER_LOCAL_CERTS (CI override).
* FOB engine (slice-5 + plan-A): RUTSTER_HTTP_BIND,
  RUTSTER_DRAIN_DEADLINE_SECS=600 (paired with compose stop_grace_period
  660s per spec §2.2 — grace exceeds drain), RUTSTER_MAX_SESSIONS,
  RUTSTER_MEDIA_BIND_IP/ADVERTISED_IP/PORT_RANGE, RUTSTER_WS_PING_SECS,
  RUTSTER_TRUSTED_PROXIES.
* Trunk (slice-5): the four RUTSTER_TWILIO_* (all-or-none — config.rs
  twilio_credentials rejects partial).
* Brain (slice-2/3): RUTSTER_TAP_URL (loopback-only until step 6).
* Future var stubs: RUTSTER_TLS_CERT/KEY (slice C), RUTSTER_METRICS_BIND
  (slice D), RUTSTER_VALKEY_URL (slice E) — commented out with a pointer
  to the sibling plan.

Bundled-binary licenses noted in the header comment (Caddy Apache-2.0,
valkey BSD-3, s6-overlay ISC, libopus0 BSD-3 — aggregation-clean vs
GPL-3 per ADR-0004); seeds the future cargo-deny-adjacent image license
scan (spec §6.1, out of scope for this slice).

Registry namespace git.adlee.work/alee/rutster-* + the 'if your Gitea
registry uses a different owner segment, replace alee throughout' note.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-06 15:30:44 -04:00
7bab5f5561 feat(deploy): Caddyfile — tuned timeouts, honest X-Forwarded, stream_close_delay 24h (deploy-B §3.2)
The shared edge config for rutster-edge (T2 modular) and rutster-allinone
(T1 solo). ~6-line site block + global options per spec §3.2 / TLS brief
§3(a):

* read_body 30s / read_header 30s — Twilio webhook budget (15s cap by
  invariant 6 + slack).
* write 0 — 20ms media frames self-flush; never write-timeout.
* idle 24h / stream_close_delay 24h — above max call duration (invariant
  3); the universal 60s-class default kills quiet mid-call WS.
* reverse_proxy + header_up — Caddy sets honest X-Forwarded-Proto/Host/For
  for the engine's reconstruct_public_url (plan-A Task 5); engine-side
  RUTSTER_TRUSTED_PROXIES is the trust gate (fail-closed default).

CI override via RUTSTER_LOCAL_CERTS=true uses Caddy's internal CA (no
ACME in CI per spec §9). The reload-during-live-call smoke (Task 9)
regresses the stream_close_delay mitigation against caddy #6420/#7222.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-06 15:30:44 -04:00
1688612b8c chore(deploy): .dockerignore — prune build context (deploy-B §6.1)
Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-06 15:30:44 -04:00
c612a0d305 feat(deploy): multi-stage Dockerfile producing four first-party images (deploy-B §6.1)
One Dockerfile, four named --target stages: rutster-engine (FOB),
rutster-brain (brain), rutster-edge (custom xcaddy + curated DNS plugins
cloudflare/route53/porkbun/hetzner/desec, duckdns excluded per spec §3.2),
rutster-allinone (s6-overlay v3 supervising caddy + FOB + brain +
valkey-server).

Builder pins rust:1.85-slim-bookworm (matches rust-toolchain.toml).
libopus-dev in builder, libopus0 in runtime (matches dev — spec §6.1
distro-over-static-bundled). Brain image gets libopus0 too: although the
brain never calls an opus symbol, the linker records NEEDED:libopus.so.0
because rutster-brain-realtime depends on rutster-media (spec ambiguity
resolved in this slice's plan README).

All-in-one vendors s6-overlay v3.2.0.0 (ISC) + uses upstream valkey/valkey
binary (COPY --from pattern, BSD-3). Bundled-binary licenses
aggregation-clean vs GPL-3 (Caddy Apache-2.0). No Rust crates touched;
cargo-deny unaffected.

s6-rc service definitions land in this commit (caddy/engine/brain/
valkey-database run scripts + dependency edges). The Caddyfile lands in
Task 3 (same PR); the image-build CI job (Task 7) runs after both.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-06 15:30:44 -04:00