3 Commits
Author SHA1 Message Date
grenadeandClaude Opus 5 3790f2589c fix(core): the per-key manifest lock did not actually exclude
deploy / build (push) Waiting to run
deploy / build-web (push) Failing after 55s
deploy / deploy (push) Has been cancelled
deploy / deploy-web (push) Has been cancelled
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
2026-08-31 13:03:55 +03:00
grenadeandClaude Opus 5 acd47a31c8 feat(web): add the operator UI and the /v1 inventory API it reads
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
2026-08-31 12:37:26 +03:00
grenadeandClaude Opus 5 cb1ca8b6af feat(rustingface): implement the registry, admin CLI and deployment
deploy / build (push) Waiting to run
deploy / deploy (push) Has been cancelled
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
2026-08-31 10:46:25 +03:00