error: redundant reference in `assert_eq!` argument
--> tests/injection.rs:633:13
= note: `-D clippy::useless-borrows-in-formatting` implied by `-D warnings`
No code changed to cause this. ci.yml installs whatever rustup calls
stable and the tree pins no toolchain, so the gate moved under the
repository: `useless_borrows_in_formatting` fires on a line that has been
there since the injection tests were written, and `RUSTFLAGS: -D
warnings` turns it into a hard failure.
The borrow was never doing anything -- `{:?}` formats a String and a
&String identically -- so this is the lint being right, not a workaround
for it.
Verified against the toolchain CI actually resolved to (1.97.1, not the
1.85 in Cargo.toml's rust-version), all four gates the workflow runs:
fmt clean, clippy --all-targets --all-features clean with no other lint,
208 tests passing, and cargo deny reporting advisories/bans/licenses/
sources ok.
Worth separating from this fix: the failure was invisible until it
happened because the toolchain floats. Nothing here pins it, so the next
stable release can turn this repository red without a commit. A
rust-toolchain.toml would close that, at the cost of not seeing new lints
until it is bumped -- a policy call, not a bug fix, so it is not in here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
JRay public server
A community manifest exchange for JRay. Jellyfin servers running the JRay plugin pull actor-timeline manifests ("Jmanifests") for titles they own instead of running the CV pipeline locally, and optionally contribute the manifests they generate back.
See SPEC.md for the design. Section references throughout the code
point at it. If you are considering running an instance, read
docs/legal-posture.md first: it states what an
instance holds, what it structurally cannot do, and the operator checklist.
The community instance is https://jray.tourolle.paris. The JRay plugin
ships with it pre-configured but disabled — §9 requires that no traffic leave
an installation until an admin opts in, so the default entry exists to save the
admin from typing a URL, not to enable sharing on their behalf. Set
JRAY_SERVER_ID=jray.tourolle.paris when deploying that instance: it becomes the
origin stamped on manifests it first accepts (§9a) and the salt for report IP
hashes.
Status
First implementation pass: the core vertical slice, reconciled against the system spec.
Per-requirement status is in docs/requirements.md —
32 requirements (UR-001..018, DR-001..014), each tracing up to an SR-nnn
or PR-nnn. UR-015..018 are the pending SR-003 schema bump and are marked
Planned rather than omitted.
Implemented:
- §2 Jmanifest format and series bundles
- §3 cut matching —
exact/runtime/loosetiers - §4 the API surface, less
POST /manifests/search - §5 rate limiting, in-process fixed-window counters
- §5a trust model — anonymous bearer tokens, closed-vocabulary storage, automatic revocation
- §6 upload validation, all four stages
- §7 relational storage, no JSON blobs on the write path
- §8 Rust + Axum + SQLite, single serialized writer, in-process job queue
- §9a content addressing (
content_id), computed on upload - §9a federation — change feed, fetch by
content_id, batchhave, peer directory, capabilities, and a pull worker that re-derives judgement rather than inheriting it: every pulled manifest runs the full §6 validation and this server's own cast check, and the body is verified to hash to thecontent_idrequested before it is stored
Schema version 2 (SR-003). jmanifest_version moved to 2 in lockstep with
the truth file's schema_version — breaking changes are batched and ship
together across all three repos. It is a flag day: version 1 is rejected
outright rather than carried alongside, because a v1 read path would be the one
nobody exercises and so the one that rots.
Reconciled with the system spec (see docs/requirements.md for the detail):
anneal_secremoved. Withdrawn upstream by AR-012/AR-013 — presence now follows track extent, so a track survives its own gaps and there is nothing to anneal. Its successorextinction_secand the newgallery_scopeare accepted, stored, and (for scope) used in §7 ranking. A manifest still carryinganneal_secis now a hard400, not silently ignored: it was produced by a pipeline whose window semantics differ from what this server assumes.- Windows carry belief and route.
scenesare objects rather than float pairs:{ "start", "end", "belief", "route" }, where belief is the posterior that justified the claim and route islive/deferred/pooled. Both are excluded fromcontent_id— belief is a producer-side estimate that may differ between pipeline versions for identical timings, so hashing it would give two servers different ids for the same content.src/content_id.rsis unchanged by the bump and its golden vector still passes, which is the evidence rather than the claim. - Audio signature: the 120 s rule now matches both producers. An earlier
draft of §3 allowed a shortened window for items under 150 s; that conflicted
with
scene-actor-extractionIR-007 and was the weaker rule, since a caller-varying length is the property SR-004 forbids. Items under 120 s now send no signature at all.
Deferred:
- §3 audio signatures — the field is accepted, validated and stored, and
content_idalready excludes it, butaudio-tier matching andPOST /manifests/searchare not wired up. This follows §3's own recommended sequencing: ship the plugin-side computation first, let signatures accumulate, then enable matching once coverage is useful. (Federation landed — see below.)
Running
cargo run
Configuration is entirely environment variables:
| Variable | Default | Purpose |
|---|---|---|
JRAY_BIND |
127.0.0.1:8080 |
Listen address. Terminate TLS at the operator's proxy (§8) |
JRAY_DB |
jray.db |
SQLite path. WAL mode, created on first run |
JRAY_TMDB_API_KEY |
— | Hard dependency for UR-3. Without it, uploads stay pending and are never listed |
JRAY_TMDB_BASE_URL |
https://api.themoviedb.org/3 |
Override for testing |
JRAY_TRUSTED_PROXIES |
— | Comma-separated proxy IPs whose X-Forwarded-For is honoured. Not default-on: §5 rate limiting and report attribution key on client IP, so a spoofable header defeats both |
JRAY_SERVER_ID |
localhost |
This server's identity, used as manifest origin and as the report IP-hash salt |
JRAY_REQUEST_TIMEOUT_SEC |
30 |
Request timeout so a slow bundle query fails fast |
JRAY_JOB_BATCH |
8 |
Cast-check jobs leased per worker tick |
JRAY_JOB_POLL_SEC |
5 |
Worker poll interval |
JRAY_LOG |
info |
tracing filter |
Contributing requires a token (§5a) — an anonymous bearer capability, not an account. Self-issue one:
curl -sX POST -H 'content-type: application/json' -d '{}' \
http://127.0.0.1:8080/api/v1/tokens
Deployment
§8's deployment notes are requirements, not suggestions:
- Enforce the body cap at both the proxy and the app.
client_max_body_size(nginx) /request_body max_size(Caddy) should match §6 stage 1, so oversized uploads are dropped at the edge and never occupy an application worker. The app must also be safe when run without a proxy, which it is. - Set
JRAY_TRUSTED_PROXIESto the proxy's address, orX-Forwarded-Foris ignored and every client behind it shares one rate-limit bucket. - Back up the SQLite file with
VACUUM INTOor the backup API — never a plain file copy of a live WAL database. Manifests represent real CV compute.
Tests
cargo test # 212 tests
cargo deny check # advisories, licences, bans, sources
scripts/traceability-gate.sh # requirement coverage
The traceability gate needs the shared tooling submodule:
git submodule update --init --recursive
It reports coverage against docs/requirements.md,
flags orphan tags (an ID no register defines), and fails on a >100% ratio — the
signal that the computation itself is broken. Currently 25/32 (78%); the
untraced remainder is UR-007, which is plugin-side.
Unit tests per module, plus two integration suites:
tests/api.rs— end-to-end through the real router: status codes, headers, and the properties that only hold if the layers compose correctly (per-route body caps, rate-limit surfaces, the strict schema actually reaching uploads).tests/injection.rs— that hostile input cannot escape its layer: SQL payloads in query parameters, path segments, JSON bodies, bearer tokens and report notes; JSON structure abuse; CRLF header injection; path traversal; and Unicode tricks against the §5a character class.
Security-relevant properties are asserted rather than assumed —
movie/jellyfin_id rejection, the §5a character class defeating base64/hex
smuggling, per-route body caps, a lying Content-Length not bypassing the cap,
and a forged X-Forwarded-For not resetting a rate-limit budget.
On injection specifically. Two independent defences apply, and they fail differently, so both are tested:
- Parameterised queries. Every value reaches SQLite through
params![]. The onlyformat!-built SQL interpolates two compile-time constants (a column list and a status literal) — no runtime input ever becomes SQL syntax. This is what actually prevents injection. - Closed-vocabulary validation. Identifiers are regex-constrained and free text is limited to a closed character class, so most payloads never reach the query layer at all.
The injection suite would still pass on defence 1 alone, which is deliberate: if validation were ever loosened, the tests should not silently start depending on it.
Writing that suite found two real gaps, both since fixed:
- Compatibility homoglyphs (
𝐒𝐭𝐞𝐯𝐞,Actor) passed the §5a class. They are letters by Unicode category and NFC does not fold them — only NFKC would. Beyond name spoofing, a fullwidth-digit alphabet would have reopened the encoding channel the "no digits" rule exists to close. - A one-frame
audio_signaturewas accepted on a feature-length manifest, making the field the variable-length container §3 explicitly forbids. The length floor is now derived from the declared runtime, keeping §3's genuine short-item exception without trusting the client's length.
Two tests worth knowing about:
content_id::tests::golden_vector_hash_is_stablelocks the §9a canonical form. The JRay plugin must reproduce it byte-identically; §8 notes the extraction side is Python, so this can no longer be one shared implementation and must be cross-tested instead.GOLDEN_VECTORSis that fixture, and its hash was verified against an independent Python implementation.- The integration tests use an on-disk temporary database, not
:memory:, because §8's topology is one writer connection plus a read pool — in-memory SQLite is per-connection, so the readers would see an empty database.
CI and container image
.gitea/workflows/ci.yml runs on Gitea Actions (and unmodified on GitHub
Actions), gated fastest-first: fmt → clippy → test → cargo deny → static
musl build. The dependency audit also runs weekly, since advisories appear without
any code changing.
The Dockerfile is the optional convenience, not the intended deployment path —
§8 is explicit that neither Docker nor Compose should be required. It builds
against musl and runs from scratch as uid 65534: 8.7 MB, no libc, no shell,
no package manager. rusqlite bundles SQLite and reqwest uses rustls, so there
is nothing left to link.
docker build -t jray-server .
docker run --rm -p 8080:8080 -v jray-data:/data -e JRAY_TMDB_API_KEY=... jray-server
Two things that are easy to get wrong, so they are handled explicitly:
- Static linkage is asserted with
file, notldd. The musl target produces a static-PIE, andlddprints the musl loader for one — anldd-based check reports a perfectly static binary as dynamic. /datais staged with the runtime uid's ownership. Docker seeds a named volume from the image directory, ownership included, so a non-root server can create the database on first run. A bind mount is not seeded this way — the host directory keeps its own ownership, sochown 65534:65534it first or the server exits with "unable to open database file".
Two tests are worth knowing about:
content_id::tests::golden_vector_hash_is_stablelocks the §9a canonical form. The JRay plugin must reproduce it byte-identically; §8 notes the extraction side is Python, so this can no longer be one shared implementation and must be cross-tested instead.GOLDEN_VECTORSis that fixture, and its hash was verified against an independent Python implementation.- The integration tests use an on-disk temporary database, not
:memory:, because §8's topology is one writer connection plus a read pool — in-memory SQLite is per-connection, so the readers would see an empty database.
Known gaps
- The §6 stage-3 thresholds are still §10's guesses. §10 (5) is explicit that
running the check over the 331 real corpus files would give the true
distribution of honest-upload match ratios, and is "the single cheapest way to
de-risk UR-3 and UR-5". The scoring logic is deliberately pure functions in
castcheck.rsso that retuning is a test-data exercise, not a code change. POST /manifests/{id}/reportrecords reports but nothing consumes them yet. §5a's divergence detection and the operator kill switch are not implemented; delisting is currently a manualUPDATE, which §5a does note is the intended shape ("one UPDATE", not a moderation queue).- No admin surface. Revocation is automatic (§5a), but an operator has no endpoint for the deliberate kill-switch case.