Skip to content

feat: make Keryx deployable with pluggable storage, OCI sharing, Postgres and a config file - #12

Merged
prom3theu5 merged 57 commits into
mainfrom
feat/deployable
Sep 19, 2026
Merged

prom3theu5 merged 57 commits into
mainfrom
feat/deployable

Conversation

@prom3theu5

@prom3theu5 prom3theu5 commented Sep 19, 2026 •

Copy link
Copy Markdown
Member

Keryx could only run with a local disk: draft HTML lived in files, metadata lived in a local SQLite file, and everything was one crate where any module could reach any dependency. This makes Keryx deployable without a persistent volume, while an existing installation upgrades in place with no flags and can go back to 0.5.1 on the same files.

What changed

  • Workspace split. src/ becomes eight internal crates, so boundaries like "the TUI cannot touch the database" are compile errors rather than conventions. A pure move: the 58 test names were identical before and after.
  • Pluggable blob storage. A BlobBackend trait with one OpenDAL implementation for disk and S3. The blob write moved out of the database transaction, so a slow object store can never hold the database lock. Adds --storage and --s3-*, a startup probe, keryx storage migrate and keryx storage gc.
  • OCI sharing. keryx share, pull and inspect. A draft version becomes a single text/html layer, so someone with no Keryx gets the file from one oras pull. Pushes happen on the client with the operator's own registry credentials. Explicit :v<n> tags or digests only, never :latest.
  • SeaORM store, SQLite by default, Postgres opt-in. A DraftStore trait replaces hand-written rusqlite. An existing keryx.db is adopted in place after a VACUUM INTO backup, and PRAGMA user_version is left alone so rollback stays safe. --database-url selects Postgres, with an advisory lock around migrations and FOR UPDATE on the draft row.
  • Config file. ~/.config/keryx/config.toml, resolved as flag > environment variable > config.toml > built-in default. Every existing environment variable keeps its name. The old ~/.keryx/config.json is migrated into it.
  • Shutdown fix. Ctrl-C hung while a dashboard tab was open, because graceful shutdown waited on the live-update stream. Shutdown now ends those streams and also handles SIGTERM.
  • Version 0.6.0.

Parity with existing installations

This was the acceptance criterion. The parity gate runs at user_version 0, 1 and 2 and against a database written by the released 0.5.1:

  • the adopted schema matches one the migrator builds from empty;
  • every row matches what the old upgrade path produced, and the new store answers exactly what the old query layer answered, both held as golden files generated by the old code before it was deleted;
  • the real binary serves, uploads to, lists and publishes from a 0.5.1 installation, byte for byte.

A data directory written by the released 0.5.1 binary reads in place. Rollback was checked for real: 0.6.0 adopted a 0.5.1 database, 0.5.1 then served and uploaded on the same files, and 0.6.0 picked it back up.

Supply chain

  • aws-lc-rs never enters the build; CI now fails if it does. oci-client builds on ring with no TLS feature of its own, and there is one reqwest in the lockfile.
  • stylo is built from the SimCubeLtd fork pinned by revision. SeaORM enables derive_more's debug feature, and stylo 0.8.0 imports every derive_more macro with #[macro_use], so its #[derive(Debug)] stopped compiling. The fork carries that one fix; drop it when fulgur moves to a fixed stylo.
  • cargo vet exemptions went from 763 to 501 through imported audits, per-crate trust where at least two imported organisations agree, and seven hand audits. supply-chain/README.md records the policy and the dependency families that are knowingly unreviewed.
  • deny.toml allows CC0-1.0, and temporarily ignores RUSTSEC-2026-0285 until rustls 0.23.45 clears the 14-day age gate on 28 September.

Verified beyond the test suite

Against real services: MinIO (migrate, resume, source removal, serving, gc), registry:3.1.1 with a plain oras pull, a Harbor instance with bearer-token auth, and Postgres 18.6 including killing the database under a running server, which heals without a restart. The keryx-db and keryx-server suites pass on both SQLite and Postgres, and CI now runs them on both.

Every commit passes cargo deny and cargo vet --locked. On the head: fmt, clippy, 119 tests passed with 1 ignored (the registry round trip, which needs oras and a registry).

