Skip to main content

router_protocol/
lib.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3#![forbid(unsafe_code)]
4
5//! Canonical, provider-neutral M1 protocol and replay state.
6
7use serde::{Deserialize, Deserializer, Serialize};
8use serde_json::{Value, json};
9use std::collections::HashSet;
10use std::fmt;
11use thiserror::Error;
12
13mod catalog_proof;
14mod route_identity;
15
16pub use catalog_proof::*;
17pub use route_identity::*;
18
19pub const M1_WIRE_VERSION: &str = "2026-07-24.m1";
20
21/// Protocol-level sanity bound for a per-request spend authorization
22/// (10 USD in micro-USD). The phase owner's configured request ceiling is the
23/// effective binding bound; this only prevents absurd declared values.
24pub const MAX_SPEND_AUTHORIZATION_MICRO_USD: u64 = 10_000_000;
25
26macro_rules! identifier {
27    ($name:ident) => {
28        #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
29        #[serde(transparent)]
30        pub struct $name(String);
31
32        impl $name {
33            pub fn parse(value: impl Into<String>) -> Result<Self, ValidationError> {
34                let value = value.into();
35                if value.is_empty()
36                    || value.len() > 128
37                    || !value.bytes().all(|byte| {
38                        byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':')
39                    })
40                {
41                    return Err(ValidationError::InvalidIdentifier(stringify!($name)));
42                }
43                Ok(Self(value))
44            }
45
46            pub fn as_str(&self) -> &str {
47                &self.0
48            }
49        }
50
51        impl fmt::Display for $name {
52            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53                formatter.write_str(&self.0)
54            }
55        }
56
57        impl<'de> Deserialize<'de> for $name {
58            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
59            where
60                D: Deserializer<'de>,
61            {
62                let value = String::deserialize(deserializer)?;
63                Self::parse(value).map_err(serde::de::Error::custom)
64            }
65        }
66    };
67}
68
69identifier!(HarnessInstanceId);
70identifier!(SessionId);
71identifier!(TurnId);
72identifier!(RequestId);
73identifier!(AttemptId);
74identifier!(ResponseId);
75identifier!(ItemId);
76identifier!(CallId);
77
78#[derive(Debug, Error, PartialEq, Eq)]
79pub enum ValidationError {
80    #[error("an identifier is invalid")]
81    InvalidIdentifier(&'static str),
82    #[error("the wire version is unsupported")]
83    UnsupportedWireVersion,
84    #[error("streaming is required")]
85    StreamingRequired,
86    #[error("the request contains a non-translatable capability")]
87    NonTranslatableCapability,
88    #[error("the tool definition is invalid or collides with another tool")]
89    InvalidToolDefinition,
90    #[error("the request declares an invalid spend authorization")]
91    InvalidSpendAuthorization,
92}
93
94#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
95#[serde(deny_unknown_fields)]
96pub struct CanonicalRequest {
97    pub wire_version: String,
98    pub model: String,
99    pub stream: bool,
100    pub harness_instance: HarnessInstanceId,
101    pub session_id: SessionId,
102    pub turn_id: TurnId,
103    pub input: Vec<InputItem>,
104    #[serde(default)]
105    pub tools: Vec<FunctionTool>,
106    #[serde(default)]
107    pub tool_choice: ToolChoice,
108    #[serde(default)]
109    pub structured_output: Option<StructuredOutputIntent>,
110    #[serde(default)]
111    pub reasoning: ReasoningIntent,
112    #[serde(default)]
113    pub max_output_tokens: Option<u32>,
114    /// Owner-bounded per-request spend authorization in micro-USD. A value of
115    /// zero explicitly forbids metered-phase spend for this request; absent
116    /// means the phase's configured request ceiling applies unchanged.
117    #[serde(default)]
118    pub max_spend_micro_usd: Option<u64>,
119    #[serde(default)]
120    pub capabilities: CapabilityRequirements,
121}
122
123impl CanonicalRequest {
124    pub fn validate(&self) -> Result<(), ValidationError> {
125        if self.wire_version != M1_WIRE_VERSION {
126            return Err(ValidationError::UnsupportedWireVersion);
127        }
128        if !self.stream {
129            return Err(ValidationError::StreamingRequired);
130        }
131        if let Some(max_spend) = self.max_spend_micro_usd
132            && max_spend > MAX_SPEND_AUTHORIZATION_MICRO_USD
133        {
134            return Err(ValidationError::InvalidSpendAuthorization);
135        }
136        validate_tools(&self.tools)?;
137        if let ToolChoice::Named(name) = &self.tool_choice {
138            validate_tool_name(name)?;
139            if self.tools.iter().filter(|tool| tool.name == *name).count() != 1 {
140                return Err(ValidationError::InvalidToolDefinition);
141            }
142        }
143        for item in &self.input {
144            if let InputItem::FunctionCall {
145                namespace, name, ..
146            } = item
147            {
148                validate_tool_name(name)?;
149                if let Some(namespace) = namespace {
150                    validate_tool_namespace(namespace)?;
151                }
152            }
153        }
154        Ok(())
155    }
156
157    pub fn function_output(&self) -> Option<(&CallId, &Value)> {
158        self.input.iter().find_map(|item| match item {
159            InputItem::FunctionCallOutput { call_id, output } => Some((call_id, output)),
160            InputItem::Message { .. } | InputItem::FunctionCall { .. } => None,
161        })
162    }
163
164    pub fn function_call(&self, expected_call_id: &CallId) -> Option<(Option<&str>, &str)> {
165        self.input.iter().find_map(|item| match item {
166            InputItem::FunctionCall {
167                call_id,
168                namespace,
169                name,
170                ..
171            } if call_id == expected_call_id => Some((namespace.as_deref(), name.as_str())),
172            InputItem::Message { .. }
173            | InputItem::FunctionCall { .. }
174            | InputItem::FunctionCallOutput { .. } => None,
175        })
176    }
177}
178
179#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
180#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
181pub enum InputItem {
182    Message {
183        role: MessageRole,
184        content: Vec<ContentBlock>,
185    },
186    FunctionCall {
187        call_id: CallId,
188        #[serde(default, skip_serializing_if = "Option::is_none")]
189        namespace: Option<String>,
190        name: String,
191        arguments: Value,
192    },
193    FunctionCallOutput {
194        call_id: CallId,
195        output: Value,
196    },
197}
198
199#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
200#[serde(rename_all = "snake_case")]
201pub enum MessageRole {
202    Developer,
203    System,
204    User,
205    Assistant,
206}
207
208#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
209#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
210pub enum ContentBlock {
211    Text { text: String },
212    ImageReference { url: String },
213}
214
215#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
216#[serde(deny_unknown_fields)]
217pub struct FunctionTool {
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    pub namespace: Option<String>,
220    pub name: String,
221    pub description: String,
222    pub parameters: Value,
223    pub strict: bool,
224}
225
226pub const MAX_TOOL_COMPONENT_BYTES: usize = 64;
227
228pub fn validate_tool_name(value: &str) -> Result<(), ValidationError> {
229    validate_tool_component(value)
230}
231
232pub fn validate_tool_namespace(value: &str) -> Result<(), ValidationError> {
233    validate_tool_component(value)?;
234    if matches!(
235        value,
236        "function" | "custom" | "namespace" | "tool_search" | "web_search"
237    ) {
238        return Err(ValidationError::InvalidToolDefinition);
239    }
240    Ok(())
241}
242
243fn validate_tool_component(value: &str) -> Result<(), ValidationError> {
244    if value.is_empty()
245        || value.len() > MAX_TOOL_COMPONENT_BYTES
246        || !value
247            .bytes()
248            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
249    {
250        return Err(ValidationError::InvalidToolDefinition);
251    }
252    Ok(())
253}
254
255fn validate_tools(tools: &[FunctionTool]) -> Result<(), ValidationError> {
256    let mut identities = HashSet::new();
257    let mut namespaces = HashSet::new();
258    let mut direct_names = HashSet::new();
259    for tool in tools {
260        validate_tool_name(&tool.name)?;
261        if let Some(namespace) = &tool.namespace {
262            validate_tool_namespace(namespace)?;
263            namespaces.insert(namespace.as_str());
264        } else {
265            direct_names.insert(tool.name.as_str());
266        }
267        if !identities.insert((tool.namespace.as_deref(), tool.name.as_str())) {
268            return Err(ValidationError::InvalidToolDefinition);
269        }
270    }
271    if namespaces
272        .iter()
273        .any(|namespace| direct_names.contains(namespace))
274    {
275        return Err(ValidationError::InvalidToolDefinition);
276    }
277    Ok(())
278}
279
280#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
281#[serde(rename_all = "snake_case")]
282pub enum ToolChoice {
283    #[default]
284    Auto,
285    None,
286    Required,
287    Named(String),
288}
289
290#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
291#[serde(deny_unknown_fields)]
292pub struct StructuredOutputIntent {
293    pub name: String,
294    pub schema: Value,
295    pub strict: bool,
296}
297
298#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
299#[serde(rename_all = "snake_case")]
300pub enum ReasoningIntent {
301    #[default]
302    None,
303    PublicSummary,
304}
305
306#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
307#[serde(deny_unknown_fields)]
308pub struct CapabilityRequirements {
309    #[serde(default)]
310    pub images: bool,
311    #[serde(default)]
312    pub structured_output: bool,
313    #[serde(default)]
314    pub public_reasoning: bool,
315    #[serde(default)]
316    pub audio: bool,
317}
318
319#[derive(Clone, Copy, Debug, PartialEq, Eq)]
320pub struct ProviderCapabilities {
321    pub images: bool,
322    pub structured_output: bool,
323    pub public_reasoning: bool,
324    pub audio: bool,
325}
326
327impl CapabilityRequirements {
328    pub fn preflight(&self, supported: ProviderCapabilities) -> Result<(), PublicErrorCode> {
329        if (self.images && !supported.images)
330            || (self.structured_output && !supported.structured_output)
331            || (self.public_reasoning && !supported.public_reasoning)
332            || (self.audio && !supported.audio)
333        {
334            return Err(PublicErrorCode::UnsupportedCapability);
335        }
336        Ok(())
337    }
338}
339
340#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
341#[serde(rename_all = "snake_case")]
342pub enum PublicErrorCode {
343    InvalidRequest,
344    AuthenticationRequired,
345    ModelNotFound,
346    UnsupportedCapability,
347    UnsupportedWireCombination,
348    RouterUnavailable,
349    CorrelationMismatch,
350    ProviderFailure,
351}
352
353impl PublicErrorCode {
354    pub fn as_str(&self) -> &'static str {
355        match self {
356            Self::InvalidRequest => "invalid_request",
357            Self::AuthenticationRequired => "authentication_required",
358            Self::ModelNotFound => "model_not_found",
359            Self::UnsupportedCapability => "unsupported_capability",
360            Self::UnsupportedWireCombination => "unsupported_wire_combination",
361            Self::RouterUnavailable => "router_unavailable",
362            Self::CorrelationMismatch => "correlation_mismatch",
363            Self::ProviderFailure => "provider_failure",
364        }
365    }
366}
367
368#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
369#[serde(deny_unknown_fields)]
370pub struct PublicError {
371    pub code: PublicErrorCode,
372    pub message: String,
373    pub request_id: RequestId,
374    pub retryable: bool,
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub details: Option<Value>,
377}
378
379#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
380#[serde(rename_all = "snake_case")]
381pub enum InterruptionCause {
382    ProviderFailure,
383    MalformedStream,
384    Timeout,
385    Cancelled,
386}
387
388impl InterruptionCause {
389    const fn as_str(self) -> &'static str {
390        match self {
391            Self::ProviderFailure => "provider_failure",
392            Self::MalformedStream => "malformed_stream",
393            Self::Timeout => "timeout",
394            Self::Cancelled => "cancelled",
395        }
396    }
397}
398
399#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
400#[serde(rename_all = "snake_case")]
401pub enum SafeResumeBoundary {
402    PublicOutput,
403    ToolCall,
404    ForwardedToolResult,
405}
406
407impl SafeResumeBoundary {
408    const fn as_str(self) -> &'static str {
409        match self {
410            Self::PublicOutput => "public_output",
411            Self::ToolCall => "tool_call",
412            Self::ForwardedToolResult => "forwarded_tool_result",
413        }
414    }
415}
416
417impl PublicError {
418    pub fn redacted(code: PublicErrorCode, request_id: RequestId, retryable: bool) -> Self {
419        let message = match code {
420            PublicErrorCode::InvalidRequest => "The request is invalid.",
421            PublicErrorCode::AuthenticationRequired => "Local authentication is required.",
422            PublicErrorCode::ModelNotFound => "The requested model alias is unavailable.",
423            PublicErrorCode::UnsupportedCapability => {
424                "The route does not support a required capability."
425            }
426            PublicErrorCode::UnsupportedWireCombination => {
427                "The requested wire combination is unsupported."
428            }
429            PublicErrorCode::RouterUnavailable => "The local router is unavailable.",
430            PublicErrorCode::CorrelationMismatch => "The tool result correlation is invalid.",
431            PublicErrorCode::ProviderFailure => "The transformed provider failed.",
432        };
433        Self {
434            code,
435            message: message.to_owned(),
436            request_id,
437            retryable,
438            details: None,
439        }
440    }
441
442    pub fn interrupted(
443        request_id: RequestId,
444        cause: InterruptionCause,
445        boundary: SafeResumeBoundary,
446    ) -> Self {
447        let mut error = Self::redacted(PublicErrorCode::ProviderFailure, request_id, false);
448        error.message = "The provider stream stopped after a non-replayable boundary.".to_owned();
449        error.details = Some(json!({
450            "interruption": {
451                "cause": cause.as_str(),
452                "boundary": boundary.as_str(),
453                "automatic_replay": false,
454                "safe_resume": {
455                    "action": "start_new_turn",
456                    "preserve_visible_output": true,
457                    "repeat_completed_tool_calls": false
458                }
459            }
460        }));
461        error
462    }
463}
464
465/// How a subagent-bearing request is served, recorded before any visible
466/// output or harness-visible tool effect (PRD-001l R6 — no silent
467/// degradation). Every subagent request ends in exactly one of these states.
468#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
469#[serde(tag = "type", rename_all = "snake_case")]
470pub enum SubagentDisposition {
471    /// Served natively by a phase that returns the `namespace` field.
472    Native,
473    /// Served by the labeled emulation fallback (fan-out of independent
474    /// sessions); never claims native semantics.
475    Emulated,
476    /// Refused with a deterministic, human-readable reason before any visible
477    /// output or harness-visible tool effect.
478    Refused { reason: String },
479}
480
481impl SubagentDisposition {
482    /// Derives the disposition for a subagent-bearing request from whether a
483    /// native phase was selected and whether emulation is enabled. Emulation
484    /// is only ever labeled; a request is refused when neither native service
485    /// nor emulation is available.
486    pub fn derive(native_selected: bool, emulation_enabled: bool) -> Self {
487        if native_selected {
488            Self::Native
489        } else if emulation_enabled {
490            Self::Emulated
491        } else {
492            Self::Refused {
493                reason: "no_phase_serves_subagents_natively_and_emulation_is_disabled".to_owned(),
494            }
495        }
496    }
497}
498
499#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
500#[serde(tag = "type")]
501pub enum CanonicalEvent {
502    #[serde(rename = "response.created")]
503    ResponseCreated {
504        response_id: ResponseId,
505        request_id: RequestId,
506        sequence: u64,
507    },
508    #[serde(rename = "response.in_progress")]
509    ResponseInProgress {
510        response_id: ResponseId,
511        sequence: u64,
512    },
513    #[serde(rename = "response.output_item.added")]
514    OutputItemAdded {
515        response_id: ResponseId,
516        item_id: ItemId,
517        sequence: u64,
518        item_kind: OutputItemKind,
519        #[serde(skip_serializing_if = "Option::is_none")]
520        call_id: Option<CallId>,
521        #[serde(skip_serializing_if = "Option::is_none")]
522        name: Option<String>,
523        #[serde(skip_serializing_if = "Option::is_none")]
524        namespace: Option<String>,
525    },
526    #[serde(rename = "response.output_item.done")]
527    OutputItemDone {
528        response_id: ResponseId,
529        item_id: ItemId,
530        sequence: u64,
531        #[serde(skip_serializing_if = "Option::is_none")]
532        call_id: Option<CallId>,
533    },
534    #[serde(rename = "response.content_part.added")]
535    ContentPartAdded {
536        response_id: ResponseId,
537        item_id: ItemId,
538        sequence: u64,
539    },
540    #[serde(rename = "response.content_part.done")]
541    ContentPartDone {
542        response_id: ResponseId,
543        item_id: ItemId,
544        sequence: u64,
545    },
546    #[serde(rename = "response.output_text.delta")]
547    OutputTextDelta {
548        response_id: ResponseId,
549        item_id: ItemId,
550        sequence: u64,
551        delta: String,
552    },
553    #[serde(rename = "response.output_text.done")]
554    OutputTextDone {
555        response_id: ResponseId,
556        item_id: ItemId,
557        sequence: u64,
558        text: String,
559    },
560    #[serde(rename = "response.function_call_arguments.delta")]
561    FunctionCallArgumentsDelta {
562        response_id: ResponseId,
563        item_id: ItemId,
564        call_id: CallId,
565        sequence: u64,
566        delta: String,
567    },
568    #[serde(rename = "response.function_call_arguments.done")]
569    FunctionCallArgumentsDone {
570        response_id: ResponseId,
571        item_id: ItemId,
572        call_id: CallId,
573        sequence: u64,
574        arguments: Value,
575    },
576    #[serde(rename = "response.completed")]
577    ResponseCompleted {
578        response_id: ResponseId,
579        sequence: u64,
580        usage: Usage,
581    },
582    #[serde(rename = "response.failed")]
583    ResponseFailed {
584        response_id: ResponseId,
585        sequence: u64,
586        error: PublicError,
587    },
588    #[serde(rename = "error")]
589    Error { sequence: u64, error: PublicError },
590}
591
592impl CanonicalEvent {
593    pub fn event_name(&self) -> &'static str {
594        match self {
595            Self::ResponseCreated { .. } => "response.created",
596            Self::ResponseInProgress { .. } => "response.in_progress",
597            Self::OutputItemAdded { .. } => "response.output_item.added",
598            Self::OutputItemDone { .. } => "response.output_item.done",
599            Self::ContentPartAdded { .. } => "response.content_part.added",
600            Self::ContentPartDone { .. } => "response.content_part.done",
601            Self::OutputTextDelta { .. } => "response.output_text.delta",
602            Self::OutputTextDone { .. } => "response.output_text.done",
603            Self::FunctionCallArgumentsDelta { .. } => "response.function_call_arguments.delta",
604            Self::FunctionCallArgumentsDone { .. } => "response.function_call_arguments.done",
605            Self::ResponseCompleted { .. } => "response.completed",
606            Self::ResponseFailed { .. } => "response.failed",
607            Self::Error { .. } => "error",
608        }
609    }
610
611    pub fn visibility(&self) -> EventVisibility {
612        match self {
613            Self::OutputTextDelta { .. }
614            | Self::OutputTextDone { .. }
615            | Self::ContentPartAdded { .. }
616            | Self::ContentPartDone { .. } => EventVisibility::PublicOutput,
617            Self::OutputItemAdded {
618                item_kind: OutputItemKind::FunctionCall,
619                ..
620            }
621            | Self::FunctionCallArgumentsDelta { .. }
622            | Self::FunctionCallArgumentsDone { .. } => EventVisibility::ToolCall,
623            _ => EventVisibility::None,
624        }
625    }
626}
627
628#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
629#[serde(rename_all = "snake_case")]
630pub enum OutputItemKind {
631    Message,
632    FunctionCall,
633}
634
635#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
636#[serde(deny_unknown_fields)]
637pub struct Usage {
638    pub input_tokens: u64,
639    pub output_tokens: u64,
640}
641
642#[derive(Clone, Copy, Debug, PartialEq, Eq)]
643pub enum EventVisibility {
644    None,
645    PublicOutput,
646    ToolCall,
647}
648
649#[derive(Clone, Copy, Debug, PartialEq, Eq)]
650pub enum VisibilityState {
651    PreVisible,
652    VisibleOutput,
653    ToolVisible,
654    VisibleOutputAndTool,
655    TransportAmbiguous,
656}
657
658#[derive(Debug)]
659pub struct ReplayGuard {
660    state: VisibilityState,
661}
662
663#[derive(Debug)]
664pub struct ReplayAuthorization(());
665
666impl Default for ReplayGuard {
667    fn default() -> Self {
668        Self {
669            state: VisibilityState::PreVisible,
670        }
671    }
672}
673
674impl ReplayGuard {
675    pub fn observe(&mut self, visibility: EventVisibility) {
676        self.state = match (self.state, visibility) {
677            (state, EventVisibility::None) => state,
678            (VisibilityState::PreVisible, EventVisibility::PublicOutput) => {
679                VisibilityState::VisibleOutput
680            }
681            (VisibilityState::PreVisible, EventVisibility::ToolCall) => {
682                VisibilityState::ToolVisible
683            }
684            (VisibilityState::VisibleOutput, EventVisibility::ToolCall)
685            | (VisibilityState::ToolVisible, EventVisibility::PublicOutput) => {
686                VisibilityState::VisibleOutputAndTool
687            }
688            (state, EventVisibility::PublicOutput | EventVisibility::ToolCall) => state,
689        };
690    }
691
692    pub fn mark_transport_ambiguous(&mut self) {
693        self.state = VisibilityState::TransportAmbiguous;
694    }
695
696    pub fn mark_tool_result_forwarded(&mut self) {
697        self.state = VisibilityState::ToolVisible;
698    }
699
700    pub fn authorize_transient_retry(&self) -> Option<ReplayAuthorization> {
701        (self.state == VisibilityState::PreVisible).then_some(ReplayAuthorization(()))
702    }
703
704    pub fn state(&self) -> VisibilityState {
705        self.state
706    }
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712    use serde_json::json;
713
714    #[test]
715    fn strict_request_rejects_hidden_reasoning_and_unknown_fields() {
716        let request = json!({
717            "wire_version": M1_WIRE_VERSION,
718            "model": "router-auto",
719            "stream": true,
720            "harness_instance": "cli",
721            "session_id": "session-1",
722            "turn_id": "turn-1",
723            "input": [],
724            "encrypted_reasoning": "must-not-cross"
725        });
726        assert!(serde_json::from_value::<CanonicalRequest>(request).is_err());
727    }
728
729    #[test]
730    fn identifier_deserialization_enforces_constructor_invariants() {
731        for invalid in [json!(""), json!("line\nbreak"), json!("x".repeat(129))] {
732            assert!(serde_json::from_value::<SessionId>(invalid).is_err());
733        }
734        assert_eq!(
735            serde_json::from_value::<SessionId>(json!("session-safe_1"))
736                .expect("valid identifier")
737                .as_str(),
738            "session-safe_1"
739        );
740    }
741
742    #[test]
743    fn replay_truth_table_is_fail_closed() {
744        let mut pre = ReplayGuard::default();
745        assert!(pre.authorize_transient_retry().is_some());
746
747        pre.observe(EventVisibility::PublicOutput);
748        assert!(pre.authorize_transient_retry().is_none());
749
750        let mut tool = ReplayGuard::default();
751        tool.observe(EventVisibility::ToolCall);
752        assert!(tool.authorize_transient_retry().is_none());
753
754        let mut both = ReplayGuard::default();
755        both.observe(EventVisibility::PublicOutput);
756        both.observe(EventVisibility::ToolCall);
757        assert!(both.authorize_transient_retry().is_none());
758
759        let mut ambiguous = ReplayGuard::default();
760        ambiguous.mark_transport_ambiguous();
761        assert!(ambiguous.authorize_transient_retry().is_none());
762
763        let mut forwarded_result = ReplayGuard::default();
764        forwarded_result.mark_tool_result_forwarded();
765        assert!(forwarded_result.authorize_transient_retry().is_none());
766    }
767
768    #[test]
769    fn structured_interruption_has_one_safe_non_replay_resume_action() {
770        let error = PublicError::interrupted(
771            RequestId::parse("request-interrupted").expect("fixture ID"),
772            InterruptionCause::ProviderFailure,
773            SafeResumeBoundary::ForwardedToolResult,
774        );
775        assert_eq!(error.code, PublicErrorCode::ProviderFailure);
776        assert!(!error.retryable);
777        assert_eq!(
778            error.details,
779            Some(json!({
780                "interruption": {
781                    "cause": "provider_failure",
782                    "boundary": "forwarded_tool_result",
783                    "automatic_replay": false,
784                    "safe_resume": {
785                        "action": "start_new_turn",
786                        "preserve_visible_output": true,
787                        "repeat_completed_tool_calls": false
788                    }
789                }
790            }))
791        );
792    }
793
794    #[test]
795    fn named_tool_choice_requires_one_unambiguous_declared_tool() {
796        let function = |namespace: Option<&str>| FunctionTool {
797            namespace: namespace.map(str::to_owned),
798            name: "lookup".to_owned(),
799            description: "fixture".to_owned(),
800            parameters: json!({}),
801            strict: true,
802        };
803        let mut request = CanonicalRequest {
804            wire_version: M1_WIRE_VERSION.to_owned(),
805            model: "fixture-transformed".to_owned(),
806            stream: true,
807            harness_instance: HarnessInstanceId::parse("cli").expect("fixture ID"),
808            session_id: SessionId::parse("session").expect("fixture ID"),
809            turn_id: TurnId::parse("turn").expect("fixture ID"),
810            input: Vec::new(),
811            tools: vec![function(None)],
812            tool_choice: ToolChoice::Named("lookup".to_owned()),
813            structured_output: None,
814            reasoning: ReasoningIntent::None,
815            max_output_tokens: None,
816            max_spend_micro_usd: None,
817            capabilities: CapabilityRequirements::default(),
818        };
819        assert_eq!(request.validate(), Ok(()));
820        request.tools = vec![function(Some("one")), function(Some("two"))];
821        assert_eq!(
822            request.validate(),
823            Err(ValidationError::InvalidToolDefinition)
824        );
825        request.tools = vec![function(None)];
826        request.tool_choice = ToolChoice::Named("other".to_owned());
827        assert_eq!(
828            request.validate(),
829            Err(ValidationError::InvalidToolDefinition)
830        );
831    }
832
833    #[test]
834    fn spend_authorization_is_bounded_and_zero_means_metered_spend_forbidden() {
835        let base = CanonicalRequest {
836            wire_version: M1_WIRE_VERSION.to_owned(),
837            model: "router-auto".to_owned(),
838            stream: true,
839            harness_instance: HarnessInstanceId::parse("desktop").expect("fixture ID"),
840            session_id: SessionId::parse("session-spend").expect("fixture ID"),
841            turn_id: TurnId::parse("turn-spend").expect("fixture ID"),
842            input: Vec::new(),
843            tools: Vec::new(),
844            tool_choice: ToolChoice::None,
845            structured_output: None,
846            reasoning: ReasoningIntent::None,
847            max_output_tokens: None,
848            max_spend_micro_usd: None,
849            capabilities: CapabilityRequirements::default(),
850        };
851        assert_eq!(base.validate(), Ok(()));
852
853        let mut zero = base.clone();
854        zero.max_spend_micro_usd = Some(0);
855        assert_eq!(zero.validate(), Ok(()));
856
857        let mut at_bound = base.clone();
858        at_bound.max_spend_micro_usd = Some(MAX_SPEND_AUTHORIZATION_MICRO_USD);
859        assert_eq!(at_bound.validate(), Ok(()));
860
861        let mut absurd = base.clone();
862        absurd.max_spend_micro_usd = Some(MAX_SPEND_AUTHORIZATION_MICRO_USD + 1);
863        assert_eq!(
864            absurd.validate(),
865            Err(ValidationError::InvalidSpendAuthorization)
866        );
867    }
868
869    #[test]
870    fn required_capability_is_never_silently_removed() {
871        let required = CapabilityRequirements {
872            images: true,
873            structured_output: true,
874            public_reasoning: true,
875            audio: false,
876        };
877        let supported = ProviderCapabilities {
878            images: true,
879            structured_output: false,
880            public_reasoning: true,
881            audio: false,
882        };
883        assert_eq!(
884            required.preflight(supported),
885            Err(PublicErrorCode::UnsupportedCapability)
886        );
887    }
888
889    #[test]
890    fn canonical_fixture_round_trip_preserves_supported_semantics() {
891        let request = CanonicalRequest {
892            wire_version: M1_WIRE_VERSION.to_owned(),
893            model: "fixture-transformed".to_owned(),
894            stream: true,
895            harness_instance: HarnessInstanceId::parse("desktop").expect("fixture ID"),
896            session_id: SessionId::parse("session-semantic").expect("fixture ID"),
897            turn_id: TurnId::parse("turn-semantic").expect("fixture ID"),
898            input: vec![
899                InputItem::Message {
900                    role: MessageRole::Developer,
901                    content: vec![
902                        ContentBlock::Text {
903                            text: "public fixture".to_owned(),
904                        },
905                        ContentBlock::ImageReference {
906                            url: "fixture://image-1".to_owned(),
907                        },
908                    ],
909                },
910                InputItem::FunctionCall {
911                    call_id: CallId::parse("call-semantic").expect("fixture ID"),
912                    namespace: Some("catalog".to_owned()),
913                    name: "lookup".to_owned(),
914                    arguments: json!({"key": "value"}),
915                },
916                InputItem::FunctionCallOutput {
917                    call_id: CallId::parse("call-semantic").expect("fixture ID"),
918                    output: json!({"result": 42}),
919                },
920            ],
921            tools: vec![FunctionTool {
922                namespace: Some("catalog".to_owned()),
923                name: "lookup".to_owned(),
924                description: "Synthetic lookup".to_owned(),
925                parameters: json!({"type": "object", "properties": {"key": {"type": "string"}}}),
926                strict: true,
927            }],
928            tool_choice: ToolChoice::Named("lookup".to_owned()),
929            structured_output: Some(StructuredOutputIntent {
930                name: "fixture_result".to_owned(),
931                schema: json!({"type": "object", "properties": {"result": {"type": "integer"}}}),
932                strict: true,
933            }),
934            reasoning: ReasoningIntent::PublicSummary,
935            max_output_tokens: Some(128),
936            max_spend_micro_usd: None,
937            capabilities: CapabilityRequirements {
938                images: true,
939                structured_output: true,
940                public_reasoning: true,
941                audio: false,
942            },
943        };
944        let encoded = serde_json::to_vec(&request).expect("serialize canonical fixture");
945        let decoded: CanonicalRequest =
946            serde_json::from_slice(&encoded).expect("deserialize canonical fixture");
947        assert_eq!(decoded, request);
948        assert_eq!(decoded.tools[0].namespace.as_deref(), Some("catalog"));
949
950        let failed = CanonicalEvent::ResponseFailed {
951            response_id: ResponseId::parse("response-semantic").expect("fixture ID"),
952            sequence: 17,
953            error: PublicError::redacted(
954                PublicErrorCode::ProviderFailure,
955                RequestId::parse("request-semantic").expect("fixture ID"),
956                false,
957            ),
958        };
959        let failed_round_trip: CanonicalEvent = serde_json::from_value(
960            serde_json::to_value(&failed).expect("serialize terminal failure"),
961        )
962        .expect("deserialize terminal failure");
963        assert_eq!(failed_round_trip, failed);
964    }
965
966    #[test]
967    fn canonical_tool_namespaces_reject_duplicates_reserved_names_and_collisions() {
968        let function = |namespace: Option<&str>, name: &str| FunctionTool {
969            namespace: namespace.map(str::to_owned),
970            name: name.to_owned(),
971            description: "fixture".to_owned(),
972            parameters: json!({}),
973            strict: true,
974        };
975        assert_eq!(
976            validate_tools(&[
977                function(Some("collaboration"), "spawn_agent"),
978                function(Some("collaboration"), "spawn_agent"),
979            ]),
980            Err(ValidationError::InvalidToolDefinition)
981        );
982        assert_eq!(
983            validate_tools(&[
984                function(None, "collaboration"),
985                function(Some("collaboration"), "spawn_agent"),
986            ]),
987            Err(ValidationError::InvalidToolDefinition)
988        );
989        assert_eq!(
990            validate_tool_namespace("namespace"),
991            Err(ValidationError::InvalidToolDefinition)
992        );
993        assert_eq!(
994            validate_tool_name(&"x".repeat(MAX_TOOL_COMPONENT_BYTES + 1)),
995            Err(ValidationError::InvalidToolDefinition)
996        );
997    }
998
999    #[test]
1000    fn subagent_disposition_is_deterministic_and_never_silent() {
1001        assert_eq!(
1002            SubagentDisposition::derive(true, false),
1003            SubagentDisposition::Native
1004        );
1005        assert_eq!(
1006            SubagentDisposition::derive(true, true),
1007            SubagentDisposition::Native
1008        );
1009        assert_eq!(
1010            SubagentDisposition::derive(false, true),
1011            SubagentDisposition::Emulated
1012        );
1013        assert_eq!(
1014            SubagentDisposition::derive(false, false),
1015            SubagentDisposition::Refused {
1016                reason: "no_phase_serves_subagents_natively_and_emulation_is_disabled".to_owned()
1017            }
1018        );
1019    }
1020}