anvilsign in

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
11 - [ ] 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)_
34 - I want to get this to a sufficient state that we could bootstrap this app using this CI and automate deployments if we wished
45 - probably might want to have other isolated anvil workers or something running for CI jobs
56 - [ ] 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).