anvilsign in

collin/anvil

BoardRenderedSource

1# Misc TODO
2
3- [ ] should show subject line of latest commit in repo page view
4 - we should mirror certain things like this from github ui
5- [ ] don't expose a users email on their profile page
6- [ ] don't include anvil before the breadcrumbs in the repo name i.e. anvil/collin/repo just do collin/repo
7- [x] implement github action style CI feature _(core done — see "Session notes" below; the "isolated workers" idea is the (c) sandboxed broker, still TODO)_
8 - I want to get this to a sufficient state that we could bootstrap this app using this CI and automate deployments if we wished
9 - probably might want to have other isolated anvil workers or something running for CI jobs
10- [ ] implement github pages style hosting feature
11- [ ] security audit _(started: see (b) threat-model below)_
12
13---
14
15# Error in git push _(FIXED 2026-06-10, uncommitted)_
16
17**Root cause:** every push after the first sends a *thin pack* — deltas whose
18base objects aren't in the pack (the server already has them), referenced by
19object id (`REF_DELTA`). `vendor/gitserver-core/src/receive_pack.rs::write_pack`
20passed `None` as `thin_pack_base_object_lookup` to
21`gix_pack::Bundle::write_to_directory`, so gix couldn't resolve the bases and
22aborted. First-push-to-empty-repo worked because that pack is self-contained.
23
24**Fix:** pass the already-open `gix::Repository` as the lookup (it implements
25`gix_object::Find`). Regression test
26`receive_thin_pack_with_ref_deltas` builds a real thin pack via
27`git pack-objects --thin`, asserts it contains ref-deltas, and pushes it
28through `receive_pack`. Verified the test fails without the fix.
29
30Original report:
31
32```
33collin@mini ~/C/anvil (main)> git push
34Enter passphrase for key '/Users/collin/.ssh/id_ed25519':
35Enumerating objects: 117, done.
36Counting objects: 100% (117/117), done.
37Delta compression using up to 8 threads
38Compressing objects: 100% (62/62), done.
39Writing objects: 100% (66/66), 27.57 KiB | 3.94 MiB/s, done.
40Total 66 (delta 39), reused 0 (delta 0), pack-reused 0 (from 0)
41send-pack: unexpected disconnect while reading sideband packet
42fatal: the remote end hung up unexpectedly
43collin@mini ~/C/anvil (main) [128]>
44```
45
46server logs:
47
48```
4926-06-09T21:53:46.466577Z INFO anvil_ssh: ssh auth: accepted key SHA256:Rg41caN7vw2WYYxiJN6lrIlX0DTXYF0rC2QzKZW1tB0 (user 1)
502026-06-09T21:53:46.635946Z INFO anvil_ssh: ssh git-receive-pack on collin/anvil.git (user Some(1))
512026-06-09T21:53:46.805146Z ERROR anvil_ssh: git ssh git-receive-pack: protocol error: failed to write incoming pack: Ref delta objects are not supported as there is no way to look them up. Resolve them beforehand.
522026-06-09T21:54:28.571834Z INFO anvil_ssh: ssh auth: rejected unknown key SHA256:ZlWZyHqspqFeUQV84qaXtDQq4gcA33dR7y8dYbeg9u8
532026-06-09T21:54:35.565597Z INFO anvil_ssh: ssh auth: accepted key SHA256:Rg41caN7vw2WYYxiJN6lrIlX0DTXYF0rC2QzKZW1tB0 (user 1)
542026-06-09T21:54:35.676113Z INFO anvil_ssh: ssh git-receive-pack on collin/anvil.git (user Some(1))
552026-06-09T21:54:36.268520Z ERROR anvil_ssh: git ssh git-receive-pack: protocol error: failed to write incoming pack: Ref delta objects are not supported as there is no way to look them up. Resolve them beforehand.
562026-06-09T21:55:54.223033Z INFO anvil_ssh: ssh auth: rejected unknown key SHA256:ZlWZyHqspqFeUQV84qaXtDQq4gcA33dR7y8dYbeg9u8
572026-06-09T21:56:00.758384Z INFO anvil_ssh: ssh auth: accepted key SHA256:Rg41caN7vw2WYYxiJN6lrIlX0DTXYF0rC2QzKZW1tB0 (user 1)
582026-06-09T21:56:00.906553Z INFO anvil_ssh: ssh git-receive-pack on collin/anvil.git (user Some(1))
592026-06-09T21:56:01.067903Z ERROR anvil_ssh: git ssh git-receive-pack: protocol error: failed to write incoming pack: Ref delta objects are not supported as there is no way to look them up. Resolve them beforehand.
60```
61
62---
63
64# Session notes / resume point
65
66_Last updated: 2026-06-10. Working state is clean: `cargo build`, `cargo clippy
67--workspace`, `cargo fmt --all`, and `cargo test --workspace` all pass.
68Everything below is UNCOMMITTED (repo convention: commit only when asked)._
69
70Two of your top-of-file items are quick wins we noticed but did NOT do yet:
71- **profile email** — `user_profile` in `crates/anvil-web/src/ui.rs` renders
72 `owner.email`; just drop that block.
73- **breadcrumb `anvil/` prefix** — `repo_index` header in the same file starts
74 with `a href="/" { "anvil" } " / "`; remove the leading anvil link.
75
76## Done this session
77
78- **CI UI** — runs list `/{owner}/{repo}/ci`, run-detail (status/timing/log),
79 per-commit status badges, "CI" nav link. (`crates/anvil-web/src/ui.rs`)
80- **CD redeploy webhook** — on a green run of `[ci] deploy_branch` in the single
81 `[ci] deploy_repo`, POST to `[ci] deploy_webhook` (`X-Anvil-Deploy-Secret`
82 header). Scoped to ONE repo. `CiConfig` in `crates/anvil-core/src/config.rs`;
83 `deploy()` in `crates/anvil-ci/src/lib.rs`. Docs: `DEPLOY.md` §7,
84 `deploy/anvil.toml`. `reqwest` added with NO TLS feature (keeps musl
85 cross-compile aws-lc-free).
86- **Docker socket on hagrid** — `deploy/run.sh` mounts it + `--group-add`s the
87 gid for the non-root user; caveat in `DEPLOY.md` §4.
88- **Toasty ORM cleanup** — `ci.rs` `list_by_repo`/`latest_for_commit`/
89 `queued_ids` now sort/limit/filter in SQL, not in memory. Verified by the new
90 `ordering_and_limit_run_in_the_database` test. (Sweep: these were the only
91 real instances; `repos::list_all_with_owner` sorts by a joined username and
92 needs all rows — intentionally left.)
93- **(a) CSRF + cookie hardening** —
94 - Cookie: `HttpOnly` + `SameSite=Lax` + `Secure` (auto via
95 `Config::secure_cookies()` when base_url is https).
96 - Synchronizer token `HMAC-SHA256(server_secret, session)`; secret persisted
97 at `data_dir/csrf_secret` (`App::csrf_token` in `crates/anvil-core/src/lib.rs`).
98 Deps `hmac`, `sha2`.
99 - `Csrf` extractor + constant-time `verify_csrf` (`crates/anvil-web/src/auth.rs`).
100 Hidden `csrf` field + verification on add/delete SSH key, new repo, repo
101 settings. Login exempt; logout relies on SameSite.
102 - htmx insurance: `auth::csrf_context` middleware → request-scoped task-local;
103 `layout` sends the token via `hx-headers` on every htmx request.
104
105## Next up (the agreed a/b/c plan — (a) done)
106
107### (b) untrusted-mode threat-model doc ← START HERE
108Write `docs/untrusted-mode.md` (or `SECURITY.md`). Capture the severity-ranked
109analysis:
1101. **CI runner = root-equiv RCE via Docker socket** — the hard blocker; fix is (c).
1112. Stored XSS if we ever serve raw blobs → separate origin + `text/plain` + CSP.
1123. Git resource exhaustion (pack/decompression bombs, huge ref ads) → size/
113 storage quotas + timeouts.
1144. Open registration anti-abuse (email verify, rate limit, CAPTCHA, quotas, ban).
1155. Authorization granularity → collaborator roles + per-repo tokens / deploy keys.
1166. Webhook SSRF (when webhooks land) → block private IPs / metadata / localhost.
117Already-right: private repos 404 (no leak), reserved usernames + `/-/`, CI
118tar-upload (not bind-mount), single-repo CD gate, argon2, CSRF + Secure cookies.
119Recommendation to record: single-tenant/owner-operated stays the supported
120stance; untrusted is gated behind (c).
121
122### (c) sandboxed CI broker
123Make anvil the only Docker client; the job container gets NO socket. Forbid bind
124mounts; `--cap-drop=ALL`, `--security-opt=no-new-privileges`, read-only rootfs,
125non-root uid, `--pids-limit`, mem/cpu caps, wall-clock timeout, egress limits,
126image allowlist + CI-minute quotas. Stronger tier: gVisor/Kata/Firecracker
127microVMs (this is the "isolated workers" idea from the list above). Current
128runner: `crates/anvil-ci/src/lib.rs::execute`.
129
130## Loose ends
131
132- **CSRF header consumption:** `hx-headers` sends the token as a `csrf` header,
133 but `verify_csrf` only reads the form field. When we add a tokenless htmx
134 action (raw `hx-post`/`hx-delete`, no `<form>`), also read the `csrf` header.
135- **Toasty migrations:** schema only pushed on a fresh DB (`db::connect`); new
136 columns won't apply to an existing DB until migrations land. (The
137 `data_dir/csrf_secret` file is created automatically — no DB change.)
138- **Suggested commits when ready:** (1) CI UI, (2) CD webhook + deploy wiring,
139 (3) ci.rs ORM cleanup + test, (4) CSRF + cookies. Trailer: `Co-Authored-By:
140 Claude ...`.
141
142## Remaining roadmap (plan milestones beyond a/b/c)
143
1448. Issues · 9. Pull requests (gix merge) · 10. Webhooks (mind the SSRF item) ·
145github-pages-style static hosting (your list item).