Files
glm-5.3-flash f4b5f896b4 docs: sweep em dashes from all markdown prose
Renames the written-usage rule into practice across the contract surface:
docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files,
taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the
.opencode/agents/ prompts. Every replacement is a sense-preserving
rewrite (colons, semicolons, commas, parentheses, sentence splits, varied),
never a mechanical swap; no spaced hyphens were substituted. Enum fields,
frontmatter, tables, wire data, and cited identifiers survive verbatim;
en dashes inside numeric ranges keep their meaning.

Verification: taskgraph validate (43 tasks), repo grep clean (single
exempt hit: the verification command's own search pattern in AGENTS.md),
cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
2026-11-04 20:28:00 +00:00

240 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: w3-git-mapping
name: "Wave 3: the git mapping (exec grammar + alkgit dispatch)"
status: completed
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),
which is 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 '<path>'` handles 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, with `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 resolving; 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, and 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)`, namely 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), and nothing is
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, and
the resolved identity (the bridge's `auth_succeeded` ride-along)
reaches the seam, so 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**, with 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, so 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 <git rows>`,
`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 and its decline-first-alloc-never rule; §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.
### Decisions recorded (implementation, 2026-10-28)
- **Course correction to `w3-seam-config`**: `GitSeam` gains
`limits: alkgit::Limits`, since `serve_duplex_resolved` requires
the
alkgit ADR-009 session budget, which the landed handle had no field
for. alkgit's own contract shapes this ("constructed by the
door/adapter from assembler config"), so the budget rides the seam
(assembly-supplied, `Copy`), not a crate default. First seam
consumer; landing it here, recorded on the field and in the config
docs.
- **The grammar** (`dispatch.rs`, the probe's rows migrated): exact
serving-service token + single separating space + path-shaped
argument (the wave-2 probe's pinned verdicts preserved verbatim:
`git-upload-pack rel` and double-space arguments are *not git*);
quoted shape is fully `'`-wrapped (unterminated → pipe, the probe's
non-path-shape verdict); leading `/` stripped after quotes; **one
trailing `.git` suffix stripped** (the vanilla-clone idiom; the
alkgit registry grammar excludes `.`, so the door normalizes the
idiom rather than asking registries for dotted keys; an empty
remainder (`/.git`) is *kept* and the git row's validation
declines it, never a silent emptying). Validation:
`alkgit::validate_repo_id` at the git row (`git_map.rs`,
feature-gated, while the parse core stays feature-free); a
shape-invalid
id declines `invalid-repo-id` alkssh-side; a shape-valid id is
handed through untouched and **alkssh never resolves** (resolution,
authorize, and the collapsed generic denial live at the seam).
Unknown services (`git-send-pack`, `git-upload-archive`) and
service-only (including an empty argument) route to
`ExecRoute::Declined` (build-independent, off the pipe row, per
POC-3 §2's honest-surface verdict.
- **Wire shapes per decline class** (recorded): SSH request replies
carry no text, so the reason lives only in the event vocabulary;
the git declinables (`unknown-service` / `service-only` /
`invalid-repo-id` / `exec-oversize`) share the request-level reply
shape with parse-then-fail: `channel_failure`, channel alive
(POC-3's decline shape, POC-1 §5 posture); the reply vocabulary is
the decision ("unknown service" vs "not git at all" differ in
nothing observable on the wire, only the event). Serving-path
declines (the row owned the channel) keep failure+close:
`seam-unconfigured` (feature on, seam unset, the unset-declines
contract; keeps the wave-2 decline-matrix rows' shape in builds
without a configured seam), plus the unreachable-in-practice
guards `no-identity` and `channel-not-held`. The *serve's own*
denials are not alkssh declines: unknown/ungranted repo flows to
alkgit, whose collapsed generic denial writes
`ERR access denied: <repo>` as the **first byte** on the channel
(pinned byte-level in `tests/git_mapping.rs`), then exit-status(1),
then close.
- **Exit mapping** (POC-3 §A.5, decided): `Ok(())` → exit-status(0)
(vanilla git exits 0 on success; the clean-end wire row exercises
the flush-only request's `Ok(None)` → `Ok(())` shape); every
`SubstrateError` → exit-status(1): `AccessDenied` per the POC's
wire evidence, `Io`/`Protocol` the same (the service ran and
failed), the machinery-misuse variants defensively 1 (they never
arise from the serving entry). `255` never fabricated (OpenSSH's
could-not-start semantic this door never claims). Status before
close, from the spawned task via the cloned handle; a failed report
send is the vocabulary's one `warn` (the wave-2 stub mechanics). A
serve `Err` is *not* a decline event, as the substrate already
mapped its own error onto the wire.
- **Store-and-take handoff**: `SessionHandler.channels` (plain
`std` HashMap, single-writer, with every mutation on the packet
loop's task). Stored at the confirmed open (accept then insert); taken by
the git row (`remove` → `into_stream()` in the spawned serving
task); released by `channel_close` (the cap is released there too:
one hook, both budgets). The session rows still hold until their
wave-3 tasks take them. A routed exec answers `channel_success`
inline (the POC-3 §A.5 shape, where the request routed and the
serving outcome reports through exit-status).
- **Identity** (`w2-auth-bridge`'s parked ride-along, discharged):
the resolved identity is cloned into the serving task and passed on
`serve_duplex_resolved`'s identity param. The cheapest live witness
row: **alkgit's `authorize` evaluation**, giving two
otherwise-identical
rows (private repo granted to the riding id → the advertisement
flows; the same repo ungranted → the denial band + exit 1); no
direct identity introspection needed, and the deny row doubles as
the authorize-path witness. `None` can never occur post-auth and
declines serving-path.
- **Push semantics through the doubles**: the packless CAS path
(alkgit ADR-020 §1) is the pack-free receive-pack row, with commands +
flush (caps NUL-attached on the first command line, `report-status`
requested), no pack; the wire layer applies via
`GitRefs::apply_updates` and writes the report (`unpack ok` /
`ok <ref>`). The duplex discipline *drains the raw tail packless*,
which is why the row had to signal client-side EOF after the report
flush (observed, the real client-eof shape) and also why vanilla
delete-pushes end cleanly. `GitPackIngest::prepare` never fires
packless, as asserted.
- **Per-build matrix**: unchanged rows in `tests/dispatch_core.rs`
(their observables hold, since the config-driven decline keeps the
failure-then-close shape in seam-less builds; unknown services keep
failure-then-alive), docs/stamps updated for the new decision set;
the git row's serving + grammar-decline rows live in the new
`tests/git_mapping.rs` (empty module in non-git builds, compile-time
feature isolation). `tests/observe.rs`'s git-row event row updated
to the `seam-unconfigured` reason.
- **Real-tool smoke posture** (recorded): the clone smoke (a real
`git clone` of an unborn-HEAD repo over a TCP-mounted adapter, on
the same loopback server stack, the accept loop as the assembly's
mount) **requires git ≥ 2.43 and ssh in the environment**;
missing/older tooling is skipped *with a printed reason*
(skipped-with-reason at most, never silently green). The empty-repo
clone exercises the full chain (exec parse → dispatch → the seam
advertisement → ls-refs → exit-status(0)) with zero pack bytes: the
unborn-HEAD line is what real git 2.43's clone consumed (POC-3's
OQ-19 rider shape), so the hand-rolled doubles stay noop-faithful.
- **Verification**: `cargo test` (default build), `cargo test
--features git --test git_mapping`, `cargo test --features session`,
`cargo test --all-features`, `cargo clippy --all-targets --all-
features -- -D warnings` ×2 (default + all-features), `cargo fmt
--check`, and `cargo doc --no-deps`: all green.