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
28 changes: 28 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
# Preserve concurrent additions to the shared agent instructions.
AGENTS.md merge=union

# Preserve every authored byte occurrence hashed into provider-generator identity.
Cargo.lock -text
Cargo.toml -text
rust-toolchain.toml -text
crates/echo-edict-canonical/Cargo.toml -text
crates/echo-edict-canonical/src/lib.rs -text
crates/echo-wesley-gen/Cargo.toml -text
crates/echo-wesley-gen/src/lib.rs -text
crates/echo-wesley-gen/src/provider_*.rs -text
crates/echo-wesley-gen/src/bin/echo-edict-provider-*.rs -text
schemas/edict-provider/contracts/v1/** -text
schemas/edict-provider/echo-provider-semantics-v1.json -text
schemas/edict-provider/generation-settings-v1.json -text

# Preserve the exact audited component owners before package assembly.
schemas/edict-provider/components/v1/** -text
schemas/edict-provider/components/v1/**/*.wasm binary

# Preserve exact checked provider corpus bytes across checkout configurations.
schemas/edict-provider/generated/v1/** -text
schemas/edict-provider/generated/v1/**/*.cbor binary

# Preserve the exact digest-locked provider distribution across checkouts.
schemas/edict-provider/package/v1/** -text
schemas/edict-provider/package/v1/**/*.cbor binary
schemas/edict-provider/package/v1/**/*.wasm binary

# Preserve package-local provider source/artifact carriers byte-for-byte.
crates/echo-wesley-gen/assets/v1/** -text
crates/echo-wesley-gen/assets/v1/**/*.cbor binary
crates/echo-wesley-gen/assets/v1/**/*.wasm binary
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ jobs:
.
- name: cargo test (workspace sans warp-core)
run: cargo test --workspace --exclude warp-core
- name: checked Edict provider crate assets are current and packaged
run: cargo +1.90.0 run --locked -p echo-wesley-gen --bin echo-edict-provider-assets -- --check-package-list
- name: checked Edict provider package is current
run: cargo +1.90.0 run --locked -p echo-wesley-gen --bin echo-edict-provider-package -- --check
- name: package publishable Edict canonical leaf
run: cargo package -p echo-edict-canonical --locked

Expand Down
46 changes: 43 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,42 @@

### Added

- `echo-wesley-gen` now purely assembles and digest-admits the first complete
Echo Edict provider distribution from the verified 22-file generated corpus
and explicit lowerer/verifier bytes. The derived provider manifest carries ten
exact routes and 24 schema bindings—nine invocation domains, the generated
artifact profile, and 14 generated-resource domains—but never inventories
itself. A versioned canonical-CBOR package root binds those semantics plus raw hashes of
all 24 non-manifest members, while the exact 25-file inventory, deterministic
JSON rendering, mixed raw/domain-framed digest laws, packaged Wesley
provenance/review, component bounds, and an external expected provider pin
all fail closed through structured errors. This is package-occurrence
authentication only; Edict schema/component preflight and Echo runtime
installation remain separate authority crossings. A dedicated publisher now
checks that all 22 generated members exactly reproduce the current checked
provider corpus introduced by #652 before writing the two components and derived manifest as a
self-contained 25-file distribution. Its capability-oriented filesystem
boundary refuses invalid expected inventories before resolving the root,
bounds actual-tree enumeration and expected-byte reads, never opens an
unexpected regular file, and makes `--check` report sorted drift without
creating, deleting, or rewriting package material.
- The checked provider package now passes an isolated Edict-native readiness
boundary pinned to Edict merge `c75c3f55`. The exact manifest constructs its
immutable 24-domain schema registry, all five canonical primaries and 14
generated resources satisfy their owning CDDL roots, resource references are
bound field-by-field to independently recomputed domain-framed digests, both
components pass frozen-WIT preflight, and both request kinds produce opaque
validation proofs. Schema-valid byte replacement, digest mutation, semantic
field swaps, authority-source disagreement, malformed schemas/components,
and invalid requests all fail before guest execution. This proves package
readiness only, never Echo installation, execution, or runtime authority.
- `echo-wesley-gen` now carries a fixed 35-file package-local source and provider
asset boundary, preserving original logical source labels while making its
`.crate` archive independent of workspace-parent files. An explicit sync tool
distinguishes authoritative generated/component owners from their checked
package corroboration, supports staged regeneration without circularity, and
checks exact Cargo archive selection. The extracted archive compiles when its
still-unpublished Echo dependencies are supplied through local patches.
- Echo now provides the exact `edict:target-provider/lowerer@1.0.0`
Component Model implementation for the first checked provider closure. The
pure lowerer accepts only explicit digest-bound Core, target-profile,
Expand Down Expand Up @@ -50,8 +86,8 @@
predicate, input-constraint, require-failure, and Core-value shapes before
separating malformed artifacts from well-formed unsupported semantics, and
one admitted diagnostic-ABI identity now binds both the target profile and
every emitted report. The 188,736-byte checked component has SHA-256
`e13eda6e02d5a46d2aecdec0546d53a7bf66f2580f8d5ec06e5d76710716a27b`
every emitted report. The 189,515-byte checked component has SHA-256
`11fcaf291ffacac62800ab3fc198e29a23a591bcd30ca98828fa10a8ad1f1952`
and reproduces byte-for-byte across independently provisioned designated
`linux/amd64` builders. The isolated pinned Edict host preflights the exact
request artifacts and declared output schema, invokes that checked component,
Expand All @@ -64,7 +100,11 @@
canonical CBOR and domain-framed digest contracts as a publishable `0.1.0`
leaf. `echo-wesley-gen` retains its existing compatibility surface through a
re-export, while executable provider components use the same codec without
depending on generator or Wesley APIs.
depending on generator or Wesley APIs. Its decoder now applies a 65,536-node
host materialization ceiling, charging map keys and values separately. A
matching cumulative reservation budget rejects both oversized direct
containers and nested declarations that attempt to reuse the same capacity
allowance before reserving their storage.
- `echo-wesley-gen` now checks in the first exact 22-file Edict provider
artifact corpus: five canonical-CBOR primaries, fourteen canonical-CBOR
resources, the self-contained CDDL, Wesley provenance JSON, and
Expand Down
7 changes: 6 additions & 1 deletion crates/echo-edict-canonical/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ definite-length byte and UTF-8 text strings, arrays, and maps. Encoding sorts
maps by canonical key bytes and rejects duplicate canonical keys. Decoding
rejects indefinite-length, tagged, floating-point, simple, non-minimal,
trailing, malformed, duplicate-key, and noncanonical inputs. Both directions
enforce Edict's exact 128-container nesting boundary.
enforce Edict's exact 128-container nesting boundary. Decoding additionally
caps one materialized value tree at 65,536 nodes, charging map keys and values
separately. A matching cumulative reservation budget is charged before every
container capacity allocation, so nested declarations cannot reuse the same
budget to amplify host allocation. Those ceilings are Echo host resource
bounds, not new Edict byte-identity claims.

