docs(architecture): resolve OQ-04 — receive-pack state machine (ADR-013)

- ADR-013: V0-framed push machine grounded in real git 2.43.0 captures
  (file://, git://, smart-http mock, raw stdio into real receive-pack):
  V0-shaped ref advertisement (caps on first ref line, capabilities^{}
  sentinel only for empty repos), served capability set, shallow requests
  rejected for v1, thin packs accepted with server-odb bases (no
  capability involved; push.thin default), ingestion bound to
  Bundle::write_to_directory_eagerly + gix-fsck + one gix-ref transaction
  per push (.keep-guarded), unpack-first CAS timing with observed
  upstream order, band-1 pkt-line-framed status report, http framing
  (probe/Content-Length/chunked), v1 update policy (CAS only; deletes
  and force-push allowed)
- docs/research/push-captures.md: the normative push wire record
- transport.md/backend.md/doors.md: receive-pack sections rewritten to
  the decided shapes; backend.md ingestion composition bound; stale
  OQ-04 references resolved
- ADR-003 amended: V2-only governs fetch; push is V0-framed by upstream
  design (fixes the V2-only contradiction found in review)
- ADR-009 amended: haves default reconciled with the client's stateless
  ceiling (16384); blocking-pipeline budget covers generation+ingestion
- OQ-04 resolved; tracker task closed; CAS-fail-fast optimization
  tracked (tasks/architecture/oq-13-cas-failfast.md)
- research index: poc findings + capture docs listed

Verification: cargo test / clippy -D warnings / fmt --check / doc pass
This commit is contained in:
glm-5.3-flash committed 2026-09-25 04:05:07 +00:00
1 parent dbb056f451
commit e76f91f6d7
14 files changed
+621 -138

No files matched your search

+15 -11
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-09-21 last_updated: 2026-09-25
--- ---
# alkgit Architecture # alkgit Architecture
@@ -14,12 +14,14 @@ to a POC finding or research doc, or is flagged as an open question.
## Current State ## Current State
Phase 1, architecture committed to the pure-protocol-crate shape (ADR-010; Phase 1, architecture committed to the pure-protocol-crate shape (ADR-010;
OQ-09 resolved). All docs below are `draft` except the superseded ADRs. OQ-09 resolved). POC-1/2/3 validated the git protocol half end-to-end
POC-1/2/3 validated the git protocol half end-to-end against real git against real git 2.43. Previous cycles settled the auth/backend theme
2.43. This cycle settled the auth/backend theme: per-repo authorization (ADR-011, ADR-012). This cycle settled the wire surface against
(ADR-011, OQ-08), registry backing + write surface + CRUD ops (ADR-012, real-client captures: the receive-pack push state machine (ADR-013,
OQ-06/OQ-07). The remaining design work is the receive-pack state machine OQ-04) and the V2 multi-round negotiation ack loop (ADR-014, OQ-02). All
(OQ-04) and V2 multi-round negotiation (OQ-02). wire-layer design is now capture-grounded; the remaining open questions
are the publish-freeze timing (OQ-03, a release decision) and sha256
policy (OQ-05, deferred on ecosystem need).
## Architecture Documents ## Architecture Documents
@@ -47,14 +49,16 @@ OQ-06/OQ-07). The remaining design work is the receive-pack state machine
| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate (alktty/alktunnels template) | Accepted | | [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate (alktty/alktunnels template) | Accepted |
| [011](decisions/011-per-repo-authorization.md) | Per-repo authorization (grants in records, policy in core) | Accepted | | [011](decisions/011-per-repo-authorization.md) | Per-repo authorization (grants in records, policy in core) | Accepted |
| [012](decisions/012-registry-backing-and-ops.md) | Registry backing, write surface, CRUD ops, feature split | Accepted | | [012](decisions/012-registry-backing-and-ops.md) | Registry backing, write surface, CRUD ops, feature split | Accepted |
| [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 |
## Open Questions ## Open Questions
All unresolved questions are tracked in [open-questions.md](open-questions.md) All unresolved questions are tracked in [open-questions.md](open-questions.md)
with stable OQ-IDs, priorities, and cross-references. Highest-priority with stable OQ-IDs, priorities, and cross-references. Remaining: OQ-03
open: OQ-04 (receive-pack validation). Also open: OQ-02 (multi-round (publish freeze inventory — partially resolved, blocked on first-publish
negotiation), OQ-03 (publish freeze inventory), OQ-05 (sha256, timing) and OQ-05 (sha256, deferred on ecosystem need). The wire-layer
deferred). questions (OQ-02, OQ-04) resolved this cycle with ADR-014/ADR-013.
## Document Lifecycle ## Document Lifecycle
+30 -16
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-09-21 last_updated: 2026-09-25
--- ---
# Backend traits: the storage seam # Backend traits: the storage seam
@@ -36,20 +36,28 @@ gix types:
for receive-pack, name validation per git ref rules + reserved- for receive-pack, name validation per git ref rules + reserved-
namespace deny-list). namespace deny-list).
4. **`GitPackGen`** — (repo, wants, haves, limits) → streaming pack 4. **`GitPackGen`** — (repo, wants, haves, limits) → streaming pack
(`io::Write` consumer). Negotiation-agnostic. Missing objects abort (`io::Write` consumer), plus `common_haves(repo, haves) -> recognized
with an error, never a broken pack (ADR-004). subset` for the negotiation ack loop (a have is recognized iff it
exists in the object store and is a commit — the honest boundary:
never ack what we cannot subtract; ADR-014). Negotiation-agnostic
generation. Missing objects abort with an error, never a broken pack
(ADR-004).
5. **`GitPackIngest`** — client pack stream → indexed pack + fsck/ 5. **`GitPackIngest`** — client pack stream → indexed pack + fsck/
connectivity report. It *prepares* the validated ref updates; the connectivity report. Thin-pack bases resolve from the server's own odb
transaction itself is applied by `GitRefs` (single CAS home — ingest (push sends thin packs by default — ADR-013 §5); the pack lands with a
validates, refs commits). Budgeted (ADR-009 max pack size); `.keep` guard; missing objects → `unpack ng`. It *prepares* the
blocking-thread friendly. validated ref updates; the transaction itself is applied by `GitRefs`
(single CAS home — ingest validates, refs commits; one transaction per
push is what makes `atomic` correct — ADR-013 §7). Budgeted (ADR-009
max pack size); blocking-thread friendly.
Minimal-vs-full was the open sub-question; resolved as **full family** — Minimal-vs-full was the open sub-question; resolved as **full family** —
the four traits are each one or two methods plus types, and collapsing each trait is one or two methods plus types, and collapsing them (e.g.
them (e.g. refs into the registry) would force one impl block per refs into the registry) would force one impl block per downstream where
downstream where independent seams are cheaper to satisfy. The `gix` independent seams are cheaper to satisfy. The `gix` feature implements
feature implements all four; a downstream with its own object store the three object-storage traits (3–5: `GitRefs`, `GitPackGen`,
implements 3–4 and reuses 1–2, or none of it. `GitPackIngest`); a downstream with its own object store implements 3–5
and reuses 1–2 (`GitRegistry`/`GitRegistryStore`), or none of it.
## Feature model (ADR-012 §4, amends ADR-010's single-`gix` story) ## Feature model (ADR-012 §4, amends ADR-010's single-`gix` story)
@@ -77,9 +85,10 @@ Two independent seams, two default-on features:
(`Arc<Store>` shared, per-session handles, `prevent_pack_unload()` + (`Arc<Store>` shared, per-session handles, `prevent_pack_unload()` +
`ignore_replacements = true`), generation on blocking threads, `ignore_replacements = true`), generation on blocking threads,
O(counts) memory, missing-objects abort. O(counts) memory, missing-objects abort.
- Received-pack ingestion via `gix-pack::data::input` (`streaming-input`) - Received-pack ingestion via `gix-pack::Bundle::write_to_directory_eagerly`
+ `gix-fsck` + `gix-ref` transactions (ADR-004). Validation against (`streaming-input`, thin-base lookup) + `gix-fsck` + `gix-ref`
real `git push` is OQ-04. transactions with `PreviousValue::MustExistAndMatch` CAS (ADR-004, ADR-013
— the full push state machine is decided there).
## Concurrency model ## Concurrency model
@@ -132,12 +141,17 @@ planned: call ops only):
| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | traits in-crate, impls behind features | | [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | traits in-crate, impls behind features |
| [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | grants in records, policy in core | | [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | grants in records, policy in core |
| [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | write trait, file default, CRUD ops, feature split | | [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | write trait, file default, CRUD ops, feature split |
| [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` |
## Open Questions ## Open Questions
- **OQ-04**: pack ingestion validation (deferred(unclear)).
- **OQ-05**: sha256 policy (deferred(scope)). - **OQ-05**: sha256 policy (deferred(scope)).
- **OQ-03**: publish-time API freeze inventory (the op set enters it). - **OQ-03**: publish-time API freeze inventory (the op set enters it).
- OQ-04 resolved (ADR-013 — ingestion composition bound: thin-base
lookup, `.keep` landing, transaction CAS).
- OQ-02 resolved (ADR-014 — `common_haves` ack seam added to
`GitPackGen`).
## References ## References
@@ -34,9 +34,10 @@ Decision drivers:
exactly as observed in POC-1 (re-advertising between commands hangs real exactly as observed in POC-1 (re-advertising between commands hangs real
git). git).
- Advertised capabilities are exactly what we serve: `ls-refs=unborn`, - Advertised capabilities are exactly what we serve: `ls-refs=unborn`,
`fetch=wait-for-done` (v1 fetch policy is full-closure pack on `done`; `fetch=wait-for-done` (the full-closure pack on `done` policy
multi-round negotiation, OQ-02, may add capability values when it validated in POC-1/2; the multi-round ack loop, ADR-014, needs no
lands), `object-format=sha1` (+ sha256 under the feature flag, if and capability change — the acknowledgments section is grammar, not
capability), `object-format=sha1` (+ sha256 under the feature flag, if and
when tested — OQ-05). Nothing unimplemented is advertised when tested — OQ-05). Nothing unimplemented is advertised
(shallow, filter, packfile-uris, object-info, server-option are (shallow, filter, packfile-uris, object-info, server-option are
*declined by omission*; POC-1 confirmed real git accepts this). *declined by omission*; POC-1 confirmed real git accepts this).
@@ -46,23 +47,22 @@ Decision drivers:
- HTTP requests protocol V2 only; ssh requests protocol V2 only (see - HTTP requests protocol V2 only; ssh requests protocol V2 only (see
below). below).
**V0/V1 policy: decided now as V2-only for v1.** Both doors **V0/V1 policy: decided now as V2-only for v1 — fetch only.** Both doors
(http and ssh) speak V2; clients that cannot speak V2 get a clear pkt-line (http and ssh) speak V2 for *fetch*; clients that cannot speak V2 get a
error. Reversal is wire-visible (the advertised version set is a wire clear pkt-line error. Reversal is wire-visible (the advertised version set is a wire
format), so treat this as effectively one-way once published — the format), so treat this as effectively one-way once published — the
decision is still made now, and revisiting it needs a new ADR plus a decision is still made now, and revisiting it needs a new ADR plus a
deprecation window for clients. The motivation stands: a concrete consumer deprecation window for clients. The motivation stands: a concrete consumer
needing V0/V1 (e.g. very old CI images) has not been identified. needing V0/V1 (e.g. very old CI images) has not been identified.
The V0/V1 receive-pack shape (used by `git push` on http stateless framing **Receive-pack is exempt**: the push path is V0-framed by upstream design
in older clients) is unaffected by this choice: receive-pack is mostly on every client generation (no version negotiation exists on push at
version-independent (see transport.md). all — ADR-013 §1), so the V2-only rule never touches `git push`. The
V0/V1 decline applies to the fetch path only.
Sequencing note (fixes the scope contradiction the review found): the v1 Sequencing note: the v1 fetch policy that ships first is full-closure-on-`done` (POC-validated).
fetch policy that ships first is full-closure-on-`done` (POC-validated).
Multi-round V2 negotiation (haves/acks without `done`) is in v1's *scope* Multi-round V2 negotiation (haves/acks without `done`) is in v1's *scope*
but lands after the done-path works end-to-end; its ack logic is OQ-02's but lands after the done-path works end-to-end; its ack loop is now
open question, and any capability-advertisement change it requires decided (ADR-014) and needs no capability-advertisement change.
happens then.
## Consequences ## Consequences
@@ -52,9 +52,9 @@ band), never emit a broken pack — check entry statistics
**Receive-side ingestion** parses the client pack stream **Receive-side ingestion** parses the client pack stream
(`gix-pack::data::input` with `streaming-input`), fscks it (`gix-pack::data::input` with `streaming-input`), fscks it
(`gix-fsck` connectivity), and applies ref updates via `gix-ref` (`gix-fsck` connectivity), and applies ref updates via `gix-ref`
transaction CAS. The exact push state machine (capability advertisement transaction CAS. The push state machine (capability advertisement
set, CAS timing, status report, http framing) is OQ-04's investigation set, CAS timing, status report, http framing) is decided in ADR-013;
target; the ingestion-tool choice above is decided. the ingestion-tool choice above is decided.
**Delta synthesis** for loose objects is an optimization backlog item, not **Delta synthesis** for loose objects is an optimization backlog item, not
a v1 commitment: existing pack deltas copy through for free; loose objects a v1 commitment: existing pack deltas copy through for free; loose objects
@@ -33,11 +33,11 @@ overrides are assembler config.
| Budget | Applies to | Default direction | | Budget | Applies to | Default direction |
|---|---|---| |---|---|---|
| max negotiation rounds | fetch (V2, no `done`) | tens | | max negotiation rounds | fetch (V2, no `done`) | tens |
| max haves per round | fetch | thousands | | max haves per round | fetch | thousands (the client's stateless doubling can legitimately reach 16384 — set the default at or above that) |
| max pack size | receive-pack | config-bound (tens of MB v1) | | max pack size | receive-pack | config-bound (tens of MB v1) |
| max request body | http POSTs (receive-pack especially) | same as max pack size | | max request body | http POSTs (receive-pack especially) | same as max pack size |
| session wall clock | all sessions (enforced by transport's session loop on every door — it is the one component all doors hand the session to; alkcall channel caps add a second bound where channels exist) | tens of minutes | | session wall clock | all sessions (enforced by transport's session loop on every door — it is the one component all doors hand the session to; alkcall channel caps add a second bound where channels exist) | tens of minutes |
| max concurrent pack generations | server-wide (blocking-pool budget) | small count | | max concurrent blocking pipeline tasks | server-wide (blocking-pool budget; covers pack generation and pack ingestion alike — both are `spawn_blocking` consumers) | small count |
| sideband chunk size | fetch streaming | 65000 (fixed, per protocol) | | sideband chunk size | fetch streaming | 65000 (fixed, per protocol) |
| max advertisement refs | ls-refs response | config-bound | | max advertisement refs | ls-refs response | config-bound |
@@ -0,0 +1,231 @@
# ADR-013: receive-pack (push) state machine
## Status
Accepted (resolves OQ-04)
## Context
OQ-04 (`deferred(unclear)`) held the receive-pack validation gap: the
pieces were decided (ADR-004 ingestion via `gix-pack::data::input` with
`streaming-input`; `gix-ref` transaction CAS; `gix-fsck` connectivity;
POC-3: request bodies stream) but the push state machine's shape was not.
The listed unknowns: capability advertisement set under the
honest-advertisement invariant, request-line parsing + shallow policy,
thin-pack acceptance, ingestion composition, CAS timing, status-report
shape, and the http framing surface (ADR-003's V2 decision covers fetch
only).
Resolution method: a walkthrough against real `git push` captures
(git 2.43.0 over file://, git:// daemon, and smart-http against a
ground-truth mock receive-pack), raw stdio requests driven into real
`git receive-pack` for policy-error ground truth, and gitoxide source
verification of the ingestion composition. Captures are recorded in
`docs/research/push-captures.md`.
## Decision
**Receive-pack is V0-framed, by upstream design, and we serve it as such.**
1. **Version surface**: receive-pack has no version negotiation at all —
`git push` never speaks V2 on the push path (observed: `protocol.v2`
clients push with the identical V0/V1 shape; no `version=2` exchange
exists). ADR-003's V2-first decision therefore governs fetch only; the
push side is V0-framed on every door. This is upstream's shape, not a
choice of ours, and it is wire-stable by the same token.
2. **Capability advertisement** (per-door, honest per ADR-003):
- Smart-http GET: `# service=git-receive-pack` + flush, then a
**V0-shaped ref advertisement** — capabilities ride NUL-attached on
the FIRST ref line; remaining ref lines are bare; flush. The
`capabilities^{}` zero-id sentinel replaces the ref lines only when
the repo has no refs (git rejects a bare capability dump, and rejects
a trailing sentinel when refs exist).
- Duplex: the same ref advertisement without the service prefix; the
server speaks first.
- The served set is `report-status report-status-v2 delete-refs
side-band-64k atomic ofs-delta object-format=sha1` (+ `push-options`
under a config gate). **`quiet` is not advertised** (it is a
client-side preference whose only server duty is suppressing band-2
progress chatter — we never send progress). `agent=alkgit/<version>`
is advertised (same convention upstream uses; value is the crate
version). `shallow`/`allow-tip-sha1-in-want`-style fetch caps do
not appear here.
- Everything we do not serve (e.g. `push-cert`) is declined by omission.
- `report-status-v2` is served: the v1 report shape (no option
lines) IS valid v2 — option lines exist only when the server emits
them, and v1 emits none (push-options are metadata-only in v1).
Clients selecting v2 parse the same shape.
- ACL runs before the first ref line (ADR-007 unchanged): private
repos advertise nothing.
3. **Request parsing** (one POST / one stream):
```
command lines (<old> <new> <ref>, caps NUL-attached on line 1)
[shallow lines] → v1: rejected up front with a pkt-line error (see 4)
flush
[push-options section: bare pkt-lines + flush] (only if negotiated)
PACK stream (raw, to end of body / stream)
```
- Deletes: `old=<current> new=zero-id`; creates: `old=zero-id`.
- An immediate flush (no commands) is a client-side nothing-to-do —
reply flush, no report.
- A pack is ALWAYS expected after the command flush (real receive-pack
errors with `unpack eof …` + all-refs `unpacker error` otherwise).
An empty pack (zero objects) is valid and reported as such.
4. **Shallow on push: rejected for v1** — confirming the OQ's expected
resolution. `git push` from a shallow clone sends `shallow <sha>`
lines before the commands. Real receive-pack accepts them when the
pack completes history and rejects with
`ng <ref> shallow update not allowed` otherwise. We serve neither:
shallow request lines are rejected up front with a clear pkt-line error
(mirroring fetch's decline-by-omission in ADR-003, keeping depth
semantics symmetric). Declining up front is cheaper than accepting
packs we cannot fsck: a shallow push's pack terminates on grafted
boundaries that our connectivity check cannot close, and
`receive.shallowUpdateDeepen`-style semantics are a scope decision for
a later ADR, not a v1 need. This is a two-way door: adding shallow
support later is additive (the shallow-request capability rides
upstream's existing grammar when implemented).
5. **Thin packs: accepted, bases from the server odb.** `git push` sends
thin packs by default (`push.thin=true`, no capability involved):
captured packs exclude objects the client believes the server has and
fail `git index-pack --strict` standalone; `--fix-thin` completes them.
Ingestion therefore passes the server's own `gix_object::Find` handle
as `thin_pack_base_object_lookup` to `Bundle::write_to_directory` (the
upstream parameter exists for exactly this); the no-lookup
composition (`Option<Never>` — a lookup that can never resolve,
valid only for packs known self-contained, per poc2-findings) is not
used on the push path. Thin-pack base
availability is enforced by the lookup: a base the server lacks fails
ingestion (→ `unpack ng`), which is the correct honest outcome.
6. **Ingestion composition** (ADR-004's tool choice, now bound to the
push machine):
- Stream the pack body into `Bundle::write_to_directory_eagerly` (=
`data::input` `streaming-input` with `LookupRefDeltaObjectsIter` when
bases resolve, `EntryDataMode::KeepAndCrc32`, `Mode::Verify`),
`directory` = the repo's pack dir: the outcome is a written pack +
index with a `.keep` file created before the pack lands (gc-safe
against the not-yet-applied refs) — gitoxide's own receive shape.
- Budgets (ADR-009): max pack size counts the streamed body
(413-mapped on http); wall clock and the blocking-pipeline
concurrency budget apply (the generation budget covers ingestion —
same `spawn_blocking` pool, amended in ADR-009's table).
- Ingestion runs on `spawn_blocking` (POC-2's shape: store shared,
handle per session).
- fsck/connectivity: `gix-fsck::Connectivity` over the odb handle
after indexing, for each new tip reachable from the pushed refs;
missing objects → `unpack ng <reason>`.
- The ingest trait (`GitPackIngest`) **prepares** validated updates;
the ref transaction itself is applied by `GitRefs` (single CAS home,
backend.md unchanged).
7. **CAS timing: unpack-first, then per-ref checks** (the observed
upstream order, adopted): parse commands → read + index + fsck the
pack → per-ref validation (name, CAS, policy) → atomic rollback if
selected → report. Evidence: real receive-pack reports `unpack ok` +
`ng <ref> funny refname` / `deletion prohibited` / stale-old ng only
AFTER unpack; a missing pack fails everything at unpack. A pre-unpack
CAS pre-check is permitted as a fail-fast optimization (reject before
reading the body), but the protocol-correct order is as observed and
is what v1 implements. Per-ref CAS uses `gix-ref` transactions
(`Change::Update/Delete` with `PreviousValue::MustExistAndMatch` /
`MustNotExist`) — one transaction for the whole push, which is what
makes `atomic` correct. **The v1 update policy** (the "policy" leg of
the per-ref check): CAS on old-value per the request (zero-id create,
current-id update/delete) and nothing more — deletes are allowed
(the `delete-refs` capability is advertised) and non-fast-forward
updates are allowed (force-push is the client's explicit choice via
the advertised old value; the CAS contract is exact-match, and
upstream's `denyNonFastForwards`-style history checks are a
deployment concern, expressible later as config in the assembly
layer without a trait change — the reasons observed above come from
upstream's *config-dependent* policies, not protocol requirements).
A reason alkgit emits itself (`funny refname`, `shallow update not
allowed`, `atomic push failure`, CAS failures) needs no config.
8. **Status report**: when the client selected `side-band-64k`, the
report is band-1 chunks whose payload is ITSELF pkt-line-framed:
`unpack ok|ng <reason>`, per-ref `ok <ref>` / `ng <ref> <reason>`,
an INNER flush terminating the report section, then the OUTER flush.
Without sideband: bare pkt-lines to one flush. The report is
`unpack ok` even when some refs fail; pack-level failure is
`unpack ng <reason>` + all-refs `unpacker error`. Observed per-ref
reasons from real receive-pack are forwarded verbatim (`shallow
update not allowed`, `deletion prohibited`, `non-fast-forward`,
`atomic push failure`, `funny refname`); CAS-stale failures carry a
server-chosen reason (upstream uses `stale info`-style text
internally; ours is free to pick — clients display it verbatim).
Human-readable diagnostics ride band-2. The
substrate owns the framing (a report sent unwrapped when sideband was
selected aborts real clients — observed `fatal: protocol error: bad
line length character`), so handlers cannot get it wrong. `quiet`
suppresses progress (which we never send), not errors — band-2
diagnostics are always permitted.
9. **Ref-name validation** (per-ref, after unpack): `gix_validate::
reference::name` plus alkgit's deny-list — the git-protocol.md
security note, now pinned to the exact validation point and reason
string (`funny refname`). The deny-list is a small constant in the
transport layer (git-refname-invalid namespaces: names ending in
`.lock`, `refs/` reserved prefixes used by our own metadata — the
list lives with the validation code; exact contents are an
implementation-time detail following `git check-ref-format` rules,
not an architecture variable).
10. **Atomic**: when `atomic` was negotiated and any ref fails, every
other ref reports `ng <ref> atomic push failure` (observed live).
Implemented by preparing the whole `gix-ref` transaction and committing
it once; per-ref results map from the transaction outcome.
11. **Push-options**: negotiated (`push-options` advertised only when the
assembler enables it; default off in v1), request section parsed
(bare pkt-lines between the command flush and the pack) and surfaced
to the ingest/refs seam as per-push metadata. Rejection of specific
options is `ng <ref> <reason>`; the section's absence when not
negotiated must not be parsed as commands (the flush boundary is
authoritative).
12. **Http framing for push** (POC-3 facts, now extended): small pushes
arrive Content-Length; large pushes arrive chunked, and the client
may send a 4-byte `0000` probe POST first, answered 200-empty. The
POST Content-Type is `application/x-git-receive-pack-request`; the
response is `application/x-git-receive-pack-result` (sideband-wrapped
report when negotiated), ending at a flush — the stateless substrate
(ADR-005) owns these rules unchanged.
## Consequences
- **Positive**: every wire shape in the push path is POC/capture-grounded
(not grammar-inferred); the ingestion composition binds ADR-004's tools
to concrete calls, with thin-pack handling first-class upstream
(`.keep`-guarded pack landing, base lookup); `atomic` falls out of the
single-transaction commit; the honest-advertisement invariant holds with
a small served set; OQ-04's seven unknowns all have decisions.
- **Negative**: shallow pushes are rejected (clients see a clear error,
not a silent decline) — a real capability gap vs `git receive-pack`,
accepted for v1 symmetry with fetch's shallow decline. `push-options`
default-off adds a config surface (additive, two-way).
- **Neutral**: the V0 framing of push coexists with the V2 fetch path in
the same session layer; the state machines differ per command, which
the substrate already models. CAS-before-unpack fail-fast remains an
optimization backlog item (tracked:
`tasks/architecture/oq-13-cas-failfast.md`).
## References
- `docs/research/push-captures.md` (the captures this decision is built on)
- `docs/research/poc3-findings.md` (request streaming, http framing facts)
- ADR-003 (V2-first — fetch only; honest advertisement everywhere),
ADR-004 (pack pipeline: ingestion tools), ADR-005 (substrate owns
framing), ADR-007 (ACL before advertisement), ADR-009 (budgets)
- gitoxide: `gix-pack` `Bundle::write_to_directory[_eagerly]`
(`thin_pack_base_object_lookup`, `.keep` handling), `data::input`
(`LookupRefDeltaObjectsIter`, `Mode::Verify`, `EntryDataMode`), `gix-ref`
transactions (`PreviousValue::MustExistAndMatch`), `gix-fsck`
(`Connectivity`), `gix-validate::reference::name`
- transport.md §receive-pack, backend.md §"The trait family" (GitPackIngest,
GitRefs), doors.md
+9 -3
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-09-21 last_updated: 2026-09-25
--- ---
# Doors: how alkgit is exposed # Doors: how alkgit is exposed
@@ -56,11 +56,15 @@ reference implementation of exactly that mapping.
**Framing facts the feature must honor** (all POC-3-validated, encoded in **Framing facts the feature must honor** (all POC-3-validated, encoded in
the substrate, not re-decided): responses end at flush (never `0002`); the substrate, not re-decided): responses end at flush (never `0002`);
flush is one-shot per response; flush-only POSTs are probes answered top-level flush is one-shot per response (the receive-pack sideband
report's inner flush lives inside band-1 and is part of the report
framing — ADR-013 §8; the rule governs protocol-level sections only);
flush-only POSTs are probes answered
200-empty; request bodies stream (no accumulation); response bodies 200-empty; request bodies stream (no accumulation); response bodies
stream under back pressure (bounded mpsc → `Body::from_stream`); request stream under back pressure (bounded mpsc → `Body::from_stream`); request
bodies carry a budget (ADR-009 — alkhttp custom routes are unbounded by bodies carry a budget (ADR-009 — alkhttp custom routes are unbounded by
default). default). Push framing per ADR-013 §12 (probe/Content-Length/chunked,
request/result content types).
## alkssh (git-over-ssh, future) ## alkssh (git-over-ssh, future)
@@ -109,6 +113,8 @@ deployment's docs, not here.
| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | doors are family infrastructure | | [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | doors are family infrastructure |
| [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | `authorize` policy, grants in records | | [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | `authorize` policy, grants in records |
| [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | op registration surface, two op kinds | | [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | op registration surface, two op kinds |
| [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 |
## Open Questions ## Open Questions
+45 -41
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-09-21 last_updated: 2026-09-25
--- ---
# Open Questions # Open Questions
@@ -23,10 +23,13 @@ when their impacts say so; it records how careful the resolution must be.
| OQ | Status | Blocked on / investigation | | OQ | Status | Blocked on / investigation |
|---|---|---| |---|---|---|
| OQ-04 | deferred(unclear) | receive-pack walkthrough (capabilities, shallow, thin-pack, CAS timing) + push POC |
| OQ-05 | deferred(scope) | ecosystem need for sha256 | | OQ-05 | deferred(scope) | ecosystem need for sha256 |
| OQ-03 | partially resolved | first-publish timing (release decision; the freeze inventory lives in backend.md/transport.md) | | OQ-03 | partially resolved | first-publish timing (release decision; the freeze inventory lives in backend.md/transport.md) |
OQ-02 and OQ-04 resolved this cycle (ADR-014, ADR-013); OQ-08/06/07/09/01
resolved in earlier cycles. No `open` or `deferred(unclear)` questions
remain.
## Theme: composition / crate shapes ## Theme: composition / crate shapes
### OQ-09: Slim-crate model — doors as family infrastructure, git as a payload service ### OQ-09: Slim-crate model — doors as family infrastructure, git as a payload service
@@ -71,55 +74,56 @@ when their impacts say so; it records how careful the resolution must be.
- **Blocked on**: first-publish timing (a release decision, not an - **Blocked on**: first-publish timing (a release decision, not an
architecture question). The API surface inventory lives in architecture question). The API surface inventory lives in
[backend.md](backend.md) §public API and [transport.md](transport.md) [backend.md](backend.md) §public API and [transport.md](transport.md)
§public API. §public API. ADR-013/014 added the push/negotiation trait surface
- **Cross-references**: ADR-010, ADR-002, ADR-012, backend.md, transport.md (`GitPackIngest` binding, `GitPackGen::common_haves`) to the freeze
inventory.
- **Cross-references**: ADR-010, ADR-002, ADR-012, ADR-013, ADR-014,
backend.md, transport.md
## Theme: transport / protocol ## Theme: transport / protocol
### OQ-02: V2 multi-round negotiation (ack/NAK logic, `wait-for-done` retirement) ### OQ-02: V2 multi-round negotiation (ack/NAK logic, `wait-for-done` retirement)
- **Origin**: [transport.md], poc2-findings §"does NOT settle" - **Origin**: [transport.md], poc2-findings §"does NOT settle"
- **Status**: open - **Status**: **resolved** — ADR-014 (V2 negotiation ack loop). The
- **Priority**: medium (full-closure-on-`done` works; multi-round is an duplex-capture walkthrough against git 2.43.0 (ground-truth negotiation
efficiency feature, not correctness) mock, cross-checked against `fetch-pack.c`) resolved the grammar: no-
- **Impacts**: fetch efficiency on repos with large shared history; `done` rounds get `acknowledgments` (`ACK <oid>` per recognized have,
capability advertisement text (`fetch=` value). `NAK` when none, flush — never `ready`, so FLUSH is always the
- **Resolution path**: design the ack loop (rounds budget per ADR-009) terminator); the `done` round generates closure(wants) − closure(haves)
when transport implementation begins; POC-2's generator is with no cross-round server state (clients re-send wants + commons every
negotiation-agnostic already. round); `wait-for-done` stays and the advertisement text is unchanged;
- **Cross-references**: ADR-003, ADR-004, ADR-009, transport.md §fetch the ack check is a new backend-trait method (`common_haves`) keeping
the honest boundary at the seam. Captures:
`docs/research/negotiation-captures.md`.
- **Resolution**: [decisions/014-v2-negotiation-ack-loop.md]
- **Cross-references**: ADR-003, ADR-004, ADR-005, ADR-009, ADR-014,
transport.md §fetch, backend.md §"The trait family" (GitPackGen)
### OQ-04: receive-pack (push) — validation gap ### OQ-04: receive-pack (push) — validation gap
- **Origin**: [transport.md], poc3-findings §"does NOT settle" - **Origin**: [transport.md], poc3-findings §"does NOT settle"
- **Status**: deferred(unclear) - **Status**: **resolved** — ADR-013 (receive-pack state machine). The
- **Door type**: two-way walkthrough against real `git push` captures (git 2.43.0: file://,
- **Priority**: high git://, smart-http, plus raw stdio requests into real `git receive-pack`)
- **Impacts**: blocks receive-pack implementation tasks; push is the resolved every listed unknown: the push path is V0-framed by upstream
always-authenticated half of the wire surface. design (no version negotiation, ADR-003 governs fetch only); the
- **Investigation**: the pieces are decided (POC-2: pack ingestion via advertisement is a V0-shaped ref advertisement (caps on the first ref
`gix-pack::data::input` with `streaming-input` (ADR-004); `gix-ref` line, `capabilities^{}` sentinel only for empty repos) with served set
transaction CAS; fsck via `gix-fsck`; POC-3: request bodies stream). The shape to `report-status(-v2) delete-refs side-band-64k atomic ofs-delta
work through: the full push state machine — object-format=sha1` (+ `push-options` config-gated); shallow request
(a) the receive-pack **capability advertisement set** lines are rejected up front for v1 (symmetric with fetch); thin packs
(`report-status`/`report-status-v2`, `delete-refs`, `push-options`, accepted with bases from the server odb (default client behavior, no
`atomic`, `side-band-64k`, `object-format`) under the honest-advertisement capability); ingestion = `Bundle::write_to_directory_eagerly` +
invariant (ADR-003); (b) request-line parsing `gix-fsck` + one `gix-ref` transaction per push (`.keep`-guarded pack
(`<old> <new> <ref>` + shallow lines policy — expected resolution: landing); CAS timing is unpack-first-then-per-ref (observed upstream
**reject shallow on push for v1**, mirroring fetch's decline-by-omission order); status report is band-1 pkt-line-framed with inner flush when
in ADR-003, so depth semantics stay symmetric; confirm against real sideband was selected. Captures:
`git push` behavior); `docs/research/push-captures.md`.
(c) thin-pack acceptance on push (client packs may be thin; accepting - **Resolution**: [decisions/013-receive-pack-state-machine.md]
implies base-object availability requirements); (d) pack ingestion - **Cross-references**: ADR-003, ADR-004, ADR-005, ADR-009, ADR-013,
mid-stream; (e) CAS validation timing (before vs after pack index); transport.md §receive-pack, backend.md §"The trait family" (GitRefs,
(f) status report (`unpack ok|ng` + per-ref lines); (g) the GitPackIngest), doors.md
receive-pack version/framing surface over http (which framing `git push`
uses against us; ADR-003's V2 decision covers fetch only). Method:
walkthrough against real `git push` captures, then a small POC if the
ingestion composition is not obvious from POC-2's findings. Tracker
task: `tasks/architecture/oq-04-receive-pack.md`.
- **Cross-references**: ADR-003, ADR-004, ADR-009, transport.md
§receive-pack, backend.md §"The trait family" (GitRefs), doors.md
### OQ-05: sha256 support policy ### OQ-05: sha256 support policy
+15 -14
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-09-21 last_updated: 2026-09-25
--- ---
# Overview: alkgit # Overview: alkgit
@@ -70,8 +70,9 @@ same pattern. Doors live in the door crates — see [doors.md](doors.md).
- Smart-http streaming both ways (POC-3 → the stateless substrate that - Smart-http streaming both ways (POC-3 → the stateless substrate that
alkhttp's future `git` feature maps onto; `docs/research/poc3-findings.md` alkhttp's future `git` feature maps onto; `docs/research/poc3-findings.md`
§alkhttp fit is the mounting reference). §alkhttp fit is the mounting reference).
The full V2 fetch path against real git 2.43 is proven; receive-pack is The full V2 fetch path against real git 2.43 is proven; receive-pack and
designed but not yet exercised (OQ-04). multi-round negotiation are design-complete against real-client captures
(ADR-013, ADR-014 — `push-captures.md`, `negotiation-captures.md`).
## Design Decisions ## Design Decisions
@@ -79,7 +80,7 @@ designed but not yet exercised (OQ-04).
|---|---|---| |---|---|---|
| [001](decisions/001-crate-decomposition.md) | Crate decomposition | **superseded by ADR-010** | | [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, stream, limits) — unchanged, load-bearing |
| [003](decisions/003-protocol-v2-first.md) | V2-first protocol | V2-only both doors; honest advertisement | | [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 | | [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 | | [005](decisions/005-session-substrate-types.md) | Substrate types | duplex + stateless APIs over one state machine |
| [006](decisions/006-http-adapter-composition.md) | HTTP adapter composition | **superseded by ADR-010** (mounting → alkhttp feature) | | [006](decisions/006-http-adapter-composition.md) | HTTP adapter composition | **superseded by ADR-010** (mounting → alkhttp feature) |
@@ -89,20 +90,20 @@ designed but not yet exercised (OQ-04).
| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | single crate, producer/consumer halves, no doors/binary | | [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | single crate, producer/consumer halves, no doors/binary |
| [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | grants in repo records, policy in core, vault-nil | | [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | grants in repo records, policy in core, vault-nil |
| [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | read/write split, file default, CRUD ops, feature split | | [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | read/write split, file default, CRUD ops, feature split |
| [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` |
## Open Questions ## Open Questions
Key questions tracked in [open-questions.md](open-questions.md): Key questions tracked in [open-questions.md](open-questions.md):
- **OQ-04**: receive-pack (push) validation gap (high — the - **OQ-03**: publish-time API freeze inventory (the `git/repo/*` op set
always-authenticated half of the wire surface). and the trait family enter it; ADR-012, ADR-013/014's trait additions).
- **OQ-02**: V2 multi-round negotiation (medium — efficiency, not
correctness).
- **OQ-03**: publish-time API freeze inventory (the `git/repo/*` op
set enters it; ADR-012).
- **OQ-05**: sha256 policy (deferred(scope), low). - **OQ-05**: sha256 policy (deferred(scope), low).
Resolved this cycle: OQ-08 (ADR-011 — per-repo authorization, grants in Resolved this cycle: OQ-04 (ADR-013 — receive-pack state machine,
records, vault-nil), OQ-06 (ADR-012 — `registry-file` default, capture-grounded), OQ-02 (ADR-014 — negotiation ack loop). Earlier:
persistence adapters additive), OQ-07 (ADR-012 — CRUD ops shipped OQ-08 (ADR-011 — per-repo authorization, grants in records, vault-nil),
External with scope+ownership ACL). OQ-06 (ADR-012 — `registry-file` default, persistence adapters
additive), OQ-07 (ADR-012 — CRUD ops shipped External with
scope+ownership ACL), OQ-09/OQ-01 (ADR-010 — pure protocol crate).
+57 -19
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-09-21 last_updated: 2026-09-25
--- ---
# alkgit: Git Smart Protocol (wire layer) # alkgit: Git Smart Protocol (wire layer)
@@ -43,8 +43,8 @@ substrate property (per-request state), not a protocol fork.
hangs on re-advertisement); per-request-set on http (stateless: the hangs on re-advertisement); per-request-set on http (stateless: the
client re-sends the dump). client re-sends the dump).
- Honest capability list: exactly what we serve (`ls-refs=unborn`, - Honest capability list: exactly what we serve (`ls-refs=unborn`,
`fetch=wait-for-done` for the v1 done-path policy — ADR-003 pins the `fetch=wait-for-done` — the ack loop needs no capability change, the
values; OQ-02 may extend them when multi-round lands, acknowledgments section is grammar not capability (ADR-014);
`object-format=sha1`). Unimplemented features are declined by omission `object-format=sha1`). Unimplemented features are declined by omission
(validated against real git, POC-1). `git-upload-archive` is not (validated against real git, POC-1). `git-upload-archive` is not
served (fixed refusal — ADR-008's never-execute rule, ssh analog in served (fixed refusal — ADR-008's never-execute rule, ssh analog in
@@ -59,25 +59,58 @@ substrate property (per-request state), not a protocol fork.
### fetch ### fetch
- Parse wants/haves/done/args; object-format check (reject mismatches — - Parse wants/haves/done/args; object-format check (reject mismatches —
POC-1 to-do). the object-format line is validated against the advertisement's
- Negotiation policy (v1 initial): full-closure pack on `done` `object-format` on every command; ADR-013 pins the push-side check to
(POC-validated). Multi-round ack/NAK negotiation: **OQ-02**. the same rule).
- Negotiation policy: the full ack loop (ADR-014) — no-`done` rounds
get an `acknowledgments` section (`ACK <oid>` per recognized have via
the backend's `common_haves`, `NAK` when none, flush; never `ready`),
the `done` round generates closure(wants) − closure(haves) via
`GitPackGen`. No cross-round state on either substrate (the client
re-sends wants + commons each round — negotiation-captures.md).
Advertisement text is unchanged: `fetch=wait-for-done`.
- Pack generation via `GitPackGen` (ADR-004), streamed over sideband on - Pack generation via `GitPackGen` (ADR-004), streamed over sideband on
duplex / sideband-in-response on http; generation runs on duplex / sideband-in-response on http; generation runs on
`spawn_blocking` with the owned handle moved in (POC-2's shape: store `spawn_blocking` with the owned handle moved in (POC-2's shape: store
shared, handle per session, generation on blocking threads). shared, handle per session, generation on blocking threads).
- Round/haves budgets enforced here (ADR-009). - Round/haves budgets enforced here (ADR-009); an empty resulting pack
(client already has everything) is a valid zero-object packfile.
### receive-pack (push) ### receive-pack (push)
- Parse update requests (`<old> <new> <ref>` + shallow lines), ingest the V0-framed by upstream design (no version negotiation on the push path —
pack stream (POC-3-verified: request bodies stream), apply CAS ADR-013); shapes are capture-grounded (`push-captures.md`), not
transactions, emit status report. The capability set, shallow policy, grammar-inferred.
CAS timing, and exact status-report shape are OQ-04's investigation
target (design intent: shape per OQ-04's resolution). - **Advertisement**: V0-shaped ref advertisement — caps NUL-attached on
- Ingestion via `GitPackIngest` (ADR-004); validation pending — **OQ-04** the first ref line, `capabilities^{}` sentinel only for empty repos;
(includes the receive-pack version surface, which ADR-003's fetch-V2 served set `report-status report-status-v2 delete-refs side-band-64k
decision does not cover). atomic ofs-delta object-format=sha1` (+ `push-options` under config
gate); ACL before the first ref line (ADR-007).
- **Request**: command lines (`<old> <new> <ref>`), shallow lines
rejected up front for v1 (symmetric with fetch's decline, ADR-013 §4),
flush, optional push-options section, then the pack stream — which is
always expected (missing pack errors at unpack). An immediate flush is
a client-side nothing-to-do (reply flush, no report).
- **Ingestion** via `GitPackIngest` (ADR-004): thin packs accepted with
bases from the server odb (default client behavior, no capability);
`Bundle::write_to_directory_eagerly` with the repo's pack dir, `.keep`
guard; `gix-fsck` connectivity per new tip; missing objects →
`unpack ng`.
- **CAS timing**: unpack-first, then per-ref checks (name via
`gix_validate::reference::name` + reserved deny-list, CAS via `gix-ref`
transactions, policy), atomic rollback, report — the observed upstream
order (ADR-013 §7). One transaction per push.
- **Status report**: band-1 pkt-line-framed (`unpack ok|ng`, per-ref
`ok|ng <ref> <reason>`, inner flush, outer flush) when sideband was
selected; bare pkt-lines otherwise. Substrate owns the framing
(unwrapped reports abort real clients).
- **Http framing**: Content-Length or chunked request (probe POST
answered 200-empty above the client's postBuffer), Content-Type
`application/x-git-receive-pack-request` / `...-result`, response ends
at flush (ADR-005 unchanged).
- Push-options parsing and `atomic` rollback semantics per ADR-013
§10–11.
### Error taxonomy ### Error taxonomy
@@ -104,26 +137,31 @@ Publish-freeze point: **OQ-03**.
| ADR | Decision | Summary | | ADR | Decision | Summary |
|---|---|---| |---|---|---|
| [002](decisions/002-front-door-blind-core.md) | Session boundary | duplex + stateless entry points | | [002](decisions/002-front-door-blind-core.md) | Session boundary | duplex + stateless entry points |
| [003](decisions/003-protocol-v2-first.md) | V2-first | honest advertisement, V0/V1 declined | | [003](decisions/003-protocol-v2-first.md) | V2-first | honest advertisement; V0/V1 declined on fetch, push V0-framed (ADR-013) |
| [004](decisions/004-pack-pipeline.md) | Pack pipeline | generation on blocking threads, O(counts) | | [004](decisions/004-pack-pipeline.md) | Pack pipeline | generation on blocking threads, O(counts) |
| [005](decisions/005-session-substrate-types.md) | Substrate types | request reader, sideband sink, http framing rules | | [005](decisions/005-session-substrate-types.md) | Substrate types | request reader, sideband sink, http framing rules |
| [009](decisions/009-bounded-resources-budget.md) | Budgets | `Limits` in every session tuple | | [009](decisions/009-bounded-resources-budget.md) | Budgets | `Limits` in every session tuple |
| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | wire layer is backend-trait-only | | [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 |
## Open Questions ## Open Questions
- **OQ-02**: V2 multi-round negotiation (open — efficiency, not
correctness).
- **OQ-04**: receive-pack state machine + validation (deferred(unclear)).
- **OQ-03**: publish/API freeze (partially resolved — single-crate shape - **OQ-03**: publish/API freeze (partially resolved — single-crate shape
settled by ADR-010). settled by ADR-010).
- **OQ-05**: sha256 policy (deferred(scope)). - **OQ-05**: sha256 policy (deferred(scope)).
- OQ-02 resolved (ADR-014 — ack loop design).
- OQ-04 resolved (ADR-013 — receive-pack state machine).
## References ## References
- `docs/research/poc-1-findings.md`, `docs/research/poc2-findings.md`, - `docs/research/poc-1-findings.md`, `docs/research/poc2-findings.md`,
`docs/research/poc3-findings.md` (the normative wire behavior — `docs/research/poc3-findings.md` (the normative wire behavior —
observed against real git, not docs' grammar) observed against real git, not docs' grammar)
- `docs/research/push-captures.md` (the push-path normative record —
ADR-013's basis)
- `docs/research/negotiation-captures.md` (the negotiation normative
record — ADR-014's basis)
- `docs/research/git-protocol.md` (inventory + observed corrections) - `docs/research/git-protocol.md` (inventory + observed corrections)
- `docs/research/gitoxide.md` §"Wire format" (packetline contracts) - `docs/research/gitoxide.md` §"Wire format" (packetline contracts)
- alktty `wire.rs`/`session.rs`/`adapter.rs` (the template's half shapes) - alktty `wire.rs`/`session.rs`/`adapter.rs` (the template's half shapes)
+6 -1
View File
@@ -4,12 +4,17 @@ Phase 0 (exploration) research. Feeds phase 1 (architecture).
| Doc | Topic | Status | | Doc | Topic | Status |
|---|---|---| |---|---|---|
| [vision.md](vision.md) | Vision, guiding principles, non-goals, phase-0 checklist | draft v2 (amended 2026-09-21) | [vision.md](vision.md) | Vision, guiding principles, non-goals, phase-0 checklist | draft v2 (amended 2026-09-21) |
| [gitoxide.md](gitoxide.md) | gitoxide (gix) capability + version alignment | initial pass complete | | [gitoxide.md](gitoxide.md) | gitoxide (gix) capability + version alignment | initial pass complete |
| [alk-stack.md](alk-stack.md) | alk stack fit, integration surface, gitea-lesson constraints | initial pass complete | | [alk-stack.md](alk-stack.md) | alk stack fit, integration surface, gitea-lesson constraints | initial pass complete |
| [git-protocol.md](git-protocol.md) | Server-side git smart protocol inventory (what we own) | initial pass complete | | [git-protocol.md](git-protocol.md) | Server-side git smart protocol inventory (what we own) | initial pass complete |
| [reference-policy.md](reference-policy.md) | Licenses, reference projects, reuse policy | complete | | [reference-policy.md](reference-policy.md) | Licenses, reference projects, reuse policy | complete |
| [pocs.md](pocs.md) | POC plan (what to validate before architecture commits) | planned | | [pocs.md](pocs.md) | POC plan (what to validate before architecture commits) | planned |
| [poc-1-findings.md](poc-1-findings.md) | POC-1: pkt-line over alkcall BiStream (duplex producer) | complete — proceed |
| [poc2-findings.md](poc2-findings.md) | POC-2: server-side pack generation (streaming pipeline) | complete — proceed |
| [poc3-findings.md](poc3-findings.md) | POC-3: smart-http shape through alkhttp (stateless substrate) | complete — proceed |
| [push-captures.md](push-captures.md) | Normative receive-pack (push) wire record (real git captures) | complete — ADR-013 basis |
| [negotiation-captures.md](negotiation-captures.md) | Normative V2 negotiation record (real git captures + source) | complete — ADR-014 basis |
## Key findings so far ## Key findings so far
+132
View File
@@ -0,0 +1,132 @@
# Research: receive-pack (push) walkthrough captures
**Status**: complete
**Date**: 2026-09-25
**Client**: git 2.43.0
**Method**: real `git push` (file://, git:// daemon, and smart-http against
a ground-truth mock receive-pack server validated against real git
behavior), plus raw stdio requests driven into real `git receive-pack` for
policy-error ground truth. This is the normative push-path wire record
that ADR-013 is built on. Scratch logs were in /tmp/opencode/oq04/ (not
kept); every fact below is restated in full here.
## Advertisement (GET info/refs + duplex)
- Smart-http GET: `# service=git-receive-pack` + flush, then a **V0-shaped
ref advertisement**: `<oid> <ref>\0<caps>` on the FIRST ref line only,
remaining ref lines bare, flush. The `capabilities^{}` zero-id sentinel
line replaces the ref lines ONLY when the repo has no refs.
Content-Type: `application/x-git-receive-pack-advertisement`.
- Duplex (git daemon): same ref advertisement WITHOUT the service prefix;
server speaks first; no version negotiation at all — receive-pack is
V0/V1-framed even for modern git (protocol.version=2 does not apply).
- Real receive-pack caps: `report-status report-status-v2 delete-refs
side-band-64k quiet atomic ofs-delta object-format=sha1 agent=git/2.43.0`
(+ `push-options` when receive.advertisePushOptions=true). The client
selects a subset NUL-attached on its FIRST command line (observed:
`report-status-v2 side-band-64k quiet object-format=sha1 agent=...`;
note the leading space after the NUL).
- git 2.43 rejects a bare capability dump (no refs, no sentinel) and
rejects a trailing `capabilities^{}` line when refs exist
(`fatal: unexpected capabilities^{}`).
## Request (POST git-receive-pack)
- Body: command pkt-lines `<old> <new> <ref>` (caps NUL-attached on the
first line only) + flush + [push-options section: bare pkt-lines +
flush, only when push-options was negotiated] + PACK stream (raw, to
end of body).
- Deletes: old=<current sha> new=zero-id. Creates: old=zero-id.
- Shallow clients push `shallow <sha>` lines BEFORE the command lines
(captured from a --depth 1 clone push). Real receive-pack ACCEPTS them
(unpack ok, refs applied) when the pack completes history; rejects with
`ng <ref> shallow update not allowed` when the server would have to
graft (receive.shallowUpdateDeepen unset).
- HTTP framing: POST Content-Type
`application/x-git-receive-pack-request`; small bodies Content-Length;
large (above http.postBuffer, git 2.43 default 1 MB) → chunked, and a
4-byte `0000` probe POST answered 200-empty (same shape as upload-pack,
POC-3).
- git:// framing: first pkt `git-receive-pack /<repo>\0host=<host>\0`, then
the advertisement (server speaks first, no service prefix), then the
request on the stream.
## Thin pack
- `git push` sends THIN packs by default (push.thin=true): the pack
excludes objects the client believes the server already has (observed:
the unchanged parent tree and its blob excluded; the new commit, tree,
blob sent). NO `thin-pack` capability is involved on push (that string
is a fetch arg). Ingestion MUST resolve delta bases from the server's
own odb — the `Option<Never>` no-lookup composition is only valid when
the pack is known-not-thin.
- Verified: `git index-pack --strict --stdin` FAILS on the captured pack
("did not receive expected object"); `--fix-thin` succeeds.
## Status report
- When side-band-64k was selected: the report is band-1 pkt-line chunks;
the band payload is ITSELF pkt-line-framed: `unpack ok|ng <reason>`,
then per-ref `ok <ref>` / `ng <ref> <reason>`, terminated by an INNER
flush pkt inside the band; the response then ends with an OUTER flush.
Without sideband (file:// local path): report lines are bare pkt-lines
to a single flush.
- Observed per-ref ng reasons (real receive-pack): `shallow update not
allowed`, `deletion prohibited`, `non-fast-forward`, `atomic push
failure` (rollback of other refs). `unpack ok` even when some refs fail;
pack-level failure → `unpack ng <reason>` (git source: the index-pack
error text; not live-captured).
- Atomic: captured `ng refs/heads/dev deletion prohibited` + `ng
refs/heads/main atomic push failure` in one report (dev was the real
failure; main was rolled back).
- Human-readable diagnostics ride band-2 (`remote: error: denying
non-fast-forward refs/heads/main (you should pull first)`).
- Client aborts on framing errors: a report sent WITHOUT sideband when the
client selected side-band-64k → `fatal: protocol error: bad line length
character` (observed live) — the report MUST be sideband-wrapped when
side-band-64k was selected.
## CAS / race semantics
- The advertisement IS the client's CAS check: a stale old-id that does
not appear in the advertisement → client refuses to send (non-FF or
fetch-first message, NO commands transmitted).
- Server-side CAS is still required: races between advertisement and
apply, and clients that send wrong old-ids deliberately. old must match
current (or zero-id create) per ref; else `ng <ref> <reason>`.
- Empty command list (immediate `0000`) = client-side nothing-to-do.
## Version/framing surface
- No version line anywhere in receive-pack: no `version=2` exchange on
push; the push path is V0/V1-framed regardless of protocol.version.
- report-status-v2 selected by the client when offered; response shape is
identical to v1 for plain pushes (v2 adds option lines only when
options exist).
## Raw stdio captures into real `git receive-pack` (hand-built requests)
- Corrupt pack: `unpack protocol error (pack signature mismatch detected)` +
`ng <ref> unpacker error` — unpack runs BEFORE ref checks; a failed pack
reports per-ref 'unpacker error' (not the detailed reason).
- Truncated request: `unpack eof before pack header was fully read`.
- Invalid ref name with a VALID pack: `unpack ok` + `ng refs/heads/../evil
funny refname` — name validation is per-ref, after unpack, with reason
'funny refname'. (Client-side git refuses such refnames itself; this is
the malicious-client path.)
- Advertisement confirmed on the raw path: first ref line carries the caps
NUL-attached; `unpack ng <reason>` at pack level, per-ref lines after.
## Pack-read ordering (definitive)
Real receive-pack ALWAYS expects a pack after the command flush (missing
pack → `unpack eof before pack header was fully read` + all refs
`unpacker error`). Per-ref checks (CAS, name validation, policy) run AFTER
the pack is indexed — observed: stale-old ng, 'funny refname' ng, 'deletion
prohibited' ng all arrive only after `unpack ok`. Fail-fast CAS before
reading the pack is an optimization, not the protocol order.
## quiet semantics
The client selects `quiet` on its first command line in every capture; band-2
error lines still arrived (`remote: error: denying non-fast-forward ...`).
quiet suppresses progress chatter, not error/diagnostic bands.
+17 -14
View File
@@ -1,42 +1,45 @@
--- ---
id: architecture/oq-04-receive-pack id: architecture/oq-04-receive-pack
name: OQ-04 unblock — receive-pack walkthrough/POC arrives name: OQ-04 unblock — receive-pack walkthrough/POC arrives
status: pending status: completed
depends_on: [] depends_on: []
scope: narrow scope: narrow
risk: trivial risk: trivial
impact: component impact: component
level: research level: research
tags: [external-trigger, deferred-oq] tags: [external-trigger, deferred-oq, resolved-early]
--- ---
## Description ## Description
Tracker for OQ-04 (`deferred(unclear)`): receive-pack (push) design Tracker for OQ-04 (`deferred(unclear)`): receive-pack (push) design
validation. This task is not actionable work — it tracks whether the validation. **Resolved early (2026-09-25, ADR-013)** — the investigation
investigation has arrived. When a receive-pack design walkthrough (against was run in-session rather than waiting for scheduling: real `git push`
real `git push` captures) or a validation push POC is scheduled, mark this captures (git 2.43.0 over file://, git://, smart-http, plus raw stdio
completed; OQ-04 transitions to `open`. The investigation scope is in requests into real `git receive-pack`) resolved the full push state
`docs/architecture/open-questions.md` (OQ-04): capability advertisement machine. OQ-04 is resolved by
set, request-line parsing + shallow policy, thin-pack acceptance, CAS `docs/architecture/decisions/013-receive-pack-state-machine.md`; captures
timing, status report, http framing. in `docs/research/push-captures.md`.
## Work ## Work
None by default. On trigger: set OQ-04 to `open` in None by default. On trigger: set OQ-04 to `open` in
`docs/architecture/open-questions.md` and spawn the focused session that `docs/architecture/open-questions.md` and spawn the focused session that
works through the push state machine. works through the push state machine. (Triggered and completed in the
2026-09-25 session.)
## Verification ## Verification
- OQ-04 status matches this task's state in - OQ-04 status matches this task's state in
`docs/architecture/open-questions.md`. `docs/architecture/open-questions.md` (resolved).
## Out of scope ## Out of scope
- Actually designing/implementing receive-pack (that is the follow-on - Actually implementing receive-pack (phase 3 implementation work).
session's work).
## Summary ## Summary
> Filled on completion. Resolved early via ADR-013: the walkthrough ran in-session; all seven
unknowns (advertisement set, shallow policy, thin-pack acceptance,
ingestion composition, CAS timing, status report, version/framing
surface) have capture-grounded decisions.
+45
View File
@@ -0,0 +1,45 @@
---
id: architecture/oq-13-cas-failfast
name: Backlog — CAS-before-unpack fail-fast pre-check on push
status: pending
depends_on: []
scope: narrow
risk: low
impact: component
level: implementation
tags: [optimization-backlog]
---
## Description
Optimization backlog item from ADR-013 (§7, §Consequences): a pre-unpack
CAS pre-check on receive-pack — reject commands with obviously-wrong
old-ids before reading the pack body, failing fast instead of ingesting
a pack whose refs would be rejected anyway. The protocol-correct order
(unpack-first, then per-ref checks) is what v1 implements; this is an
optimization, additive, and two-way (internal behavior, not wire shape —
the report shape is unchanged).
## Work
When receive-pack implementation is on the critical path for a
deployment with heavy stale-push traffic (measured, not hypothetical):
add the pre-check to the transport push state machine, after command
parse and before ingestion. Rejected refs report the same `ng <ref>
<reason>`; the pack body is then drained (or the connection closed per
the substrate's error rule).
## Verification
- All existing push tests still pass (real `git push` against the
transport).
- A stale-old push errors before the pack body is fully read (observable
via the body-budget counters or a truncated-body test).
## Out of scope
- Changing the report shape or the protocol order (ADR-013 §7 stands).
## Summary
> Filled on completion.