Files
alkgit/docs/architecture/decisions/016-native-session-preamble.md
T
glm-5.3-flash 41b0894740 docs(architecture): ADR-016 — native session preamble (review 001 A-3)
- new ADR-016: channels open-op params pinned as {repo, service}
  (channels/git/sub, additionalProperties: false); direct-ALPN GitAdapter
  parses the git-daemon request line (POC-1-verbatim grammar, capture-
  backed); session tuple gains the service dimension on both substrates;
  GitSession mirrors the shapes; version deliberately stays out of the
  preamble (service fully determines the state machine)
- amend ADR-002/005/010 (tuple, substrate inputs, open-op params pin) and
  transport.md/doors.md/overview.md/backend.md accordingly
- add the ADR-016 wire shapes to OQ-03's freeze inventory; note in
  AGENTS.md convention 9 that the alkgit-specific framing now exists and
  is pinned
- review 001: A-3 marked resolved

verification: cargo test, clippy -D warnings, fmt --check, doc — clean
2026-09-29 08:50:54 +00:00

187 lines
10 KiB
Markdown
Raw 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.
# ADR-016: The native session preamble — service selection on the `alk/git` paths
## Status
Accepted (resolves review 001 A-3; amends ADR-002's session tuple,
ADR-005's substrate inputs, and ADR-010's open-op params pin)
## Context
Every door selects the git *service* (upload-pack vs receive-pack) before
the protocol starts: http by route (`POST /{repo}/git-upload-pack` vs
`…/git-receive-pack`, doors.md), ssh by the parsed exec command
(`git-upload-pack '<repo>'` / `git-receive-pack '<repo>'`, doors.md's
alkssh requirement). The native paths have no equivalent, and review 001
(A-3, critical) established that as written push is unservable over the
`alk/git` ALPN:
1. **Channels path** (`register_openable`): the open-op params are pinned
as "repo id" only (ADR-010, overview crate map). Both consequences the
review named follow: the open-time ACL point cannot evaluate the write
tier — the reason the repo id rides the params is that the open op is
the negotiation + ACL enforcement point (ADR-007's resolve→authorize
runs at open time), but `authorize(record, identity, action)` is
per-action (ADR-011/015: read for fetch, write for push) and with no
service in the params the gate cannot know which check to run; and the
session cannot choose its state machine — both advertisements are
server-emitted firsts on the duplex path (the V2 capability
advertisement per ADR-003; the V0 ref advertisement per ADR-013 §2),
so the server cannot even choose which first bytes to emit.
2. **Direct-ALPN path** (`GitAdapter`): no preamble is pinned anywhere.
The connection is symmetric — a push client waits for the server's ref
advertisement while the server waits for the client — so with no
in-band service carrier the path deadlocks by construction. POC-1's
bridge carried the service in-band (the git-daemon request line) but
that framing was never adopted as an alkgit contract; AGENTS.md
convention 9 requires alkgit-specific wire framing to get an ADR, and
none existed.
3. **The session tuple lacks the dimension**: ADR-002's tuple and
transport.md's substrate inputs are "(peer identity, resolved repo,
limits)" (+ the ADR-007 authorized-repo marker) — no service selector.
Even on the stateless substrate, where the door's route selects the
service, the substrate input must carry it explicitly: nothing in a
POST body reliably distinguishes an upload-pack POST from a
receive-pack POST before parsing. `GitSession` (the consumer half)
mirrors whatever is pinned here.
The version dimension is *not* part of this gap: fetch is V2-only
(ADR-003 — V0/V1 fetch clients get a clear error) and push is V0-framed
unconditionally (ADR-013 §1 — no version negotiation exists on the push
path), so the service alone fully determines the state machine. One
field suffices; this ADR states that as the reason the shape is stable
under future version policy changes.
## Decision
**The service is explicit at session establishment on every native path;
the preamble carriers are the channels open-op params (JSON
`{repo, service}`) and the git-daemon request line (direct ALPN), and
the session tuple gains the service dimension.**
1. **Channels open-op params are `{repo, service}`** — JSON, both
required, `service ∈ {"git-upload-pack", "git-receive-pack"}`
(string enum), `additionalProperties: false` (the alksocks `{}`-schema
precedent: unknown fields fail rather than get silently ignored, so
extensions stay additive and fail-closed — the first extension already
exists at v1). The op is `channels/git/sub` with the
`ChannelOpenSpec::new("alk/git")` marker (the alktty
`channels/tty/sub` convention, alkcall ADR-047). The params are the
open-time ACL point (ADR-010 unchanged in *position* — the establisher
resolves the repo and runs `authorize(record, identity, read|write)`
with the action selected by the service; write requires identity plus
the write-or-manage grant per ADR-011/015) *and* the service selector:
the service at open time is what lets the gate run the correct check
instead of deferring the write check into the session. `git-upload-
archive` is not in the enum, so the open op rejects it at the gate —
the same fixed refusal every door applies, one layer earlier.
Registry-side errors map into alkcall's `channel:open_failed`
vocabulary (the establisher pattern; no phantom channel). The
registry-side ACL in the `AccessControl` stays empty (per-repo grant
checks are handler-side per ADR-011/015 — there is no static scope
gate to express them with).
2. **Direct-ALPN `GitAdapter` parses the git-daemon request line**
(POC-1 verbatim, capture-backed against real git 2.43 — poc-1-findings
§2, push-captures git:// framing): one pkt-line, client-sent before
the server speaks —
- fetch: `git-upload-pack <repo>\0host=<host>\0\0version=2\0`
- push: `git-receive-pack <repo>\0host=<host>\0`
The server parses the line, resolves the repo, runs
`authorize` with the action the service selects, and only then emits
the advertisement (ADR-007's resolve→authorize order, unchanged).
`host` and `version=2` extras are parsed and accepted verbatim; the
version then flows through the existing dispatch exactly as if real
git had spoken — a V0/V1 fetch request gets ADR-003's existing clear
error (the request line carries `version=2` for fetch; a request line
without it, or with an unrecognized version token, is the same
V0/V1-decline path). Malformed lines (unknown service, empty repo id)
fail before any advertisement; repo resolution and authorization
failures collapse per ADR-008 (unknown ≡ unauthorized). This is
alkgit-specific wire format on a published ALPN — it enters OQ-03's
freeze inventory (one-way). The grammar is also the one a git://-to-
`alk/git` bridge would need anyway, and it is the framing
`GitSession::connect_direct` sends — the two native paths speak one
preamble dialect.
3. **The session tuple gains the service dimension.**
- Duplex: `(identity, repo, service, authorized-repo marker, stream,
limits)`.
- Stateless: `(identity, repo, service, marker, request-reader,
response-writer, limits)` — the door's route still selects the
service (doors.md), but the substrate input carries it explicitly.
- The service in the tuple is *the* state-machine selector: the
transport layer dispatches on it alone (version never rides the
preamble — consequence of the fetch-V2-only / push-V0-framed split
above).
4. **`GitSession` mirrors the same shapes**: `connect_direct` sends the
request-line preamble before waiting for the server; `open_via_
channels` sends the same `{repo, service}` params schema.
5. **Rejected alternatives** (recorded so the decomposer does not
reinvent them):
- *Repo-only params + service learned in-band post-open*: the open
gate would admit the channel before knowing the action, deferring
the write check past channel establishment — directly weakening the
ADR-007 posture the params exist to serve.
- *Two open ops* (`…/up`, `…/rec`): buys nothing — per-repo grant
checks are handler-evaluated (ADR-011/015), not static per-op scope
gates, so separate ops get no extra enforcement from the registry;
they double the registration and discovery surface.
- *JSON preamble on the direct path*: a binary pkt-line stream would
carry a length-framed JSON dialect nobody can debug with standard
git tooling, inventing a second preamble form to keep in sync with
the channels params for zero benefit.
- *No preamble / out-of-band service on the direct path*: the
connection is symmetric; without an in-band carrier the path
deadlocks at birth.
- *Two ALPNs* (`alk/git-up`, `alk/git-rec`): breaks the one-ALPN-per-
payload family convention (`alk/tty`, `alk/tunnel`, `alk/socks5`)
and doubles every door's surface.
## Consequences
- **Positive:** push over `alk/git` is servable (the server knows which
advertisement to emit); the open-time gate evaluates the correct
`authorize` action — the write tier is enforced at the gate the way
ADR-007 intends, not deferred into the session; `GitSession`'s
constructor shapes become concrete (A-5 unblock); the freeze inventory
gains its last native-path wire surface, pinned *before* decomposition
as the review required.
- **Negative:** the `{repo, service}` params schema and the request-line
grammar are alkgit-specific wire surfaces on a published ALPN
(one-way doors; OQ-03 inventory) — pinned exactly so nothing ad-hoc
hardens first. The stateless substrate gains one required input every
http door must now supply (the route already knows it).
- **Neutral:** the version dimension stays where it already lives
(ADR-003 fetch dispatch / ADR-013 push framing); the direct-path
request line parses-and-accepts `host=` and `version=2` so a future
protocol version rides an established grammar, not a new field.
## References
- Review 001 A-3 (the trigger; the recommendation this ADR adopts after
deliberation), OQ-03 (the freeze inventory this ADR's wire shapes enter)
- ADR-002 (session boundary — tuple amended), ADR-005 (substrate types —
inputs amended), ADR-010 (open-op params pin — amended; producer/
consumer halves unchanged), ADR-007 (resolve→authorize before any
protocol byte), ADR-008 (unknown ≡ unauthorized), ADR-003 (V2-only
fetch; the version-decline path), ADR-013 §1–2 (push V0-framed;
server-speaks-first advertisement), ADR-011/015 (`authorize` actions,
write grant)
- alkcall ADR-047 (channel open ops are operations; `channels/<alpn>/sub`
convention), ADR-049 (the establisher phase — open-time validation and
ACL-adjacent checks), alktty `src/channels.rs` (`channels/tty/sub`
template: partial registry schema + establisher full-parse + ACL)
- `docs/research/poc-1-findings.md` §2 (the capture-backed request-line
shape), `docs/research/push-captures.md` (git:// framing of the push
request line), AGENTS.md convention 9 (alkgit-specific framing needs
this ADR)
- transport.md §substrate, doors.md §"The alkcall-native path",
overview.md §crate map