- 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
187 lines
10 KiB
Markdown
187 lines
10 KiB
Markdown
# 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 |