1use std::collections::{BTreeSet, VecDeque};
2use std::fs::OpenOptions;
3use std::io::Read;
4use std::path::{Path, PathBuf};
5use std::process::{Child, Command, Stdio};
6use std::thread;
7use std::time::{Duration, Instant, UNIX_EPOCH};
8
9const MAX_VERSION_OUTPUT_BYTES: usize = 16 * 1024;
10const MAX_LAUNCHER_BYTES: u64 = 512 * 1024 * 1024;
11const VERSION_PROBE_CACHE_CAPACITY: usize = 16;
12const VERSION_PROBE_POLL_INTERVAL: Duration = Duration::from_millis(5);
13const VERSION_PROBE_MAX_CLEANUP_RESERVE: Duration = Duration::from_millis(100);
14const CLAUDE_VERSION_ARGV: &[&str] = &["--version"];
15const CODEX_VERSION_ARGV: &[&str] = &["--version"];
16const KIMI_VERSION_ARGV: &[&str] = &["--version"];
17
18pub const CLAUDE_WINDOWS_X86_64_2_1_223_CONTRACT_ID: &str =
19 "claude.windows-x86_64.2.1.223";
20pub const CLAUDE_WINDOWS_X86_64_2_1_224_CONTRACT_ID: &str =
21 "claude.windows-x86_64.2.1.224";
22
23const RAW_PASSTHROUGH: VendorCapabilityVerdict = VendorCapabilityVerdict::RawPassthrough;
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum VendorCliFamily {
27 Claude,
28 Codex,
29 Kimi,
30 Unknown,
31}
32
33impl VendorCliFamily {
34 pub fn from_agent_id(agent_id: &str) -> Self {
35 if agent_id.eq_ignore_ascii_case("claude") {
36 Self::Claude
37 } else if agent_id.eq_ignore_ascii_case("codex") {
38 Self::Codex
39 } else if agent_id.eq_ignore_ascii_case("kimi") {
40 Self::Kimi
41 } else {
42 Self::Unknown
43 }
44 }
45
46 pub const fn label(self) -> &'static str {
47 match self {
48 Self::Claude => "claude",
49 Self::Codex => "codex",
50 Self::Kimi => "kimi",
51 Self::Unknown => "unknown",
52 }
53 }
54}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum VendorPlatform {
58 WindowsX86_64,
59 WindowsAarch64,
60 LinuxX86_64,
61 LinuxAarch64,
62 MacosX86_64,
63 MacosAarch64,
64 Other,
65}
66
67impl VendorPlatform {
68 pub fn current() -> Self {
69 match (std::env::consts::OS, std::env::consts::ARCH) {
70 ("windows", "x86_64") => Self::WindowsX86_64,
71 ("windows", "aarch64") => Self::WindowsAarch64,
72 ("linux", "x86_64") => Self::LinuxX86_64,
73 ("linux", "aarch64") => Self::LinuxAarch64,
74 ("macos", "x86_64") => Self::MacosX86_64,
75 ("macos", "aarch64") => Self::MacosAarch64,
76 _ => Self::Other,
77 }
78 }
79
80 pub const fn label(self) -> &'static str {
81 match self {
82 Self::WindowsX86_64 => "windows-x86_64",
83 Self::WindowsAarch64 => "windows-aarch64",
84 Self::LinuxX86_64 => "linux-x86_64",
85 Self::LinuxAarch64 => "linux-aarch64",
86 Self::MacosX86_64 => "macos-x86_64",
87 Self::MacosAarch64 => "macos-aarch64",
88 Self::Other => "other",
89 }
90 }
91}
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub enum VendorRuntimeMode {
95 RawPassthrough,
96 VerifiedSemantic,
97}
98
99impl VendorRuntimeMode {
100 pub const fn label(self) -> &'static str {
101 match self {
102 Self::RawPassthrough => "raw_passthrough",
103 Self::VerifiedSemantic => "verified_semantic",
104 }
105 }
106}
107
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub enum VendorVersionStatus {
110 Normalized,
111 Missing,
112 Ambiguous,
113 Unparseable,
114 Unavailable,
115}
116
117#[derive(Clone, Copy, Debug, Eq, PartialEq)]
118pub enum VendorFallbackReason {
119 UnsupportedVendor,
120 NoVerifiedProfile,
121 UnsupportedPlatform,
122 LegacyVersion,
123 FutureVersion,
124 UnlistedVersion,
125 MissingVersion,
126 AmbiguousVersion,
127 UnparseableVersion,
128 InvalidLauncher,
129 LauncherUnavailable,
130 LauncherChanged,
131 ProbeDeadlineExceeded,
132 ProbeSpawnFailed,
133 ProbeNonzero,
134 ProbeOutputOverflow,
135}
136
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub enum VendorCapabilityUnavailableReason {
139 NoVerifiedContract(VendorFallbackReason),
140 NotVerifiedByContract,
141}
142
143#[derive(Clone, Copy, Debug, Eq, PartialEq)]
144pub enum VendorCapabilityVerdict {
145 RawPassthrough,
147 Verified { capability_contract: &'static str },
149 Unavailable {
151 reason: VendorCapabilityUnavailableReason,
152 },
153}
154
155impl VendorCapabilityVerdict {
156 pub const fn is_admitted(self) -> bool {
157 !matches!(self, Self::Unavailable { .. })
158 }
159
160 pub const fn is_verified(self) -> bool {
161 matches!(self, Self::Verified { .. })
162 }
163
164 pub const fn unavailable_reason(self) -> Option<VendorCapabilityUnavailableReason> {
165 match self {
166 Self::Unavailable { reason } => Some(reason),
167 Self::RawPassthrough | Self::Verified { .. } => None,
168 }
169 }
170}
171
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
173pub struct VendorCapabilitySet {
174 pub raw_pty_spawn: VendorCapabilityVerdict,
175 pub terminal_screen: VendorCapabilityVerdict,
176 pub terminal_resize: VendorCapabilityVerdict,
177 pub terminal_interrupt: VendorCapabilityVerdict,
178 pub terminal_stop: VendorCapabilityVerdict,
179 pub semantic_readiness: VendorCapabilityVerdict,
180 pub provider_session_identity: VendorCapabilityVerdict,
181 pub structured_prompt: VendorCapabilityVerdict,
182 pub semantic_resume: VendorCapabilityVerdict,
183}
184
185impl VendorCapabilitySet {
186 pub const fn admits_raw_pty_lifecycle(self) -> bool {
187 self.raw_pty_spawn.is_admitted()
188 && self.terminal_screen.is_admitted()
189 && self.terminal_resize.is_admitted()
190 && self.terminal_interrupt.is_admitted()
191 && self.terminal_stop.is_admitted()
192 }
193}
194
195#[derive(Clone, Debug, Eq, PartialEq)]
196pub struct VendorContractResolution {
197 vendor: VendorCliFamily,
198 platform: VendorPlatform,
199 mode: VendorRuntimeMode,
200 version_status: VendorVersionStatus,
201 normalized_version: Option<String>,
202 contract_id: Option<&'static str>,
203 fallback_reason: Option<VendorFallbackReason>,
204 capabilities: VendorCapabilitySet,
205}
206
207impl VendorContractResolution {
208 pub const fn vendor(&self) -> VendorCliFamily {
209 self.vendor
210 }
211
212 pub const fn platform(&self) -> VendorPlatform {
213 self.platform
214 }
215
216 pub const fn mode(&self) -> VendorRuntimeMode {
217 self.mode
218 }
219
220 pub const fn version_status(&self) -> VendorVersionStatus {
221 self.version_status
222 }
223
224 pub fn normalized_version(&self) -> Option<&str> {
225 self.normalized_version.as_deref()
226 }
227
228 pub const fn contract_id(&self) -> Option<&'static str> {
229 self.contract_id
230 }
231
232 pub const fn fallback_reason(&self) -> Option<VendorFallbackReason> {
233 self.fallback_reason
234 }
235
236 pub const fn capabilities(&self) -> VendorCapabilitySet {
237 self.capabilities
238 }
239
240 pub const fn admits_raw_pty_lifecycle(&self) -> bool {
241 self.capabilities.admits_raw_pty_lifecycle()
242 }
243}
244
245#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
246pub struct VendorLauncherIdentity {
247 path_fingerprint: u64,
248 bytes: u64,
249 modified_unix_nanos: Option<u128>,
250 content_fingerprint: u64,
251}
252
253impl VendorLauncherIdentity {
254 pub const fn path_fingerprint(self) -> u64 {
255 self.path_fingerprint
256 }
257
258 pub const fn bytes(self) -> u64 {
259 self.bytes
260 }
261
262 pub const fn modified_unix_nanos(self) -> Option<u128> {
263 self.modified_unix_nanos
264 }
265
266 pub const fn content_fingerprint(self) -> u64 {
267 self.content_fingerprint
268 }
269}
270
271#[derive(Clone, Copy, Debug, Eq, PartialEq)]
272pub enum VendorVersionProbeStatus {
273 Resolved,
274 UnsupportedVendor,
275 InvalidLauncher,
276 LauncherUnavailable,
277 LauncherChanged,
278 DeadlineExceeded,
279 SpawnFailed,
280 NonzeroExit,
281 OutputOverflow,
282 MissingVersion,
283 AmbiguousVersion,
284 UnparseableVersion,
285}
286
287impl VendorVersionProbeStatus {
288 pub const fn is_resolved(self) -> bool {
289 matches!(self, Self::Resolved)
290 }
291}
292
293#[derive(Clone, Debug, Eq, PartialEq)]
294pub struct VendorVersionProbeResult {
295 status: VendorVersionProbeStatus,
296 resolution: VendorContractResolution,
297 launcher_identity: Option<VendorLauncherIdentity>,
298 cache_hit: bool,
299}
300
301impl VendorVersionProbeResult {
302 pub const fn status(&self) -> VendorVersionProbeStatus {
303 self.status
304 }
305
306 pub const fn resolution(&self) -> &VendorContractResolution {
307 &self.resolution
308 }
309
310 pub const fn launcher_identity(&self) -> Option<VendorLauncherIdentity> {
311 self.launcher_identity
312 }
313
314 pub const fn was_cached(&self) -> bool {
315 self.cache_hit
316 }
317}
318
319struct CachedVersionProbe {
320 vendor: VendorCliFamily,
321 platform: VendorPlatform,
322 launcher_identity: VendorLauncherIdentity,
323 status: VendorVersionProbeStatus,
324 resolution: VendorContractResolution,
325}
326
327pub struct VendorVersionProbeCache {
331 entries: VecDeque<CachedVersionProbe>,
332}
333
334impl Default for VendorVersionProbeCache {
335 fn default() -> Self {
336 Self {
337 entries: VecDeque::with_capacity(VERSION_PROBE_CACHE_CAPACITY),
338 }
339 }
340}
341
342impl VendorVersionProbeCache {
343 pub fn clear(&mut self) {
344 self.entries.clear();
345 }
346
347 pub fn len(&self) -> usize {
348 self.entries.len()
349 }
350
351 pub fn is_empty(&self) -> bool {
352 self.entries.is_empty()
353 }
354
355 pub fn probe(
356 &mut self,
357 agent_id: &str,
358 exact_launcher: &Path,
359 deadline: Instant,
360 ) -> VendorVersionProbeResult {
361 probe_installed_vendor_version(self, agent_id, exact_launcher, deadline)
362 }
363
364 fn lookup(
365 &mut self,
366 vendor: VendorCliFamily,
367 platform: VendorPlatform,
368 launcher_identity: VendorLauncherIdentity,
369 ) -> Option<VendorVersionProbeResult> {
370 let position = self.entries.iter().position(|entry| {
371 entry.vendor == vendor
372 && entry.platform == platform
373 && entry.launcher_identity == launcher_identity
374 })?;
375 let entry = self
376 .entries
377 .remove(position)
378 .expect("located vendor version cache entry must remain present");
379 let result = VendorVersionProbeResult {
380 status: entry.status,
381 resolution: entry.resolution.clone(),
382 launcher_identity: Some(entry.launcher_identity),
383 cache_hit: true,
384 };
385 self.entries.push_back(entry);
386 Some(result)
387 }
388
389 fn insert(
390 &mut self,
391 vendor: VendorCliFamily,
392 platform: VendorPlatform,
393 launcher_identity: VendorLauncherIdentity,
394 status: VendorVersionProbeStatus,
395 resolution: VendorContractResolution,
396 ) {
397 self.entries.retain(|entry| {
398 entry.vendor != vendor
399 || entry.platform != platform
400 || entry.launcher_identity.path_fingerprint
401 != launcher_identity.path_fingerprint
402 });
403 if self.entries.len() == VERSION_PROBE_CACHE_CAPACITY {
404 self.entries.pop_front();
405 }
406 self.entries.push_back(CachedVersionProbe {
407 vendor,
408 platform,
409 launcher_identity,
410 status,
411 resolution,
412 });
413 }
414}
415
416#[derive(Clone, Copy)]
417struct VerifiedProfile {
418 contract_id: &'static str,
419 vendor: VendorCliFamily,
420 platform: VendorPlatform,
421 version: VersionTriple,
422 semantic_readiness_contract: Option<&'static str>,
423 provider_session_identity_contract: Option<&'static str>,
424 structured_prompt_contract: Option<&'static str>,
425 semantic_resume_contract: Option<&'static str>,
426}
427
428const VERIFIED_PROFILES: &[VerifiedProfile] = &[
429 VerifiedProfile {
430 contract_id: CLAUDE_WINDOWS_X86_64_2_1_223_CONTRACT_ID,
431 vendor: VendorCliFamily::Claude,
432 platform: VendorPlatform::WindowsX86_64,
433 version: VersionTriple::new(2, 1, 223),
434 semantic_readiness_contract: Some("readiness.bracketed_paste"),
435 provider_session_identity_contract: None,
436 structured_prompt_contract: Some("pty.prompt_round_trip"),
437 semantic_resume_contract: None,
438 },
439 VerifiedProfile {
440 contract_id: CLAUDE_WINDOWS_X86_64_2_1_224_CONTRACT_ID,
441 vendor: VendorCliFamily::Claude,
442 platform: VendorPlatform::WindowsX86_64,
443 version: VersionTriple::new(2, 1, 224),
444 semantic_readiness_contract: Some("readiness.win32_input_focus_cursor"),
445 provider_session_identity_contract: None,
446 structured_prompt_contract: Some("pty.prompt_round_trip"),
447 semantic_resume_contract: None,
448 },
449];
450
451struct ResolvedLauncher {
452 path: PathBuf,
453 identity: VendorLauncherIdentity,
454}
455
456#[derive(Clone, Copy)]
457enum LauncherResolveError {
458 Invalid,
459 Unavailable,
460 DeadlineExceeded,
461}
462
463struct CapturedProbeOutput {
464 bytes: Vec<u8>,
465 overflowed: bool,
466}
467
468enum VersionChildOutcome {
469 Completed {
470 success: bool,
471 stdout: CapturedProbeOutput,
472 stderr: CapturedProbeOutput,
473 },
474 DeadlineExceeded,
475 SpawnFailed,
476}
477
478pub fn probe_installed_vendor_version(
486 cache: &mut VendorVersionProbeCache,
487 agent_id: &str,
488 exact_launcher: &Path,
489 deadline: Instant,
490) -> VendorVersionProbeResult {
491 let vendor = VendorCliFamily::from_agent_id(agent_id);
492 let platform = VendorPlatform::current();
493 if vendor == VendorCliFamily::Unknown {
494 return unavailable_probe_result(
495 vendor,
496 platform,
497 VendorVersionProbeStatus::UnsupportedVendor,
498 VendorFallbackReason::UnsupportedVendor,
499 None,
500 );
501 }
502 if Instant::now() >= deadline {
503 return unavailable_probe_result(
504 vendor,
505 platform,
506 VendorVersionProbeStatus::DeadlineExceeded,
507 VendorFallbackReason::ProbeDeadlineExceeded,
508 None,
509 );
510 }
511 let launcher = match resolve_exact_launcher(exact_launcher, deadline) {
512 Ok(launcher) => launcher,
513 Err(LauncherResolveError::Invalid) => {
514 return unavailable_probe_result(
515 vendor,
516 platform,
517 VendorVersionProbeStatus::InvalidLauncher,
518 VendorFallbackReason::InvalidLauncher,
519 None,
520 );
521 }
522 Err(LauncherResolveError::Unavailable) => {
523 return unavailable_probe_result(
524 vendor,
525 platform,
526 VendorVersionProbeStatus::LauncherUnavailable,
527 VendorFallbackReason::LauncherUnavailable,
528 None,
529 );
530 }
531 Err(LauncherResolveError::DeadlineExceeded) => {
532 return unavailable_probe_result(
533 vendor,
534 platform,
535 VendorVersionProbeStatus::DeadlineExceeded,
536 VendorFallbackReason::ProbeDeadlineExceeded,
537 None,
538 );
539 }
540 };
541 if let Some(cached) = cache.lookup(vendor, platform, launcher.identity) {
542 return cached;
543 }
544 let Some(command) = provider_version_command(vendor, &launcher.path) else {
545 return unavailable_probe_result(
546 vendor,
547 platform,
548 VendorVersionProbeStatus::InvalidLauncher,
549 VendorFallbackReason::InvalidLauncher,
550 Some(launcher.identity),
551 );
552 };
553 let (stdout, stderr) = match run_version_child(command, deadline) {
554 VersionChildOutcome::Completed {
555 success: false, ..
556 } => {
557 return unavailable_probe_result(
558 vendor,
559 platform,
560 VendorVersionProbeStatus::NonzeroExit,
561 VendorFallbackReason::ProbeNonzero,
562 Some(launcher.identity),
563 );
564 }
565 VersionChildOutcome::Completed {
566 success: true,
567 stdout,
568 stderr,
569 } => (stdout, stderr),
570 VersionChildOutcome::DeadlineExceeded => {
571 return unavailable_probe_result(
572 vendor,
573 platform,
574 VendorVersionProbeStatus::DeadlineExceeded,
575 VendorFallbackReason::ProbeDeadlineExceeded,
576 Some(launcher.identity),
577 );
578 }
579 VersionChildOutcome::SpawnFailed => {
580 return unavailable_probe_result(
581 vendor,
582 platform,
583 VendorVersionProbeStatus::SpawnFailed,
584 VendorFallbackReason::ProbeSpawnFailed,
585 Some(launcher.identity),
586 );
587 }
588 };
589 let combined_output_bytes = stdout
590 .bytes
591 .len()
592 .checked_add(stderr.bytes.len())
593 .and_then(|bytes| bytes.checked_add(1));
594 if stdout.overflowed
595 || stderr.overflowed
596 || combined_output_bytes.is_none()
597 || combined_output_bytes.is_some_and(|bytes| bytes > MAX_VERSION_OUTPUT_BYTES)
598 {
599 return unavailable_probe_result(
600 vendor,
601 platform,
602 VendorVersionProbeStatus::OutputOverflow,
603 VendorFallbackReason::ProbeOutputOverflow,
604 Some(launcher.identity),
605 );
606 }
607 let after = match resolve_exact_launcher(exact_launcher, deadline) {
608 Ok(after) => after,
609 Err(LauncherResolveError::DeadlineExceeded) => {
610 return unavailable_probe_result(
611 vendor,
612 platform,
613 VendorVersionProbeStatus::DeadlineExceeded,
614 VendorFallbackReason::ProbeDeadlineExceeded,
615 Some(launcher.identity),
616 );
617 }
618 Err(LauncherResolveError::Invalid | LauncherResolveError::Unavailable) => {
619 return unavailable_probe_result(
620 vendor,
621 platform,
622 VendorVersionProbeStatus::LauncherChanged,
623 VendorFallbackReason::LauncherChanged,
624 Some(launcher.identity),
625 );
626 }
627 };
628 if after.identity != launcher.identity {
629 return unavailable_probe_result(
630 vendor,
631 platform,
632 VendorVersionProbeStatus::LauncherChanged,
633 VendorFallbackReason::LauncherChanged,
634 Some(after.identity),
635 );
636 }
637 let resolution = resolve_vendor_contract(agent_id, platform, &stdout.bytes, &stderr.bytes);
638 let status = probe_status_for_resolution(&resolution);
639 cache.insert(
640 vendor,
641 platform,
642 launcher.identity,
643 status,
644 resolution.clone(),
645 );
646 VendorVersionProbeResult {
647 status,
648 resolution,
649 launcher_identity: Some(launcher.identity),
650 cache_hit: false,
651 }
652}
653
654fn unavailable_probe_result(
655 vendor: VendorCliFamily,
656 platform: VendorPlatform,
657 status: VendorVersionProbeStatus,
658 fallback_reason: VendorFallbackReason,
659 launcher_identity: Option<VendorLauncherIdentity>,
660) -> VendorVersionProbeResult {
661 VendorVersionProbeResult {
662 status,
663 resolution: raw_resolution(
664 vendor,
665 platform,
666 VendorVersionStatus::Unavailable,
667 None,
668 fallback_reason,
669 ),
670 launcher_identity,
671 cache_hit: false,
672 }
673}
674
675fn probe_status_for_resolution(
676 resolution: &VendorContractResolution,
677) -> VendorVersionProbeStatus {
678 match resolution.version_status() {
679 VendorVersionStatus::Normalized => VendorVersionProbeStatus::Resolved,
680 VendorVersionStatus::Missing => VendorVersionProbeStatus::MissingVersion,
681 VendorVersionStatus::Ambiguous => VendorVersionProbeStatus::AmbiguousVersion,
682 VendorVersionStatus::Unparseable => VendorVersionProbeStatus::UnparseableVersion,
683 VendorVersionStatus::Unavailable => VendorVersionProbeStatus::LauncherUnavailable,
684 }
685}
686
687fn resolve_exact_launcher(
688 exact_launcher: &Path,
689 deadline: Instant,
690) -> Result<ResolvedLauncher, LauncherResolveError> {
691 if !exact_launcher.is_absolute() || Instant::now() >= deadline {
692 return if Instant::now() >= deadline {
693 Err(LauncherResolveError::DeadlineExceeded)
694 } else {
695 Err(LauncherResolveError::Invalid)
696 };
697 }
698 let canonical_path = exact_launcher
699 .canonicalize()
700 .map_err(|_| LauncherResolveError::Unavailable)?;
701 let mut launcher = OpenOptions::new()
702 .read(true)
703 .open(&canonical_path)
704 .map_err(|_| LauncherResolveError::Unavailable)?;
705 let before = launcher
706 .metadata()
707 .map_err(|_| LauncherResolveError::Unavailable)?;
708 if !before.is_file() || before.len() > MAX_LAUNCHER_BYTES {
709 return Err(LauncherResolveError::Invalid);
710 }
711 #[cfg(unix)]
712 {
713 use std::os::unix::fs::PermissionsExt;
714 if before.permissions().mode() & 0o111 == 0 {
715 return Err(LauncherResolveError::Invalid);
716 }
717 }
718 let modified = before.modified().ok();
719 let mut content_fingerprint = fnv_offset();
720 let mut bytes = 0_u64;
721 let mut buffer = [0_u8; 16 * 1024];
722 loop {
723 if Instant::now() >= deadline {
724 return Err(LauncherResolveError::DeadlineExceeded);
725 }
726 let read = launcher
727 .read(&mut buffer)
728 .map_err(|_| LauncherResolveError::Unavailable)?;
729 if read == 0 {
730 break;
731 }
732 bytes = bytes
733 .checked_add(read as u64)
734 .ok_or(LauncherResolveError::Invalid)?;
735 if bytes > MAX_LAUNCHER_BYTES {
736 return Err(LauncherResolveError::Invalid);
737 }
738 content_fingerprint = fnv_bytes(content_fingerprint, &buffer[..read]);
739 }
740 let after = launcher
741 .metadata()
742 .map_err(|_| LauncherResolveError::Unavailable)?;
743 if before.len() != after.len()
744 || bytes != before.len()
745 || modified != after.modified().ok()
746 {
747 return Err(LauncherResolveError::Unavailable);
748 }
749 let modified_unix_nanos = modified
750 .and_then(|value| value.duration_since(UNIX_EPOCH).ok())
751 .map(|value| value.as_nanos());
752 Ok(ResolvedLauncher {
753 identity: VendorLauncherIdentity {
754 path_fingerprint: fingerprint_path(&canonical_path),
755 bytes,
756 modified_unix_nanos,
757 content_fingerprint,
758 },
759 path: canonical_path,
760 })
761}
762
763fn provider_version_command(vendor: VendorCliFamily, launcher: &Path) -> Option<Command> {
764 let argv = match vendor {
765 VendorCliFamily::Claude => CLAUDE_VERSION_ARGV,
766 VendorCliFamily::Codex => CODEX_VERSION_ARGV,
767 VendorCliFamily::Kimi => KIMI_VERSION_ARGV,
768 VendorCliFamily::Unknown => return None,
769 };
770 #[cfg(windows)]
771 let mut command = {
772 let extension = launcher
773 .extension()
774 .and_then(|value| value.to_str())
775 .map(str::to_ascii_lowercase)?;
776 if extension != "exe" {
777 return None;
778 }
779 let mut command = Command::new(launcher);
780 command.args(argv);
781 command.env_clear();
782 command
783 };
784 #[cfg(not(windows))]
785 let mut command = {
786 let mut command = Command::new(launcher);
787 command.args(argv);
788 command.env_clear();
789 command
790 };
791 command
792 .stdin(Stdio::null())
793 .stdout(Stdio::piped())
794 .stderr(Stdio::piped());
795 #[cfg(windows)]
796 {
797 use std::os::windows::process::CommandExt;
798 const CREATE_NO_WINDOW: u32 = 0x0800_0000;
799 command.creation_flags(CREATE_NO_WINDOW);
800 }
801 Some(command)
802}
803
804fn run_version_child(mut command: Command, deadline: Instant) -> VersionChildOutcome {
805 if Instant::now() >= deadline {
806 return VersionChildOutcome::DeadlineExceeded;
807 }
808 let Ok(mut child) = command.spawn() else {
809 return VersionChildOutcome::SpawnFailed;
810 };
811 let stdout = child.stdout.take().map(spawn_output_reader);
812 let stderr = child.stderr.take().map(spawn_output_reader);
813 let execution_deadline = execution_deadline(deadline);
814 let status = loop {
815 match child.try_wait() {
816 Ok(Some(status)) => break status,
817 Ok(None) if Instant::now() < execution_deadline => {
818 sleep_until_poll(execution_deadline);
819 }
820 Ok(None) => {
821 terminate_child_before(&mut child, deadline);
822 return VersionChildOutcome::DeadlineExceeded;
823 }
824 Err(_) => {
825 terminate_child_before(&mut child, deadline);
826 return VersionChildOutcome::SpawnFailed;
827 }
828 }
829 };
830 let Some(stdout) = finish_output_reader(stdout, deadline) else {
831 return VersionChildOutcome::DeadlineExceeded;
832 };
833 let Some(stderr) = finish_output_reader(stderr, deadline) else {
834 return VersionChildOutcome::DeadlineExceeded;
835 };
836 VersionChildOutcome::Completed {
837 success: status.success(),
838 stdout,
839 stderr,
840 }
841}
842
843fn execution_deadline(deadline: Instant) -> Instant {
844 let now = Instant::now();
845 let remaining = deadline.saturating_duration_since(now);
846 let reserve = (remaining / 4).min(VERSION_PROBE_MAX_CLEANUP_RESERVE);
847 deadline.checked_sub(reserve).unwrap_or(now)
848}
849
850fn sleep_until_poll(deadline: Instant) {
851 let remaining = deadline.saturating_duration_since(Instant::now());
852 if !remaining.is_zero() {
853 thread::sleep(remaining.min(VERSION_PROBE_POLL_INTERVAL));
854 }
855}
856
857fn terminate_child_before(child: &mut Child, deadline: Instant) {
858 let _ = child.kill();
859 loop {
860 match child.try_wait() {
861 Ok(Some(_)) | Err(_) => return,
862 Ok(None) if Instant::now() < deadline => sleep_until_poll(deadline),
863 Ok(None) => return,
864 }
865 }
866}
867
868fn spawn_output_reader(reader: impl Read + Send + 'static) -> thread::JoinHandle<CapturedProbeOutput> {
869 thread::spawn(move || read_capped_output(reader))
870}
871
872fn read_capped_output(mut reader: impl Read) -> CapturedProbeOutput {
873 let mut bytes = Vec::with_capacity(MAX_VERSION_OUTPUT_BYTES.min(4 * 1024));
874 let mut overflowed = false;
875 let mut buffer = [0_u8; 4 * 1024];
876 loop {
877 let Ok(read) = reader.read(&mut buffer) else {
878 overflowed = true;
879 break;
880 };
881 if read == 0 {
882 break;
883 }
884 let remaining = MAX_VERSION_OUTPUT_BYTES.saturating_sub(bytes.len());
885 let retained = remaining.min(read);
886 bytes.extend_from_slice(&buffer[..retained]);
887 overflowed |= retained != read;
888 }
889 CapturedProbeOutput { bytes, overflowed }
890}
891
892fn finish_output_reader(
893 reader: Option<thread::JoinHandle<CapturedProbeOutput>>,
894 deadline: Instant,
895) -> Option<CapturedProbeOutput> {
896 let Some(reader) = reader else {
897 return Some(CapturedProbeOutput {
898 bytes: Vec::new(),
899 overflowed: false,
900 });
901 };
902 while !reader.is_finished() && Instant::now() < deadline {
903 sleep_until_poll(deadline);
904 }
905 if !reader.is_finished() {
906 return None;
907 }
908 reader.join().ok()
909}
910
911const fn fnv_offset() -> u64 {
912 0xcbf2_9ce4_8422_2325
913}
914
915fn fnv_bytes(mut value: u64, bytes: &[u8]) -> u64 {
916 for byte in bytes {
917 value ^= u64::from(*byte);
918 value = value.wrapping_mul(0x0000_0100_0000_01b3);
919 }
920 value
921}
922
923#[cfg(unix)]
924fn fingerprint_path(path: &Path) -> u64 {
925 use std::os::unix::ffi::OsStrExt;
926 fnv_bytes(fnv_offset(), path.as_os_str().as_bytes())
927}
928
929#[cfg(windows)]
930fn fingerprint_path(path: &Path) -> u64 {
931 use std::os::windows::ffi::OsStrExt;
932 path.as_os_str()
933 .encode_wide()
934 .fold(fnv_offset(), |value, word| {
935 fnv_bytes(value, &word.to_le_bytes())
936 })
937}
938
939#[cfg(not(any(unix, windows)))]
940fn fingerprint_path(path: &Path) -> u64 {
941 fnv_bytes(fnv_offset(), path.to_string_lossy().as_bytes())
942}
943
944pub fn resolve_vendor_contract(
947 agent_id: &str,
948 platform: VendorPlatform,
949 stdout: &[u8],
950 stderr: &[u8],
951) -> VendorContractResolution {
952 let vendor = VendorCliFamily::from_agent_id(agent_id);
953 let version = normalize_version(stdout, stderr);
954 let version_status = version.status();
955 let normalized_version = version.normalized();
956
957 if let ParsedVersion::Normalized { triple, .. } = &version {
958 if let Some(profile) = exact_profile(vendor, platform, *triple) {
959 return verified_resolution(
960 vendor,
961 platform,
962 version_status,
963 normalized_version,
964 profile,
965 );
966 }
967 }
968
969 let fallback_reason = match version {
970 ParsedVersion::Missing => VendorFallbackReason::MissingVersion,
971 ParsedVersion::Ambiguous => VendorFallbackReason::AmbiguousVersion,
972 ParsedVersion::Unparseable => VendorFallbackReason::UnparseableVersion,
973 ParsedVersion::Normalized { triple, .. } => {
974 unverified_version_reason(vendor, platform, triple)
975 }
976 };
977 raw_resolution(
978 vendor,
979 platform,
980 version_status,
981 normalized_version,
982 fallback_reason,
983 )
984}
985
986fn verified_resolution(
987 vendor: VendorCliFamily,
988 platform: VendorPlatform,
989 version_status: VendorVersionStatus,
990 normalized_version: Option<String>,
991 profile: &VerifiedProfile,
992) -> VendorContractResolution {
993 VendorContractResolution {
994 vendor,
995 platform,
996 mode: VendorRuntimeMode::VerifiedSemantic,
997 version_status,
998 normalized_version,
999 contract_id: Some(profile.contract_id),
1000 fallback_reason: None,
1001 capabilities: VendorCapabilitySet {
1002 raw_pty_spawn: RAW_PASSTHROUGH,
1003 terminal_screen: RAW_PASSTHROUGH,
1004 terminal_resize: RAW_PASSTHROUGH,
1005 terminal_interrupt: RAW_PASSTHROUGH,
1006 terminal_stop: RAW_PASSTHROUGH,
1007 semantic_readiness: verified_or_unavailable(
1008 profile.semantic_readiness_contract,
1009 ),
1010 provider_session_identity: verified_or_unavailable(
1011 profile.provider_session_identity_contract,
1012 ),
1013 structured_prompt: verified_or_unavailable(profile.structured_prompt_contract),
1014 semantic_resume: verified_or_unavailable(profile.semantic_resume_contract),
1015 },
1016 }
1017}
1018
1019fn raw_resolution(
1020 vendor: VendorCliFamily,
1021 platform: VendorPlatform,
1022 version_status: VendorVersionStatus,
1023 normalized_version: Option<String>,
1024 fallback_reason: VendorFallbackReason,
1025) -> VendorContractResolution {
1026 let semantic_unavailable = VendorCapabilityVerdict::Unavailable {
1027 reason: VendorCapabilityUnavailableReason::NoVerifiedContract(fallback_reason),
1028 };
1029 VendorContractResolution {
1030 vendor,
1031 platform,
1032 mode: VendorRuntimeMode::RawPassthrough,
1033 version_status,
1034 normalized_version,
1035 contract_id: None,
1036 fallback_reason: Some(fallback_reason),
1037 capabilities: VendorCapabilitySet {
1038 raw_pty_spawn: RAW_PASSTHROUGH,
1039 terminal_screen: RAW_PASSTHROUGH,
1040 terminal_resize: RAW_PASSTHROUGH,
1041 terminal_interrupt: RAW_PASSTHROUGH,
1042 terminal_stop: RAW_PASSTHROUGH,
1043 semantic_readiness: semantic_unavailable,
1044 provider_session_identity: semantic_unavailable,
1045 structured_prompt: semantic_unavailable,
1046 semantic_resume: semantic_unavailable,
1047 },
1048 }
1049}
1050
1051const fn verified_or_unavailable(
1052 capability_contract: Option<&'static str>,
1053) -> VendorCapabilityVerdict {
1054 match capability_contract {
1055 Some(capability_contract) => VendorCapabilityVerdict::Verified {
1056 capability_contract,
1057 },
1058 None => VendorCapabilityVerdict::Unavailable {
1059 reason: VendorCapabilityUnavailableReason::NotVerifiedByContract,
1060 },
1061 }
1062}
1063
1064fn exact_profile(
1065 vendor: VendorCliFamily,
1066 platform: VendorPlatform,
1067 version: VersionTriple,
1068) -> Option<&'static VerifiedProfile> {
1069 VERIFIED_PROFILES.iter().find(|profile| {
1070 profile.vendor == vendor && profile.platform == platform && profile.version == version
1071 })
1072}
1073
1074fn unverified_version_reason(
1075 vendor: VendorCliFamily,
1076 platform: VendorPlatform,
1077 version: VersionTriple,
1078) -> VendorFallbackReason {
1079 if vendor == VendorCliFamily::Unknown {
1080 return VendorFallbackReason::UnsupportedVendor;
1081 }
1082 if !VERIFIED_PROFILES.iter().any(|profile| profile.vendor == vendor) {
1083 return VendorFallbackReason::NoVerifiedProfile;
1084 }
1085 let platform_versions = VERIFIED_PROFILES
1086 .iter()
1087 .filter(|profile| profile.vendor == vendor && profile.platform == platform)
1088 .map(|profile| profile.version)
1089 .collect::<Vec<_>>();
1090 let Some(oldest) = platform_versions.iter().min() else {
1091 return VendorFallbackReason::UnsupportedPlatform;
1092 };
1093 let newest = platform_versions
1094 .iter()
1095 .max()
1096 .expect("a verified platform version must have a newest member");
1097 if version < *oldest {
1098 VendorFallbackReason::LegacyVersion
1099 } else if version > *newest {
1100 VendorFallbackReason::FutureVersion
1101 } else {
1102 VendorFallbackReason::UnlistedVersion
1103 }
1104}
1105
1106#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
1107struct VersionTriple {
1108 major: u64,
1109 minor: u64,
1110 patch: u64,
1111}
1112
1113impl VersionTriple {
1114 const fn new(major: u64, minor: u64, patch: u64) -> Self {
1115 Self {
1116 major,
1117 minor,
1118 patch,
1119 }
1120 }
1121
1122 fn normalized(self) -> String {
1123 format!("{}.{}.{}", self.major, self.minor, self.patch)
1124 }
1125}
1126
1127enum ParsedVersion {
1128 Normalized {
1129 triple: VersionTriple,
1130 normalized: String,
1131 },
1132 Missing,
1133 Ambiguous,
1134 Unparseable,
1135}
1136
1137impl ParsedVersion {
1138 const fn status(&self) -> VendorVersionStatus {
1139 match self {
1140 Self::Normalized { .. } => VendorVersionStatus::Normalized,
1141 Self::Missing => VendorVersionStatus::Missing,
1142 Self::Ambiguous => VendorVersionStatus::Ambiguous,
1143 Self::Unparseable => VendorVersionStatus::Unparseable,
1144 }
1145 }
1146
1147 fn normalized(&self) -> Option<String> {
1148 match self {
1149 Self::Normalized { normalized, .. } => Some(normalized.clone()),
1150 Self::Missing | Self::Ambiguous | Self::Unparseable => None,
1151 }
1152 }
1153}
1154
1155fn normalize_version(stdout: &[u8], stderr: &[u8]) -> ParsedVersion {
1156 let Some(total_bytes) = stdout
1157 .len()
1158 .checked_add(stderr.len())
1159 .and_then(|bytes| bytes.checked_add(1))
1160 else {
1161 return ParsedVersion::Unparseable;
1162 };
1163 if total_bytes > MAX_VERSION_OUTPUT_BYTES {
1164 return ParsedVersion::Unparseable;
1165 }
1166 if stdout.iter().chain(stderr).all(u8::is_ascii_whitespace) {
1167 return ParsedVersion::Missing;
1168 }
1169 if !stdout.is_ascii() || !stderr.is_ascii() {
1170 return ParsedVersion::Unparseable;
1171 }
1172 let mut output = Vec::with_capacity(total_bytes);
1173 output.extend_from_slice(stdout);
1174 output.push(b'\n');
1175 output.extend_from_slice(stderr);
1176 let Some(candidates) = version_candidates(&output) else {
1177 return ParsedVersion::Unparseable;
1178 };
1179 match candidates.len() {
1180 0 => ParsedVersion::Unparseable,
1181 1 => {
1182 let triple = *candidates
1183 .first()
1184 .expect("single version candidate must be present");
1185 ParsedVersion::Normalized {
1186 normalized: triple.normalized(),
1187 triple,
1188 }
1189 }
1190 _ => ParsedVersion::Ambiguous,
1191 }
1192}
1193
1194fn version_candidates(bytes: &[u8]) -> Option<BTreeSet<VersionTriple>> {
1195 let mut candidates = BTreeSet::new();
1196 let mut index = 0;
1197 while index < bytes.len() {
1198 if !bytes[index].is_ascii_digit() || !valid_start_boundary(bytes, index) {
1199 index += 1;
1200 continue;
1201 }
1202 let start = index;
1203 let Some(major_end) = decimal_end(bytes, start) else {
1204 index += 1;
1205 continue;
1206 };
1207 if major_end >= bytes.len() || bytes[major_end] != b'.' {
1208 index = major_end;
1209 continue;
1210 }
1211 let minor_start = major_end + 1;
1212 let Some(minor_end) = decimal_end(bytes, minor_start) else {
1213 index = minor_start;
1214 continue;
1215 };
1216 if minor_end >= bytes.len() || bytes[minor_end] != b'.' {
1217 index = minor_end;
1218 continue;
1219 }
1220 let patch_start = minor_end + 1;
1221 let Some(end) = decimal_end(bytes, patch_start) else {
1222 index = patch_start;
1223 continue;
1224 };
1225 if !valid_end_boundary(bytes, end)
1226 || !valid_decimal(&bytes[start..major_end])
1227 || !valid_decimal(&bytes[minor_start..minor_end])
1228 || !valid_decimal(&bytes[patch_start..end])
1229 {
1230 return None;
1231 }
1232 let major = parse_decimal(&bytes[start..major_end])?;
1233 let minor = parse_decimal(&bytes[minor_start..minor_end])?;
1234 let patch = parse_decimal(&bytes[patch_start..end])?;
1235 candidates.insert(VersionTriple::new(major, minor, patch));
1236 index = end.max(index + 1);
1237 }
1238 Some(candidates)
1239}
1240
1241fn valid_start_boundary(bytes: &[u8], index: usize) -> bool {
1242 if index == 0 {
1243 return true;
1244 }
1245 let previous = bytes[index - 1];
1246 if matches!(previous, b'v' | b'V') {
1247 return index == 1 || is_version_boundary(bytes[index - 2]);
1248 }
1249 is_version_boundary(previous)
1250}
1251
1252fn valid_end_boundary(bytes: &[u8], end: usize) -> bool {
1253 end == bytes.len() || is_version_boundary(bytes[end])
1254}
1255
1256fn is_version_boundary(byte: u8) -> bool {
1257 !byte.is_ascii_alphanumeric() && !matches!(byte, b'.' | b'-' | b'+' | b'_')
1258}
1259
1260fn decimal_end(bytes: &[u8], start: usize) -> Option<usize> {
1261 if start >= bytes.len() || !bytes[start].is_ascii_digit() {
1262 return None;
1263 }
1264 let mut end = start + 1;
1265 while end < bytes.len() && bytes[end].is_ascii_digit() {
1266 end += 1;
1267 }
1268 Some(end)
1269}
1270
1271fn valid_decimal(bytes: &[u8]) -> bool {
1272 bytes.len() == 1 || bytes.first() != Some(&b'0')
1273}
1274
1275fn parse_decimal(bytes: &[u8]) -> Option<u64> {
1276 bytes.iter().try_fold(0_u64, |value, byte| {
1277 value
1278 .checked_mul(10)?
1279 .checked_add(u64::from(byte.checked_sub(b'0')?))
1280 })
1281}