diff --git a/AGENTS.md b/AGENTS.md index c9f483a..9c0c1cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,8 +120,9 @@ implementation agents. 9. **Wire formats are stable one-way doors** — once a wire surface is published (the git smart protocol is defined upstream and not ours to - change; any alkgit-specific alkgit↔alkgit framing will get ADRs when it - exists), its shape must not change. Protocol capability advertisement + change; the one existing piece of alkgit-specific framing — the native + session preamble, ADR-016 — is pinned and in the OQ-03 freeze + inventory), its shape must not change. Protocol capability advertisement is honest: never advertise what we don't serve. 10. **Feature flags** — optional surface is feature-gated: `gix` diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 1edaafc..7a942aa 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -24,7 +24,10 @@ are the publish-freeze timing (OQ-03, a release decision), sha256 policy (OQ-05, deferred on ecosystem need), and the grant-key identity namespace (OQ-16, deferred on the first cross-assembly deployment — blocks nothing in v1). ADR-015 resolved -review 001's A-1 (the repo-op gate) with the `manage` grant tier. +review 001's A-1 (the repo-op gate) with the `manage` grant tier; ADR-016 +resolved its A-3 (the native session preamble — `{repo, service}` +open-op params, the git-daemon request line on the direct path, and the +service dimension in the session tuple). ## Architecture Documents @@ -55,6 +58,7 @@ review 001's A-1 (the repo-op gate) with the `manage` grant tier. | [013](decisions/013-receive-pack-state-machine.md) | receive-pack state machine (V0-framed push, thin-pack, unpack-first CAS) | Accepted | | [014](decisions/014-v2-negotiation-ack-loop.md) | V2 negotiation ack loop (no `ready`, wait-for-done stays) | Accepted | | [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant tier + repo-op gate (admin scope OR manage grant) | Accepted | +| [016](decisions/016-native-session-preamble.md) | Native session preamble (`{repo, service}` params, request line, service in tuple) | Accepted | ## Open Questions diff --git a/docs/architecture/backend.md b/docs/architecture/backend.md index b7cc5ca..868a108 100644 --- a/docs/architecture/backend.md +++ b/docs/architecture/backend.md @@ -143,9 +143,11 @@ alkgit ships both alkcall op kinds from one crate — the first family payload to do so (alktty/alktunnels: open ops only; alknet-docker, planned: call ops only): -- **Open op** (`alk/git` via `register_openable`; repo id in the - open-op params) — the binary half: negotiation + ACL point (ADR-007), - yields the duplex git session. +- **Open op** (`channels/git/sub` via `register_openable`; open-op + params `{repo, service}` — ADR-016) — the binary half: negotiation + + service selector + ACL point (ADR-007; the service selects the + `authorize` action, read for fetch / write for push), yields the + duplex git session. - **Call ops** (`git/repo/{create,delete,update,get}`) — the JSON half: thin `OperationSpec`+handler pairs over `Arc`, `Visibility::External`, always-on (no gitoxide). `create` is gated by @@ -173,6 +175,7 @@ planned: call ops only): | [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant + op gate | `manage` tier, admin-scope-OR-manage gate, create seeds manage | | [013](decisions/013-receive-pack-state-machine.md) | receive-pack | thin-pack ingestion, transaction CAS, report framing | | [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | ack loop, `common_haves` seam, no `ready` | +| [016](decisions/016-native-session-preamble.md) | Native session preamble | `{repo, service}` open-op params, request-line preamble, service in the tuple | ## Open Questions diff --git a/docs/architecture/decisions/002-front-door-blind-core.md b/docs/architecture/decisions/002-front-door-blind-core.md index 2f47ce6..9340501 100644 --- a/docs/architecture/decisions/002-front-door-blind-core.md +++ b/docs/architecture/decisions/002-front-door-blind-core.md @@ -1,7 +1,10 @@ # ADR-002: Front-door-blind core — the session boundary ## Status -Accepted + +Accepted (tuple amended by ADR-016: the session is per-service — +`service` ∈ {upload-pack, receive-pack} joins the tuple, selected at +establishment on every path) ## Context @@ -30,16 +33,24 @@ Alternatives considered: ## Decision The transport exposes two session entry points, both consuming the same -tuple (peer identity, resolved repo, limits): +tuple (peer identity, resolved repo, service, limits) — the service +(upload-pack vs receive-pack) is explicit at establishment on every path +(ADR-016): doors select it by route/exec command; the native paths carry +it in the open-op params or the in-band request line: 1. **Duplex session** (ssh, git://, any stream door): transport consumes a `BiStream`-shaped duplex byte stream (`AsyncRead + AsyncWrite + Unpin`, the alkcall `BiStream` contract, alkcall ADR-005/009) and runs the - advertise-once → command-loop state machine. Adapters hand it over after - ACL and repo resolution (ADR-007, ADR-008). + service-selected advertise-once → command-loop state machines (V2 + advertisement for fetch; the V0 ref advertisement for push — both are + server-emitted firsts, so the service must precede the stream). + Adapters hand it over after ACL and repo resolution (ADR-007, + ADR-008). 2. **Stateless session** (smart-http): transport consumes a (request-reader, response-writer) pair per http request and runs one - command per invocation, matching smart-http's stateless framing. The + command per invocation, matching smart-http's stateless framing; the + door's route supplies the service even here (nothing in a POST body + distinguishes the services before parsing — ADR-016). The same core state machines run under both entry points. Storage-facing side: the wire layer calls the backend traits for advertisement data, @@ -69,5 +80,6 @@ follow-up 2). (stateless shape validated) - alkcall ADR-005 (`BiStream` type), ADR-009 (BiStream as handler leaf) - ADR-005 (substrate types detail), ADR-007/008 (what adapters do before - calling transport) + calling transport), ADR-016 (the service selection this document's + tuple carries) - overview.md §"Crate map" \ No newline at end of file diff --git a/docs/architecture/decisions/005-session-substrate-types.md b/docs/architecture/decisions/005-session-substrate-types.md index 9ce51cb..2d43442 100644 --- a/docs/architecture/decisions/005-session-substrate-types.md +++ b/docs/architecture/decisions/005-session-substrate-types.md @@ -1,7 +1,9 @@ # ADR-005: Session substrate types — duplex + stateless over one state machine ## Status -Accepted + +Accepted (substrate inputs amended by ADR-016: both substrates receive +the service dimension explicitly) ## Context @@ -32,16 +34,19 @@ If adapters or handlers hand-roll any of this, each gets it subtly wrong. `alkgit` owns the substrate layer; doors and handlers see friendly types: -1. **`Session` (duplex)** — wraps (stream, limits) into the pkt-line - session: owns the split/compat bridge, the request reader (command line +1. **`Session` (duplex)** — wraps (stream, limits, service) into the + pkt-line session: owns the split/compat bridge, the request reader (command line + pre-delim lines + args + flush, with the `reset()` discipline and the unconditional-error-break rule), and the writer (sideband sink with - 65000-byte chunks, idempotent flush). Seed: POC-1's `serve_v2`. -2. **`StatelessRequest` (http)** — wraps (request-reader, response-writer) - per POST: performs the capability-dump skip, the delim-aware arg split, + 65000-byte chunks, idempotent flush). The service selects the + advertisement and state machine at start (ADR-016). Seed: POC-1's + `serve_v2`. +2. **`StatelessRequest` (http)** — wraps (request-reader, response-writer, + service) per POST: performs the capability-dump skip, the delim-aware arg split, and the flush-only response rule (no `0002`, idempotent flush, - 200-empty for flush-only probes). Seed: POC-3's `httpservice.rs` + - `BodyReader`. + 200-empty for flush-only probes); the door's route supplies the + service (nothing in a POST body distinguishes the services — ADR-016). + Seed: POC-3's `httpservice.rs` + `BodyReader`. 3. **One protocol core** — the V2 state machines (ADR-003) run under both substrates; the stateless substrate presents each POST as a command invocation. Version dispatch happens at session start, in one place. @@ -67,5 +72,6 @@ exactly one task drives a given sink (POC-2's `SidebandSink` contract). follow-ups 1–2 - `docs/research/poc3-findings.md` §"Course corrections" 1, follow-ups 2, 4 - `docs/research/gitoxide.md` §"Wire format" -- ADR-002 (substrate shapes), ADR-003 (V2-first), ADR-004 (sink = io::Write) +- ADR-002 (substrate shapes), ADR-003 (V2-first), ADR-004 (sink = io::Write), + ADR-016 (service selection in the substrate inputs) - transport.md §substrate \ No newline at end of file diff --git a/docs/architecture/decisions/010-pure-protocol-crate.md b/docs/architecture/decisions/010-pure-protocol-crate.md index 5a5f56d..170e2fe 100644 --- a/docs/architecture/decisions/010-pure-protocol-crate.md +++ b/docs/architecture/decisions/010-pure-protocol-crate.md @@ -19,10 +19,11 @@ The git service fits that pattern exactly: - **Producer half** = POC-1's substrate verbatim: an adapter implementing alkcall `ProtocolHandler` for the `alk/git` ALPN → `accept_bi()` → - duplex V2 session, plus a `register_openable` helper so a git session is + duplex V2 session, plus a `register_openable` helper so a git session is openable through the `alk/channels` multiplexer (alktty ADR-007/009 - pattern: the channels open-op carries the negotiation — here, the repo - id as params, which is also the natural ACL enforcement point). + pattern: the channels open-op carries the negotiation — here, the + `{repo, service}` params (ADR-016), which is also the natural ACL + enforcement point). - **Consumer half** = a typed `GitSession` client (alktty's `TtySession` analog) driving fetch/push against a remote alkgit service — the alkcall-native primitive for replication/mirroring in the alknet @@ -58,9 +59,12 @@ template: crates.io as `alkgit`. - **Producer half**: `GitAdapter` (direct `alk/git` ALPN via `ProtocolHandler`) + channels `register_openable` (open-op params carry - the repo id — the negotiation, and the ACL enforcement point). + `{repo, service}` — ADR-016's schema; the negotiation, the service + selector, and the ACL enforcement point; the service at open time is + what lets the gate run the correct `authorize` action per ADR-011/015). - **Consumer half**: `GitSession` typed client with `connect_direct` and - `open_via_channels` constructors. + `open_via_channels` constructors — both send the ADR-016 preamble + (request line / open-op params) including the service. - **Backend traits** (registry, refs, pack-gen, pack-ingest) in-crate; `gix` implementation behind the default-on `gix` feature (disable it to embed your own storage). diff --git a/docs/architecture/decisions/016-native-session-preamble.md b/docs/architecture/decisions/016-native-session-preamble.md new file mode 100644 index 0000000..c8066f2 --- /dev/null +++ b/docs/architecture/decisions/016-native-session-preamble.md @@ -0,0 +1,187 @@ +# 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 ''` / `git-receive-pack ''`, 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 \0host=\0\0version=2\0` + - push: `git-receive-pack \0host=\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//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 \ No newline at end of file diff --git a/docs/architecture/doors.md b/docs/architecture/doors.md index eeef15f..da893c7 100644 --- a/docs/architecture/doors.md +++ b/docs/architecture/doors.md @@ -87,11 +87,17 @@ an embedder assembly concern, not alkgit scope. ## The alkcall-native path (no adapter at all) -The `alk/git` ALPN producer and the channels open-op (repo id in the -open-op params) need zero door code — POC-1 is that shape verbatim. Any -alkcall-speaking client (including alkgit's own consumer half, -`GitSession`) can use it. This is the baseline path; the http/ssh doors -are conveniences layered on top for stock git clients. +The `alk/git` ALPN producer and the channels open-op (`channels/git/sub`, +params `{repo, service}` — ADR-016: the params are the negotiation, the +service selector, and the open-time ACL point; the establisher resolves +the repo and runs `authorize` with the action the service selects) need +zero door code — POC-1 is that shape verbatim. On the direct-ALPN path +`GitAdapter` parses the git-daemon request line +(`git-upload-pack \0host=…\0\0version=2\0` / +`git-receive-pack \0host=…\0`) as the in-band preamble (ADR-016). +Any alkcall-speaking client (including alkgit's own consumer half, +`GitSession`) can use either path. This is the baseline path; the +http/ssh doors are conveniences layered on top for stock git clients. ## Assembly (downstream responsibility) @@ -117,6 +123,7 @@ deployment's docs, not here. | [013](decisions/013-receive-pack-state-machine.md) | receive-pack | V0-framed push advertisement per door, report framing | | [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | one round per POST; ack section is per-round, stateless | | [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant + op gate | `manage` tier, admin-OR-manage op gate | +| [016](decisions/016-native-session-preamble.md) | Native session preamble | `{repo, service}` open-op params, request-line preamble, service in the tuple | ## Open Questions diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index 62df4fa..78de430 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -68,7 +68,11 @@ resolved in earlier cycles. OQ-16 is deferred but blocks nothing in v1 optional door features. What remains deferred is the publish-time API freeze itself: which type/feature/op names are pinned at first crates.io publish. ADR-012 added the `git/repo/*` op set (names + - schemas) to the freeze inventory. + schemas) to the freeze inventory; ADR-013/014 added the push/negotiation + trait surface (`GitPackIngest` binding, `GitPackGen::common_haves`); + ADR-016 added the native-path preamble wire shapes (the `{repo, + service}` open-op params schema and the git-daemon request-line + grammar — the last native-path wire surface). - **Door type**: one-way (API freeze is registry-visible to dependents) - **Priority**: medium - **Impacts**: blocks the first publish only, not implementation. @@ -77,9 +81,9 @@ resolved in earlier cycles. OQ-16 is deferred but blocks nothing in v1 [backend.md](backend.md) §public API and [transport.md](transport.md) §public API. ADR-013/014 added the push/negotiation trait surface (`GitPackIngest` binding, `GitPackGen::common_haves`) to the freeze - inventory. + inventory; ADR-016 added the native preamble wire shapes. - **Cross-references**: ADR-010, ADR-002, ADR-012, ADR-013, ADR-014, - backend.md, transport.md + ADR-016, backend.md, transport.md ## Theme: transport / protocol diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 12dd60d..cb926ca 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -29,8 +29,8 @@ Single crate `alkgit`: | Half | Contents | POC evidence | |---|---|---| -| Producer | `GitAdapter` (`alk/git` ALPN via alkcall `ProtocolHandler`), channels `register_openable` (repo id in open-op params — the negotiation + ACL point) | POC-1 verbatim | -| Consumer | `GitSession` typed client (`connect_direct`, `open_via_channels`) — the replication/mirroring primitive for alknet | new, small (TtySession analog) | +| Producer | `GitAdapter` (`alk/git` ALPN via alkcall `ProtocolHandler`; parses the ADR-016 request-line preamble), channels `register_openable` (`channels/git/sub` — open-op params `{repo, service}`, the negotiation + service selector + ACL point; ADR-016) | POC-1 verbatim | +| Consumer | `GitSession` typed client (`connect_direct`, `open_via_channels`) — sends the same ADR-016 preamble shapes | new, small (TtySession analog) | | Substrate | duplex session + stateless request/response layer (ADR-005); wire framing, V2 state machines (ADR-003) | POC-1, POC-3 | | Backends | `GitRegistry` (+ write supertrait), `GitRefs`, `GitPackGen`, `GitPackIngest` traits; impls behind the default-on `gix` (engine) and `registry-file` (records) features | POC-2 (gix impl) | | Management ops | `git/repo/*` call ops over `GitRegistryStore` (ADR-012 §3) — the JSON half alongside the `alk/git` open op (first dual-kind payload; ADR-012 §5) | thin over the store trait | @@ -79,7 +79,7 @@ multi-round negotiation are design-complete against real-client captures | ADR | Decision | Summary | |---|---|---| | [001](decisions/001-crate-decomposition.md) | Crate decomposition | **superseded by ADR-010** | -| [002](decisions/002-front-door-blind-core.md) | Session boundary | (identity, repo, stream, limits) — unchanged, load-bearing | +| [002](decisions/002-front-door-blind-core.md) | Session boundary | (identity, repo, service, stream, limits) — unchanged, load-bearing; service per ADR-016 | | [003](decisions/003-protocol-v2-first.md) | V2-first protocol | V2-only fetch both doors; push is V0-framed by upstream design (ADR-013); honest advertisement | | [004](decisions/004-pack-pipeline.md) | Pack pipeline | `data::output` gen / `data::input` ingestion | | [005](decisions/005-session-substrate-types.md) | Substrate types | duplex + stateless APIs over one state machine | @@ -93,7 +93,7 @@ multi-round negotiation are design-complete against real-client captures | [013](decisions/013-receive-pack-state-machine.md) | receive-pack | V0-framed push machine, thin-pack acceptance, unpack-first CAS | | [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | ack loop, `common_haves` seam, no `ready` | | [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant + op gate | `manage` tier, admin-OR-manage gate, create seeds manage | -| [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant + op gate | `manage` tier, admin-OR-manage gate, create seeds manage | +| [016](decisions/016-native-session-preamble.md) | Native session preamble | `{repo, service}` open-op params, request-line preamble, service in the tuple | ## Open Questions @@ -101,7 +101,9 @@ Key questions tracked in [open-questions.md](open-questions.md): - **OQ-03**: publish-time API freeze inventory (the `git/repo/*` op set and the trait family enter it; ADR-012, ADR-013/014's trait additions; - the ADR-015 three-action grant shape must land in it). + the ADR-015 three-action grant shape must land in it; the ADR-016 + native preamble wire shapes — `{repo, service}` params schema and the + request-line grammar — entered it). - **OQ-05**: sha256 policy (deferred(scope), low). - **OQ-16**: grant-key identity namespace (deferred(scope); blocks nothing in v1 — ADR-015 §7). diff --git a/docs/architecture/transport.md b/docs/architecture/transport.md index fbf63fa..1f2726e 100644 --- a/docs/architecture/transport.md +++ b/docs/architecture/transport.md @@ -22,16 +22,25 @@ full decision (what each substrate owns and why); the surface is: - **Duplex session** (the `alk/git` ALPN producer, channels-opened git sessions, embedder stream doors) — input: (peer identity, resolved repo - id, authorized-repo marker, duplex stream, `Limits`). The marker is a - type the door/adapter constructs only after the ADR-007 check passes - (skipping the check is a type error — ADR-007; D-3). Encapsulates the - split/compat/packetline - bridge, the request reader (delim-aware parsing, `reset()` discipline, - break-on-error), and the sideband writer. + id, service, authorized-repo marker, duplex stream, `Limits`). The + marker is a type the door/adapter constructs only after the ADR-007 + check passes (skipping the check is a type error — ADR-007; D-3). The + service is selected at establishment on every native path (ADR-016: + the channels open-op params `{repo, service}`, or the in-band + git-daemon request line the direct-ALPN `GitAdapter` parses, + POC-1-verbatim), and selects the state machine: the V2 capability + advertisement for fetch, the V0 ref advertisement for push — both are + server-emitted firsts, so the service must precede the stream. + Encapsulates the split/compat/packetline bridge, the request reader + (delim-aware parsing, `reset()` discipline, break-on-error), and the + sideband writer. - **Stateless session** (smart-http doors, e.g. alkhttp's future `git` - feature) — input: (peer identity, resolved repo id, authorized-repo - marker, request-reader, response-writer, `Limits`) per http POST; adds - the http-framing rules + feature) — input: (peer identity, resolved repo id, service, + authorized-repo marker, request-reader, response-writer, `Limits`) per + http POST. The door's route still selects the service (doors.md), but + the substrate input carries it explicitly — nothing in a POST body + distinguishes an upload-pack POST from a receive-pack POST before + parsing (ADR-016). Adds the http-framing rules (capability-dump skip, flush-only responses, probe handling). IO-abstract: the door supplies reader/writer; see [doors.md](doors.md) for the mounting. @@ -145,7 +154,8 @@ a partial ref list is the worst failure mode an advertisement can have Crate-root re-exports (alktty pattern; the full list in [backend.md](backend.md) §public API): `GitAdapter` + `register_openable`, -`GitSession`, substrate types, `Limits`, the backend traits (ADR-010's +`GitSession`, substrate types (including the service selector and the +authorized-repo marker), `Limits`, the backend traits (ADR-010's seam), protocol error enums; gix-feature types under the feature. Publish-freeze point: **OQ-03**. @@ -161,11 +171,13 @@ Publish-freeze point: **OQ-03**. | [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | wire layer is backend-trait-only | | [013](decisions/013-receive-pack-state-machine.md) | receive-pack | V0-framed push machine, thin-pack acceptance, unpack-first CAS | | [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | ack loop, no `ready`, wait-for-done stays | +| [016](decisions/016-native-session-preamble.md) | Native session preamble | `{repo, service}` open-op params, request-line preamble, service in the tuple | ## Open Questions - **OQ-03**: publish/API freeze (partially resolved — single-crate shape - settled by ADR-010). + settled by ADR-010; the native-path preamble shapes entered the freeze + inventory via ADR-016). - **OQ-05**: sha256 policy (deferred(scope)). - OQ-02 resolved (ADR-014 — ack loop design). - OQ-04 resolved (ADR-013 — receive-pack state machine). @@ -176,7 +188,7 @@ Publish-freeze point: **OQ-03**. `docs/research/poc3-findings.md` (the normative wire behavior — observed against real git, not docs' grammar) - `docs/research/push-captures.md` (the push-path normative record — - ADR-013's basis) + ADR-013's basis; the git:// request-line framing ADR-016 adopts) - `docs/research/negotiation-captures.md` (the negotiation normative record — ADR-014's basis) - `docs/research/git-protocol.md` (inventory + observed corrections) diff --git a/docs/reviews/001-architecture-pre-decomposition-review.md b/docs/reviews/001-architecture-pre-decomposition-review.md index 255d621..9a806d3 100644 --- a/docs/reviews/001-architecture-pre-decomposition-review.md +++ b/docs/reviews/001-architecture-pre-decomposition-review.md @@ -760,7 +760,7 @@ criticals are ADR-writing work, not code): |----|---------|----------------|--------|------|--------| | A-1 | op-gate OR not expressible in `AccessControl` | new ADR (or ADR-012 §3 amendment): handler-side two-tier check, create keeps static scope gate | small | none | **resolved (ADR-015)** — option (a) shape with the OR-term generalized to the `manage` grant | | A-2 | `async fn` traits not dyn-compatible | ADR-012 §1 + backend.md amendment: `#[async_trait]`; add `async-trait = "0.1"` to manifest | small | none | **resolved** — all five traits `#[async_trait]`, desugared boxed form pinned in the freeze inventory (OQ-03), dep in manifest | -| A-3 | native preamble / service dimension unpinned | new ADR: open-op params `{repo, service}`, session tuple + stateless entry gain the service selector, `GitAdapter` preamble pinned | moderate | wire-format (freeze inventory) | open | +| A-3 | native preamble / service dimension unpinned | new ADR: open-op params `{repo, service}`, session tuple + stateless entry gain the service selector, `GitAdapter` preamble pinned | moderate | wire-format (freeze inventory) | **resolved (ADR-016)** — `{repo, service}` open-op params (`channels/git/sub`, `additionalProperties: false`), git-daemon request line on the direct path (POC-1 verbatim, freeze inventory), service in both substrate tuples, `GitSession` mirrors the shapes | | A-4 | done-round boundary set unverified | ADR-014 §2 + transport.md clause: boundary = `common_haves`-filtered haves | trivial | none | **resolved** — boundary set is the recognized subset (`common_haves`-filtered), amendment clause in ADR-014 §2 + transport.md §fetch | | A-5 | consumer half unspecified | user scope decision, then amendment or small ADR (recommended: thin wrapper, deps carried with purpose) | small | scope | open | | A-6 | trait execution model unspecified | backend.md paragraph + transport.md rephrase: async traits, wire-layer permit, impl-internal spawn_blocking | small | none | **resolved** — backend.md concurrency model: wire layer enforces the ADR-009 permit around gen/ingest trait calls; impls own internal `spawn_blocking` (ADR-009/ADR-013 aligned) |