Failures expose stable `CanonicalValueErrorKind` values rather than Rust debug
spellings. The implementation is deterministic and pure: it performs no
Expand Down
77 changes: 75 additions & 2 deletions crates/echo-edict-canonical/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ pub const EDICT_DIGEST_FRAME_V1: &str = "edict.digest/v1";
/// is accepted; one additional container is rejected.
pub const MAX_CANONICAL_NESTING_DEPTH_V1: usize = 128;

/// Maximum number of value nodes materialized by one canonical decode.
///
/// The root counts as one node. Array members, map keys, and map values each
/// count separately. This is an Echo host resource bound rather than part of
/// the Edict canonical byte identity; it prevents compact collection headers
/// from amplifying into unbounded host allocations before admission.
pub const MAX_CANONICAL_DECODE_NODES_V1: usize = 65_536;

/// Stable failure categories for Edict canonical values and byte streams.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CanonicalValueErrorKind {
Expand Down Expand Up @@ -360,18 +368,30 @@ where
struct Decoder<'a> {
bytes: &'a [u8],
position: usize,
remaining_nodes: usize,
remaining_reserved_nodes: usize,
}

impl<'a> Decoder<'a> {
const fn new(bytes: &'a [u8]) -> Self {
Self { bytes, position: 0 }
Self::with_node_budget(bytes, MAX_CANONICAL_DECODE_NODES_V1)
}

const fn with_node_budget(bytes: &'a [u8], node_budget: usize) -> Self {
Self {
bytes,
position: 0,
remaining_nodes: node_budget,
remaining_reserved_nodes: node_budget,
}
}

const fn remaining(&self) -> usize {
self.bytes.len() - self.position
}

fn value(&mut self, depth: usize) -> Result<CanonicalValueV1, CanonicalValueError> {
self.charge_nodes(1)?;
check_depth(depth)?;
let initial = self.byte()?;
let major = initial >> 5;
Expand Down Expand Up @@ -401,6 +421,8 @@ impl<'a> Decoder<'a> {
4 => {
check_container_depth(depth)?;
let length = self.length(additional)?;
self.ensure_nodes_available(length)?;
self.reserve_nodes(length)?;
let mut values = Vec::with_capacity(length);
for _ in 0..length {
values.push(self.value(depth + 1)?);
Expand All @@ -410,6 +432,9 @@ impl<'a> Decoder<'a> {
5 => {
check_container_depth(depth)?;
let length = self.length(additional)?;
let child_nodes = length.checked_mul(2).ok_or_else(node_budget_error)?;
self.ensure_nodes_available(child_nodes)?;
self.reserve_nodes(child_nodes)?;
let mut entries = Vec::with_capacity(length);
let mut encoded_keys = BTreeSet::new();
for _ in 0..length {
Expand Down Expand Up @@ -469,6 +494,29 @@ impl<'a> Decoder<'a> {
checked_collection_length::<usize>(declared, remaining)
}

fn ensure_nodes_available(&self, required: usize) -> Result<(), CanonicalValueError> {
if required > self.remaining_nodes {
return Err(node_budget_error());
}
Ok(())
}

fn charge_nodes(&mut self, count: usize) -> Result<(), CanonicalValueError> {
self.remaining_nodes = self
.remaining_nodes
.checked_sub(count)
.ok_or_else(node_budget_error)?;
Ok(())
}

fn reserve_nodes(&mut self, count: usize) -> Result<(), CanonicalValueError> {
self.remaining_reserved_nodes = self
.remaining_reserved_nodes
.checked_sub(count)
.ok_or_else(node_budget_error)?;
Ok(())
}

fn byte(&mut self) -> Result<u8, CanonicalValueError> {
let Some(value) = self.bytes.get(self.position).copied() else {
return Err(CanonicalValueError::new(
Expand Down Expand Up @@ -504,9 +552,16 @@ impl<'a> Decoder<'a> {
}
}

fn node_budget_error() -> CanonicalValueError {
CanonicalValueError::new(
CanonicalValueErrorKind::UnsupportedValue,
"canonical CBOR decoded node budget exceeded",
)
}

#[cfg(test)]
mod tests {
use super::{checked_collection_length, CanonicalValueError, CanonicalValueErrorKind};
use super::{checked_collection_length, CanonicalValueError, CanonicalValueErrorKind, Decoder};

#[test]
fn declared_length_bounds_precede_host_width_conversion() {
Expand All @@ -518,4 +573,22 @@ mod tests {
))
);
}

#[test]
fn nested_container_reservations_are_cumulatively_bounded() {
let bytes = [0x87, 0x86, 0x85, 0x84, 0x83, 0x82, 0x81, 0xf6];
let mut decoder = Decoder::with_node_budget(&bytes, 8);

assert_eq!(
decoder.value(0),
Err(CanonicalValueError::new(
CanonicalValueErrorKind::UnsupportedValue,
"canonical CBOR decoded node budget exceeded"
))
);
assert_eq!(
decoder.position, 2,
"the second container must be refused before its capacity is reserved"
);
}
}
39 changes: 38 additions & 1 deletion crates/echo-edict-canonical/tests/canonical_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

use echo_edict_canonical::{
decode_canonical_cbor_v1, digest_canonical_value_v1, encode_canonical_cbor_v1,
CanonicalValueErrorKind, CanonicalValueV1, MAX_CANONICAL_NESTING_DEPTH_V1,
CanonicalValueErrorKind, CanonicalValueV1, MAX_CANONICAL_DECODE_NODES_V1,
MAX_CANONICAL_NESTING_DEPTH_V1,
};

fn text(value: &str) -> CanonicalValueV1 {
Expand Down Expand Up @@ -267,3 +268,39 @@ fn unsupported_cbor_and_digest_domains_fail_closed() {
digest_canonical_value_v1("test.second/v1", &value).expect("second digest computes")
);
}

#[test]
fn decoded_node_budget_rejects_compact_container_amplification_before_reserve() {
let node_limit = u32::try_from(MAX_CANONICAL_DECODE_NODES_V1)
.expect("canonical decode-node limit fits the CBOR u32 length form");

let mut array = vec![0x9a];
array.extend_from_slice(&node_limit.to_be_bytes());
array.resize(array.len() + MAX_CANONICAL_DECODE_NODES_V1, 0xf6);
let array_error = decode_canonical_cbor_v1(&array)
.expect_err("an array that exhausts the child-node budget rejects before reserve");
assert_eq!(
array_error.kind(),
CanonicalValueErrorKind::UnsupportedValue
);
assert_eq!(
array_error.detail(),
"canonical CBOR decoded node budget exceeded"
);

let map_entries = node_limit / 2;
let mut map = vec![0xb9];
map.extend_from_slice(
&u16::try_from(map_entries)
.expect("half the canonical decode-node limit fits the CBOR u16 length form")
.to_be_bytes(),
);
map.resize(map.len() + MAX_CANONICAL_DECODE_NODES_V1, 0xf6);
let map_error = decode_canonical_cbor_v1(&map)
.expect_err("a map that exhausts the key/value-node budget rejects before reserve");
assert_eq!(map_error.kind(), CanonicalValueErrorKind::UnsupportedValue);
assert_eq!(
map_error.detail(),
"canonical CBOR decoded node budget exceeded"
);
}
4 changes: 2 additions & 2 deletions crates/echo-edict-provider-verifier/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ target profile's diagnostic ABI and every emitted report consume one shared
admitted identity.
The `wasm32` guest adapter vendors Edict's exact frozen
`edict:target-provider/verifier@1.0.0` WIT world and performs only exhaustive
transport-to-model conversion. Its reproducibly built 188,736-byte checked
transport-to-model conversion. Its reproducibly built 189,515-byte checked
component has SHA-256
`e13eda6e02d5a46d2aecdec0546d53a7bf66f2580f8d5ec06e5d76710716a27b`.
`11fcaf291ffacac62800ab3fc198e29a23a591bcd30ca98828fa10a8ad1f1952`.
Component identity and admitted host replay remain separate propositions: the
pinned Edict host preflights the request artifacts and declared output schema,
invokes the checked component, then admits and manifests each returned accepted
Expand Down
58 changes: 58 additions & 0 deletions crates/echo-wesley-gen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,56 @@ semantic/settings/contract inputs already bound by Wesley and every generated
output, preventing a circular generator identity. This is source/dependency-lock
identity, not an executable-build or supply-chain attestation.

`provider_package` purely assembles the 22 generated files and two explicit
provider components into one 24-member, non-self-referential package closure,
then derives the separate `edict.provider-manifest/v1` JSON file. The Echo-owned
package root is a domain-framed canonical-CBOR value over the exact manifest
routes, 24 schema bindings (nine invocation domains, the generated artifact
profile, and 14 generated-resource domains), and raw SHA-256 of every
non-manifest member.
Canonical-CBOR artifact routes retain their Edict domain-framed identities;
CDDL, Wesley evidence JSON, components, and every physical member retain raw
exact-byte identities. Digest admission requires exact deterministic manifest
JSON, one shared semantic-source/generator reference proven against the packaged
Wesley provenance and review, the exact sorted 25-file inventory, and an
external caller pin. The resulting proof authenticates package occurrence only.
Edict schema-registry construction and component contract preflight remain
separate required crossings before guest execution, and Echo runtime admission
remains later still.

`echo-edict-provider-package` is the explicit publication boundary for that
distribution. Before any filesystem action it proves that the package's 22
`generated/` members are the current checked provider corpus introduced by #652,
then writes the two
checked components and derived manifest through the same no-follow,
unexpected-entry-refusing filesystem boundary used by the artifact corpus. The
checked 25-file result lives under `schemas/edict-provider/package/v1/`.
`--check` reports sorted missing, changed, or unexpected package members and
never repairs or creates them. The shared boundary validates a strictly sorted,
unique expected inventory before resolving the root, caps that inventory at 256
files and 64 MiB, caps an actual scan at 1,024 entries, and never opens or reads
an unexpected regular file.

`echo-edict-provider-assets` maintains the exact 35-file package-local carrier
tree under `assets/v1/`. The physical carrier names are packaging locations,
not replacement source identities: generator provenance continues to name the
original repository-relative authored paths. Read-only mode requires every
carrier to match its fixed owner, requires generated artifacts and components
to match their checked package copies, and can prove that `cargo package --list`
selects exactly the complete carrier inventory. Explicit `--write` mode copies
authoritative owners without requiring the temporarily stale package copy,
allowing the honest staged sequence artifact generation, carrier sync, package
generation, then final carrier corroboration. It never discovers a preferred
owner or normalizes authored bytes.

The isolated `tests/edict-provider-host-v1` gate pins Edict revision
`c75c3f550d049485ba00eae0dc272c6dd6aca11f` and consumes the exact checked
package. It constructs the native 24-domain schema registry, validates all 19
canonical package members under their owning roots, binds every lawpack and
target-profile resource field to exact packaged bytes, prepares both frozen-WIT
components, and validates both request kinds without invoking guest code. That
is pre-execution package readiness, not Echo installation or runtime authority.

The dedicated `echo-edict-provider-artifacts` binary is the explicit filesystem
boundary. The caller-supplied `--out` path is resolved once under ambient
filesystem authority; its final corpus-root entry is opened or created without
Expand Down Expand Up @@ -151,6 +201,14 @@ cargo +1.90.0 run --locked -p echo-wesley-gen \
# Report checked-corpus drift without rewriting anything
cargo +1.90.0 run --locked -p echo-wesley-gen \
--bin echo-edict-provider-artifacts -- --check

# Publish the self-contained digest-locked provider package
cargo +1.90.0 run --locked -p echo-wesley-gen \
--bin echo-edict-provider-package --

# Report package drift without rewriting anything
cargo +1.90.0 run --locked -p echo-wesley-gen \
--bin echo-edict-provider-package -- --check
```

## Notes
Expand Down
Loading
Loading