diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b93a51..a27c6ab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ ### Added +- `warp-core` optic invocation admission now has a narrow RuntimeSupport v0 + boundary. After BasisResolution v0, ApertureResolution v0, and + BudgetResolution v0 all resolve, Echo checks runtime-owned support facts for + the registered requirements digest. Without that Echo-owned support fact, + admission still obstructs at `RuntimeSupportUnavailable`; with the exact + `runtime-support:resolved-fixture:v0` support fixture, the ladder advances to + `InvocationAdmissionUnavailable`. Runtime support fixture recording is scoped + through Echo-issued artifact handles, so unknown handles cannot publish + support facts, repeated recordings for the same requirements digest do not + duplicate support facts, and registration rejects artifacts whose stored + requirements digest does not match the artifact requirements digest. This does + not add caller-supplied runtime support testimony, admission tickets, law + witnesses, scheduler work, execution, budget reservation, or successful grant + validation. - `dispatch_optic_intent(...)` and the default `KernelPort` optic dispatch path now reject EINT payloads that use Echo's reserved scheduler/control op id, closing the remaining application-facing control-intent ingress path. diff --git a/crates/warp-core/src/causal_facts.rs b/crates/warp-core/src/causal_facts.rs index d547ae69..a9d42308 100644 --- a/crates/warp-core/src/causal_facts.rs +++ b/crates/warp-core/src/causal_facts.rs @@ -63,6 +63,9 @@ pub enum InvocationObstructionKind { /// Echo could not prove that its runtime support surface covers the /// registered artifact requirements. RuntimeSupportUnavailable, + /// Echo proved runtime support, but this slice still has no lawful + /// invocation admission path. + InvocationAdmissionUnavailable, /// Invocation supplied no capability presentation. MissingCapability, /// Invocation supplied a malformed capability presentation. @@ -108,6 +111,7 @@ impl InvocationObstructionKind { Self::UnsupportedApertureResolution => b"unsupported-aperture-resolution", Self::UnsupportedBudgetResolution => b"unsupported-budget-resolution", Self::RuntimeSupportUnavailable => b"runtime-support-unavailable", + Self::InvocationAdmissionUnavailable => b"invocation-admission-unavailable", Self::MissingCapability => b"missing-capability", Self::MalformedCapabilityPresentation => b"malformed-capability-presentation", Self::UnboundCapabilityPresentation => b"unbound-capability-presentation", @@ -166,6 +170,14 @@ pub enum GraphFact { /// Structured obstruction kind. obstruction: ArtifactRegistrationObstructionKind, }, + /// Echo recorded runtime-owned support evidence for registered artifact + /// requirements. + RuntimeSupportRecorded { + /// Registered requirements digest covered by the runtime support fact. + requirements_digest: String, + /// Digest of the Echo-owned runtime support material. + support_digest: [u8; 32], + }, /// Echo refused optic invocation before admission success. OpticInvocationObstructed { /// Echo-owned runtime-local artifact handle id named by the invocation. @@ -247,6 +259,18 @@ impl GraphFact { ); push_digest_field(&mut bytes, b"obstruction", obstruction.digest_label()); } + Self::RuntimeSupportRecorded { + requirements_digest, + support_digest, + } => { + push_digest_field(&mut bytes, b"variant", b"runtime-support-recorded"); + push_digest_field( + &mut bytes, + b"requirements-digest", + requirements_digest.as_bytes(), + ); + push_digest_field(&mut bytes, b"support-digest", support_digest); + } Self::OpticInvocationObstructed { artifact_handle_id, operation_id, diff --git a/crates/warp-core/src/optic_artifact.rs b/crates/warp-core/src/optic_artifact.rs index eb488a9a..c8840b21 100644 --- a/crates/warp-core/src/optic_artifact.rs +++ b/crates/warp-core/src/optic_artifact.rs @@ -599,6 +599,9 @@ pub struct OpticInvocation { const OPTIC_BASIS_RESOLUTION_V0_FIXTURE_BYTES: &[u8] = b"basis-request:resolved-fixture:v0"; const OPTIC_APERTURE_RESOLUTION_V0_FIXTURE_BYTES: &[u8] = b"aperture-request:resolved-fixture:v0"; const OPTIC_BUDGET_RESOLUTION_V0_FIXTURE_BYTES: &[u8] = b"budget-request:resolved-fixture:v0"; +const OPTIC_RUNTIME_SUPPORT_V0_FIXTURE_BYTES: &[u8] = b"runtime-support:resolved-fixture:v0"; +const OPTIC_RUNTIME_SUPPORT_V0_FIXTURE_DIGEST_DOMAIN: &[u8] = + b"echo.optic-runtime-support.fixture.v0"; /// Admission obstruction for an optic invocation. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -626,6 +629,9 @@ pub enum OpticInvocationObstruction { /// Echo cannot prove that this runtime supports the registered artifact /// requirements in this slice. RuntimeSupportUnavailable, + /// Echo proved runtime support, but this slice still cannot lawfully admit + /// the invocation. + InvocationAdmissionUnavailable, /// The invocation does not carry authority to use the registered artifact. MissingCapability, /// The invocation carries a presentation that is structurally unusable. @@ -1021,11 +1027,19 @@ fn push_optional_receipt_field(bytes: &mut Vec, field: Option<&[u8]>) { } } +fn runtime_support_v0_fixture_digest() -> [u8; 32] { + digest_invocation_request_bytes( + OPTIC_RUNTIME_SUPPORT_V0_FIXTURE_DIGEST_DOMAIN, + OPTIC_RUNTIME_SUPPORT_V0_FIXTURE_BYTES, + ) +} + /// Echo-owned runtime-local registry for Wesley-compiled optic artifacts. #[derive(Clone, Debug, Default)] pub struct OpticArtifactRegistry { next_handle_index: u64, artifacts_by_handle: BTreeMap, + runtime_support_v0_by_requirements: BTreeMap, published_graph_facts: Vec, artifact_registration_receipts: Vec, } @@ -1080,6 +1094,43 @@ impl OpticArtifactRegistry { Ok(handle) } + /// Records Echo-owned RuntimeSupport v0 fixture evidence for a registered + /// artifact's requirements digest. + /// + /// This is runtime context, not invocation context. Callers cannot provide + /// a support request through [`OpticInvocation`]; the admission ladder only + /// consults facts recorded on this registry. + /// + /// # Errors + /// + /// Returns [`OpticArtifactRegistrationError::UnknownHandle`] if Echo did not + /// issue the handle in this registry instance. + pub fn record_runtime_support_v0_fixture_for_artifact( + &mut self, + handle: &OpticArtifactHandle, + ) -> Result<(), OpticArtifactRegistrationError> { + let requirements_digest = self + .resolve_optic_artifact_handle(handle)? + .requirements_digest + .clone(); + self.record_runtime_support_v0_fixture_for_requirements_digest(requirements_digest); + Ok(()) + } + + fn record_runtime_support_v0_fixture_for_requirements_digest( + &mut self, + requirements_digest: String, + ) { + let support_digest = runtime_support_v0_fixture_digest(); + if self + .runtime_support_v0_by_requirements + .insert(requirements_digest.clone(), support_digest) + .is_none() + { + self.publish_runtime_support_recorded_fact(requirements_digest, support_digest); + } + } + /// Resolves an opaque Echo handle to registered artifact metadata. /// /// # Errors @@ -1192,8 +1243,15 @@ impl OpticArtifactRegistry { .unwrap_or_else(|| { Self::resolve_aperture_v0(&invocation.aperture_request).unwrap_or_else( || { - Self::resolve_budget_v0(&invocation.budget_request).unwrap_or( - OpticInvocationObstruction::RuntimeSupportUnavailable, + Self::resolve_budget_v0(&invocation.budget_request).unwrap_or_else( + || { + self.resolve_runtime_support_v0_for_requirements( + ®istered.requirements_digest, + ) + .unwrap_or( + OpticInvocationObstruction::InvocationAdmissionUnavailable, + ) + }, ) }, ) @@ -1242,6 +1300,20 @@ impl OpticArtifactRegistry { Some(OpticInvocationObstruction::UnsupportedBudgetResolution) } + fn resolve_runtime_support_v0_for_requirements( + &self, + requirements_digest: &str, + ) -> Option { + if self + .runtime_support_v0_by_requirements + .contains_key(requirements_digest) + { + return None; + } + + Some(OpticInvocationObstruction::RuntimeSupportUnavailable) + } + fn classify_aperture_request( aperture_request: &OpticApertureRequest, ) -> Option { @@ -1341,6 +1413,9 @@ impl OpticArtifactRegistry { if descriptor.requirements_digest != artifact.requirements_digest { return Err(OpticArtifactRegistrationError::RequirementsDigestMismatch); } + if artifact.requirements.digest != artifact.requirements_digest { + return Err(OpticArtifactRegistrationError::RequirementsDigestMismatch); + } if descriptor.operation_id != artifact.operation.operation_id { return Err(OpticArtifactRegistrationError::OperationIdMismatch); } @@ -1402,6 +1477,19 @@ impl OpticArtifactRegistry { } } + fn publish_runtime_support_recorded_fact( + &mut self, + requirements_digest: String, + support_digest: [u8; 32], + ) { + self.published_graph_facts.push(PublishedGraphFact::new( + GraphFact::RuntimeSupportRecorded { + requirements_digest, + support_digest, + }, + )); + } + fn publish_invocation_obstruction_fact( &mut self, invocation: &OpticInvocation, @@ -1482,6 +1570,9 @@ fn invocation_obstruction_kind( OpticInvocationObstruction::RuntimeSupportUnavailable => { InvocationObstructionKind::RuntimeSupportUnavailable } + OpticInvocationObstruction::InvocationAdmissionUnavailable => { + InvocationObstructionKind::InvocationAdmissionUnavailable + } OpticInvocationObstruction::MissingCapability => { InvocationObstructionKind::MissingCapability } diff --git a/crates/warp-core/tests/causal_fact_publication_tests.rs b/crates/warp-core/tests/causal_fact_publication_tests.rs index 5718ad83..008666a8 100644 --- a/crates/warp-core/tests/causal_fact_publication_tests.rs +++ b/crates/warp-core/tests/causal_fact_publication_tests.rs @@ -4,8 +4,8 @@ use warp_core::{ ArtifactRegistrationObstructionKind, GraphFact, OpticAdmissionRequirements, OpticArtifact, - OpticArtifactOperation, OpticArtifactRegistrationError, OpticArtifactRegistry, - OpticRegistrationDescriptor, ARTIFACT_REGISTRATION_RECEIPT_KIND, + OpticArtifactHandle, OpticArtifactOperation, OpticArtifactRegistrationError, + OpticArtifactRegistry, OpticRegistrationDescriptor, ARTIFACT_REGISTRATION_RECEIPT_KIND, }; fn fixture_artifact() -> OpticArtifact { @@ -112,6 +112,90 @@ fn artifact_registration_obstruction_publishes_graph_fact_without_receipt() -> R Ok(()) } +#[test] +fn runtime_support_v0_fixture_publishes_graph_fact_without_registration_receipt( +) -> Result<(), 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:?}"))?; + + registry + .record_runtime_support_v0_fixture_for_artifact(&handle) + .map_err(|err| format!("registered handle should record runtime support: {err:?}"))?; + + assert_eq!(registry.published_graph_facts().len(), 2); + assert_eq!(registry.artifact_registration_receipts().len(), 1); + let published = ®istry.published_graph_facts()[1]; + assert_eq!(published.digest, published.fact.digest()); + assert!(matches!( + &published.fact, + GraphFact::RuntimeSupportRecorded { + requirements_digest, + support_digest, + } if requirements_digest == "requirements-digest:stack-witness-0001" + && *support_digest != [0_u8; 32] + )); + Ok(()) +} + +#[test] +fn runtime_support_v0_fixture_publishes_once_per_requirements_digest() -> Result<(), String> { + let mut sibling_artifact = fixture_artifact(); + sibling_artifact.artifact_id = "optic-artifact:stack-witness-0002".to_owned(); + sibling_artifact.artifact_hash = "artifact-hash:stack-witness-0002".to_owned(); + let mut sibling_descriptor = fixture_descriptor(); + sibling_descriptor.artifact_id = sibling_artifact.artifact_id.clone(); + sibling_descriptor.artifact_hash = sibling_artifact.artifact_hash.clone(); + let mut registry = OpticArtifactRegistry::new(); + let first_handle = registry + .register_optic_artifact(fixture_artifact(), fixture_descriptor()) + .map_err(|err| format!("fixture descriptor should register: {err:?}"))?; + let second_handle = registry + .register_optic_artifact(sibling_artifact, sibling_descriptor) + .map_err(|err| format!("sibling descriptor should register: {err:?}"))?; + + registry + .record_runtime_support_v0_fixture_for_artifact(&first_handle) + .map_err(|err| format!("first handle should record runtime support: {err:?}"))?; + registry + .record_runtime_support_v0_fixture_for_artifact(&second_handle) + .map_err(|err| format!("second handle should record runtime support: {err:?}"))?; + + let support_fact_count = registry + .published_graph_facts() + .iter() + .filter(|published| matches!(published.fact, GraphFact::RuntimeSupportRecorded { .. })) + .count(); + assert_eq!(support_fact_count, 1); + Ok(()) +} + +#[test] +fn runtime_support_v0_fixture_rejects_unknown_handle_without_graph_fact() -> Result<(), String> { + let mut registry = OpticArtifactRegistry::new(); + registry + .register_optic_artifact(fixture_artifact(), fixture_descriptor()) + .map_err(|err| format!("fixture descriptor should register: {err:?}"))?; + let unknown_handle = OpticArtifactHandle { + kind: "optic-artifact-handle".to_owned(), + id: "unregistered-handle".to_owned(), + }; + + let err = registration_err_or_panic( + registry.record_runtime_support_v0_fixture_for_artifact(&unknown_handle), + "unknown artifact handle should reject runtime support recording", + )?; + + assert!(matches!(err, OpticArtifactRegistrationError::UnknownHandle)); + assert_eq!(registry.published_graph_facts().len(), 1); + assert!(!registry + .published_graph_facts() + .iter() + .any(|published| matches!(published.fact, GraphFact::RuntimeSupportRecorded { .. }))); + Ok(()) +} + #[test] fn graph_fact_digest_is_deterministic_and_kind_separated() { let registered = GraphFact::ArtifactRegistered { @@ -126,9 +210,17 @@ fn graph_fact_digest_is_deterministic_and_kind_separated() { artifact_hash: Some("same-artifact".to_owned()), obstruction: ArtifactRegistrationObstructionKind::ArtifactHashMismatch, }; + let support = GraphFact::RuntimeSupportRecorded { + requirements_digest: "requirements".to_owned(), + support_digest: [7_u8; 32], + }; + let repeated_support = support.clone(); assert_eq!(registered.digest(), repeated.digest()); assert_ne!(registered.digest(), obstructed.digest()); + assert_eq!(support.digest(), repeated_support.digest()); + assert_ne!(registered.digest(), support.digest()); + assert_ne!(obstructed.digest(), support.digest()); } #[test] diff --git a/crates/warp-core/tests/optic_artifact_registry_tests.rs b/crates/warp-core/tests/optic_artifact_registry_tests.rs index 945e6465..9255b57a 100644 --- a/crates/warp-core/tests/optic_artifact_registry_tests.rs +++ b/crates/warp-core/tests/optic_artifact_registry_tests.rs @@ -3,8 +3,9 @@ //! Regression tests for Echo-owned optic artifact registration. use warp_core::{ - OpticAdmissionRequirements, OpticArtifact, OpticArtifactHandle, OpticArtifactOperation, - OpticArtifactRegistrationError, OpticArtifactRegistry, OpticRegistrationDescriptor, + GraphFact, OpticAdmissionRequirements, OpticArtifact, OpticArtifactHandle, + OpticArtifactOperation, OpticArtifactRegistrationError, OpticArtifactRegistry, + OpticRegistrationDescriptor, }; fn fixture_artifact() -> OpticArtifact { @@ -127,6 +128,30 @@ fn optic_artifact_registry_rejects_tampered_requirements_digest() -> Result<(), Ok(()) } +#[test] +fn optic_artifact_registry_rejects_mismatched_stored_requirements_digest() -> Result<(), String> { + let mut artifact = fixture_artifact(); + artifact.requirements.digest = "requirements-digest:stored-mismatch".to_owned(); + let mut registry = OpticArtifactRegistry::new(); + + let err = registration_err_or_panic( + registry.register_optic_artifact(artifact, fixture_descriptor()), + "stored requirements digest mismatch should reject", + )?; + + assert!(matches!( + err, + OpticArtifactRegistrationError::RequirementsDigestMismatch + )); + assert_eq!(registry.len(), 0); + assert!(registry.artifact_registration_receipts().is_empty()); + assert!(registry + .published_graph_facts() + .iter() + .all(|published| !matches!(published.fact, GraphFact::RuntimeSupportRecorded { .. }))); + Ok(()) +} + #[test] fn optic_artifact_registry_rejects_mismatched_operation_id() -> Result<(), String> { let artifact = fixture_artifact(); diff --git a/crates/warp-core/tests/optic_invocation_admission_tests.rs b/crates/warp-core/tests/optic_invocation_admission_tests.rs index 4618fbe6..1afe4856 100644 --- a/crates/warp-core/tests/optic_invocation_admission_tests.rs +++ b/crates/warp-core/tests/optic_invocation_admission_tests.rs @@ -791,8 +791,7 @@ fn budget_obstruction_fact_digest_is_deterministic() { } #[test] -fn runtime_support_unavailable_is_defined_but_unreachable_until_basis_resolution_exists( -) -> Result<(), String> { +fn runtime_support_unavailable_is_unreachable_when_basis_resolution_fails() -> Result<(), String> { let (mut registry, handle) = fixture_registry_and_handle()?; let invocation = fixture_invocation_with_presentation(handle, "grant:covered"); let mut gate = fixture_gate_with_grant(fixture_grant("grant:covered")); @@ -1028,7 +1027,7 @@ fn budget_resolution_is_unreachable_when_aperture_resolution_fails() -> Result<( } #[test] -fn resolved_budget_still_obstructs_before_runtime_support() -> Result<(), String> { +fn resolved_budget_still_requires_echo_owned_runtime_support() -> Result<(), String> { let (mut registry, handle) = fixture_registry_and_handle()?; let invocation = fixture_invocation_with_resolved_basis_aperture_budget_and_presentation( handle, @@ -1057,6 +1056,157 @@ fn resolved_budget_still_obstructs_before_runtime_support() -> Result<(), String Ok(()) } +#[test] +fn runtime_support_is_checked_only_after_budget_resolution() -> Result<(), String> { + let (mut registry, handle) = fixture_registry_and_handle()?; + registry + .record_runtime_support_v0_fixture_for_artifact(&handle) + .map_err(|err| format!("registered handle should record runtime support: {err:?}"))?; + let mut gate = fixture_gate_with_grant(fixture_grant("grant:covered")); + + let mut unsupported_basis = + fixture_invocation_with_resolved_basis_aperture_budget_and_presentation( + handle.clone(), + "grant:covered", + ); + unsupported_basis.basis_request = OpticBasisRequest { + bytes: b"basis-request:unsupported".to_vec(), + }; + let outcome = + registry.admit_optic_invocation_with_capability_validator(&unsupported_basis, &mut gate); + assert_eq!( + obstruction_for(&outcome), + OpticInvocationObstruction::UnsupportedBasisResolution + ); + + let mut unsupported_aperture = + fixture_invocation_with_resolved_basis_aperture_budget_and_presentation( + handle.clone(), + "grant:covered", + ); + unsupported_aperture.aperture_request = OpticApertureRequest { + bytes: b"aperture-request:unsupported".to_vec(), + }; + let outcome = + registry.admit_optic_invocation_with_capability_validator(&unsupported_aperture, &mut gate); + assert_eq!( + obstruction_for(&outcome), + OpticInvocationObstruction::UnsupportedApertureResolution + ); + + let mut unsupported_budget = + fixture_invocation_with_resolved_basis_aperture_budget_and_presentation( + handle, + "grant:covered", + ); + unsupported_budget.budget_request = OpticBudgetRequest { + bytes: b"budget-request:unsupported".to_vec(), + }; + let outcome = + registry.admit_optic_invocation_with_capability_validator(&unsupported_budget, &mut gate); + assert_eq!( + obstruction_for(&outcome), + OpticInvocationObstruction::UnsupportedBudgetResolution + ); + Ok(()) +} + +#[test] +fn runtime_support_v0_resolves_only_echo_owned_fixture() -> Result<(), String> { + let (mut registry, handle) = fixture_registry_and_handle()?; + let invocation = fixture_invocation_with_resolved_basis_aperture_budget_and_presentation( + handle, + "grant:covered", + ); + let mut gate = fixture_gate_with_grant(fixture_grant("grant:covered")); + + let outcome = registry.admit_optic_invocation_with_capability_validator(&invocation, &mut gate); + assert_eq!( + obstruction_for(&outcome), + OpticInvocationObstruction::RuntimeSupportUnavailable + ); + + registry + .record_runtime_support_v0_fixture_for_artifact(&invocation.artifact_handle) + .map_err(|err| format!("registered handle should record runtime support: {err:?}"))?; + let outcome = registry.admit_optic_invocation_with_capability_validator(&invocation, &mut gate); + assert_eq!( + obstruction_for(&outcome), + OpticInvocationObstruction::InvocationAdmissionUnavailable + ); + Ok(()) +} + +#[test] +fn caller_cannot_supply_runtime_support_testimony() -> Result<(), String> { + let (mut registry, handle) = fixture_registry_and_handle()?; + let mut invocation = fixture_invocation_with_resolved_basis_aperture_budget_and_presentation( + handle, + "grant:covered", + ); + invocation.canonical_variables_digest = b"runtime-support:resolved-fixture:v0".to_vec(); + invocation.capability_presentation = Some(OpticCapabilityPresentation { + presentation_id: "runtime-support:resolved-fixture:v0".to_owned(), + bound_grant_id: Some("grant:covered".to_owned()), + }); + let mut gate = fixture_gate_with_grant(fixture_grant("grant:covered")); + + let outcome = registry.admit_optic_invocation_with_capability_validator(&invocation, &mut gate); + assert_eq!( + obstruction_for(&outcome), + OpticInvocationObstruction::RuntimeSupportUnavailable + ); + + registry + .record_runtime_support_v0_fixture_for_artifact(&invocation.artifact_handle) + .map_err(|err| format!("registered handle should record runtime support: {err:?}"))?; + let outcome = registry.admit_optic_invocation_with_capability_validator(&invocation, &mut gate); + assert_eq!( + obstruction_for(&outcome), + OpticInvocationObstruction::InvocationAdmissionUnavailable + ); + Ok(()) +} + +#[test] +fn resolved_runtime_support_still_does_not_admit_invocation() -> Result<(), String> { + let (mut registry, handle) = fixture_registry_and_handle()?; + registry + .record_runtime_support_v0_fixture_for_artifact(&handle) + .map_err(|err| format!("registered handle should record runtime support: {err:?}"))?; + let invocation = fixture_invocation_with_resolved_basis_aperture_budget_and_presentation( + handle, + "grant:covered", + ); + let mut gate = fixture_gate_with_grant(fixture_grant("grant:covered")); + + let outcome = registry.admit_optic_invocation_with_capability_validator(&invocation, &mut gate); + + assert!(matches!( + outcome, + OpticInvocationAdmissionOutcome::Obstructed(OpticAdmissionTicketPosture { + obstruction: OpticInvocationObstruction::InvocationAdmissionUnavailable, + .. + }) + )); + assert!(matches!( + latest_invocation_obstruction_fact(®istry)?, + GraphFact::OpticInvocationObstructed { + obstruction, + .. + } if *obstruction == InvocationObstructionKind::InvocationAdmissionUnavailable + )); + assert!(registry.published_graph_facts().iter().all(|published| { + matches!( + published.fact, + GraphFact::ArtifactRegistered { .. } + | GraphFact::RuntimeSupportRecorded { .. } + | GraphFact::OpticInvocationObstructed { .. } + ) + })); + Ok(()) +} + #[test] fn invocation_obstruction_fact_is_not_counterfactual_candidate() -> Result<(), String> { let (mut registry, handle) = fixture_registry_and_handle()?; diff --git a/docs/design/optic-admission-ladder-checkpoint.md b/docs/design/optic-admission-ladder-checkpoint.md index 577b8383..80e382c0 100644 --- a/docs/design/optic-admission-ladder-checkpoint.md +++ b/docs/design/optic-admission-ladder-checkpoint.md @@ -3,13 +3,14 @@ # Optic Admission Ladder Checkpoint -Status: BudgetResolution v0 boundary checkpoint. -Scope: refusal ladder with narrow controlled basis, aperture, and budget fixtures. +Status: RuntimeSupport v0 boundary checkpoint. +Scope: refusal ladder with narrow controlled basis, aperture, budget, and +runtime-support fixtures. ## Doctrine This checkpoint records the optic invocation admission ladder at the first -controlled aperture-resolution boundary. +controlled runtime-support boundary. Echo can now explain why an optic invocation is refused, but it cannot yet admit one. There is no successful admission path in this checkpoint. @@ -22,7 +23,9 @@ the narrow deterministic ApertureResolution v0 fixture after basis resolution. Budget resolution exists only for the narrow deterministic BudgetResolution v0 fixture after aperture resolution. A resolved aperture is not permission to act. A budget request is not spendable -runtime capacity. +runtime capacity. Runtime support is Echo-owned context recorded by the +registry; it is not caller-provided testimony. Resolved runtime support is not +permission to act. Refusal is causal evidence. Refusal is not admission, not execution, not a law witness, and not a counterfactual candidate. @@ -43,17 +46,30 @@ The current optic invocation admission path evaluates checks in this order: narrow BasisResolution v0 fixture or obstruct unsupported basis material. 10. If that basis fixture resolves, resolve the narrow ApertureResolution v0 fixture or obstruct unsupported aperture material. -11. If that aperture fixture resolves, obstruct before budget resolution. -12. Publish the invocation obstruction fact. +11. If that aperture fixture resolves, resolve the narrow BudgetResolution v0 + fixture or obstruct unsupported budget material. +12. If that budget fixture resolves, check Echo-owned RuntimeSupport v0 facts + for the registered requirements or obstruct at `RuntimeSupportUnavailable`. +13. If RuntimeSupport v0 resolves, obstruct at + `InvocationAdmissionUnavailable`. +14. Publish the invocation obstruction fact. Presence checks come before resolution checks. Basis resolution gates aperture resolution. Aperture resolution gates budget evaluation and runtime support -checks. The current resolved fixture shapes are: +checks. The current invocation request fixture shapes are: - BasisResolution v0: `basis-request:resolved-fixture:v0` - ApertureResolution v0: `aperture-request:resolved-fixture:v0` - BudgetResolution v0: `budget-request:resolved-fixture:v0` +The current Echo-owned runtime support fixture is +`runtime-support:resolved-fixture:v0`. It is recorded by the runtime registry +through an Echo-issued artifact handle for that artifact's registered +requirements. Artifact registration requires the stored requirements digest to +match the registered artifact requirements digest. Recording the fixture is +idempotent per requirements digest, and runtime support is not carried by +`OpticInvocation`. + ## Obstruction reachability | Obstruction | Reachability | Meaning | @@ -70,11 +86,16 @@ checks. The current resolved fixture shapes are: | `UnsupportedBasisResolution` | Reachable today | Identity-covered material reaches the basis boundary, but the basis shape is outside BasisResolution v0. | | `UnsupportedApertureResolution` | Reachable today | BasisResolution v0 succeeded, but the aperture shape is outside ApertureResolution v0. | | `UnsupportedBudgetResolution` | Reachable today | ApertureResolution v0 succeeded, but the budget shape is outside BudgetResolution v0. | -| `RuntimeSupportUnavailable` | Reachable today | BudgetResolution v0 succeeded, but runtime support evaluation does not exist yet. | +| `RuntimeSupportUnavailable` | Reachable today | BudgetResolution v0 succeeded, but Echo has no runtime support fact for the registered requirements. | +| `InvocationAdmissionUnavailable` | Reachable today | RuntimeSupport v0 succeeded, but invocation admission does not exist yet. | `RuntimeSupportUnavailable` is lawfully reachable after BasisResolution v0, -ApertureResolution v0, and BudgetResolution v0 all resolve. It is the current -terminal refusal before runtime support evaluation exists. +ApertureResolution v0, and BudgetResolution v0 all resolve when Echo has no +runtime-owned support fact for the registered requirements. + +`InvocationAdmissionUnavailable` is lawfully reachable after RuntimeSupport v0 +resolves. It is the current terminal refusal after Echo proves support but +before successful invocation admission exists. `UnsupportedApertureResolution` is reachable only after the exact BasisResolution v0 fixture resolves. For identity-covered material, unsupported @@ -97,7 +118,8 @@ This checkpoint does not introduce: - WASM behavior - Continuum behavior - authority success -- runtime support enforcement +- caller-supplied runtime support testimony +- general runtime support enforcement - budget reservation The system remains obstruction-first. It records refusal; it does not authorize @@ -140,16 +162,31 @@ budget-request:resolved-fixture:v0 Budget resolution establishes a bounded resource envelope under consideration. It does not create permission to act, reserve spendable capacity, validate a -grant, or admit an invocation. The only lawful next refusal in this slice is -`RuntimeSupportUnavailable`. +grant, or admit an invocation. The next boundary is RuntimeSupport v0: absent +Echo-owned support obstructs at `RuntimeSupportUnavailable`; resolved support +advances to `InvocationAdmissionUnavailable`. + +## RuntimeSupport v0 + +RuntimeSupport v0 is not general runtime support. It recognizes exactly one +Echo-owned fixture for registered requirements: + +```text +runtime-support:resolved-fixture:v0 +``` + +Runtime support establishes only that Echo has recorded runtime-owned support +evidence for the artifact handle's registered requirements digest. It is not an +invocation request field, not caller testimony, not authority, not admission, +not scheduler work, and not execution. If runtime support resolves, the only +lawful next refusal in this slice is `InvocationAdmissionUnavailable`. ## Next transition point -The next transition point is RuntimeSupport v0. +The next transition point is InvocationAdmission v0. That transition must be narrow and explicit. It must not imply successful -admission, budget spendability, runtime support, execution, or authority -validation. +execution, scheduler work, or unconstrained authority validation. ## Tripwire @@ -160,4 +197,7 @@ If a future slice introduces a successful admission path before a resolved basis resolved aperture, evaluated budget, runtime support check, and validated grant exist, the admission ladder is wrong. -BudgetResolution v0 is controlled resolved state, not admission. +If a future slice makes `InvocationAdmissionUnavailable` reachable before a +lawful Echo-owned runtime support fact exists, the admission ladder is wrong. + +RuntimeSupport v0 is controlled resolved runtime context, not admission.