Notes for review

  • There is no SQLite to Postgres copy. A Postgres server starts empty and drafts are re-uploaded.
  • First-party clean rebuild time went from 3.5 s to 3.1 s with the split, which is marginal.
  • Follow-ups before tagging: remove the rustls advisory ignore after 28 September, and refresh the vet imports.

Summary by CodeRabbit

  • New Features
    • Added TOML configuration with clear precedence for flags, environment variables, and file settings.
    • Added optional PostgreSQL and S3-compatible storage support.
    • Added OCI artifact commands to share, inspect, and pull versioned drafts with integrity and policy validation.
    • Added offline storage migration and garbage-collection commands.
    • Added graceful shutdown handling and improved startup validation.
  • Bug Fixes
    • Existing databases and storage from earlier releases are migrated while preserving data, with optional backups.
  • Documentation
    • Expanded configuration, storage, migration, and sharing guidance.

… age gate

rustls 0.23.45 fixes the advisory but is five days old, so the 14-day
publish-age gate refuses it. Same temporary pattern as the chacha20 entry.
tiny-keccak is CC0-1.0 and arrives with reqsign's profile file parser,
which the S3 backend needs. CC0 is a public domain dedication, and kache
allows it the same way.
The root keeps the keryx binary, so cargo build --release still produces
target/release/keryx. Dependencies move to [workspace.dependencies] so the
internal crates that follow declare each one exactly once.
types.rs and ids.rs move unchanged. sha256_hex leaves main.rs and now /
format_timestamp leave db.rs, because the binary cannot export to its own
libraries and the CLI should not reach into the database for a string
format.
policy.rs, storage.rs and db.rs each become the lib.rs of keryx-policy,
keryx-store and keryx-db. The test helpers test_store() and
test_connection() sit behind a test-support feature so dependent crates
can still use them across the crate boundary.
render.rs and pdf.rs become keryx-render; server.rs, notifications.rs and
realtime.rs become keryx-server. Each asset moves next to the crate that
embeds it, so the nine include paths are unchanged. The two dashboard
fragment renderers go from pub(crate) to pub because the server now calls
them across a crate boundary.
…yx-client

client.rs becomes the crate root and gitmeta.rs its one module.
main.rs, cli.rs and tui.rs are what remain in src/. Their crate:: paths
become crate names, the temporary aliases in main.rs go, and the root
dependency list shrinks to what the binary itself uses.
Exemptions generated by cargo vet for reqwest 0.13.4 and the platform
certificate verifier it uses with rustls. An exemption records that a crate
is unreviewed; auditing happens before release.
OpenDAL's transport at 0.58.2 needs reqwest 0.13, so Keryx moves first and
the build keeps one HTTP stack. rustls-no-provider keeps aws-lc-rs out, so
rustls becomes a direct dependency and main() installs the ring provider
once, before the CLI or the server can make an HTTPS request. The
provenance test installs it too because it makes its own requests.

reqwest gains the query feature: 0.13 gates RequestBuilder::query behind
it and keryx-client fails to compile without it.
Exemptions generated by cargo vet for opendal 0.58.2, reqsign 3.3 and their
transitives. An exemption records that a crate is unreviewed; auditing
happens before release.
…eature

opendal-core, the fs and s3 services, the reqwest transport and reqsign are
pinned exactly as kache pins them, defaults off. The transport takes
rustls-no-provider so ring stays the only TLS provider. tokio gains fs for
the OpenDAL filesystem service, and async-trait arrives for the BlobBackend
trait that follows.

s3 is a default feature forwarded root -> keryx-server -> keryx-store, so
--no-default-features gives a disk-only build. A test builds an S3 and an
fs operator to prove the stack compiles and links before logic lands.
BlobBackend is the seam the rest of Keryx will hold as Arc<dyn>. One
OpenDalBackend covers disk and S3, picked by BackendConfig through
create_backend, and a memory operator sits behind test-support.

