Skip to main content

codex_ws/
runtime.rs

1use thiserror::Error;
2
3/// Environment variable used by the runtime entrypoint for apt packages.
4pub const CODEX_WS_APT_PACKAGES_ENV: &str = "CODEX_WS_APT_PACKAGES";
5
6/// Environment variable used by the runtime entrypoint for setup commands.
7pub const CODEX_WS_SETUP_COMMANDS_ENV: &str = "CODEX_WS_SETUP_COMMANDS";
8
9/// Docker environment variable generated from a workspace runtime config.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct RuntimeEnvironmentVariable {
12    name: &'static str,
13    value: String,
14}
15
16impl RuntimeEnvironmentVariable {
17    /// Create a runtime environment variable.
18    ///
19    /// # Arguments
20    ///
21    /// * `name` - Environment variable name recognized by the runtime entrypoint.
22    /// * `value` - Environment variable value passed to Docker.
23    ///
24    /// # Returns
25    ///
26    /// A runtime environment variable.
27    #[must_use]
28    pub fn new(name: &'static str, value: String) -> Self {
29        Self { name, value }
30    }
31
32    /// Return the environment variable name.
33    ///
34    /// # Returns
35    ///
36    /// The runtime entrypoint environment variable name.
37    #[must_use]
38    pub const fn name(&self) -> &'static str {
39        self.name
40    }
41
42    /// Return the environment variable value.
43    ///
44    /// # Returns
45    ///
46    /// The value passed to Docker.
47    #[must_use]
48    pub fn value(&self) -> &str {
49        &self.value
50    }
51
52    /// Return the `NAME=value` form accepted by `docker run -e`.
53    ///
54    /// # Returns
55    ///
56    /// A Docker environment assignment.
57    #[must_use]
58    pub fn docker_assignment(&self) -> String {
59        format!("{}={}", self.name, self.value)
60    }
61}
62
63/// Validate apt package names from a workspace manifest.
64///
65/// # Arguments
66///
67/// * `packages` - Package names requested by `runtime.apt`.
68///
69/// # Returns
70///
71/// Trimmed package names in input order.
72///
73/// # Errors
74///
75/// Returns [`RuntimeSpecError::EmptyAptPackage`] for a blank package or
76/// [`RuntimeSpecError::InvalidAptPackage`] when a package contains shell metacharacters.
77pub fn validate_apt_packages(packages: Vec<String>) -> Result<Vec<String>, RuntimeSpecError> {
78    let mut validated_packages = Vec::with_capacity(packages.len());
79    for package in packages {
80        let package = package.trim().to_owned();
81        if package.is_empty() {
82            return Err(RuntimeSpecError::EmptyAptPackage);
83        }
84        if !is_valid_apt_package(&package) {
85            return Err(RuntimeSpecError::InvalidAptPackage { package });
86        }
87        validated_packages.push(package);
88    }
89
90    Ok(validated_packages)
91}
92
93/// Validate setup commands from a workspace manifest.
94///
95/// # Arguments
96///
97/// * `commands` - Shell commands requested by `runtime.setup`.
98///
99/// # Returns
100///
101/// Trimmed commands in input order.
102///
103/// # Errors
104///
105/// Returns [`RuntimeSpecError::EmptySetupCommand`] when a setup command is blank.
106pub fn validate_setup_commands(commands: Vec<String>) -> Result<Vec<String>, RuntimeSpecError> {
107    let mut validated_commands = Vec::with_capacity(commands.len());
108    for command in commands {
109        let command = command.trim().to_owned();
110        if command.is_empty() {
111            return Err(RuntimeSpecError::EmptySetupCommand);
112        }
113        validated_commands.push(command);
114    }
115
116    Ok(validated_commands)
117}
118
119fn is_valid_apt_package(package: &str) -> bool {
120    package.bytes().all(|byte| {
121        byte.is_ascii_alphanumeric()
122            || matches!(byte, b'+' | b'-' | b'.' | b'_' | b':' | b'=' | b'~')
123    })
124}
125
126/// Errors returned while parsing workspace runtime setup.
127#[derive(Debug, Error, PartialEq, Eq)]
128pub enum RuntimeSpecError {
129    /// An apt package entry was empty or only whitespace.
130    #[error("runtime apt package cannot be empty")]
131    EmptyAptPackage,
132
133    /// An apt package entry contained characters that are unsafe for shell word splitting.
134    #[error("invalid runtime apt package '{package}'")]
135    InvalidAptPackage {
136        /// Invalid package entry.
137        package: String,
138    },
139
140    /// A setup command was empty or only whitespace.
141    #[error("runtime setup command cannot be empty")]
142    EmptySetupCommand,
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn validate_apt_packages_accepts_common_package_syntax() {
151        let packages = validate_apt_packages(vec![
152            " python3 ".to_owned(),
153            "libssl-dev:amd64".to_owned(),
154            "nodejs=22.0.0-1nodesource1".to_owned(),
155        ])
156        .expect("apt packages should validate");
157
158        assert_eq!(
159            packages,
160            vec![
161                "python3".to_owned(),
162                "libssl-dev:amd64".to_owned(),
163                "nodejs=22.0.0-1nodesource1".to_owned()
164            ]
165        );
166    }
167
168    #[test]
169    fn validate_apt_packages_rejects_shell_metacharacters() {
170        let error = validate_apt_packages(vec!["python3;curl".to_owned()])
171            .expect_err("shell metacharacters should fail");
172
173        assert!(matches!(
174            error,
175            RuntimeSpecError::InvalidAptPackage { package } if package == "python3;curl"
176        ));
177    }
178
179    #[test]
180    fn validate_setup_commands_rejects_empty_commands() {
181        let error =
182            validate_setup_commands(vec![" ".to_owned()]).expect_err("blank command should fail");
183
184        assert_eq!(error, RuntimeSpecError::EmptySetupCommand);
185    }
186}