radicle-artifact-node 0.23.1

Seeding node for radicle artifacts: iroh-blobs store and blob serving daemon
docs.rs failed to build radicle-artifact-node-0.23.1
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: radicle-artifact-node-0.23.0

Radicle Artifact

Secure artifact distribution for Radicle.

Radicle Artifact helps you securely publish large files bound to git commits and tags, without bloating your repo. It's useful for distributing any data tied to code: binaries, static sites, model weights, and scientific datasets.

The project is split into focused crates:

Crate What it is
radicle-artifact COB types/operations + the rad-artifact CLI.
radicle-artifact-node The seeding daemon: iroh endpoint, iroh-blobs store + blob serving.
radicle-artifact-core Shared substrate: wire protocol, CID helpers, endpoint identity. For library consumers.
radicle-artifact-client Control-socket client for interaction with radicle-artifact-node.

Note: COB operations with radicle-artifact (creating releases, registering artifacts, attesting, and adding locations) never touch the iroh stack; the radicle-artifact-node crate is needed to seed or fetch artifacts.

Principles

  • Signed and verifiable — every artifact is content-addressed via a CID (with a BLAKE3 hash) and bound to the exact commit it was built from. Every interaction is signed by the user's Ed25519 key.
  • Multi-party — signing, seeding, attesting, and redacting are open to independent parties by design, aggregating trust while reducing the attack surface. Artifacts can be distributed in a peer-to-peer fashion for redundancy and resilience.
  • Transport-agnostic — an artifact can have many locations and travel over HTTP, iroh, IPFS, magnet links, rasl://, or any URL scheme. The rad-artifact CLI ships with iroh-blobs for reliable peer-to-peer seeding and fetching, plus HTTP fetching.

Trust stems from the repository's delegates, its maintainers as named in the Radicle repository identity; by default you see only the releases and artifacts that delegates or you authored.

Why

Git was never built to distribute large files and binaries.

Existing solutions like Git LFS are designed around a client-server model, resolving through a single blessed endpoint; if that host moves, dies, or rate-limits you, the pointers still exist in git but the bytes are unreachable. There's no built-in fallback or multi-source resolution.

Moreover, existing solutions have no signing model of their own; the only option is signing the enclosing Git commit or tag, which carries a single signature and can't express independent, multi-party attestation. With radicle-artifact, signing, attestation, and redaction are multi-party by design, built on Radicle's key-based identity, giving you verifiable assurances about artifact provenance.

For a more elaborate comparison to Git LFS, git-annex, and Git's upcoming Large Object Promisor, see the comparison document.

Installation

Install a prebuilt binary:

curl -sSf https://files.radicle.dev/releases/radicle-artifact/install | sh

The installer also links rad-art to rad-artifact, so you can use the short name, or rad art <command> as a rad subcommand.

Or build from source via crates.io:

cargo install radicle-artifact        # the rad-artifact CLI (COB operations)
cargo install radicle-artifact-node   # the seeding daemon (optional, for iroh seeding)
ln -s rad-artifact ~/.cargo/bin/rad-art   # optional: the rad-art / rad art alias

Or depend on the cargo crate straight from the Radicle git remote:

[dependencies]
radicle-artifact = { git = "https://radicle.norman.life/z4VYyJ9KuwMNkXGQnmKuGPGKw3inv.git" }

Note: The rad-artifact CLI requires Radicle to be installed.

Workflow

  1. Tag: Create a release tag or pick a commit. Push it to your Radicle remote (git push rad); see the note below. To attach a release to a tag, the tag must be a canonical reference.
  2. Build: Build your release artifacts.
  3. Register: Register artifacts in a release with the rad-artifact register <PATH> command, which creates the release if it doesn't exist and records the artifact CID. The COB holds only signed discovery metadata, synced over the Radicle protocol, not the bytes.
  4. Seed: Start the local seeder node (rad-artifact node start) and seed the file over iroh-blobs (rad-artifact seed <PATH>), which also adds its radiroh:// location. Or upload artifacts to an HTTP server and add the location with rad-artifact location add.
  5. Download: Download artifacts to disk with rad-artifact download, or fetch them into the local store without writing a file using rad-artifact fetch. Both need the local artifact node running (rad-artifact node start), even for HTTP locations.
  6. Verify: Check a file you downloaded or rebuilt against the COB with rad-artifact verify <PATH>. It hashes the file and looks for an artifact with that CID, honoring the same delegate and redaction rules as list/show. Reads local storage only: no daemon, no network, no signer.
  7. Attest: Other delegates check out the release version, build the artifacts independently, verify the CIDs match, and attest. Note that attest records a signed claim and rehashes nothing itself, so run verify first.
  8. Redact: If an artifact is found to be compromised or fails reproducibility checks, redact it with a reason. A redaction by a delegate or by the artifact's author makes verify fail, so it withdraws a published artifact without touching wherever the bytes are hosted.

