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

### Added

- `warp-core` now publishes `GraphFact::OpticInvocationObstructed` whenever the
optic invocation admission skeleton refuses an invocation. The fact records
the artifact handle id, operation id, canonical variables digest, basis and
aperture request digests, and structured obstruction kind without creating a
success admission ticket, law witness, execution, scheduler output, or
counterfactual candidate.
- `docs/design/invocation-obstruction-graph-facts.md` defines the invocation
refusal publication boundary: registered handles are not authority, and
invocation obstruction facts are causal refusal evidence rather than
counterfactual worlds.
- Local verification now maps `warp-core` optic artifact and causal fact source
changes to the exact integration test targets they exercise, avoiding broad
Cargo name-filter runs while preserving targeted smoke coverage.
Expand Down
84 changes: 84 additions & 0 deletions crates/warp-core/src/causal_facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,37 @@ pub enum ArtifactRegistrationObstructionKind {
RequirementsDigestMismatch,
}

/// Obstruction kind recorded when optic invocation admission refuses before a
/// success ticket, witness, scheduler selection, or execution boundary.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InvocationObstructionKind {
/// Echo did not issue the supplied artifact handle.
UnknownHandle,
/// Invocation operation id did not match the registered artifact operation.
OperationMismatch,
/// Invocation supplied no capability presentation.
MissingCapability,
/// Invocation supplied a malformed capability presentation.
MalformedCapabilityPresentation,
/// Invocation supplied a presentation not bound to a grant id.
UnboundCapabilityPresentation,
/// Invocation supplied a placeholder presentation before grant validation exists.
CapabilityValidationUnavailable,
}

impl InvocationObstructionKind {
fn digest_label(self) -> &'static [u8] {
match self {
Self::UnknownHandle => b"unknown-handle",
Self::OperationMismatch => b"operation-mismatch",
Self::MissingCapability => b"missing-capability",
Self::MalformedCapabilityPresentation => b"malformed-capability-presentation",
Self::UnboundCapabilityPresentation => b"unbound-capability-presentation",
Self::CapabilityValidationUnavailable => b"capability-validation-unavailable",
}
}
}

impl ArtifactRegistrationObstructionKind {
fn digest_label(self) -> &'static [u8] {
match self {
Expand Down Expand Up @@ -71,6 +102,21 @@ pub enum GraphFact {
/// Structured obstruction kind.
obstruction: ArtifactRegistrationObstructionKind,
},
/// Echo refused optic invocation before admission success.
OpticInvocationObstructed {
/// Echo-owned runtime-local artifact handle id named by the invocation.
artifact_handle_id: String,
/// Operation id named by the invocation.
operation_id: String,
/// Digest of the invocation's canonical variable bytes.
canonical_variables_digest: Vec<u8>,
/// Digest of the opaque basis request bytes.
basis_request_digest: [u8; 32],
/// Digest of the opaque aperture request bytes.
aperture_request_digest: [u8; 32],
/// Structured invocation obstruction kind.
obstruction: InvocationObstructionKind,
},
}

impl GraphFact {
Expand Down Expand Up @@ -111,12 +157,50 @@ impl GraphFact {
);
push_digest_field(&mut bytes, b"obstruction", obstruction.digest_label());
}
Self::OpticInvocationObstructed {
artifact_handle_id,
operation_id,
canonical_variables_digest,
basis_request_digest,
aperture_request_digest,
obstruction,
} => {
push_digest_field(&mut bytes, b"variant", b"optic-invocation-obstructed");
push_digest_field(
&mut bytes,
b"artifact-handle-id",
artifact_handle_id.as_bytes(),
);
push_digest_field(&mut bytes, b"operation-id", operation_id.as_bytes());
push_digest_field(
&mut bytes,
b"canonical-variables-digest",
canonical_variables_digest,
);
push_digest_field(&mut bytes, b"basis-request-digest", basis_request_digest);
push_digest_field(
&mut bytes,
b"aperture-request-digest",
aperture_request_digest,
);
push_digest_field(&mut bytes, b"obstruction", obstruction.digest_label());
}
}

FactDigest(*blake3::hash(&bytes).as_bytes())
}
}

/// Computes a deterministic digest for opaque invocation request bytes named
/// inside graph facts.
#[must_use]
pub fn digest_invocation_request_bytes(domain: &[u8], bytes: &[u8]) -> [u8; 32] {
let mut input = Vec::new();
push_digest_field(&mut input, b"domain", domain);
push_digest_field(&mut input, b"bytes", bytes);
*blake3::hash(&input).as_bytes()
}

