Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/echo-keep-experimental.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ jobs:
with:
components: rustfmt, clippy
- run: cargo test --manifest-path experiments/echo-keep/Cargo.toml --locked
- run: cargo clippy --manifest-path experiments/echo-keep/Cargo.toml --locked --all-targets -- -D warnings
- run: cargo test --manifest-path experiments/echo-keep/Cargo.toml --locked --features reference-adapter
- run: cargo clippy --manifest-path experiments/echo-keep/Cargo.toml --locked --all-targets --all-features -- -D warnings
- run: cargo fmt --manifest-path experiments/echo-keep/Cargo.toml -- --check
- name: Verify dependency-tree failure handling
run: bash scripts/tests/keep_dependency_boundary_test.sh
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

### Added

- A disabled-by-default experimental Keep ReferenceStore adapter implements the complete-object CAS port with explicit limits, private bindings, sanitized backend errors and atomic output promotion. It preserves existing CAS defaults and claims no restart durability.

- An isolated experimental Echo–Keep identity bridge checks both independent hash laws and exact byte length from one bounded stream. It leaves the default CAS dependency graph unchanged.

- A fallible complete-object CAS port stages and verifies exact bytes before atomic destination promotion. Memory and disk adapters share conformance checks; existing APIs remain compatible and no durability or authenticated absence is claimed.
Expand Down
8 changes: 7 additions & 1 deletion docs/architecture/echo-keep-physical-content-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
- **Status:** Accepted for experimental conformance; production adoption is
not accepted.
- **Decision date:** 2026-08-09
- **Implementation posture:** `experiments/echo-keep` supplies a bounded dual-identity bridge against Keep revision `3165890e9291cfb5fe10e81a9d7cd151f3e59464` in a separate Rust 1.96 workspace. `echo-cas::physical_content` supplies the fallible complete-object port and MemoryTier/DiskTier adapters. Borrowed views certify no pinned filesystem generation, complete-view absence, retention, synchronization, or crash durability. No Keep backend adapter is implemented yet. Echo CAS remains the default.
- **Implementation posture:** `experiments/echo-keep` supplies a bounded dual-identity bridge against Keep revision `3165890e9291cfb5fe10e81a9d7cd151f3e59464` in a separate Rust 1.96 workspace. `echo-cas::physical_content` supplies the fallible complete-object port and MemoryTier/DiskTier adapters. Borrowed views certify no pinned filesystem generation, complete-view absence, retention, synchronization, or crash durability. The disabled-by-default `reference-adapter` feature supplies an in-memory Keep ReferenceStore backend with private identity/layout bindings and explicit payload, object, count and layout-entry limits. Process death loses its store and bindings; no durable Keep adapter is implemented. Echo CAS remains the default.
- **Refines:** [Retained reading storage and proof boundary](../adr/0020-retained-reading-storage-and-proof-boundary.md)
- **Depends on:** [Durable external-action settlement](../adr/0026-durable-external-action-settlement.md)
- **Related:** [Keep authenticated reconstruction contract](https://github.com/flyingrobots/keep/blob/3bf7b9179db41e90620e6d1875c2d40222a2330b/docs/architecture/authenticated-reconstruction-contract.md)
Expand Down Expand Up @@ -122,6 +122,12 @@ limit.

The materializing port bounds each staging operation, not aggregate retained MemoryTier capacity or process RSS. Existing MemoryTier budgets remain advisory. Missing content is `CapabilityUnavailable`, with no authenticated absence receipt. Disk views verify bytes at read time without a pinned-generation claim. All initial receipts explicitly report unsupported durability and complete-view evidence. This additive port does not reroute current consumers or adopt Keep in production.

## Experimental ReferenceStore adapter

The isolated `experiments/echo-keep` workspace exposes `KeepReferenceAdapter` only with the `reference-adapter` feature. Expected publication computes both identities from the same sealed Echo bytes, stages against the exact Keep identity and validates the commit receipt before registering its private binding. Reconstruction borrows the immutable store, validates the selected Keep target, layout and byte count, and independently seals the Echo identity and exact length before destination promotion. The backend-neutral suite is shared with MemoryTier and DiskTier. No existing consumer is rerouted, and there is no implicit fallback. Backend failures keep their concrete Keep causes privately; an adapter-owned error hides coordinates from public formatting, downcast and source chains. Echo ingress and destination I/O causes retain their existing API.

Physical payload capacity is distinct from per-object, object-count and layout-entry limits. Metadata growth is bounded by the latter two caps, with no process-RSS guarantee. Source, capacity, layout, receipt or destination failures emit no success receipt. Unbound targets report `CapabilityUnavailable`; bound targets whose Keep state is unavailable report an operational failure. The ReferenceStore and its bindings are volatile, with no persisted carrier, restart recovery, durable generation, authenticated absence, retention or crash-durability proposition.

## Output visibility

An ordinary `Write` sink can fail after accepting a prefix. Keep may therefore
Expand Down
6 changes: 5 additions & 1 deletion experiments/echo-keep/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ edition = "2024"
rust-version = "1.96.0"
publish = false
license = "Apache-2.0"
description = "Experimental identity conformance for Echo and Keep"
description = "Experimental Echo–Keep identity conformance and optional reference adapter"

# A separate workspace keeps Keep out of Echo default dependency resolution.
[workspace]

[features]
default = []
reference-adapter = []

[dependencies]
echo-cas = { version = "0.1.0", path = "../../crates/echo-cas" }
blake3 = "=1.8.5"
Expand Down
15 changes: 11 additions & 4 deletions experiments/echo-keep/README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
<!-- SPDX-License-Identifier: Apache-2.0 OR LicenseRef-MIND-UCAL-1.0 -->
<!-- © James Ross Ω FLYING•ROBOTS <https://github.com/flyingrobots> -->

# Experimental Echo–Keep identity bridge
# Experimental Echo–Keep content backend

This separate Rust 1.96 workspace pins Keep at `3165890e9291cfb5fe10e81a9d7cd151f3e59464`. It does not enter Echo’s default workspace or change `echo-cas`’s Rust 1.90 dependency graph.

`IdentityBinding::from_source` computes raw BLAKE3 for Echo and the version-1 Keep domain, bytes, and exact-length hash from one bounded stream. The binding keeps Keep coordinates private and has no persisted encoding. `verify_source` rechecks both identities and length. Neither method proves presence, retention, publication, or durability.

The conformance witness stages the named Keep golden vectors into `ReferenceStore`, reconstructs them, and rechecks exact bytes, both identities, and length. The largest fixture is one MiB. Source failures, limits, and coordinate substitution refuse a complete binding. This does not establish a durable backend or a physical-content port.
The conformance witness stages the named Keep golden vectors into `ReferenceStore`, reconstructs them, and rechecks exact bytes, both identities, and length. The largest fixture is one MiB. Source failures, limits, and coordinate substitution refuse a complete binding. This identity witness establishes no durable backend.

The optional `reference-adapter` feature exposes `KeepReferenceAdapter` through Echo's complete-object physical-content port. It is disabled by default. The adapter privately binds Echo hashes and exact lengths to Keep blob and layout identities, stages expected content, commits explicitly, and verifies the Keep receipt plus Echo bytes before atomic destination promotion. No old-backend fallback occurs. Backend failures retain their original causes privately; public formatting, downcast and source chains expose no Keep coordinates or concrete Keep error types. Echo ingress and destination I/O errors retain their existing behavior.

The constructor requires limits for retained physical payload bytes, each logical object, object count, and layout entries. Payload capacity does not bound metadata or total process RSS; the object and entry caps bound metadata growth. Process death loses both the ReferenceStore and in-memory bindings. Reconstruction receipts establish no restart durability, authenticated absence, retention, synchronization, or pinned durable generation.

The feature suite reuses the exact MemoryTier/DiskTier conformance helper. Adapter tests cover limits, coordinate substitutions, missing store state, failed promotion, source interruption and recreation. Actual upstream missing-chunk and malformed-record refusals are exercised through public Keep APIs and the same quarantine helper; those tests do not inject corruption into Keep's inaccessible private chunk tables.

Run these checks inside the admitted, bounded Docker worker:

```sh
cargo +1.96.0 test --manifest-path experiments/echo-keep/Cargo.toml --locked
cargo +1.96.0 clippy --manifest-path experiments/echo-keep/Cargo.toml --locked --all-targets -- -D warnings
cargo +1.96.0 test --manifest-path experiments/echo-keep/Cargo.toml --locked --features reference-adapter
cargo +1.96.0 clippy --manifest-path experiments/echo-keep/Cargo.toml --locked --all-targets --all-features -- -D warnings
cargo +1.96.0 fmt --manifest-path experiments/echo-keep/Cargo.toml -- --check
cargo +1.90.0 check --locked -p echo-cas
```

[The canonical boundary](../../docs/architecture/echo-keep-physical-content-boundary.md) owns the contract. [Issue #759](https://github.com/flyingrobots/echo/issues/759) owns this identity slice.
[The canonical boundary](../../docs/architecture/echo-keep-physical-content-boundary.md) owns the contract. [Issue #759](https://github.com/flyingrobots/echo/issues/759) owns the identity slice; [issue #761](https://github.com/flyingrobots/echo/issues/761) owns the optional reference adapter.

The isolated graph has its own dependency-policy CI check. It derives all license, ban, advisory, and source rules from the root policy, with one scoped allowance for the pinned Keep Git source. The production workspace policy remains unchanged.
7 changes: 6 additions & 1 deletion experiments/echo-keep/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
// © James Ross Ω FLYING•ROBOTS <https://github.com/flyingrobots>
//! Experimental identity conformance for the Echo–Keep physical-content boundary.
//! Experimental identity conformance and an optional non-durable Keep backend.
//!
//! Echo hashes raw bytes. Keep has a distinct, versioned identity law. A binding
//! authenticates one bounded source under both laws; it proves no storage presence,
Expand Down Expand Up @@ -132,3 +132,8 @@ impl IdentityBinding {

#[cfg(test)]
mod tests;

#[cfg(feature = "reference-adapter")]
mod reference_adapter;
#[cfg(feature = "reference-adapter")]
pub use reference_adapter::{KeepReferenceAdapter, KeepReferenceView};
200 changes: 200 additions & 0 deletions experiments/echo-keep/src/reference_adapter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// SPDX-License-Identifier: Apache-2.0
// © James Ross Ω FLYING•ROBOTS <https://github.com/flyingrobots>
//! Optional non-durable physical backend, with private Keep coordinates.
use crate::IdentityBinding;
use echo_cas::{
BlobHash,
physical_content::{
ContentError, ContentReceipt, ContentTarget, PhysicalContentBackend, PhysicalContentView,
StagedContent, TransactionalContentDestination, reconstruct_quarantined,
},
};
use keep::{
IngestionError, LayoutEntryLimit, LayoutId, LayoutValidationError, PublishError,
ReferenceStore, ReferenceStoreCapacity,
};
use std::collections::BTreeMap;
use std::error::Error;
use std::fmt;
use std::io::Cursor;

struct Binding {
identity: IdentityBinding,
layout: LayoutId,
}

/// Capacity-bounded experimental adapter over Keep's in-memory ReferenceStore.
///
/// Process death loses both store and bindings. No receipt establishes crash
/// durability, retention, generation, complete-view absence or causal authority.
/// The backend is unavailable unless the `reference-adapter` feature is enabled.
pub struct KeepReferenceAdapter {
store: ReferenceStore,
bindings: BTreeMap<BlobHash, Binding>,
object_limit: usize,
object_count: usize,
layout_limit: LayoutEntryLimit,
}
impl KeepReferenceAdapter {
/// Creates an empty backend with explicit physical payload, per-object,
/// object-count and per-layout entry limits. Metadata is bounded by the
/// object-count and layout-entry limits, not the physical payload capacity.
///
/// # Errors
/// Returns ResourceLimit if the layout limit exceeds the supported protocol.
pub fn new(
physical_bytes: usize,
object_bytes: usize,
objects: usize,
layout_entries: u32,
) -> Result<Self, ContentError> {
let layout_limit =
LayoutEntryLimit::new(layout_entries).map_err(|_| ContentError::ResourceLimit)?;
Ok(Self {
store: ReferenceStore::new(ReferenceStoreCapacity::new(physical_bytes)),
bindings: BTreeMap::new(),
object_limit: object_bytes,
object_count: objects,
layout_limit,
})
}
}

/// An immutable borrowed Keep capability without a durable-generation claim.
pub struct KeepReferenceView<'a>(&'a KeepReferenceAdapter);
impl PhysicalContentView for KeepReferenceView<'_> {
fn reconstruct(
&self,
target: ContentTarget,
destination: &mut dyn TransactionalContentDestination,
) -> Result<ContentReceipt, ContentError> {
reconstruct_quarantined(target, destination, |output| {
let binding = self
.0
.bindings
.get(&target.hash)
.ok_or(ContentError::CapabilityUnavailable)?;
if binding.identity.echo_identity() != target.hash
|| binding.identity.length() != target.length
{
return Err(ContentError::Mismatch);
}
let receipt = self
.0
.store
.reconstruct(binding.identity.keep, output)
.map_err(|error| backend_failure("reconstruction", error))?;
if receipt.target() != binding.identity.keep
|| receipt.layout_id() != binding.layout
|| receipt.bytes_written().get() != target.length
{
return Err(ContentError::Mismatch);
}
// Shared quarantine independently verifies Echo identity and length
// before a complete handle can reach destination promotion.
Ok(())
})
}
}
impl PhysicalContentBackend for KeepReferenceAdapter {
type View<'a> = KeepReferenceView<'a>;
fn content_view(&self) -> Self::View<'_> {
KeepReferenceView(self)
}
fn publish_content(&mut self, staged: StagedContent) -> Result<ContentReceipt, ContentError> {
let target = staged.target();
if usize::try_from(target.length)
.ok()
.is_none_or(|length| length > self.object_limit)
{
return Err(ContentError::ResourceLimit);
}
if !self.bindings.contains_key(&target.hash) && self.bindings.len() >= self.object_count {
return Err(ContentError::ResourceLimit);
}
let content = staged.into_verified();
let identity =
IdentityBinding::from_source(&mut Cursor::new(content.bytes()), target.length)
.map_err(|error| backend_failure("identity verification", error))?;
if identity.echo_identity() != target.hash || identity.length() != target.length {
return Err(ContentError::Mismatch);
}
if self
.bindings
.get(&target.hash)
.is_some_and(|existing| existing.identity != identity)
{
return Err(ContentError::Mismatch);
}
let staged = self
.store
.stage_expected(
&mut Cursor::new(content.bytes()),
identity.keep,
self.layout_limit,
)
.map_err(ingestion_error)?;
let layout = staged.layout_id();
if staged.target() != identity.keep {
return Err(ContentError::Mismatch);
}
let receipt = staged.commit(&mut self.store).map_err(publication_error)?;
if receipt.target() != identity.keep || receipt.layout_id() != layout {
return Err(ContentError::Mismatch);
}
self.bindings
.insert(target.hash, Binding { identity, layout });
Ok(ContentReceipt::from_verified(&content))
}
}
fn ingestion_error(error: IngestionError) -> ContentError {
match error {
IngestionError::CapacityExceeded { .. }
| IngestionError::Allocation { .. }
| IngestionError::Layout(
LayoutValidationError::EntryLimitExceeded { .. }
| LayoutValidationError::Allocation { .. },
) => ContentError::ResourceLimit,
error => backend_failure("staging", error),
}
}

fn publication_error(error: PublishError) -> ContentError {
match error {
PublishError::CapacityExceeded { .. } => ContentError::ResourceLimit,
error => backend_failure("publication", error),
}
}

// Keep coordinates and concrete types cannot escape through error formatting,
// downcast or the public source chain. The original cause remains privately
// owned; ordinary Echo staging and destination I/O errors retain their API.
struct BackendFailure {
operation: &'static str,
_cause: Box<dyn Error + Send + Sync>,
}
impl fmt::Debug for BackendFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KeepBackendFailure")
.field("operation", &self.operation)
.finish_non_exhaustive()
}
}
impl fmt::Display for BackendFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Keep reference {} failed", self.operation)
}
}
impl Error for BackendFailure {}
fn backend_failure(
operation: &'static str,
cause: impl Error + Send + Sync + 'static,
) -> ContentError {
ContentError::Backend(Box::new(BackendFailure {
operation,
_cause: Box::new(cause),
}))
}

#[cfg(test)]
mod tests;
Loading
Loading