diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7380c3b8..21ca95a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs
index dcbdd9ed..26542c68 100644
--- a/crates/warp-core/src/lib.rs
+++ b/crates/warp-core/src/lib.rs
@@ -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,
diff --git a/crates/warp-core/src/optic_artifact.rs b/crates/warp-core/src/optic_artifact.rs
index f563b1d8..a894568f 100644
--- a/crates/warp-core/src/optic_artifact.rs
+++ b/crates/warp-core/src/optic_artifact.rs
@@ -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>,
 }
 
@@ -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 {
@@ -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);
@@ -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(_) => {
+                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 {
diff --git a/crates/warp-core/tests/optic_invocation_admission_tests.rs b/crates/warp-core/tests/optic_invocation_admission_tests.rs
index a2fb1b92..bf509a25 100644
--- a/crates/warp-core/tests/optic_invocation_admission_tests.rs
+++ b/crates/warp-core/tests/optic_invocation_admission_tests.rs
@@ -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 {
@@ -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,
@@ -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();
@@ -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()?;
diff --git a/docs/design/invocation-grant-validation-obstruction-routing.md b/docs/design/invocation-grant-validation-obstruction-routing.md
new file mode 100644
index 00000000..120fdd59
--- /dev/null
+++ b/docs/design/invocation-grant-validation-obstruction-routing.md
@@ -0,0 +1,130 @@
+<!-- SPDX-License-Identifier: Apache-2.0 OR LicenseRef-MIND-UCAL-1.0 -->
+<!-- © James Ross Ω FLYING•ROBOTS <https://github.com/flyingrobots> -->
+
+# Invocation Grant Validation Obstruction Routing
+
+Status: implementation slice.
+Scope: validator-routed refusal evidence for optic invocation admission.
+
+## Doctrine
+
+Validation evidence refines refusal; it does not create authority.
+
+An invocation may carry a bound capability presentation. Echo may ask a narrow
+validator to inspect that presentation against the registered artifact identity.
+The validator may publish sharper graph evidence such as
+`GraphFact::CapabilityGrantValidationObstructed`, but invocation admission still
+returns the conservative obstruction:
+
+```text
+OpticInvocationObstruction::CapabilityValidationUnavailable
+```
+
+Identity coverage is not admission. It only says recorded grant material names
+the same artifact hash, operation id, and requirements digest as the registered
+artifact. It does not issue an admission ticket, law witness, scheduler work, or
+execution.
+
+Failed presentation validation is causal refusal evidence, not a
+counterfactual.
+
+## Flow
+
+```mermaid
+flowchart TD
+  Invocation[OpticInvocation]
+  Registry[OpticArtifactRegistry]
+  Artifact[RegisteredOpticArtifact]
+  Presentation[OpticCapabilityPresentation]
+  Validator[CapabilityPresentationValidator]
+  Shape{presentation structurally available?}
+  ValidationFact[GraphFact::CapabilityGrantValidationObstructed]
+  InvocationFact[GraphFact::OpticInvocationObstructed]
+  Posture[OpticAdmissionTicketPosture]
+
+  Invocation --> Registry
+  Registry --> Artifact
+  Invocation --> Presentation
+  Presentation --> Shape
+  Registry --> Shape
+  Shape -->|yes| Validator
+  Shape -->|no| InvocationFact
+  Artifact -->|yes| Validator
+  Validator -->|failed coverage| ValidationFact
+  Validator -->|after validation| Registry
+  Registry --> InvocationFact
+  Registry --> Posture
+```
+
+## Sequence
+
+```mermaid
+sequenceDiagram
+  participant Caller as caller
+  participant Registry as OpticArtifactRegistry
+  participant Validator as CapabilityPresentationValidator
+  participant Facts as graph fact log
+
+  Caller->>Registry: admit_optic_invocation_with_capability_validator(invocation, validator)
+  Registry->>Registry: resolve artifact handle
+  Registry->>Registry: check operation id
+  Registry->>Registry: classify presentation shape
+  alt presentation structurally available
+    Registry->>Validator: validate_capability_presentation(artifact, invocation, presentation)
+    Validator->>Facts: publish CapabilityGrantValidationObstructed when validation fails
+  else missing, malformed, or unbound presentation
+    Registry->>Registry: skip validator
+  end
+  Registry->>Facts: publish OpticInvocationObstructed
+  Registry-->>Caller: Obstructed(CapabilityValidationUnavailable or structural obstruction)
+```
+
+## Class diagram
+
+```mermaid
+classDiagram
+  class OpticArtifactRegistry {
+    +admit_optic_invocation(invocation)
+    +admit_optic_invocation_with_capability_validator(invocation, validator)
+  }
+
+  class CapabilityPresentationValidator {
+    <<trait>>
+    +validate_capability_presentation(artifact, invocation, presentation)
+  }
+
+  class CapabilityGrantIntentGate {
+    +validate_capability_presentation_for_artifact(presentation, artifact, expiry_posture)
+  }
+
+  class CapabilityGrantValidationOutcome {
+    IdentityCovered
+    Obstructed
+  }
+
+  OpticArtifactRegistry --> CapabilityPresentationValidator
+  CapabilityGrantIntentGate ..|> CapabilityPresentationValidator
+  CapabilityPresentationValidator --> CapabilityGrantValidationOutcome
+```
+
+## Operating rule
+
+The validator is evidence infrastructure, not an authority oracle.
+
+`CapabilityGrantValidationOutcome::IdentityCovered` must not be treated as
+accepted authority. Until Echo has accepted grant material, admission tickets,
+and law witnesses, invocation admission remains obstructed even when validation
+finds identity coverage.
+
+## Non-goals
+
+- no successful invocation admission;
+- no successful `AdmissionTicket`;
+- no `LawWitness`;
+- no accepted grant material;
+- no delegation validation;
+- no expiry parsing;
+- no scheduler work;
+- no execution;
+- no WASM ABI;
+- no Continuum schema.
