G2/G4 complete: Gitea live, LFS landing in R2, four traps pinned

VERIFIED END TO END from outside the network:
  - create repo via API -> clone -> commit -> push -> read back over HTTPS
  - 3 MB LFS object pushed through the tunnel, landed in R2 at lfs/34/2d/...
  - NO local lfs/ directory on the host: I-3 confirmed by measurement
  - /health/full: db, gitea and r2 all green

Adds strand G4A recording four traps that each cost a crash loop, with tests:
  1. [lfs] STORAGE_TYPE creates a separate storage section that does not
     inherit [storage] — crash loop, error names the symptom not the cause
  2. storage backend != LFS enabled; LFS_START_SERVER is separate, and its
     absence reads as a permissions error
  3. Gitea env-to-ini SETS but never UNSETS — removing a compose var leaves the
     line in the persisted app.ini, so repo config and prod config silently
     disagree. Exactly the drift this cell exists to end.
  4. R2 rejects the default S3 checksum algorithm

And G4A.5, which is architecture rather than a bug: an 8 MB non-LFS push died
with HTTP 524 at Cloudflare's ~100s limit. This makes the LFS threshold
load-bearing and REQUIRES G10 to serve model weights via presigned R2 URLs
rather than proxying blobs through the tunnel — client straight to R2, which is
what Hugging Face does and which takes Grant's home upstream out of the path.

G2.4: Gitea's MIT text and a NOTICE now travel with the repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Grant Whitmer
2026-08-11 15:13:58 -04:00
parent 647d414ec1
commit 82044933ed
4 changed files with 142 additions and 0 deletions

View File

@@ -265,6 +265,54 @@ Strands G0–G4 are sequential. G5–G12 are concurrent once G4 lands.
- **G4.6** Quota: on push, call the kernel's quota check; over-quota → refuse with a repair-pointer error whose `speak` is grandma-words and whose `remediation_tool` names the upgrade path (Storage-Kingdom cross-sell hook, §0.5). **We emit the hook; the kernel owns the price** (I-11).
- **G4.7** Object-count and byte accounting per repo, recomputed nightly, exposed on `GET /{id}/status`.
## Strand G4A — Gitea/R2 traps paid for on 2026-08-11
Four failures hit while wiring G2–G4 live. Each cost a crash loop or a dead end,
and each presents as a different problem than it is. All four are now pinned by
tests in `api/tests/test_invariants.py`.
- **G4A.1 — `[lfs] STORAGE_TYPE` breaks storage inheritance.** Naming a storage
type inside `[lfs]` creates a **separate** storage section that does *not*
inherit the endpoint or credentials from `[storage]`. Gitea crash-loops on
`Endpoint: does not follow ip address or domain name standards` — an error that
names the symptom and not the cause. **Let LFS inherit `[storage]`.** Avatars
initialising correctly is the tell that the rest of the config is fine.
- **G4A.2 — setting the storage backend does not turn LFS on.** `[server]
LFS_START_SERVER = true` is separate. Without it the batch endpoint 404s and
the client reports *"Repository or object not found"*, which reads like a
permissions or credentials problem and is neither.
- **G4A.3 — ⚠️ Gitea's env-to-ini SETS but never UNSETS.** Removing a `GITEA__*`
variable from compose does **not** remove the line from the persisted
`app.ini`. The container will keep booting with a setting that no longer exists
anywhere in the repo, so the config in git and the config in production
silently disagree — the exact class of drift this whole cell exists to end.
**To remove a setting you must edit `app.ini` on the host**
(`/srv/windygit/git/gitea/conf/app.ini`), not just the compose file.
- **G4A.4 — R2 rejects the default S3 checksum algorithm.** Set
`MINIO_CHECKSUM_ALGORITHM = md5` or uploads fail with an opaque checksum error.
### G4A.5 — ⚠️ Cloudflare's 100-second limit caps a push, and it shapes G10
A plain (non-LFS) push of an 8 MB file over the tunnel died with **HTTP 524**.
Cloudflare's proxy times out at ~100 s on the Free plan, and Grant's residential
upstream measured ~19 KB/s during the LFS test, so anything large is a coin flip.
This is not a bug to fix; it is a constraint that **dictates the model-hub
architecture**:
1. **The LFS threshold (G4.5) is load-bearing, not tidiness.** Anything big must
go via LFS, because a large blob inside a git pack has no way to be resumed
or offloaded.
2. **G10 must serve LFS objects via presigned R2 URLs — client straight to R2 —
rather than proxying blobs through the tunnel.** That removes the 100 s
ceiling, removes Grant's home upstream from the path entirely, and is exactly
what Hugging Face does. Until that lands, model repos are capped by whatever
fits in 100 seconds.
**Verified working on 2026-08-11:** a 3 MB LFS object pushed through the tunnel
and landed in R2 at `lfs/34/2d/…`, with **no local `lfs/` directory on the host**
— I-3 confirmed by measurement rather than by assertion.
## Strand G5 — THE SHELTER (permissions plane over Windy Cloud) · **the v0 product**
*This strand is the one that ships first and the one that has no competitor. Windy Cloud today has no sharing, no permissions and no versioning of any kind (D-8).*