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();
|
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");
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
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