--- id: w3-seam-config name: Wave 3 — serve-config seam handles (alktty/alkgit per-feature) status: completed depends_on: [review-wave-2, w1-config] scope: narrow risk: low impact: component level: implementation --- ## Description Extend [`crate::ServeConfig`] with the per-feature family-seam handles the wave-3 mappings consume. This is wave 3's first *course correction* to the wave-1 config surface — the assembly-supplied composition inputs were never carried (mapping.md's seam-entries table names them inside the seam signatures; the wave-1 config bullet pre-dates that reading), and the mappings cannot call `drive_session[_pre_negotiated]` / `serve_duplex_resolved` without them. Landing them here, before the mapping tasks, keeps the un-set-declines posture structural and the review surface unified: - **The `session` seam handle** (feature-gated), unset → the session rows decline even with the feature on (the decline matrix's "an unset field declines its row though the feature is on" note, config-driven). Contents, per alktty 0.8.1's actual `drive_session[_pre_negotiated]` signature — decide the exact final shape (flat `Option` fields vs an `Option`-struct per seam; record): - the alktty backends map (`Arc>>` — assembled by the assembly; alkssh never touches a real backend, the `local` shapes stay alktty-side); - the backend **selector** — the `NegotiateRequest.backend` string alkssh writes (assembly-supplied, never wire-derived; decide and record); - the ownership provider `Option>` (an alkcall type — no new dep; pass-through to `drive_session_pre_negotiated`, family ACL layer riding along). - **The `git` seam handle** (feature-gated), unset → the git rows decline even with the feature on. Contents, per alkgit 0.1.0's `serve_duplex_resolved` signature: the four backend traits (`GitRegistry` / `GitRefs` / `GitPackGen` / `GitPackIngest`), the caller-owned server-wide `pipeline: Arc`, and `push_options_negotiated: bool` (the assembler's config flag; false in the v1 default). - **Manifest** (the wave-1 rule: per-feature sibling deps land with their first consuming wave — the config fields consume them first, so they are not dead entries): optional deps `alktty = "0.8.1"`, `alkgit = "0.1.0"` wired feature-to-dep (`session = ["dep:alktty"]`, `git = ["dep:alkgit"]`) so a git-build never compiles alktty and vice versa; verify the pins against crates.io before committing (verified at decomposition: 0.8.1 / 0.1.0, both over the shared `alkcall 0.10` req → 0.10.1; MSRVs 1.88 ≤ our 1.89; MIT OR Apache-2.0). **alktunnels is NOT landed here** — the config carries no alktunnels types beyond the already-landed `Arc`; its optional dep lands with its first consuming code (`w3-tunnel-direct`). No construction-time validation (the unset-declines contract lives in the type shape, checked at request time by the mappings); the tunnel handle stays `Option>` as landed; `Debug` impls and the config module docs updated (the direction list now states what each seam handle carries). ## Acceptance Criteria - [x] `ServeConfig` gains the feature-gated seam handles (the final shape decided and recorded); unset → the owned rows decline, structurally carried, no construction-time validation - [x] The optional deps + `dep:` feature wiring land with crates.io pins verified; a git-build never compiles alktty and vice versa (no code path outside the cfg gate touches the types) - [x] The none-build's `ServeConfig::new` shape is unchanged; feature-gated fields absent from featureless builds - [x] Tests: the per-feature construction shapes, the unset-declines-preserved rows, the `Debug` posture - [x] `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo clippy --all-features --all-targets -- -D warnings`, `cargo fmt --check`, `cargo doc --no-deps` pass ## References - docs/plans/implementation.md §Wave 1 (config bullet), §Wave 3 - docs/architecture/mapping.md §The seams (the signature shapes), §Session-channel dispatch (the unset note), §Decline matrix - docs/architecture/decisions/002-russh-pin.md (the manifest rule), docs/architecture/decisions/003-feature-graph.md (the feature graph) - alktty 0.8.1: `src/adapter.rs:205-241` (`drive_session[ _pre_negotiated]`), `src/negotiation.rs:64-92` (`NegotiateRequest`) - alkgit 0.1.0: `src/duplex.rs:1412-1426` (`serve_duplex_resolved`), `src/backend.rs:31-162` (the four traits) ## Notes ### What landed (2026-10-28) - `Cargo.toml`: optional `alktty = "0.8.1"` / `alkgit = "0.1.0"` (both `default-features = false`), wired `session = ["dep:alktty"]`, `git = ["dep:alkgit"]`. alktty's `local` feature stays off — alkssh never touches a real backend; the `local` shapes stay alktty-side. Feature-graph comment updated (wave-3 seam-config is the deps' first consumer; alktunnels still lands with `w3-tunnel-direct`). - `src/config.rs` (re-exported through `lib.rs`): the two seam-handle types + the two cfg-gated `ServeConfig` fields + the cfg-gated `session_seam_configured()` / `git_seam_configured()` accessors + config-module docs updated (the direction list and the unset-declines rule both state the seam handles now). ### Shape decision (the task's decide-and-record item): whole-`Option` struct per seam `SessionSeam{backends, backend, ownership}` and `GitSeam{registry, refs, packgen, ingest, push_options_negotiated, pipeline}` held as `cfg`-gated `Option` fields (`session_seam` / `git_seam`), each `Clone` (all payload types `Arc` or plain data — the spawned serving tasks clone the seam), each with a hand-rolled `Debug`. **Flat `Option` fields were rejected**: each seam is consumed atomically by exactly one seam call (`drive_session[_pre_negotiated]` / `serve_duplex_resolved`), so a struct admits no half-supplied seam states (backends without a selector; three traits without the fourth) that would all map to the same all-or-nothing decline — the unset-declines contract reads as one `Option` per seam, "unset → the owned rows decline even though the feature is on" (the decline matrix's config-driven note), checked at request time by the mappings, never at construction. The already-landed tunnel handle stays the flat `Option>` — a single-field seam needs no struct. Naming: struct names carry the crate (the consuming mappings will see it in the field docs); the fields carry the row vocabulary (`session_seam` / `git_seam`, matching the decline matrix's row names). ### Pins verified against crates.io (2026-10-28, this task re-verified) - `alktty 0.8.1`: published 2026-10-22, not yanked, `MIT OR Apache-2.0`, `rust_version = "1.88"` ≤ our 1.89; `alkcall ^0.10.1` — shares our `alkcall = "0.10"` resolution (0.10.1), so no alkcall fork in the lockfile. - `alkgit 0.1.0`: `MIT OR Apache-2.0`, `rust_version = "1.88"`; `alkcall ^0.10` — same shared resolution. - Signatures confirmed against the published crates (the registry sources under `~/.cargo/registry/src/`): alktty's `drive_session[_pre_negotiated]` (adapter.rs:205-241) takes `(writer, reader, [req,] backends: Arc>>, ownership: Option>, identity: Option)` — the identity is the per-connection resolved one, not config; alkgit's `serve_duplex_resolved` (duplex.rs:1412) takes the four traits + `push_options_negotiated: bool` + `pipeline: Arc`. `TtyBackend` / `NegotiateRequest` / `TerminalParamsWire` and `GitRegistry/GitRefs/GitPackGen/GitPackIngest` are all re-exported from the two lib roots. ### One course correction absorbed: `ObjectId` is not re-exported by alkgit `GitPackGen::generate`/`common_haves` name `Vec` and alkgit's lib root does not re-export `ObjectId` — a backend-trait doubled in tests has to name the type, so `gix-hash = "0.28"` (matching alkgit's `^0.28` req; the same crate already in the lockfile) lands as a **`[dev-dependencies]` entry only**, exercised exclusively by the cfg-gated git test module. The lib dependency graph keeps the feature-to-dep isolation below; the dev graph is universal by design (tokio and friends are too). Flagged for `w3-git-mapping`: the hand-rolled in-mem quartet will lean on this dev-dep for `ObjectId` (same as this task's noop doubles, modeled on alkgit's own `backend.rs:164-254` noop shapes — own code, not copied). ### Dependency isolation — verified structurally, not by assertion `cargo tree -e features --features git`: zero `alktty` occurrences; `cargo tree -e features --features session`: zero `alkgit` occurrences; `--all-features`: both present. Combined with the cfg gates (all `alktty`/`alkgit` type references live inside `#[cfg(feature = "session")]` / `#[cfg(feature = "git")]` blocks — imports, fields, accessors, Debug, constructor, tests), the "a git-build never compiles alktty and vice versa" invariant is compile-proven for both consumption sides. ### The unset-declines wire-level rows stay the mappings' consumption The wave-3 mappings own the request-time decline checks; this task lands the config-level contract rows (constructor posture, `Option` configured() semantics, seam-vs-OQ-08-field independence, Debug posture — 8 unit rows across the two feature modules) and leaves the wave-2 harness rows untouched (featured-but-unset already declines at the row the stubs own; the real serving paths make those rows meaningful in `w3-git-mapping` / `w3-tty-session`, whose seam consumption reads exactly these fields). ### Verification (all green) `cargo test` (none build, 9 binaries + doctests) · `cargo test --features session` / `--features git` / `--all-features` (67 unit rows under all-features) · `cargo clippy --all-targets -- -D warnings` ✓ · `cargo clippy --all-features --all-targets -- -D warnings` ✓ · `cargo fmt --check` ✓ · `cargo doc --no-deps` 0 warnings · `taskgraph validate` ✓.