Federation
Federation and Git-native shares are complementary boundaries. The
[[quipu.federation.remotes]] configuration drives live read fan-out through
federated_from_config; it does not silently publish local facts or bypass the
share scrub gate. Durable exchange uses a canonical share followed by explicit
import, quarantine, and promotion. This keeps remote availability and local
publication policy independent: adding a read peer cannot turn it into an
outbound replication target.
Implementation status (2026-08-25): ✅ Built. In
src/provider/(with tests): theGraphProvidertrait,ProviderStatus,LocalProvider,FederatedProviderwith outcome-reportingquery_all, and — behind theremotefeature —RemoteProviderplusfederated_from_config()(re-exported fromlib.rs).quipu-serverhealth-checks every configured remote at startup, andPOST /querywith"federated": truefans the query out through the federated provider per request (quipu-tkh). Since quipu-fd1, remotes carry an operator-declared trust/freshness label (src/provider/label.rs) and configured[quipu.labels]floors refuse a federated result exactly as a local one. Seedocs/design/federation-remote-provider.md.
Quipu defines federated queries across multiple graph providers through
the GraphProvider trait, so that a host embedding quipu can query a local store
and its own remote providers in a single operation.
The GraphProvider Trait
#![allow(unused)]
fn main() {
pub trait GraphProvider {
fn name(&self) -> &str;
fn query(&self, sparql: &str) -> Result<QueryResult>;
fn entities(&self, type_filter: Option<&str>, limit: usize) -> Result<JsonValue>;
fn health(&self) -> ProviderStatus;
}
}
Any data source that implements this trait can participate in federated queries.
Built-in Providers
LocalProvider
Wraps a local Quipu Store:
#![allow(unused)]
fn main() {
use quipu::provider::LocalProvider;
let provider = LocalProvider::new(&store, "local");
let result = provider.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 5").unwrap();
}
FederatedProvider
Aggregates multiple providers and merges their results:
#![allow(unused)]
fn main() {
use quipu::provider::{FederatedProvider, LocalProvider};
let mut federation = FederatedProvider::new();
federation.add(Box::new(LocalProvider::new(&store, "local")));
// Add remote providers as they become available
// Query all providers; the outcome reports who answered.
let fq = federation.query_all("SELECT ?s ?p ?o WHERE { ?s ?p ?o }");
assert!(fq.complete, "every member contributed: {:?}", fq.providers);
// Health check all
let statuses = federation.health_all();
for s in &statuses {
println!("{}: healthy={}, facts={:?}", s.name, s.healthy, s.fact_count);
}
}
query_all never aborts because one member is down — a dead peer must not
deny the whole result — but it never hides it either: the returned
FederatedQuery carries the merged rows plus a ProviderOutcome per member
(row count, or the failure reason), and complete is the one-field answer to
“can I trust this result set as exhaustive?”. A member that errors, answers a
non-SELECT shape, or disagrees on the variable list is a reported failure,
never a silent merge.
RemoteProvider
Behind the remote feature (a default of the shipped binaries): another
quipu-server, reached over its REST API — POST /query, POST /cord, and
GET /stats as the health probe.
Configuration
[[quipu.federation.remotes]]
name = "prod"
url = "http://quipu.example:3030"
auth_token = "…" # optional; sent as `Authorization: Bearer …`
timeout_ms = 5000 # optional; default 5000
# The label this remote's rows carry, DECLARED by you, the local operator
# (quipu-fd1). Never read from the remote itself — a remote asserting its own
# trustworthiness would defeat the trust boundary. All optional; trust needs
# all three fields (a rank means nothing outside its chain) and a partial
# declaration is refused at startup and on every federated query.
trust = "urn:trust:partner"
trust_chain = "https://quipu.dev/ontology/defaultTrustChain"
trust_rank = 30
freshness = "fresh" # fresh | recomputing | stale
quipu-server builds the federated provider from these at startup and
health-checks every remote (reported on stderr, with each remote’s declared
label — or undeclared), so a dead peer, a wrong token, or a missing label
is visible without waiting for a federated query to be issued.
Trust labels at the federation edge
A remote’s rows enter your composed result set, so they must enter your label
lattice — and the label is declared by the local operator, never inferred
and never read from the remote (the SARC trust boundary, surfaced at the
federation edge — see docs/design/multi-db-composition.md §5).
- Rows are stamped. Beside
_provider, rows from a declared remote carry_trust(the trust IRI; rank and chain ride the per-memberprovidersentry) and_freshness. Rows from an undeclared member simply lack the binding — undeclared is absent, never fabricated. ProviderStatuscarries the label. Health reports (startup stderr, and thelabelfield on eachprovidersentry) show what each member’s rows are declared as;null/undeclaredmeans exactly that.- The composed label folds remotes in as members. The federated response’s
labelskey is the local dataset fold with each remote’s declared label met in — trust and freshness by meet, so composition never widens; the axes a remote cannot declare (durability, policy, kind) degrade coverage topartial. - Configured floors apply. With
[quipu.labels]floors set, a federated query is refused when a local member fails the floor (same check as the local path) or when a remote’s declared label is below it — and the refusal names the remote. An undeclared remote fails a configured freshness or trust floor, exactly as an unlabelled local graph does: fail-safe at enforcement, honest at reporting. With no floor configured, nothing changes.
Federated queries over REST
POST /query with "federated": true fans the whole query text out to the
local store and every configured remote:
{ "query": "SELECT ?s ?p ?o WHERE { ?s ?p ?o }", "federated": true }
The response carries the merged rows — each tagged with a _provider field —
plus the per-member account:
{
"variables": ["s", "p", "o", "_provider", "_trust", "_freshness"],
"rows": [
{ "s": "ex:traefik", "p": "ex:port", "o": "443", "_provider": "local" },
{ "s": "ex:nginx", "p": "ex:port", "o": "80", "_provider": "prod",
"_trust": "urn:trust:partner", "_freshness": "fresh" }
],
"count": 2,
"providers": [
{ "name": "local", "ok": true, "rows": 1, "error": null, "label": null },
{ "name": "prod", "ok": true, "rows": 1, "error": null,
"label": { "trust": { "iri": "urn:trust:partner",
"chain": "https://quipu.dev/ontology/defaultTrustChain",
"rank": 30 },
"freshness": "fresh" } }
],
"complete": true,
"labels": null
}
_trust/_freshness columns appear only when at least one member declares
that axis; labels is the composed dataset label (local members’ fold with
every remote met in), null when nothing local or remote declared anything.
Whole-query federation only: every member gets the same query text and the
results are unioned, not joined across members. The temporal/graph parameters
(valid_at, tx, graph, row_labels) shape the local evaluator’s
context and are refused on a federated query rather than silently meaning
something different per member. SPARQL 1.1 SERVICE and write federation are
deliberately out of scope (design §7).