diff --git a/CHANGELOG.md b/CHANGELOG.md index 4767d311..e8180d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,10 +28,11 @@ presentations all remain obstructed until real bounded grant validation exists. - `warp-core` now has an Echo-owned capability grant intent obstruction - skeleton. `CapabilityGrantIntentRegistry` records well-formed submitted - authority intents deterministically, obstructs malformed, duplicate, and - identity-incomplete grant intents, and keeps all submissions from becoming - authority until future witnessed grant admission exists. + skeleton. `CapabilityGrantIntentGate` records well-formed submitted authority + intents deterministically, obstructs malformed, missing-issuer, + invalid-delegation, scope-escalation, replay/duplicate, and unsupported-policy + grant intents, and keeps all submissions from becoming authority until future + witnessed grant admission exists. - 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 ac907396..d4ae77c4 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -248,14 +248,14 @@ pub use optic::{ StagedIntent, StagedIntentReason, WitnessBasis, WorldlineHeadOptic, }; pub use optic_artifact::{ - AuthorityContext, AuthorityPolicy, CapabilityGrantIntent, CapabilityGrantIntentGate, - CapabilityGrantIntentObstruction, CapabilityGrantIntentOutcome, CapabilityGrantIntentPosture, - OpticAdmissionRequirements, OpticAdmissionTicketPosture, OpticApertureRequest, OpticArtifact, - OpticArtifactHandle, OpticArtifactOperation, OpticArtifactRegistrationError, - OpticArtifactRegistry, OpticBasisRequest, OpticCapabilityPresentation, OpticInvocation, - OpticInvocationAdmissionOutcome, OpticInvocationObstruction, OpticRegistrationDescriptor, - PrincipalRef, RegisteredOpticArtifact, OPTIC_ADMISSION_TICKET_POSTURE_KIND, - OPTIC_ARTIFACT_HANDLE_KIND, + AuthorityContext, AuthorityPolicy, AuthorityPolicyEvaluation, CapabilityGrantIntent, + CapabilityGrantIntentGate, CapabilityGrantIntentObstruction, CapabilityGrantIntentOutcome, + CapabilityGrantIntentPosture, OpticAdmissionRequirements, OpticAdmissionTicketPosture, + OpticApertureRequest, OpticArtifact, OpticArtifactHandle, OpticArtifactOperation, + OpticArtifactRegistrationError, OpticArtifactRegistry, OpticBasisRequest, + OpticCapabilityPresentation, OpticInvocation, OpticInvocationAdmissionOutcome, + OpticInvocationObstruction, OpticRegistrationDescriptor, PrincipalRef, RegisteredOpticArtifact, + OPTIC_ADMISSION_TICKET_POSTURE_KIND, OPTIC_ARTIFACT_HANDLE_KIND, }; pub use playback::{CursorReceipt, TruthFrame, TruthSink}; pub use provenance_store::{ diff --git a/crates/warp-core/src/optic_artifact.rs b/crates/warp-core/src/optic_artifact.rs index ad7f1a5d..02693c51 100644 --- a/crates/warp-core/src/optic_artifact.rs +++ b/crates/warp-core/src/optic_artifact.rs @@ -157,6 +157,21 @@ pub struct AuthorityPolicy { pub policy_id: String, } +/// Obstruction-only authority policy evaluation posture. +/// +/// This is vocabulary, not governance. It lets Echo name policy failure +/// surfaces without accepting a grant intent, issuing a receipt, or treating a +/// policy shape as trusted authority. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AuthorityPolicyEvaluation { + /// The proposed delegation basis is not valid for the authority change. + InvalidDelegation, + /// The proposed grant would exceed the issuer's authority scope. + ScopeEscalation, + /// Echo does not have a supported authority policy implementation yet. + Unsupported, +} + /// Authority context supplied when proposing a capability grant intent. #[derive(Clone, Debug, PartialEq, Eq)] pub struct AuthorityContext { @@ -164,6 +179,8 @@ pub struct AuthorityContext { pub issuer: Option, /// Policy that should evaluate the issuer's authority. pub policy: Option, + /// Obstruction-only policy evaluation posture. + pub policy_evaluation: AuthorityPolicyEvaluation, } /// Causal authority intent submitted to Echo for future grant admission. @@ -203,8 +220,12 @@ pub enum CapabilityGrantIntentObstruction { MissingIssuerAuthority, /// The grant intent is structurally unusable. MalformedGrantIntent, + /// The proposed delegation basis is invalid. + InvalidDelegation, + /// The proposed scope would exceed the issuer's authority. + ScopeEscalation, /// Echo already saw a grant intent with the supplied intent id. - DuplicateGrantIntent, + ReplayOrDuplicateIntent, /// No real authority policy exists in this slice. UnsupportedAuthorityPolicy, } @@ -357,7 +378,7 @@ impl CapabilityGrantIntentGate { authority_context: AuthorityContext, ) -> CapabilityGrantIntentOutcome { let obstruction = self.classify_capability_grant_intent(&intent, &authority_context); - if obstruction == CapabilityGrantIntentObstruction::UnsupportedAuthorityPolicy { + if Self::records_submitted_intent(obstruction) { self.intents_by_id .insert(intent.intent_id.clone(), intent.clone()); } @@ -401,7 +422,7 @@ impl CapabilityGrantIntentGate { } if self.intents_by_id.contains_key(&intent.intent_id) { - return CapabilityGrantIntentObstruction::DuplicateGrantIntent; + return CapabilityGrantIntentObstruction::ReplayOrDuplicateIntent; } let Some(issuer) = &authority_context.issuer else { @@ -411,7 +432,33 @@ impl CapabilityGrantIntentGate { return CapabilityGrantIntentObstruction::MissingIssuerAuthority; } - CapabilityGrantIntentObstruction::UnsupportedAuthorityPolicy + let Some(policy) = &authority_context.policy else { + return CapabilityGrantIntentObstruction::UnsupportedAuthorityPolicy; + }; + if policy.policy_id.is_empty() { + return CapabilityGrantIntentObstruction::UnsupportedAuthorityPolicy; + } + + match authority_context.policy_evaluation { + AuthorityPolicyEvaluation::InvalidDelegation => { + CapabilityGrantIntentObstruction::InvalidDelegation + } + AuthorityPolicyEvaluation::ScopeEscalation => { + CapabilityGrantIntentObstruction::ScopeEscalation + } + AuthorityPolicyEvaluation::Unsupported => { + CapabilityGrantIntentObstruction::UnsupportedAuthorityPolicy + } + } + } + + fn records_submitted_intent(obstruction: CapabilityGrantIntentObstruction) -> bool { + matches!( + obstruction, + CapabilityGrantIntentObstruction::InvalidDelegation + | CapabilityGrantIntentObstruction::ScopeEscalation + | CapabilityGrantIntentObstruction::UnsupportedAuthorityPolicy + ) } fn obstructed_grant_intent( diff --git a/crates/warp-core/tests/capability_grant_intent_tests.rs b/crates/warp-core/tests/capability_grant_intent_tests.rs index 305e3324..cbc77453 100644 --- a/crates/warp-core/tests/capability_grant_intent_tests.rs +++ b/crates/warp-core/tests/capability_grant_intent_tests.rs @@ -3,9 +3,9 @@ //! Regression tests for Echo-owned capability grant intent obstruction. use warp_core::{ - AuthorityContext, AuthorityPolicy, CapabilityGrantIntent, CapabilityGrantIntentGate, - CapabilityGrantIntentObstruction, CapabilityGrantIntentOutcome, CapabilityGrantIntentPosture, - PrincipalRef, + AuthorityContext, AuthorityPolicy, AuthorityPolicyEvaluation, CapabilityGrantIntent, + CapabilityGrantIntentGate, CapabilityGrantIntentObstruction, CapabilityGrantIntentOutcome, + CapabilityGrantIntentPosture, PrincipalRef, }; fn principal(id: &str) -> PrincipalRef { @@ -33,6 +33,7 @@ fn fixture_authority_context() -> AuthorityContext { policy: Some(AuthorityPolicy { policy_id: "authority-policy:fixture".to_owned(), }), + policy_evaluation: AuthorityPolicyEvaluation::Unsupported, } } @@ -110,24 +111,24 @@ fn capability_grant_intent_obstructs_missing_required_identity_as_malformed() { } #[test] -fn capability_grant_intent_obstructs_duplicate_grant_intent() { +fn capability_grant_intent_obstructs_replay_or_duplicate_grant_intent() { let mut registry = CapabilityGrantIntentGate::new(); - let first_intent = fixture_intent("intent:duplicate"); - let duplicate_intent = fixture_intent("intent:duplicate"); + let first_intent = fixture_intent("intent:replay"); + let replay_intent = fixture_intent("intent:replay"); let first_outcome = registry.submit_grant_intent(first_intent, fixture_authority_context()); - let duplicate_outcome = - registry.submit_grant_intent(duplicate_intent.clone(), fixture_authority_context()); + let replay_outcome = + registry.submit_grant_intent(replay_intent.clone(), fixture_authority_context()); assert_eq!( obstruction_for(&first_outcome), CapabilityGrantIntentObstruction::UnsupportedAuthorityPolicy ); assert_eq!( - duplicate_outcome, + replay_outcome, expected_obstructed_posture( - &duplicate_intent, - CapabilityGrantIntentObstruction::DuplicateGrantIntent + &replay_intent, + CapabilityGrantIntentObstruction::ReplayOrDuplicateIntent ) ); } @@ -141,6 +142,7 @@ fn capability_grant_intent_obstructs_missing_issuer_authority() { policy: Some(AuthorityPolicy { policy_id: "authority-policy:fixture".to_owned(), }), + policy_evaluation: AuthorityPolicyEvaluation::Unsupported, }; let outcome = registry.submit_grant_intent(intent.clone(), authority_context); @@ -154,6 +156,69 @@ fn capability_grant_intent_obstructs_missing_issuer_authority() { ); } +#[test] +fn capability_grant_intent_obstructs_invalid_delegation() { + let mut registry = CapabilityGrantIntentGate::new(); + let intent = fixture_intent("intent:invalid-delegation"); + let authority_context = AuthorityContext { + issuer: Some(principal("principal:issuer")), + policy: Some(AuthorityPolicy { + policy_id: "authority-policy:fixture".to_owned(), + }), + policy_evaluation: AuthorityPolicyEvaluation::InvalidDelegation, + }; + + let outcome = registry.submit_grant_intent(intent.clone(), authority_context); + + assert_eq!( + outcome, + expected_obstructed_posture(&intent, CapabilityGrantIntentObstruction::InvalidDelegation) + ); +} + +#[test] +fn capability_grant_intent_obstructs_scope_escalation() { + let mut registry = CapabilityGrantIntentGate::new(); + let intent = fixture_intent("intent:scope-escalation"); + let authority_context = AuthorityContext { + issuer: Some(principal("principal:issuer")), + policy: Some(AuthorityPolicy { + policy_id: "authority-policy:fixture".to_owned(), + }), + policy_evaluation: AuthorityPolicyEvaluation::ScopeEscalation, + }; + + let outcome = registry.submit_grant_intent(intent.clone(), authority_context); + + assert_eq!( + outcome, + expected_obstructed_posture(&intent, CapabilityGrantIntentObstruction::ScopeEscalation) + ); +} + +#[test] +fn capability_grant_intent_obstructs_missing_policy_identity_as_unsupported_policy() { + let mut registry = CapabilityGrantIntentGate::new(); + let intent = fixture_intent("intent:missing-policy-identity"); + let authority_context = AuthorityContext { + issuer: Some(principal("principal:issuer")), + policy: Some(AuthorityPolicy { + policy_id: String::new(), + }), + policy_evaluation: AuthorityPolicyEvaluation::InvalidDelegation, + }; + + let outcome = registry.submit_grant_intent(intent.clone(), authority_context); + + assert_eq!( + outcome, + expected_obstructed_posture( + &intent, + CapabilityGrantIntentObstruction::UnsupportedAuthorityPolicy + ) + ); +} + #[test] fn capability_grant_intent_obstructs_unsupported_authority_policy() { let mut registry = CapabilityGrantIntentGate::new(); @@ -172,29 +237,64 @@ fn capability_grant_intent_obstructs_unsupported_authority_policy() { #[test] fn capability_grant_intent_never_makes_grant_authority() { - let mut registry = CapabilityGrantIntentGate::new(); - let intents = [ - fixture_intent("intent:malformed-empty-rights"), - fixture_intent("intent:missing-issuer"), - fixture_intent("intent:unsupported-policy"), - ]; - - let mut malformed = intents[0].clone(); + let mut malformed = fixture_intent("intent:malformed-empty-rights"); malformed.rights.clear(); + + let mut malformed_registry = CapabilityGrantIntentGate::new(); + let mut missing_issuer_registry = CapabilityGrantIntentGate::new(); + let mut invalid_delegation_registry = CapabilityGrantIntentGate::new(); + let mut scope_escalation_registry = CapabilityGrantIntentGate::new(); + let mut replay_registry = CapabilityGrantIntentGate::new(); + let mut unsupported_registry = CapabilityGrantIntentGate::new(); + + let missing_issuer = fixture_intent("intent:missing-issuer"); + let invalid_delegation = fixture_intent("intent:invalid-delegation-never-authority"); + let scope_escalation = fixture_intent("intent:scope-escalation-never-authority"); + let replay = fixture_intent("intent:replay-never-authority"); + let unsupported = fixture_intent("intent:unsupported-policy"); + + let replay_first_outcome = + replay_registry.submit_grant_intent(replay.clone(), fixture_authority_context()); let outcomes = [ - registry.submit_grant_intent(malformed, fixture_authority_context()), - registry.submit_grant_intent( - intents[1].clone(), + malformed_registry.submit_grant_intent(malformed, fixture_authority_context()), + missing_issuer_registry.submit_grant_intent( + missing_issuer, AuthorityContext { issuer: None, policy: Some(AuthorityPolicy { policy_id: "authority-policy:fixture".to_owned(), }), + policy_evaluation: AuthorityPolicyEvaluation::Unsupported, }, ), - registry.submit_grant_intent(intents[2].clone(), fixture_authority_context()), + invalid_delegation_registry.submit_grant_intent( + invalid_delegation, + AuthorityContext { + issuer: Some(principal("principal:issuer")), + policy: Some(AuthorityPolicy { + policy_id: "authority-policy:fixture".to_owned(), + }), + policy_evaluation: AuthorityPolicyEvaluation::InvalidDelegation, + }, + ), + scope_escalation_registry.submit_grant_intent( + scope_escalation, + AuthorityContext { + issuer: Some(principal("principal:issuer")), + policy: Some(AuthorityPolicy { + policy_id: "authority-policy:fixture".to_owned(), + }), + policy_evaluation: AuthorityPolicyEvaluation::ScopeEscalation, + }, + ), + replay_registry.submit_grant_intent(replay, fixture_authority_context()), + unsupported_registry.submit_grant_intent(unsupported, fixture_authority_context()), ]; + assert_eq!( + obstruction_for(&replay_first_outcome), + CapabilityGrantIntentObstruction::UnsupportedAuthorityPolicy + ); assert_eq!( obstruction_for(&outcomes[0]), CapabilityGrantIntentObstruction::MalformedGrantIntent @@ -205,6 +305,18 @@ fn capability_grant_intent_never_makes_grant_authority() { ); assert_eq!( obstruction_for(&outcomes[2]), + CapabilityGrantIntentObstruction::InvalidDelegation + ); + assert_eq!( + obstruction_for(&outcomes[3]), + CapabilityGrantIntentObstruction::ScopeEscalation + ); + assert_eq!( + obstruction_for(&outcomes[4]), + CapabilityGrantIntentObstruction::ReplayOrDuplicateIntent + ); + assert_eq!( + obstruction_for(&outcomes[5]), CapabilityGrantIntentObstruction::UnsupportedAuthorityPolicy ); } diff --git a/docs/design/optic-capability-grant-registry.md b/docs/design/optic-capability-grant-intent-boundary.md similarity index 86% rename from docs/design/optic-capability-grant-registry.md rename to docs/design/optic-capability-grant-intent-boundary.md index e78de15b..26369757 100644 --- a/docs/design/optic-capability-grant-registry.md +++ b/docs/design/optic-capability-grant-intent-boundary.md @@ -22,7 +22,8 @@ The ladder is: - registered handle is not authority; - presentation slot is not validated grant; - grant object is not admitted authority; -- grant intent is not accepted policy decision. +- grant intent is not accepted policy decision; +- policy shape is not trusted governance. ## System fit @@ -75,9 +76,10 @@ flowchart LR ## Grant intent sequence -The gate checks structure, duplicate/replay posture, issuer authority presence, -and policy support. Since no real policy exists in this slice, even a -well-formed intent with issuer context obstructs as `UnsupportedAuthorityPolicy`. +The gate checks structure, replay/duplicate posture, issuer authority presence, +policy identity, delegation posture, scope posture, and policy support. Since no +real policy exists in this slice, even a well-formed intent with issuer context +obstructs. ```mermaid sequenceDiagram @@ -91,12 +93,20 @@ sequenceDiagram alt malformed intent G-->>E: Obstructed(MalformedGrantIntent) E-->>P: not authority - else duplicate intent id - G-->>E: Obstructed(DuplicateGrantIntent) + else replay or duplicate intent id + G-->>E: Obstructed(ReplayOrDuplicateIntent) E-->>P: not authority else missing issuer authority G-->>E: Obstructed(MissingIssuerAuthority) E-->>P: not authority + else invalid delegation + G->>G: record submitted intent id for replay/duplicate obstruction + G-->>E: Obstructed(InvalidDelegation) + E-->>P: not authority + else scope escalation + G->>G: record submitted intent id for replay/duplicate obstruction + G-->>E: Obstructed(ScopeEscalation) + E-->>P: not authority else no supported policy exists G->>G: record submitted intent id for replay/duplicate obstruction G-->>E: Obstructed(UnsupportedAuthorityPolicy) @@ -142,9 +152,17 @@ classDiagram +policy_id } + class AuthorityPolicyEvaluation { + <> + InvalidDelegation + ScopeEscalation + Unsupported + } + class AuthorityContext { +issuer +policy + +policy_evaluation } class CapabilityGrantIntent { @@ -185,7 +203,9 @@ classDiagram <> MissingIssuerAuthority MalformedGrantIntent - DuplicateGrantIntent + InvalidDelegation + ScopeEscalation + ReplayOrDuplicateIntent UnsupportedAuthorityPolicy } @@ -198,6 +218,7 @@ classDiagram CapabilityGrantIntent --> PrincipalRef : subject AuthorityContext --> PrincipalRef : issuer AuthorityContext --> AuthorityPolicy : policy + AuthorityContext --> AuthorityPolicyEvaluation : classifies CapabilityGrantIntentGate --> CapabilityGrantIntent : records submitted CapabilityGrantIntentGate --> AuthorityContext : evaluates with CapabilityGrantIntentGate --> CapabilityGrantIntentOutcome : returns @@ -229,6 +250,7 @@ erDiagram AUTHORITY_CONTEXT { string issuer string policy_id + string policy_evaluation } OPTIC_ARTIFACT { @@ -284,7 +306,9 @@ The current `CapabilityGrantIntent` shape carries proposed authority material: - opaque expiry bytes; - opaque delegation-basis bytes. -`AuthorityContext` carries the issuer and selected policy shape. No policy is +`AuthorityContext` carries the issuer, selected policy shape, and +`policy_evaluation` posture used to classify obstruction vocabulary. The +evaluation field is policy-shaped evidence only; no trusted governance policy is implemented in this slice. ## This slice does @@ -294,8 +318,10 @@ implemented in this slice. - defines `CapabilityGrantIntent`; - defines `CapabilityGrantIntentPosture`; - classifies malformed grant intents; -- classifies duplicate grant intents; +- classifies replay/duplicate grant intents as `ReplayOrDuplicateIntent`; - classifies missing issuer authority; +- classifies invalid delegation; +- classifies scope escalation; - classifies unsupported authority policy; - records well-formed unique submitted intent ids deterministically; - keeps all grant intent submissions obstructed.