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

10 KiB
Raw Blame History

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