/// Graph fact plus its deterministic digest.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PublishedGraphFact {
Expand Down
3 changes: 2 additions & 1 deletion crates/warp-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ pub use attachment::{
CodecRegistry, DecodeError, ErasedCodec, RegistryError,
};
pub use causal_facts::{
ArtifactRegistrationObstructionKind, ArtifactRegistrationReceipt, FactDigest, GraphFact,
digest_invocation_request_bytes, ArtifactRegistrationObstructionKind,
ArtifactRegistrationReceipt, FactDigest, GraphFact, InvocationObstructionKind,
PublishedGraphFact, ARTIFACT_REGISTRATION_RECEIPT_KIND,
};
pub use clock::{GlobalTick, RunId, WorldlineTick};
Expand Down
69 changes: 57 additions & 12 deletions crates/warp-core/src/optic_artifact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
use std::collections::BTreeMap;

use crate::{
ArtifactRegistrationObstructionKind, ArtifactRegistrationReceipt, GraphFact,
PublishedGraphFact, ARTIFACT_REGISTRATION_RECEIPT_KIND,
digest_invocation_request_bytes, ArtifactRegistrationObstructionKind,
ArtifactRegistrationReceipt, GraphFact, InvocationObstructionKind, PublishedGraphFact,
ARTIFACT_REGISTRATION_RECEIPT_KIND,
};
use thiserror::Error;

Expand Down Expand Up @@ -759,24 +760,20 @@ impl OpticArtifactRegistry {
/// and that obstruction is reported as a structured pre-ticket posture.
#[must_use = "optic invocation admission outcomes carry obstructions that must be handled"]
pub fn admit_optic_invocation(
&self,
&mut self,
invocation: &OpticInvocation,
) -> OpticInvocationAdmissionOutcome {
let Ok(registered) = self.resolve_optic_artifact_handle(&invocation.artifact_handle) else {
return Self::obstructed_invocation(
invocation,
OpticInvocationObstruction::UnknownHandle,
);
return self
.obstructed_invocation(invocation, OpticInvocationObstruction::UnknownHandle);
};

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

Self::obstructed_invocation(
self.obstructed_invocation(
invocation,
Self::classify_capability_presentation(invocation.capability_presentation.as_ref()),
)
Expand Down Expand Up @@ -806,9 +803,11 @@ impl OpticArtifactRegistry {
}

fn obstructed_invocation(
&mut self,
invocation: &OpticInvocation,
obstruction: OpticInvocationObstruction,
) -> OpticInvocationAdmissionOutcome {
self.publish_invocation_obstruction_fact(invocation, obstruction);
OpticInvocationAdmissionOutcome::Obstructed(OpticAdmissionTicketPosture {
kind: OPTIC_ADMISSION_TICKET_POSTURE_KIND.to_owned(),
artifact_handle: invocation.artifact_handle.clone(),
Expand Down Expand Up @@ -918,6 +917,29 @@ impl OpticArtifactRegistry {
));
}
}

fn publish_invocation_obstruction_fact(
&mut self,
invocation: &OpticInvocation,
obstruction: OpticInvocationObstruction,
) {
self.published_graph_facts.push(PublishedGraphFact::new(
GraphFact::OpticInvocationObstructed {
artifact_handle_id: invocation.artifact_handle.id.clone(),
operation_id: invocation.operation_id.clone(),
canonical_variables_digest: invocation.canonical_variables_digest.clone(),
basis_request_digest: digest_invocation_request_bytes(
b"echo.optic-invocation.basis-request.v0",
&invocation.basis_request.bytes,
),
aperture_request_digest: digest_invocation_request_bytes(
b"echo.optic-invocation.aperture-request.v0",
&invocation.aperture_request.bytes,
),
obstruction: invocation_obstruction_kind(obstruction),
},
));
}
}

fn artifact_registration_obstruction_kind(
Expand All @@ -942,3 +964,26 @@ fn artifact_registration_obstruction_kind(
OpticArtifactRegistrationError::UnknownHandle => None,
}
}

fn invocation_obstruction_kind(
obstruction: OpticInvocationObstruction,
) -> InvocationObstructionKind {
match obstruction {
OpticInvocationObstruction::UnknownHandle => InvocationObstructionKind::UnknownHandle,
OpticInvocationObstruction::OperationMismatch => {
InvocationObstructionKind::OperationMismatch
}
OpticInvocationObstruction::MissingCapability => {
InvocationObstructionKind::MissingCapability
}
OpticInvocationObstruction::MalformedCapabilityPresentation => {
InvocationObstructionKind::MalformedCapabilityPresentation
}
OpticInvocationObstruction::UnboundCapabilityPresentation => {
InvocationObstructionKind::UnboundCapabilityPresentation
}
OpticInvocationObstruction::CapabilityValidationUnavailable => {
InvocationObstructionKind::CapabilityValidationUnavailable
}
}
}
Loading
Loading