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:
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
State(state): State<AppState>,
|
||||
Query(user_param): Query<UserParam>,
|
||||
|
||||
194
scripts/birth.sh
Executable file
194
scripts/birth.sh
Executable 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"
|
||||
Reference in New Issue
Block a user