diff --git a/DNA_STRAND_MASTER_PLAN.md b/DNA_STRAND_MASTER_PLAN.md index 71cd27f..36a10fe 100644 --- a/DNA_STRAND_MASTER_PLAN.md +++ b/DNA_STRAND_MASTER_PLAN.md @@ -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).* diff --git a/LICENSES/NOTICE.md b/LICENSES/NOTICE.md new file mode 100644 index 0000000..864728d --- /dev/null +++ b/LICENSES/NOTICE.md @@ -0,0 +1,24 @@ +# Third-party notices — Windy Git + +Windy Git runs **stock Gitea** as an unforked component (D-2 / I-1). Gitea is +distributed under the MIT license, reproduced in `gitea-MIT.txt`. + +MIT's only obligation is that the copyright notice and license text travel with +copies of the software that are **distributed**. Running Windy Git as a hosted +service is not distribution, so strictly this file is not required today — it is +here anyway, because it will be required the moment a self-host bundle ships, and +because shipping it costs nothing. + +MIT does **not** require us to advertise the lineage, does not restrict +commercial use, and does not require publishing our modifications. That last +point is why Gitea (MIT) was chosen over Forgejo (GPLv3 from v9): GPLv3 does not +trigger on running a service — that is AGPL, and it is widely gotten wrong — but +it **does** trigger on distributing a binary, which the self-host line would do. + +**Gitea's trademarks are not used.** The product is branded Windy Git throughout. + +| Component | Version | License | +|---|---|---| +| Gitea | 1.24.6 (pinned) | MIT — `gitea-MIT.txt` | +| PostgreSQL | 16 | PostgreSQL License | +| cloudflared | 2026.1.2 | Apache-2.0 | diff --git a/LICENSES/gitea-MIT.txt b/LICENSES/gitea-MIT.txt new file mode 100644 index 0000000..a8d4b49 --- /dev/null +++ b/LICENSES/gitea-MIT.txt @@ -0,0 +1,20 @@ +Copyright (c) 2016 The Gitea Authors +Copyright (c) 2015 The Gogs Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/api/tests/test_invariants.py b/api/tests/test_invariants.py index f054b80..fba92ef 100644 --- a/api/tests/test_invariants.py +++ b/api/tests/test_invariants.py @@ -234,3 +234,53 @@ def test_g05_no_secret_literals_committed(): text = path.read_text(encoding="utf-8", errors="ignore") for pattern in patterns: assert not pattern.search(text), f"credential literal in {path}" + + +# -------------------------------------------------------------------------- +# G2.1 / G2.7 — the Gitea version is PINNED, and drift is a failure +# -------------------------------------------------------------------------- +def test_g21_gitea_version_is_pinned_not_latest(): + compose = (ROOT / "docker-compose.yml").read_text() + m = re.search(r"image:\s*\S*gitea/gitea:(\S+)", compose) + assert m, "no gitea image pin found" + assert m.group(1) != "latest", "G2.1: pin an exact Gitea version, never `latest`" + assert re.match(r"^\d+\.\d+\.\d+$", m.group(1)), f"not an exact version: {m.group(1)}" + + +def test_g24_gitea_license_travels_with_us(): + """MIT's one obligation. Cheap to honour, embarrassing to miss.""" + assert (ROOT / "LICENSES" / "gitea-MIT.txt").exists() + assert "MIT" in (ROOT / "LICENSES" / "gitea-MIT.txt").read_text() + + +# -------------------------------------------------------------------------- +# G4.3 — the two Gitea storage traps that cost a crash loop each +# -------------------------------------------------------------------------- +def test_g43_no_lfs_storage_type_override(): + """Naming a storage type inside [lfs] creates a SEPARATE storage section + that does not inherit endpoint or credentials from [storage], and Gitea + crash-loops with an error that names the symptom and not the cause.""" + # Check real settings only — the compose file deliberately NAMES this key in + # a warning comment so the next person does not re-add it. + active = [ + ln for ln in (ROOT / "docker-compose.yml").read_text().splitlines() + if ln.strip() and not ln.strip().startswith("#") + ] + assert not any("GITEA__lfs__STORAGE_TYPE" in ln for ln in active) + + +def test_g43_lfs_server_is_actually_enabled(): + """Setting the storage backend does NOT turn LFS on. Without this the batch + endpoint 404s and the client says 'Repository or object not found', which + reads like a permissions problem and is not one.""" + active = [ + ln for ln in (ROOT / "docker-compose.yml").read_text().splitlines() + if ln.strip() and not ln.strip().startswith("#") + ] + assert any("GITEA__server__LFS_START_SERVER" in ln for ln in active) + + +def test_g43_r2_checksum_trap_is_pinned(): + """R2 rejects the checksum algorithm S3 clients send by default.""" + compose = (ROOT / "docker-compose.yml").read_text() + assert "MINIO_CHECKSUM_ALGORITHM" in compose