--- id: w3-git-mapping name: Wave 3 — the git mapping (exec grammar + alkgit dispatch) status: pending depends_on: [w3-decline-tracing] scope: moderate risk: medium impact: phase level: implementation --- ## Description Land the git branch of the spine (ADR-006 decision 2, exec-only) — the alkforge primary purpose: the full exec grammar replaces the wave-2 skeleton probe, and the git row becomes a real dispatch to the published alkgit seam. Everything POC-3 stage A proved, re-landed as contract. - **The full grammar** (replaces `dispatch::classify_exec` — the wave-2 interpretation's named replacement; its 7 probe unit rows migrate): `git-{upload,receive}-pack ''` — quoted/unquoted paths; strip the quotes and the leading `/` (POC-3 stage A rows: what real OpenSSH git sends); unknown services (`git-send-pack`, `git-upload-archive`) → decline-with-reason, the honest surface; service-only (no repo) → decline-with-reason — POC-3 §A: SSH exec must carry the repo; per-length limits consumed at last — `Limits::max_exec_command_len` enforced **before** any parse or allocation (decline-first, alloc-never; the wave-2 carried-not-enforced reservation). Decide the wire shapes per decline class (open/reply vocabulary, whether "unknown service" and "not git at all" share a reason shape) and record. - **Repo path → repo_id, no existence oracle**: the parse validates via `alkgit::validate_repo_id` (1–2 segments, charset, ≤ 100 bytes) and hands the id through — alkssh never resolves; the **collapsed generic denial before any protocol byte** is `serve_duplex_resolved`'s `SubstrateError::AccessDenied` posture, contract-pinned here: over the SSH channel, no pack/ref byte precedes the denial band (the POC-3 §A.5 byte-order evidence, now a test row). Decide the `.git`-suffix normalization against the alkgit registry contract (what vanilla git sends vs `validate_repo_id`'s grammar) and record. - **The dispatch** — the git row becomes real: the taken channel (the store-and-take handoff lands **here first** — the accepted channel is stored at the open, `channels.remove(&id)` → `into_stream()` in the spawned serving task; the session task reuses the store) → `alkgit::serve_duplex_resolved(repo_id, identity, service, duplex, limits, registry, refs, packgen, ingest, push_options_negotiated, pipeline)` — the serve-config's `git` seam handle (`w3-seam-config`), the resolved identity riding the `identity` param. Process-mode dialect: alkgit speaks first (the V0 advertisement, no service prefix) — nothing alkssh-side; verify the dialect holds over the channel duplex with a byte-level row. - **Exit-status both directions** (POC-3 §A.5): clean serve end → `exit-status(0)`... decide the code mapping (`SubstrateError` outcomes → the code — POC evidence: denied → the client observed `ExitStatus{1}`; vanilla git exits 0 on success) — sent before close, from the spawned task via the cloned `Session::handle()` (the wave-2 mechanics reused). Record the decision. - **Identity** — the parked wave-2 deferred item discharges here: the resolved identity (the bridge's `auth_succeeded` ride-along) reaches the seam — assert it live (the authorize-path/witness row the harness can observe; decide the cheapest row) so the visible-surface invariant is observable through the git row. - **Contract rows through the harness, per build** — the `tests/dispatch_core.rs` skeleton rows upgraded: git-only serves the git row through the doubles (clone/push semantics over the in-mem composition), declines plain exec/pty/shell (parse-then-fail, no resource alloc); session-only still declines git-parseable exec; none-build still declines at open; under `--all-features` the routing holds. The declined rows keep their wire shapes (`channel_failure`, channel usable / serving-path failure+close per class). - **The real-tool smoke** — one `git clone` (2.43+) through the harness against hand-rolled in-mem alkgit doubles (alkgit's `Mem*` are test-private — alkssh hand-rolls the minimal quartet per `alkgit/src/backend.rs:164-254`'s noop shapes; never sibling-checkout paths, never copies of the doubles' code). Decide the tooling posture (require git in the environment vs conditional-skip — skipped-with-reason at most, never silently green) and record. ## Acceptance Criteria - [ ] The full grammar replaces the probe (quoted/unquoted, unknown- service vs non-git decline rows, service-only decline, the `max_exec_command_len` pre-parse cap); the probe's unit rows migrate/extend - [ ] The git row dispatches to `serve_duplex_resolved` over the channel duplex; process-mode dialect verified byte-level; declined routes and the `AccessDenied`-before-protocol-bytes order pinned by contract rows - [ ] The store-and-take handoff lands (channel stored at open, taken in the serving task), structured so the session task reuses it - [ ] The exit-status mapping (clean end / terminal error) decided + recorded; status before close from the spawned task - [ ] The identity ride asserted live through the seam (the parked wave-2 item discharged here) - [ ] The decline-matrix rows per build green through the harness (git-only / session-only / none / all-features), each with its contract stamp; the real-tool clone smoke row - [ ] `cargo test`, `cargo test --features git --test `, `cargo test --all-features`, clippy ×2 (`-D warnings`), fmt, `cargo doc --no-deps` pass ## References - docs/architecture/mapping.md (the git channel-type row; §Session- channel dispatch — decline-first-alloc-never; §the structural constraint) - docs/architecture/decisions/006-mapping-bridge-and-dispatch.md (decision 2 — exec-only; decision 4 — spawn-and-return) - docs/architecture/decisions/004-auth-dispositions.md (the identity ride), docs/architecture/auth.md §Server direction (step 4) - docs/research/poc-3-mapping-bakeoff.md §Stage A (the grammar rows, the denial byte-order, the exit evidence, the spawn rule) - docs/research/mapping-table.md (the validated research ancestor) - alkgit 0.1.0: `serve_duplex_resolved` (`src/duplex.rs:1412`), `SubstrateError` (`src/substrate.rs:116`), `validate_repo_id` (`src/registry.rs:132`), the trait shapes (`src/backend.rs:164-254`) ## Notes > To be filled at implementation time.