Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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&amp;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 &lt;id&gt;<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):

FileWhat it carries
export.ntthe facts, as N-Triples
shapes.ttlthe SHACL shapes those facts were validated against
manifest.jsonhashes, producer name and version, and the lineage link
queries.ttlonly 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:

KhipuQuipu
Primary corda store
Pendant corda 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 corda delta share, hanging off the pendant it amends
Tying a pendant onto a primary cordcomposing an imported pendant into a store
Knots between cordsrelationships — 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 .qpack extension. For one release, anything named .qpack, .qpack.tar.gz or .qpack.db still 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.

Repository pendants compose; dependencies and alignment remain distinct Four repository cords join one receiving store after verification and explicit promotion. Inside each are ideas, interfaces and symbols pointing to repository files. A separate example shows Bobbin knowledge_import depending on Quipu /import. A dashed illustrative mapping between differently named ideas requires a reviewed alignment record and is not an accepted identity. One receiving store · explicit composition Verify each pendant → quarantine → explicitly promote QuipuBobbin YupanaCamayoc IdeasIdeas IdeasIdeas Interfaces · symbolsInterfaces · symbols Interfaces · symbolsInterfaces · symbols Repository filesRepository files Repository filesRepository files Pointers, not copied text Pointers, not copied text Pointers, not copied text Pointers, not copied text Repository-qualified entity IRIs · generic namespace shown Dependency · source-backed example Bobbinknowledge_import Quipu/import Mapping · illustrative candidate only Idea “A”Idea “B” depends on An edge between interfaces, not an identity merge Shareable alignment record: propose → decide → apply No accepted cross-stack identity is asserted here

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:

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 knowledgeWhat to connect
High-level ideasRepository concepts to the interfaces, symbols and source files that implement them
Public interfacesCLI commands, MCP tools, REST routes, configuration keys and file formats to defining files; cross-repository dependencies to their target interfaces
Stored queriesQueries the repository answers, with their definitions in queries.ttl
DecisionsDecision status and supersession, with links to the evidence and affected source
FreshnessreviewAfter / maxAge information on knowledge facts, so missing or expired review remains visible
Start-here knowledgeA glossary linked to Quechua terms, plus source files to read first
ShapesValidation of ideas, interfaces and mappings; a prose claim is not a shape
Cross-repository mappingsThe 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 config from 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_id and mapping_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 authorize owl: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 — _provider always, plus _trust and _freshness where 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.

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:

LayerWhat speaks the format
CLIquipu share / import / import promote / status / merge
MCPquipu_export, quipu_import, quipu_import_promote
Bobbinbobbin: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.

CapabilityStatus
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.

tierwhat it meanswhat it does NOT mean
transportNo attestation supplied. The payload hashes verify, so the bytes are intact.Anything about authorship.
claimedA 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.
attestedThe 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:

CounterLabelsMeaning
quipu_share_import_totaloutcome, tierOne completed or failed library import attempt.
quipu_attestation_verify_totalbinding, resultOne 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.