collin/anvil · 836274ec
docs: add CLAUDE.md and session resume notes in TODO.md
Collin Richards · 2026-06-09 21:52 UTC · 836274ecbf88554b93560d99bc4a953d7d5f6995 · parent f0c2b35b · browse files
addedCLAUDE.md+8 −0
| 1 | + | # anvil | |
| 2 | + | ||
| 3 | + | A minimal, self-hosted git forge in Rust, built on gix (gitoxide). | |
| 4 | + | ||
| 5 | + | Current status, resume notes, the agreed next steps (a/b/c), and the roadmap live | |
| 6 | + | in the TODO. Read it first: | |
| 7 | + | ||
| 8 | + | @TODO.md |
modifiedTODO.md+88 −2
| 1 | 1 | - [ ] don't expose a users email on their profile page | |
| 2 | - | - [ ] implement github action style CI feature | |
| 2 | + | - [ ] don't include anvil before the breadcrumbs in the repo name i.e. anvil/collin/repo just do collin/repo | |
| 3 | + | - [x] implement github action style CI feature _(core done — see "Session notes" below; the "isolated workers" idea is the (c) sandboxed broker, still TODO)_ | |
| 3 | 4 | - I want to get this to a sufficient state that we could bootstrap this app using this CI and automate deployments if we wished | |
| 4 | 5 | - probably might want to have other isolated anvil workers or something running for CI jobs | |
| 5 | 6 | - [ ] implement github pages style hosting feature | |
| 6 | - | - [ ] security audit | |
| 7 | + | - [ ] security audit _(started: see (b) threat-model below)_ | |
| 8 | + | ||
| 9 | + | --- | |
| 10 | + | ||
| 11 | + | # Session notes / resume point | |
| 12 | + | ||
| 13 | + | _Last updated: 2026-06-10. Working state is clean: `cargo build`, `cargo clippy | |
| 14 | + | --workspace`, `cargo fmt --all`, and `cargo test --workspace` all pass. | |
| 15 | + | Everything below is UNCOMMITTED (repo convention: commit only when asked)._ | |
| 16 | + | ||
| 17 | + | Two of your top-of-file items are quick wins we noticed but did NOT do yet: | |
| 18 | + | - **profile email** — `user_profile` in `crates/anvil-web/src/ui.rs` renders | |
| 19 | + | `owner.email`; just drop that block. | |
| 20 | + | - **breadcrumb `anvil/` prefix** — `repo_index` header in the same file starts | |
| 21 | + | with `a href="/" { "anvil" } " / "`; remove the leading anvil link. | |
| 22 | + | ||
| 23 | + | ## Done this session | |
| 24 | + | ||
| 25 | + | - **CI UI** — runs list `/{owner}/{repo}/ci`, run-detail (status/timing/log), | |
| 26 | + | per-commit status badges, "CI" nav link. (`crates/anvil-web/src/ui.rs`) | |
| 27 | + | - **CD redeploy webhook** — on a green run of `[ci] deploy_branch` in the single | |
| 28 | + | `[ci] deploy_repo`, POST to `[ci] deploy_webhook` (`X-Anvil-Deploy-Secret` | |
| 29 | + | header). Scoped to ONE repo. `CiConfig` in `crates/anvil-core/src/config.rs`; | |
| 30 | + | `deploy()` in `crates/anvil-ci/src/lib.rs`. Docs: `DEPLOY.md` §7, | |
| 31 | + | `deploy/anvil.toml`. `reqwest` added with NO TLS feature (keeps musl | |
| 32 | + | cross-compile aws-lc-free). | |
| 33 | + | - **Docker socket on hagrid** — `deploy/run.sh` mounts it + `--group-add`s the | |
| 34 | + | gid for the non-root user; caveat in `DEPLOY.md` §4. | |
| 35 | + | - **Toasty ORM cleanup** — `ci.rs` `list_by_repo`/`latest_for_commit`/ | |
| 36 | + | `queued_ids` now sort/limit/filter in SQL, not in memory. Verified by the new | |
| 37 | + | `ordering_and_limit_run_in_the_database` test. (Sweep: these were the only | |
| 38 | + | real instances; `repos::list_all_with_owner` sorts by a joined username and | |
| 39 | + | needs all rows — intentionally left.) | |
| 40 | + | - **(a) CSRF + cookie hardening** — | |
| 41 | + | - Cookie: `HttpOnly` + `SameSite=Lax` + `Secure` (auto via | |
| 42 | + | `Config::secure_cookies()` when base_url is https). | |
| 43 | + | - Synchronizer token `HMAC-SHA256(server_secret, session)`; secret persisted | |
| 44 | + | at `data_dir/csrf_secret` (`App::csrf_token` in `crates/anvil-core/src/lib.rs`). | |
| 45 | + | Deps `hmac`, `sha2`. | |
| 46 | + | - `Csrf` extractor + constant-time `verify_csrf` (`crates/anvil-web/src/auth.rs`). | |
| 47 | + | Hidden `csrf` field + verification on add/delete SSH key, new repo, repo | |
| 48 | + | settings. Login exempt; logout relies on SameSite. | |
| 49 | + | - htmx insurance: `auth::csrf_context` middleware → request-scoped task-local; | |
| 50 | + | `layout` sends the token via `hx-headers` on every htmx request. | |
| 51 | + | ||
| 52 | + | ## Next up (the agreed a/b/c plan — (a) done) | |
| 53 | + | ||
| 54 | + | ### (b) untrusted-mode threat-model doc ← START HERE | |
| 55 | + | Write `docs/untrusted-mode.md` (or `SECURITY.md`). Capture the severity-ranked | |
| 56 | + | analysis: | |
| 57 | + | 1. **CI runner = root-equiv RCE via Docker socket** — the hard blocker; fix is (c). | |
| 58 | + | 2. Stored XSS if we ever serve raw blobs → separate origin + `text/plain` + CSP. | |
| 59 | + | 3. Git resource exhaustion (pack/decompression bombs, huge ref ads) → size/ | |
| 60 | + | storage quotas + timeouts. | |
| 61 | + | 4. Open registration anti-abuse (email verify, rate limit, CAPTCHA, quotas, ban). | |
| 62 | + | 5. Authorization granularity → collaborator roles + per-repo tokens / deploy keys. | |
| 63 | + | 6. Webhook SSRF (when webhooks land) → block private IPs / metadata / localhost. | |
| 64 | + | Already-right: private repos 404 (no leak), reserved usernames + `/-/`, CI | |
| 65 | + | tar-upload (not bind-mount), single-repo CD gate, argon2, CSRF + Secure cookies. | |
| 66 | + | Recommendation to record: single-tenant/owner-operated stays the supported | |
| 67 | + | stance; untrusted is gated behind (c). | |
| 68 | + | ||
| 69 | + | ### (c) sandboxed CI broker | |
| 70 | + | Make anvil the only Docker client; the job container gets NO socket. Forbid bind | |
| 71 | + | mounts; `--cap-drop=ALL`, `--security-opt=no-new-privileges`, read-only rootfs, | |
| 72 | + | non-root uid, `--pids-limit`, mem/cpu caps, wall-clock timeout, egress limits, | |
| 73 | + | image allowlist + CI-minute quotas. Stronger tier: gVisor/Kata/Firecracker | |
| 74 | + | microVMs (this is the "isolated workers" idea from the list above). Current | |
| 75 | + | runner: `crates/anvil-ci/src/lib.rs::execute`. | |
| 76 | + | ||
| 77 | + | ## Loose ends | |
| 78 | + | ||
| 79 | + | - **CSRF header consumption:** `hx-headers` sends the token as a `csrf` header, | |
| 80 | + | but `verify_csrf` only reads the form field. When we add a tokenless htmx | |
| 81 | + | action (raw `hx-post`/`hx-delete`, no `<form>`), also read the `csrf` header. | |
| 82 | + | - **Toasty migrations:** schema only pushed on a fresh DB (`db::connect`); new | |
| 83 | + | columns won't apply to an existing DB until migrations land. (The | |
| 84 | + | `data_dir/csrf_secret` file is created automatically — no DB change.) | |
| 85 | + | - **Suggested commits when ready:** (1) CI UI, (2) CD webhook + deploy wiring, | |
| 86 | + | (3) ci.rs ORM cleanup + test, (4) CSRF + cookies. Trailer: `Co-Authored-By: | |
| 87 | + | Claude ...`. | |
| 88 | + | ||
| 89 | + | ## Remaining roadmap (plan milestones beyond a/b/c) | |
| 90 | + | ||
| 91 | + | 8. Issues · 9. Pull requests (gix merge) · 10. Webhooks (mind the SSRF item) · | |
| 92 | + | github-pages-style static hosting (your list item). |