Skip to main content

gate4agent_tool_protocol/
lib.rs

1//! Dependency-light, versioned contracts for the Gate4Agent capability plane.
2//!
3//! These types perform no I/O and grant no authority by themselves. The tool
4//! engine validates every deserialized envelope again at its trusted ingress.
5
6use gate4agent_types::{AgentInstanceId, SessionGeneration};
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10pub const TOOL_ACTOR_ID_MAX_BYTES: usize = 256;
11pub const TOOL_CONSUMER_ID_MAX_BYTES: usize = 256;
12pub const TOOL_PROVIDER_ID_MAX_BYTES: usize = 256;
13pub const TOOL_CAPABILITY_ID_MAX_BYTES: usize = 256;
14pub const TOOL_RESOURCE_SCOPE_ID_MAX_BYTES: usize = 512;
15pub const TOOL_CAPABILITY_DESCRIPTION_MAX_BYTES: usize = 2_048;
16pub const TOOL_APPROVAL_SUMMARY_MAX_BYTES: usize = 1_024;
17pub const TOOL_PAYLOAD_MAX_BYTES: usize = 64 * 1_024;
18pub const TOOL_RESULT_MAX_BYTES: u64 = 16 * 1_024 * 1_024;
19pub const TOOL_INLINE_RESULT_MAX_BYTES: usize = 256 * 1_024;
20pub const TOOL_RESULT_REFERENCE_MAX_BYTES: usize = 2_048;
21pub const TOOL_RESULT_SUMMARY_MAX_BYTES: usize = 4_096;
22pub const TOOL_FAILURE_MESSAGE_MAX_BYTES: usize = 4_096;
23pub const TOOL_MEDIA_TYPE_MAX_BYTES: usize = 256;
24pub const TOOL_PROVIDERS_MAX: usize = 64;
25pub const TOOL_CAPABILITIES_PER_PROVIDER_MAX: usize = 256;
26pub const TOOL_POLICIES_MAX: usize = 4_096;
27pub const TOOL_REQUESTS_MAX: usize = 512;
28pub const TOOL_ACTIVE_REQUESTS_PER_CLIENT_MAX: usize = 32;
29pub const TOOL_EFFECTS_MAX: usize = TOOL_REQUESTS_MAX * 2;
30pub const TOOL_COMPLETIONS_MAX: usize = 128;
31pub const TOOL_AUDIT_EVENTS_MAX: usize = 4_096;
32macro_rules! bounded_id {
33    ($name:ident, $field:literal, $max:expr) => {
34        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
35        #[serde(try_from = "String", into = "String")]
36        pub struct $name(String);
37
38        impl $name {
39            pub fn new(value: impl Into<String>) -> Result<Self, ToolValidationError> {
40                let value = value.into();
41                validate_identifier($field, &value, $max)?;
42                Ok(Self(value))
43            }
44
45            pub fn as_str(&self) -> &str {
46                &self.0
47            }
48        }
49
50        impl fmt::Display for $name {
51            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52                formatter.write_str(&self.0)
53            }
54        }
55
56        impl TryFrom<String> for $name {
57            type Error = ToolValidationError;
58
59            fn try_from(value: String) -> Result<Self, Self::Error> {
60                Self::new(value)
61            }
62        }
63
64        impl From<$name> for String {
65            fn from(value: $name) -> Self {
66                value.0
67            }
68        }
69    };
70}
71
72bounded_id!(ToolActorId, "tool actor id", TOOL_ACTOR_ID_MAX_BYTES);
73bounded_id!(ConsumerId, "tool consumer id", TOOL_CONSUMER_ID_MAX_BYTES);
74bounded_id!(
75    ResourceScopeId,
76    "tool resource scope id",
77    TOOL_RESOURCE_SCOPE_ID_MAX_BYTES
78);
79bounded_id!(
80    ToolProviderId,
81    "tool provider id",
82    TOOL_PROVIDER_ID_MAX_BYTES
83);
84bounded_id!(
85    ToolCapabilityId,
86    "tool capability id",
87    TOOL_CAPABILITY_ID_MAX_BYTES
88);
89
90#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
91#[serde(transparent)]
92pub struct CapabilityRequestId(pub u64);
93
94#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
95#[serde(transparent)]
96pub struct ToolOperationId(pub u64);
97
98#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
99#[serde(transparent)]
100pub struct ProviderBindingId(pub u64);
101
102/// Provider ownership is an admission boundary. `Consumer(owner)` can serve
103/// only requests whose exact `consumer_id` equals `owner`; `Gate` may be
104/// shared, but only through an otherwise exact policy grant.
105#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
106#[serde(rename_all = "kebab-case")]
107pub enum CapabilityOwner {
108    Gate,
109    Consumer(ConsumerId),
110}
111
112#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
113#[serde(rename_all = "kebab-case")]
114pub enum CapabilityClass {
115    Browser,
116    ConsumerState,
117}
118
119#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
120pub struct CapabilityDescriptor {
121    pub id: ToolCapabilityId,
122    pub class: CapabilityClass,
123    pub description: String,
124}
125
126impl CapabilityDescriptor {
127    pub fn new(
128        id: ToolCapabilityId,
129        class: CapabilityClass,
130        description: impl Into<String>,
131    ) -> Result<Self, ToolValidationError> {
132        let descriptor = Self {
133            id,
134            class,
135            description: description.into(),
136        };
137        descriptor.validate_admission()?;
138        validate_required_text(
139            "tool capability description",
140            &descriptor.description,
141            TOOL_CAPABILITY_DESCRIPTION_MAX_BYTES,
142        )?;
143        Ok(descriptor)
144    }
145
146    pub fn validate_admission(&self) -> Result<(), ToolValidationError> {
147        let normalized = self.id.as_str().to_ascii_lowercase();
148        let has_forbidden_namespace = has_forbidden_capability_namespace(&normalized);
149        let prefix = match self.class {
150            CapabilityClass::Browser => "browser.",
151            CapabilityClass::ConsumerState => "consumer-state.",
152        };
153        if has_forbidden_namespace || !normalized.starts_with(prefix) {
154            return Err(ToolValidationError::CapabilityOutsideAdmission {
155                capability_id: self.id.clone(),
156            });
157        }
158        Ok(())
159    }
160}
161
162#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
163pub struct CapabilityProviderDescriptor {
164    pub id: ToolProviderId,
165    pub owner: CapabilityOwner,
166    pub capabilities: Vec<CapabilityDescriptor>,
167}
168
169impl CapabilityProviderDescriptor {
170    pub fn validate(&self) -> Result<(), ToolValidationError> {
171        if self.capabilities.is_empty() {
172            return Err(ToolValidationError::Required {
173                field: "tool provider capabilities",
174            });
175        }
176        if self.capabilities.len() > TOOL_CAPABILITIES_PER_PROVIDER_MAX {
177            return Err(ToolValidationError::TooMany {
178                field: "tool provider capabilities",
179                max: TOOL_CAPABILITIES_PER_PROVIDER_MAX,
180                actual: self.capabilities.len(),
181            });
182        }
183        let mut ids = self
184            .capabilities
185            .iter()
186            .map(|capability| capability.id.clone())
187            .collect::<Vec<_>>();
188        ids.sort();
189        if ids.windows(2).any(|pair| pair[0] == pair[1]) {
190            return Err(ToolValidationError::DuplicateIdentifier {
191                field: "tool capability id",
192            });
193        }
194        for capability in &self.capabilities {
195            capability.validate_admission()?;
196            validate_required_text(
197                "tool capability description",
198                &capability.description,
199                TOOL_CAPABILITY_DESCRIPTION_MAX_BYTES,
200            )?;
201        }
202        Ok(())
203    }
204
205    pub fn has_capability(&self, capability_id: &ToolCapabilityId) -> bool {
206        self.capabilities
207            .iter()
208            .any(|capability| &capability.id == capability_id)
209    }
210}
211
212#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
213pub struct PolicyKey {
214    pub consumer_id: ConsumerId,
215    pub actor_id: ToolActorId,
216    pub instance_id: AgentInstanceId,
217    pub generation: SessionGeneration,
218    pub provider_id: ToolProviderId,
219    pub capability_id: ToolCapabilityId,
220    pub resource_scope_id: ResourceScopeId,
221}
222
223#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
224#[serde(rename_all = "kebab-case")]
225pub enum GrantMode {
226    Allow,
227    RequireApproval,
228}
229
230#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
231pub struct PolicyGrant {
232    pub key: PolicyKey,
233    pub mode: GrantMode,
234}
235
236#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
237pub struct CapabilityRequestKey {
238    pub consumer_id: ConsumerId,
239    pub actor_id: ToolActorId,
240    pub local_id: CapabilityRequestId,
241}
242
243#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
244pub struct CapabilityRequestInput {
245    pub local_id: CapabilityRequestId,
246    pub instance_id: AgentInstanceId,
247    pub generation: SessionGeneration,
248    pub provider_id: ToolProviderId,
249    pub capability_id: ToolCapabilityId,
250    pub resource_scope_id: ResourceScopeId,
251    pub approval_summary: String,
252    pub deadline_tick: u64,
253    pub payload: Vec<u8>,
254}
255
256impl fmt::Debug for CapabilityRequestInput {
257    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
258        formatter
259            .debug_struct("CapabilityRequestInput")
260            .field("local_id", &self.local_id)
261            .field("instance_id", &self.instance_id)
262            .field("generation", &self.generation)
263            .field("provider_id", &self.provider_id)
264            .field("capability_id", &self.capability_id)
265            .field("resource_scope_id", &self.resource_scope_id)
266            .field("approval_summary", &self.approval_summary)
267            .field("deadline_tick", &self.deadline_tick)
268            .field("payload_bytes", &self.payload.len())
269            .finish()
270    }
271}
272
273impl CapabilityRequestInput {
274    pub fn validate(&self, current_tick: u64) -> Result<(), ToolValidationError> {
275        if self.local_id.0 == 0 {
276            return Err(ToolValidationError::ZeroIdentifier {
277                field: "tool request id",
278            });
279        }
280        if self.deadline_tick <= current_tick {
281            return Err(ToolValidationError::DeadlineElapsed {
282                current_tick,
283                deadline_tick: self.deadline_tick,
284            });
285        }
286        if self.payload.len() > TOOL_PAYLOAD_MAX_BYTES {
287            return Err(ToolValidationError::TooLarge {
288                field: "tool request payload",
289                max: TOOL_PAYLOAD_MAX_BYTES,
290                actual: self.payload.len(),
291            });
292        }
293        validate_required_text(
294            "tool approval summary",
295            &self.approval_summary,
296            TOOL_APPROVAL_SUMMARY_MAX_BYTES,
297        )?;
298        Ok(())
299    }
300}
301
302#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
303pub struct ConsumerBoundCapabilityRequest {
304    pub consumer_id: ConsumerId,
305    pub actor_id: ToolActorId,
306    pub request: CapabilityRequestInput,
307}
308
309impl ConsumerBoundCapabilityRequest {
310    pub fn new(
311        consumer_id: ConsumerId,
312        actor_id: ToolActorId,
313        request: CapabilityRequestInput,
314    ) -> Self {
315        Self {
316            consumer_id,
317            actor_id,
318            request,
319        }
320    }
321
322    pub fn key(&self) -> CapabilityRequestKey {
323        CapabilityRequestKey {
324            consumer_id: self.consumer_id.clone(),
325            actor_id: self.actor_id.clone(),
326            local_id: self.request.local_id,
327        }
328    }
329
330    pub fn validate(&self, current_tick: u64) -> Result<(), ToolValidationError> {
331        self.request.validate(current_tick)
332    }
333}
334
335impl fmt::Debug for ConsumerBoundCapabilityRequest {
336    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
337        formatter
338            .debug_struct("ConsumerBoundCapabilityRequest")
339            .field("key", &self.key())
340            .field("request", &self.request)
341            .finish()
342    }
343}
344
345#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
346pub struct ProviderBoundCapabilityRequest {
347    pub provider_binding_id: Option<ProviderBindingId>,
348    pub request: ConsumerBoundCapabilityRequest,
349}
350
351impl ProviderBoundCapabilityRequest {
352    pub fn new(
353        provider_binding_id: Option<ProviderBindingId>,
354        request: ConsumerBoundCapabilityRequest,
355    ) -> Self {
356        Self {
357            provider_binding_id,
358            request,
359        }
360    }
361
362    pub fn key(&self) -> CapabilityRequestKey {
363        self.request.key()
364    }
365
366    pub fn validate(&self, current_tick: u64) -> Result<(), ToolValidationError> {
367        self.validate_provider_binding()?;
368        self.request.validate(current_tick)
369    }
370
371    pub fn validate_provider_binding(&self) -> Result<(), ToolValidationError> {
372        if self
373            .provider_binding_id
374            .is_some_and(|binding_id| binding_id.0 == 0)
375        {
376            return Err(ToolValidationError::ZeroIdentifier {
377                field: "provider binding id",
378            });
379        }
380        Ok(())
381    }
382}
383
384impl fmt::Debug for ProviderBoundCapabilityRequest {
385    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
386        formatter
387            .debug_struct("ProviderBoundCapabilityRequest")
388            .field("provider_binding_id", &self.provider_binding_id)
389            .field("request", &self.request)
390            .finish()
391    }
392}
393
394pub type CapabilityRequest = ConsumerBoundCapabilityRequest;
395
396#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
397#[serde(rename_all = "kebab-case")]
398pub enum PolicyDenial {
399    UnknownInstance,
400    InactiveInstance,
401    StaleGeneration { current: SessionGeneration },
402    UnknownProvider,
403    UnknownCapability,
404    ProviderOwnerMismatch,
405    MissingGrant,
406}
407
408#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
409#[serde(rename_all = "kebab-case")]
410pub enum PolicyDecision {
411    Deny(PolicyDenial),
412    RequireApproval,
413    Allow,
414}
415
416#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
417#[serde(rename_all = "kebab-case")]
418pub enum ApprovalDecision {
419    ApproveOnce,
420    Deny,
421}
422
423#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
424pub struct ApprovalResolution {
425    pub request_key: CapabilityRequestKey,
426    pub accepted_sequence: u64,
427    pub instance_id: AgentInstanceId,
428    pub generation: SessionGeneration,
429    pub decision: ApprovalDecision,
430}
431
432#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
433pub struct ToolAuthorityEnvelope {
434    pub sequence: u64,
435    pub command: ToolAuthorityCommand,
436}
437
438impl ToolAuthorityEnvelope {
439    pub fn validate(&self) -> Result<(), ToolValidationError> {
440        if self.sequence == 0 {
441            return Err(ToolValidationError::ZeroIdentifier {
442                field: "tool authority sequence",
443            });
444        }
445        Ok(())
446    }
447}
448
449#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
450#[serde(rename_all = "kebab-case")]
451pub enum ToolAuthorityCommand {
452    SetGrant {
453        grant: PolicyGrant,
454    },
455    RevokeGrant {
456        key: PolicyKey,
457    },
458    ResolveApproval {
459        resolution: ApprovalResolution,
460    },
461    CloseClient {
462        consumer_id: ConsumerId,
463        actor_id: ToolActorId,
464    },
465}
466
467#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
468#[serde(rename_all = "kebab-case")]
469pub enum ToolAuthorityOutcome {
470    GrantSet,
471    GrantRevoked {
472        existed: bool,
473    },
474    ApprovalResolved,
475    ApprovalExpired {
476        request_key: CapabilityRequestKey,
477        accepted_sequence: u64,
478    },
479    ClientClosed {
480        purged_grant_count: usize,
481        closed_request_count: usize,
482    },
483}
484
485#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
486#[serde(rename_all = "kebab-case")]
487pub enum ToolInstanceState {
488    Active,
489    Inactive,
490}
491
492#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
493#[serde(rename_all = "kebab-case")]
494pub enum InvocationCancelReason {
495    DeadlineElapsed,
496    GenerationSuperseded,
497    GrantRevoked,
498    InstanceClosed,
499    ClientClosed,
500}
501
502#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
503#[serde(rename_all = "kebab-case")]
504pub enum CapabilityEffect {
505    Invoke {
506        consumer_id: ConsumerId,
507        actor_id: ToolActorId,
508        capability_id: ToolCapabilityId,
509        resource_scope_id: ResourceScopeId,
510        payload: Vec<u8>,
511    },
512    Cancel {
513        reason: InvocationCancelReason,
514    },
515}
516
517impl fmt::Debug for CapabilityEffect {
518    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
519        match self {
520            Self::Invoke {
521                consumer_id,
522                actor_id,
523                capability_id,
524                resource_scope_id,
525                payload,
526            } => formatter
527                .debug_struct("Invoke")
528                .field("consumer_id", consumer_id)
529                .field("actor_id", actor_id)
530                .field("capability_id", capability_id)
531                .field("resource_scope_id", resource_scope_id)
532                .field("payload_bytes", &payload.len())
533                .finish(),
534            Self::Cancel { reason } => formatter
535                .debug_struct("Cancel")
536                .field("reason", reason)
537                .finish(),
538        }
539    }
540}
541
542#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
543pub struct CapabilityEffectEnvelope {
544    pub sequence: u64,
545    pub operation_id: ToolOperationId,
546    pub request_key: CapabilityRequestKey,
547    pub instance_id: AgentInstanceId,
548    pub generation: SessionGeneration,
549    pub provider_id: ToolProviderId,
550    pub deadline_tick: u64,
551    pub effect: CapabilityEffect,
552}
553
554impl fmt::Debug for CapabilityEffectEnvelope {
555    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
556        formatter
557            .debug_struct("CapabilityEffectEnvelope")
558            .field("sequence", &self.sequence)
559            .field("operation_id", &self.operation_id)
560            .field("request_key", &self.request_key)
561            .field("instance_id", &self.instance_id)
562            .field("generation", &self.generation)
563            .field("provider_id", &self.provider_id)
564            .field("deadline_tick", &self.deadline_tick)
565            .field("effect", &self.effect)
566            .finish()
567    }
568}
569
570#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
571pub struct CapabilityResultMetadata {
572    pub byte_len: u64,
573    pub media_type: Option<String>,
574    pub truncated: bool,
575    pub redacted_summary: Option<String>,
576}
577
578impl CapabilityResultMetadata {
579    pub fn validate(&self) -> Result<(), ToolValidationError> {
580        if self.byte_len > TOOL_RESULT_MAX_BYTES {
581            return Err(ToolValidationError::ResultTooLarge {
582                max: TOOL_RESULT_MAX_BYTES,
583                actual: self.byte_len,
584            });
585        }
586        validate_optional_text(
587            "tool result media type",
588            self.media_type.as_deref(),
589            TOOL_MEDIA_TYPE_MAX_BYTES,
590        )?;
591        validate_optional_text(
592            "tool result redacted summary",
593            self.redacted_summary.as_deref(),
594            TOOL_RESULT_SUMMARY_MAX_BYTES,
595        )
596    }
597}
598
599/// Bounded result delivery released to the caller through
600/// a [`CapabilityCompletionBatch`]. Neither variant is retained in
601/// snapshots or audit events. An opaque reference is meaningful only to the
602/// provider identified by its completion envelope; the core never resolves or
603/// interprets it.
604#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
605#[serde(rename_all = "kebab-case")]
606pub enum CapabilityResultDelivery {
607    Inline { bytes: Vec<u8> },
608    OpaqueReference { reference: String },
609}
610
611impl fmt::Debug for CapabilityResultDelivery {
612    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
613        match self {
614            Self::Inline { bytes } => formatter
615                .debug_struct("Inline")
616                .field("byte_len", &bytes.len())
617                .finish(),
618            Self::OpaqueReference { reference } => formatter
619                .debug_struct("OpaqueReference")
620                .field("reference_bytes", &reference.len())
621                .finish(),
622        }
623    }
624}
625
626#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
627pub struct CapabilityResult {
628    pub metadata: CapabilityResultMetadata,
629    pub delivery: CapabilityResultDelivery,
630}
631
632impl fmt::Debug for CapabilityResult {
633    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
634        formatter
635            .debug_struct("CapabilityResult")
636            .field("metadata", &self.metadata)
637            .field("delivery", &self.delivery)
638            .finish()
639    }
640}
641
642impl CapabilityResult {
643    pub fn validate(&self) -> Result<(), ToolValidationError> {
644        self.metadata.validate()?;
645        match &self.delivery {
646            CapabilityResultDelivery::Inline { bytes } => {
647                if bytes.len() > TOOL_INLINE_RESULT_MAX_BYTES {
648                    return Err(ToolValidationError::TooLarge {
649                        field: "inline tool result",
650                        max: TOOL_INLINE_RESULT_MAX_BYTES,
651                        actual: bytes.len(),
652                    });
653                }
654                if self.metadata.byte_len != bytes.len() as u64 {
655                    return Err(ToolValidationError::ResultLengthMismatch {
656                        declared: self.metadata.byte_len,
657                        actual: bytes.len(),
658                    });
659                }
660                Ok(())
661            }
662            CapabilityResultDelivery::OpaqueReference { reference } => validate_required_text(
663                "tool result opaque reference",
664                reference,
665                TOOL_RESULT_REFERENCE_MAX_BYTES,
666            ),
667        }
668    }
669}
670
671#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
672#[serde(rename_all = "kebab-case")]
673pub enum CapabilityTerminalOutcome {
674    Succeeded {
675        result: CapabilityResult,
676    },
677    Failed {
678        failure: ToolFailure,
679    },
680    PolicyDenied {
681        reason: PolicyDenial,
682    },
683    ApprovalDenied,
684    GrantRevoked {
685        cancellation: CancellationDisposition,
686    },
687    TimedOut {
688        cancellation: CancellationDisposition,
689    },
690    Superseded {
691        current_generation: SessionGeneration,
692        cancellation: CancellationDisposition,
693    },
694    InstanceClosed {
695        cancellation: CancellationDisposition,
696    },
697    ClientClosed {
698        cancellation: CancellationDisposition,
699    },
700    ProviderDetached {
701        cancellation: CancellationDisposition,
702    },
703}
704
705impl fmt::Debug for CapabilityTerminalOutcome {
706    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
707        match self {
708            Self::Succeeded { result } => formatter
709                .debug_struct("Succeeded")
710                .field("result", result)
711                .finish(),
712            Self::Failed { failure } => formatter
713                .debug_struct("Failed")
714                .field("failure", failure)
715                .finish(),
716            Self::PolicyDenied { reason } => formatter
717                .debug_struct("PolicyDenied")
718                .field("reason", reason)
719                .finish(),
720            Self::ApprovalDenied => formatter.write_str("ApprovalDenied"),
721            Self::GrantRevoked { cancellation } => formatter
722                .debug_struct("GrantRevoked")
723                .field("cancellation", cancellation)
724                .finish(),
725            Self::TimedOut { cancellation } => formatter
726                .debug_struct("TimedOut")
727                .field("cancellation", cancellation)
728                .finish(),
729            Self::Superseded {
730                current_generation,
731                cancellation,
732            } => formatter
733                .debug_struct("Superseded")
734                .field("current_generation", current_generation)
735                .field("cancellation", cancellation)
736                .finish(),
737            Self::InstanceClosed { cancellation } => formatter
738                .debug_struct("InstanceClosed")
739                .field("cancellation", cancellation)
740                .finish(),
741            Self::ClientClosed { cancellation } => formatter
742                .debug_struct("ClientClosed")
743                .field("cancellation", cancellation)
744                .finish(),
745            Self::ProviderDetached { cancellation } => formatter
746                .debug_struct("ProviderDetached")
747                .field("cancellation", cancellation)
748                .finish(),
749        }
750    }
751}
752
753#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
754pub struct CapabilityCompletionEnvelope {
755    pub sequence: u64,
756    pub accepted_sequence: u64,
757    pub operation_id: Option<ToolOperationId>,
758    pub request_key: CapabilityRequestKey,
759    pub instance_id: AgentInstanceId,
760    pub generation: SessionGeneration,
761    pub provider_id: ToolProviderId,
762    pub outcome: CapabilityTerminalOutcome,
763}
764
765impl fmt::Debug for CapabilityCompletionEnvelope {
766    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
767        formatter
768            .debug_struct("CapabilityCompletionEnvelope")
769            .field("sequence", &self.sequence)
770            .field("accepted_sequence", &self.accepted_sequence)
771            .field("operation_id", &self.operation_id)
772            .field("request_key", &self.request_key)
773            .field("instance_id", &self.instance_id)
774            .field("generation", &self.generation)
775            .field("provider_id", &self.provider_id)
776            .field("outcome", &self.outcome)
777            .finish()
778    }
779}
780
781#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
782pub struct CapabilityCompletionBatch {
783    pub completions: Vec<CapabilityCompletionEnvelope>,
784    pub dropped_since_last_drain: u64,
785    pub total_dropped: u64,
786    pub next_sequence: u64,
787    pub sequence_exhausted: bool,
788}
789
790#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
791#[serde(rename_all = "kebab-case")]
792pub enum ToolFailureKind {
793    Rejected,
794    Unavailable,
795    InvalidInput,
796    Execution,
797    Cancelled,
798    ProviderContractViolation,
799}
800
801#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
802pub struct ToolFailure {
803    pub kind: ToolFailureKind,
804    pub redacted_message: Option<String>,
805}
806
807impl ToolFailure {
808    pub fn validate(&self) -> Result<(), ToolValidationError> {
809        validate_optional_text(
810            "tool failure redacted message",
811            self.redacted_message.as_deref(),
812            TOOL_FAILURE_MESSAGE_MAX_BYTES,
813        )
814    }
815
816    pub fn provider_contract_violation() -> Self {
817        Self {
818            kind: ToolFailureKind::ProviderContractViolation,
819            redacted_message: None,
820        }
821    }
822}
823
824#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
825#[serde(rename_all = "kebab-case")]
826pub enum CapabilityObservation {
827    Succeeded { result: CapabilityResult },
828    Failed { failure: ToolFailure },
829}
830
831impl fmt::Debug for CapabilityObservation {
832    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
833        match self {
834            Self::Succeeded { result } => formatter
835                .debug_struct("Succeeded")
836                .field("result", result)
837                .finish(),
838            Self::Failed { failure } => formatter
839                .debug_struct("Failed")
840                .field("failure", failure)
841                .finish(),
842        }
843    }
844}
845
846impl CapabilityObservation {
847    pub fn validate(&self) -> Result<(), ToolValidationError> {
848        match self {
849            Self::Succeeded { result } => result.validate(),
850            Self::Failed { failure } => failure.validate(),
851        }
852    }
853}
854
855#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
856pub struct CapabilityObservationEnvelope {
857    pub operation_id: ToolOperationId,
858    pub request_key: CapabilityRequestKey,
859    pub instance_id: AgentInstanceId,
860    pub generation: SessionGeneration,
861    pub provider_id: ToolProviderId,
862    pub observation: CapabilityObservation,
863}
864
865impl fmt::Debug for CapabilityObservationEnvelope {
866    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
867        formatter
868            .debug_struct("CapabilityObservationEnvelope")
869            .field("operation_id", &self.operation_id)
870            .field("request_key", &self.request_key)
871            .field("instance_id", &self.instance_id)
872            .field("generation", &self.generation)
873            .field("provider_id", &self.provider_id)
874            .field("observation", &self.observation)
875            .finish()
876    }
877}
878
879impl CapabilityObservationEnvelope {
880    pub fn validate(&self) -> Result<(), ToolValidationError> {
881        if self.operation_id.0 == 0 {
882            return Err(ToolValidationError::ZeroIdentifier {
883                field: "tool operation id",
884            });
885        }
886        self.observation.validate()
887    }
888}
889
890#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
891pub struct ProviderRuntimeEnvelope {
892    pub sequence: u64,
893    pub command: ProviderRuntimeCommand,
894}
895
896impl ProviderRuntimeEnvelope {
897    pub fn validate(&self) -> Result<(), ToolValidationError> {
898        if self.sequence == 0 {
899            return Err(ToolValidationError::ZeroIdentifier {
900                field: "provider runtime sequence",
901            });
902        }
903        if self.command.binding_id().0 == 0 {
904            return Err(ToolValidationError::ZeroIdentifier {
905                field: "provider binding id",
906            });
907        }
908        if let ProviderRuntimeCommand::Observe { observation, .. } = &self.command {
909            observation.validate()?;
910        }
911        Ok(())
912    }
913
914    pub fn validate_observation_provider(
915        &self,
916        expected_provider_id: &ToolProviderId,
917    ) -> Result<(), ToolValidationError> {
918        self.validate()?;
919        if let ProviderRuntimeCommand::Observe { observation, .. } = &self.command {
920            if &observation.provider_id != expected_provider_id {
921                return Err(ToolValidationError::ProviderMismatch {
922                    field: "provider runtime observation provider id",
923                    expected: expected_provider_id.clone(),
924                    actual: observation.provider_id.clone(),
925                });
926            }
927        }
928        Ok(())
929    }
930}
931
932#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
933#[serde(rename_all = "kebab-case")]
934pub enum ProviderRuntimeCommand {
935    Attach {
936        binding_id: ProviderBindingId,
937        provider_id: ToolProviderId,
938    },
939    Detach {
940        binding_id: ProviderBindingId,
941        provider_id: ToolProviderId,
942    },
943    Observe {
944        binding_id: ProviderBindingId,
945        observation: CapabilityObservationEnvelope,
946    },
947}
948
949impl ProviderRuntimeCommand {
950    pub fn binding_id(&self) -> ProviderBindingId {
951        match self {
952            Self::Attach { binding_id, .. }
953            | Self::Detach { binding_id, .. }
954            | Self::Observe { binding_id, .. } => *binding_id,
955        }
956    }
957}
958
959#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
960pub struct ProviderRuntimeBindingSnapshot {
961    pub binding_id: ProviderBindingId,
962    pub provider_id: ToolProviderId,
963}
964
965#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
966pub struct ProviderRuntimeSnapshot {
967    pub last_sequence: u64,
968    pub sequence_exhausted: bool,
969    pub bindings: Vec<ProviderRuntimeBindingSnapshot>,
970}
971
972impl ProviderRuntimeSnapshot {
973    pub fn validate(&self) -> Result<(), ToolValidationError> {
974        if self.bindings.len() > TOOL_PROVIDERS_MAX {
975            return Err(ToolValidationError::TooMany {
976                field: "provider runtime bindings",
977                max: TOOL_PROVIDERS_MAX,
978                actual: self.bindings.len(),
979            });
980        }
981        for binding in &self.bindings {
982            if binding.binding_id.0 == 0 {
983                return Err(ToolValidationError::ZeroIdentifier {
984                    field: "provider binding id",
985                });
986            }
987        }
988        Ok(())
989    }
990}
991
992#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
993pub struct ProviderBoundCapabilityEffectEnvelope {
994    pub binding_id: ProviderBindingId,
995    pub effect: CapabilityEffectEnvelope,
996}
997
998impl ProviderBoundCapabilityEffectEnvelope {
999    pub fn validate(&self) -> Result<(), ToolValidationError> {
1000        if self.binding_id.0 == 0 {
1001            return Err(ToolValidationError::ZeroIdentifier {
1002                field: "provider binding id",
1003            });
1004        }
1005        if self.effect.sequence == 0 {
1006            return Err(ToolValidationError::ZeroIdentifier {
1007                field: "tool effect sequence",
1008            });
1009        }
1010        if self.effect.operation_id.0 == 0 {
1011            return Err(ToolValidationError::ZeroIdentifier {
1012                field: "tool operation id",
1013            });
1014        }
1015        Ok(())
1016    }
1017}
1018
1019/// Cancellation remains unconfirmed after a cancel effect is queued. Only
1020/// `QueuedInvokeRemoved` proves the invocation never left the engine.
1021#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1022#[serde(rename_all = "kebab-case")]
1023pub enum CancellationDisposition {
1024    NotRequired,
1025    QueuedInvokeRemoved,
1026    CancelQueuedUnconfirmed,
1027    ProviderDetachedUnconfirmed,
1028    DroppedQueueFull,
1029    DroppedSequenceExhausted,
1030}
1031
1032#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1033#[serde(rename_all = "kebab-case")]
1034pub enum CapabilityRequestStatus {
1035    Denied {
1036        reason: PolicyDenial,
1037    },
1038    AwaitingApproval,
1039    Dispatched {
1040        operation_id: ToolOperationId,
1041    },
1042    Succeeded {
1043        operation_id: ToolOperationId,
1044        result: CapabilityResultMetadata,
1045    },
1046    Failed {
1047        operation_id: ToolOperationId,
1048        failure: ToolFailure,
1049    },
1050    ApprovalDenied,
1051    GrantRevoked {
1052        operation_id: Option<ToolOperationId>,
1053        cancellation: CancellationDisposition,
1054    },
1055    TimedOut {
1056        operation_id: Option<ToolOperationId>,
1057        cancellation: CancellationDisposition,
1058    },
1059    Superseded {
1060        operation_id: Option<ToolOperationId>,
1061        cancellation: CancellationDisposition,
1062        current_generation: SessionGeneration,
1063    },
1064    InstanceClosed {
1065        operation_id: Option<ToolOperationId>,
1066        cancellation: CancellationDisposition,
1067    },
1068    ClientClosed {
1069        operation_id: Option<ToolOperationId>,
1070        cancellation: CancellationDisposition,
1071    },
1072    ProviderDetached {
1073        operation_id: Option<ToolOperationId>,
1074        cancellation: CancellationDisposition,
1075    },
1076}
1077
1078impl CapabilityRequestStatus {
1079    pub fn is_terminal(&self) -> bool {
1080        matches!(
1081            self,
1082            Self::Denied { .. }
1083                | Self::Succeeded { .. }
1084                | Self::Failed { .. }
1085                | Self::ApprovalDenied
1086                | Self::GrantRevoked { .. }
1087                | Self::TimedOut { .. }
1088                | Self::Superseded { .. }
1089                | Self::InstanceClosed { .. }
1090                | Self::ClientClosed { .. }
1091                | Self::ProviderDetached { .. }
1092        )
1093    }
1094}
1095
1096#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1097pub struct CapabilityRequestSnapshot {
1098    pub key: CapabilityRequestKey,
1099    pub accepted_sequence: u64,
1100    pub accepted_at_tick: u64,
1101    pub instance_id: AgentInstanceId,
1102    pub generation: SessionGeneration,
1103    pub provider_id: ToolProviderId,
1104    pub capability_id: ToolCapabilityId,
1105    pub resource_scope_id: ResourceScopeId,
1106    /// Bounded, intentionally displayable text for a trusted approval surface.
1107    pub approval_summary: String,
1108    pub approval_summary_bytes: usize,
1109    pub deadline_tick: u64,
1110    pub payload_bytes: usize,
1111    pub policy_decision: PolicyDecision,
1112    pub status: CapabilityRequestStatus,
1113}
1114
1115#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1116pub struct ToolAuditSubject {
1117    pub request_key: CapabilityRequestKey,
1118    pub accepted_sequence: u64,
1119    pub instance_id: AgentInstanceId,
1120    pub generation: SessionGeneration,
1121    pub provider_id: ToolProviderId,
1122    pub capability_id: ToolCapabilityId,
1123    pub resource_scope_id: ResourceScopeId,
1124}
1125
1126#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1127#[serde(rename_all = "kebab-case")]
1128pub enum ObservationIgnoredReason {
1129    UnknownRequest,
1130    StaleGeneration,
1131    InstanceMismatch,
1132    ProviderMismatch,
1133    RequestNotDispatched,
1134    OperationMismatch,
1135    DeadlineElapsed,
1136}
1137
1138#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1139#[serde(rename_all = "kebab-case")]
1140pub enum CapabilityObservationDisposition {
1141    Applied,
1142    Ignored { reason: ObservationIgnoredReason },
1143}
1144
1145#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1146#[serde(rename_all = "kebab-case")]
1147pub enum ToolAuditEventKind {
1148    ProviderRegistered {
1149        provider_id: ToolProviderId,
1150        owner: CapabilityOwner,
1151        capability_count: usize,
1152    },
1153    GrantSet {
1154        grant: PolicyGrant,
1155    },
1156    GrantRevoked {
1157        key: PolicyKey,
1158    },
1159    GenerationAdvanced {
1160        instance_id: AgentInstanceId,
1161        previous: Option<SessionGeneration>,
1162        current: SessionGeneration,
1163        purged_grant_count: usize,
1164    },
1165    InstanceStateChanged {
1166        instance_id: AgentInstanceId,
1167        generation: SessionGeneration,
1168        state: ToolInstanceState,
1169        purged_grant_count: usize,
1170    },
1171    InstanceRemoved {
1172        instance_id: AgentInstanceId,
1173        previous_generation: SessionGeneration,
1174        purged_grant_count: usize,
1175    },
1176    ClientClosed {
1177        consumer_id: ConsumerId,
1178        actor_id: ToolActorId,
1179        purged_grant_count: usize,
1180        closed_request_count: usize,
1181    },
1182    RequestEvaluated {
1183        decision: PolicyDecision,
1184        payload_bytes: usize,
1185    },
1186    ApprovalResolved {
1187        decision: ApprovalDecision,
1188    },
1189    RequestGrantRevoked {
1190        operation_id: Option<ToolOperationId>,
1191        cancellation: CancellationDisposition,
1192    },
1193    InvocationDispatched {
1194        operation_id: ToolOperationId,
1195    },
1196    InvocationSucceeded {
1197        operation_id: ToolOperationId,
1198        result_bytes: u64,
1199        truncated: bool,
1200    },
1201    InvocationFailed {
1202        operation_id: ToolOperationId,
1203        failure_kind: ToolFailureKind,
1204    },
1205    RequestTimedOut {
1206        operation_id: Option<ToolOperationId>,
1207        cancellation: CancellationDisposition,
1208    },
1209    RequestSuperseded {
1210        operation_id: Option<ToolOperationId>,
1211        cancellation: CancellationDisposition,
1212        current_generation: SessionGeneration,
1213    },
1214    RequestInstanceClosed {
1215        operation_id: Option<ToolOperationId>,
1216        cancellation: CancellationDisposition,
1217    },
1218    RequestClientClosed {
1219        operation_id: Option<ToolOperationId>,
1220        cancellation: CancellationDisposition,
1221    },
1222    RequestProviderDetached {
1223        operation_id: Option<ToolOperationId>,
1224        cancellation: CancellationDisposition,
1225    },
1226    CompletionDropped {
1227        completion_sequence: Option<u64>,
1228        reason: CompletionDropReason,
1229    },
1230    ObservationIgnored {
1231        operation_id: ToolOperationId,
1232        reason: ObservationIgnoredReason,
1233    },
1234}
1235
1236#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1237pub struct ToolAuditEvent {
1238    pub sequence: u64,
1239    pub tick: u64,
1240    pub subject: Option<ToolAuditSubject>,
1241    pub event: ToolAuditEventKind,
1242}
1243
1244#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1245pub struct ToolEngineSnapshot {
1246    pub revision: u64,
1247    pub current_tick: u64,
1248    pub generations: Vec<(AgentInstanceId, SessionGeneration)>,
1249    pub instance_states: Vec<(AgentInstanceId, ToolInstanceState)>,
1250    pub providers: Vec<CapabilityProviderDescriptor>,
1251    pub grants: Vec<PolicyGrant>,
1252    pub requests: Vec<CapabilityRequestSnapshot>,
1253    pub audit_events: Vec<ToolAuditEvent>,
1254    pub dropped_audit_events: u64,
1255    pub revision_overflow_count: u64,
1256    pub next_completion_sequence: u64,
1257    pub dropped_completions: u64,
1258    pub effect_sequence_exhausted: bool,
1259    pub completion_sequence_exhausted: bool,
1260    pub audit_sequence_exhausted: bool,
1261}
1262
1263#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1264#[serde(rename_all = "kebab-case")]
1265pub enum CompletionDropReason {
1266    QueueFull,
1267    SequenceExhausted,
1268}
1269
1270#[derive(Clone, Debug, Eq, PartialEq)]
1271pub enum ToolValidationError {
1272    Required {
1273        field: &'static str,
1274    },
1275    InvalidIdentifier {
1276        field: &'static str,
1277    },
1278    ControlCharacter {
1279        field: &'static str,
1280    },
1281    CapabilityOutsideAdmission {
1282        capability_id: ToolCapabilityId,
1283    },
1284    DuplicateIdentifier {
1285        field: &'static str,
1286    },
1287    ZeroIdentifier {
1288        field: &'static str,
1289    },
1290    TooLarge {
1291        field: &'static str,
1292        max: usize,
1293        actual: usize,
1294    },
1295    TooMany {
1296        field: &'static str,
1297        max: usize,
1298        actual: usize,
1299    },
1300    ResultTooLarge {
1301        max: u64,
1302        actual: u64,
1303    },
1304    ResultLengthMismatch {
1305        declared: u64,
1306        actual: usize,
1307    },
1308    ProviderMismatch {
1309        field: &'static str,
1310        expected: ToolProviderId,
1311        actual: ToolProviderId,
1312    },
1313    DeadlineElapsed {
1314        current_tick: u64,
1315        deadline_tick: u64,
1316    },
1317}
1318
1319impl fmt::Display for ToolValidationError {
1320    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1321        write!(formatter, "invalid tool contract: {self:?}")
1322    }
1323}
1324
1325impl std::error::Error for ToolValidationError {}
1326
1327fn validate_identifier(
1328    field: &'static str,
1329    value: &str,
1330    max: usize,
1331) -> Result<(), ToolValidationError> {
1332    if value.trim().is_empty() {
1333        return Err(ToolValidationError::Required { field });
1334    }
1335    if value.len() > max {
1336        return Err(ToolValidationError::TooLarge {
1337            field,
1338            max,
1339            actual: value.len(),
1340        });
1341    }
1342    if !value.bytes().all(|byte| {
1343        byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':' | b'/')
1344    }) {
1345        return Err(ToolValidationError::InvalidIdentifier { field });
1346    }
1347    Ok(())
1348}
1349
1350fn has_forbidden_capability_namespace(value: &str) -> bool {
1351    const SEPARATORS: [char; 5] = ['.', ':', '/', '-', '_'];
1352
1353    let compact = value
1354        .chars()
1355        .filter(|character| !SEPARATORS.contains(character))
1356        .collect::<String>();
1357    if ["shell", "filesystem", "mcp"]
1358        .iter()
1359        .any(|forbidden| compact.contains(forbidden))
1360    {
1361        return true;
1362    }
1363
1364    let segments = value
1365        .split(SEPARATORS)
1366        .filter(|segment| !segment.is_empty())
1367        .collect::<Vec<_>>();
1368    segments.iter().enumerate().any(|(start, _)| {
1369        let mut candidate = String::new();
1370        segments.iter().skip(start).any(|segment| {
1371            if candidate.len() >= 2 {
1372                return false;
1373            }
1374            candidate.push_str(segment);
1375            candidate == "fs"
1376        })
1377    })
1378}
1379
1380fn validate_required_text(
1381    field: &'static str,
1382    value: &str,
1383    max: usize,
1384) -> Result<(), ToolValidationError> {
1385    if value.trim().is_empty() {
1386        return Err(ToolValidationError::Required { field });
1387    }
1388    validate_text(field, value, max)
1389}
1390
1391fn validate_optional_text(
1392    field: &'static str,
1393    value: Option<&str>,
1394    max: usize,
1395) -> Result<(), ToolValidationError> {
1396    match value {
1397        Some(value) => validate_text(field, value, max),
1398        None => Ok(()),
1399    }
1400}
1401
1402fn validate_text(field: &'static str, value: &str, max: usize) -> Result<(), ToolValidationError> {
1403    if value.len() > max {
1404        return Err(ToolValidationError::TooLarge {
1405            field,
1406            max,
1407            actual: value.len(),
1408        });
1409    }
1410    if value.chars().any(char::is_control) {
1411        return Err(ToolValidationError::ControlCharacter { field });
1412    }
1413    Ok(())
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418    use super::*;
1419
1420    fn request() -> ConsumerBoundCapabilityRequest {
1421        ConsumerBoundCapabilityRequest::new(
1422            ConsumerId::new("station.test").unwrap(),
1423            ToolActorId::new("agent.primary").unwrap(),
1424            CapabilityRequestInput {
1425                local_id: CapabilityRequestId(7),
1426                instance_id: AgentInstanceId(11),
1427                generation: SessionGeneration(3),
1428                provider_id: ToolProviderId::new("gate.browser").unwrap(),
1429                capability_id: ToolCapabilityId::new("browser.page.snapshot").unwrap(),
1430                resource_scope_id: ResourceScopeId::new("workspace:test/page:active").unwrap(),
1431                approval_summary: "Read active page state".to_owned(),
1432                deadline_tick: 20,
1433                payload: b"secret payload".to_vec(),
1434            },
1435        )
1436    }
1437
1438    #[test]
1439    fn request_wire_round_trip_preserves_scoped_key_and_validates() {
1440        let request = request();
1441        let encoded = serde_json::to_string(&request).unwrap();
1442        let decoded: ConsumerBoundCapabilityRequest = serde_json::from_str(&encoded).unwrap();
1443        assert_eq!(decoded, request);
1444        assert_eq!(
1445            decoded.key(),
1446            CapabilityRequestKey {
1447                consumer_id: ConsumerId::new("station.test").unwrap(),
1448                actor_id: ToolActorId::new("agent.primary").unwrap(),
1449                local_id: CapabilityRequestId(7),
1450            }
1451        );
1452        decoded.validate(1).unwrap();
1453    }
1454
1455    #[test]
1456    fn unbounded_id_fails_validation() {
1457        let invalid = serde_json::to_string(
1458            &String::from_utf8(vec![b'x'; TOOL_ACTOR_ID_MAX_BYTES + 1]).unwrap(),
1459        )
1460        .unwrap();
1461        assert!(serde_json::from_str::<ToolActorId>(&invalid).is_err());
1462    }
1463
1464    #[test]
1465    fn raw_request_debug_is_redacted() {
1466        let request = request();
1467        let rendered = format!("{request:?}");
1468        assert!(!rendered.contains("secret payload"));
1469        assert!(rendered.contains("payload_bytes"));
1470    }
1471
1472    #[test]
1473    fn provider_runtime_wire_contract_is_versioned_bounded_and_redacted() {
1474        let provider_id = ToolProviderId::new("gate.browser").unwrap();
1475        for command in [
1476            ProviderRuntimeCommand::Attach {
1477                binding_id: ProviderBindingId(4),
1478                provider_id: provider_id.clone(),
1479            },
1480            ProviderRuntimeCommand::Detach {
1481                binding_id: ProviderBindingId(4),
1482                provider_id: provider_id.clone(),
1483            },
1484        ] {
1485            let lifecycle = ProviderRuntimeEnvelope {
1486                sequence: 4,
1487                command,
1488            };
1489            lifecycle.validate().unwrap();
1490            let encoded = serde_json::to_string(&lifecycle).unwrap();
1491            assert_eq!(
1492                serde_json::from_str::<ProviderRuntimeEnvelope>(&encoded).unwrap(),
1493                lifecycle
1494            );
1495            assert!(!format!("{lifecycle:?}").is_empty());
1496        }
1497        let observation = CapabilityObservationEnvelope {
1498            operation_id: ToolOperationId(9),
1499            request_key: request().key(),
1500            instance_id: AgentInstanceId(11),
1501            generation: SessionGeneration(3),
1502            provider_id: provider_id.clone(),
1503            observation: CapabilityObservation::Succeeded {
1504                result: CapabilityResult {
1505                    metadata: CapabilityResultMetadata {
1506                        byte_len: 6,
1507                        media_type: Some("text/plain".to_owned()),
1508                        truncated: false,
1509                        redacted_summary: None,
1510                    },
1511                    delivery: CapabilityResultDelivery::Inline {
1512                        bytes: b"secret".to_vec(),
1513                    },
1514                },
1515            },
1516        };
1517        let envelope = ProviderRuntimeEnvelope {
1518            sequence: 4,
1519            command: ProviderRuntimeCommand::Observe {
1520                binding_id: ProviderBindingId(4),
1521                observation,
1522            },
1523        };
1524
1525        envelope.validate().unwrap();
1526        envelope
1527            .validate_observation_provider(&provider_id)
1528            .unwrap();
1529        let encoded = serde_json::to_string(&envelope).unwrap();
1530        let decoded: ProviderRuntimeEnvelope = serde_json::from_str(&encoded).unwrap();
1531        assert_eq!(decoded, envelope);
1532        let rendered = format!("{envelope:?}");
1533        assert!(!rendered.contains("secret"));
1534        assert!(rendered.contains("byte_len"));
1535
1536        let mut zero_sequence = envelope.clone();
1537        zero_sequence.sequence = 0;
1538        assert!(matches!(
1539            zero_sequence.validate(),
1540            Err(ToolValidationError::ZeroIdentifier {
1541                field: "provider runtime sequence"
1542            })
1543        ));
1544        let mut zero_binding = envelope.clone();
1545        if let ProviderRuntimeCommand::Observe { binding_id, .. } = &mut zero_binding.command {
1546            *binding_id = ProviderBindingId(0);
1547        }
1548        assert!(matches!(
1549            zero_binding.validate(),
1550            Err(ToolValidationError::ZeroIdentifier {
1551                field: "provider binding id"
1552            })
1553        ));
1554        assert!(matches!(
1555            envelope
1556                .validate_observation_provider(&ToolProviderId::new("gate.browser.other").unwrap()),
1557            Err(ToolValidationError::ProviderMismatch { .. })
1558        ));
1559    }
1560
1561    #[test]
1562    fn provider_runtime_snapshots_and_bound_effects_are_bounded_wire_contracts() {
1563        let provider_id = ToolProviderId::new("gate.browser").unwrap();
1564        let snapshot = ProviderRuntimeSnapshot {
1565            last_sequence: 8,
1566            sequence_exhausted: false,
1567            bindings: vec![ProviderRuntimeBindingSnapshot {
1568                binding_id: ProviderBindingId(4),
1569                provider_id: provider_id.clone(),
1570            }],
1571        };
1572        snapshot.validate().unwrap();
1573        let encoded = serde_json::to_string(&snapshot).unwrap();
1574        assert_eq!(
1575            serde_json::from_str::<ProviderRuntimeSnapshot>(&encoded).unwrap(),
1576            snapshot
1577        );
1578
1579        let bound = ProviderBoundCapabilityEffectEnvelope {
1580            binding_id: ProviderBindingId(4),
1581            effect: CapabilityEffectEnvelope {
1582                sequence: 1,
1583                operation_id: ToolOperationId(9),
1584                request_key: request().key(),
1585                instance_id: AgentInstanceId(11),
1586                generation: SessionGeneration(3),
1587                provider_id,
1588                deadline_tick: 20,
1589                effect: CapabilityEffect::Invoke {
1590                    consumer_id: ConsumerId::new("station.test").unwrap(),
1591                    actor_id: ToolActorId::new("agent.primary").unwrap(),
1592                    capability_id: ToolCapabilityId::new("browser.page.snapshot").unwrap(),
1593                    resource_scope_id: ResourceScopeId::new("workspace:test/page:active").unwrap(),
1594                    payload: b"secret payload".to_vec(),
1595                },
1596            },
1597        };
1598        bound.validate().unwrap();
1599        let encoded = serde_json::to_string(&bound).unwrap();
1600        assert_eq!(
1601            serde_json::from_str::<ProviderBoundCapabilityEffectEnvelope>(&encoded).unwrap(),
1602            bound
1603        );
1604        assert!(!format!("{bound:?}").contains("secret payload"));
1605
1606        let mut too_many = snapshot;
1607        too_many.bindings = vec![
1608            ProviderRuntimeBindingSnapshot {
1609                binding_id: ProviderBindingId(1),
1610                provider_id: ToolProviderId::new("gate.browser").unwrap(),
1611            };
1612            TOOL_PROVIDERS_MAX + 1
1613        ];
1614        assert!(matches!(
1615            too_many.validate(),
1616            Err(ToolValidationError::TooMany {
1617                field: "provider runtime bindings",
1618                ..
1619            })
1620        ));
1621    }
1622
1623    #[test]
1624    fn capability_admission_rejects_separator_obfuscation_without_rejecting_browser_terms() {
1625        for id in [
1626            "browser.file-system.read",
1627            "browser.f-i-l-e-s-y-s-t-e-m.read",
1628            "browser.s-h-e-l-l.exec",
1629            "browser.m-c-p.call",
1630            "browser.f-s.read",
1631        ] {
1632            assert!(matches!(
1633                CapabilityDescriptor::new(
1634                    ToolCapabilityId::new(id).unwrap(),
1635                    CapabilityClass::Browser,
1636                    "unsafe capability",
1637                ),
1638                Err(ToolValidationError::CapabilityOutsideAdmission { .. })
1639            ));
1640        }
1641
1642        for id in [
1643            "browser.page.snapshot",
1644            "browser.frame.offset.read",
1645            "browser.dom.forms.inspect",
1646        ] {
1647            assert!(CapabilityDescriptor::new(
1648                ToolCapabilityId::new(id).unwrap(),
1649                CapabilityClass::Browser,
1650                "bounded browser capability",
1651            )
1652            .is_ok());
1653        }
1654    }
1655
1656    #[test]
1657    fn output_contracts_round_trip_for_service_and_wasm_consumers() {
1658        let outcome = ToolAuthorityOutcome::ClientClosed {
1659            purged_grant_count: 2,
1660            closed_request_count: 1,
1661        };
1662        let encoded = serde_json::to_string(&outcome).unwrap();
1663        assert_eq!(
1664            serde_json::from_str::<ToolAuthorityOutcome>(&encoded).unwrap(),
1665            outcome
1666        );
1667
1668        let terminal = CapabilityTerminalOutcome::ProviderDetached {
1669            cancellation: CancellationDisposition::ProviderDetachedUnconfirmed,
1670        };
1671        let encoded = serde_json::to_string(&terminal).unwrap();
1672        assert_eq!(
1673            serde_json::from_str::<CapabilityTerminalOutcome>(&encoded).unwrap(),
1674            terminal
1675        );
1676        assert_eq!(
1677            format!("{terminal:?}"),
1678            "ProviderDetached { cancellation: ProviderDetachedUnconfirmed }"
1679        );
1680
1681        let snapshot = ToolEngineSnapshot {
1682            revision: 4,
1683            current_tick: 9,
1684            generations: vec![(AgentInstanceId(11), SessionGeneration(3))],
1685            instance_states: vec![(AgentInstanceId(11), ToolInstanceState::Active)],
1686            providers: Vec::new(),
1687            grants: Vec::new(),
1688            requests: Vec::new(),
1689            audit_events: vec![ToolAuditEvent {
1690                sequence: 1,
1691                tick: 9,
1692                subject: None,
1693                event: ToolAuditEventKind::ClientClosed {
1694                    consumer_id: ConsumerId::new("station.test").unwrap(),
1695                    actor_id: ToolActorId::new("agent.primary").unwrap(),
1696                    purged_grant_count: 2,
1697                    closed_request_count: 1,
1698                },
1699            }],
1700            dropped_audit_events: 0,
1701            revision_overflow_count: 0,
1702            next_completion_sequence: 2,
1703            dropped_completions: 0,
1704            effect_sequence_exhausted: false,
1705            completion_sequence_exhausted: false,
1706            audit_sequence_exhausted: false,
1707        };
1708        let encoded = serde_json::to_string(&snapshot).unwrap();
1709        let decoded = serde_json::from_str::<ToolEngineSnapshot>(&encoded).unwrap();
1710        assert_eq!(decoded, snapshot);
1711    }
1712}