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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@

### Added

- `warp-core` optic invocation admission can now route bound capability
presentations through a narrow `CapabilityPresentationValidator` to publish
sharper grant-validation obstruction facts while preserving conservative
`CapabilityValidationUnavailable` invocation refusal. Identity coverage still
does not issue an admission ticket, law witness, scheduler work, or execution.
- `docs/design/invocation-grant-validation-obstruction-routing.md` defines the
validator routing boundary: validation evidence refines refusal, but it does
not create authority.
- `warp-core` now publishes `GraphFact::CapabilityGrantValidationObstructed`
when recorded capability grant material fails narrow identity coverage against
a registered optic artifact. The validation checks artifact hash, operation
Expand Down
8 changes: 4 additions & 4 deletions crates/warp-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,10 @@ pub use optic_artifact::{
CapabilityGrantIdentityCoverage, CapabilityGrantIntent, CapabilityGrantIntentGate,
CapabilityGrantIntentObstruction, CapabilityGrantIntentOutcome, CapabilityGrantIntentPosture,
CapabilityGrantValidationObstruction, CapabilityGrantValidationOutcome,
CapabilityGrantValidationPosture, ObstructionReceipt, OpticAdmissionRequirements,
OpticAdmissionTicketPosture, OpticApertureRequest, OpticArtifact, OpticArtifactHandle,
OpticArtifactOperation, OpticArtifactRegistrationError, OpticArtifactRegistry,
OpticBasisRequest, OpticCapabilityPresentation, OpticInvocation,
CapabilityGrantValidationPosture, CapabilityPresentationValidator, ObstructionReceipt,
OpticAdmissionRequirements, OpticAdmissionTicketPosture, OpticApertureRequest, OpticArtifact,
OpticArtifactHandle, OpticArtifactOperation, OpticArtifactRegistrationError,
OpticArtifactRegistry, OpticBasisRequest, OpticCapabilityPresentation, OpticInvocation,
OpticInvocationAdmissionOutcome, OpticInvocationObstruction, OpticRegistrationDescriptor,
PrincipalRef, RegisteredOpticArtifact, RewriteDisposition,
CAPABILITY_GRANT_VALIDATION_POSTURE_KIND, OBSTRUCTION_RECEIPT_KIND,
Expand Down
82 changes: 79 additions & 3 deletions crates/warp-core/src/optic_artifact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,9 @@ pub struct OpticCapabilityPresentation {
/// Grant id the presentation claims to bind to.
///
/// [`OpticArtifactRegistry::admit_optic_invocation`] does not validate this
/// grant. A non-empty value only moves the presentation from unbound to
/// validation-unavailable obstruction inside invocation admission; it never
/// authorizes invocation.
/// grant. The validator-aware invocation path may validate it only to
/// publish sharper refusal evidence. A non-empty value never authorizes
/// invocation in this slice.
pub bound_grant_id: Option<String>,
}

Expand Down Expand Up @@ -549,6 +549,24 @@ pub enum CapabilityGrantValidationOutcome {
Obstructed(CapabilityGrantValidationPosture),
}

/// Narrow validator surface for capability presentations used during optic
/// invocation refusal.
///
/// Validation evidence refines refusal; it does not create authority. A
/// validator may publish graph facts or other causal evidence, but
/// [`CapabilityGrantValidationOutcome::IdentityCovered`] still is not
/// invocation admission.
pub trait CapabilityPresentationValidator {
/// Validates a capability presentation against a registered artifact and
/// invocation context.
fn validate_capability_presentation(
&mut self,
registered: &RegisteredOpticArtifact,
invocation: &OpticInvocation,
presentation: &OpticCapabilityPresentation,
) -> CapabilityGrantValidationOutcome;
}

/// Runtime invocation request against a registered optic artifact.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OpticInvocation {
Expand Down Expand Up @@ -936,6 +954,21 @@ impl CapabilityGrantIntentGate {
}
}