Note: A release is bound to a revision in your Radicle storage, not your working copy. Release operations resolve <REVISION> against Radicle storage, so the commit (or annotated tag) must already be there before you can register against it. Push it first with git push rad --tags. A tag name resolves only when the tag is a canonical reference.

See CONTEXT.md for a glossary of the project's terminology: Register, Seed, Add, Announce, Trust, and the drift states they produce.

How it works

A Release is a Radicle COB (Collaborative Object) identified by a Release ID linked to a Git commit and optionally an annotated tag.

Releases contain one or more Artifacts, each identified by a content identifier (CID) and a name string. Each artifact tracks the DID that first registered it (the artifact author), and only that DID can update the artifact's name. Anyone can add a location to any artifact, so many parties can make its bytes available.

Users can also attest to an artifact, recording that they independently verified the CID matches a build from the same commit. The author's own attestation is a no-op, because registering already implies it. They can also redact an artifact with a reason, signaling that it should not be used (e.g. due to a supply chain compromise or build reproducibility failure). Anyone can redact, but only a redaction by the artifact's author or a delegate withdraws it; other redactions are ignored, so no stranger can veto a release. Redaction is permanent: it supersedes any prior attestation from the same DID and prevents that DID from attesting again.

The artifact author and repository delegates can attach free-form metadata entries to an artifact, e.g. a build-environment note or an SBOM URL. Keys are strings; values are arbitrary JSON. The keyspace is shared: for each key, the last write from the author or a delegate wins. Anyone can write, but writes from other users have no effect.

Each user is identified by a DID that is currently mapped 1:1 to the Radicle NodeID, an Ed25519 public key.

This COB is build-system agnostic. It works with any toolchain or build process that produces addressable artifacts. Ideally your builds are deterministic (reproducible), which lets other delegates independently verify artifacts and record attestations. However, deterministic builds are not a requirement; you can use radicle-artifact purely for publishing and discovering release artifacts without attestation.

Data model

data-model-diagram

