steid

@jamesgill /

steid/plans/current.md
9.7 KBCode·Blame·Raw
ab7fea9chore: plans setup1mo
1# Current
2
3> Keep this file short. One active step, one ordered backlog. Completed work moves to
4> [progress.md]progress.md. If this file starts reading like a changelog, it has
5> drifted — that's exactly what went wrong last time.
6
6a2a925docs: close Milestone 3, plan Milestone 4a8d
7## Active: Milestone 4a — Clone over HTTP
2eb8681docs: put git next, plan the repo model24d
8
6a2a925docs: close Milestone 3, plan Milestone 4a8d
9**Goal:** `git clone https://host/{handle}/repos/{name}.git` works against a public
10repository, for anyone, with no credentials. The protocol is delegated to `git
11http-backend` per [0001]decisions/0001-git-over-http-not-ssh.md.
2eb8681docs: put git next, plan the repo model24d
12
6a2a925docs: close Milestone 3, plan Milestone 4a8d
13**Out of scope:** personal access tokens, HTTP Basic, push, cloning a private repo —
14all of that is 4b. Also out: browsing a tree in the UI (Milestone 5), and any repo
15statistic the clone path could tempt us into computing.
2eb8681docs: put git next, plan the repo model24d
16
17### Steps
18
6a2a925docs: close Milestone 3, plan Milestone 4a8d
19- [ ] Probe `git http-backend`'s actual contract — which CGI variables it reads, how it
20 reports failure, what it does with an unauthorised path. Findings to
21 `progress.md`; no application code in this step.
47db238feat: serve the git protocol through http-backend, behind a port8d
22- [x] Application: `GitProtocolServer` port — a CGI-shaped request/response pair, plus
6a2a925docs: close Milestone 3, plan Milestone 4a8d
23 `GitOperation` (Read/Write) as the thing authorization is decided on
47db238feat: serve the git protocol through http-backend, behind a port8d
24- [x] Infrastructure: `GitHttpBackend` adapter, spawning through the existing `run_git`
6a2a925docs: close Milestone 3, plan Milestone 4a8d
25 invoker; streams stdin in and stdout out, parsing CGI headers off the front
26- [ ] Application: `serve_git` use case — resolves the repository, enforces visibility,
27 refuses writes outright, and only then delegates
47db238feat: serve the git protocol through http-backend, behind a port8d
28- [ ] Web: the three git routes under `/{handle}/repos/{name}.git/`
6a2a925docs: close Milestone 3, plan Milestone 4a8d
29- [ ] Verify with a real `git clone` of a repo with enough refs to trigger a gzipped
30 request body
ab7fea9chore: plans setup1mo
31
32### Done when
33
6a2a925docs: close Milestone 3, plan Milestone 4a8d
34`git clone http://127.0.0.1:3000/{handle}/repos/{name}.git` produces a working
35checkout of a public repository, with no credentials, and the cloned history matches
36the origin. A private repository is not clonable by anyone yet — not even its owner.
37`git push` is refused.
2eb8681docs: put git next, plan the repo model24d
38
c6d74a8docs: record the repo model decisions24d
39### Settled
2eb8681docs: put git next, plan the repo model24d
40
6a2a925docs: close Milestone 3, plan Milestone 4a8d
41- **`git http-backend`, not direct `--stateless-rpc`.** Both put identical bytes on the
42 wire for a modern clone of a small repo; the difference is entirely in the tail, which
43 is what wide adoption means. Measured before choosing: a client cloning a repo with
44 201 refs **gzip-compresses the POST body** (5KB here), on protocol v0 *and* v2. A
45 direct implementation must therefore inflate request bodies and forward
46 `Git-Protocol` itself, and gets neither the dumb-protocol fallback nor the header set.
47 The failure mode decided it — a direct implementation passes against a one-ref test
48 repo and breaks on the first real one.
49- **The clone URL is `/{handle}/repos/{name}.git`**, matching the page at
50 `/{handle}/repos/{name}`. Scoped rather than root-level, per
51 [0003]decisions/0003-scoped-urls.md; the `.git` suffix separates protocol from page.
52- **Only the three known endpoints are routed**`info/refs`, `git-upload-pack`,
53 `git-receive-pack`. `http-backend` will otherwise serve dumb-protocol object files
54 under any path handed to it, which would be a read of a repository nothing
55 authorized. The router is the allowlist.
56- **Authorization is decided before the subprocess is spawned**, from the service name
57 in the request, not from anything `http-backend` reports back. By the time git is
58 running it is too late to refuse.
47db238feat: serve the git protocol through http-backend, behind a port8d
59- **Bodies stream in both directions, rather than buffering.** A pack is arbitrarily
60 large, and buffering would bound a clone by RAM instead of by disk;
61 [0001]decisions/0001-git-over-http-not-ssh.md named streaming as the thing that made
62 this transport viable in the first place. The port therefore carries
63 `Pin<Box<dyn AsyncRead + Send>>` in both directions. **No new crate**: this needs only
64 tokio's `io-util` feature, and the `bytes` / `http-body-util` / `tokio-util` the web
65 layer will use to bridge Topcoat's `Body` are already in `Cargo.lock` as transitive
66 dependencies, so nothing new enters the build.
67- **`run_git` was split into `git_command`.** The old invoker buffers with `output()`,
68 which the protocol cannot use. Rather than a second recipe — the thing
69 [0006]decisions/0006-git-binary-behind-narrow-ports.md exists to prevent — the
70 isolation moved into a shared builder both call sites start from.
71- **`body_limit` turns out not to exist** in `topcoat-router` 0.5.0. Bodies are read by
72 the handler via `to_bytes(body, limit)` with a caller-chosen limit, so there is no
73 layer to raise — and the git routes take the body unbuffered anyway. The warning
74 carried from [0001]decisions/0001-git-over-http-not-ssh.md is stale.
6a2a925docs: close Milestone 3, plan Milestone 4a8d
75- **Milestone 4 was split.** See [ROADMAP.md]ROADMAP.md#why-this-order.
3954b45docs: record milestone 2 phase 125d
76
be4fac5docs: correct the Topcoat guidance in CLAUDE.md24d
77### Open
78
6a2a925docs: close Milestone 3, plan Milestone 4a8d
79- **What a private repository answers to an anonymous clone.** 4a has no credentials at
80 all, so 404 is the only honest answer and matches `view_repo`'s "absent, not
81 forbidden" rule. But git only sends credentials *after* a 401, so 4b will need a 401
82 with `WWW-Authenticate` on exactly the case that 404s today — which leaks that the
83 repository exists. Gitea and GitHub both accept that leak. Decide it in 4b, with the
84 tension recorded rather than rediscovered.
be4fac5docs: correct the Topcoat guidance in CLAUDE.md24d
85
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
86### Watch for
d7b99d9docs: record milestone 0 progress and routing findings1mo
87
6a2a925docs: close Milestone 3, plan Milestone 4a8d
88- **CGI header parsing sits in front of a stream.** `http-backend` writes headers, a
89 blank line, then the body. Reading the headers must not buffer the body — that is the
90 whole reason this transport was judged viable on `Body::into_data_stream`.
47db238feat: serve the git protocol through http-backend, behind a port8d
91- **A client that disappears mid-request leaves the body-copy task waiting.** The copy
92 into git's stdin runs in its own task; nothing cancels it if the connection drops.
93 Bounded by the backend exiting and closing the pipe, but not by anything deliberate.
6a2a925docs: close Milestone 3, plan Milestone 4a8d
94- **A subprocess per request**, unlike Milestone 3's once-per-creation. Fork/exec cost
95 now sits on a hot path; measure before assuming it is fine.
96- **`http-backend` reports failure through CGI status lines**, not exit codes alone. A
97 non-zero exit and a `404 Not Found` on stdout mean different things.
98- **The advertisement must not be cached.** `Cache-Control: no-cache` on `info/refs`, or
99 clients fetch a stale ref list and fail to find commits that exist.
076dbc9docs: close milestone 1, open milestone 21mo
100
f0444b7docs: plan milestone 2 in two phases1mo
101### Carried over — small, unblocked
076dbc9docs: close milestone 1, open milestone 21mo
102
6a2a925docs: close Milestone 3, plan Milestone 4a8d
103- **An orphaned repo directory is possible** if the process dies between the record
104 write and the filesystem write, and it then blocks re-creating that name. The durable
105 fix is a reconciliation sweep on boot
106 ([architecture.md]architecture.md#db-plus-filesystem-writes); clearing one is a
107 manual `rm` today, since repo deletion does not exist.
108- **The duplicate-name check races.** The loser is caught by `init_bare` or the unique
109 constraint, but surfaces as an opaque storage error rather than "name taken".
110- **Bare repos created on macOS carry `ignorecase = true`.** A migration gotcha if the
111 data directory ever moves to Linux.
2eb8681docs: put git next, plan the repo model24d
112- **Fonts are not loaded.** The theme names Geist and IBM Plex Mono; both fall back
113 today. Topcoat's `font-fontsource` feature handles it.
114- **Light mode is untested.** The palette defines it; nobody has looked at it.
f0444b7docs: plan milestone 2 in two phases1mo
115- **No rate limiting** on `/auth/login` or `/auth/setup`.
076dbc9docs: close milestone 1, open milestone 21mo
116- **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is
117 enforced on read, so this is tidiness, not a hole.
2eb8681docs: put git next, plan the repo model24d
118- **CSRF.** `SameSite=Lax` covers the common case. Forms now exist, so this is decidable
119 rather than hypothetical.
ab7fea9chore: plans setup1mo
120
121## Backlog
122
123Ordered. Pull from the top.
124
6a2a925docs: close Milestone 3, plan Milestone 4a8d
1251. **Milestone 4b — Push and tokens.** Personal access tokens over HTTP Basic, `git
126 push`, private clone. Open decisions when it starts: how tokens are hashed (session
127 token hashing already exists to copy), whether tokens carry scopes, and the 401-vs-404
128 tension above.
02eb2e4feat: GitStorage port and DiskGitStorage24d
1292. **Milestone 5 — Repo browsing.** Tree, blob, commit log. **Start with domain value
130 objects**`ObjectId`, `RefName`, `TreeEntry` — before any adapter. A query port
131 returning `String`s is an anaemic pass-through that pushes validation into the page.
132 Also the point to measure fork/exec cost per page view, and to reconsider `gix` for
133 the read path ([0006]decisions/0006-git-binary-behind-narrow-ports.md).
2eb8681docs: put git next, plan the repo model24d
1343. **Milestone 6 — Writing.** Posts, markdown, `/{handle}/posts/{slug}`. Still open
135 whether writing or projects/showcases is the better first portfolio feature.
ab7fea9chore: plans setup1mo
136
137## Open questions
138
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
139- **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected
140 by its own authors). Expect churn that isn't feature work.
141- Topcoat ships Tailwind without Node, which reopens the design system attempt #1
142 dropped purely to avoid an npm build step — see [ui.md]ui.md.
143
144## Routing findings (Milestone 0)
145
146- **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat`
147 silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local
148 stable is now 1.97.1.
149- `Router::builder().discover()` collects `#[page]`-annotated items **at link time**,
150 so pages can live in any module. Layering is our choice, not the framework's.
151- `module_router!` derives each URL from the module tree rather than a path string.
aaefaabfeat: root handles, grouped routes, reserved-handle denylist1mo
152 Still deferred. Application routes now group cleanly (`auth/login`, `api/me`), but
153 handles sit at the root ([0004]decisions/0004-root-handles-grouped-routes.md), so a
154 parameterised root segment still has to coexist with static ones. Worth checking how
155 `module_router!` handles that before committing to it.
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
156- Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not
157 injected as handler arguments. Parses are memoized per request.
158- Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's
159 `NotFoundError` to render a branded 404.
160- `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone.
161- `Body` is a boxed `http_body::Body` used for both requests and responses, with
162 `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can
ca76e1bdocs: fix milestone cross-references after the reorder24d
163 stream both directions without buffering. This is what makes Milestone 4 viable.