Sharing & Federation
The primitive: a Quipu store can hand its knowledge to another store, and compose another store’s knowledge, without either one having to trust the other by default. Every step is explicit, hash-verified, and labelled with where it came from — so you never absorb someone else’s knowledge by accident.
That is the whole idea. The rest of this page is how it works, and what proves it.
Try the receiving half in your browser — Quipu releases can include a knowledge pack of this repository, and that page imports it with Quipu compiled to WebAssembly. You watch the manifest get verified, the bundled shapes get adopted, and the graph get staged and promoted, and then you query it — and then edit it and export a pack that declares this one as its parent, which is the lineage half of the same story. It is the transcript below, with you as the receiver.
Follow the contributor constellation walkthrough to explore the vision, a design decision, and the code it governs on a phone.
Embed the browser playground
Use the same released engine and repository pack in an iframe:
<iframe id="quipu-playground" title="Quipu playground"
src="https://scbrown.github.io/quipu/explore/?embed=1&theme=intigraph"
style="width:100%;height:860px;border:0"></iframe>
embed=1 opens Map first and hides navigation, the loading-ceremony hero,
and the take-away cards. Ask, Map, the node inspector, and Try a bad write
still use the real engine. Narrow screens place the map before starter questions.
After one successful embedded load, the browser can show a saved map preview
while the worker verifies a fresh copy. The cache is bounded and keyed to the
exact pack bytes, engine revision and compiled features. It contains the map’s
real query rows from a pristine imported pack. The preview is labelled; Ask and
writes stay disabled until verification finishes. A verification refusal hides
and removes it. Pack or engine changes, unavailable storage and invalid cache
entries take the normal cold-load path.
The under-five-second warm target measures when that saved map appears. Full verification and interactive query readiness take longer; the page reports that transition explicitly. Loading still verifies and imports the complete pack on every visit.
theme=intigraph selects Intigraph’s palette, Inter and JetBrains Mono, with
its light variant following the system color scheme. The normal page’s Theme
picker also offers Intigraph. System, Light and Dark remain available.
The fonts load from Google Fonts only when Intigraph is selected; system fonts
remain the fallback if that service is unavailable.
Embedded pages send {type: "quipu:resize", height: <pixels>} to their parent
when content height changes, including after tab selection. A host can resize
the iframe after checking both the sender origin and frame identity:
const frame = document.querySelector("#quipu-playground");
window.addEventListener("message", (event) => {
if (event.origin !== "https://scbrown.github.io"
|| event.source !== frame.contentWindow
|| event.data?.type !== "quipu:resize") return;
const height = event.data.height;
if (Number.isInteger(height) && height > 0 && height < 20000) {
frame.style.height = `${height}px`;
}
});
One run, end to end
This transcript is not illustrative output. It is
examples/sharing-demo/expected.txt,
included here verbatim. just sharing-demo creates two fresh stores and checks
that a new run still matches it; the required CI Build job runs the same
check.
The demo loads an explicit identifier-policy catalogue before its outward share. Its policy lives in a separate named graph, so it does not become widget data in the shared ROOT graph.
1. SHARE A scope=root facts=2 shapes=1
2. IMPORT B outcome=quarantined admitted=0 quarantined=2 blocker=off_vocabulary
3. ADOPT + REIMPORT outcome=staged admitted=2 quarantined=0 promotion_eligible=true
4. PROMOTE B outcome=promoted triples=2
5. COMPOSE B root_names=shared
6. DIVERGE A_adds=a-only B_adds=b-only
7. STATUS diverged=true ours_added=1 theirs_added=1 conflicts=0
8. RECONNECT outcome=merged asserted=1 retracted=0 provenance_parents=2
9. CONVERGED B root_names=a-only,b-only,shared
10. BOUNDARY provider federation unions labelled query results; it does not merge store histories
The receiver first quarantines the intact bundle because the type is not in
its local vocabulary. It then adopts the bundled shape deliberately, re-imports,
and promotes with a named actor. After A and B make independent additions,
status proves both histories moved and merge records two provenance parents.
The demo intentionally stops at the current boundary: provider federation
unions labelled query results but does not merge store histories. The broader
evaluation and claims belong to the
arXiv submission source for the shape-aware merge paper,
not to this ten-line walkthrough.
Every claim below names the command, symbol, or verbatim message that backs it, so you can check it rather than believe it. Citations are file plus symbol, not line numbers, because line numbers rot and a page that cites them stops being true without anyone editing it. Quoted messages are exact — grep for them.
Two things most graph databases do, Quipu deliberately does not. It does not merge on receipt, and it does not take a peer’s word for how trustworthy that peer is.
flowchart LR
subgraph A["Store A — producer"]
AR[("ROOT")]
end
subgraph B["Store B — consumer"]
BQ[["quarantine graph<br/>urn:quipu:import:quarantine:…"]]
BR[("ROOT")]
end
AR -->|"quipu share --output dir/"| S["share bundle<br/>export.nt · shapes.ttl · manifest.json"]
S -->|"quipu import dir/<br/>hashes verified"| BQ
BQ -->|"quipu import promote <id><br/>an operator's explicit act"| BR
BR -.->|"quipu status dir/"| D{{"diverged?"}}
D -.->|"quipu merge dir/<br/>conflict ⇒ exit 2"| BR
Prepare an outward share
New local shares stamp pack_dir: "pendant" in their manifest. Use
--pack-dir knowledge to keep an explicit repository layout. Older manifests
without this field still resolve to qpack; importing them does not rewrite
their identity or silently move their deltas. HTTP shares do not assert a
repository directory on a caller’s behalf.
Outward sharing checks the outgoing bytes against the store’s block-tier identifier-policy catalogue. Load your reviewed policy before the first share; loading only your data’s SHACL shapes does not supply these rules.
For a minimal example, save this shape as identifier-policy.shapes.ttl:
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix aegis: <http://aegis.gastown.local/ontology/> .
<urn:demo:IdentifierPolicyShape> a sh:NodeShape ;
sh:targetClass aegis:InternalIdentifierPattern ;
sh:nodeKind sh:IRI .
Save its rule as identifier-policy.ttl:
@prefix aegis: <http://aegis.gastown.local/ontology/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
# The demo's explicit identifier policy. Stored in its own named graph so the
# shared payload and its quarantine/admission transcript remain unchanged.
<urn:demo:private-identifiers> a aegis:InternalIdentifierPattern ;
rdfs:label "Demo private identifiers" ;
aegis:regex "private[.]example" ;
aegis:enforcementTier "block" .
This demonstration rule only detects private.example. For real publication,
replace it with a reviewed catalogue covering the identifiers your organization
needs to exclude. The checker evaluates the supplied rules; a non-empty
catalogue does not establish that your policy covers every private identifier.
Load the policy into the same database that will produce the share:
quipu shapes load identifier-policy identifier-policy.shapes.ttl --db my.db
quipu knot identifier-policy.ttl --graph urn:quipu:identifier-policy --db my.db
quipu share --output graph-share --db my.db
Also load the shapes governing your application data before ingesting it. The
catalogue graph is separate from ROOT, the default share scope; its location
does not change which application facts travel. Each rule’s type, label,
regular expression and block tier must be together in ROOT or one named graph.
Fragments split across graphs do not form a usable rule.
The CLI exits 2 if there is no block-tier catalogue, 1 if a rule matches,
and 0 after checking a clean payload. A refusal creates no partial output
directory. --no-shapes does not bypass the catalogue check. Use
--destination internal only for an intended internal transfer; it skips the
outward check and stamps the resulting artifact accordingly.
Release publication rollout
Native releases can ship before the trusted repository-share producer is enabled.
While the repository variable TRUSTED_REPOSITORY_SHARE_ENABLED is unset or not
true, the release summary explicitly records that no new repository pendant was
published. The release may therefore have no graph artifact for the explorer.
Enable that variable only after the managed trusted producer is installed and its scheduled execution has been observed. Once enabled, the release job requires the pendant, checksum and source/binary provenance receipt; missing or invalid artifacts fail the job. The first enabled release must separately prove published artifact delivery. Hosted runners never receive the private policy catalogue.
The repository pack also carries a separate, public receiver catalogue from
docs/knowledge/publication-policy.ttl, with its SHACL constraints. It contains
generic access-token, private-key and user-directory patterns plus a fictional
domain control. It does not publish the trusted producer’s private rules or
claim to cover every organization’s private identifiers. Load stricter local
rules when your destination requires them. The producer’s fresh private-policy
check remains mandatory and is not replaced by this public catalogue.
Before emitting a release pack, the builder imports it into a fresh receiver and proves outward sharing and re-import without injecting policy afterward. Acceptance pins all four public rule IRIs and their exact labels, regexes and block tiers; missing one rule or changing any of those values fails the check. Missing receiver policy and a planted forbidden value must still refuse without output, and native SHACL must refuse an incomplete policy rule. Carrying shapes does not establish that a particular browser binary has compiled SHACL; the explorer reports that capability from the engine build.
What a share is
A share is a directory you can commit to git, attach to an email, or publish
as a release asset. quipu share --output <dir> writes it deterministically
(src/cli_pack.rs, cmd_share), and it holds exactly three files — the same
three quipu import reads back (cmd_import):
| File | What it carries |
|---|---|
export.nt | the facts, as N-Triples |
shapes.ttl | the SHACL shapes those facts were validated against |
manifest.json | hashes, producer name and version, and the lineage link |
queries.ttl | only when the store has stored queries registered against the scope: the competency questions a receiver can quipu_ask, sealed by the manifest |
The manifest’s parent_share field (src/share.rs, ShareOptions) is what
makes a share a link in a chain rather than a loose dump: it names the share
this one descends from, which is what later lets quipu merge find a common
base.
Shares carry their shapes on purpose. A receiving store is never asked to guess what the sender meant by a predicate — it gets the constraints alongside the facts.
Pendants: what the artifact is called
On a khipu, pendant cords hang from a primary cord, and each pendant carries a self-contained record. Quipu names its share artifact after them:
| Khipu | Quipu |
|---|---|
| Primary cord | a store |
| Pendant cord | a pendant: a shareable graph with its shapes and stored queries — the manifest, the payload (export.nt), shapes.ttl, and stored queries where the share carries them |
| Subsidiary cord | a delta share, hanging off the pendant it amends |
| Tying a pendant onto a primary cord | composing an imported pendant into a store |
| Knots between cords | relationships — edges join cords; they are not pendants |
“Share” still names the protocol — quipu share, quipu import, share deltas.
“Pendant” names the thing that travels. Packed for release, a pendant is a
deterministic tar archive named *.pendant.tar.gz.
Renamed from “qpack”. Artifacts used to be called qpacks and carried a
.qpackextension. For one release, anything named.qpack,.qpack.tar.gzor.qpack.dbstill loads unchanged, with a deprecation warning; release pages publish the repository pendant under both asset names. New artifacts are written only as.pendant.
How repository pendants connect
A repository pendant describes that repository and points back to its source. Load Quipu, Bobbin, Yupana and Camayoc pendants into the same receiving store to query their relationships together. Each import still passes verification and quarantine; composition follows explicit promotion, not merely downloading or opening an archive. The cords below illustrate that composition, not a claim that every repository already publishes a complete pendant.
An idea, interface or symbol can carry a repository-qualified entity IRI and a file pointer. The pointer identifies where to read the definition; it does not copy the file’s text into Quipu. For example:
| Repository | Source pointer to inspect |
|---|---|
| Quipu | Import verification and resolution |
| Bobbin | The knowledge import MCP handler |
| Yupana | CLI entry point |
| Camayoc | Native WorkItem backfill adapter |
These are navigable source references, not embedded copies or four equivalent
interfaces. A released pendant’s source revision and provenance receipt identify
the version to inspect; the links above follow each repository’s main branch.
Bobbin’s knowledge_import handler calls Quipu’s /import endpoint, so a
cross-repository depends-on edge can describe that interface dependency.
It does not say that the two interfaces are the same entity.
The repository content contract requires the following alongside code and documentation pointers. This table specifies what a producer must represent; it does not certify those contents in every currently released artifact.
| Required knowledge | What to connect |
|---|---|
| High-level ideas | Repository concepts to the interfaces, symbols and source files that implement them |
| Public interfaces | CLI commands, MCP tools, REST routes, configuration keys and file formats to defining files; cross-repository dependencies to their target interfaces |
| Stored queries | Queries the repository answers, with their definitions in queries.ttl |
| Decisions | Decision status and supersession, with links to the evidence and affected source |
| Freshness | reviewAfter / maxAge information on knowledge facts, so missing or expired review remains visible |
| Start-here knowledge | A glossary linked to Quechua terms, plus source files to read first |
| Shapes | Validation of ideas, interfaces and mappings; a prose claim is not a shape |
| Cross-repository mappings | The existing shareable alignment record, identifying the two IRIs and the reviewed relationship |
Chunk nodes, where retained for navigation, carry pointer metadata and relationships. Chunking and source-text retrieval belong to Bobbin; this contract does not ask Quipu to duplicate chunk bodies or line spans. Missing producer coverage is unknown, not evidence that a relationship does not exist. Embedding model metadata and vector rebuilding are separate load capabilities; composing two graphs does not itself prove either one.
Composition is not an identity decision
- The same exact IRI names the same RDF subject in the composed data.
Repository-qualified identifiers prevent unrelated local names such as
configfrom colliding. The diagram deliberately uses a generic repository namespace rather than declaring a new permanent namespace. - Different IRIs remain different by default, even when their labels match.
The current import resolver reports exact canonical-name matches as proposals;
it preserves the foreign IRI unless the caller explicitly enables
accept_exact. That compatibility option rewrites IRIs and does not create a retractable alignment record. - A dependency is not identity. Bobbin calling Quipu is a relationship between interfaces, not permission to merge their names.
- A proposed mapping is not accepted alignment. Use the existing
propose → decide → apply workflow. Its SSSOM record carries
subject_id,object_id,predicate_idandmapping_justification; the decision records the reviewer. Applying the decided version can derive accepted identity into a separate alignment graph, leaving both source graphs recoverable. A close match, undecided row or rejected identity does not authorizeowl:sameAs.
The two differently named ideas in the diagram are an illustrative mapping candidate, not a published cross-stack alignment dataset. Share a reviewed alignment record with the pendants when one exists; do not manufacture mappings from a picture or a common label. See Aligning concepts across graphs for the expected-version guard and rejection history.
Receiving: verify, quarantine, promote
Import is two verbs, and the split is the point.
quipu import ./their-share --source https://example.org/share --actor alice
quipu import promote <share-id> --actor alice
quipu import verifies before storing anything. It recomputes the payload hash
and refuses a mismatch (src/share_import.rs, verify_share):
share graph hash mismatch: manifest=… actual=…
What survives verification lands in a quarantine graph named
urn:quipu:import:quarantine:<hash> (staging_graph) — present in the store,
queryable, and not part of ROOT. The result reports an ImportCounts split of
admitted versus quarantined triples.
quipu import promote is the separate, actor-attributed step that moves a staged
share into ROOT (cmd_import, the promote arm). Nothing reaches your ROOT
because a file arrived; it reaches ROOT because a named person ran the second
command.
Identity across stores
Two stores will call the same thing by different names. Quipu handles that in two places, and today one of them is stronger than the other.
quipu knot writes real owl:sameAs edges, so the claim “these two IRIs are one
entity” is itself a fact in the graph — visible, queryable, and retractable like
any other fact. That is the model the project is built around.
Import proposes; it does not merge. On an exact canonical-name match
(src/share_import.rs, resolve_and_rewrite: score == 1.0 && matched_on == "canonical_name:exact"), the incoming foreign IRI is kept. The match is reported
as an exact_merges entry carrying its score and matched_on, so a caller can accept
the exact hits in one action — but nothing is applied to the triples, and the foreign
identity survives the import intact.
Sub-exact matches are reported the same way, as candidates for review. The difference between exact and sub-exact is now the confidence attached to a proposal, not whether it is applied behind you.
accept_exact restores the older behaviour explicitly: exact matches are applied as IRI
rewrites at import time. It defaults to false. Use it only where you control the naming
in both stores, and note that a rewrite is still not recorded as an owl:sameAs — it
remains the one path on which identity is not queryable and not retractable.
Accepting a proposal is knot’s job, and that is the point of the split: an accepted
alignment becomes an owl:sameAs fact — visible, queryable, retractable — rather than a
silent edit to someone else’s identifiers.
What travels: facts, graphs, whole repositories
A share is the portable graph artifact. Its scope may be a slice of facts, a
whole graph, or a repository graph; a release .pendant.tar.gz is a deterministic
archive of the same text bundle, not a SQLite database. The bundle contains a
canonical RDF payload, SHACL shapes, and JSON plus PROV-O/DCAT/SPDX RDF
manifests (src/share.rs: share_payload, manifest_turtle).
Import accepts a directory, archive, or HTTP(S) release artifact. URL and archive
inputs are bounded, verified, and loaded into a fresh in-memory store without a
user-visible download (src/share_transport.rs: read_reference,
import_in_memory). Verification is unchanged because bytes arrived over the
network: graph, shapes, and envelope hashes must agree before the store is
opened.
For a modified copy, --since writes a parent-bound delta using the deliberately
restricted, interoperable part of SPARQL 1.1 Update:
quipu share --out child --since <parent-share>
quipu import delta <parent-share> child
The delta contains only ground DELETE DATA and INSERT DATA. Its manifest
names and hashes the immediate parent and the materialized result; the importer
rejects a wrong parent, a changed update, any other Update operation, or a result
digest mismatch (src/share_delta.rs: write_delta, materialize). These are
local artifact writes and reads: they do not send a delta to a remote store or
grant ROOT admission.
Pointers: graphs a share names but does not carry
A share can also name graphs it does not contain. A repository that only points at knowledge ships a share with no facts:
quipu share --output pointers --pointers pointers.toml --no-shapes
[[pointer]]
graph = "https://example.org/homelab"
kind = "endpoint" # a live graph, reached by federation
endpoint = "https://quipu.example.org/sparql"
[[pointer]]
graph = "https://example.org/reference"
kind = "pack" # a pinned pack, fetched on first use
url = "https://example.org/reference.pendant.tar.gz"
sha256 = "<64 lowercase hex digits of the archive>"
Pointers are part of the share’s identity, like the internal destination
marker. Removing or editing one changes the hash, so verify_share refuses the
share, and the verifier applies the producer’s own rules, so a hand-edited
manifest cannot carry what quipu share would refuse:
- an IRI graph;
- an
http(s)URL with no credentials; - a 64-hex
sha256; - one pointer per graph.
A share without pointers is byte-identical to one produced before the field existed. An older quipu refuses a share that has pointers, because it recomputes a different identity; it never silently drops them.
quipu compose reports every input’s pointers, each with the share that
declared it, and never follows them. Loading a pointer is the consumer’s trust
decision (caboodle packs trust). Following kind = "pack" pointers on first
use, and joining kind = "endpoint" pointers to the federation list, are
separate steps.
The older SQLite pack commands remain an internal/archive compatibility surface:
quipu pack <graph-iri> --out <file> # export a graph as an attachable pack
quipu pack --verify <file> # check one before trusting it
quipu unpack <file> [--into <graph-iri>]
(src/cli_pack.rs: cmd_pack, cmd_unpack.) --verify exists so that “is this
pack intact?” is answerable before you load it rather than after. SQLite packs
are no longer the published repository interchange artifact.
For archives, quipu graph freeze|thaw|list is the deep freeze surface
(src/cli_graph.rs), producing read-only full-history graphs. See
Graph Kinds & Deep Freeze and
Knowledge Packs.
Querying across stores
A FederatedProvider composes members behind one query (src/provider/mod.rs),
and two properties are what make its answers honest.
Trust is declared locally. A member’s DeclaredLabel carries only trust
and freshness — in the source’s words, “the axes an operator can honestly
declare about a peer” — and it is declared by the local operator, never read
from the member itself (src/provider/label.rs). A remote that declares
nothing does not quietly pass a configured floor: an undeclared value fails a
configured trust or freshness floor, which the source describes as “fail-safe at
enforcement, honest at reporting”. And because durability, policy and kind cannot
be honestly declared about a remote, a remote member degrades a dataset’s
coverage on those axes to partial — “the conservative reading, not an
omission”.
A merge is never silent. FederatedQuery returns one ProviderOutcome per
member (src/provider/mod.rs), so a partial answer is reported as a partial
answer instead of arriving as a short one. And when a share has diverged,
quipu merge performs a genuine three-way reconnect against the common base
located through parent_share (src/share_merge.rs, locate_base — which
refuses outright with “incoming share has no parent_share; three-way merge has no
base”). Conflicts are detected against the SHACL cardinalities (max_counts); on
conflict Quipu keeps the base value, emits a DecisionRecord, and the CLI
exits 2 (src/cli_pack.rs, cmd_merge). It would rather stop and tell you
than pick a winner.
Federation and SPARQL SERVICE
Quipu supports SPARQL SERVICE, restricted to endpoints the operator has
configured. Queries are parsed by spargebra (src/sparql/mod.rs) and the
SERVICE pattern is evaluated in src/sparql/pattern.rs under the remote
feature — which server implies and the shipped full build includes
(Cargo.toml).
It is narrower than SPARQL 1.1’s open federation, by design:
- Variable endpoints are refused — “variable SERVICE endpoints are refused; use a configured endpoint IRI”. A query cannot compute its target at runtime.
- Unconfigured hosts are unreachable — with no configured remotes an endpoint
fails with “SERVICE endpoint ‘…’ is unavailable: no configured remotes”, or
returns the seed row under
SILENT. - Every returned row is labelled —
_provideralways, plus_trustand_freshnesswhere you declared them. A federated answer cannot arrive anonymous.
The pinned W3C federated-query ledger scores all seven approved SERVICE
cases. Quipu implements SERVICE as a query-planned remote subquery path using
the same operator-configured declarations and labels as RemoteProvider; it is
not the GraphProvider whole-query fanout path, and it is not open federation.
The variable-endpoint case is a deliberate policy
deviation because query data cannot widen the operator’s remote allowlist.
See SPARQL 1.1 conformance for the measured
score and named verdicts.
Endpoint pointers and Caboodle consent
A pendant can declare a kind = "endpoint" pointer without carrying that
remote graph. Register the local declaring pendant and its repository remote
in the operator’s federation configuration:
[[quipu.federation.pendants]]
path = "/path/to/repo/knowledge/declaration.pendant"
repo_remote = "https://github.com/example/project.git"
On each SERVICE <exact-endpoint-url> use, Quipu verifies the pendant’s share
identity and invokes caboodle packs trust check-endpoint <repo-remote> <url>.
That command requires Tambo/Caboodle with endpoint decision support. Missing,
refused, invalid or timed-out policy checks fail before network access; the error
names the endpoint and pointing repository. Consent is rechecked after policy
changes, including revocation. A foreign archive allow alone does not admit its
endpoint. Use Caboodle’s packs trust allow-endpoint or owner policy to grant
consent; Quipu keeps no second trust policy or persistent admission cache.
An admitted pointer contributes a credential-free, unlabelled endpoint only to
the current SPARQL SERVICE plan. Its _provider is the declared graph IRI.
It does not import triples or shapes, promote to ROOT, forward local credentials,
follow redirects, infer trust labels, or enable REST whole-query fanout. The
URL must match exactly, including its trailing slash: consent does not extend
to an appended /query URL. Existing explicit federation remotes retain their
operator-defined credentials and labels.
The server and MCP use their federation configuration. To opt into strict project/user configuration from the native CLI, use:
quipu query 'SELECT ?s WHERE { SERVICE <https://example.org/sparql> { ?s ?p ?o } }' --federation
Without the remote build feature, SERVICE remains unsupported. Local
filesystem declarations and the native Caboodle policy process are not a browser
API; browser pack resolution is a separate host interface.
The whole stack speaks it
A primitive is only first-class if the tools around it use it, rather than reaching past it. The same share format and the same verify → quarantine → promote discipline appear at every layer:
| Layer | What speaks the format |
|---|---|
| CLI | quipu share / import / import promote / status / merge |
| MCP | quipu_export, quipu_import, quipu_import_promote |
| Bobbin | bobbin:src/knowledge/share_contract.rs plus its quipu-share-v1 fixtures |
The MCP layer is worth a second look, because the discipline is in the tool
contract, not merely in the documentation. quipu_import’s own description reads
“Stage a v1 share bundle into a per-source named graph. Never promotes.”, and
promotion is a separate tool. An agent working through MCP therefore gets the same
two-step admission an operator gets at the CLI — without having to know to ask for
it, and without a way to skip it by accident.
Bobbin carries the canonical fixture set — export.nt, manifest.json, shapes.ttl
and the import request/response pair — so “does this bundle conform?” is a test in
another repository rather than a claim made in this one.
Built vs designed
Everything described above is built and reachable from the CLI today. The gaps are stated here rather than left to be inferred from careful wording.
| Capability | Status |
|---|---|
share / URL import / import promote / status / merge | ✅ Built (src/cli_pack.rs, src/share_transport.rs) |
| Parent-bound SPARQL Update delta write/materialize | ✅ Built (src/share_delta.rs) |
SQLite pack / unpack / pack --verify, deep freeze | ✅ Internal compatibility/archive surface (src/cli_pack.rs, src/cli_graph.rs) |
knot — owl:sameAs across stores | ✅ Built |
| Provider model, declared trust/freshness, per-member outcomes | ✅ Built (src/provider/) |
SPARQL SERVICE to operator-configured endpoints | ✅ Built (feature remote) |
MCP share tools — quipu_export / quipu_import / quipu_import_promote | ✅ Built and provisioned |
Bobbin share contract + quipu-share-v1 fixtures | ✅ Built (bobbin:src/knowledge/share_contract.rs) |
| Bobbin runtime adapter — producing and consuming bundles live | 🔶 Designed, not built. The contract and fixtures exist; the adapter does not yet. |
| External share attestation — proving who produced a share | ✅ Built, in three tiers — see below. Hash verification proves a share is intact; an attestation proves who signed it, and only a producer registered out of band reaches attested. |
| Re-runnable two-store transcript | ✅ Built (examples/sharing-demo/run.sh), checked in CI, and embedded above from expected.txt |
Producing the repository release share
The repository release producer runs on a trusted host that can reach the live
identifier-policy authority. Configure QUIPU_POLICY_SERVER there; use
QUIPU_POLICY_TOKEN_FILE when authentication is required. Keep both the authority
configuration and catalogue on that host. A missing, unreachable, empty,
truncated, or malformed catalogue refuses production. There is no fixture or
internal-destination fallback for a public release.
just contributor pack <output> uses this same boundary. The producer projects
policy into a temporary copy of the index, scrubs the outward share, and proves
its import into a fresh receiver before making the output directory available.
The source index retains no policy projection. Failure leaves no share output.
For a release, check out the exact release tag in a clean source clone and use its checksum-verified native binary. On the trusted host:
bash scripts/publish-repository-share.sh --prepare "$TAG" \
"$QUIPU_BIN" "$BOBBIN_BIN" "$SOURCE_REPO" "$NEW_OUTPUT"
--prepare creates the text pendant archive, checksum and provenance receipt
locally. --publish additionally uploads those three explicit files to the
existing release. It refuses to replace existing assets. The receipt binds the
archive to the release tag, source revision and native binary hash; it includes
no policy content or authority address.
The hosted release job uploads native binaries first. A separately managed
trusted-host trigger must then run the producer. The hosted job waits for its
three assets and verifies the receipt against its own source and binary. It
fails if the producer cannot deliver within the deadline. Provision and prove
that trigger before enabling this workflow; a local --prepare pass alone does
not establish release automation.
These receipt checks establish byte and build correspondence under the release
uploader’s authority. They are not a cryptographic producer attestation and do
not upgrade an import from transport to attested; those tiers are described
below. Neither the catalogue nor the producer’s private logs are release assets.
What an import proves about its producer
Hash verification proves a share is intact. It says nothing about who made it. Those are different claims, and since aegis-tadzdf every import reports which one it reached.
| tier | what it means | what it does NOT mean |
|---|---|---|
transport | No attestation supplied. The payload hashes verify, so the bytes are intact. | Anything about authorship. |
claimed | A signature verifies against the key the share itself carried. The bundle is unaltered since signing and its identity fields are bound together. | That the key belongs to whoever it names — nobody here vouched for it. Replay is not defended at this tier. |
attested | The signature verifies against a session binding registered out of band on this store. | — |
A first import from an unknown producer reports claimed, and that is correct rather than a
failure. Reaching attested requires someone to decide that this key is that producer, using
quipu attest register with a key obtained some other way.
Importing a share never registers its producer. That is the design, not an omission: a key that vouches for the bundle it arrived in vouches for nothing, and an attacker replacing the whole bundle would replace the key along with it. The governance plane states the same rule for itself — quipu never self-registers.
Automated callers should require attested. Accepting claimed is defensible, but it should be a
caller’s deliberate choice rather than the effect of a tier that merely does not read as failure.
Command reference: CLI — sharing.
Import and attestation metrics
The server’s /metrics endpoint exposes process-local counters at the import and
signature-verification decisions:
| Counter | Labels | Meaning |
|---|---|---|
quipu_share_import_total | outcome, tier | One completed or failed library import attempt. |
quipu_attestation_verify_total | binding, result | One verification decision for the write or share signed domain. |
Import outcomes are staged, quarantined, unchanged, and error. Tiers are
transport, claimed, attested, and unverified. A failure before trust has
been established uses unverified, even if the request supplied an envelope.
A failure after verification retains the verified tier. Malformed HTTP JSON and
HTTP authentication refusals do not enter the library import path; see the HTTP
request counters for those failures. An import is staging, not ROOT promotion.
Verification results are ok, badsig, replay, revoked, unbound, skew,
invalid, and error. invalid covers malformed envelopes and binding/domain
mismatches; error covers inability to consult protected registry/replay state.
skew includes expired or not-yet-valid registered sessions. No unsigned import
increments verification success. A claimed signature can verify successfully
without proving the producer’s identity: use the import tier for that claim.
The counters also cover library/CLI callers within their own processes; only the
server’s own observations appear in its scrape.
Labels never contain keys, signatures, nonces, session identities, or error text.
Series appear after observation; family declarations exist before the first
attempt. Counters reset with the process, so compare increments within one
process_start_time_seconds interval or use reset-aware Prometheus functions.
A zero or absent verification-success series is not evidence that an import has
proved authorship; verify a real operation and its corresponding counter delta.
Quarantine review events
Verified share imports append receiver-local lifecycle events to the existing
GET /events cursor feed: import.quarantined, import.staged,
import.adopted, import.promoted, import.rejected, import.expired, and
import.reopened. State and its event commit together; identical retries do not
announce a transition again. Import integrity failures produce no import event.
import.adopted means an earlier quarantine became eligible on re-import after
local validation; it does not install any bundled shape. Promotion stays explicit.
Events contain the share ID, source, caller and separately claimed actor, unknown types, blocking reasons, shapes hash, triple counts, byte size and waiting age. Old imports without review metadata gain that metadata when re-imported; this is observation time, not a reconstructed historical arrival. Composition inspection datasets are separate from the share-import review lifecycle.
The pending registry survives pruning of the event delivery log. Review pages
are bounded, ordered by share ID and return has_more and next_share_id.
Pass the cursor with --after to inspect the next page. These local commands
operate on the receiver selected by --db:
quipu import review pending --limit 100 --db receiver.db
quipu import review notify --age-seconds 86400 --route reviewer --db receiver.db
quipu import review rejected <share-id> --actor alice --reason 'not trusted' --db receiver.db
quipu import review expired <share-id> --actor alice --reason 'review policy expired' --db receiver.db
quipu import review reopen <share-id> --actor alice --reason 'reconsider authority' --db receiver.db
notify emits import.review_due after the explicit threshold, once per share
for the current threshold/route policy. Changing that policy deliberately permits
another notice. Consumers poll the existing feed and commit their offsets using
POST /events/commit; they decide whether and how to send a nudge. No timer,
webhook, external message, or automatic shape adoption is installed. A scheduled
consumer must page through pending reviews, specify its policy and verify its
own observed run and delivery. This library/CLI provides the event producer.
Rejection and expiry require an actor and reason and retain quarantine contents. They prevent re-import and promotion until an explicit reopening; eligible or already promoted packs cannot be closed through the quarantine-review command. Receiver-local review metadata stays in the receiver SQLite database across restart and event retention. It is excluded from portable packs and fact shares: a new receiver records its own decisions. Event offsets follow the existing retention/replay contract.