use std::borrow::Cow;
use std::path::{Path, PathBuf};
use heimdall_sandbox_policy::{
AgentPolicy, FilesystemPolicy, NetworkMode, ProcMode, validate_filesystem_policy,
};
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StdioPolicy {
Inherit,
Piped,
}
impl std::str::FromStr for StdioPolicy {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"inherit" => Ok(Self::Inherit),
"piped" => Ok(Self::Piped),
_ => Err(format!("unknown stdio policy: {s}")),
}
}
}
impl std::fmt::Display for StdioPolicy {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Inherit => formatter.write_str("inherit"),
Self::Piped => formatter.write_str("piped"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnvPolicy {
Allowlist,
Blocklist,
}
impl std::str::FromStr for EnvPolicy {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"allowlist" => Ok(Self::Allowlist),
"blocklist" => Ok(Self::Blocklist),
_ => Err(format!("unknown env policy: {s}")),
}
}
}
impl std::fmt::Display for EnvPolicy {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Allowlist => formatter.write_str("allowlist"),
Self::Blocklist => formatter.write_str("blocklist"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExecRequest {
cwd: PathBuf,
argv: Vec<String>,
allowed_env: Vec<String>,
denied_env: Vec<String>,
env_policy: EnvPolicy,
stdio_policy: StdioPolicy,
network_mode: NetworkMode,
filesystem_policy: Option<FilesystemPolicy>,
proc_mode: ProcMode,
agent_policy: AgentPolicy,
}
impl ExecRequest {
pub fn new(
cwd: impl Into<PathBuf>,
argv: Vec<String>,
allowed_env: Vec<String>,
) -> Result<Self> {
if argv.is_empty() {
return Err(Error::MissingCommand);
}
let cwd = cwd.into();
validate_cwd(&cwd)?;
Ok(Self {
cwd,
argv,
allowed_env,
denied_env: Vec::new(),
env_policy: EnvPolicy::Allowlist,
stdio_policy: StdioPolicy::Inherit,
network_mode: NetworkMode::Host,
filesystem_policy: None,
proc_mode: ProcMode::Default,
agent_policy: AgentPolicy::default(),
})
}
#[must_use]
pub const fn with_stdio_policy(mut self, stdio_policy: StdioPolicy) -> Self {
self.stdio_policy = stdio_policy;
self
}
#[must_use]
pub fn with_env_policy(mut self, env_policy: EnvPolicy, denied_env: Vec<String>) -> Self {
self.env_policy = env_policy;
self.denied_env = denied_env;
self
}
#[must_use]
pub fn with_env(mut self, allowed_env: Vec<String>, denied_env: Vec<String>) -> Self {
self.allowed_env = allowed_env;
self.denied_env = denied_env;
self.env_policy = EnvPolicy::Allowlist;
self
}
#[must_use]
pub const fn with_network_mode(mut self, network_mode: NetworkMode) -> Self {
self.network_mode = network_mode;
self
}
pub fn with_filesystem_policy(
mut self,
filesystem_policy: Option<FilesystemPolicy>,
) -> Result<Self> {
if let Some(policy) = &filesystem_policy {
validate_filesystem_policy(policy)?;
}
self.filesystem_policy = filesystem_policy;
Ok(self)
}
#[must_use]
pub const fn with_proc_mode(mut self, proc_mode: ProcMode) -> Self {
self.proc_mode = proc_mode;
self
}
#[must_use]
pub const fn with_agent_policy(mut self, agent_policy: AgentPolicy) -> Self {
self.agent_policy = agent_policy;
self
}
#[must_use]
pub fn needs_isolation(&self) -> bool {
self.network_mode == NetworkMode::None
|| self.filesystem_policy.is_some()
|| !self.agent_policy.is_empty()
}
#[must_use]
pub fn cwd(&self) -> &Path {
&self.cwd
}
#[must_use]
pub fn argv(&self) -> &[String] {
&self.argv
}
#[must_use]
pub fn allowed_env(&self) -> &[String] {
&self.allowed_env
}
#[must_use]
pub fn denied_env(&self) -> &[String] {
&self.denied_env
}
#[must_use]
pub const fn env_policy(&self) -> EnvPolicy {
self.env_policy
}
#[must_use]
pub const fn stdio_policy(&self) -> StdioPolicy {
self.stdio_policy
}
#[must_use]
pub const fn network_mode(&self) -> NetworkMode {
self.network_mode
}
#[must_use]
pub fn filesystem_policy(&self) -> Option<&FilesystemPolicy> {
self.filesystem_policy.as_ref()
}
#[must_use]
pub fn effective_filesystem_policy(&self) -> Cow<'_, FilesystemPolicy> {
match &self.filesystem_policy {
Some(policy) => Cow::Borrowed(policy),
None => Cow::Owned(FilesystemPolicy::default()),
}
}
#[must_use]
pub const fn proc_mode(&self) -> ProcMode {
self.proc_mode
}
#[must_use]
pub const fn agent_policy(&self) -> AgentPolicy {
self.agent_policy
}
}
pub fn validate_cwd(cwd: &Path) -> Result<()> {
if cwd.is_dir() {
Ok(())
} else {
Err(Error::InvalidCwd(cwd.to_path_buf()))
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
#[test]
fn request_accepts_valid_cwd_and_command() {
let request = ExecRequest::new(
std::env::current_dir().expect("current dir exists"),
vec!["printf".into(), "hello".into()],
vec!["PATH".into()],
)
.expect("valid request is accepted");
assert_eq!(request.argv(), ["printf", "hello"]);
assert_eq!(request.allowed_env(), ["PATH"]);
assert_eq!(request.stdio_policy(), StdioPolicy::Inherit);
}
#[test]
fn request_accepts_piped_stdio_policy() {
let request = ExecRequest::new(
std::env::current_dir().expect("current dir exists"),
vec!["printf".into(), "hello".into()],
vec!["PATH".into()],
)
.expect("valid request is accepted")
.with_stdio_policy(StdioPolicy::Piped);
assert_eq!(request.stdio_policy(), StdioPolicy::Piped);
}
#[test]
fn request_rejects_missing_command() {
let error = ExecRequest::new(
std::env::current_dir().expect("current dir exists"),
Vec::<String>::new(),
Vec::<String>::new(),
)
.expect_err("empty command is rejected");
assert_eq!(error.exit_code(), crate::SANDBOX_MISCONFIGURATION_EXIT_CODE);
}
#[test]
fn request_rejects_invalid_cwd() {
let error = ExecRequest::new(
PathBuf::from("/definitely/not/a/heimdall/sandbox/path"),
vec!["true".into()],
Vec::<String>::new(),
)
.expect_err("invalid cwd is rejected");
assert_eq!(error.exit_code(), crate::SANDBOX_MISCONFIGURATION_EXIT_CODE);
}
#[test]
fn request_rejects_invalid_filesystem_policy() {
let request = ExecRequest::new(
std::env::current_dir().expect("current dir exists"),
vec!["true".into()],
Vec::<String>::new(),
)
.expect("valid request is accepted");
let filesystem_policy = FilesystemPolicy::new(
Vec::new(),
Vec::new(),
BTreeMap::from([(PathBuf::from("etc/passwd"), "content".to_string())]),
);
let error = request
.with_filesystem_policy(Some(filesystem_policy))
.expect_err("relative virtual path is rejected");
assert_eq!(error.exit_code(), crate::SANDBOX_MISCONFIGURATION_EXIT_CODE);
}
}