Publishing the name was a block of printed instructions, which put the
knowledge in whoever last read them. It is now a step like every other:
absent records are created, correct ones are reported, and a name that
points somewhere else is left alone with a pointer to public-dns.md §4.
`--skip-dns` opts out.
The apex takes a CNAME to the site indirection, not an A record.
Cloudflare flattens it, and it buys the property §2 is about -- a WAN
address change is one record rather than a hunt across zones. bansko.io
already does this; the claim in public-dns.md §3 that the fleet's apexes
use A records was wrong and is corrected there. Note that `dig` cannot
tell you which a zone uses, because flattening makes both answer with an
A; only the API can.
The script lives in script/publish-dns.sh and is piped to the proxy
rather than embedded as a heredoc. Quoting shell through an unquoted
heredoc silently mangled two earlier versions of this same code -- once
producing `tr -d "'" ")` -- and a file can be linted and run directly.
It executes on the proxy because that is where the Cloudflare token is,
and the token is never copied off that host: it can rewrite DNS for
every zone on the account.
Token parsing uses awk rather than a PCRE lookbehind. Widening the
lookbehind to tolerate variable spacing made grep fail outright --
lookbehinds must be fixed-length -- so the fixed-width form would have
broken silently the day someone reformatted the credentials file.
Verified: first run created both records, second reported them ok,
rustingface.com and www both resolve to the site address, and the
Let's Encrypt chain validates against the public trust store.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
Building the Cloudflare guidance out of nested-quote `echo` lines
mangled the credential extraction into
... | tr -d "'" ")
which is a syntax error if pasted. The whole point of printing it is
that somebody pastes it.
Prints a quoted heredoc instead, so the shell reproduces it verbatim,
and substitutes the three values with a sed pass afterwards -- doing it
inside the heredoc would put the quoting back in play, which is what
broke it. Verified by extracting the rendered snippet and running
`bash -n` over it.
Also states the ordering that matters, since the certificate and vhost
now exist while the name does not resolve: land a deploy that enforces
auth before publishing DNS, because until then an unresolvable name is
the only thing keeping the bucket private. And corrects the closing
advice, which still listed the pre-CLIENT_TOKENS secret set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
The lock added alongside compare-and-set removed its map entry inside
the critical section. A queued waiter still held that mutex, so the next
arrival found no entry, created a fresh one, and the two then ran
concurrently -- defeating the lock exactly when there was contention,
which is the only time it does anything.
I previously reported this fix as working on the strength of ten clean
release-mode runs. That was wrong: in debug, where the timing differs,
the guarding test fails 3 times in 12. Retiring the entry only after
dropping our own reference, under the same shard lock `entry` takes,
gives 0 failures in 25 -- and restoring the old behaviour reproduces 3
in 12, so the test really does catch it.
The consequence was the one the test names: a file served to a client
but never recorded, and so invisible the moment the instance is sealed.
Real against the local filesystem store, which has no conditional put
and relies on the verify-after-write retry; MinIO's compare-and-set
covers the same race, which is why the deployed path never showed it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
Three routes, per architecture/generic.md §4 (Vite + React + SWC + TS,
static, served by nginx): / renders the repository readme, /models is a
paged and filterable listing of what the bucket holds, and
/{namespace}/{name} shows one repository -- its pinned refs, the files
actually stored, and its model card.
The API it reads is /v1/, deliberately not /api/. That surface is the
Hub's, recorded and replayed verbatim, and adding routes of our own to
it risks a client mistaking one for the real thing. /v1/ answers a
question the Hub has no equivalent for -- what is in this bucket --
which is inventory, not the model search the spec rules out.
The listing shows only what has actually been fetched, never upstream's
siblings. A repository pulled one file at a time shows one file, which
is the honest answer to "what can I get from here offline".
Model cards are third-party content, so their HTML is parsed and then
sanitised against GitHub's allowlist. The plugin order is load-bearing
and commented as such. Verified against a card crafted with <script>,
onerror, a javascript: href, an <iframe>, an SVG-embedded script and an
inline handler: none execute and ordinary markdown still renders. Card
images are not loaded at all -- fetching them would leak the viewer's
address to a third party and make an offline registry's pages depend on
the internet.
Routing: rustingface's URL space is the Hub's, so /Qwen/Qwen3-0.6B is
both a page and the prefix of a file. The vhost splits them the way the
Hub does -- /resolve/ anywhere in the path, plus /api/ and /v1/, go to
the service; everything else is the app. A repo legitimately named
v1/repos is handed back to the resolve path by the router, and there is
a test for it. The /resolve/ test is on the repo type rather than the
path substring, because a repo may contain a directory called resolve
and /api/models/a/b/tree/main/resolve/f must stay a tree request.
Deployment: the frontend ships to hanzalova:/var/www/rustingface, so
that host now gets its own scoped gitea_ci drop-in -- narrower than the
service host's: a webroot rsync, a relabel, nginx -t and a reload. The
health check probes rf.internal from the proxy rather than from the
runner, because a runner is a plain Fedora container with no internal
root CA (verified: fedora:43 gets 000, the proxy gets 200).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
The render step guards against shipping a config with an unrendered
{{PLACEHOLDER}} by grepping for a bare "{{". The template's own header
comment explains the substitution syntax and contains that string, so
the guard matched line 3 of every successfully rendered config and
failed the deploy with "unrendered placeholder in the config" -- while
every actual placeholder had been substituted correctly.
Matches the placeholder shape ({{UPPER_SNAKE}}) instead, so prose about
the syntax is not a false positive, and rewores the template comment to
keep it clear of the guard either way.
Also whitelists the post-deploy `doctor` invocation in the scoped
sudoers drop-in. It runs as the service account rather than root -- the
point is to prove the credentials the service will actually use can
reach and write to the bucket -- and `sudo -u rustingface` was not
covered by any existing rule, so the step would have been refused.
Verified with `sudo -l -U gitea_ci`: the rule is listed, and `sudo -u
rustingface /bin/bash` remains denied.
Found by auditing every deploy step against the installed whitelist
after run 7 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
The post-reload check compares the certificate on the wire against the
one on disk, because nginx keeps its previous cycle -- and its previous
certificates -- when a reload cannot rebind, while still reporting
success. That check is right and it fired on the first run of this
script.
It was a false positive. Old workers finish their in-flight connections
before exiting, so for a second or two after a reload either cycle may
answer, and the check sampled the old one. A warning that cries wolf on
every first install is worse than no warning, because the real thing
gets ignored.
Retries for up to twenty seconds before reporting a mismatch. Verified
against hanzalova: the first run warned, the serials matched moments
later, and a re-run reports the match immediately.
Also makes the caveman and DNS steps report state rather than always
printing instructions, now that both are provisioned, and points the
bucket guidance at architecture/object-storage.md §3 -- including why
the script refuses to read the MinIO root credentials itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
Scaffolds the workspace per architecture/generic.md §1 and implements
phases 0-3 of doc/spec.md §12.
Crates:
entities manifest/ref/repo schemas, bucket key layout, config, the
X-Error-Code taxonomy. No I/O.
core resolver, freeze pinning, single-flight, the streaming tee,
policy, gc/verify/refresh. Defines the Store and Upstream
ports.
data object_store (S3 + local) and reqwest Hub adapters.
api the axum surface: resolve, model/dataset info, tree, refs,
whoami, metrics, bearer auth, range handling.
bin one binary: serve plus fetch/pin/refresh/list/show/rm/gc/
verify/doctor.
Deployment targets bob.hanzalova.internal:20482 (port derived per
architecture/port-allocations.md §3), storing to the MinIO on
caveman.kosherinata.internal, fronted by hanzalova at rf.internal.
Ships the sysusers drop-in, hardened unit, firewalld service, nginx
vhost, config template, infra-setup.sh and the Gitea Actions
ci/deploy/conformance workflows.
Testing: 112 unit and integration tests, including the sovereignty
suite (cold fetch, sealed replay, single-flight, client disconnect,
range resume, freeze stability, gc-after-rm, digest mismatch), plus a
conformance suite driving a pinned huggingface_hub against a real Hub.
Deviations from the spec, all deliberate:
- one binary with subcommands (spec §9) rather than generic.md's
separate -api and -cli binaries; the library split is unchanged.
- a dedicated sysusers account and hardened unit (generic.md §8)
rather than the spec's illustrative DynamicUser unit.
- manifests carry an optional repo_tree recorded verbatim, resolving
spec §13's "record whole, filter on read" question for the tree
endpoint as well as model-info.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64