The disk operator roots at the data directory so existing files read in
place, always sets atomic_write_dir to <data-dir>/.staging (OpenDAL then
syncs before renaming, which today's code never does), empties that
directory at startup, and checks both sit on one filesystem. remove_many
tidies the emptied drafts/<id>/ directory on disk only.

The S3 operator follows kache: no retry layer, Content-MD5, the prefix as
operator root, and a credential provider that keeps --s3-profile and
credential_process working without touching the process environment. The
helper runs through std::process on a blocking task, so tokio needs no
process feature.

BlobStore stays until its callers move; its object_key now delegates so the
key shape has one definition.
The fixture is three uploads across two drafts made with the released
0.5.1 binary, plus the object keys, hashes and sizes it recorded. The disk
backend must read every key in place with matching content before the old
storage code can go.
record_upload opened a write transaction and called store.put() inside it.
With a network store that would hold the database lock for a multi-second
PutObject, and it cannot await at all. resolve_upload_target now answers
the draft id with a cheap read, the caller mints the version id and key and
writes the blob, and record_upload records metadata only.

Because the draft can now vanish between the two steps, record_upload
re-checks that it is live inside its transaction and returns DraftNotFound;
the upload handler then removes the blob it wrote, best effort. The blob
still lands before the metadata commits, so the ordering invariant holds.

keryx-db no longer depends on keryx-store. The handler keeps the old
synchronous BlobStore for one more commit.
AppState holds an Arc<dyn BlobBackend> built from the new --storage and
--s3-* flags. Startup probes the store with a write and a delete and aborts
with a readable error, and the banner reports the store and probe time.

The upload handler now awaits the blob put with no database lock held and
removes its blob when the record step rejects. serve_draft, publish_pdf and
remove_blobs are async; a version whose blob is missing serves a clean
not-found instead of a 500, and purge issues one remove_many call rather
than one request per key.

The synchronous BlobStore and test_store() are deleted now that the 0.5.1
compatibility gate has passed; server tests run on the memory backend.
Both are written against dyn BlobBackend in keryx-store and run offline
with the server stopped.

migrate copies every blob a version row records, reads each back from the
destination and checks it against the stored sha256. A key already there
with the recorded size is skipped, so an interrupted run resumes by
re-running. --remove-source deletes only after every key verified, and any
failure exits non-zero with the source untouched. It works in both
directions.

gc diffs the store against the recorded keys. It is report-only unless
--delete, and skips anything younger than one hour, because an upload now
writes its blob before its row commits.

The S3 flags move into a shared S3Args so serve and the storage commands
read the same flags and environment variables.
Covers the --storage and --s3-* flags, the minimal S3 permissions, the
startup probe, moving between stores, orphan collection, and the caveat
that S3 alone does not make Keryx multi-node while SQLite stays local.
Exemptions generated by cargo vet. An exemption records that a crate is
unreviewed; auditing happens before release.
oci-client 0.17 and docker_credential 1.4 arrive with defaults off behind a
default share feature. oci-client takes neither TLS feature: rustls-tls
hard-wires aws-lc-rs twice over and native-tls means OpenSSL. It compiles
because keryx-share declares reqwest with rustls-no-provider itself, and a
test builds a client to prove it links on the ring provider.

cargo tree -i aws-lc-rs finds nothing, the lockfile holds one reqwest, and
CI now fails if aws-lc-rs or aws-lc-sys ever enters the graph.
A draft version becomes a single-layer artifact: a text/html layer holding
the exact stored bytes and named <title-slug>-v<n>.html through the ORAS
title annotation, a self-describing config blob, an artifactType, and
standard plus namespaced manifest annotations. Following synapse's
share.rs, async stays inside this crate behind a current-thread runtime,
and it is the only crate that depends on oci-client and docker_credential.

Explicit versions only, unlike synapse: push writes :v<n> and nothing else,
and pull and inspect accept a :v<n> tag or a digest, checked after parsing
because Reference::parse rewrites a bare reference to :latest. Push first
reads the tag: the same manifest is a no-op, a different one is refused
without --force. Pull verifies the layer's sha256 against the recorded
content hash, and a mismatch or a missing hash is a hard error.

Registry auth resolves KERYX_REGISTRY_TOKEN, then KERYX_REGISTRY_USER and
KERYX_REGISTRY_PASS, then docker credentials, then anonymous.
Api::version answers the draft summary plus the requested version, or the
latest, which is what keryx share needs to describe the HTML it pushes.
share fetches a version's exact bytes through /raw and pushes it from the
operator's machine with their own registry credentials, so the server keeps
no registry secrets. The artifact title comes from that version's own
<title>, and its content hash is the sha256 of the bytes pushed.

pull verifies integrity, then runs validate_html against the destination
server's policy before uploading anything: a pulled artifact is untrusted
HTML and there is no way to skip the gate. It replays the original git
provenance through the upload metadata and records the reference, tag plus
resolved digest, as the upload filename, so there is no schema change.
--output writes a file and touches no server. inspect reads the manifest
and config only.

An ignored integration test covers the real interop path: share, then a
plain oras pull in a subprocess asserting filename and bytes, then inspect
and pull. It passes against registry:3.1.1.
The README gains a Sharing section that leads with the plain oras pull
recipe. keryx-read learns to read a shared reference through keryx pull
--output, and html-communication learns keryx share, on request only.
stylo 0.8.0 imports every derive_more macro with #[macro_use]. SeaORM
enables derive_more's debug feature, Cargo unifies it across the build, and
every #[derive(Debug)] in stylo then resolves to derive_more's Debug, whose
expansion overflows on stylo's recursive types. fulgur 0.40.0 is the newest
fulgur and pins this stylo, so there is no release to move to.

The fork is the published 0.8.0 commit plus that fix, pinned by revision.
cargo deny allows that one git source. cargo vet treats the fork as
third-party (audit-as-crates-io) and carries the same exemption at the git
revision; the policy and exemption change with the patch because vet cannot
pass without all three. Drop the patch once fulgur moves to a fixed stylo.
Exemptions generated by cargo vet for sea-orm 2.0.2, sea-orm-migration,
sqlx 0.9, sea-query and their transitives. An exemption records that a
crate is unreviewed; auditing happens before release.
Both take default-features = false with sqlx-sqlite, sqlx-postgres and
runtime-tokio-rustls, plus macros on sea-orm. Nothing uses them yet:
rusqlite stays the live path until the parity test passes. sqlx accepts
libsqlite3-sys from 0.30.1, so it shares rusqlite's bundled SQLite and the
build links one copy. No new crypto backend arrives; aws-lc-rs is still
absent.
One baseline migration, in Rust, branching on the backend. On SQLite it
runs the init() create block through execute_unprepared; the block moves
byte for byte into a constant that rusqlite's init() now shares, so there is
one copy. On Postgres it creates the same tables with BOOLEAN for the two
flag columns. It forces a transaction on both backends, because SeaORM only
wraps migrations by default on Postgres.

The Postgres block also uses BIGINT for version_number, file_size and
attempts. The plan says only booleans diverge, but a Postgres INTEGER is
32-bit and the entities read these as i64, which sqlx refuses to decode
from INT4.

connect_sqlite sets what SeaORM does not: WAL, foreign keys, a 5 second
busy timeout (rusqlite's default, so unchanged), create if missing, and a
pool of one. A test asserts WAL and foreign keys are on.
One entity per table, matching the existing columns exactly; a test
compares each entity's columns with PRAGMA table_info. Timestamps stay
String because they are TEXT on both backends, and git_dirty and
has_inline_script map to bool. The plan counts six entities, but the schema
has five tables.
An existing keryx.db has no seaql_migrations table; it sits at user_version
0, 1 or 2 with hand-rolled upgrade history. open_sqlite tells three cases
apart: a fresh database goes to the migrator, a managed one runs what is
pending, and a legacy one is adopted.

Adoption takes a VACUUM INTO snapshot first (a plain copy can miss pages
still in the WAL), then in one immediate transaction runs the IF NOT EXISTS
create block for tables that arrived later, the two old conditional upgrade
steps ported from init(), and inserts the baseline row. The plan lists only
the two steps, but a pre-0.5.0 database also lacks the push tables, which
init() used to add the same way. user_version is read and never written, so
an older binary still understands the file. The Adoption value says exactly
what changed, for the server log.

Fixtures rebuild history without Keryx code: the first release's schema and
the old steps as SQL, plus a database written by the released 0.5.1.
Run at user_version 0, 1 and 2 (columns appended by ALTER) and against a
database written by the released 0.5.1 (every column inline), which are the
two legacy populations.

Level 1, schema: an adopted database matches one the migrator builds from
empty, by column name, declared type, nullability, default and primary key,
plus indexes and their columns. Not raw sqlite_master text or column
order, which already differ between the populations.

Level 2, data: adoption leaves every row exactly as the old rusqlite
upgrade path does, and the old query layer answers identically from both.
Those answers are pinned as golden JSON so they outlive rusqlite and the
ported store can be held to them.

Level 3, end to end: the real binary serves, uploads to, lists and publishes
from an adopted 0.5.1 installation exactly as it does from an untouched
one, and serves what 0.5.1 stored byte for byte.

The plan reads level 2 through DraftStore, which does not exist until the
store port, so here it is judged through the old query layer instead.
NewUpload, UploadOutcome, the error enums, ServedVersion, BlobRecord,
PendingDelivery and normalize_wake_time mean the same thing on every
backend, so they move to their own module where the SeaORM store can share
them. They stay re-exported from the crate root; no caller changes.
DraftStore is the metadata seam the server will hold as Arc<dyn>. SeaOrmStore
ports every function of the rusqlite layer with the same names, semantics
and error variants, written once through SeaORM's builder for SQLite and
Postgres. Opening a SQLite store goes through adoption, so the pragmas and
the pool of one apply, and every write transaction begins immediate.

Three shapes changed without changing meaning. INSERT OR IGNORE became an
ON CONFLICT DO NOTHING insert. The INSERT ... SELECT that queued deliveries
became a select of opted-in subscriptions and an insert_many in the same
transaction. record_due_wakes built its NOT EXISTS key by concatenating in
SQL, which has no portable builder form, so it now computes the wake keys
and skips the ones already recorded.

The old test suite is ported onto the store unchanged in meaning, beside a
test that 24 concurrent uploads over two handles surface no SQLITE_BUSY and
number their versions without gaps. The parity gate now also asks the store
the golden questions, and it answers exactly as the old query layer did at
every legacy level.
AppState and the notification dispatcher take the store. Arc<Mutex<Connection>>
and every .lock().unwrap() are gone, so no handler holds a std mutex on a
Tokio worker any more, and the server crate no longer depends on rusqlite.

Startup opens the SQLite store through adoption: a database from an older
Keryx is snapshotted with VACUUM INTO, brought under migration management in
place, and the banner says what happened. --no-backup skips the snapshot.
/healthz asks the store to ping.

Server tests run on an in-memory store. The few that assert on rows no store
method exposes keep the concrete store and use a test-only peek.
…mmands

storage migrate and storage gc opened SQLite directly. They now ask the
store, so they work on whichever database the server uses once Postgres
arrives, and they adopt a legacy database the same way the server does.
The parity gate compares an adopted database row for row with one upgraded
by rusqlite's init(). Those rows are now golden files as well, generated by
the old code while it still exists, so the comparison survives its removal.
The hand-written rusqlite layer, its init() upgrade steps and its tests go.
Every caller already uses DraftStore, the old tests were ported onto the
store earlier, and the upgrade steps live on in adoption.

The parity gate no longer has old code to compare against, so it judges
adoption and the store against the golden files the old code generated:
the rows its upgrade path produced and the answers its query layer gave, at
user_version 0, 1 and 2 and for a database written by 0.5.1.
A postgres:// URL in --database-url or KERYX_DATABASE_URL selects Postgres.
Absent means SQLite at --db, so every existing deployment stays on its
current path with no new flags. --db-pool-size defaults to 1 on SQLite and
4 on Postgres. The flags live in a shared DatabaseArgs, so the offline
storage commands reach the same database as the server.

The Postgres pool pings a connection before handing it out and has connect,
acquire and idle timeouts, so a failover heals without a restart: killing
the database under a running server gives 503s from /healthz, then 200 again
once it is back, in the same process. TLS is configured in the URL through
sslmode and sslrootcert. The URL can carry a password, so the banner and
every error print it with credentials and query string removed.

The store's test factory opens in-memory SQLite, or a fresh schema in the
Postgres named by KERYX_TEST_DATABASE_URL, so one suite runs on both. The
keryx-db and keryx-server suites pass on postgres:18.6.
Two things that are safe on SQLite by construction need a lock on Postgres,
and without them the new tests fail there.

SeaORM's migrator takes no lock, and a rolling update starts a new pod while
the old one runs. Two migrators creating the schema together fail on a
duplicate pg_type key. The migrator now runs inside a transaction holding
pg_advisory_xact_lock, so a second pod waits and then finds nothing pending.

MAX(version_number) + 1 can collide between concurrent uploads to one draft.
UNIQUE (draft_id, version_number) turns that into an error rather than
corruption, but a user would still see it. record_upload now reads the
draft row FOR UPDATE on Postgres. SQLite has no row locks and needs none:
its immediate transaction already serialises writers.

Tests: sixteen concurrent uploads to one draft number their versions without
gaps on both backends, and four migrators started together all succeed with
exactly one creating the schema.
A Postgres 18.6 service container and KERYX_TEST_DATABASE_URL turn the
keryx-db and keryx-server suites into a Postgres run; the existing test job
stays the SQLite run. Custom selects are only checked at runtime, so the
whole store suite has to pass on both backends.
The flags, TLS with a private CA, required privileges, connecting past a
pooler, probes, manual disaster recovery, and the single-replica boundary.
Exemptions generated by cargo vet. An exemption records that a crate is
unreviewed; auditing happens before release.
config takes default-features = false with only toml: Keryx reads one TOML
file, so the json, yaml, ini, ron and json5 parsers, the async support and
case conversion stay out. toml_edit was already in the lockfile as a
transitive; it becomes direct so Keryx can update the config file without
losing the user's comments.
keryx-config owns the config file: where it lives, its typed schema, strict
validation, and updating it in place. It is the lowest layer beneath
environment variables and flags, which stay with clap.

The file is $XDG_CONFIG_HOME/keryx/config.toml, then ~/.config/keryx on
every platform, then the platform's own config directory; --config or
KERYX_CONFIG names another. Sections follow concerns ([client], [server],
[database], [storage] and [storage.s3]) and keys follow the flags they
stand in for. A missing file is fine. An unknown key, a wrong type or an
unknown storage kind is an error naming the file and the entry, so a typo
can never silently do nothing. A leading ~/ in a path is the home directory.

Keryx writes one key, client.api_url, through toml_edit, so the rest of a
hand-written file survives, comments included. A file it creates is 0600,
because the file may come to hold a server API key or database password.
The binary loads the config file first and hands its values to clap as
defaults, so clap resolves flag > environment variable > config.toml >
built-in default on its own. Every existing environment variable keeps its
name, and --help shows the value in effect. A value from the file also
satisfies a required flag, which is how client.share_to gives keryx share a
default --to. --help never prints server.api_key or database.url.

--config and KERYX_CONFIG name another file; the flag is read from the raw
arguments because the file has to load before clap parses. An invalid file
stops any command with the file and entry named, exit status 2.

The integration tests now cut the spawned binary off from the developer's
own config file.
The client resolves its API URL as flag > KERYX_API_URL > client.api_url in
config.toml > localhost, and keryx auth set --api-url writes that key
instead of ~/.keryx/config.json. One config file is enough.

The first run that finds the old JSON moves its apiUrl into config.toml and
deletes it. A URL already in the TOML wins. If the TOML cannot be written
the JSON stays, so nothing is lost. The API key is a secret, not config,
and stays in ~/.keryx/credentials.json.
Pluggable blob storage, OCI sharing, the SeaORM store with Postgres, and
the config file change how Keryx is run and what it depends on, which is a
minor version while Keryx is pre-1.0.
Imports the audit sets published by Mozilla, Google, the Bytecode Alliance,
Embark, ISRG, Zcash, Fermyon and Ariel OS. Their audits replace 128
exemptions: 121 crates are now fully audited and 7 partially. imports.lock
pins what was imported, so cargo vet --locked stays offline in CI.
… trust

Records cargo vet trust for 129 crates, each for one named publisher, never
as a blanket --all. The rule: only where at least two of the imported
organisations (Mozilla, ISRG, Zcash, Ariel OS, the Bytecode Alliance and
others) already trust that publisher. The publishers are dtolnay, epage,
kennykerr, BurntSushi, seanmonstar, alexcrichton, cuviper, Amanieu,
Darksonn, JohnTitor, rust-lang-owner, mbrubeck, Thomasdezeeuw, sunfishcode
and str4d. Publishers only Mozilla or only Zcash trusts are left out.

Trust is delegation, not review: it says we accept these publishers'
releases of these crates because organisations that do review them do.
Exemptions drop from 635 to 508.
Each was read in full and certified safe-to-deploy with notes on what was
checked: docker_credential, pathdiff, reqsign-file-read-tokio, sqlx-macros,
olpc-cjson, atoi and opendal-service-fs. The notes record the two
'unsafe impl Sync' in OpenDAL's fs service and why they are sound, that
docker_credential's helper error carries the helper's output, and that
sqlx-macros delegates to sqlx-macros-core, which is not covered.

The reviewer field says these were read by an AI assistant for SimCube,
because that is what happened. Exemptions drop from 508 to 501.
…nreviewed

How a crate gets past cargo vet, in order of preference: imported audits,
trust in a publisher at least two imported organisations trust, our own
audits, and exemptions. It names the large dependency families that have no
public audit and are exempted with open eyes, notes that 65 exempted crates
are locked but never compiled, and lists the mitigations that do not depend
on review.
Graceful shutdown waits for open connections, and the dashboard's SSE stream
never finishes on its own, so with a dashboard tab open Ctrl-C hung until the
tab was closed. Under systemd it only ended when TimeoutStopSec killed it,
making every restart take the full timeout.

Shutdown now ends the live-update streams, so the browser sees a clean end of
stream and reconnects when the server is back. SIGTERM triggers the same
graceful path as Ctrl-C, where before only Ctrl-C did and systemd's signal
was never handled. A second signal exits at once, so a terminal is never
stuck behind a connection that refuses to finish.

The behaviour predates this branch; the shutdown code was unchanged from
0.5.1. A test holds a stream open over a real listener and requires shutdown
to complete, and fails without the fix.
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 4c92b26d-dde0-4196-b153-04f89089cf28

📥 Commits

Reviewing files that changed from the base of the PR and between 9fc6eb1 and 2e5d094.

⛔ Files ignored due to path filters (7)
  • Cargo.lock is excluded by !**/*.lock
  • crates/keryx-db/tests/fixtures/keryx-0.5.1.db is excluded by !**/*.db
  • crates/keryx-render/assets/keryx-logo.svg is excluded by !**/*.svg
  • crates/keryx-server/assets/pwa-icon-192.png is excluded by !**/*.png
  • crates/keryx-server/assets/pwa-icon-512.png is excluded by !**/*.png
  • crates/keryx-store/tests/fixtures/data-0.5.1/manifest.tsv is excluded by !**/*.tsv
  • supply-chain/imports.lock is excluded by !**/*.lock
📒 Files selected for processing (91)
  • .github/workflows/ci.yml
  • Cargo.toml
  • README.md
  • crates/keryx-client/Cargo.toml
  • crates/keryx-client/src/gitmeta.rs
  • crates/keryx-client/src/lib.rs
  • crates/keryx-config/Cargo.toml
  • crates/keryx-config/src/lib.rs
  • crates/keryx-config/src/schema.rs
  • crates/keryx-config/src/write.rs
  • crates/keryx-config/tests/config_file.rs
  • crates/keryx-core/Cargo.toml
  • crates/keryx-core/src/ids.rs
  • crates/keryx-core/src/lib.rs
  • crates/keryx-core/src/types.rs
  • crates/keryx-db/Cargo.toml
  • crates/keryx-db/src/adopt.rs
  • crates/keryx-db/src/connect.rs
  • crates/keryx-db/src/entity/draft.rs
  • crates/keryx-db/src/entity/draft_version.rs
  • crates/keryx-db/src/entity/mod.rs
  • crates/keryx-db/src/entity/notification_delivery.rs
  • crates/keryx-db/src/entity/notification_event.rs
  • crates/keryx-db/src/entity/push_subscription.rs
  • crates/keryx-db/src/lib.rs
  • crates/keryx-db/src/migration/m0001_baseline.rs
  • crates/keryx-db/src/migration/mod.rs
  • crates/keryx-db/src/store.rs
  • crates/keryx-db/src/store_tests.rs
  • crates/keryx-db/src/types.rs
  • crates/keryx-db/tests/adopt.rs
  • crates/keryx-db/tests/common/mod.rs
  • crates/keryx-db/tests/fixtures/golden/released-0.5.1.json
  • crates/keryx-db/tests/fixtures/golden/released-0.5.1.rows.json
  • crates/keryx-db/tests/fixtures/golden/user-version-0.json
  • crates/keryx-db/tests/fixtures/golden/user-version-0.rows.json
  • crates/keryx-db/tests/fixtures/golden/user-version-1.json
  • crates/keryx-db/tests/fixtures/golden/user-version-1.rows.json
  • crates/keryx-db/tests/fixtures/golden/user-version-2.json
  • crates/keryx-db/tests/fixtures/golden/user-version-2.rows.json
  • crates/keryx-db/tests/fixtures/legacy_seed.sql
  • crates/keryx-db/tests/fixtures/legacy_seed_v2.sql
  • crates/keryx-db/tests/fixtures/legacy_to_v1.sql
  • crates/keryx-db/tests/fixtures/legacy_to_v2.sql
  • crates/keryx-db/tests/fixtures/legacy_v0_schema.sql
  • crates/keryx-db/tests/parity.rs
  • crates/keryx-policy/Cargo.toml
  • crates/keryx-policy/src/lib.rs
  • crates/keryx-render/Cargo.toml
  • crates/keryx-render/assets/NotoSans-Regular.woff2.b64
  • crates/keryx-render/assets/dashboard.css
  • crates/keryx-render/assets/dashboard.js
  • crates/keryx-render/assets/keryx-logo.json
  • crates/keryx-render/src/lib.rs
  • crates/keryx-render/src/pdf.rs
  • crates/keryx-server/Cargo.toml
  • crates/keryx-server/assets/manifest.webmanifest
  • crates/keryx-server/assets/service-worker.js
  • crates/keryx-server/src/lib.rs
  • crates/keryx-server/src/notifications.rs
  • crates/keryx-server/src/realtime.rs
  • crates/keryx-share/Cargo.toml
  • crates/keryx-share/src/lib.rs
  • crates/keryx-share/src/meta.rs
  • crates/keryx-share/src/reference.rs
  • crates/keryx-store/Cargo.toml
  • crates/keryx-store/src/backend.rs
  • crates/keryx-store/src/lib.rs
  • crates/keryx-store/src/maintenance.rs
  • crates/keryx-store/src/s3.rs
  • crates/keryx-store/tests/compat_0_5_1.rs
  • crates/keryx-store/tests/fixtures/data-0.5.1/README.md
  • crates/keryx-store/tests/fixtures/data-0.5.1/drafts/g3q1hbw3lw0c/9zuAQ57ES2EZjlq4dx2o.html
  • crates/keryx-store/tests/fixtures/data-0.5.1/drafts/g3q1hbw3lw0c/MrTRWmNpwE89zhqPe1pv.html
  • crates/keryx-store/tests/fixtures/data-0.5.1/drafts/w5s7hqmozxa6/P8bLhVEEl4NCY28fvZ4G.html
  • deny.toml
  • skills/html-communication/SKILL.md
  • skills/keryx-read/SKILL.md
  • src/cli.rs
  • src/config.rs
  • src/db.rs
  • src/main.rs
  • src/share.rs
  • src/storage.rs
  • src/tui.rs
  • supply-chain/README.md
  • supply-chain/audits.toml
  • supply-chain/config.toml
  • tests/legacy_database.rs
  • tests/provenance.rs
  • tests/share_roundtrip.rs
 _______________________________________
< Nullus Bugus Maximus. No bug too big. >
 ---------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

CI installed the newest prebuilt cargo-vet, 0.10.0 from 2024. The audits were
imported with 0.10.2, which records crates published through crates.io
Trusted Publishing as trusted-publisher entries with no user id. 0.10.0
cannot parse those, so cargo vet --locked failed on imports.lock before
vetting anything.

CI now builds 0.10.2 from crates.io, once, since rust-cache keeps
~/.cargo/bin. The supply-chain README names the version to use locally.
cargo-vet 0.10.2's lockfile pins rustix 0.37, which enables internal rustc
attributes when it detects a nightly compiler, and the pinned nightly rejects
them. Only the tool build uses stable; Keryx stays on nightly, which the
minimum-publish-age gate needs.
@prom3theu5
prom3theu5 merged commit dd8d1ac into main Sep 19, 2026
3 checks passed
@prom3theu5
prom3theu5 deleted the feat/deployable branch September 19, 2026 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant