diff --git a/CHANGELOG.md b/CHANGELOG.md index f747adeae..7b70bce33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,8 +31,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Credential-free TLS evidence containing canonical origin, TCP peers, reference identity, TLS version, cipher-suite identifier, selected ALPN or explicit absence, leaf certificate and SPKI hashes, server-presented certificate hashes and bounds, trust-bundle identity and hash, validity interval, fixed verification time, revocation configuration, and measured handshake duration. - Credential-free sensitive-handle lifecycle evidence binds issuance, exclusive expiry, bounded uses, observed resolution count, and revocation to the exact credential-free `OpaqueHandleOnly` sensitive-access receipt, preserving tenant, actor, task, field set, purpose, destination, classification, policy version, and decision time without storing opaque handle tokens or protected values. - Credential-free connection and redirect evidence containing canonical addresses, destination classes, target digests, hop numbers, and approved-address counts. -- Credential-free verified TCP evidence containing the logical origin, requested socket, observed peer, destination class, successful attempt number, and per-attempt timeout. -- Standard `Display` and `std::error::Error` contracts for destination, redirect, digest, direct-network, TLS, and resource-budget failures, including preserved destination-policy, rustls, and operating-system sources where applicable. +- Standard `Display` and `std::error::Error` contracts for core extension identifiers, destination, redirect, digest, direct-network, TLS, and resource-budget failures, including preserved destination-policy, rustls, and operating-system sources where applicable. - Real loopback TCP integration proof plus deterministic timeout, refusal, retry, peer-inspection, peer-mismatch, canonicalization, IPv6 metadata, and single-use replay tests. - Real loopback rustls integration covering trusted DNS SAN, Common-Name fallback rejection, wrong-name and untrusted-root rejection, fixed-time expiry and not-yet-valid failures, exact IPv4 and IPv6 SANs, TLS 1.2/TLS 1.3, required and optional ALPN, and transport-origin binding. - Cumulative interactive-first RAM, VRAM, batch, local-model, admission, pause, and compositor-pressure mitigation plans, including active-consumer reduction at exact hard limits. diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index e33a7e7e5..b26a40147 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -955,6 +955,18 @@ pub enum ExtensionIdError { InvalidExtensionId, } +impl fmt::Display for ExtensionIdError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidExtensionId => formatter.write_str( + "extension identifier must be exactly 32 lowercase characters from a through p", + ), + } + } +} + +impl std::error::Error for ExtensionIdError {} + /// An OriginWeave Agent capability that a browser extension may request explicitly. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ExtensionAgentCapability { diff --git a/crates/originweave-core/tests/extension_error_contract.rs b/crates/originweave-core/tests/extension_error_contract.rs new file mode 100644 index 000000000..ba6e19914 --- /dev/null +++ b/crates/originweave-core/tests/extension_error_contract.rs @@ -0,0 +1,21 @@ +use std::error::Error; + +use originweave_core::{ExtensionId, ExtensionIdError}; + +fn assert_standard_error() {} + +#[test] +fn extension_id_error_exposes_a_stable_standard_error_contract() { + assert_standard_error::(); + + let result = ExtensionId::parse("invalid"); + assert_eq!(result, Err(ExtensionIdError::InvalidExtensionId)); + + let rendered = result.as_ref().err().map(ToString::to_string); + assert_eq!( + rendered.as_deref(), + Some("extension identifier must be exactly 32 lowercase characters from a through p") + ); + let source = result.as_ref().err().and_then(|error| error.source()); + assert!(source.is_none()); +}