CLI: sharing, import and legacy packs
Reference for the commands behind Sharing & Federation.
Every flag here is checked against quipu --help by tests/cli_doc_drift.rs, so
this page cannot quietly fall behind the binary.
A note on vocabulary: the share is the portable artifact. quipu share
writes its standard text files directly; releases may carry the same files in a
deterministic .qpack.tar.gz archive. The older pack and unpack commands
remain for local SQLite compatibility, but a .qpack.db is not the published
interchange format.
quipu share — produce a share
Prerequisite: load the identifier-policy catalogue
and the shapes governing your data. The default destination is outward.
An empty block-tier catalogue exits 2 (cannot verify); a matching identifier
exits 1; a checked, clean share exits 0. --no-shapes does not bypass this check.
quipu share --output <dir> [--graph IRI|--group-id ID|--construct QUERY]
[--shapes NAME]... [--no-shapes] [--parent-share ID]
[--since <parent-reference>] [--turtle] [--destination internal]
Writes a deterministic, git-native share into <dir>: RDFC-1.0 canonical
export.nt (the facts), shapes.ttl (the constraints they were validated
against), and JSON plus PROV-O/DCAT/SPDX Turtle manifests.
| Flag | Effect |
|---|---|
--output <dir> | where to write. Required. |
--graph <IRI> | share one named graph |
--group-id <ID> | share by group |
--construct <QUERY> | share exactly what a CONSTRUCT query yields |
--shapes <NAME> | include a named shape set; repeatable |
--no-shapes | omit shapes.ttl — the receiver then has no constraints to validate against, so prefer not to |
--parent-share <ID> | record lineage: the share this one descends from |
--since <reference> | emit a parent-bound SPARQL Update delta instead of a full share; the parent may be a directory, archive or URL, not a share_id |
--turtle | additionally write a Turtle view for humans |
--destination internal | skip the outward scrub and stamp the manifest destination: internal. LAN-internal destinations only — see below |
The outward scrub, and --destination internal
Every share is checked against the store’s own aegis:InternalIdentifierPattern
catalogue — the rules tiered block — and refused if the payload matches one.
Nothing is rewritten: an internal hostname or an RFC1918 address is entity
identity, and silently editing it would produce a share that says something the
store never said.
That is the right default for a share bound for a public remote, and the wrong
one for a share bound for an internal forge, where those identifiers are the
point. --destination internal is the single explicit way to say so:
quipu share --output qpack/today --destination internal
It does three things, and the third is what makes the first two safe:
- Skips the outward scrub entirely. Not a per-pattern exception, not an allowlist — the check does not run.
- Stamps
destination: "internal"intomanifest.json, andquipu:destination "internal"intomanifest.ttl. The exemption travels with the bytes instead of living in the shell history of whoever produced them. - Binds that stamp into
share_id. Unlikeattestation, the field is not stripped before the manifest is hashed. Deleting it to launder the payload onward leaves a manifest that no longer hashes to the id it carries, and every consumer’s verification refuses it.
There is no environment variable and no config setting. A share that says
nothing is scrubbed, including every share produced over HTTP: POST /share
cannot select a destination, because a caller who could would be turning the
guard off on a server they do not own.
What the marker buys you downstream. quipu import of a stamped share runs
the scrub the producer skipped. If the payload passes it imports normally — a
share marked internal out of caution is not quarantined for it. If the payload
fails, the import is refused unless the operator repeats the declaration with
quipu import <dir> --destination internal. So internal facts cannot enter a
store silently and then leave it in someone else’s outward share.
Deltas are scrubbed separately, and they have to be. delta.ru is not built
from the store: its DELETE clause is lifted verbatim from the parent’s
export.nt. An identifier retracted from the graph yesterday is still quoted in
today’s delta — and the parent is usually the very internal share that was
allowed to carry it. A full outward share of the same store passes cleanly while
that delta does not, which is exactly why the delta document gets its own check
rather than riding on the result share’s.
--since compares the current share with the referenced parent. Its default
8 MiB limit applies to the serialized delta file map (update, shapes and
manifests), not to the full result graph. A large unchanged graph can therefore
produce a small delta; a large insertion or deletion can still exceed the limit.
The producer still materializes the full result internally, so this transport
limit is not a memory bound. Use --parent-share to record an identity without
computing a delta.
--parent-share is what makes quipu merge possible later. A share without a
parent cannot be three-way merged — merge refuses with “incoming share has no
parent_share; three-way merge has no base” — so record it at production time,
when you know it, rather than trying to reconstruct it at reconnect time.
quipu import — receive a share, into quarantine
quipu import <share-dir|archive|URL> [--source <uri>] [--actor <id>]
[--destination internal] [--db <path>]
quipu import delta <parent-share> <delta-share> [--actor <id>]
Verifies the manifest and payload hashes, then stages a local directory in its
selected store. Archives and URLs are fetched under fixed size limits and
materialized in a fresh in-memory store by default, so no downloaded artifact
or database is left behind. With an explicit --db <path>, archives and URLs
stage in that database using its loaded shapes and registered identities, just
like a directory. Carried shapes are not automatically adopted: a receiver
without a matching local vocabulary still quarantines the typed data. Import never touches ROOT without promotion. A hash mismatch
is refused outright:
share graph hash mismatch: manifest=… actual=…
| Flag | Effect |
|---|---|
--source <uri> | record where the share came from; defaults to the directory, archive path, or URL |
--actor <id> | attribute the import |
--db <path> | stage in this store, including archive and URL imports |
import delta verifies the full parent and the delta’s lineage, hashes and
restricted DELETE DATA / INSERT DATA operations, materializes the declared
result, then sends that result through the same verified in-memory import path.
quipu import promote — admit a staged share into ROOT
quipu import promote <share-id> [--actor <id>] [--db <path>]
The second, separate verb. Nothing reaches ROOT because a file arrived; it reaches ROOT because someone ran this. Keeping admission in its own command is the point rather than an inconvenience — see the primitive.
quipu status — has this share diverged?
quipu status <share-dir> [--db <path>]
Reports divergence between the local store and the share’s parent, as JSON. Read
it before merge to see what a reconnect would have to decide.
quipu merge — three-way reconnect
quipu merge <share-dir> [--actor <id>] [--db <path>]
Locates the common base through parent_share, merges shape-aware (SHACL
cardinalities decide what is a conflict), and on conflict keeps the base value
and records a decision rather than guessing.
| Exit | Meaning |
|---|---|
0 | merged |
2 | conflicts — nothing was guessed; the decision records name what needs a human |
1 | error |
Exit 2 is a distinct code precisely so a script can tell “needs a decision”
from “went wrong”.
quipu pack / quipu unpack — legacy SQLite compatibility
quipu pack <graph-iri> --out <file.qpack.db> [--name N] [--version V] [--space N]
[--shapes S]... [--queries Q]... [--with-vectors] [--format turtle]
quipu pack --verify <file.qpack.db>
quipu unpack <file.qpack.db> [--into <graph-iri>] [--db <path>]
| Flag | Effect |
|---|---|
--out <file> | destination |
--verify <file> | check an existing pack instead of writing one |
--name / --version | identify the pack in its manifest |
--space <N> | term space to write into |
--shapes <S> / --queries <Q> | carry shape sets and named queries alongside the facts; repeatable |
--with-vectors | include embeddings |
--format turtle | carry the payload as Turtle |
--into <graph-iri> | unpack into a named graph |
--verify answers whether a legacy SQLite pack is intact before loading it.
New repository and release workflows use share and import; they do not
publish .qpack.db files.
quipu pack --full / quipu restore — whole-store packs
A --full pack is a different artifact from everything above: not a graph, but
the whole store, carried losslessly for internal backup. share carries
current facts; --full carries facts as whole rows — g, tx, valid_from, valid_to, op, retracted_tx — so history, including what was retracted, travels
with it.
quipu pack --full [--format text] --destination internal --out <path> [--db <path>]
quipu restore <file.qpack | text-pack-dir> [--force] [--db <path>]
| Flag | Effect |
|---|---|
--full | pack the whole store losslessly, rather than one graph |
--format text | render that whole-store pack as a git-friendly DIRECTORY of text instead of a SQLite file |
--force | allow restore to replace a destination that still holds live facts |
Both forms refuse an outward destination, and that refusal is atomic — no output is left behind. A full pack carries the event log and every operational table that is not explicitly excluded, so publishing one is the operator’s decision rather than something this command may acquire by convenience.
--format text does not transport the declared regenerated set. Today that
is vectors: embeddings are derived data, roughly 2.2 GB of floats at homelab
scale, and quote() renders a BLOB as X'<hex>' — so inlining them would
produce a 4–5 GB “git-friendly” artifact, which is not one. The manifest instead
records what was left out, how many rows it was, and the recipe to rebuild it
(embedding model name, its SHA-256, and the dimension), and restore prints a
REGENERATE: line naming them. A restore from a text pack is therefore complete
in facts, history and provenance, and not complete in derived data until
those are rebuilt — which is why it says so rather than reporting plain success.
The binary --full pack still transports vectors: a backup that forces a
re-embed on restore is a poor backup. The two whole-store packs therefore carry
different content by design, and their content hashes are not comparable to
each other.
--format text writes manifest.json, schema.sql, and data/<table>.sql.
Rows are emitted one INSERT per line, ordered by the row text itself, so a row
moving on disk produces no diff and a committed pack changes only when its
contents do. restore rebuilds the store, checks referential integrity, and
refuses unless the reconstruction hashes identically to what the manifest
claims — nothing reaches the destination until that holds, so a dump missing a
file, a table or a single row is rejected rather than installed as a quietly
smaller store.
restore REPLACES; to merge a published pack into an existing store use
unpack. Each verb refuses the other’s format by name rather than reporting an
intact artifact as corrupt.
quipu knot — assert facts, including identity across stores
quipu knot <file.ttl> [--graph <iri>] [--shapes <shapes.ttl>]
[--timestamp <ISO-8601>] [--db <path>]
Asserts Turtle into the store, validated against shapes. In the sharing context
this is how owl:sameAs between two stores’ IRIs gets written — identity is a
fact in the graph, visible and retractable, not a string-matching heuristic.
quipu load is an alias for knot.
Archives
quipu graph freeze <iri> [--out <dir>] [--actor <who>] [--db <path>]
quipu graph thaw <iri> [--actor <who>] [--db <path>]
quipu graph list [--kind <token>] [--frozen] [--db <path>]
Deep freeze produces read-only, full-history graphs. See Graph Kinds & Deep Freeze.
Producer attestation and the three trust tiers
A share can carry a signed statement of who produced it. Every import reports the tier it reached, and the three are genuinely different claims — not degrees of the same one.
| tier | what it means |
|---|---|
transport | No envelope. The payload hashes verify, so the bytes are intact, but nothing says who produced them. |
claimed | A signature verifies against the key the share itself supplied. The bundle is unaltered since signing and its identity fields are bound together — but nobody here vouched for that key. Integrity without provenance. Replay is not defended at this tier. |
attested | The signature verifies against a session binding registered out of band on the importing store. |
Minting a share with an attestation
quipu share --output <dir> ... --attest \
--attest-agent <agent> --attest-session <session> --attest-introducer <who> \
--attest-issued-at <epoch> --attest-nonce <32 hex chars> \
[--attest-key <path>] [--attest-ttl <secs>]
--attest-issued-at is required rather than defaulted to the wall clock: two runs
over one pinned dataset must produce the same signed bytes, or the share is not
re-derivable. --attest-nonce must be 32 lowercase hex characters and is checked at
mint time — a share minted with any other nonce is refused by every importer.
The key comes from --attest-key, else $QUIPU_SIGNING_KEY, else
.quipu/verifier.pk8, created 0600 on first use. That is v1 host-file custody, the
same the governance plane uses; it is not an HSM.
Registering a producer, out of band
quipu attest register --agent <a> --session <s> --public-key <hex> \
--introducer <who> --issued-at <epoch> --expires-at <epoch> [--db <path>]
quipu attest list [--db <path>]
Importing a share never registers its producer. This is the point, not an omission: a key that vouches for the bundle it arrived in vouches for nothing, and an attacker substituting the whole bundle would substitute the key with it. Registration is a separate act by the consumer, using a key obtained some other way — the same rule the governance plane states as quipu never self-registers.
So a first import from an unknown producer reports claimed, and reports it honestly.
Reaching attested requires someone to decide that this key is that producer.
Automated callers should require attested. Accepting claimed is reasonable, but
it should be a deliberate choice by a caller who says so, not the effect of a tier that
merely does not read as failure.
Keeping this page honest
tests/cli_doc_drift.rs reconciles three surfaces: the dispatch arms in
src/main.rs, the --help text, and this page. Checking any two is not enough —
when that test was written, --help documented share, status, merge and
unpack but not import, so a page-versus-help check would have passed while
the verb that receives a share stayed undiscoverable.