# rustingface **A sovereign, HuggingFace-compatible model registry.** Single Rust binary. One config file. S3-compatible storage. Anything a client fetches through it lives in your bucket until you explicitly remove it. Set `HF_ENDPOINT` and nothing else changes: ```sh export HF_ENDPOINT=https://rf.internal python -c "from transformers import AutoModel; AutoModel.from_pretrained('Qwen/Qwen3-0.6B')" ``` The design is in [`doc/spec.md`](doc/spec.md). Architectural conventions this project inherits are in [`~/git/architecture`](https://git.lair.cafe/grenade/architecture) — workspace layout, deployment, systemd hardening, firewalld, SELinux, port allocation. --- ## What it guarantees 1. **Fetch-once retention.** Any blob served through rustingface is durably stored before the response completes. 2. **No eviction.** No TTL, no LRU, no background cleaner. Storage growth is the operator's problem, by design. 3. **Offline sufficiency.** Once a `(repo, revision, file)` triple has been served, it is servable forever with no network path to any upstream. 4. **Reference stability.** `main` resolves to the same commit on every later request until an operator repins it. 5. **State portability.** The bucket is the entire system state. It does not reimplement the Hub's semantics. It records the exact HTTP metadata upstream returned at first fetch — `ETag`, `X-Repo-Commit`, `X-Linked-Etag`, `X-Linked-Size` — and replays it byte-identically, so a `~/.cache/huggingface` populated through rustingface is interchangeable with one populated directly from the Hub. --- ## Layout ``` crates/ rustingface-entities/ manifest, ref and repo schemas; bucket key layout; configuration; the X-Error-Code taxonomy. No I/O. rustingface-core/ resolver, freeze pinning, single-flight, the streaming tee, policy, gc/verify/refresh. Defines the Store and Upstream ports. rustingface-data/ adapters: object_store (S3 and local) and reqwest. rustingface-api/ the axum surface and the header contract. rustingface/ the binary: serve, plus the admin subcommands. asset/ systemd, firewalld, nginx, config template script/infra-setup.sh one-time host provisioning test/conformance/ the suite that drives a real huggingface_hub ``` Two departures from `architecture/generic.md`, both deliberate: - **One binary, not `-api` plus `-cli`.** `doc/spec.md` §9 specifies a single binary with subcommands so the CLI and the service cannot drift. The library crate split is unchanged. - **`rustingface-api` is a library**, consumed by the binary, rather than being the binary itself. Same reason. --- ## Build and run ```sh cargo build --release cargo test --workspace cargo clippy --workspace --all-targets --all-features -- -D warnings ``` Locally, against a directory instead of a bucket: ```sh cargo run -- --config asset/config/config.toml.example serve HF_ENDPOINT=http://127.0.0.1:20482 hf download Qwen/Qwen3-0.6B config.json ``` `storage.local_path` forfeits guarantee 5 — a directory is not a bucket you can re-point a new host at — and the service says so at startup. It exists for tests and trials. ### Configuration One TOML file, `/etc/rustingface/config.toml` by default. Every key is overridable by environment (`RUSTINGFACE__SERVER__LISTEN`). Secrets are named by path, never inline, and a path that does not exist is retried under `$CREDENTIALS_DIRECTORY` so one file works both under systemd's `LoadCredential=` and when run by hand. See [`asset/config/config.toml.tmpl`](asset/config/config.toml.tmpl). The two settings worth understanding: - **`policy.ref_resolution`** — `freeze` (default) records the first resolution of a mutable ref and reuses it indefinitely. `follow` re-resolves upstream, which breaks guarantee 4 and warns at startup. - **`upstream.enabled`** — `false` seals the registry. No upstream client is constructed at all, so there is no code path that could reach the network. --- ## Admin CLI Same binary, subcommands. ``` rustingface serve # default rustingface fetch [@rev] [--files …] # warm the bucket ahead of need rustingface pin # set a ref explicitly rustingface refresh [] # re-resolve upstream and repin rustingface list [--repo-type models] # stored repos, revisions, sizes rustingface show [@rev] # manifest contents rustingface rm [@rev] --yes # remove refs/manifests (NOT blobs) rustingface gc [--dry-run] # delete blobs no manifest references rustingface verify [] # re-read blobs, check digests rustingface doctor # config, S3 reachability, permissions ``` `rm` and `gc` are separate on purpose. Removal detaches metadata; reclamation is a second, explicit, dry-runnable step. Nothing deletes bytes without an operator typing `gc`. --- ## Deployment | Concern | Where | | --- | --- | | Service | `bob.hanzalova.internal:20482` | | Object storage | MinIO on `caveman.kosherinata.internal:9000`, bucket `rustingface` | | Ingress | `rf.internal`, via the hanzalova edge proxy (mesh-only, no public record) | | Port | `20482`, derived from the service name per `architecture/port-allocations.md` §3 | Deployment is CI-driven (`.gitea/workflows/deploy.yml`): push to `main`, the workflow runs the lint/test gate, builds a static musl binary, ships it as `gitea_ci` over scoped sudo, and health-checks with `/healthz` followed by `rustingface doctor` — which proves the bucket is actually writable rather than merely that a socket opened. One-time host provisioning is `script/infra-setup.sh`, run by an operator from a workstation with full sudo. It is idempotent and skips unreachable hosts. Required Gitea repo secrets: `RSYNC_SSH_KEY`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `HF_TOKEN`. The service holds no durable state, which is what makes the systemd hardening easy: `ProtectSystem=strict` with no `ReadWritePaths` and no `StateDirectory` at all. --- ## Testing ```sh cargo test --workspace # unit + the sovereignty suite python3 -m venv .venv && .venv/bin/pip install -r test/conformance/requirements.txt .venv/bin/python test/conformance/conformance.py --binary target/release/rustingface ``` `crates/rustingface-api/tests/sovereignty.rs` runs the whole service against a fake Hub that speaks the real header protocol and counts what it was asked for: cold fetch, sealed replay, single-flight (eight clients, one upstream fetch), client disconnect mid-transfer, range resume, freeze stability across an upstream commit move, `gc` after `rm`, digest mismatch, deduplication, policy, bearer auth, and state portability. `test/conformance/conformance.py` drives a **pinned, unmodified `huggingface_hub`** through rustingface against the real Hub, then seals it and re-runs. It also re-checks the `X-Error-Code` strings against the installed client, because those are client-internal constants rather than a stable API — a client upgrade is a spec-review trigger. Run the real offline test by hand before trusting a deployment: null-route `huggingface.co` at the site router and verify there, not by trusting the service. --- ## Xet The Hub is migrating to Xet-backed storage. rustingface implements none of it: it never advertises Xet capability downstream and never negotiates it upstream, so files arrive whole through the Git LFS bridge. The cost is fetch-time bandwidth on first pull. The benefit is that the on-disk format stays "files with names" and no future maintainer can strand your data behind a chunk-reconstruction format. `upstream.disable_xet = false` is refused at startup. --- ## Licence GPL-3.0-or-later.