Skip to main content

ito_common/
harness.rs

1//! Harness session environment detection shared across Ito crates.
2
3/// Ito harness identifiers inferred from session environment variables.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum HarnessKind {
6    /// Generic Ito harness session override.
7    Generic,
8    /// Claude Code harness.
9    ClaudeCode,
10    /// OpenCode harness.
11    OpenCode,
12    /// Codex harness.
13    Codex,
14    /// GitHub Copilot harness.
15    GitHubCopilot,
16    /// Pi coding agent harness.
17    Pi,
18}
19
20impl HarnessKind {
21    /// Return the canonical harness identifier used in generated Ito assets.
22    pub fn canonical_name(self) -> &'static str {
23        match self {
24            Self::Generic => "generic",
25            Self::ClaudeCode => "claude-code",
26            Self::OpenCode => "opencode",
27            Self::Codex => "codex",
28            Self::GitHubCopilot => "github-copilot",
29            Self::Pi => "pi",
30        }
31    }
32}
33
34/// Resolved harness session environment.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct HarnessSessionEnv {
37    /// Environment variable that supplied the session id.
38    pub env_var: &'static str,
39    /// Session id value from the environment.
40    pub session_id: String,
41    /// Harness kind inferred from the environment variable.
42    pub harness: HarnessKind,
43}
44
45#[derive(Debug, Clone, Copy)]
46struct HarnessSessionProbe {
47    env_var: &'static str,
48    harness: HarnessKind,
49}
50
51const HARNESS_SESSION_PROBES: &[HarnessSessionProbe] = &[
52    HarnessSessionProbe {
53        env_var: "ITO_HARNESS_SESSION_ID",
54        harness: HarnessKind::Generic,
55    },
56    HarnessSessionProbe {
57        env_var: "CLAUDE_SESSION_ID",
58        harness: HarnessKind::ClaudeCode,
59    },
60    HarnessSessionProbe {
61        env_var: "OPENCODE_SESSION_ID",
62        harness: HarnessKind::OpenCode,
63    },
64    HarnessSessionProbe {
65        env_var: "CODEX_SESSION_ID",
66        harness: HarnessKind::Codex,
67    },
68    HarnessSessionProbe {
69        env_var: "GITHUB_COPILOT_CHAT_SESSION_ID",
70        harness: HarnessKind::GitHubCopilot,
71    },
72    HarnessSessionProbe {
73        env_var: "PI_SESSION_ID",
74        harness: HarnessKind::Pi,
75    },
76];
77
78/// Environment variables that Ito checks for harness session context, in precedence order.
79pub const HARNESS_SESSION_ENV_VARS: &[&str] = &[
80    "ITO_HARNESS_SESSION_ID",
81    "CLAUDE_SESSION_ID",
82    "OPENCODE_SESSION_ID",
83    "CODEX_SESSION_ID",
84    "GITHUB_COPILOT_CHAT_SESSION_ID",
85    "PI_SESSION_ID",
86];
87
88/// Resolve harness session environment from the current process.
89pub fn resolve_harness_session_env() -> Option<HarnessSessionEnv> {
90    resolve_harness_session_env_with(|env_var| std::env::var(env_var).ok())
91}
92
93fn resolve_harness_session_env_with(
94    mut get_env: impl FnMut(&str) -> Option<String>,
95) -> Option<HarnessSessionEnv> {
96    for HarnessSessionProbe { env_var, harness } in HARNESS_SESSION_PROBES {
97        let Some(session_id) = get_env(env_var) else {
98            continue;
99        };
100        let session_id = session_id.trim();
101        if session_id.is_empty() {
102            continue;
103        }
104        let session_id = session_id.to_owned();
105        return Some(HarnessSessionEnv {
106            env_var,
107            session_id,
108            harness: *harness,
109        });
110    }
111
112    None
113}
114
115/// Resolve the first configured harness session id from the current process.
116pub fn resolve_harness_session_id() -> Option<String> {
117    resolve_harness_session_env().map(|env| env.session_id)
118}
119
120/// Detect the current harness canonical name from session environment variables.
121pub fn detect_harness_name() -> &'static str {
122    resolve_harness_session_env()
123        .map(|env| env.harness.canonical_name())
124        .unwrap_or("unknown")
125}
126
127#[cfg(test)]
128#[path = "harness_tests.rs"]
129mod harness_tests;