Release
├── id: Oid                           # release ID
├── oid: Oid                          # git commit ID the release is linked to
├── tag: Option<Oid>                  # optional annotated tag OID linked to the commit
├── creator: Did                      # user that created the release
├── timestamp: Timestamp              # creation time, from the root op
└── artifacts: Map<CID, Artifact>
    └── Artifact
        ├── author: Did               # user that registered this artifact
        ├── name: String              # human-readable description (only author can update)
        ├── locations: Map<Did, Set<Url>>
        ├── attestations: Set<Did>    # users that attested to the CID
        ├── redactions: Map<Did, String> # users that flagged the artifact, with reason
        └── metadata: Map<String, Map<DID, Write>> # latest write per user per key; reads keep author/delegate writes
  • Cid — a string newtype for any content-addressing scheme (CIDv1, sha256, etc.).
  • Locations — plain URLs (https://, radiroh://, ipfs://, magnet:, and rasl://). The CLI supports the first two natively.
  • Each user can add multiple URLs per artifact; duplicate URLs are removed automatically.

Relationship to the Radicle node

radicle-artifact is a companion to Radicle, not a replacement for it. It links the radicle crate as a library (it never shells out to the rad CLI) and borrows two things from your local Radicle installation:

  • Identity — your Ed25519 keystore. The same secret signs every COB op and derives the iroh seeder's endpoint id, so an encrypted keystore needs RAD_PASSPHRASE (or an interactive prompt). Your DID is your Radicle NodeID.
  • Storage — your Radicle profile home and git storage, where releases are read and written as COBs.

A COB write (register, attest, location add, ...) is a local git operation. By default the CLI then announces the change to the network by calling the running Radicle node over its control socket; this is the only step that needs the node up. Pass --no-announce to skip it and announce later with rad sync -a.

For other peers to actually discover an artifact, they also need to fetch the COB refs from your node, which typically happens after receiving the announcement.

Everything else works without the Radicle node running: computing CIDs, reading releases, seeding, and fetching artifacts. The artifact seeder node is a separate process (the rad-artifact-node binary, spawned by rad-artifact node start) from the Radicle node with its own control socket; it shares only your Ed25519 identity and does not talk to the Radicle node. Fetching goes through the artifact node, which resolves locations (iroh or HTTP) directly and never consults the Radicle node.

Collaboration and trust model

All actions on a release are signed by the acting user's DID. Most actions — creating a release, registering an artifact, attesting, redacting, adding a location — are open to any user. The exceptions are renaming an artifact (constrained to the artifact's original author, enforced by the COB) and writing metadata (constrained to the artifact's author or a repository delegate). The COB records every metadata write, because nodes can disagree on the delegate set. Reads then keep only the writes from the author and the delegates, the same rule that applies to redactions.

Trust is inherited from the repository's delegate set. By default, commands consider only releases and artifacts authored by a delegate or by the local user. Contributions from other users are hidden. Pass --all-authors to widen the view, or --untrusted on list and show to see only contributions from other users. Targeting a specific release with --release <id> works regardless of who created it, but its artifacts by other users stay hidden without --all-authors.

Artifact types

radicle-artifact supports two artifact types: blobs and collections, both identified with a CID.

Blobs are the common case: one binary, archive, or model file. Collections derive a hash from a collection of files, i.e. a folder, and are useful when the collection represents a single artifact, e.g. static frontend builds. See CID for more technical information.

Actions

Action Description
Create Initialize a release for a git OID (create, or implicit on first register)
RegisterArtifact Register an artifact (CID + name), or update name if author re-sends
AddLocation Add a discovery URL for an artifact
RemoveLocation Retract a previously added URL
Attest Record independent verification of a CID
Redact Flag an artifact as compromised/withdrawn
SetMetadata Attach a free-form key/value entry (author/delegate only)
RemoveMetadata Remove a metadata entry (author/delegate only)

COB type

dev.radicle.artifact

CLI usage

<REVISION> accepts a full OID, abbreviated hash, or tag name of a commit or annotated tag. It is resolved against Radicle storage, so push it first (git push rad, or git push rad --tags for an annotated tag). A tag name must be a canonical reference.

Every command accepts these global options, before or after the subcommand:

  • --repo <RID> (or -r) targets a specific repo (defaults to cwd).
  • --no-announce skips the network announcement after writes.
  • --no-input disables interactive prompts (for scripts and CI).

Commands that do not read or write a repository ignore them: --repo has no effect on cid, locate, watch, and node start/stop/status/logs, and --no-announce has no effect on read-only commands such as list and show.

--repo only needs the repository to be in local storage, so commands work from any directory:

rad-artifact list --repo rad:z4VYyJ9KuwMNkXGQnmKuGPGKw3inv

COB-facing commands

rad-artifact create [<REVISION>] [--json]                        # create a release (or reuse your own) and print its id
rad-artifact register <PATH> [--revision <REVISION>] [-n <NAME>] [--seed] # register artifact (creates release if needed; records a sizeBytes hint, skip with --no-size)
rad-artifact register <DIR> [--each|--collection]                # a folder asks which; --no-input or -n makes one collection
rad-artifact register --cid <CID> --revision <REVISION> -n <NAME>  # register a precomputed CID without local bytes
rad-artifact location add --revision <REVISION> --cid <CID> <URL>    # add discovery URL
rad-artifact location remove [--revision <REVISION> --cid <CID> <URL>] # remove discovery URL (interactive without args)
rad-artifact attest <REVISION> --cid <CID>                       # attest to an artifact
rad-artifact redact <REVISION> --cid <CID> -m <REASON>           # redact an artifact
rad-artifact delete <RELEASE_ID>                                 # remove your ref to a release (gone once nobody has one)
rad-artifact metadata set --revision <REVISION> --cid <CID> [--json] <KEY> <VALUE>  # attach metadata
rad-artifact metadata unset --revision <REVISION> --cid <CID> <KEY>                 # remove metadata
rad-artifact show <REVISION> [--pretty] [--all-authors | --untrusted] [--redacted] # show release
rad-artifact list [--pretty] [--all-authors | --untrusted] [--redacted] # list releases (default: delegate- or local-created)
rad-artifact stats [--json]                                      # count releases by creator and redaction
rad-artifact cid <PATH>                                          # compute BLAKE3 CID
rad-artifact verify <PATH> [--all-authors] [--json]              # check a local file against the registered artifacts
rad-artifact locate <CID> [--releases]                           # locate a CID across all local repos
rad-artifact fetch [<REVISION> --cid <CID>] [--url <URL>] [--seed] # fetch artifact into the store (interactive without args)
rad-artifact download [<REVISION> --cid <CID>] [-o <PATH>] [--url <URL>] [--seed] [--offline] # download artifact to disk (interactive without args)

Commands that take --revision <REVISION> or <REVISION> also accept --release <ID> to target a release by id. show and list hide artifacts that a trusted party redacted; pass --redacted to include them.

Node control

rad-artifact node start [--foreground] [--force]                 # start the seeder daemon
rad-artifact node stop                                           # graceful shutdown
rad-artifact node status [--json]                                # endpoint id, version, seeded count, disk, traffic
rad-artifact node list [--json]                                  # list CIDs the node is seeding for this repo
rad-artifact node seed <PATH> [--release <ID>] [--reference] [--no-location]  # compute CID from PATH, seed, add location
rad-artifact node unseed --cid <CID> [--release <ID>]            # stop seeding + retract your radiroh:// locations
rad-artifact node logs [--follow] [-n <LINES>]                   # tail <home>/artifacts/node.log

rad-artifact seed and rad-artifact unseed are top-level aliases for rad-artifact node seed / node unseed.

Watching

rad-artifact watch [RID...] [--budget <SIZE>] [--sweep <SECS>] [--dry-run]  # seed trusted artifacts as peers register them

Reconciling

rad-artifact reconcile [--all-repos] [--remove-orphaned <CID>] [--remove-orphaned-self]  # fix COB drift

Seeding via the local node

Seeding involves running a daemon that holds a persistent iroh-blobs store that serves the files over iroh connections. Start it once and it survives shell exits and terminal closes:

$ rad-artifact node start
🌱 Node awake and seeding — listening on /Users/you/.radicle/artifacts/control.sock
   tip: check on it anytime with `rad-artifact node status`

$ rad-artifact seed ./dist/linux-amd64.tar.gz
🌱 Seeded baf...abc (12.4 MiB, new tagged)
📡 Added a radiroh:// location in release abc1234

The daemon stores blobs under <home>/artifacts/store/ (persistent iroh-blobs FsStore), tracks what to seed via seeded/{rid}/{release}/{cid} tags, and writes a JSON log to <home>/artifacts/node.log (rotated on each start). The control socket lives at <home>/artifacts/control.sock (mode 0600); set RAD_ARTIFACT_SOCKET to override.

Log verbosity is controlled via RUST_LOG, which covers both this crate and iroh — e.g. RUST_LOG=iroh_blobs=debug rad-artifact node start. Default filter: warn,iroh=warn,iroh_blobs=warn,radicle_artifact_node=info,radicle_artifact_core=info.

The node never writes COB ops — every signed location write (add_location, remove_location) happens client-side. The daemon's identity (the iroh endpoint id) currently derives from the same Ed25519 secret as your Radicle DID, so RAD_PASSPHRASE is required on start when the keystore is encrypted (or the parent CLI will prompt).

rad-artifact reconcile compares the node's seeded set to the COB locations under your DID. It auto-adds missing radiroh://{endpoint_id} URLs for artifacts you're seeding, and flags drift in the other direction (URLs you left behind, stale endpoint ids) without auto-removing — pass --remove-orphaned <CID> or --remove-orphaned-self explicitly when you want it gone. It also reports dangling tags — CIDs the node is seeding that no release references at all (so no location can anchor to them); reclaim them with rad-artifact unseed --cid <CID>.

Watching for new artifacts

rad-artifact seed seeds one artifact you name. rad-artifact watch keeps seeding whatever your trusted peers register, so one machine can hold a copy of every artifact a repository's delegates release, and keep the bytes reachable when the original seeder goes away.

$ rad-artifact watch --budget 50G
Watching for trusted artifacts (budget 50.0 GiB)
Now seeding bafkr4ig6iu...wvva; added location to release 3f2a91c

It runs until you interrupt it, so supervise it with launchd or systemd. It needs both nodes up: the Radicle node for the COBs and the event stream, and the artifact node for the bytes.

Two things decide what it seeds:

  • Which repositories — every repository your Radicle node seeds (rad seed <RID>). Name one or more RIDs to narrow it to those. A named repository your node does not seed is an error, because its COBs would never arrive.
  • Which artifacts — the same trust rules as verify: an artifact registered by a delegate (or by you) in a release a delegate (or you) created, and redacted by nobody who counts. Everything else is ignored.

It notices new artifacts two ways. The Radicle node's event stream reports which repositories' refs moved, so an artifact registered by a peer is picked up within seconds of the COB arriving. A full sweep every --sweep seconds (15 minutes by default) covers anything that landed while either process was down.

For each artifact it fetches the bytes through your artifact node, tags them as seeded, then adds your radiroh:// location to the release with a signed COB write and announces it — the same steps as rad-artifact fetch --seed followed by a location add.

--budget <SIZE> caps the total the node seeds, e.g. 50G. Once the store reaches it, the watcher reports what it is holding back and stops fetching. Without a budget it seeds everything it trusts, which on a busy repository is a lot; --dry-run reports what it would seed and fetches nothing.

The watcher only ever adds. If a delegate redacts an artifact you already seed, drop it with rad-artifact unseed --cid <CID>.

radiroh:// location format

Seeded artifacts get a radiroh://<endpoint-id> location, where <endpoint-id> is the iroh endpoint id encoded as lowercase base32. See docs/uri-scheme.md for more detail.

Cutting a release

See RELEASE.md for the full process — drafting the changelog, cutting the crate release with cargo release, and building and uploading cross-platform binaries alongside the install script.

License

MIT OR Apache-2.0