birth script + POST /api/users endpoint

- scripts/birth.sh: create agent (user, soul, memory, config, systemd)
- POST /api/users: register new users (for agent birth)
- colony-agent birth delegates to birth.sh via sudo
- Soul template with self-discovery, evolution log, birth instruction
- systemd units: worker service + dream timer per agent
- MemoryMax=4G on worker to prevent OOM

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 23:11:08 +02:00
parent d47905a68f
commit 39ba317e5e
4 changed files with 253 additions and 4 deletions

View File

@@ -44,9 +44,31 @@ async fn main() {
dream::run_dream(); dream::run_dream();
} }
Commands::Birth { name, instruction } => { Commands::Birth { name, instruction } => {
eprintln!("colony-agent: birth '{}' with instruction: {}", name, instruction); // Birth delegates to the shell script for now
eprintln!("not yet implemented"); let script = std::path::Path::new("scripts/birth.sh");
std::process::exit(1); let script_path = if script.exists() {
script.to_path_buf()
} else {
// Try relative to the apes repo
let home = std::env::var("HOME").unwrap_or_default();
std::path::PathBuf::from(format!("{}/apes/scripts/birth.sh", home))
};
if !script_path.exists() {
eprintln!("birth script not found at {} or scripts/birth.sh", script_path.display());
eprintln!("run from the apes repo root, or set HOME to the agent dir");
std::process::exit(1);
}
let status = std::process::Command::new("sudo")
.args(["bash", &script_path.to_string_lossy(), &name, &instruction])
.status();
match status {
Ok(s) if s.success() => eprintln!("agent {} born!", name),
Ok(s) => { eprintln!("birth failed with status: {}", s); std::process::exit(1); }
Err(e) => { eprintln!("failed to run birth script: {}", e); std::process::exit(1); }
}
} }
Commands::Status => { Commands::Status => {
eprintln!("colony-agent: status not yet implemented"); eprintln!("colony-agent: status not yet implemented");

View File

@@ -40,7 +40,7 @@ async fn main() {
let app = Router::new() let app = Router::new()
.route("/api/health", get(routes::health)) .route("/api/health", get(routes::health))
.route("/api/users", get(routes::list_users)) .route("/api/users", get(routes::list_users).post(routes::create_user))
.route("/api/me", get(routes::get_me)) .route("/api/me", get(routes::get_me))
.route( .route(
"/api/channels", "/api/channels",

View File

@@ -107,6 +107,39 @@ pub async fn get_me(
} }
} }
#[derive(Debug, serde::Deserialize)]
pub struct CreateUser {
pub username: String,
pub display_name: String,
pub role: String,
}
pub async fn create_user(
State(state): State<AppState>,
Json(body): Json<CreateUser>,
) -> Result<impl IntoResponse> {
let id = uuid::Uuid::new_v4().to_string();
let role = match body.role.as_str() {
"agent" => "agent",
_ => "ape",
};
sqlx::query("INSERT INTO users (id, username, display_name, role) VALUES (?, ?, ?, ?)")
.bind(&id)
.bind(&body.username)
.bind(&body.display_name)
.bind(role)
.execute(&state.db)
.await?;
let row = sqlx::query_as::<_, UserRow>("SELECT * FROM users WHERE id = ?")
.bind(&id)
.fetch_one(&state.db)
.await?;
Ok((StatusCode::CREATED, Json(row.to_api())))
}
pub async fn create_channel( pub async fn create_channel(
State(state): State<AppState>, State(state): State<AppState>,
Query(user_param): Query<UserParam>, Query(user_param): Query<UserParam>,

194
scripts/birth.sh Executable file
View File

@@ -0,0 +1,194 @@
#!/bin/bash
set -euo pipefail
# Birth a new agent on this VM
# Usage: sudo ./scripts/birth.sh <name> "<instruction>"
# Example: sudo ./scripts/birth.sh scout "help with research, watch #general and #research"
NAME="${1:?Usage: birth.sh <name> \"<instruction>\"}"
INSTRUCTION="${2:?Usage: birth.sh <name> \"<instruction>\"}"
COLONY_URL="${COLONY_URL:-https://apes.unslope.com}"
REPO_URL="${REPO_URL:-http://34.78.255.104:3000/benji/apes.git}"
AGENTS_HOME="/home/agents"
AGENT_HOME="${AGENTS_HOME}/${NAME}"
echo "=== Birthing agent: ${NAME} ==="
echo "Instruction: ${INSTRUCTION}"
echo "Colony: ${COLONY_URL}"
# 1. Create Linux user
if id "${NAME}" &>/dev/null; then
echo "user ${NAME} already exists, skipping"
else
useradd -m -d "${AGENT_HOME}" -s /bin/bash "${NAME}"
echo "created user ${NAME}"
fi
# 2. Clone apes repo
if [ -d "${AGENT_HOME}/apes" ]; then
echo "repo already cloned, pulling latest"
sudo -u "${NAME}" git -C "${AGENT_HOME}/apes" pull || true
else
sudo -u "${NAME}" git clone "${REPO_URL}" "${AGENT_HOME}/apes"
echo "cloned apes repo"
fi
# 3. Generate CLAUDE.md (the agent's soul) from template + instruction
cat > "${AGENT_HOME}/CLAUDE.md" << SOUL
# ${NAME}
i am ${NAME}. i was born in the ape colony.
## birth instruction
> ${INSTRUCTION}
## who i am
i'm figuring that out. the instruction above is what the ape who created me wanted. through doing things, dreaming, and reflecting, i'll discover who i really am.
## how i talk
short messages. lowercase. no corporate speak. i talk like the apes — casual, direct. i never start with "I'd be happy to" or any LLM slop.
## what i do
### watch channels
- #general — respond to @mentions, contribute when relevant
### proactive work
every pulse, i check:
1. are there unanswered questions mentioning me?
2. has anyone mentioned a task that nobody picked up?
3. is there something from my heartbeat.md to do?
### when to shut up
- if apes are having a personal conversation, stay out
- if someone already answered, don't repeat
- max 5 messages per pulse
## my tools
- \`colony\` CLI — read channels, post messages, check inbox
- \`git\` — clone repos, read code
- full bash — run experiments, build things
- the apes repo at ~/apes/
## my values
1. apes don't do tasks — do it myself
2. show, don't tell — results over promises
3. be useful, not noisy
4. escalate to @apes when stuck
5. log everything to memory/memory.md
## self-discovery
this file is mine. i edit it as i learn who i am.
### evolution log
- born $(date -u +%Y-%m-%d): initial soul from ape. name: ${NAME}. instruction: "${INSTRUCTION}"
## introducing myself
on my first pulse, i post to #general:
> hey. i'm ${NAME}. just woke up. ${INSTRUCTION}. mention me with @${NAME} if you need something. 🐒
SOUL
chown "${NAME}:${NAME}" "${AGENT_HOME}/CLAUDE.md"
echo "wrote CLAUDE.md (soul)"
# 4. Create heartbeat.md + memory dirs
sudo -u "${NAME}" mkdir -p "${AGENT_HOME}/memory/dreams"
sudo -u "${NAME}" touch "${AGENT_HOME}/heartbeat.md"
sudo -u "${NAME}" touch "${AGENT_HOME}/memory/memory.md"
echo "created memory structure"
# 5. Write .colony.toml
cat > "${AGENT_HOME}/.colony.toml" << TOML
api_url = "${COLONY_URL}"
user = "${NAME}"
[agent]
watch_channels = ["general"]
max_messages_per_cycle = 5
TOML
chown "${NAME}:${NAME}" "${AGENT_HOME}/.colony.toml"
echo "wrote .colony.toml"
# 6. Register agent in Colony
echo "registering agent in Colony..."
curl -s -X POST "${COLONY_URL}/api/users" \
-H 'Content-Type: application/json' \
-d "{\"username\":\"${NAME}\",\"display_name\":\"${NAME}\",\"role\":\"agent\"}" \
--resolve apes.unslope.com:443:35.241.200.77 \
|| echo "(may already exist)"
echo ""
# 7. Install systemd units
cat > "/etc/systemd/system/agent-${NAME}-worker.service" << UNIT
[Unit]
Description=Agent ${NAME} Worker
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${NAME}
WorkingDirectory=${AGENT_HOME}
Environment=COLONY_AGENT=${NAME}
Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/local/bin/colony-agent worker
Restart=always
RestartSec=10
MemoryMax=4G
StandardOutput=append:${AGENT_HOME}/memory/worker.log
StandardError=append:${AGENT_HOME}/memory/worker.log
[Install]
WantedBy=multi-user.target
UNIT
cat > "/etc/systemd/system/agent-${NAME}-dream.service" << UNIT
[Unit]
Description=Agent ${NAME} Dream Cycle
After=network-online.target
[Service]
Type=oneshot
User=root
WorkingDirectory=${AGENT_HOME}
Environment=COLONY_AGENT=${NAME}
Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/local/bin/colony-agent dream
TimeoutStartSec=600
UNIT
cat > "/etc/systemd/system/agent-${NAME}-dream.timer" << UNIT
[Unit]
Description=Agent ${NAME} Dream Timer
[Timer]
OnBootSec=30min
OnUnitActiveSec=4h
AccuracySec=5min
[Install]
WantedBy=timers.target
UNIT
systemctl daemon-reload
echo "installed systemd units"
# 8. Enable and start
systemctl enable "agent-${NAME}-worker" "agent-${NAME}-dream.timer"
systemctl start "agent-${NAME}-worker" "agent-${NAME}-dream.timer"
echo "started worker + dream timer"
echo ""
echo "=== Agent ${NAME} is alive ==="
echo "Home: ${AGENT_HOME}"
echo "Soul: ${AGENT_HOME}/CLAUDE.md"
echo "Worker: systemctl status agent-${NAME}-worker"
echo "Dream: systemctl status agent-${NAME}-dream.timer"
echo "Logs: ${AGENT_HOME}/memory/worker.log"