impl CapabilityPresentationValidator for CapabilityGrantIntentGate {
fn validate_capability_presentation(
&mut self,
registered: &RegisteredOpticArtifact,
_invocation: &OpticInvocation,
presentation: &OpticCapabilityPresentation,
) -> CapabilityGrantValidationOutcome {
self.validate_capability_presentation_for_artifact(
presentation,
registered,
CapabilityGrantExpiryPosture::NotEvaluated,
)
}
}

fn push_receipt_field(bytes: &mut Vec<u8>, field: &[u8]) {
bytes.extend_from_slice(&(field.len() as u64).to_be_bytes());
bytes.extend_from_slice(field);
Expand Down Expand Up @@ -1054,6 +1087,49 @@ impl OpticArtifactRegistry {
)
}

/// Admits or obstructs an invocation while asking a capability presentation
/// validator for refusal evidence.
///
/// This remains an obstructed-only path. Validator evidence can publish a
/// sharper graph fact, but identity coverage is not invocation admission and
/// does not issue a success ticket, law witness, scheduler work, or
/// execution.
#[must_use = "optic invocation admission outcomes carry obstructions that must be handled"]
pub fn admit_optic_invocation_with_capability_validator(
&mut self,
invocation: &OpticInvocation,
validator: &mut impl CapabilityPresentationValidator,
) -> OpticInvocationAdmissionOutcome {
let registered = match self.resolve_optic_artifact_handle(&invocation.artifact_handle) {
Ok(registered) => registered,
Err(_) => {
Comment thread
flyingrobots marked this conversation as resolved.
return self
.obstructed_invocation(invocation, OpticInvocationObstruction::UnknownHandle);
}
};

if invocation.operation_id != registered.operation_id {
return self
.obstructed_invocation(invocation, OpticInvocationObstruction::OperationMismatch);
}

let obstruction =
Self::classify_capability_presentation(invocation.capability_presentation.as_ref());
if obstruction != OpticInvocationObstruction::CapabilityValidationUnavailable {
return self.obstructed_invocation(invocation, obstruction);
}

if let Some(presentation) = invocation.capability_presentation.as_ref() {
let _ =
validator.validate_capability_presentation(registered, invocation, presentation);
}

self.obstructed_invocation(
invocation,
OpticInvocationObstruction::CapabilityValidationUnavailable,
)
}

