diff --git a/crates/colony-agent/src/main.rs b/crates/colony-agent/src/main.rs index 9d59da2..d860585 100644 --- a/crates/colony-agent/src/main.rs +++ b/crates/colony-agent/src/main.rs @@ -44,9 +44,31 @@ async fn main() { dream::run_dream(); } Commands::Birth { name, instruction } => { - eprintln!("colony-agent: birth '{}' with instruction: {}", name, instruction); - eprintln!("not yet implemented"); - std::process::exit(1); + // Birth delegates to the shell script for now + let script = std::path::Path::new("scripts/birth.sh"); + 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 => { eprintln!("colony-agent: status not yet implemented"); diff --git a/crates/colony/src/main.rs b/crates/colony/src/main.rs index b6e80df..6917d92 100644 --- a/crates/colony/src/main.rs +++ b/crates/colony/src/main.rs @@ -40,7 +40,7 @@ async fn main() { let app = Router::new() .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/channels", diff --git a/crates/colony/src/routes.rs b/crates/colony/src/routes.rs index 422033d..67d470b 100644 --- a/crates/colony/src/routes.rs +++ b/crates/colony/src/routes.rs @@ -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, + Json(body): Json, +) -> Result { + 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( State(state): State, Query(user_param): Query, diff --git a/scripts/birth.sh b/scripts/birth.sh new file mode 100755 index 0000000..f3c8295 --- /dev/null +++ b/scripts/birth.sh @@ -0,0 +1,194 @@ +#!/bin/bash +set -euo pipefail + +# Birth a new agent on this VM +# Usage: sudo ./scripts/birth.sh "" +# Example: sudo ./scripts/birth.sh scout "help with research, watch #general and #research" + +NAME="${1:?Usage: birth.sh \"\"}" +INSTRUCTION="${2:?Usage: birth.sh \"\"}" +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"