Skip to content

Commit f4776fe

Browse files
committed
feat: agent user with sudo for apt-install, run all commands as non-root agent
- Dockerfile: pre-install common runtimes (python, node, go, java, ruby, rust, build tools) - Dockerfile: create 'agent' user with passwordless sudo for apt-get - executor.rs: run install/agent/test commands as 'agent' user via sudo -u - executor.rs: prepare_install_command adds sudo + DEBIAN_FRONTEND for apt commands - executor.rs: chmod repo dirs writable for agent user
1 parent 3855f2d commit f4776fe

File tree

2 files changed

+104
-45
lines changed

2 files changed

+104
-45
lines changed

Dockerfile

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,41 @@ RUN cargo build --release && strip target/release/term-executor
1010

1111
# ── Runtime stage ──
1212
FROM debian:bookworm-slim
13+
14+
# Pre-install all common runtimes and tools at build time (as root)
1315
RUN apt-get update && apt-get install -y --no-install-recommends \
14-
ca-certificates git curl unzip libssl3 libssl-dev pkg-config \
15-
python3 python3-pip python3-venv \
16-
build-essential nodejs npm \
16+
ca-certificates git curl wget unzip libssl3 libssl-dev pkg-config sudo \
17+
python3 python3-pip python3-venv python3-dev \
18+
build-essential gcc g++ make cmake autoconf automake libtool \
19+
nodejs npm \
1720
golang-go \
18-
default-jdk maven \
21+
default-jdk maven gradle \
22+
ruby ruby-dev \
23+
libffi-dev libxml2-dev libxslt1-dev zlib1g-dev libyaml-dev \
24+
libreadline-dev libncurses-dev libgdbm-dev libdb-dev \
25+
sqlite3 libsqlite3-dev postgresql-client libpq-dev \
26+
imagemagick libmagickwand-dev \
27+
jq \
1928
&& ln -sf /usr/bin/python3 /usr/bin/python \
2029
&& npm install -g corepack yarn pnpm \
2130
&& corepack enable \
2231
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
2332
&& rm -rf /var/lib/apt/lists/*
2433
ENV PATH="/root/.cargo/bin:${PATH}"
34+
35+
# Create non-root 'agent' user for running agent code and install commands.
36+
# Agents can apt-install as this user (has passwordless sudo).
37+
RUN useradd -m -s /bin/bash agent \
38+
&& echo "agent ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/agent \
39+
&& chmod 0440 /etc/sudoers.d/agent \
40+
&& cp -r /root/.cargo /home/agent/.cargo \
41+
&& chown -R agent:agent /home/agent/.cargo \
42+
&& mkdir -p /home/agent/.local/bin \
43+
&& chown -R agent:agent /home/agent
44+
ENV AGENT_USER=agent
45+
2546
COPY --from=builder /build/target/release/term-executor /usr/local/bin/
26-
RUN mkdir -p /tmp/sessions
47+
RUN mkdir -p /tmp/sessions && chmod 777 /tmp/sessions
2748
ENV IMAGE_NAME=platformnetwork/term-executor
2849
ENV IMAGE_DIGEST=""
2950
EXPOSE 8080

src/executor.rs

Lines changed: 78 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ use crate::task::{ExtractedArchive, SweForgeTask};
1515

1616
const MAX_OUTPUT: usize = 1024 * 1024;
1717

18+
fn shell_escape(s: &str) -> String {
19+
format!("'{}'", s.replace('\'', "'\\''"))
20+
}
21+
1822
fn truncate_output(raw: &[u8]) -> String {
1923
if raw.len() <= MAX_OUTPUT {
2024
String::from_utf8_lossy(raw).to_string()
@@ -64,6 +68,7 @@ async fn run_cmd(
6468
))
6569
}
6670

71+
#[allow(dead_code)]
6772
async fn run_shell(
6873
shell_cmd: &str,
6974
cwd: &Path,
@@ -73,22 +78,56 @@ async fn run_shell(
7378
run_cmd(&["sh", "-c", shell_cmd], cwd, timeout, env).await
7479
}
7580

76-
/// Filter out system-level package commands that can't run in restricted containers.
77-
/// Keeps project-level install commands (npm install, pip install, yarn install, etc.)
78-
fn filter_install_command(cmd: &str) -> String {
79-
let system_prefixes = [
80-
"apt-get", "apt ", "dpkg", "yum ", "dnf ", "pacman ", "apk ", "snap ", "flatpak ",
81-
];
81+
/// Get the agent user name from AGENT_USER env var (default: "agent").
82+
fn agent_user() -> String {
83+
std::env::var("AGENT_USER").unwrap_or_else(|_| "agent".to_string())
84+
}
85+
86+
/// Wrap a shell command to run as the agent user via sudo.
87+
fn as_agent_cmd(shell_cmd: &str, cwd: &Path) -> Vec<String> {
88+
let user = agent_user();
89+
vec![
90+
"sudo".to_string(),
91+
"-u".to_string(),
92+
user,
93+
"--preserve-env=PATH,HOME,JAVA_HOME,GOPATH,CARGO_HOME,NODE_PATH".to_string(),
94+
"sh".to_string(),
95+
"-c".to_string(),
96+
format!("cd {} && {}", cwd.display(), shell_cmd),
97+
]
98+
}
99+
100+
/// Run a shell command as the agent user.
101+
async fn run_shell_as_agent(
102+
shell_cmd: &str,
103+
cwd: &Path,
104+
timeout: Duration,
105+
env: Option<&[(&str, &str)]>,
106+
) -> Result<(String, String, i32)> {
107+
let argv_owned = as_agent_cmd(shell_cmd, cwd);
108+
let argv: Vec<&str> = argv_owned.iter().map(|s| s.as_str()).collect();
109+
run_cmd(&argv, cwd, timeout, env).await
110+
}
82111

83-
// Split on && and filter out system commands
112+
/// Prepare install command: add -y flag to apt-get commands, add DEBIAN_FRONTEND=noninteractive.
113+
fn prepare_install_command(cmd: &str) -> String {
84114
let parts: Vec<&str> = cmd.split("&&").collect();
85-
let filtered: Vec<&str> = parts
115+
let prepared: Vec<String> = parts
86116
.iter()
87-
.map(|p| p.trim())
88-
.filter(|p| !system_prefixes.iter().any(|prefix| p.starts_with(prefix)))
117+
.map(|p| {
118+
let trimmed = p.trim();
119+
if trimmed.starts_with("apt-get") || trimmed.starts_with("apt ") {
120+
if trimmed.contains("install") && !trimmed.contains("-y") {
121+
format!("DEBIAN_FRONTEND=noninteractive sudo {}", trimmed.replace("install", "install -y"))
122+
} else {
123+
format!("DEBIAN_FRONTEND=noninteractive sudo {}", trimmed)
124+
}
125+
} else {
126+
trimmed.to_string()
127+
}
128+
})
89129
.collect();
90-
91-
filtered.join(" && ")
130+
prepared.join(" && ")
92131
}
93132

94133
pub struct Executor {
@@ -347,21 +386,18 @@ async fn run_task_pipeline(
347386
}
348387

349388
result.status = TaskStatus::InstallingDeps;
389+
// Ensure repo_dir is writable by the agent user
390+
let _ = run_cmd(
391+
&["chmod", "-R", "a+rwX", &repo_dir.to_string_lossy()],
392+
work_dir,
393+
Duration::from_secs(30),
394+
None,
395+
).await;
350396
if let Some(ref install_cmds) = task.workspace.install {
351397
for cmd in install_cmds {
352-
// Split chained commands and filter out system package commands
353-
// that can't run in a restricted container (apt-get, dpkg, etc.)
354-
let effective_cmd = filter_install_command(cmd);
355-
if effective_cmd.is_empty() {
356-
info!(
357-
"[{}] Skipping system install: {}",
358-
task.id,
359-
&cmd[..cmd.len().min(100)]
360-
);
361-
continue;
362-
}
363-
info!("[{}] Installing: {}", task.id, effective_cmd);
364-
let (_, stderr, exit) = run_shell(
398+
let effective_cmd = prepare_install_command(cmd);
399+
info!("[{}] Installing (as {}): {}", task.id, agent_user(), effective_cmd);
400+
let (_, stderr, exit) = run_shell_as_agent(
365401
&effective_cmd,
366402
&repo_dir,
367403
Duration::from_secs(config.clone_timeout_secs),
@@ -536,20 +572,21 @@ async fn run_agent(
536572
tokio::fs::write(&prompt_path, prompt).await?;
537573

538574
let argv_owned = agent_runner(agent_language, &script_name);
539-
let argv: Vec<&str> = argv_owned.iter().map(|s| s.as_str()).collect();
540-
info!("Running agent: {:?}", argv);
541-
542-
let env_vars = [
543-
("TASK_PROMPT", prompt_path.to_string_lossy().to_string()),
544-
("REPO_DIR", repo_dir.to_string_lossy().to_string()),
545-
];
546-
let env_refs: Vec<(&str, &str)> = env_vars.iter().map(|(k, v)| (*k, v.as_str())).collect();
547-
548-
let (stdout, stderr, exit) = run_cmd(
549-
&argv,
575+
let runner_cmd = argv_owned.join(" ");
576+
info!("Running agent as {}: {}", agent_user(), runner_cmd);
577+
578+
let env_export = format!(
579+
"export TASK_PROMPT={} REPO_DIR={} && {}",
580+
shell_escape(&prompt_path.to_string_lossy()),
581+
shell_escape(&repo_dir.to_string_lossy()),
582+
runner_cmd,
583+
);
584+
585+
let (stdout, stderr, exit) = run_shell_as_agent(
586+
&env_export,
550587
repo_dir,
551588
Duration::from_secs(timeout_secs),
552-
Some(&env_refs),
589+
None,
553590
)
554591
.await?;
555592

@@ -581,9 +618,10 @@ async fn run_tests(
581618
let _ = std::fs::set_permissions(&script_path, perms);
582619
}
583620

584-
debug!("Running test script: {}", name);
585-
let result = run_cmd(
586-
&["bash", &script_path.to_string_lossy()],
621+
debug!("Running test script as {}: {}", agent_user(), name);
622+
let test_cmd = format!("bash {}", shell_escape(&script_path.to_string_lossy()));
623+
let result = run_shell_as_agent(
624+
&test_cmd,
587625
repo_dir,
588626
Duration::from_secs(timeout_secs),
589627
None,

0 commit comments

Comments
 (0)