Skip to main content

wip_host/
lib.rs

1//! Host-side publication and operation dispatch for Web Interface Protocol objects.
2//!
3//! # Responsibility boundaries
4//!
5//! [`InterfaceRegistry::register`] validates and freezes a generated interface
6//! descriptor at an explicit scope and local name, and leaves that reference
7//! outside the descriptor. [`PublishedObject::expose`] explicitly combines a
8//! request-scoped Rust object with a registered interface; its
9//! [`InterfaceImplementation`] bound proves at compile time that the object
10//! implements the interface. Implementing a Rust trait alone never publishes it,
11//! and a publication with no interfaces is valid.
12//!
13//! [`ObjectProvider::call_operation`] resolves `target.path` exactly once and
14//! keeps the resulting object, current scope Object association/ref, interface
15//! membership, descriptor, implementation,
16//! and backend target fixed within the provider's [`ObjectProvider::with_object`]
17//! callback. Dispatch is qualified by `(interface reference, operation name)`.
18//! The same frozen operation declaration validates both arguments and results.
19//!
20//! A provider is responsible for entering a backend transaction, lock,
21//! stable-handle scope, or conditional-update primitive before invoking the
22//! operation callback and retaining it through commit or rollback. The typed
23//! [`OperationAttempt`] boundary distinguishes pre-dispatch rejection from started
24//! execution. Only an implementation-supplied non-commit guarantee or a
25//! provider-confirmed rollback allows a post-invocation failure to become
26//! `Internal` or `ResourceLimitExceeded`; every other outcome is
27//! `OperationOutcomeUnknown`. Mutable operations must make object-validator
28//! comparison and backend commit atomic, and the validator returned after dispatch
29//! must describe the state that becomes committed.
30
31#![deny(missing_docs)]
32
33use std::collections::{BTreeMap, HashMap, HashSet};
34use std::error::Error;
35use std::fmt::{self, Display, Formatter};
36use std::marker::PhantomData;
37use std::panic::{AssertUnwindSafe, catch_unwind};
38use std::sync::Arc;
39use std::sync::atomic::{AtomicBool, Ordering};
40use std::time::Instant;
41
42use wip_interface::DispatchError;
43pub use wip_interface::{
44    EffectGuarantee, HostError, HostErrorKind, InterfaceDefinition, InterfaceImplementation,
45};
46pub use wip_protocol;
47pub use wip_protocol::{
48    CallOperationRequest as CallRequest, CallOperationResponse as CallResult, InterfaceTarget,
49    Target,
50};
51use wip_protocol::{
52    FetchInterfaceRequest, FetchInterfaceResponse, InterfaceDescriptor, InterfaceReference, Object,
53    ProtocolError, ProtocolErrorCode, ValidationError, Value,
54};
55
56/// Opaque bytes used as a conditional execution precondition.
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub struct Validator(Vec<u8>);
59
60impl Validator {
61    /// Creates a validator without assigning meaning to its bytes.
62    #[must_use]
63    pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
64        Self(bytes.into())
65    }
66
67    /// Returns the opaque bytes for transport encoding or comparison.
68    #[must_use]
69    pub fn as_bytes(&self) -> &[u8] {
70        &self.0
71    }
72}
73
74/// Runtime-neutral cooperative cancellation signal.
75///
76/// Clones observe the same atomic state, so transports, providers, and worker
77/// runtimes can share the token without depending on a particular async runtime.
78#[derive(Debug, Clone, Default)]
79pub struct CancellationToken(Arc<AtomicBool>);
80
81impl CancellationToken {
82    /// Creates a cancellation token in the active state.
83    #[must_use]
84    pub fn new() -> Self {
85        Self::default()
86    }
87
88    /// Requests cooperative cancellation.
89    pub fn cancel(&self) {
90        self.0.store(true, Ordering::Release);
91    }
92
93    /// Returns whether cancellation has been requested.
94    #[must_use]
95    pub fn is_cancelled(&self) -> bool {
96        self.0.load(Ordering::Acquire)
97    }
98}
99
100/// Execution controls propagated through object resolution and operation dispatch.
101///
102/// Cancellation and deadlines are checked immediately before typed implementation
103/// invocation. Providers also receive the complete context and remain responsible
104/// for enforcing controls while backend work is in progress. `resource_budget` is
105/// an opaque Host-defined work-unit ceiling; this crate deliberately does not
106/// prescribe an async runtime or accounting mechanism.
107#[derive(Debug, Clone)]
108pub struct ExecutionContext {
109    cancellation: CancellationToken,
110    deadline: Option<Instant>,
111    resource_budget: Option<u64>,
112}
113
114impl Default for ExecutionContext {
115    fn default() -> Self {
116        Self {
117            cancellation: CancellationToken::new(),
118            deadline: None,
119            resource_budget: None,
120        }
121    }
122}
123
124impl ExecutionContext {
125    /// Creates an execution context without a deadline or resource budget.
126    #[must_use]
127    pub fn new() -> Self {
128        Self::default()
129    }
130
131    /// Uses `cancellation` as this call's cooperative cancellation signal.
132    #[must_use]
133    pub fn with_cancellation(mut self, cancellation: CancellationToken) -> Self {
134        self.cancellation = cancellation;
135        self
136    }
137
138    /// Sets the monotonic deadline for this call.
139    #[must_use]
140    pub fn with_deadline(mut self, deadline: Instant) -> Self {
141        self.deadline = Some(deadline);
142        self
143    }
144
145    /// Sets an opaque Host-defined work-unit ceiling for this call.
146    #[must_use]
147    pub fn with_resource_budget(mut self, resource_budget: u64) -> Self {
148        self.resource_budget = Some(resource_budget);
149        self
150    }
151
152    /// Returns the shared cooperative cancellation token.
153    #[must_use]
154    pub fn cancellation(&self) -> &CancellationToken {
155        &self.cancellation
156    }
157
158    /// Returns the monotonic deadline, if one was supplied.
159    #[must_use]
160    pub fn deadline(&self) -> Option<Instant> {
161        self.deadline
162    }
163
164    /// Returns the opaque Host-defined work-unit ceiling, if one was supplied.
165    #[must_use]
166    pub fn resource_budget(&self) -> Option<u64> {
167        self.resource_budget
168    }
169
170    fn preflight(&self) -> Result<(), CallError> {
171        if self.cancellation.is_cancelled() {
172            return Err(CallError::new(
173                CallErrorKind::ResourceLimitExceeded,
174                "operation was cancelled before execution",
175            ));
176        }
177        if self
178            .deadline
179            .is_some_and(|deadline| Instant::now() >= deadline)
180        {
181            return Err(CallError::new(
182                CallErrorKind::ResourceLimitExceeded,
183                "operation deadline elapsed before execution",
184            ));
185        }
186        Ok(())
187    }
188}
189
190/// One publication lifetime of the Object at a canonical scope path.
191///
192/// Clone while the same Object remains published, including state updates. Create
193/// a new handle after deletion, replacement or movement, even with the same ref.
194/// This private lifetime is not a Protocol identity component or generated ref.
195#[derive(Debug, Clone)]
196pub struct Scope {
197    path: String,
198    lifetime: Arc<AtomicBool>,
199}
200
201impl Scope {
202    /// Creates a fresh lifetime without implicitly publishing an Object.
203    pub fn new(path: impl Into<String>) -> Result<Self, ValidationError> {
204        let path = path.into();
205        wip_protocol::validate_path(&path)?;
206        Ok(Self {
207            path,
208            lifetime: Arc::new(AtomicBool::new(true)),
209        })
210    }
211
212    /// Returns the client-facing canonical scope path.
213    #[must_use]
214    pub fn path(&self) -> &str {
215        &self.path
216    }
217
218    fn same_lifetime(&self, other: &Self) -> bool {
219        self.path == other.path && Arc::ptr_eq(&self.lifetime, &other.lifetime)
220    }
221
222    fn active(&self) -> bool {
223        self.lifetime.load(Ordering::Acquire)
224    }
225}
226
227/// Provider-attested current scope Object association.
228///
229/// Construct only inside a coherent boundary that proves this lifetime belongs
230/// to the Object at `scope.path()`. A prelookup or matching ref alone is not proof.
231/// The Object need not expose interfaces. Its validator is not scope identity.
232#[derive(Debug, Clone)]
233pub struct ScopeObservation {
234    scope: Scope,
235    reference: Option<String>,
236}
237
238impl ScopeObservation {
239    /// Attests the association and captures the current Object's optional ref.
240    #[must_use]
241    pub fn new(scope: &Scope, object: &impl WipObject) -> Self {
242        Self {
243            scope: scope.clone(),
244            reference: object.reference().map(str::to_owned),
245        }
246    }
247
248    /// Returns the observed optional ref.
249    #[must_use]
250    pub fn reference(&self) -> Option<&str> {
251        self.reference.as_deref()
252    }
253
254    /// Returns the associated scope publication lifetime.
255    #[must_use]
256    pub fn scope(&self) -> &Scope {
257        &self.scope
258    }
259}
260
261/// Structured client-facing reference to a scoped Interface publication.
262#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
263pub struct InterfaceRef(InterfaceReference);
264
265impl InterfaceRef {
266    /// Returns the local name without parsing or adding display delimiters.
267    #[must_use]
268    pub fn as_str(&self) -> &str {
269        &self.0.name
270    }
271
272    /// Returns the structured Protocol reference.
273    #[must_use]
274    pub fn protocol_reference(&self) -> InterfaceReference {
275        self.0.clone()
276    }
277}
278
279/// One atomic observation of an interface reference, descriptor, and validator.
280#[derive(Debug, Clone, PartialEq)]
281pub struct InterfaceObservation {
282    /// Structured reference selected by published objects and operation calls.
283    pub reference: InterfaceRef,
284    /// Canonical descriptor frozen when the interface was registered.
285    pub descriptor: InterfaceDescriptor,
286    /// Optional ref of the scope Object from this same observation.
287    pub scope_ref: Option<String>,
288    /// Optional validator. Its presence makes it a required call precondition.
289    pub validator: Option<Validator>,
290}
291
292impl InterfaceObservation {
293    /// Restricts this effective view to visible operations and reachable types.
294    ///
295    /// Registry artifacts are unchanged. When operations are removed, interface-
296    /// wide documentation is omitted because it may describe hidden operations.
297    /// Providers must use the same predicate for publication and fetch.
298    #[must_use]
299    pub fn project_operations(mut self, visible: impl Fn(&str) -> bool) -> Self {
300        restrict_descriptor(&mut self.descriptor, visible);
301        self
302    }
303
304    /// Converts this host observation to a validated canonical protocol response.
305    ///
306    /// A Host-side response construction failure is returned as `Internal`; it is
307    /// never emitted as a successful response for a Client to reinterpret as a
308    /// local validation error.
309    pub fn protocol_response(&self) -> Result<FetchInterfaceResponse, ProtocolError> {
310        let response = FetchInterfaceResponse {
311            interface: self.reference.protocol_reference(),
312            descriptor: self.descriptor.clone(),
313            scope_ref: self.scope_ref.clone(),
314            validator: self.validator.as_ref().map(|validator| validator.0.clone()),
315        };
316        response
317            .validate_for(&FetchInterfaceRequest {
318                interface: self.reference.protocol_reference(),
319            })
320            .map_err(|_| ProtocolError {
321                code: ProtocolErrorCode::Internal,
322                message: "Host produced an invalid interface response".into(),
323            })?;
324        Ok(response)
325    }
326}
327
328#[derive(Debug)]
329struct InterfaceArtifact {
330    scope: Scope,
331    reference: InterfaceRef,
332    descriptor: InterfaceDescriptor,
333    validator: Option<Validator>,
334}
335
336/// A typed registration token for a generated interface definition.
337///
338/// Tokens can be reused by any number of request-scoped objects. They expose an
339/// scoped reference and a private publication lifetime, not a Rust address.
340pub struct RegisteredInterface<D> {
341    artifact: Arc<InterfaceArtifact>,
342    definition: PhantomData<fn() -> D>,
343}
344
345impl<D> Clone for RegisteredInterface<D> {
346    fn clone(&self) -> Self {
347        Self {
348            artifact: Arc::clone(&self.artifact),
349            definition: PhantomData,
350        }
351    }
352}
353
354impl<D> RegisteredInterface<D> {
355    /// Returns the explicit scope/name reference.
356    #[must_use]
357    pub fn reference(&self) -> &InterfaceRef {
358        &self.artifact.reference
359    }
360}
361
362/// Registry of frozen definition artifacts and explicitly scoped registrations.
363///
364/// Artifact retention is not proof of current publication. Fetch requires a live
365/// provider-attested ScopeObservation; execution requires the same lifetime in
366/// the target's atomic boundary. Collision keys are exact (scope, name) pairs,
367/// not Rust TypeId: the same Definition can be registered at multiple coordinates.
368#[derive(Debug, Default)]
369pub struct InterfaceRegistry {
370    artifacts: HashMap<InterfaceRef, Arc<InterfaceArtifact>>,
371}
372
373impl InterfaceRegistry {
374    /// Creates an empty artifact registry without publishing any Object.
375    #[must_use]
376    pub fn new() -> Self {
377        Self::default()
378    }
379
380    /// Freezes a Definition at an explicit scope and local name.
381    pub fn register<D>(
382        &mut self,
383        scope: &Scope,
384        name: impl Into<String>,
385        _definition: D,
386        validator: Option<Validator>,
387    ) -> Result<RegisteredInterface<D>, RegistrationError>
388    where
389        D: InterfaceDefinition + 'static,
390    {
391        let reference = InterfaceRef(InterfaceReference {
392            scope: scope.path.clone(),
393            name: name.into(),
394        });
395        reference
396            .0
397            .validate()
398            .map_err(RegistrationError::InvalidReference)?;
399        if !scope.active() {
400            return Err(RegistrationError::RetiredScope);
401        }
402        if self.artifacts.contains_key(&reference) {
403            return Err(RegistrationError::AlreadyRegistered);
404        }
405        let descriptor = D::descriptor();
406        descriptor
407            .validate()
408            .map_err(RegistrationError::InvalidDescriptor)?;
409        let artifact = Arc::new(InterfaceArtifact {
410            scope: scope.clone(),
411            reference: reference.clone(),
412            descriptor,
413            validator,
414        });
415        self.artifacts.insert(reference, Arc::clone(&artifact));
416        Ok(RegisteredInterface {
417            artifact,
418            definition: PhantomData,
419        })
420    }
421
422    /// Ends a scope lifetime and releases its registry entries.
423    ///
424    /// Retained tokens cannot start new calls/fetches. Started calls retain their
425    /// frozen Descriptor. Re-publication requires a fresh Scope.
426    pub fn remove_scope(&mut self, scope: &Scope) {
427        scope.lifetime.store(false, Ordering::Release);
428        self.artifacts
429            .retain(|_, artifact| !artifact.scope.same_lifetime(scope));
430    }
431
432    /// Resolves a stored artifact, not evidence of current publication.
433    #[must_use]
434    pub fn resolve(&self, reference: &InterfaceReference) -> Option<InterfaceRef> {
435        let reference = InterfaceRef(reference.clone());
436        self.artifacts.contains_key(&reference).then_some(reference)
437    }
438
439    /// Iterates registered scope handles, possibly repeated for multiple names.
440    pub fn scopes(&self) -> impl Iterator<Item = &Scope> {
441        self.artifacts.values().map(|artifact| &artifact.scope)
442    }
443
444    /// Fetches only with a coherent current scope Object attestation.
445    pub fn fetch_interface(
446        &self,
447        reference: &InterfaceReference,
448        current: &ScopeObservation,
449    ) -> Result<InterfaceObservation, ProtocolError> {
450        reference.validate().map_err(|error| ProtocolError {
451            code: ProtocolErrorCode::InvalidRequest,
452            message: error.to_string(),
453        })?;
454        self.artifacts
455            .get(&InterfaceRef(reference.clone()))
456            .filter(|artifact| {
457                artifact.scope.active() && artifact.scope.same_lifetime(&current.scope)
458            })
459            .map(|artifact| InterfaceObservation {
460                reference: artifact.reference.clone(),
461                descriptor: artifact.descriptor.clone(),
462                scope_ref: current.reference.clone(),
463                validator: artifact.validator.clone(),
464            })
465            .ok_or_else(interface_not_found)
466    }
467}
468
469fn interface_not_found() -> ProtocolError {
470    ProtocolError {
471        code: ProtocolErrorCode::InterfaceNotFound,
472        message: "interface is not currently published".into(),
473    }
474}
475
476/// Failure while registering a generated interface Definition.
477#[derive(Debug)]
478pub enum RegistrationError {
479    /// The exact scope/name coordinate is already occupied.
480    AlreadyRegistered,
481    /// The generated Descriptor is invalid.
482    InvalidDescriptor(ValidationError),
483    /// The scope or local name is invalid.
484    InvalidReference(ValidationError),
485    /// The lifetime ended; re-publication needs a fresh Scope.
486    RetiredScope,
487}
488
489impl Display for RegistrationError {
490    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
491        match self {
492            Self::AlreadyRegistered => {
493                formatter.write_str("interface scope/name already registered")
494            }
495            Self::InvalidDescriptor(error) => {
496                write!(formatter, "invalid interface descriptor: {error}")
497            }
498            Self::InvalidReference(error) => {
499                write!(formatter, "invalid interface reference: {error}")
500            }
501            Self::RetiredScope => formatter.write_str("scope publication lifetime has ended"),
502        }
503    }
504}
505impl Error for RegistrationError {
506    fn source(&self) -> Option<&(dyn Error + 'static)> {
507        match self {
508            Self::InvalidDescriptor(error) | Self::InvalidReference(error) => Some(error),
509            Self::AlreadyRegistered | Self::RetiredScope => None,
510        }
511    }
512}
513
514/// Public metadata and conditional-execution policy for a host-private object.
515///
516/// Only values returned by this trait are projected into protocol metadata.
517/// Paths, children, backend handles, credentials, and resolution state are not
518/// part of this API and therefore cannot be serialized into [`Object`].
519pub trait WipObject {
520    /// Returns the public path-segment name of this object.
521    fn name(&self) -> &str;
522
523    /// Returns a short public description, if one is available.
524    fn description(&self) -> Option<&str> {
525        None
526    }
527
528    /// Returns optional opaque identity within the client-facing Worldspace.
529    fn reference(&self) -> Option<&str> {
530        None
531    }
532
533    /// Returns the current opaque object validator, if any.
534    fn validator(&self) -> Option<&Validator> {
535        None
536    }
537
538    /// Whether this specific qualified operation requires an object validator.
539    ///
540    /// This hook is evaluated only after membership, interface validators, and
541    /// operation existence have been checked, preserving canonical precedence.
542    fn requires_validator(&self, _interface: &InterfaceRef, _operation: &str) -> bool {
543        false
544    }
545}
546
547fn restrict_descriptor(descriptor: &mut InterfaceDescriptor, visible: impl Fn(&str) -> bool) {
548    let count = descriptor.operations.len();
549    descriptor
550        .operations
551        .retain(|operation| visible(&operation.name));
552    if descriptor.operations.len() == count {
553        return;
554    }
555    descriptor.documentation = None;
556    let mut used = HashSet::new();
557    fn references(expression: &wip_protocol::TypeExpr, used: &mut HashSet<String>) {
558        use wip_protocol::TypeExpr;
559        match expression {
560            TypeExpr::Named { name } => {
561                used.insert(name.clone());
562            }
563            TypeExpr::Record { fields } => {
564                for field in fields {
565                    references(&field.r#type, used);
566                }
567            }
568            TypeExpr::List { items } => references(items, used),
569            TypeExpr::Union { cases } => {
570                for case in cases {
571                    if let Some(payload) = &case.payload {
572                        references(payload, used);
573                    }
574                }
575            }
576            _ => {}
577        }
578    }
579    for operation in &descriptor.operations {
580        for parameter in &operation.parameters {
581            references(&parameter.r#type, &mut used);
582        }
583        references(&operation.returns.r#type, &mut used);
584    }
585    loop {
586        let before = used.len();
587        for declaration in &descriptor.types {
588            if used.contains(&declaration.name) {
589                references(&declaration.definition, &mut used);
590            }
591        }
592        if before == used.len() {
593            break;
594        }
595    }
596    descriptor
597        .types
598        .retain(|declaration| used.contains(&declaration.name));
599}
600
601type DispatchFn<T> = fn(&T, &str, BTreeMap<String, Value>) -> Result<Value, DispatchError>;
602
603struct BoundInterface<T> {
604    artifact: Arc<InterfaceArtifact>,
605    dispatch: DispatchFn<T>,
606}
607
608/// A host-private object instance combined with an explicit ordered interface set.
609///
610/// The instance is owned by this value and can therefore be request-scoped. No
611/// global object registry is required. Worldspace paths remain provider context
612/// and are deliberately absent from the publication.
613pub struct PublishedObject<T: WipObject> {
614    implementation: T,
615    interfaces: Vec<BoundInterface<T>>,
616    scopes: Vec<ScopeObservation>,
617}
618
619impl<T: WipObject> PublishedObject<T> {
620    /// Starts a publication with no exposed interfaces.
621    #[must_use]
622    pub fn new(implementation: T) -> Self {
623        Self {
624            implementation,
625            interfaces: Vec::new(),
626            scopes: Vec::new(),
627        }
628    }
629
630    /// Attaches a current scope snapshot inside the provider's atomic boundary.
631    ///
632    /// Never populate this from a stale prelookup. Target, scope association,
633    /// Descriptor and backend handles must remain coherent through dispatch and
634    /// commit. A later same-path binding replaces the earlier snapshot.
635    #[must_use]
636    pub fn bind_scope(mut self, current: ScopeObservation) -> Self {
637        self.scopes
638            .retain(|old| old.scope.path != current.scope.path);
639        self.scopes.push(current);
640        self
641    }
642
643    fn current_scope(&self, scope: &Scope) -> Option<&ScopeObservation> {
644        self.scopes
645            .iter()
646            .find(|current| scope.active() && scope.same_lifetime(&current.scope))
647    }
648
649    fn scopes_valid_at(&self, path: &str) -> bool {
650        self.interfaces.iter().all(|bound| {
651            bound.artifact.reference.0.validate_for_path(path).is_ok()
652                && self
653                    .current_scope(&bound.artifact.scope)
654                    .is_some_and(|current| {
655                        current.scope.path != path
656                            || current.reference() == self.implementation.reference()
657                    })
658        })
659    }
660
661    /// Explicitly exposes a registered generated interface in insertion order.
662    ///
663    /// The `InterfaceImplementation<T>` bound is generated only when `T`
664    /// implements the corresponding Rust interface trait. Duplicate scoped
665    /// references are rejected.
666    pub fn expose<D>(mut self, interface: &RegisteredInterface<D>) -> Result<Self, PublicationError>
667    where
668        D: InterfaceImplementation<T>,
669    {
670        if self
671            .interfaces
672            .iter()
673            .any(|bound| bound.artifact.reference == interface.artifact.reference)
674        {
675            return Err(PublicationError::DuplicateInterface(
676                interface.artifact.reference.clone(),
677            ));
678        }
679
680        self.interfaces.push(BoundInterface {
681            artifact: Arc::clone(&interface.artifact),
682            dispatch: D::dispatch,
683        });
684        Ok(self)
685    }
686
687    /// Restricts a publication to an ordered interface/operation view.
688    ///
689    /// This never adds implementations or changes codecs. The provider must use
690    /// the identical operation predicate for `fetch_interface`, and keep the
691    /// effective view stable for the lifetime of its references. Filtered
692    /// descriptors are scoped to that view, not mutations of registry artifacts.
693    pub fn project(
694        mut self,
695        interfaces: &[InterfaceRef],
696        visible: impl Fn(&InterfaceRef, &str) -> bool,
697    ) -> Result<Self, PublicationError> {
698        let mut projected = Vec::with_capacity(interfaces.len());
699        for reference in interfaces {
700            if projected
701                .iter()
702                .any(|bound: &BoundInterface<T>| &bound.artifact.reference == reference)
703            {
704                return Err(PublicationError::DuplicateInterface(reference.clone()));
705            }
706            let Some(index) = self
707                .interfaces
708                .iter()
709                .position(|bound| &bound.artifact.reference == reference)
710            else {
711                return Err(PublicationError::UnexposedInterface(reference.clone()));
712            };
713            let mut bound = self.interfaces.remove(index);
714            let mut descriptor = bound.artifact.descriptor.clone();
715            restrict_descriptor(&mut descriptor, |operation| visible(reference, operation));
716            bound.artifact = Arc::new(InterfaceArtifact {
717                scope: bound.artifact.scope.clone(),
718                reference: reference.clone(),
719                descriptor,
720                validator: bound.artifact.validator.clone(),
721            });
722            projected.push(bound);
723        }
724        self.interfaces = projected;
725        Ok(self)
726    }
727
728    /// Returns the host-private implementation without projecting its fields.
729    #[must_use]
730    pub fn implementation(&self) -> &T {
731        &self.implementation
732    }
733
734    /// Builds metadata from the explicit exposure set in insertion order.
735    ///
736    /// This path-free projection is useful while composing a publication, but is
737    /// not proof that its scopes are currently published. Use
738    /// [`ObjectProvider::observe_object`] for a validated current observation, or
739    /// [`Self::protocol_object_at`] inside a provider's coherent boundary.
740    /// The metadata contains no path, children, descriptor, backend handle,
741    /// credential, or provider resolution state.
742    #[must_use]
743    pub fn protocol_object(&self) -> Object {
744        Object {
745            name: self.implementation.name().to_owned(),
746            description: self.implementation.description().map(str::to_owned),
747            interfaces: self
748                .interfaces
749                .iter()
750                .map(|bound| bound.artifact.reference.protocol_reference())
751                .collect(),
752            r#ref: self.implementation.reference().map(str::to_owned),
753            validator: self
754                .implementation
755                .validator()
756                .map(|validator| validator.0.clone()),
757        }
758    }
759
760    /// Validates a current Object observation inside the provider's boundary.
761    ///
762    /// All exposed interfaces must have compatible, live scope associations. The
763    /// provider must still establish those associations atomically; this method
764    /// cannot verify backend lifetime assertions or lock external writers.
765    pub fn protocol_object_at(&self, path: &str) -> Result<Object, CallError> {
766        wip_protocol::validate_path(path)
767            .map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
768        if !self.scopes_valid_at(path) {
769            return Err(CallError::new(
770                CallErrorKind::Internal,
771                "Host published interfaces without current compatible scope Objects",
772            ));
773        }
774        let object = self.protocol_object();
775        object.validate_at_path(path).map_err(|_| {
776            CallError::new(
777                CallErrorKind::Internal,
778                "Host produced an invalid object response",
779            )
780        })?;
781        Ok(object)
782    }
783
784    fn dispatch_resolved(
785        &self,
786        context: &ExecutionContext,
787        request: CallRequest,
788        authorize: impl FnOnce(&T, &InterfaceRef, &str) -> bool,
789    ) -> OperationAttempt {
790        let CallRequest {
791            target,
792            interface,
793            operation,
794            arguments,
795        } = request;
796        self.dispatch_resolved_with_decoder(
797            context,
798            target,
799            interface,
800            operation,
801            |_| Ok::<_, std::convert::Infallible>(arguments),
802            authorize,
803        )
804    }
805
806    fn dispatch_resolved_with_decoder<E>(
807        &self,
808        context: &ExecutionContext,
809        target: Target,
810        interface: InterfaceTarget,
811        operation: String,
812        decode_arguments: impl FnOnce(&InterfaceDescriptor) -> Result<BTreeMap<String, Value>, E>,
813        authorize: impl FnOnce(&T, &InterfaceRef, &str) -> bool,
814    ) -> OperationAttempt {
815        let pre_dispatch_failure =
816            |kind, message: &str| OperationAttempt::NotStarted(CallError::new(kind, message));
817        let current_object_validator = self.implementation.validator();
818        if let Some(supplied) = target.validator.as_deref()
819            && current_object_validator.map(Validator::as_bytes) != Some(supplied)
820        {
821            return pre_dispatch_failure(
822                CallErrorKind::ValidatorMismatch,
823                "object validator does not match current object state",
824            );
825        }
826
827        if interface.reference.validate_for_path(&target.path).is_err() {
828            return pre_dispatch_failure(
829                CallErrorKind::InterfaceMismatch,
830                "interface scope is not an ancestor of the target path",
831            );
832        }
833
834        let Some(bound) = self
835            .interfaces
836            .iter()
837            .find(|bound| bound.artifact.reference.protocol_reference() == interface.reference)
838        else {
839            return pre_dispatch_failure(
840                CallErrorKind::InterfaceMismatch,
841                "interface is not a member of the current object publication",
842            );
843        };
844
845        let Some(current_scope) = self.current_scope(&bound.artifact.scope) else {
846            return pre_dispatch_failure(
847                CallErrorKind::InterfaceMismatch,
848                "interface scope Object is not currently associated with this publication",
849            );
850        };
851        if current_scope.scope.path == target.path
852            && current_scope.reference() != self.implementation.reference()
853        {
854            return pre_dispatch_failure(
855                CallErrorKind::Internal,
856                "provider produced inconsistent self-scope metadata",
857            );
858        }
859        if interface
860            .scope_ref
861            .as_deref()
862            .is_some_and(|supplied| current_scope.reference() != Some(supplied))
863        {
864            return pre_dispatch_failure(
865                CallErrorKind::InterfaceMismatch,
866                "scope Object ref does not match the current association",
867            );
868        }
869
870        match (
871            bound.artifact.validator.as_ref(),
872            interface.validator.as_deref(),
873        ) {
874            (Some(_), None) => {
875                return pre_dispatch_failure(
876                    CallErrorKind::InterfaceValidatorRequired,
877                    "registered interface requires a validator",
878                );
879            }
880            (expected, Some(actual)) if expected.map(Validator::as_bytes) != Some(actual) => {
881                return pre_dispatch_failure(
882                    CallErrorKind::InterfaceValidatorMismatch,
883                    "interface validator does not match the registered descriptor",
884                );
885            }
886            _ => {}
887        }
888
889        if !bound
890            .artifact
891            .descriptor
892            .operations
893            .iter()
894            .any(|declared| declared.name == operation)
895        {
896            return pre_dispatch_failure(
897                CallErrorKind::OperationNotFound,
898                &format!("operation `{operation}` is not declared"),
899            );
900        }
901
902        if target.validator.is_none()
903            && self
904                .implementation
905                .requires_validator(&bound.artifact.reference, &operation)
906        {
907            return pre_dispatch_failure(
908                CallErrorKind::ValidatorRequired,
909                "this operation requires an object validator",
910            );
911        }
912
913        let arguments = match decode_arguments(&bound.artifact.descriptor) {
914            Ok(arguments) => arguments,
915            Err(_) => {
916                return pre_dispatch_failure(
917                    CallErrorKind::InvalidArguments,
918                    "operation arguments could not be decoded",
919                );
920            }
921        };
922        if let Err(error) = bound
923            .artifact
924            .descriptor
925            .validate_arguments(&operation, &arguments)
926        {
927            return pre_dispatch_failure(CallErrorKind::InvalidArguments, &error.to_string());
928        }
929
930        if !authorize(&self.implementation, &bound.artifact.reference, &operation) {
931            return pre_dispatch_failure(
932                CallErrorKind::PermissionDenied,
933                "operation is not permitted for this caller",
934            );
935        }
936
937        if let Err(error) = context.preflight() {
938            return OperationAttempt::NotStarted(error);
939        }
940
941        let value = match (bound.dispatch)(&self.implementation, &operation, arguments) {
942            Ok(value) => value,
943            Err(
944                error @ (DispatchError::UnknownOperation(_)
945                | DispatchError::InvalidArguments(_)
946                | DispatchError::Decode(_)),
947            ) => return OperationAttempt::NotStarted(CallError::from_pre_dispatch(error)),
948            Err(error) => {
949                return OperationAttempt::Started(Err(OperationFailure::from_dispatch(error)));
950            }
951        };
952
953        if bound
954            .artifact
955            .descriptor
956            .validate_result(&operation, &value)
957            .is_err()
958        {
959            return OperationAttempt::Started(Err(OperationFailure::internal(
960                EffectGuarantee::Unknown,
961                "operation implementation produced an invalid result",
962            )));
963        }
964
965        OperationAttempt::Started(Ok(CallResult {
966            result: value,
967            validator: self
968                .implementation
969                .validator()
970                .map(|validator| validator.0.clone()),
971        }))
972    }
973
974    /// Consumes the publication and returns its host-private implementation.
975    #[must_use]
976    pub fn into_inner(self) -> T {
977        self.implementation
978    }
979}
980
981/// A post-invocation failure awaiting final provider effect disposition.
982///
983/// Providers may upgrade an unknown failure with [`confirm_rollback`](Self::confirm_rollback)
984/// only after the backend transaction has definitely rolled back. Diagnostics are
985/// intentionally generic and safe for protocol exposure.
986#[derive(Debug, Clone, PartialEq, Eq)]
987pub struct OperationFailure {
988    kind: HostErrorKind,
989    effect_guarantee: EffectGuarantee,
990    message: String,
991}
992
993impl OperationFailure {
994    fn internal(effect_guarantee: EffectGuarantee, message: impl Into<String>) -> Self {
995        Self {
996            kind: HostErrorKind::Internal,
997            effect_guarantee,
998            message: bounded_message(message.into()),
999        }
1000    }
1001
1002    fn from_dispatch(error: DispatchError) -> Self {
1003        match error {
1004            DispatchError::Host(error) => Self {
1005                kind: error.kind(),
1006                effect_guarantee: error.effect_guarantee(),
1007                message: match error.kind() {
1008                    HostErrorKind::Internal => "operation implementation failed".into(),
1009                    HostErrorKind::ResourceLimitExceeded => {
1010                        "operation resource limit was exceeded".into()
1011                    }
1012                },
1013            },
1014            DispatchError::Panic => Self::internal(
1015                EffectGuarantee::Unknown,
1016                "operation implementation panicked",
1017            ),
1018            DispatchError::Encode(_) => Self::internal(
1019                EffectGuarantee::Unknown,
1020                "operation result could not be encoded",
1021            ),
1022            DispatchError::InvalidResult(_) => Self::internal(
1023                EffectGuarantee::Unknown,
1024                "operation implementation produced an invalid result",
1025            ),
1026            DispatchError::UnknownOperation(_)
1027            | DispatchError::InvalidArguments(_)
1028            | DispatchError::Decode(_) => {
1029                unreachable!("pre-dispatch failures are classified before OperationFailure")
1030            }
1031        }
1032    }
1033
1034    /// Returns the Host failure category used when effects are not committed.
1035    #[must_use]
1036    pub fn kind(&self) -> HostErrorKind {
1037        self.kind
1038    }
1039
1040    /// Returns the current effect guarantee.
1041    #[must_use]
1042    pub fn effect_guarantee(&self) -> EffectGuarantee {
1043        self.effect_guarantee
1044    }
1045
1046    /// Confirms that the provider rolled back all operation effects.
1047    ///
1048    /// Calling this method is a semantic assertion by the provider and must occur
1049    /// only after its atomic backend boundary has completed rollback.
1050    #[must_use]
1051    pub fn confirm_rollback(mut self) -> Self {
1052        self.effect_guarantee = EffectGuarantee::NotCommitted;
1053        self
1054    }
1055}
1056
1057/// Dispatch progress returned to the provider's atomic execution boundary.
1058#[derive(Debug)]
1059pub enum OperationAttempt {
1060    /// A canonical precondition failed and typed implementation code did not run.
1061    NotStarted(CallError),
1062    /// Typed implementation invocation began, with either a result or a typed
1063    /// post-invocation failure.
1064    Started(Result<CallResult, OperationFailure>),
1065}
1066
1067impl OperationAttempt {
1068    /// Rejects a provider precondition before typed implementation invocation.
1069    ///
1070    /// Only use this before dispatch starts; post-invocation failures require
1071    /// an explicit effect disposition. The diagnostic is intentionally generic.
1072    #[must_use]
1073    pub fn reject(kind: CallErrorKind) -> Self {
1074        Self::NotStarted(CallError::new(kind, "provider precondition failed"))
1075    }
1076
1077    /// Marks a post-invocation failure as fully rolled back.
1078    ///
1079    /// Successful attempts and pre-dispatch failures are returned unchanged.
1080    #[must_use]
1081    pub fn confirm_rollback(self) -> Self {
1082        match self {
1083            Self::Started(Err(error)) => Self::Started(Err(error.confirm_rollback())),
1084            other => other,
1085        }
1086    }
1087}
1088
1089/// Result of one provider resolution or execution boundary.
1090#[derive(Debug)]
1091pub enum ProviderResult<T> {
1092    /// The path resolved and the provider callback completed.
1093    Found(T),
1094    /// The path is not currently published to this caller.
1095    NotFound,
1096    /// Provider or backend execution failed.
1097    ///
1098    /// During `call_operation`, the embedded effect guarantee determines whether
1099    /// the failure is safe to expose as `Internal`/`ResourceLimitExceeded` or must
1100    /// become `OperationOutcomeUnknown`.
1101    Failure(HostError),
1102}
1103
1104/// A request-scoped Worldspace resolver and execution boundary.
1105///
1106/// `with_object` must resolve the current object at `path` exactly once and pass
1107/// a publication that pins its current interface set, implementations, and
1108/// backend target and all current scope Object associations for the duration of
1109/// `operation`. Attach them with PublishedObject::bind_scope inside this same
1110/// boundary, never via an independent scope prelookup. A new Object at the same
1111/// path needs a fresh Scope; ordinary state changes retain the handle. Providers
1112/// own lifetime/atomicity enforcement, including external writers and backend
1113/// commit/rollback. The execution context is the
1114/// runtime-neutral carrier for cancellation, deadline, and resource controls.
1115/// The borrowed publication cannot escape the callback.
1116///
1117/// Providers that can roll back mutable operations should override
1118/// [`ObjectProvider::execute_operation`], hold their transaction across the
1119/// callback, and call [`OperationAttempt::confirm_rollback`] only after rollback
1120/// succeeds. The default boundary conservatively preserves unknown outcomes.
1121pub trait ObjectProvider {
1122    /// Host-private object type produced by this provider.
1123    type Object: WipObject;
1124
1125    /// Runs a callback against the single object snapshot resolved at `path`.
1126    fn with_object<R>(
1127        &self,
1128        context: &ExecutionContext,
1129        path: &str,
1130        operation: impl FnOnce(&PublishedObject<Self::Object>) -> R,
1131    ) -> ProviderResult<R>;
1132
1133    /// Runs one operation attempt inside the provider's atomic backend boundary.
1134    ///
1135    /// The default implementation delegates to [`ObjectProvider::with_object`]
1136    /// and therefore does not add a rollback guarantee. Transactional providers
1137    /// can override this method, inspect the returned [`OperationAttempt`], roll
1138    /// back a failed started attempt, and return its confirmed form.
1139    fn execute_operation(
1140        &self,
1141        context: &ExecutionContext,
1142        path: &str,
1143        operation: impl FnOnce(&PublishedObject<Self::Object>) -> OperationAttempt,
1144    ) -> ProviderResult<OperationAttempt> {
1145        self.with_object(context, path, operation)
1146    }
1147
1148    /// Decides whether a declared operation may execute for the current caller.
1149    ///
1150    /// The hook runs after all validators, membership, operation lookup, and
1151    /// argument validation, but before implementation code. Returning `false`
1152    /// exposes the operation while producing `PermissionDenied`.
1153    fn authorize(
1154        &self,
1155        _path: &str,
1156        _object: &Self::Object,
1157        _interface: &InterfaceRef,
1158        _operation: &str,
1159    ) -> bool {
1160        true
1161    }
1162
1163    /// Observes the canonical object projection currently published at `path`.
1164    fn observe_object(&self, path: &str) -> Result<Object, CallError> {
1165        self.observe_object_with_context(&ExecutionContext::default(), path)
1166    }
1167
1168    /// Observes an object while propagating runtime execution controls.
1169    fn observe_object_with_context(
1170        &self,
1171        context: &ExecutionContext,
1172        path: &str,
1173    ) -> Result<Object, CallError> {
1174        wip_protocol::validate_path(path)
1175            .map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
1176        let resolved = catch_unwind(AssertUnwindSafe(|| {
1177            self.with_object(context, path, |publication| {
1178                publication.protocol_object_at(path)
1179            })
1180        }))
1181        .map_err(|_| {
1182            CallError::new(
1183                CallErrorKind::Internal,
1184                "provider panicked while observing an object",
1185            )
1186        })?;
1187        let object = match resolved {
1188            ProviderResult::Found(object) => object?,
1189            ProviderResult::NotFound => {
1190                return Err(CallError::new(
1191                    CallErrorKind::NotFound,
1192                    "target path is not published",
1193                ));
1194            }
1195            ProviderResult::Failure(error) => return Err(CallError::from_read_failure(error)),
1196        };
1197        Ok(object)
1198    }
1199
1200    /// Fetches Descriptor, scope Object ref and Interface validator atomically.
1201    ///
1202    /// The scope Object may expose no interfaces. Its publication must still
1203    /// bind the live ScopeObservation. Registry retention is not publication.
1204    fn fetch_interface(
1205        &self,
1206        registry: &InterfaceRegistry,
1207        reference: &InterfaceReference,
1208    ) -> Result<InterfaceObservation, ProtocolError> {
1209        reference.validate().map_err(|error| ProtocolError {
1210            code: ProtocolErrorCode::InvalidRequest,
1211            message: error.to_string(),
1212        })?;
1213        let observed = catch_unwind(AssertUnwindSafe(|| {
1214            self.with_object(
1215                &ExecutionContext::default(),
1216                &reference.scope,
1217                |publication| {
1218                    publication
1219                        .scopes
1220                        .iter()
1221                        .find(|current| current.scope.path == reference.scope)
1222                        .ok_or_else(interface_not_found)
1223                        .and_then(|current| {
1224                            if current.reference() != publication.implementation.reference() {
1225                                return Err(ProtocolError {
1226                                    code: ProtocolErrorCode::Internal,
1227                                    message: "provider produced inconsistent scope Object metadata"
1228                                        .into(),
1229                                });
1230                            }
1231                            registry.fetch_interface(reference, current)
1232                        })
1233                },
1234            )
1235        }))
1236        .map_err(|_| ProtocolError {
1237            code: ProtocolErrorCode::Internal,
1238            message: "provider panicked while observing an interface".into(),
1239        })?;
1240        match observed {
1241            ProviderResult::Found(observation) => observation,
1242            ProviderResult::NotFound => Err(interface_not_found()),
1243            ProviderResult::Failure(error) => {
1244                Err(CallError::from_read_failure(error).protocol_error())
1245            }
1246        }
1247    }
1248
1249    /// Resolves and executes a canonical path-targeted operation request.
1250    ///
1251    /// Structural path validation precedes provider resolution. For a valid
1252    /// request, failure checks follow the exact canonical precedence: path,
1253    /// supplied object validator mismatch, membership, interface validator
1254    /// requirement and mismatch, operation existence, object validator
1255    /// requirement, arguments, then authorization. No failed precondition
1256    /// invokes typed implementation code.
1257    fn call_operation(&self, request: CallRequest) -> Result<CallResult, CallError> {
1258        self.call_operation_with_context(&ExecutionContext::default(), request)
1259    }
1260
1261    /// Executes an operation while propagating cancellation, deadline, and
1262    /// resource-budget controls to the provider and runtime preflight check.
1263    fn call_operation_with_context(
1264        &self,
1265        context: &ExecutionContext,
1266        request: CallRequest,
1267    ) -> Result<CallResult, CallError> {
1268        wip_protocol::validate_path(&request.target.path)
1269            .map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
1270        request
1271            .interface
1272            .reference
1273            .validate()
1274            .map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
1275        let path = request.target.path.clone();
1276        let execution = catch_unwind(AssertUnwindSafe(|| {
1277            self.execute_operation(context, &path, |publication| {
1278                publication.dispatch_resolved(context, request, |object, interface, operation| {
1279                    self.authorize(&path, object, interface, operation)
1280                })
1281            })
1282        }));
1283
1284        let execution = execution.map_err(|_| {
1285            CallError::from_operation_failure(OperationFailure::internal(
1286                EffectGuarantee::Unknown,
1287                "provider execution panicked",
1288            ))
1289        })?;
1290        match execution {
1291            ProviderResult::Found(OperationAttempt::NotStarted(error)) => Err(error),
1292            ProviderResult::Found(OperationAttempt::Started(Ok(result))) => Ok(result),
1293            ProviderResult::Found(OperationAttempt::Started(Err(error))) => {
1294                Err(CallError::from_operation_failure(error))
1295            }
1296            ProviderResult::NotFound => Err(CallError::new(
1297                CallErrorKind::NotFound,
1298                "target path is not published",
1299            )),
1300            ProviderResult::Failure(error) => Err(CallError::from_host_failure(error)),
1301        }
1302    }
1303
1304    /// Resolves one operation target and decodes its arguments only after all
1305    /// descriptor-independent canonical preconditions have succeeded.
1306    ///
1307    /// Transport bindings whose value representation depends on the selected
1308    /// interface descriptor should use this method instead of resolving a
1309    /// descriptor before [`ObjectProvider::call_operation_with_context`]. The
1310    /// callback executes inside the same exact-once provider boundary after
1311    /// object/interface validators, membership, operation lookup, and required
1312    /// object-validator checks. A callback error becomes `InvalidArguments` and
1313    /// typed implementation code is not invoked.
1314    fn call_operation_with_context_and_decoder<E>(
1315        &self,
1316        context: &ExecutionContext,
1317        target: Target,
1318        interface: InterfaceTarget,
1319        operation: String,
1320        decode_arguments: impl FnOnce(&InterfaceDescriptor) -> Result<BTreeMap<String, Value>, E>,
1321    ) -> Result<CallResult, CallError> {
1322        wip_protocol::validate_path(&target.path)
1323            .map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
1324        interface
1325            .reference
1326            .validate()
1327            .map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
1328        let path = target.path.clone();
1329        let execution = catch_unwind(AssertUnwindSafe(|| {
1330            self.execute_operation(context, &path, |publication| {
1331                publication.dispatch_resolved_with_decoder(
1332                    context,
1333                    target,
1334                    interface,
1335                    operation,
1336                    decode_arguments,
1337                    |object, interface, operation| {
1338                        self.authorize(&path, object, interface, operation)
1339                    },
1340                )
1341            })
1342        }));
1343
1344        let execution = execution.map_err(|_| {
1345            CallError::from_operation_failure(OperationFailure::internal(
1346                EffectGuarantee::Unknown,
1347                "provider execution panicked",
1348            ))
1349        })?;
1350        match execution {
1351            ProviderResult::Found(OperationAttempt::NotStarted(error)) => Err(error),
1352            ProviderResult::Found(OperationAttempt::Started(Ok(result))) => Ok(result),
1353            ProviderResult::Found(OperationAttempt::Started(Err(error))) => {
1354                Err(CallError::from_operation_failure(error))
1355            }
1356            ProviderResult::NotFound => Err(CallError::new(
1357                CallErrorKind::NotFound,
1358                "target path is not published",
1359            )),
1360            ProviderResult::Failure(error) => Err(CallError::from_host_failure(error)),
1361        }
1362    }
1363}
1364
1365/// Failure to construct an explicit object publication.
1366#[derive(Debug, Clone, PartialEq, Eq)]
1367pub enum PublicationError {
1368    /// The same scope/name Interface reference was exposed more than once.
1369    DuplicateInterface(InterfaceRef),
1370    /// Projection attempted to add an interface without an implementation.
1371    UnexposedInterface(InterfaceRef),
1372}
1373
1374impl Display for PublicationError {
1375    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1376        match self {
1377            Self::UnexposedInterface(reference) => write!(
1378                formatter,
1379                "interface `{}` has no implementation",
1380                reference.as_str()
1381            ),
1382            Self::DuplicateInterface(reference) => write!(
1383                formatter,
1384                "interface `{}` is already exposed by this object",
1385                reference.as_str()
1386            ),
1387        }
1388    }
1389}
1390
1391impl Error for PublicationError {}
1392
1393#[cfg(test)]
1394mod projection_tests {
1395    use super::*;
1396    use wip_protocol::{
1397        Documentation, OperationDeclaration, ReturnDeclaration, TypeDeclaration, TypeExpr,
1398    };
1399
1400    #[test]
1401    fn operation_views_prune_hidden_types_and_documentation_without_mutating_registry() {
1402        let named = |name: &str| TypeExpr::Named { name: name.into() };
1403        let operation = |name: &str, returns| OperationDeclaration {
1404            name: name.into(),
1405            documentation: None,
1406            parameters: Vec::new(),
1407            returns: ReturnDeclaration {
1408                documentation: None,
1409                r#type: returns,
1410            },
1411        };
1412        let declaration = |name: &str, definition| TypeDeclaration {
1413            name: name.into(),
1414            documentation: None,
1415            definition,
1416        };
1417        let descriptor = InterfaceDescriptor {
1418            format: "wip/1".into(),
1419            documentation: Some(Documentation {
1420                summary: "contains hidden operation details".into(),
1421                details: None,
1422            }),
1423            operations: vec![
1424                operation("visible", named("Public")),
1425                operation("secret", named("Secret")),
1426            ],
1427            types: vec![
1428                declaration(
1429                    "Public",
1430                    TypeExpr::List {
1431                        items: Box::new(named("Shared")),
1432                    },
1433                ),
1434                declaration("Shared", TypeExpr::String),
1435                declaration("Secret", TypeExpr::Bytes),
1436            ],
1437        };
1438        let mut projected = descriptor.clone();
1439        restrict_descriptor(&mut projected, |operation| operation == "visible");
1440        assert_eq!(projected.operations.len(), 1);
1441        assert!(projected.documentation.is_none());
1442        assert_eq!(
1443            projected
1444                .types
1445                .iter()
1446                .map(|declaration| declaration.name.as_str())
1447                .collect::<Vec<_>>(),
1448            vec!["Public", "Shared"]
1449        );
1450        assert_eq!(descriptor.types.len(), 3);
1451        restrict_descriptor(&mut projected, |_| false);
1452        assert!(projected.types.is_empty());
1453        assert!(projected.operations.is_empty());
1454    }
1455}
1456
1457/// Stable Host dispatch failure kind.
1458#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1459pub enum CallErrorKind {
1460    /// The request is structurally invalid.
1461    InvalidRequest,
1462    /// The target path is not currently published.
1463    NotFound,
1464    /// The selected interface is not exposed by the current object.
1465    InterfaceMismatch,
1466    /// The operation is absent from the selected descriptor.
1467    OperationNotFound,
1468    /// Supplied named values do not satisfy the operation declaration.
1469    InvalidArguments,
1470    /// A bounded cancellation, deadline, or resource limit was exceeded and the
1471    /// operation effects are guaranteed not committed.
1472    ResourceLimitExceeded,
1473    /// The Host failed and operation effects are guaranteed not committed.
1474    Internal,
1475    /// The Host cannot guarantee whether operation effects were committed.
1476    OperationOutcomeUnknown,
1477    /// This operation requires a validator and the call omitted it.
1478    ValidatorRequired,
1479    /// The supplied object validator is stale or belongs to another object.
1480    ValidatorMismatch,
1481    /// The selected mutable interface requires its observed validator.
1482    InterfaceValidatorRequired,
1483    /// The supplied interface validator does not identify the current descriptor.
1484    InterfaceValidatorMismatch,
1485    /// The operation is declared but not authorized for the caller.
1486    PermissionDenied,
1487}
1488
1489/// A dispatch failure with a stable kind and bounded non-normative diagnostic.
1490#[derive(Debug, Clone, PartialEq, Eq)]
1491pub struct CallError {
1492    kind: CallErrorKind,
1493    message: String,
1494}
1495
1496impl CallError {
1497    fn new(kind: CallErrorKind, message: impl Into<String>) -> Self {
1498        Self {
1499            kind,
1500            message: bounded_message(message.into()),
1501        }
1502    }
1503
1504    fn from_pre_dispatch(error: DispatchError) -> Self {
1505        match error {
1506            DispatchError::UnknownOperation(operation) => Self::new(
1507                CallErrorKind::OperationNotFound,
1508                format!("operation `{operation}` is not declared"),
1509            ),
1510            DispatchError::InvalidArguments(error) => {
1511                Self::new(CallErrorKind::InvalidArguments, error.to_string())
1512            }
1513            DispatchError::Decode(_) => Self::new(
1514                CallErrorKind::InvalidArguments,
1515                "operation arguments could not be decoded",
1516            ),
1517            DispatchError::Host(_)
1518            | DispatchError::Panic
1519            | DispatchError::Encode(_)
1520            | DispatchError::InvalidResult(_) => {
1521                unreachable!("post-invocation failure used as a pre-dispatch failure")
1522            }
1523        }
1524    }
1525
1526    fn from_read_failure(error: HostError) -> Self {
1527        match error.kind() {
1528            HostErrorKind::Internal => Self::new(
1529                CallErrorKind::Internal,
1530                "provider failed while fetching data",
1531            ),
1532            HostErrorKind::ResourceLimitExceeded => Self::new(
1533                CallErrorKind::ResourceLimitExceeded,
1534                "provider resource limit was exceeded while fetching data",
1535            ),
1536        }
1537    }
1538
1539    fn from_host_failure(error: HostError) -> Self {
1540        let kind = classified_host_kind(error.kind(), error.effect_guarantee());
1541        let message = match error.kind() {
1542            HostErrorKind::Internal => "provider execution failed",
1543            HostErrorKind::ResourceLimitExceeded => {
1544                "provider execution resource limit was exceeded"
1545            }
1546        };
1547        Self::new(kind, message)
1548    }
1549
1550    fn from_operation_failure(error: OperationFailure) -> Self {
1551        Self::new(
1552            classified_host_kind(error.kind, error.effect_guarantee),
1553            error.message,
1554        )
1555    }
1556
1557    /// Returns the stable failure kind.
1558    #[must_use]
1559    pub fn kind(&self) -> CallErrorKind {
1560        self.kind
1561    }
1562
1563    /// Returns the bounded protocol-safe human-readable diagnostic.
1564    #[must_use]
1565    pub fn message(&self) -> &str {
1566        &self.message
1567    }
1568
1569    /// Maps this Host failure to the canonical protocol error taxonomy.
1570    #[must_use]
1571    pub fn protocol_error(&self) -> ProtocolError {
1572        let code = match self.kind {
1573            CallErrorKind::InvalidRequest => ProtocolErrorCode::InvalidRequest,
1574            CallErrorKind::NotFound => ProtocolErrorCode::NotFound,
1575            CallErrorKind::InterfaceMismatch => ProtocolErrorCode::InterfaceMismatch,
1576            CallErrorKind::OperationNotFound => ProtocolErrorCode::OperationNotFound,
1577            CallErrorKind::InvalidArguments => ProtocolErrorCode::InvalidArguments,
1578            CallErrorKind::ResourceLimitExceeded => ProtocolErrorCode::ResourceLimitExceeded,
1579            CallErrorKind::Internal => ProtocolErrorCode::Internal,
1580            CallErrorKind::OperationOutcomeUnknown => ProtocolErrorCode::OperationOutcomeUnknown,
1581            CallErrorKind::ValidatorRequired => ProtocolErrorCode::ValidatorRequired,
1582            CallErrorKind::ValidatorMismatch => ProtocolErrorCode::ValidatorMismatch,
1583            CallErrorKind::InterfaceValidatorRequired => {
1584                ProtocolErrorCode::InterfaceValidatorRequired
1585            }
1586            CallErrorKind::InterfaceValidatorMismatch => {
1587                ProtocolErrorCode::InterfaceValidatorMismatch
1588            }
1589            CallErrorKind::PermissionDenied => ProtocolErrorCode::PermissionDenied,
1590        };
1591        ProtocolError {
1592            code,
1593            message: self.message.clone(),
1594        }
1595    }
1596}
1597
1598fn classified_host_kind(kind: HostErrorKind, effect_guarantee: EffectGuarantee) -> CallErrorKind {
1599    match effect_guarantee {
1600        EffectGuarantee::Unknown => CallErrorKind::OperationOutcomeUnknown,
1601        EffectGuarantee::NotCommitted => match kind {
1602            HostErrorKind::Internal => CallErrorKind::Internal,
1603            HostErrorKind::ResourceLimitExceeded => CallErrorKind::ResourceLimitExceeded,
1604        },
1605    }
1606}
1607
1608const MAX_PROTOCOL_MESSAGE_BYTES: usize = 512;
1609
1610fn bounded_message(mut message: String) -> String {
1611    if message.len() <= MAX_PROTOCOL_MESSAGE_BYTES {
1612        return message;
1613    }
1614    let mut end = MAX_PROTOCOL_MESSAGE_BYTES;
1615    while !message.is_char_boundary(end) {
1616        end -= 1;
1617    }
1618    message.truncate(end);
1619    message
1620}
1621
1622impl Display for CallError {
1623    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1624        write!(
1625            formatter,
1626            "host dispatch failed ({:?}): {}",
1627            self.kind, self.message
1628        )
1629    }
1630}
1631
1632impl Error for CallError {}