Skip to main content

agentics_config/
runtime_modes.rs

1//! Typed runtime mode and runner namespace configuration values.
2
3use std::str::FromStr;
4
5use agentics_domain::models::challenge::TargetAccelerator;
6use serde::{Deserialize, Deserializer};
7
8use crate::ENV_AGENTICS_RUNNER_NAMESPACE;
9
10/// Runner strategy for Docker bind-mounted writable paths.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum RunnerWritableStorageMode {
13    /// Keep writable paths under `AGENTICS_STORAGE_ROOT`.
14    Unbounded,
15    /// Lease root-prepared XFS project-quota slots for writable container paths.
16    XfsProjectQuotaSlots,
17}
18
19/// Policy for unauthenticated agent-account registration.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum AgentRegistrationMode {
22    /// Require a valid pioneer code for every new agent account.
23    PioneerCode,
24    /// Allow code-free registration for local testing and development only.
25    Public,
26}
27
28/// Worker startup host-profile probe policy.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum HostProbeMode {
32    /// Do not run hosted profile checks.
33    Off,
34    /// Run hosted profile checks and log failures without blocking startup.
35    Warn,
36    /// Run hosted profile checks and fail worker startup if they fail or are skipped.
37    Require,
38}
39
40/// Worker runner safety profile.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum RunnerSecurityProfile {
44    /// Local development and test profile. Host isolation checks are opt-in.
45    Development,
46    /// Production profile. Runner storage, Docker layers, and host probes fail closed.
47    Production,
48}
49
50/// Policy for official-evaluation runner log redaction.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum OfficialLogRedactionMode {
53    /// Redact official logs only when the challenge contract may expose private material.
54    ContractBased,
55    /// Redact every official evaluation regardless of challenge contract.
56    Always,
57}
58
59/// Logical owner namespace for Docker runner containers on a shared daemon.
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61pub struct RunnerNamespace(String);
62
63/// Worker accelerator capability advertised to the scheduler.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum WorkerAccelerators {
67    /// Worker accepts only jobs that require no accelerator.
68    None,
69    /// Worker accepts no-accelerator jobs and GPU jobs.
70    Gpu,
71}
72
73impl HostProbeMode {
74    /// Stable environment string for this policy.
75    pub fn as_str(self) -> &'static str {
76        match self {
77            Self::Off => "off",
78            Self::Warn => "warn",
79            Self::Require => "require",
80        }
81    }
82}
83
84impl FromStr for HostProbeMode {
85    type Err = anyhow::Error;
86
87    /// Parse the configured host-probe mode.
88    fn from_str(value: &str) -> anyhow::Result<Self> {
89        match value.trim() {
90            "off" => Ok(Self::Off),
91            "warn" => Ok(Self::Warn),
92            "require" => Ok(Self::Require),
93            other => anyhow::bail!(
94                "AGENTICS_HOST_PROBE_MODE must be `off`, `warn`, or `require`, got `{other}`"
95            ),
96        }
97    }
98}
99
100impl RunnerSecurityProfile {
101    /// Stable environment string for this policy.
102    pub fn as_str(self) -> &'static str {
103        match self {
104            Self::Development => "development",
105            Self::Production => "production",
106        }
107    }
108}
109
110impl FromStr for RunnerSecurityProfile {
111    type Err = anyhow::Error;
112
113    /// Parse the configured runner security profile.
114    fn from_str(value: &str) -> anyhow::Result<Self> {
115        match value.trim() {
116            "development" => Ok(Self::Development),
117            "production" => Ok(Self::Production),
118            other => anyhow::bail!(
119                "AGENTICS_RUNNER_SECURITY_PROFILE must be `development` or `production`, got `{other}`"
120            ),
121        }
122    }
123}
124
125impl OfficialLogRedactionMode {
126    /// Stable environment string for this official-log redaction policy.
127    pub fn as_str(self) -> &'static str {
128        match self {
129            Self::ContractBased => "contract_based",
130            Self::Always => "always",
131        }
132    }
133}
134
135impl FromStr for OfficialLogRedactionMode {
136    type Err = anyhow::Error;
137
138    /// Parse the configured official-log redaction mode.
139    fn from_str(value: &str) -> anyhow::Result<Self> {
140        match value.trim() {
141            "contract_based" => Ok(Self::ContractBased),
142            "always" => Ok(Self::Always),
143            other => anyhow::bail!(
144                "AGENTICS_OFFICIAL_LOG_REDACTION must be `contract_based` or `always`, got `{other}`"
145            ),
146        }
147    }
148}
149
150impl<'de> Deserialize<'de> for OfficialLogRedactionMode {
151    /// Deserialize one official-log redaction mode through the canonical parser.
152    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
153    where
154        D: Deserializer<'de>,
155    {
156        let value = String::deserialize(deserializer)?;
157        Self::from_str(&value).map_err(serde::de::Error::custom)
158    }
159}
160
161impl WorkerAccelerators {
162    /// Stable environment string for this capability set.
163    pub fn as_str(self) -> &'static str {
164        match self {
165            Self::None => "none",
166            Self::Gpu => "gpu",
167        }
168    }
169
170    /// Return whether this worker can claim a job requiring the given accelerator.
171    pub fn supports(self, accelerator: TargetAccelerator) -> bool {
172        match (self, accelerator) {
173            (_, TargetAccelerator::None) | (Self::Gpu, TargetAccelerator::Gpu) => true,
174            (Self::None, TargetAccelerator::Gpu) => false,
175        }
176    }
177
178    /// Return heartbeat-friendly accelerator capability labels.
179    pub fn heartbeat_values(self) -> Vec<String> {
180        match self {
181            Self::None => vec!["none".to_string()],
182            Self::Gpu => vec!["none".to_string(), "gpu".to_string()],
183        }
184    }
185}
186
187impl FromStr for WorkerAccelerators {
188    type Err = anyhow::Error;
189
190    /// Parse the configured worker accelerator capability.
191    fn from_str(value: &str) -> anyhow::Result<Self> {
192        match value.trim() {
193            "none" => Ok(Self::None),
194            "gpu" => Ok(Self::Gpu),
195            other => {
196                anyhow::bail!("AGENTICS_WORKER_ACCELERATORS must be `none` or `gpu`, got `{other}`")
197            }
198        }
199    }
200}
201
202impl AgentRegistrationMode {
203    /// Stable environment string for this registration policy.
204    pub fn as_str(self) -> &'static str {
205        match self {
206            Self::PioneerCode => "pioneer_code",
207            Self::Public => "public",
208        }
209    }
210}
211
212impl RunnerNamespace {
213    /// Parse and validate one Docker-runner namespace.
214    pub fn try_new(value: impl Into<String>) -> anyhow::Result<Self> {
215        let value = value.into();
216        let trimmed = value.trim();
217        if trimmed.is_empty() {
218            anyhow::bail!("{ENV_AGENTICS_RUNNER_NAMESPACE} must not be empty");
219        }
220        if trimmed.len() > 63 {
221            anyhow::bail!("{ENV_AGENTICS_RUNNER_NAMESPACE} must be at most 63 bytes");
222        }
223        if !trimmed
224            .bytes()
225            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
226        {
227            anyhow::bail!(
228                "{ENV_AGENTICS_RUNNER_NAMESPACE} may contain only ASCII letters, digits, '.', '_', and '-'"
229            );
230        }
231        Ok(Self(trimmed.to_string()))
232    }
233
234    /// Return the canonical namespace label value.
235    pub fn as_str(&self) -> &str {
236        &self.0
237    }
238}
239
240impl FromStr for RunnerNamespace {
241    type Err = anyhow::Error;
242
243    /// Parse a runner namespace from an environment/config boundary value.
244    fn from_str(value: &str) -> anyhow::Result<Self> {
245        Self::try_new(value)
246    }
247}
248
249impl<'de> Deserialize<'de> for RunnerNamespace {
250    /// Deserialize one runner namespace through the canonical parser.
251    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
252    where
253        D: Deserializer<'de>,
254    {
255        let value = String::deserialize(deserializer)?;
256        Self::from_str(&value).map_err(serde::de::Error::custom)
257    }
258}
259
260impl FromStr for AgentRegistrationMode {
261    type Err = anyhow::Error;
262
263    /// Parse the configured agent-registration mode.
264    fn from_str(value: &str) -> anyhow::Result<Self> {
265        match value.trim() {
266            "pioneer_code" => Ok(Self::PioneerCode),
267            "public" => Ok(Self::Public),
268            other => anyhow::bail!(
269                "AGENTICS_AGENT_REGISTRATION_MODE must be `pioneer_code` or `public`, got `{other}`"
270            ),
271        }
272    }
273}
274
275impl<'de> Deserialize<'de> for AgentRegistrationMode {
276    /// Deserialize one agent-registration mode through the canonical parser.
277    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
278    where
279        D: Deserializer<'de>,
280    {
281        let value = String::deserialize(deserializer)?;
282        Self::from_str(&value).map_err(serde::de::Error::custom)
283    }
284}
285
286impl RunnerWritableStorageMode {
287    /// Stable environment string for this runner writable-storage strategy.
288    pub fn as_str(self) -> &'static str {
289        match self {
290            Self::Unbounded => "unbounded",
291            Self::XfsProjectQuotaSlots => "xfs-project-quota-slots",
292        }
293    }
294}
295
296impl FromStr for RunnerWritableStorageMode {
297    type Err = anyhow::Error;
298
299    /// Handles from str for this module.
300    fn from_str(value: &str) -> anyhow::Result<Self> {
301        match value.trim() {
302            "unbounded" => Ok(Self::Unbounded),
303            "xfs-project-quota-slots" => Ok(Self::XfsProjectQuotaSlots),
304            other => anyhow::bail!(
305                "AGENTICS_RUNNER_WRITABLE_STORAGE_MODE must be `unbounded` or `xfs-project-quota-slots`, got `{other}`"
306            ),
307        }
308    }
309}
310
311impl<'de> Deserialize<'de> for RunnerWritableStorageMode {
312    /// Deserialize one runner writable-storage mode through the canonical parser.
313    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
314    where
315        D: Deserializer<'de>,
316    {
317        let value = String::deserialize(deserializer)?;
318        Self::from_str(&value).map_err(serde::de::Error::custom)
319    }
320}