diff --git a/CHANGELOG.md b/CHANGELOG.md index 73025fd4..1deaa4b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,13 @@ artifact hash, schema id, operation id, and requirements digest before storing admission requirements internally and returning an opaque `OpticArtifactHandle`. +- `warp-core` now has an optic invocation admission skeleton that resolves + registered artifact handles internally and obstructs unknown handles, + operation mismatches, and registered-handle invocations without capability + presentation. Admission outcomes are must-use, and placeholder capability + presentations still obstruct until real grant validation exists. The + registration and invocation regression fixtures avoid `expect(...)` so + all-target Clippy remains clean. - Echo-owned WASM package boundary tooling: `scripts/build-warp-wasm-package.sh` now builds `crates/warp-wasm/pkg` with the bundler target and the package export smoke test imports `crates/warp-wasm/pkg/rmg_wasm.js` to verify the diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 87e051f3..c92b7d90 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -248,8 +248,10 @@ pub use optic::{ StagedIntent, StagedIntentReason, WitnessBasis, WorldlineHeadOptic, }; pub use optic_artifact::{ - OpticAdmissionRequirements, OpticArtifact, OpticArtifactHandle, OpticArtifactOperation, - OpticArtifactRegistrationError, OpticArtifactRegistry, OpticRegistrationDescriptor, + OpticAdmissionRequirements, OpticApertureRequest, OpticArtifact, OpticArtifactHandle, + OpticArtifactOperation, OpticArtifactRegistrationError, OpticArtifactRegistry, + OpticBasisRequest, OpticCapabilityPresentation, OpticInvocation, + OpticInvocationAdmissionOutcome, OpticInvocationObstruction, OpticRegistrationDescriptor, RegisteredOpticArtifact, OPTIC_ARTIFACT_HANDLE_KIND, }; pub use playback::{CursorReceipt, TruthFrame, TruthSink}; diff --git a/crates/warp-core/src/optic_artifact.rs b/crates/warp-core/src/optic_artifact.rs index 02e34565..add0a7a7 100644 --- a/crates/warp-core/src/optic_artifact.rs +++ b/crates/warp-core/src/optic_artifact.rs @@ -2,10 +2,11 @@ // © James Ross Ω FLYING•ROBOTS //! Echo-owned registry for Wesley-compiled optic artifacts. //! -//! This module intentionally stops at registration. It does not admit -//! invocations, validate capability grants, issue admission tickets, or execute -//! runtime work. The only authority proven here is that Echo accepted and -//! stored a specific Wesley artifact identity and its admission requirements. +//! This module owns optic artifact registration and the first admission-only +//! invocation gate. [`OpticArtifactRegistry::admit_optic_invocation`] resolves +//! handles internally, checks operation identity, and obstructs missing or +//! placeholder authority without validating grants, issuing admission tickets, +//! emitting law witnesses, or executing runtime work. use std::collections::BTreeMap; @@ -103,6 +104,70 @@ pub struct RegisteredOpticArtifact { pub requirements: OpticAdmissionRequirements, } +/// Opaque basis request bytes supplied at optic invocation time. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OpticBasisRequest { + /// Request bytes interpreted only below Echo's runtime admission boundary. + pub bytes: Vec, +} + +/// Opaque aperture request bytes supplied at optic invocation time. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OpticApertureRequest { + /// Request bytes interpreted only below Echo's runtime admission boundary. + pub bytes: Vec, +} + +/// Placeholder capability presentation supplied at optic invocation time. +/// +/// This v0 shape is intentionally not sufficient to authorize invocation. It +/// exists only so the admission skeleton can name the future presentation slot +/// without inventing grant validation semantics. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OpticCapabilityPresentation { + /// Presentation identity supplied by the caller. + pub presentation_id: String, +} + +/// Runtime invocation request against a registered optic artifact. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OpticInvocation { + /// Echo-owned runtime-local artifact handle. + pub artifact_handle: OpticArtifactHandle, + /// Operation id the caller intends to invoke. + pub operation_id: String, + /// Digest of canonical invocation variable bytes. + pub canonical_variables_digest: Vec, + /// Requested causal basis for the invocation. + pub basis_request: OpticBasisRequest, + /// Requested aperture for the invocation. + pub aperture_request: OpticApertureRequest, + /// Caller authority presentation. Registration alone is not authority. + pub capability_presentation: Option, +} + +/// Admission obstruction for an optic invocation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum OpticInvocationObstruction { + /// Echo did not issue or cannot resolve the artifact handle. + UnknownHandle, + /// The invocation operation id does not match the registered artifact. + OperationMismatch, + /// The invocation does not carry authority to use the registered artifact. + MissingCapability, + /// A placeholder presentation was supplied, but real grant validation does + /// not exist in this slice. + CapabilityValidationUnavailable, +} + +/// Admission outcome for a v0 optic invocation skeleton. +#[must_use = "optic invocation admission outcomes carry obstructions that must be handled"] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum OpticInvocationAdmissionOutcome { + /// Echo obstructed the invocation before runtime execution. + Obstructed(OpticInvocationObstruction), +} + /// Registration and lookup errors for Echo optic artifact handles. #[derive(Clone, Debug, Error, PartialEq, Eq)] pub enum OpticArtifactRegistrationError { @@ -187,6 +252,38 @@ impl OpticArtifactRegistry { .ok_or(OpticArtifactRegistrationError::UnknownHandle) } + /// Admits or obstructs an invocation against a registered optic artifact. + /// + /// This v0 skeleton intentionally has no success path. It proves that Echo + /// resolves handles internally and that a registered handle is not authority. + #[must_use = "optic invocation admission outcomes carry obstructions that must be handled"] + pub fn admit_optic_invocation( + &self, + invocation: &OpticInvocation, + ) -> OpticInvocationAdmissionOutcome { + let Ok(registered) = self.resolve_optic_artifact_handle(&invocation.artifact_handle) else { + return OpticInvocationAdmissionOutcome::Obstructed( + OpticInvocationObstruction::UnknownHandle, + ); + }; + + if invocation.operation_id != registered.operation_id { + return OpticInvocationAdmissionOutcome::Obstructed( + OpticInvocationObstruction::OperationMismatch, + ); + } + + if invocation.capability_presentation.is_none() { + return OpticInvocationAdmissionOutcome::Obstructed( + OpticInvocationObstruction::MissingCapability, + ); + } + + OpticInvocationAdmissionOutcome::Obstructed( + OpticInvocationObstruction::CapabilityValidationUnavailable, + ) + } + /// Returns the number of registered artifacts. #[must_use] pub fn len(&self) -> usize { diff --git a/crates/warp-core/tests/optic_artifact_registry_tests.rs b/crates/warp-core/tests/optic_artifact_registry_tests.rs index 6eea72d7..27a8cb77 100644 --- a/crates/warp-core/tests/optic_artifact_registry_tests.rs +++ b/crates/warp-core/tests/optic_artifact_registry_tests.rs @@ -32,22 +32,32 @@ fn fixture_descriptor() -> OpticRegistrationDescriptor { } } +fn registration_err_or_panic( + result: Result, + context: &str, +) -> Result { + match result { + Ok(_) => Err(format!("{context}: expected registration error")), + Err(err) => Ok(err), + } +} + #[test] -fn optic_artifact_registry_registers_wesley_descriptor_and_resolves_handle() { +fn optic_artifact_registry_registers_wesley_descriptor_and_resolves_handle() -> Result<(), String> { let artifact = fixture_artifact(); let descriptor = fixture_descriptor(); let mut registry = OpticArtifactRegistry::new(); let handle = registry .register_optic_artifact(artifact.clone(), descriptor) - .expect("fixture descriptor should register"); + .map_err(|err| format!("fixture descriptor should register: {err:?}"))?; assert_eq!(handle.kind, "optic-artifact-handle"); assert!(!handle.id.is_empty()); let registered = registry .resolve_optic_artifact_handle(&handle) - .expect("fresh handle should resolve"); + .map_err(|err| format!("fresh handle should resolve: {err:?}"))?; assert_eq!(registered.artifact_id, artifact.artifact_id); assert_eq!(registered.artifact_hash, artifact.artifact_hash); @@ -55,104 +65,117 @@ fn optic_artifact_registry_registers_wesley_descriptor_and_resolves_handle() { assert_eq!(registered.operation_id, artifact.operation.operation_id); assert_eq!(registered.requirements_digest, artifact.requirements_digest); assert_eq!(registered.requirements, artifact.requirements); + Ok(()) } #[test] -fn optic_artifact_registry_rejects_tampered_artifact_hash() { +fn optic_artifact_registry_rejects_tampered_artifact_hash() -> Result<(), String> { let artifact = fixture_artifact(); let mut descriptor = fixture_descriptor(); descriptor.artifact_hash = "artifact-hash:tampered".to_owned(); let mut registry = OpticArtifactRegistry::new(); - let err = registry - .register_optic_artifact(artifact, descriptor) - .expect_err("tampered artifact hash should reject"); + let err = registration_err_or_panic( + registry.register_optic_artifact(artifact, descriptor), + "tampered artifact hash should reject", + )?; assert!(matches!( err, OpticArtifactRegistrationError::ArtifactHashMismatch )); + Ok(()) } #[test] -fn optic_artifact_registry_rejects_mismatched_artifact_id() { +fn optic_artifact_registry_rejects_mismatched_artifact_id() -> Result<(), String> { let artifact = fixture_artifact(); let mut descriptor = fixture_descriptor(); descriptor.artifact_id = "optic-artifact:other".to_owned(); let mut registry = OpticArtifactRegistry::new(); - let err = registry - .register_optic_artifact(artifact, descriptor) - .expect_err("mismatched artifact id should reject"); + let err = registration_err_or_panic( + registry.register_optic_artifact(artifact, descriptor), + "mismatched artifact id should reject", + )?; assert!(matches!( err, OpticArtifactRegistrationError::ArtifactIdMismatch )); + Ok(()) } #[test] -fn optic_artifact_registry_rejects_tampered_requirements_digest() { +fn optic_artifact_registry_rejects_tampered_requirements_digest() -> Result<(), String> { let artifact = fixture_artifact(); let mut descriptor = fixture_descriptor(); descriptor.requirements_digest = "requirements-digest:tampered".to_owned(); let mut registry = OpticArtifactRegistry::new(); - let err = registry - .register_optic_artifact(artifact, descriptor) - .expect_err("tampered requirements digest should reject"); + let err = registration_err_or_panic( + registry.register_optic_artifact(artifact, descriptor), + "tampered requirements digest should reject", + )?; assert!(matches!( err, OpticArtifactRegistrationError::RequirementsDigestMismatch )); + Ok(()) } #[test] -fn optic_artifact_registry_rejects_mismatched_operation_id() { +fn optic_artifact_registry_rejects_mismatched_operation_id() -> Result<(), String> { let artifact = fixture_artifact(); let mut descriptor = fixture_descriptor(); descriptor.operation_id = "operation:replaceRange:v0".to_owned(); let mut registry = OpticArtifactRegistry::new(); - let err = registry - .register_optic_artifact(artifact, descriptor) - .expect_err("mismatched operation id should reject"); + let err = registration_err_or_panic( + registry.register_optic_artifact(artifact, descriptor), + "mismatched operation id should reject", + )?; assert!(matches!( err, OpticArtifactRegistrationError::OperationIdMismatch )); + Ok(()) } #[test] -fn optic_artifact_registry_rejects_mismatched_schema_id() { +fn optic_artifact_registry_rejects_mismatched_schema_id() -> Result<(), String> { let artifact = fixture_artifact(); let mut descriptor = fixture_descriptor(); descriptor.schema_id = "schema:other:v0".to_owned(); let mut registry = OpticArtifactRegistry::new(); - let err = registry - .register_optic_artifact(artifact, descriptor) - .expect_err("mismatched schema id should reject"); + let err = registration_err_or_panic( + registry.register_optic_artifact(artifact, descriptor), + "mismatched schema id should reject", + )?; assert!(matches!( err, OpticArtifactRegistrationError::SchemaIdMismatch )); + Ok(()) } #[test] -fn optic_artifact_registry_rejects_unknown_handle_lookup() { +fn optic_artifact_registry_rejects_unknown_handle_lookup() -> Result<(), String> { let registry = OpticArtifactRegistry::new(); let handle = OpticArtifactHandle { kind: "optic-artifact-handle".to_owned(), id: "unregistered-handle".to_owned(), }; - let err = registry - .resolve_optic_artifact_handle(&handle) - .expect_err("unknown handle should reject"); + let err = registration_err_or_panic( + registry.resolve_optic_artifact_handle(&handle), + "unknown handle should reject", + )?; assert!(matches!(err, OpticArtifactRegistrationError::UnknownHandle)); + Ok(()) } diff --git a/crates/warp-core/tests/optic_invocation_admission_tests.rs b/crates/warp-core/tests/optic_invocation_admission_tests.rs new file mode 100644 index 00000000..0c4423c6 --- /dev/null +++ b/crates/warp-core/tests/optic_invocation_admission_tests.rs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Regression tests for optic invocation admission obstruction. + +use warp_core::{ + OpticAdmissionRequirements, OpticApertureRequest, OpticArtifact, OpticArtifactHandle, + OpticArtifactOperation, OpticArtifactRegistry, OpticBasisRequest, OpticCapabilityPresentation, + OpticInvocation, OpticInvocationAdmissionOutcome, OpticInvocationObstruction, + OpticRegistrationDescriptor, +}; + +fn fixture_artifact() -> OpticArtifact { + OpticArtifact { + artifact_id: "optic-artifact:stack-witness-0001".to_owned(), + artifact_hash: "artifact-hash:stack-witness-0001".to_owned(), + schema_id: "schema:jedit-text-buffer-optic:v0".to_owned(), + requirements_digest: "requirements-digest:stack-witness-0001".to_owned(), + operation: OpticArtifactOperation { + operation_id: "operation:textWindow:v0".to_owned(), + }, + requirements: OpticAdmissionRequirements { + bytes: b"fixture admission requirements".to_vec(), + }, + } +} + +fn fixture_descriptor() -> OpticRegistrationDescriptor { + OpticRegistrationDescriptor { + artifact_id: "optic-artifact:stack-witness-0001".to_owned(), + artifact_hash: "artifact-hash:stack-witness-0001".to_owned(), + schema_id: "schema:jedit-text-buffer-optic:v0".to_owned(), + operation_id: "operation:textWindow:v0".to_owned(), + requirements_digest: "requirements-digest:stack-witness-0001".to_owned(), + } +} + +fn fixture_registry_and_handle() -> Result<(OpticArtifactRegistry, OpticArtifactHandle), String> { + let mut registry = OpticArtifactRegistry::new(); + let handle = registry + .register_optic_artifact(fixture_artifact(), fixture_descriptor()) + .map_err(|err| format!("fixture descriptor should register: {err:?}"))?; + Ok((registry, handle)) +} + +fn fixture_invocation(handle: OpticArtifactHandle) -> OpticInvocation { + OpticInvocation { + artifact_handle: handle, + operation_id: "operation:textWindow:v0".to_owned(), + canonical_variables_digest: b"vars-digest:textWindow".to_vec(), + basis_request: OpticBasisRequest { + bytes: b"basis-request:fixture".to_vec(), + }, + aperture_request: OpticApertureRequest { + bytes: b"aperture-request:fixture".to_vec(), + }, + capability_presentation: None, + } +} + +#[test] +fn optic_invocation_obstructs_unknown_handle() { + let registry = OpticArtifactRegistry::new(); + let invocation = fixture_invocation(OpticArtifactHandle { + kind: "optic-artifact-handle".to_owned(), + id: "unregistered-handle".to_owned(), + }); + + let outcome = registry.admit_optic_invocation(&invocation); + + assert_eq!( + outcome, + OpticInvocationAdmissionOutcome::Obstructed(OpticInvocationObstruction::UnknownHandle) + ); +} + +#[test] +fn optic_invocation_obstructs_operation_mismatch() -> Result<(), String> { + let (registry, handle) = fixture_registry_and_handle()?; + let mut invocation = fixture_invocation(handle); + invocation.operation_id = "operation:replaceRange:v0".to_owned(); + + let outcome = registry.admit_optic_invocation(&invocation); + + assert_eq!( + outcome, + OpticInvocationAdmissionOutcome::Obstructed(OpticInvocationObstruction::OperationMismatch) + ); + Ok(()) +} + +#[test] +fn optic_invocation_obstructs_missing_capability_for_registered_handle() -> Result<(), String> { + let (registry, handle) = fixture_registry_and_handle()?; + let invocation = fixture_invocation(handle); + + let outcome = registry.admit_optic_invocation(&invocation); + + assert_eq!( + outcome, + OpticInvocationAdmissionOutcome::Obstructed(OpticInvocationObstruction::MissingCapability) + ); + Ok(()) +} + +#[test] +fn optic_invocation_obstructs_placeholder_capability_presentation_until_grant_validation_exists( +) -> Result<(), String> { + let (registry, handle) = fixture_registry_and_handle()?; + let mut invocation = fixture_invocation(handle); + invocation.capability_presentation = Some(OpticCapabilityPresentation { + presentation_id: "presentation:placeholder".to_owned(), + }); + + let outcome = registry.admit_optic_invocation(&invocation); + + assert_eq!( + outcome, + OpticInvocationAdmissionOutcome::Obstructed( + OpticInvocationObstruction::CapabilityValidationUnavailable + ) + ); + Ok(()) +}