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:
1 parent
dbb056f451
commit
e76f91f6d7
14 files changed
+621
-138
No files matched your search
+15
-11
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
Reference in new issue
Block a user