fix: align host dispatch with canonical paths

This commit is contained in:
2026-09-29 06:28:33 +09:00
parent 2d524723a4
commit 2efda433e7
6 changed files with 816 additions and 866 deletions
+223 -160
View File
@@ -3,30 +3,26 @@
//! # Responsibility boundaries
//!
//! [`InterfaceRegistry::register`] validates and freezes a generated interface
//! descriptor, assigns an opaque [`InterfaceRef`], and makes the same descriptor
//! observation available to clients and dispatch. [`PublishedObject::expose`]
//! explicitly combines a request-scoped Rust object with a registered interface;
//! its [`InterfaceImplementation`] bound proves at compile time that the object
//! implements that interface. Implementing a Rust trait alone never publishes it,
//! descriptor, assigns an opaque [`InterfaceRef`], and leaves that reference
//! outside the descriptor. [`PublishedObject::expose`] explicitly combines a
//! request-scoped Rust object with a registered interface; its
//! [`InterfaceImplementation`] bound proves at compile time that the object
//! implements the interface. Implementing a Rust trait alone never publishes it,
//! and a publication with no interfaces is valid.
//!
//! [`PublishedObject::dispatch`] selects by `(InterfaceRef, operation name)`,
//! checks object and interface validators, current membership, the frozen
//! descriptor, and then invokes the matching generated typed dispatcher on the
//! same object instance. Arguments and results cross the protocol [`Value`]
//! boundary only through generated validation and codecs. Rust fields that are
//! not returned by [`WipObject`] are host-private and are never serialized by
//! this crate.
//! [`ObjectProvider::call_operation`] resolves `target.path` exactly once and
//! keeps the resulting object, interface membership, descriptor, implementation,
//! and backend target fixed within the provider's [`ObjectProvider::with_object`]
//! callback. Dispatch is qualified by `(interface reference, operation name)`.
//! The same frozen operation declaration validates both arguments and results.
//!
//! The call method is deliberately an execution boundary rather than a global
//! object registry: a provider may resolve and publish an object per request and
//! drop it afterward. The provider must enter any backend-specific transaction,
//! lock, stable-handle scope, or conditional-update primitive *before* creating
//! or dispatching the publication and retain it until dispatch returns. This
//! crate keeps validator checks, descriptor and implementation selection, and
//! invocation in one borrow of that publication; it does not claim that an
//! arbitrary backend stays stable, and it never reopens a path or resolves a
//! second implementation snapshot after validation.
//! A provider is responsible for entering a backend transaction, lock,
//! stable-handle scope, or conditional-update primitive before invoking the
//! callback and retaining it until the callback returns. The runtime does not
//! reopen the path or select another implementation after checking validators,
//! but cannot make an arbitrary external backend atomic on its own. Mutable
//! operations must make object-validator comparison and backend commit atomic;
//! the validator returned after dispatch must describe the committed state.
#![deny(missing_docs)]
@@ -41,8 +37,13 @@ use std::sync::atomic::{AtomicU64, Ordering};
use wip_interface::DispatchError;
pub use wip_interface::{InterfaceDefinition, InterfaceImplementation};
pub use wip_protocol;
pub use wip_protocol::{
CallOperationRequest as CallRequest, CallOperationResponse as CallResult, InterfaceTarget,
Target,
};
use wip_protocol::{
InterfaceDescriptor, Object, ProtocolError, ProtocolErrorCode, ValidationError, Value,
FetchInterfaceResponse, InterfaceDescriptor, Object, ProtocolError, ProtocolErrorCode,
ValidationError, Value,
};
/// Opaque bytes used as a conditional execution precondition.
@@ -63,14 +64,6 @@ impl Validator {
}
}
/// Host-local identity used by the legacy request-scoped publication API.
///
/// Canonical protocol calls target a Worldspace path. This wrapper remains
/// temporarily host-local so T-5 can adapt generated values without assigning
/// object identity semantics to the protocol descriptor model.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ObjectId(pub String);
/// Opaque host-assigned reference to one registered interface artifact.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InterfaceRef(String);
@@ -83,22 +76,34 @@ impl InterfaceRef {
}
}
/// An immutable observation returned for an opaque interface reference.
/// One atomic observation of an interface reference, descriptor, and validator.
#[derive(Debug, Clone, PartialEq)]
pub struct InterfaceObservation {
/// Opaque reference selected by published objects and operation calls.
pub reference: InterfaceRef,
/// Canonical descriptor frozen when the interface was registered.
pub descriptor: InterfaceDescriptor,
/// Optional validator. Its presence makes it a required call precondition.
pub validator: Option<Validator>,
/// Validated descriptor frozen when the interface was registered.
pub descriptor: InterfaceDescriptor,
}
impl InterfaceObservation {
/// Converts this host observation to the canonical protocol response.
#[must_use]
pub fn protocol_response(&self) -> FetchInterfaceResponse {
FetchInterfaceResponse {
interface: self.reference.0.clone(),
descriptor: self.descriptor.clone(),
validator: self.validator.as_ref().map(|validator| validator.0.clone()),
}
}
}
#[derive(Debug)]
struct InterfaceArtifact {
reference: InterfaceRef,
validator: Option<Validator>,
descriptor: InterfaceDescriptor,
validator: Option<Validator>,
}
/// A typed registration token for a generated interface definition.
@@ -129,12 +134,11 @@ impl<D> RegisteredInterface<D> {
/// Registry of validated, immutable interface artifacts.
///
/// This registry stores interface definitions, not object instances. Generated
/// definitions are registered explicitly and receive process-local opaque
/// Generated descriptors are never rewritten to contain their assigned
/// references. A validator-free artifact is immutable for the lifetime of its
/// reference; a validator-bearing artifact requires that exact validator on a
/// call. Re-registering a definition is rejected instead of silently changing
/// the descriptor associated with an existing reference.
/// reference. A validator-bearing artifact requires that exact validator on a
/// call. Re-registering a definition is rejected rather than silently changing
/// the observation associated with an existing reference.
#[derive(Debug)]
pub struct InterfaceRegistry {
namespace: u64,
@@ -164,12 +168,6 @@ impl InterfaceRegistry {
}
/// Registers and freezes one generated interface definition.
///
/// `validator` should be present when the host intends the reference to
/// represent a versioned descriptor. Its presence makes the same value a
/// mandatory precondition for dispatch. Registration finalizes the
/// descriptor remains independent of both registration and its assigned
/// opaque reference.
pub fn register<D>(
&mut self,
_definition: D,
@@ -198,8 +196,8 @@ impl InterfaceRegistry {
.ok_or(RegistrationError::ReferenceSpaceExhausted)?;
let artifact = Arc::new(InterfaceArtifact {
reference: reference.clone(),
validator,
descriptor,
validator,
});
self.definitions.insert(definition);
self.artifacts.insert(reference, Arc::clone(&artifact));
@@ -211,28 +209,40 @@ impl InterfaceRegistry {
}
/// Resolves a transport-provided opaque reference string in this registry.
///
/// The returned value is registry-scoped and suitable for observation or a
/// qualified [`CallRequest`]. Unknown or foreign references return `None`;
/// constructing arbitrary `InterfaceRef` values is intentionally not part of
/// the public API.
#[must_use]
pub fn resolve(&self, reference: &str) -> Option<InterfaceRef> {
let reference = InterfaceRef(reference.to_owned());
self.artifacts.contains_key(&reference).then_some(reference)
}
/// Observes the descriptor and optional validator for an opaque reference.
/// Observes a registered interface by its host-side reference.
#[must_use]
pub fn observe(&self, reference: &InterfaceRef) -> Option<InterfaceObservation> {
self.artifacts.get(reference).map(Self::observation)
}
/// Implements canonical `fetch_interface` semantics for an opaque reference.
///
/// Unknown and currently unpublished references map to `InterfaceNotFound`,
/// not object membership errors or authorization failures.
pub fn fetch_interface(&self, reference: &str) -> Result<InterfaceObservation, ProtocolError> {
let reference = InterfaceRef(reference.to_owned());
self.artifacts
.get(reference)
.map(|artifact| InterfaceObservation {
reference: artifact.reference.clone(),
validator: artifact.validator.clone(),
descriptor: artifact.descriptor.clone(),
.get(&reference)
.map(Self::observation)
.ok_or_else(|| ProtocolError {
code: ProtocolErrorCode::InterfaceNotFound,
message: "interface reference is not currently published".into(),
})
}
fn observation(artifact: &Arc<InterfaceArtifact>) -> InterfaceObservation {
InterfaceObservation {
reference: artifact.reference.clone(),
descriptor: artifact.descriptor.clone(),
validator: artifact.validator.clone(),
}
}
}
/// Failure while registering a generated interface definition.
@@ -274,19 +284,32 @@ impl Error for RegistrationError {
/// Public metadata and conditional-execution policy for a host-private object.
///
/// Only values returned by this trait are projected into protocol metadata.
/// Backend handles, filesystem paths, credentials, and other private fields on
/// the implementing type are neither inspected nor serialized.
/// Paths, children, backend handles, credentials, and resolution state are not
/// part of this API and therefore cannot be serialized into [`Object`].
pub trait WipObject {
/// Returns the current logical identity of this object.
fn id(&self) -> ObjectId;
/// Returns the public path-segment name of this object.
fn name(&self) -> &str;
/// Returns a short public description, if one is available.
fn description(&self) -> Option<&str> {
None
}
/// Returns optional opaque identity within the client-facing Worldspace.
fn reference(&self) -> Option<&str> {
None
}
/// Returns the current opaque object validator, if any.
fn validator(&self) -> Option<&Validator> {
None
}
/// Whether a call must include the current object validator.
fn requires_validator(&self) -> bool {
/// Whether this specific qualified operation requires an object validator.
///
/// This hook is evaluated only after membership, interface validators, and
/// operation existence have been checked, preserving canonical precedence.
fn requires_validator(&self, _interface: &InterfaceRef, _operation: &str) -> bool {
false
}
}
@@ -301,7 +324,8 @@ struct BoundInterface<T> {
/// A host-private object instance combined with an explicit ordered interface set.
///
/// The instance is owned by this value and can therefore be request-scoped. No
/// global or pre-populated object registry is required.
/// global object registry is required. Worldspace paths remain provider context
/// and are deliberately absent from the publication.
pub struct PublishedObject<T: WipObject> {
implementation: T,
interfaces: Vec<BoundInterface<T>>,
@@ -349,63 +373,46 @@ impl<T: WipObject> PublishedObject<T> {
&self.implementation
}
/// Builds the protocol object metadata with interface insertion order intact.
/// Builds canonical protocol object metadata in interface insertion order.
///
/// The observation contains no path, children, descriptor, backend handle,
/// credential, or provider resolution state.
#[must_use]
pub fn protocol_object(&self) -> Object {
let id = self.implementation.id();
let object = Object {
name: id.0.clone(),
description: None,
Object {
name: self.implementation.name().to_owned(),
description: self.implementation.description().map(str::to_owned),
interfaces: self
.interfaces
.iter()
.map(|bound| bound.artifact.reference.0.clone())
.collect(),
r#ref: Some(id.0),
r#ref: self.implementation.reference().map(str::to_owned),
validator: self
.implementation
.validator()
.map(|validator| validator.0.clone()),
};
debug_assert!(object.validate().is_ok());
object
}
}
/// Dispatches one qualified operation against this current publication.
///
/// Preconditions are checked before generated operation code runs. The
/// selected artifact and function pointer are borrowed once and are not
/// resolved again after validator checks.
pub fn dispatch(&self, request: CallRequest) -> Result<CallResult, CallError> {
if request.object != self.implementation.id() {
fn dispatch_resolved(
&self,
request: CallRequest,
authorize: impl FnOnce(&T, &InterfaceRef, &str) -> bool,
) -> Result<CallResult, CallError> {
let current_object_validator = self.implementation.validator();
if let Some(supplied) = request.target.validator.as_deref()
&& current_object_validator.map(Validator::as_bytes) != Some(supplied)
{
return Err(CallError::new(
CallErrorKind::ObjectNotFound,
"call target does not identify this published object",
CallErrorKind::ValidatorMismatch,
"object validator does not match current object state",
));
}
let current_object_validator = self.implementation.validator();
match request.object_validator.as_ref() {
Some(supplied) if Some(supplied) != current_object_validator => {
return Err(CallError::new(
CallErrorKind::ValidatorMismatch,
"object validator does not match current object state",
));
}
None if self.implementation.requires_validator() => {
return Err(CallError::new(
CallErrorKind::ValidatorRequired,
"this object requires a validator",
));
}
_ => {}
}
let Some(bound) = self
.interfaces
.iter()
.find(|bound| bound.artifact.reference == request.interface.reference)
else {
let Some(bound) = self.interfaces.iter().find(|bound| {
bound.artifact.reference.as_str() == request.interface.reference.as_str()
}) else {
return Err(CallError::new(
CallErrorKind::InterfaceMismatch,
"interface is not a member of the current object publication",
@@ -414,7 +421,7 @@ impl<T: WipObject> PublishedObject<T> {
match (
bound.artifact.validator.as_ref(),
request.interface.validator.as_ref(),
request.interface.validator.as_deref(),
) {
(Some(_), None) => {
return Err(CallError::new(
@@ -422,7 +429,7 @@ impl<T: WipObject> PublishedObject<T> {
"registered interface requires a validator",
));
}
(expected, Some(actual)) if expected != Some(actual) => {
(expected, Some(actual)) if expected.map(Validator::as_bytes) != Some(actual) => {
return Err(CallError::new(
CallErrorKind::InterfaceValidatorMismatch,
"interface validator does not match the registered descriptor",
@@ -444,12 +451,34 @@ impl<T: WipObject> PublishedObject<T> {
));
}
if request.target.validator.is_none()
&& self
.implementation
.requires_validator(&bound.artifact.reference, &request.operation)
{
return Err(CallError::new(
CallErrorKind::ValidatorRequired,
"this operation requires an object validator",
));
}
bound
.artifact
.descriptor
.validate_arguments(&request.operation, &request.arguments)
.map_err(|error| CallError::new(CallErrorKind::InvalidArguments, error.to_string()))?;
if !authorize(
&self.implementation,
&bound.artifact.reference,
&request.operation,
) {
return Err(CallError::new(
CallErrorKind::PermissionDenied,
"operation is not permitted for this caller",
));
}
let value = (bound.dispatch)(&self.implementation, &request.operation, request.arguments)
.map_err(CallError::from_dispatch)?;
@@ -460,8 +489,11 @@ impl<T: WipObject> PublishedObject<T> {
.map_err(|error| CallError::new(CallErrorKind::InvalidResult, error.to_string()))?;
Ok(CallResult {
value,
validator: self.implementation.validator().cloned(),
result: value,
validator: self
.implementation
.validator()
.map(|validator| validator.0.clone()),
})
}
@@ -472,6 +504,70 @@ impl<T: WipObject> PublishedObject<T> {
}
}
/// A request-scoped Worldspace resolver and execution boundary.
///
/// `with_object` must resolve the current object at `path` exactly once and pass
/// a publication that pins its current interface set, implementations, and
/// backend target for the duration of `operation`. Implementations may hold a
/// transaction or lock around the callback. Returning `None` means the path is
/// not published to the caller. The borrowed publication cannot escape the
/// callback, so the default call path cannot outlive that provider boundary.
pub trait ObjectProvider {
/// Host-private object type produced by this provider.
type Object: WipObject;
/// Runs an operation against the single object snapshot resolved at `path`.
fn with_object<R>(
&self,
path: &str,
operation: impl FnOnce(&PublishedObject<Self::Object>) -> R,
) -> Option<R>;
/// Decides whether a declared operation may execute for the current caller.
///
/// The hook runs after all validators, membership, operation lookup, and
/// argument validation, but before implementation code. Returning `false`
/// exposes the operation while producing `PermissionDenied`.
fn authorize(
&self,
_path: &str,
_object: &Self::Object,
_interface: &InterfaceRef,
_operation: &str,
) -> bool {
true
}
/// Fetches the canonical object projection currently published at `path`.
fn fetch(&self, path: &str) -> Result<Object, CallError> {
self.with_object(path, PublishedObject::protocol_object)
.ok_or_else(|| CallError::new(CallErrorKind::NotFound, "target path is not published"))
.and_then(|object| {
object.validate_at_path(path).map_err(|error| {
CallError::new(CallErrorKind::InvalidResult, error.to_string())
})?;
Ok(object)
})
}
/// Resolves and executes a canonical path-targeted operation request.
///
/// Failure checks follow the exact canonical precedence: path, supplied
/// object validator mismatch, membership, interface validator requirement
/// and mismatch, operation existence, object validator requirement,
/// arguments, then authorization. No failed precondition invokes the typed
/// implementation.
fn call_operation(&self, request: CallRequest) -> Result<CallResult, CallError> {
let path = request.target.path.clone();
self.with_object(&path, |publication| {
publication.dispatch_resolved(request, |object, interface, operation| {
self.authorize(&path, object, interface, operation)
})
})
.ok_or_else(|| CallError::new(CallErrorKind::NotFound, "target path is not published"))?
}
}
/// Failure to construct an explicit object publication.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PublicationError {
@@ -493,44 +589,11 @@ impl Display for PublicationError {
impl Error for PublicationError {}
/// Interface portion of a qualified operation target.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InterfaceTarget {
/// Opaque reference selected from the current object's interface set.
pub reference: InterfaceRef,
/// Validator copied from the descriptor observation, when one was present.
pub validator: Option<Validator>,
}
/// One transport-independent request to a published object operation.
#[derive(Debug, Clone, PartialEq)]
pub struct CallRequest {
/// Current logical object identity.
pub object: ObjectId,
/// Optional current object-state precondition.
pub object_validator: Option<Validator>,
/// Qualified interface reference and descriptor precondition.
pub interface: InterfaceTarget,
/// Operation name within the selected interface descriptor.
pub operation: String,
/// Named protocol arguments.
pub arguments: BTreeMap<String, Value>,
}
/// Successful host dispatch result.
#[derive(Debug, Clone, PartialEq)]
pub struct CallResult {
/// Encoded application result. No-data operations return [`Value::Unit`].
pub value: Value,
/// Object validator observed after the implementation returned.
pub validator: Option<Validator>,
}
/// Stable host dispatch failure kind.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallErrorKind {
/// The call names a different or unavailable object.
ObjectNotFound,
/// The target path is not currently published.
NotFound,
/// The selected interface is not exposed by the current object.
InterfaceMismatch,
/// The operation is absent from the selected descriptor.
@@ -539,7 +602,7 @@ pub enum CallErrorKind {
InvalidArguments,
/// The encoded implementation result violates its declaration.
InvalidResult,
/// This object requires a validator and the call omitted it.
/// This operation requires a validator and the call omitted it.
ValidatorRequired,
/// The supplied object validator is stale or belongs to another object.
ValidatorMismatch,
@@ -547,6 +610,8 @@ pub enum CallErrorKind {
InterfaceValidatorRequired,
/// The supplied interface validator does not identify the current descriptor.
InterfaceValidatorMismatch,
/// The operation is declared but not authorized for the caller.
PermissionDenied,
/// The typed implementation reported a host/backend failure.
Implementation,
}
@@ -602,30 +667,28 @@ impl CallError {
&self.message
}
/// Maps this host failure to the existing protocol error taxonomy.
///
/// Validator details remain available through [`CallError::kind`], while
/// the current protocol model represents conditional-request failures as
/// `InvalidRequest`. Result/codec and implementation failures are internal,
/// because exposing malformed host output as an application failure would
/// be unsafe.
/// Maps this host failure to the canonical protocol error taxonomy.
#[must_use]
pub fn protocol_error(&self) -> ProtocolError {
let category = match self.kind {
CallErrorKind::ObjectNotFound => ProtocolErrorCode::NotFound,
let code = match self.kind {
CallErrorKind::NotFound => ProtocolErrorCode::NotFound,
CallErrorKind::InterfaceMismatch => ProtocolErrorCode::InterfaceMismatch,
CallErrorKind::OperationNotFound => ProtocolErrorCode::OperationNotFound,
CallErrorKind::InvalidArguments => ProtocolErrorCode::InvalidArguments,
CallErrorKind::ValidatorRequired
| CallErrorKind::ValidatorMismatch
| CallErrorKind::InterfaceValidatorRequired
| CallErrorKind::InterfaceValidatorMismatch => ProtocolErrorCode::InvalidRequest,
CallErrorKind::InvalidResult | CallErrorKind::Implementation => {
ProtocolErrorCode::Internal
CallErrorKind::InvalidResult => ProtocolErrorCode::InvalidResult,
CallErrorKind::ValidatorRequired => ProtocolErrorCode::ValidatorRequired,
CallErrorKind::ValidatorMismatch => ProtocolErrorCode::ValidatorMismatch,
CallErrorKind::InterfaceValidatorRequired => {
ProtocolErrorCode::InterfaceValidatorRequired
}
CallErrorKind::InterfaceValidatorMismatch => {
ProtocolErrorCode::InterfaceValidatorMismatch
}
CallErrorKind::PermissionDenied => ProtocolErrorCode::PermissionDenied,
CallErrorKind::Implementation => ProtocolErrorCode::Internal,
};
ProtocolError {
code: category,
code,
message: self.message.clone(),
}
}