All checks were successful
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 1m0s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 2m51s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m53s
standards workspaces build + test / aac-rs (pull_request) Successful in 3m4s
standards workspaces build + test / map-rs (pull_request) Successful in 1m0s
standards workspaces build + test / mesh-rs (pull_request) Successful in 56s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 2m0s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m1s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 2m3s
standards workspaces build + test / openpay (pull_request) Successful in 3m27s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 8m47s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 7m13s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 6m23s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 19s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points (pull_request) Successful in 20s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 1m38s
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 1m3s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 5s
no-vanity-metrics / No new orphan source files (pull_request) Successful in 6s
spec citations resolve / Code cites no section its SPEC lacks (pull_request) Successful in 4s
lockfiles / every Cargo.lock is current (pull_request) Successful in 49s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m9s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m56s
standards workspaces build + test / jcp-rs (pull_request) Successful in 2m15s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m11s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m13s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m30s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m7s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m24s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 4m7s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m14s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 14s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 8m51s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 9s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 25s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 48s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 2m4s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m12s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 4s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 4s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m12s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 2m54s
wai-pack / wai-pack · macos (pull_request) Successful in 3m51s
wai-pack / wai-pack · linux (pull_request) Successful in 1m16s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 17m27s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 22s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 3m56s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m16s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m45s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m4s
Three workspaces could not be linted at all: a deny-level clippy lint errored,
and clippy stops at the first erroring crate, so everything behind it went
unreported. None of the ten errors was a bug; each is fixed so it cannot mask
the next.
jouleclaw-loader-gguf, jouleclaw-contract, jouleclaw-runtime: `0 * stride`
spells the shared kv head (MQA) and the (row, col) cell address, mirroring
the `hh * s` beside it. Kept, with the lint allowed and the reason written
down, rather than dropping a term that documents which head is read.
jouleclaw-backend-apple, jouleclaw-loader-gguf/ssm: a truncated SQRT_2 and
LN_2 where the constant is what is meant -- `softplus(0) = ln2` is the
assertion's whole point. Now `std::f32::consts`, which is also exact.
jouleclaw-bounded, jouleclaw-outlier, jouleclaw-tools, joule-ui-core: 3.14
as an arbitrary test value, which clippy reads as an attempt at pi. Given a
value that is not near pi; in jouleclaw-tools the decimal is the subject of
the format assertion, so expectation and input moved together.
jouleclaw-policy-ipc: `parse_args` looped, but every arm returns or exits,
so it could only ever run once. `if let` says what happens.
smart-byte-edv: `s.sequence = cur` assigned the field to itself. `update`
takes a document at its current sequence and bumps it, so the no-op was
dead and the comment above it ("bump twice") described neither. Removed;
the comment now states the mechanism the test relies on.
jcp-schema: `Origins`'s `TryFrom<OriginsWire>` had `Error = Infallible`.
Carried over to `From` with `#[serde(from = ...)]`, so the normalisation
that keeps the top of the lattice single-encoded still runs on the wire --
`origins_wire_normalizes_on_deserialize` pins it.
wai-rs: 19 unsafe `extern "C"` entry points stated no safety contract, beside
neighbours that did. Each now names what its caller must guarantee, per
pointer, from its own signature.
Verified on the build host: all 17 workspaces lint with zero errors; the tests
of every crate whose values changed pass; wai-rs builds, lints and documents.
Co-Authored-By: Claude Opus 5 <[email protected]>
167 lines
5.7 KiB
Rust
167 lines
5.7 KiB
Rust
//! Vault replication (DIF EDV v0.10 § 9 — non-normative; modelled here
|
|
//! after the implementation pattern shared by Aries cloud-agent and
|
|
//! Bedrock's Bluesky-style replication).
|
|
//!
|
|
//! Replication moves [`EncryptedDocument`] records between two vaults
|
|
//! that share a common controller (typically a primary + a hot replica,
|
|
//! or a desktop + mobile pair). Because documents are *opaque ciphertext*
|
|
//! to the vault, replication does not need to decrypt anything — it only
|
|
//! needs to compare document sequences and copy the higher-sequence
|
|
//! record across.
|
|
//!
|
|
//! [`sync`] implements a one-way pull from `source` into `target`: for
|
|
//! every document in `source`, if `target` does not have a corresponding
|
|
//! record (or has an older `sequence`), the document is copied over.
|
|
|
|
use async_trait::async_trait;
|
|
|
|
use crate::error::EdvError;
|
|
use crate::spec::EncryptedDocument;
|
|
use crate::vault::Vault;
|
|
|
|
/// Outcome of a single replication pass.
|
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
|
pub struct ReplicationReport {
|
|
/// Documents inserted into `target` because they were missing.
|
|
pub inserted: Vec<String>,
|
|
/// Documents updated in `target` because `source` had a higher
|
|
/// sequence.
|
|
pub updated: Vec<String>,
|
|
/// Documents skipped because `target` already had an equal-or-newer
|
|
/// sequence.
|
|
pub skipped: Vec<String>,
|
|
}
|
|
|
|
/// One-way pull replication: copy every document from `source` into
|
|
/// `target` if `target` is missing it or holds an older revision.
|
|
pub async fn sync(
|
|
source: &dyn Vault,
|
|
target: &dyn Vault,
|
|
) -> Result<ReplicationReport, EdvError> {
|
|
let mut report = ReplicationReport::default();
|
|
let refs = source.list().await?;
|
|
for r in refs {
|
|
let doc = source.get(&r.id).await?;
|
|
match target.get(&r.id).await {
|
|
Ok(existing) => {
|
|
if doc.sequence > existing.sequence {
|
|
let mut to_send = doc.clone();
|
|
to_send.sequence = existing.sequence;
|
|
target.update(to_send).await?;
|
|
report.updated.push(r.id);
|
|
} else {
|
|
report.skipped.push(r.id);
|
|
}
|
|
}
|
|
Err(EdvError::NotFound(_)) => {
|
|
let mut to_send = doc.clone();
|
|
to_send.sequence = 0;
|
|
target.insert(to_send).await?;
|
|
report.inserted.push(r.id);
|
|
}
|
|
Err(other) => return Err(other),
|
|
}
|
|
}
|
|
Ok(report)
|
|
}
|
|
|
|
/// A replication strategy abstraction, in case callers want to wire in
|
|
/// a different policy (e.g. push, push+pull, conflict-on-divergent).
|
|
#[async_trait]
|
|
pub trait Replicator: Send + Sync {
|
|
/// Run a replication pass and return the report.
|
|
async fn replicate(
|
|
&self,
|
|
source: &dyn Vault,
|
|
target: &dyn Vault,
|
|
) -> Result<ReplicationReport, EdvError>;
|
|
}
|
|
|
|
/// The canonical one-way pull strategy implementing [`sync`].
|
|
pub struct PullReplicator;
|
|
|
|
#[async_trait]
|
|
impl Replicator for PullReplicator {
|
|
async fn replicate(
|
|
&self,
|
|
source: &dyn Vault,
|
|
target: &dyn Vault,
|
|
) -> Result<ReplicationReport, EdvError> {
|
|
sync(source, target).await
|
|
}
|
|
}
|
|
|
|
/// Convenience: convert a list of documents to a Vec for diffing.
|
|
pub fn snapshot(docs: &[EncryptedDocument]) -> Vec<(String, u64)> {
|
|
docs.iter()
|
|
.map(|d| (d.id.clone(), d.sequence))
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::spec::{Config, EncryptedDocument, KeyDescriptor};
|
|
use crate::vault::InMemoryVault;
|
|
|
|
fn cfg() -> Config {
|
|
let k = KeyDescriptor {
|
|
id: "did:example:alice#kex-1".into(),
|
|
key_type: "JsonWebKey2020".into(),
|
|
controller: None,
|
|
};
|
|
let h = KeyDescriptor {
|
|
id: "did:example:alice#hmac-1".into(),
|
|
key_type: "Sha256HmacKey2019".into(),
|
|
controller: None,
|
|
};
|
|
Config::new("urn:uuid:vault", "did:example:alice", k, h)
|
|
}
|
|
|
|
fn doc(id: &str) -> EncryptedDocument {
|
|
EncryptedDocument {
|
|
id: id.into(),
|
|
sequence: 0,
|
|
jwe: serde_json::json!({"v": id}),
|
|
indexed: Vec::new(),
|
|
stream: None,
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn pull_inserts_missing() {
|
|
let src = InMemoryVault::new(cfg());
|
|
let dst = InMemoryVault::new(cfg());
|
|
src.insert(doc("urn:a")).await.expect("insert a");
|
|
src.insert(doc("urn:b")).await.expect("insert b");
|
|
let r = sync(&src, &dst).await.expect("sync");
|
|
assert_eq!(r.inserted.len(), 2);
|
|
assert_eq!(dst.len().await, 2);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn pull_skips_when_target_current() {
|
|
let src = InMemoryVault::new(cfg());
|
|
let dst = InMemoryVault::new(cfg());
|
|
src.insert(doc("urn:a")).await.expect("insert src");
|
|
dst.insert(doc("urn:a")).await.expect("insert dst");
|
|
let r = sync(&src, &dst).await.expect("sync");
|
|
assert!(r.inserted.is_empty());
|
|
assert!(r.updated.is_empty());
|
|
assert_eq!(r.skipped, vec!["urn:a".to_string()]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn pull_updates_when_source_newer() {
|
|
let src = InMemoryVault::new(cfg());
|
|
let dst = InMemoryVault::new(cfg());
|
|
src.insert(doc("urn:a")).await.expect("insert src");
|
|
dst.insert(doc("urn:a")).await.expect("insert dst");
|
|
// `update` takes the document at its current sequence and bumps it,
|
|
// so one update leaves the source one ahead of the target.
|
|
let s = src.get("urn:a").await.expect("get src");
|
|
let _ = src.update(s).await.expect("update src");
|
|
let r = sync(&src, &dst).await.expect("sync");
|
|
assert_eq!(r.updated, vec!["urn:a".to_string()]);
|
|
}
|
|
}
|