Skip to main content

gate4agent_types/
spec.rs

1use crate::{normalize_executable_name, AdapterBinding, AgentId, AgentReadinessSpec};
2use serde::{Deserialize, Serialize};
3
4/// Runtime in which an executable is detected or launched.
5#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
6#[serde(rename_all = "kebab-case")]
7pub enum RuntimePlatform {
8    Windows,
9    MacOs,
10    Linux,
11    Wsl,
12}
13
14impl RuntimePlatform {
15    pub fn current() -> Self {
16        if cfg!(target_os = "windows") {
17            Self::Windows
18        } else if cfg!(target_os = "macos") {
19            Self::MacOs
20        } else {
21            Self::Linux
22        }
23    }
24}
25
26#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
27pub struct DetectionSpec {
28    pub command: String,
29    #[serde(default)]
30    pub aliases: Vec<String>,
31    #[serde(default)]
32    pub required_commands: Vec<String>,
33    #[serde(default)]
34    pub unsupported_platforms: Vec<RuntimePlatform>,
35}
36
37#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
38pub struct LaunchSpec {
39    /// Executable name or absolute path. It is never a shell command string.
40    pub program: String,
41    #[serde(default)]
42    pub fixed_args: Vec<String>,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
46#[serde(tag = "kind", rename_all = "kebab-case")]
47pub enum ProcessMatcher {
48    Exact { name: String },
49    Prefix { prefix: String },
50}
51
52impl ProcessMatcher {
53    pub fn matches(&self, process_or_path: &str, platform: RuntimePlatform) -> bool {
54        let process = normalize_executable_name(process_or_path, platform);
55        match self {
56            Self::Exact { name } => {
57                let expected = normalize_executable_name(name, platform);
58                process == expected
59            }
60            Self::Prefix { prefix } => {
61                let expected = if platform == RuntimePlatform::Windows {
62                    prefix.to_ascii_lowercase()
63                } else {
64                    prefix.clone()
65                };
66                process.starts_with(&expected)
67            }
68        }
69    }
70}
71
72#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
73#[serde(tag = "kind", rename_all = "kebab-case")]
74pub enum InitialPromptMode {
75    None,
76    Positional { option_terminator: bool },
77    Flag { flag: String },
78    InteractiveFlag { flag: String },
79    AgentNativeQuery,
80    AfterReady,
81}
82
83#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
84#[serde(tag = "kind", rename_all = "kebab-case")]
85pub enum NativeDraftMode {
86    Flag { flag: String },
87}
88
89#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
90pub struct PromptSpec {
91    pub initial: InitialPromptMode,
92    #[serde(default)]
93    pub native_draft: Option<NativeDraftMode>,
94}
95
96#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
97#[serde(rename_all = "kebab-case")]
98pub enum AgentCommandMode {
99    SlashLine,
100}
101
102#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
103pub struct AgentCapabilities {
104    #[serde(default)]
105    pub agent_commands: Option<AgentCommandMode>,
106    #[serde(default)]
107    pub transports: AgentTransportCapabilities,
108    #[serde(default)]
109    pub adapters: AgentAdapterCapabilities,
110}
111
112#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
113pub struct AgentAdapterCapabilities {
114    /// Structured observation emitted beside an interactive PTY. This is an
115    /// adapter binding only and does not declare Pipe as a launch transport.
116    #[serde(default)]
117    pub pty_sidecar: Option<AdapterBinding>,
118    #[serde(default)]
119    pub hook: Option<AdapterBinding>,
120    /// Explicit host-level install/status/remove support for provider Hook
121    /// configuration. This authority is separate from raw Hook normalization.
122    #[serde(default)]
123    pub managed_hook: Option<AdapterBinding>,
124    /// Pure provider-owned non-interactive invocation planning. Native process
125    /// ownership remains in a dedicated shell even when delivered as Pipe.
126    #[serde(default)]
127    pub one_shot: Option<AdapterBinding>,
128    #[serde(default)]
129    pub history: Option<AdapterBinding>,
130    #[serde(default)]
131    pub resume: Option<AdapterBinding>,
132    #[serde(default)]
133    pub session_options: Option<AdapterBinding>,
134    #[serde(default)]
135    pub capability_probe: Option<AdapterBinding>,
136}
137
138#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
139#[serde(rename_all = "kebab-case")]
140pub enum PipePromptDelivery {
141    None,
142    StdinClose,
143    Positional,
144}
145
146#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
147#[serde(rename_all = "kebab-case")]
148pub enum PipeProtocol {
149    #[default]
150    SemanticNdjson,
151    /// Provider-owned structured JSONL emitted by a headless CLI invocation.
152    /// Unlike `SemanticNdjson`, this path may expose a provider session
153    /// identity and be resumed by a later child process.
154    StructuredJsonl,
155    OneShotText,
156}
157
158/// Optional catalog override used by controlled fixtures and providers whose
159/// headless executable differs from their interactive PTY executable.
160#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
161pub struct PipeTransportSpec {
162    pub adapter: AdapterBinding,
163    #[serde(default)]
164    pub protocol: PipeProtocol,
165    #[serde(default)]
166    pub launch_override: Option<LaunchSpec>,
167    #[serde(default = "default_pipe_prompt_delivery")]
168    pub prompt_delivery: PipePromptDelivery,
169}
170
171fn default_pipe_prompt_delivery() -> PipePromptDelivery {
172    PipePromptDelivery::None
173}
174
175#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
176pub struct AcpTransportSpec {
177    pub adapter: AdapterBinding,
178    #[serde(default)]
179    pub launch_override: Option<LaunchSpec>,
180}
181
182#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
183pub struct AgentTransportCapabilities {
184    #[serde(default = "default_true")]
185    pub pty: bool,
186    #[serde(default)]
187    pub pty_adapter: Option<AdapterBinding>,
188    #[serde(default)]
189    pub pipe: Option<PipeTransportSpec>,
190    #[serde(default)]
191    pub acp: Option<AcpTransportSpec>,
192}
193
194impl Default for AgentTransportCapabilities {
195    fn default() -> Self {
196        Self {
197            pty: true,
198            pty_adapter: None,
199            pipe: None,
200            acp: None,
201        }
202    }
203}
204
205fn default_true() -> bool {
206    true
207}
208
209/// Provenance state for a built-in launch specification.
210///
211/// `Reference` means the shape still requires verification against a pinned
212/// vendor CLI before a product presents it as fully supported.
213#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
214#[serde(rename_all = "kebab-case")]
215pub enum SpecVerification {
216    Gate4AgentVerified,
217    Reference,
218}
219
220#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
221pub struct AgentSpec {
222    pub id: AgentId,
223    /// Revision of the launch/detection contract, not the installed CLI version.
224    pub revision: String,
225    pub display_name: String,
226    pub detection: DetectionSpec,
227    pub launch: LaunchSpec,
228    pub expected_processes: Vec<ProcessMatcher>,
229    pub prompt: PromptSpec,
230    pub readiness: AgentReadinessSpec,
231    #[serde(default)]
232    pub capabilities: AgentCapabilities,
233    pub verification: SpecVerification,
234}
235
236impl AgentSpec {
237    pub fn supports_platform(&self, platform: RuntimePlatform) -> bool {
238        !self.detection.unsupported_platforms.contains(&platform)
239    }
240}