# rustingface — notes for agents Read [`doc/spec.md`](doc/spec.md) first; it is the contract, not a sketch. Then read `~/git/architecture/generic.md` for the conventions this repo inherits (workspace layout, deployment, systemd, firewalld, SELinux, commits) and `port-allocations.md` for why the port is `20482`. ## What must not regress The five guarantees in `doc/spec.md` §1 are contractual. In particular: - **Never normalise a replayed header.** `ETag`, `X-Repo-Commit`, `X-Linked-Etag` and `X-Linked-Size` are recorded as upstream sent them and emitted unchanged, quotes included. Client cache layouts are keyed on these. Do not compute a git blob SHA-1, derive an etag from content, or decide what a commit "should" be. - **Never let a redirect eat the Hub's headers.** The Hub answers a resolve request with a redirect and puts the replayable metadata on *that* response; the CDN it points at has none of it. `rustingface-data`'s upstream client follows redirects by hand for this reason. If you ever re-enable reqwest's own redirect policy, every recorded etag silently becomes wrong. - **Never set `Content-Encoding` on the resolve path.** The client sends `Accept-Encoding: identity` precisely so the sizes it learns are real. - **Never write a manifest entry before its blob upload has completed.** The ordering in `rustingface-core/src/fetch.rs` is deliberate: a crash may leak an orphan blob, which `gc` reclaims, but a manifest entry pointing at incomplete bytes is a silent correctness failure. - **Never complete a client response before the manifest write lands.** The tee runs one chunk behind for this reason. A file that was served but not recorded is invisible the moment the instance is sealed. ## Testing `cargo test --workspace` covers it, including `tests/sovereignty.rs`, which runs the real service against a fake Hub speaking the real header protocol. Add a case there for any behaviour a client depends on. The conformance suite (`test/conformance/`) needs network and a pinned `huggingface_hub`. Run it before claiming a change is safe for clients — most of the real bugs in this repo's history were found by it and not by the unit tests. It has also been run against the live MinIO on caveman; do that for anything touching `rustingface-data`, because the local filesystem store hides whole classes of problem (it has no multipart, no conditional update, and no presigning). ## The web UI `web/` is a Vite + React + SWC + TS app (`architecture/generic.md` §4), served statically from the nginx host and reading the `/v1/` API. - **Keep UI routes out of `/api/`.** That surface is the Hub's, replayed verbatim. Anything rustingface invents goes under `/v1/`. - **Model cards are untrusted third-party content.** `components/Markdown.tsx` parses their HTML and then sanitises it; the plugin order (`rehype-raw`, then `rehype-sanitize`) is not interchangeable, and reversing it would reintroduce the raw HTML unchecked. Card images are deliberately not loaded. - **Any new route must not shadow a repository id.** rustingface owns the whole URL root, which is the Hub's namespace, so every prefix collides with some repo. Where it is ambiguous, disambiguate in the handler the way `v1_repo_detail` does — and test it against `/resolve/` in a *directory* name, not just in a repo name. ## Auth, and what actually bounds the bucket Two credentials that are easy to confuse, pointing in opposite directions: - `upstream.token_file` (`hf-token`) authenticates **rustingface to the Hub**. Outbound only, never echoed downstream, never logged. Needed for gated repos. - `auth.token_file` (`client-tokens`) authenticates **clients to rustingface**. A client's token is never forwarded upstream, and rustingface's token is never shown to a client. Do not let these merge. `auth.mode = "bearer"` controls *who*, not *what* — a token holder can still cause any repository to be fetched. **`policy.allow_new_repos = false` is what bounds the bucket**, and the deployed configuration relies on it. `auth.anonymous = "catalog"` opens `/v1/` to unauthenticated requests. That is only safe while every handler under `/v1/` reads the bucket and never consults upstream. **If you add a route there that can fetch, you have made the public face able to fill the bucket** — there is a test asserting the whole surface causes zero upstream requests, and it should stay that way. ## Where things live Types in `entities`, logic in `core`, I/O in `data`, HTTP in `api`, wiring in the binary. `core` defines the `Store` and `Upstream` ports and must not gain a direct dependency on `object_store` or `reqwest`.