4c5bf95def
Bash script que provisiona Matrix user via Synapse admin API + login para access_token + scaffold completo (config.yaml, agent.go, prompts/system.md). 6 templates (user/sudo x config/agent.go/prompt). 20 tests bash pasan. Genera .env con AGENT_<ID>_TOKEN/PASSWORD/PICKLE/DEVICE_ID + URL mesh.
300 lines
12 KiB
Bash
Executable File
300 lines
12 KiB
Bash
Executable File
#!/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 <agent-id> <host> <mode>
|
|
#
|
|
# 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/<agent-id>/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
|
|
# <AGENT_ID_UPPER>_DEVICE_MESH_URL ej. http://10.42.0.10:7474 (opcional, default sentinel)
|
|
#
|
|
# Outputs:
|
|
# agents/<agent-id>/config.yaml
|
|
# agents/<agent-id>/agent.go
|
|
# agents/<agent-id>/prompts/system.md
|
|
# agents/<agent-id>/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 <agent-id> <host> <mode>" >&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/<userId> ─────────────────────────
|
|
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}'
|