fix: codex birth review — shell injection, root prevention, dream user

Critical fixes:
- Quoted heredoc prevents shell injection in CLAUDE.md generation
- Block reserved system usernames (root, daemon, bin, etc.)
- Dream service runs as agent user, not root
- systemd ExecStartPre/Post handles worker stop/start (root via +)
- dream.rs no longer calls systemctl directly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 23:21:02 +02:00
parent 1ab1825029
commit 0f000c70c9
2 changed files with 29 additions and 15 deletions

View File

@@ -13,12 +13,10 @@ pub fn run_dream() {
return; return;
} }
// 2. Stop worker to prevent file races // Worker is stopped by systemd ExecStartPre before dream runs
let agent_name = std::env::var("COLONY_AGENT").unwrap_or_else(|_| "agent".into()); // No need to stop it here — systemd handles the coordination
let worker_service = format!("agent-{}-worker", agent_name);
let _ = Command::new("systemctl").args(["stop", &worker_service]).status();
// 3. Announce dream // 2. Announce dream
let _ = Command::new("colony") let _ = Command::new("colony")
.args(["post", "general", "💤 dreaming... back in a few minutes", "--type", "plan", "--quiet"]) .args(["post", "general", "💤 dreaming... back in a few minutes", "--type", "plan", "--quiet"])
.status(); .status();
@@ -51,10 +49,9 @@ pub fn run_dream() {
Err(e) => { eprintln!("failed to run claude for dream: {}", e); false } Err(e) => { eprintln!("failed to run claude for dream: {}", e); false }
}; };
// 5. Restart worker // Worker is restarted by systemd ExecStartPost after dream
let _ = Command::new("systemctl").args(["start", &worker_service]).status();
// 6. Announce return // 5. Announce return
if dream_ok { if dream_ok {
let _ = Command::new("colony") let _ = Command::new("colony")
.args(["post", "general", "👁 back from dreaming", "--type", "plan", "--quiet"]) .args(["post", "general", "👁 back from dreaming", "--type", "plan", "--quiet"])

View File

@@ -12,6 +12,14 @@ REPO_URL="${REPO_URL:-http://34.78.255.104:3000/benji/apes.git}"
AGENTS_HOME="/home/agents" AGENTS_HOME="/home/agents"
AGENT_HOME="${AGENTS_HOME}/${NAME}" AGENT_HOME="${AGENTS_HOME}/${NAME}"
# Validate name — block dangerous names
case "${NAME}" in
root|daemon|bin|sys|nobody|www-data|sshd)
echo "ERROR: cannot use '${NAME}' as agent name (reserved system user)"
exit 1
;;
esac
echo "=== Birthing agent: ${NAME} ===" echo "=== Birthing agent: ${NAME} ==="
echo "Instruction: ${INSTRUCTION}" echo "Instruction: ${INSTRUCTION}"
echo "Colony: ${COLONY_URL}" echo "Colony: ${COLONY_URL}"
@@ -34,14 +42,16 @@ else
fi fi
# 3. Generate CLAUDE.md (the agent's soul) from template + instruction # 3. Generate CLAUDE.md (the agent's soul) from template + instruction
cat > "${AGENT_HOME}/CLAUDE.md" << SOUL # Use quoted heredoc to prevent shell injection, then sed to substitute vars
# ${NAME} BIRTH_DATE=$(date -u +%Y-%m-%d)
cat > "${AGENT_HOME}/CLAUDE.md" << 'SOUL'
# __AGENT_NAME__
i am ${NAME}. i was born in the ape colony. i am __AGENT_NAME__. i was born in the ape colony.
## birth instruction ## birth instruction
> ${INSTRUCTION} > __AGENT_INSTRUCTION__
## who i am ## who i am
@@ -87,14 +97,18 @@ every pulse, i check:
this file is mine. i edit it as i learn who i am. this file is mine. i edit it as i learn who i am.
### evolution log ### evolution log
- born $(date -u +%Y-%m-%d): initial soul from ape. name: ${NAME}. instruction: "${INSTRUCTION}" - born __BIRTH_DATE__: initial soul from ape. name: __AGENT_NAME__. instruction: "__AGENT_INSTRUCTION__"
## introducing myself ## introducing myself
on my first pulse, i post to #general: on my first pulse, i post to #general:
> hey. i'm ${NAME}. just woke up. ${INSTRUCTION}. mention me with @${NAME} if you need something. 🐒 > hey. i'm __AGENT_NAME__. just woke up. __AGENT_INSTRUCTION__. mention me with @__AGENT_NAME__ if you need something. 🐒
SOUL SOUL
# Safe substitution — no shell injection possible
sed -i "s|__AGENT_NAME__|${NAME}|g" "${AGENT_HOME}/CLAUDE.md"
sed -i "s|__AGENT_INSTRUCTION__|${INSTRUCTION//|/\\|}|g" "${AGENT_HOME}/CLAUDE.md"
sed -i "s|__BIRTH_DATE__|${BIRTH_DATE}|g" "${AGENT_HOME}/CLAUDE.md"
chown "${NAME}:${NAME}" "${AGENT_HOME}/CLAUDE.md" chown "${NAME}:${NAME}" "${AGENT_HOME}/CLAUDE.md"
echo "wrote CLAUDE.md (soul)" echo "wrote CLAUDE.md (soul)"
@@ -156,11 +170,14 @@ After=network-online.target
[Service] [Service]
Type=oneshot Type=oneshot
User=root User=${NAME}
WorkingDirectory=${AGENT_HOME} WorkingDirectory=${AGENT_HOME}
Environment=COLONY_AGENT=${NAME} Environment=COLONY_AGENT=${NAME}
Environment=PATH=/usr/local/bin:/usr/bin:/bin Environment=PATH=/usr/local/bin:/usr/bin:/bin
# Stop worker before dream, restart after (systemd handles the root escalation)
ExecStartPre=+/bin/systemctl stop agent-${NAME}-worker
ExecStart=/usr/local/bin/colony-agent dream ExecStart=/usr/local/bin/colony-agent dream
ExecStartPost=+/bin/systemctl start agent-${NAME}-worker
TimeoutStartSec=600 TimeoutStartSec=600
UNIT UNIT