fn classify_capability_presentation(
presentation: Option<&OpticCapabilityPresentation>,
) -> OpticInvocationObstruction {
Expand Down
205 changes: 199 additions & 6 deletions crates/warp-core/tests/optic_invocation_admission_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
//! Regression tests for optic invocation admission obstruction.

use warp_core::{
digest_invocation_request_bytes, GraphFact, InvocationObstructionKind,
OpticAdmissionRequirements, OpticAdmissionTicketPosture, OpticApertureRequest, OpticArtifact,
OpticArtifactHandle, OpticArtifactOperation, OpticArtifactRegistry, OpticBasisRequest,
OpticCapabilityPresentation, OpticInvocation, OpticInvocationAdmissionOutcome,
OpticInvocationObstruction, OpticRegistrationDescriptor, RewriteDisposition,
OPTIC_ADMISSION_TICKET_POSTURE_KIND,
digest_invocation_request_bytes, AuthorityContext, AuthorityPolicy, AuthorityPolicyEvaluation,
CapabilityGrantIntent, CapabilityGrantIntentGate, CapabilityGrantValidationObstructionKind,
GraphFact, InvocationObstructionKind, OpticAdmissionRequirements, OpticAdmissionTicketPosture,
OpticApertureRequest, OpticArtifact, OpticArtifactHandle, OpticArtifactOperation,
OpticArtifactRegistry, OpticBasisRequest, OpticCapabilityPresentation, OpticInvocation,
OpticInvocationAdmissionOutcome, OpticInvocationObstruction, OpticRegistrationDescriptor,
PrincipalRef, RewriteDisposition, OPTIC_ADMISSION_TICKET_POSTURE_KIND,
};

fn fixture_artifact() -> OpticArtifact {
Expand Down Expand Up @@ -46,6 +47,41 @@ fn fixture_registry_and_handle() -> Result<(OpticArtifactRegistry, OpticArtifact
Ok((registry, handle))
}

fn principal(id: &str) -> PrincipalRef {
PrincipalRef { id: id.to_owned() }
}

fn fixture_grant(grant_id: &str) -> CapabilityGrantIntent {
CapabilityGrantIntent {
intent_id: grant_id.to_owned(),
proposed_by: principal("principal:issuer"),
subject: principal("principal:jedit-session"),
artifact_hash: "artifact-hash:stack-witness-0001".to_owned(),
operation_id: "operation:textWindow:v0".to_owned(),
requirements_digest: "requirements-digest:stack-witness-0001".to_owned(),
rights: vec!["optic.invoke".to_owned()],
scope_bytes: b"scope:fixture".to_vec(),
expiry_bytes: Some(b"expiry:fixture".to_vec()),
delegation_basis_bytes: Some(b"delegation-basis:fixture".to_vec()),
}
}

fn fixture_authority_context() -> AuthorityContext {
AuthorityContext {
issuer: Some(principal("principal:issuer")),
policy: Some(AuthorityPolicy {
policy_id: "authority-policy:fixture".to_owned(),
}),
policy_evaluation: AuthorityPolicyEvaluation::Unsupported,
}
}

fn fixture_gate_with_grant(grant: CapabilityGrantIntent) -> CapabilityGrantIntentGate {
let mut gate = CapabilityGrantIntentGate::new();
let _ = gate.submit_grant_intent(grant, fixture_authority_context());
gate
}

fn fixture_invocation(handle: OpticArtifactHandle) -> OpticInvocation {
OpticInvocation {
artifact_handle: handle,
Expand Down Expand Up @@ -240,6 +276,27 @@ fn latest_invocation_obstruction_fact(
.ok_or_else(|| "expected invocation obstruction graph fact".to_owned())
}

fn latest_validation_obstruction_fact(
gate: &CapabilityGrantIntentGate,
) -> Result<&GraphFact, String> {
gate.published_graph_facts()
.last()
.map(|published| &published.fact)
.ok_or_else(|| "expected capability grant validation obstruction fact".to_owned())
}

fn fixture_invocation_with_presentation(
handle: OpticArtifactHandle,
grant_id: &str,
) -> OpticInvocation {
let mut invocation = fixture_invocation(handle);
invocation.capability_presentation = Some(OpticCapabilityPresentation {
presentation_id: "presentation:fixture".to_owned(),
bound_grant_id: Some(grant_id.to_owned()),
});
invocation
}

#[test]
fn unknown_handle_publishes_invocation_obstruction_fact() -> Result<(), String> {
let mut registry = OpticArtifactRegistry::new();
Expand Down Expand Up @@ -270,6 +327,142 @@ fn unknown_handle_publishes_invocation_obstruction_fact() -> Result<(), String>
Ok(())
}

#[test]
fn invocation_with_unknown_grant_publishes_grant_validation_obstruction_fact() -> Result<(), String>
{
let (mut registry, handle) = fixture_registry_and_handle()?;
let invocation = fixture_invocation_with_presentation(handle, "grant:unknown");
let mut gate = CapabilityGrantIntentGate::new();

let outcome = registry.admit_optic_invocation_with_capability_validator(&invocation, &mut gate);

assert_eq!(
obstruction_for(&outcome),
OpticInvocationObstruction::CapabilityValidationUnavailable
);
assert!(matches!(
latest_invocation_obstruction_fact(&registry)?,
GraphFact::OpticInvocationObstructed {
obstruction,
..
} if *obstruction == InvocationObstructionKind::CapabilityValidationUnavailable
));
assert!(matches!(
latest_validation_obstruction_fact(&gate)?,
GraphFact::CapabilityGrantValidationObstructed {
grant_id,
obstruction,
..
} if grant_id.as_deref() == Some("grant:unknown")
&& *obstruction == CapabilityGrantValidationObstructionKind::UnknownGrant
));
Ok(())
}

#[test]
fn invocation_with_artifact_hash_mismatch_publishes_grant_validation_obstruction_fact(
) -> Result<(), String> {
let (mut registry, handle) = fixture_registry_and_handle()?;
let invocation = fixture_invocation_with_presentation(handle, "grant:artifact-mismatch");
let mut grant = fixture_grant("grant:artifact-mismatch");
grant.artifact_hash = "artifact-hash:other".to_owned();
let mut gate = fixture_gate_with_grant(grant);

let outcome = registry.admit_optic_invocation_with_capability_validator(&invocation, &mut gate);

assert_eq!(
obstruction_for(&outcome),
OpticInvocationObstruction::CapabilityValidationUnavailable
);
assert!(matches!(
latest_validation_obstruction_fact(&gate)?,
GraphFact::CapabilityGrantValidationObstructed {
grant_artifact_hash,
obstruction,
..
} if grant_artifact_hash.as_deref() == Some("artifact-hash:other")
&& *obstruction == CapabilityGrantValidationObstructionKind::ArtifactHashMismatch
));
Ok(())
}

#[test]
fn invocation_with_operation_id_mismatch_publishes_grant_validation_obstruction_fact(
) -> Result<(), String> {
let (mut registry, handle) = fixture_registry_and_handle()?;
let invocation = fixture_invocation_with_presentation(handle, "grant:operation-mismatch");
let mut grant = fixture_grant("grant:operation-mismatch");
grant.operation_id = "operation:replaceRange:v0".to_owned();
let mut gate = fixture_gate_with_grant(grant);

let outcome = registry.admit_optic_invocation_with_capability_validator(&invocation, &mut gate);

assert_eq!(
obstruction_for(&outcome),
OpticInvocationObstruction::CapabilityValidationUnavailable
);
assert!(matches!(
latest_validation_obstruction_fact(&gate)?,
GraphFact::CapabilityGrantValidationObstructed {
grant_operation_id,
obstruction,
..
} if grant_operation_id.as_deref() == Some("operation:replaceRange:v0")
&& *obstruction == CapabilityGrantValidationObstructionKind::OperationIdMismatch
));
Ok(())
}

#[test]
fn invocation_with_requirements_digest_mismatch_publishes_grant_validation_obstruction_fact(
) -> Result<(), String> {
let (mut registry, handle) = fixture_registry_and_handle()?;
let invocation = fixture_invocation_with_presentation(handle, "grant:requirements-mismatch");
let mut grant = fixture_grant("grant:requirements-mismatch");
grant.requirements_digest = "requirements-digest:other".to_owned();
let mut gate = fixture_gate_with_grant(grant);

let outcome = registry.admit_optic_invocation_with_capability_validator(&invocation, &mut gate);

assert_eq!(
obstruction_for(&outcome),
OpticInvocationObstruction::CapabilityValidationUnavailable
);
assert!(matches!(
latest_validation_obstruction_fact(&gate)?,
GraphFact::CapabilityGrantValidationObstructed {
grant_requirements_digest,
obstruction,
..
} if grant_requirements_digest.as_deref() == Some("requirements-digest:other")
&& *obstruction == CapabilityGrantValidationObstructionKind::RequirementsDigestMismatch
));
Ok(())
}

#[test]
fn identity_covered_grant_still_does_not_admit_invocation() -> 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"));

let outcome = registry.admit_optic_invocation_with_capability_validator(&invocation, &mut gate);

assert_eq!(
obstruction_for(&outcome),
OpticInvocationObstruction::CapabilityValidationUnavailable
);
assert!(gate.published_graph_facts().is_empty());
assert!(matches!(
latest_invocation_obstruction_fact(&registry)?,
GraphFact::OpticInvocationObstructed {
obstruction,
..
} if *obstruction == InvocationObstructionKind::CapabilityValidationUnavailable
));
Ok(())
}

#[test]
fn operation_mismatch_publishes_invocation_obstruction_fact() -> Result<(), String> {
let (mut registry, handle) = fixture_registry_and_handle()?;
Expand Down
Loading
Loading