1use 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#[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#[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#[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 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}