#!/usr/bin/env bash # provision-agent-user.sh — provisiona un Matrix user + scaffold para un agent LLM # del flow 0009 (issue 0144b). # # Uso: # ./dev-scripts/agent/provision-agent-user.sh # # Donde: # agent-id match ^agent-[a-z0-9-]+$ # host identificador fisico del PC (home-wsl, aurgi-pc, rpi-garage, ...) # mode "user" | "sudo" # # Ejemplos: # ./provision-agent-user.sh agent-home-wsl home-wsl user # ./provision-agent-user.sh agent-home-wsl-sudo home-wsl sudo # # Idempotente: si agents//config.yaml ya existe → exit 0 con # mensaje "Already provisioned". # # Requisitos en .env: # MATRIX_HOMESERVER URL completa (ej. https://matrix-af2f3d.organic-machine.com) # MATRIX_SERVER_NAME server_name Matrix (ej. matrix-af2f3d.organic-machine.com) # MATRIX_ADMIN_TOKEN syt_... admin user access token # OPERATOR_MATRIX_ID @lucas:matrix-af2f3d.organic-machine.com # _DEVICE_MESH_URL ej. http://10.42.0.10:7474 (opcional, default sentinel) # # Outputs: # agents//config.yaml # agents//agent.go # agents//prompts/system.md # agents//data/ (gitignored) # .env <- append KEY=VALUE para token, pickle key, device id, device mesh URL # # IMPORTANTE: este script NO toca cmd/launcher/main.go ni rebuilds. # El wiring del launcher para detectar agents nuevos lo hace 0144c. set -euo pipefail # ── load helpers ─────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck disable=SC1091 source "$SCRIPT_DIR/../_common.sh" # In test mode (FN_PROV_TEST=1) we tolerate missing .env (the test fixture sets # env vars manually). In production we require the .env to exist. if [[ "${FN_PROV_TEST:-0}" != "1" ]]; then load_env fi # ── args ─────────────────────────────────────────────────────────────────── if [[ $# -ne 3 ]]; then echo "Usage: $0 " >&2 echo " agent-id: ^agent-[a-z0-9-]+$" >&2 echo " host: PC identifier (home-wsl, aurgi-pc, ...)" >&2 echo " mode: user | sudo" >&2 exit 1 fi AGENT_ID="$1" HOST="$2" MODE="$3" # ── validation ───────────────────────────────────────────────────────────── if ! [[ "$AGENT_ID" =~ ^agent-[a-z0-9-]+$ ]]; then fail "agent-id '$AGENT_ID' invalid. Expected ^agent-[a-z0-9-]+$ (ej. agent-home-wsl, agent-home-wsl-sudo)." fi if ! [[ "$HOST" =~ ^[a-z0-9-]+$ ]]; then fail "host '$HOST' invalid. Expected ^[a-z0-9-]+$ (ej. home-wsl, aurgi-pc)." fi case "$MODE" in user|sudo) ;; *) fail "mode '$MODE' invalid. Expected 'user' or 'sudo'." ;; esac AGENT_DIR="agents/$AGENT_ID" CONFIG_FILE="$AGENT_DIR/config.yaml" AGENT_GO="$AGENT_DIR/agent.go" PROMPT_FILE="$AGENT_DIR/prompts/system.md" TEMPLATES_DIR="$SCRIPT_DIR/templates" # Derived names. AGENT_ID_UPPER="$(normalize_id "$AGENT_ID")" # Go package: agent-home-wsl-sudo → agenthomewslsudo PACKAGE="$(echo "$AGENT_ID" | tr -d '-')" # Display name: "Agent Home Wsl Sudo" DISPLAY_NAME="$(echo "$AGENT_ID" | tr '-' ' ' | awk '{ for (i=1;i<=NF;i++) $i = toupper(substr($i,1,1)) substr($i,2) } 1')" # ── idempotency check ────────────────────────────────────────────────────── if [[ -f "$CONFIG_FILE" ]]; then echo "Already provisioned: $CONFIG_FILE exists. Re-run with --force? (not implemented). Skipping." exit 0 fi # ── env preconditions ───────────────────────────────────────────────────── require_env() { local var="$1" if [[ -z "${!var:-}" ]]; then fail "Missing env var: $var. Define it in .env." fi } require_env MATRIX_HOMESERVER require_env MATRIX_SERVER_NAME require_env MATRIX_ADMIN_TOKEN require_env OPERATOR_MATRIX_ID # Optional device mesh URL (sentinel if missing). DEVICE_MESH_URL_VAR="${AGENT_ID_UPPER}_DEVICE_MESH_URL" DEVICE_MESH_URL_VAL="${!DEVICE_MESH_URL_VAR:-}" if [[ -z "$DEVICE_MESH_URL_VAL" ]]; then DEVICE_MESH_URL_VAL="http://10.42.0.10:7474" warn "$DEVICE_MESH_URL_VAR not set — defaulting to $DEVICE_MESH_URL_VAL" fi # ── deps ────────────────────────────────────────────────────────────────── for bin in curl jq openssl awk sed; do command -v "$bin" &>/dev/null || fail "Missing dependency: $bin" done # ── tmp dir for HTTP responses ──────────────────────────────────────────── TMP_DIR="$(mktemp -d -t fn_prov_${AGENT_ID}_XXXXXX)" trap 'rm -rf "$TMP_DIR"' EXIT info "Provisioning agent-id=$AGENT_ID host=$HOST mode=$MODE" info " homeserver: $MATRIX_HOMESERVER" info " user_id: @$AGENT_ID:$MATRIX_SERVER_NAME" info " package: $PACKAGE" info " display: $DISPLAY_NAME" info " mesh URL: $DEVICE_MESH_URL_VAL" # ── step 1: generate password ───────────────────────────────────────────── PASSWORD="$(openssl rand -hex 32)" # ── step 2: PUT /_synapse/admin/v2/users/ ───────────────────────── USER_ID="@${AGENT_ID}:${MATRIX_SERVER_NAME}" PUT_URL="${MATRIX_HOMESERVER%/}/_synapse/admin/v2/users/${USER_ID}" PUT_PAYLOAD=$(jq -n --arg displayname "$DISPLAY_NAME" --arg password "$PASSWORD" '{ password: $password, displayname: $displayname, admin: false, deactivated: false }') info "Creating Matrix user $USER_ID..." HTTP_CODE=$(curl -sS -o "$TMP_DIR/put_user.json" -w '%{http_code}' \ -X PUT "$PUT_URL" \ -H "Authorization: Bearer $MATRIX_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d "$PUT_PAYLOAD" || echo "000") case "$HTTP_CODE" in 200|201) ok "Matrix user $USER_ID created/updated (HTTP $HTTP_CODE)" ;; *) cat "$TMP_DIR/put_user.json" >&2 2>/dev/null || true fail "Synapse admin API PUT returned HTTP $HTTP_CODE (expected 200/201)" ;; esac # ── step 3: login to obtain access_token + device_id ────────────────────── LOGIN_URL="${MATRIX_HOMESERVER%/}/_matrix/client/v3/login" LOGIN_PAYLOAD=$(jq -n --arg user "$AGENT_ID" --arg password "$PASSWORD" '{ type: "m.login.password", identifier: { type: "m.id.user", user: $user }, password: $password, initial_device_display_name: "agents_and_robots provisioner" }') info "Logging in as $AGENT_ID to obtain access_token + device_id..." HTTP_CODE=$(curl -sS -o "$TMP_DIR/login.json" -w '%{http_code}' \ -X POST "$LOGIN_URL" \ -H "Content-Type: application/json" \ -d "$LOGIN_PAYLOAD" || echo "000") if [[ "$HTTP_CODE" != "200" ]]; then cat "$TMP_DIR/login.json" >&2 2>/dev/null || true fail "Matrix /v3/login returned HTTP $HTTP_CODE (expected 200)" fi ACCESS_TOKEN=$(jq -r '.access_token' "$TMP_DIR/login.json") DEVICE_ID=$(jq -r '.device_id' "$TMP_DIR/login.json") if [[ -z "$ACCESS_TOKEN" || "$ACCESS_TOKEN" == "null" ]]; then fail "Login response missing access_token" fi ok "Logged in. device_id=$DEVICE_ID" # ── step 4: generate pickle key (32 bytes base64) ───────────────────────── PICKLE_KEY="$(openssl rand -base64 32)" # ── step 5: persist secrets to .env (idempotent upsert) ─────────────────── upsert_env() { local key="$1" val="$2" local target=".env" # In test mode write to FN_PROV_ENV_OUT if set. if [[ -n "${FN_PROV_ENV_OUT:-}" ]]; then target="$FN_PROV_ENV_OUT" fi # Quote if value contains spaces or = if [[ "$val" == *" "* || "$val" == *=* ]]; then val="\"$val\"" fi if [[ -f "$target" ]] && grep -q "^${key}=" "$target"; then awk -v key="$key" -v val="$val" \ 'index($0, key "=") == 1 { print key "=" val; next } { print }' \ "$target" > "$target.tmp" && mv "$target.tmp" "$target" else printf '%s=%s\n' "$key" "$val" >> "$target" fi chmod 0600 "$target" 2>/dev/null || true } TOKEN_VAR="MATRIX_TOKEN_${AGENT_ID_UPPER}" PASSWORD_VAR="MATRIX_PASSWORD_${AGENT_ID_UPPER}" PICKLE_VAR="PICKLE_KEY_${AGENT_ID_UPPER}" DEVICE_ID_VAR="MATRIX_DEVICE_ID_${AGENT_ID_UPPER}" info "Persisting secrets to .env (chmod 0600)..." upsert_env "$TOKEN_VAR" "$ACCESS_TOKEN" upsert_env "$PASSWORD_VAR" "$PASSWORD" upsert_env "$PICKLE_VAR" "$PICKLE_KEY" upsert_env "$DEVICE_ID_VAR" "$DEVICE_ID" upsert_env "$DEVICE_MESH_URL_VAR" "$DEVICE_MESH_URL_VAL" ok ".env updated (5 vars)" # ── step 6: create scaffold dirs ────────────────────────────────────────── mkdir -p "$AGENT_DIR/prompts" "$AGENT_DIR/data" # ── step 7: render templates ────────────────────────────────────────────── render_template() { local src="$1" dst="$2" [[ -f "$src" ]] || fail "Template missing: $src" # Use a stream of sed substitutions. Values are escaped for sed: # we use '#' as separator to avoid clashes with '/' in URLs. sed \ -e "s#{{AGENT_ID}}#${AGENT_ID}#g" \ -e "s#{{AGENT_ID_UPPER}}#${AGENT_ID_UPPER}#g" \ -e "s#{{HOST}}#${HOST}#g" \ -e "s#{{MODE}}#${MODE}#g" \ -e "s#{{PACKAGE}}#${PACKAGE}#g" \ -e "s#{{DISPLAY_NAME}}#${DISPLAY_NAME}#g" \ -e "s#{{MATRIX_HOMESERVER}}#${MATRIX_HOMESERVER}#g" \ -e "s#{{MATRIX_SERVER_NAME}}#${MATRIX_SERVER_NAME}#g" \ -e "s#{{MATRIX_DEVICE_ID}}#${DEVICE_ID}#g" \ -e "s#{{OPERATOR_MATRIX_ID}}#${OPERATOR_MATRIX_ID}#g" \ "$src" > "$dst" } if [[ "$MODE" == "user" ]]; then render_template "$TEMPLATES_DIR/config.user.yaml.tmpl" "$CONFIG_FILE" render_template "$TEMPLATES_DIR/agent.user.go.tmpl" "$AGENT_GO" render_template "$TEMPLATES_DIR/prompts/system.user.md.tmpl" "$PROMPT_FILE" else render_template "$TEMPLATES_DIR/config.sudo.yaml.tmpl" "$CONFIG_FILE" render_template "$TEMPLATES_DIR/agent.sudo.go.tmpl" "$AGENT_GO" render_template "$TEMPLATES_DIR/prompts/system.sudo.md.tmpl" "$PROMPT_FILE" fi # Permissions on data/ (gitignored, holds crypto + memory.db) chmod 0700 "$AGENT_DIR/data" 2>/dev/null || true ok "Scaffold rendered:" echo " $CONFIG_FILE" echo " $AGENT_GO" echo " $PROMPT_FILE" echo " $AGENT_DIR/data/ (mode 0700)" # ── step 8: summary ─────────────────────────────────────────────────────── echo "" echo -e "${GRN}✓ Agent $AGENT_ID provisioned successfully.${RST}" echo "" echo -e "${YLW}Next steps:${RST}" echo "" echo -e " 1. Invite the operator to the agent's room:" echo -e " ${DIM}element → /invite ${OPERATOR_MATRIX_ID} en #${HOST}${MODE_ROOM_SUFFIX:-}${RST}" echo "" echo -e " 2. Verify E2EE cross-signing (so 'not verified by its owner' goes away):" echo -e " ${DIM}./dev-scripts/agent/verify.sh ${AGENT_ID}${RST}" echo "" echo -e " 3. Wire into the launcher (issue 0144c, NOT this script):" echo -e " ${DIM}cmd/launcher/main.go add blank import _ \"github.com/enmanuel/agents/agents/${AGENT_ID}\"${RST}" echo "" echo -e " 4. Build + start:" echo -e " ${DIM}go build -tags goolm ./...${RST}" echo -e " ${DIM}./dev-scripts/server/start.sh${RST}" echo "" echo -e " 5. JSON summary (parseable):" jq -n \ --arg agent_id "$AGENT_ID" \ --arg matrix_user "$USER_ID" \ --arg device_id "$DEVICE_ID" \ --arg host "$HOST" \ --arg mode "$MODE" \ --arg ts "$(date -u +%FT%TZ)" \ '{agent_id: $agent_id, matrix_user: $matrix_user, device_id: $device_id, host: $host, mode: $mode, ts: $ts}'