Skip to main content

cageforge_command/
request.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! The final command-intent value passed from builders to an execution
4//! adapter.
5//!
6//! [`crate::CommandRequest`] intentionally contains no sandbox policy value or
7//! process handle. Policy composition and native process ownership happen in
8//! adjacent layers.
9
10use std::path::{Path, PathBuf};
11use std::time::Duration;
12
13use crate::command::contains_nul;
14use crate::{CommandError, CommandSpec, EnvironmentSpec, StdioSpec, TimeoutPolicy};
15use cageforge_path::contains_parent_traversal;
16
17/// A complete portable request to execute one command.
18///
19/// This type describes execution intent only. It does not contain a sandbox
20/// policy because policy is a separate Cageforge concern and will be composed
21/// by the backend API. It also does not expose PTY handles, inherited file
22/// descriptors, process ids, or OS-specific user/token settings.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct CommandRequest {
25    command: CommandSpec,
26    working_directory: Option<PathBuf>,
27    environment: EnvironmentSpec,
28    stdio: StdioSpec,
29    timeout: TimeoutPolicy,
30}
31
32impl CommandRequest {
33    /// Creates a request with the captured-stdio and inherited-environment
34    /// defaults.
35    pub fn new(command: CommandSpec) -> Self {
36        Self {
37            command,
38            working_directory: None,
39            environment: EnvironmentSpec::default(),
40            stdio: StdioSpec::default(),
41            timeout: TimeoutPolicy::default(),
42        }
43    }
44
45    /// Sets the working directory.
46    ///
47    /// The path is kept in the caller's native representation. Relative paths
48    /// are resolved by the backend, but lexical parent traversal is rejected
49    /// here so a request cannot escape its later execution context by using
50    /// parent components.
51    pub fn with_working_directory(
52        mut self,
53        path: impl Into<PathBuf>,
54    ) -> Result<Self, CommandError> {
55        let path = path.into();
56        if path.as_os_str().is_empty() {
57            return Err(CommandError::EmptyWorkingDirectory);
58        }
59        if contains_nul(path.as_os_str()) {
60            return Err(CommandError::WorkingDirectoryContainsNul);
61        }
62        if contains_parent_traversal(&path) {
63            return Err(CommandError::WorkingDirectoryParentTraversal { path });
64        }
65        self.working_directory = Some(path);
66        Ok(self)
67    }
68
69    /// Removes an explicitly configured working directory.
70    pub fn without_working_directory(mut self) -> Self {
71        self.working_directory = None;
72        self
73    }
74
75    /// Replaces the environment construction rules.
76    pub fn with_environment(mut self, environment: EnvironmentSpec) -> Self {
77        self.environment = environment;
78        self
79    }
80
81    /// Replaces standard stream routing.
82    pub fn with_stdio(mut self, stdio: StdioSpec) -> Self {
83        self.stdio = stdio;
84        self
85    }
86
87    /// Sets an explicit maximum execution duration.
88    pub fn with_timeout(mut self, timeout: Duration) -> Self {
89        self.timeout = TimeoutPolicy::Limit(timeout);
90        self
91    }
92
93    /// Replaces the timeout intent with an explicit policy.
94    pub fn with_timeout_policy(mut self, timeout: TimeoutPolicy) -> Self {
95        self.timeout = timeout;
96        self
97    }
98
99    /// Uses the timeout selected by the backend or resolved profile.
100    pub fn use_backend_timeout(mut self) -> Self {
101        self.timeout = TimeoutPolicy::BackendDefault;
102        self
103    }
104
105    /// Disables the automatic timeout while leaving cancellation available to
106    /// the execution lifecycle.
107    pub fn disable_timeout(mut self) -> Self {
108        self.timeout = TimeoutPolicy::Disabled;
109        self
110    }
111
112    /// Returns the command line.
113    pub fn command(&self) -> &CommandSpec {
114        &self.command
115    }
116
117    /// Returns the optional working directory.
118    pub fn working_directory(&self) -> Option<&Path> {
119        self.working_directory.as_deref()
120    }
121
122    /// Returns environment construction rules.
123    pub fn environment(&self) -> &EnvironmentSpec {
124        &self.environment
125    }
126
127    /// Returns standard stream routing.
128    pub fn stdio(&self) -> StdioSpec {
129        self.stdio
130    }
131
132    /// Returns the timeout intent.
133    pub fn timeout_policy(&self) -> TimeoutPolicy {
134        self.timeout
135    }
136}