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.
240 lines
13 KiB
Markdown
240 lines
13 KiB
Markdown
---
|
||
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. |