Files
aios/scripts/install_aios_orchestrator_gateway.sh
T

306 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
umask 0077
SOURCE_ROOT="${SOURCE_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
INSTALL_ROOT="${INSTALL_ROOT:-/opt/aios-orchestrator-gateway}"
CONFIG_ROOT="${CONFIG_ROOT:-/etc/aios-orchestrator-gateway}"
STATE_ROOT="${STATE_ROOT:-/var/lib/aios-orchestrator-gateway}"
UNIT_ROOT="${UNIT_ROOT:-/etc/systemd/system}"
SYSUSERS_ROOT="${SYSUSERS_ROOT:-/etc/sysusers.d}"
TMPFILES_ROOT="${TMPFILES_ROOT:-/etc/tmpfiles.d}"
DRY_RUN=0
if [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=1
fi
fail() {
printf 'AIOS_ORCHESTRATOR_INSTALL=BLOCKED reason=%s\n' "$1" >&2
exit 2
}
[[ "$(id -u)" -eq 0 ]] || fail OWNER_INSTALL_REQUIRED
for cmd in git python3 systemd-analyze systemctl systemd-sysusers systemd-tmpfiles sha256sum find sort xargs; do
command -v "$cmd" >/dev/null 2>&1 || fail "MISSING_COMMAND_${cmd}"
done
if [[ -f "$SOURCE_ROOT/SHA256SUMS" ]]; then
(cd "$SOURCE_ROOT" && sha256sum --check SHA256SUMS)
fi
for path in \
"$SOURCE_ROOT/services/aios_orchestrator_gateway" \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway.service" \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway-poll.service" \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway-poll.timer" \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway.sysusers" \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway.tmpfiles" \
"$SOURCE_ROOT/config/aios-orchestrator-gateway.example.json"; do
[[ -e "$path" ]] || fail "MISSING_SOURCE_${path}"
done
PYCACHE="/tmp/aios-orchestrator-install-pycache-$$"
rm -rf "$PYCACHE"
PYTHONPYCACHEPREFIX="$PYCACHE" python3 -m compileall -q "$SOURCE_ROOT/services" "$SOURCE_ROOT/scripts"
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH="$SOURCE_ROOT" \
python3 -m unittest -v tests.test_aios_orchestrator_gateway
rm -rf "$PYCACHE"
for script in "$SOURCE_ROOT"/scripts/*.sh; do bash -n "$script"; done
python3 -m json.tool "$SOURCE_ROOT/config/aios-orchestrator-gateway.example.json" >/dev/null
systemd-analyze verify \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway.service" \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway-poll.service" \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway-poll.timer"
SOURCE_FILES=(
README-AIOS-ORCHESTRATOR-GATEWAY.md
config/aios-orchestrator-gateway.example.json
schemas/decision.schema.json
schemas/event.schema.json
scripts/bootstrap_orchestration_loop.py
scripts/install_aios_orchestrator_gateway.sh
scripts/report_orchestrator_event.py
scripts/rollback_aios_orchestrator_gateway.sh
scripts/verify_aios_orchestrator_gateway.sh
systemd/aios-orchestrator-gateway-poll.service
systemd/aios-orchestrator-gateway-poll.timer
systemd/aios-orchestrator-gateway.service
systemd/aios-orchestrator-gateway.sysusers
systemd/aios-orchestrator-gateway.tmpfiles
tests/test_aios_orchestrator_gateway.py
tests/test_aios_orchestrator_gateway_rollback.sh
workspace-agent/CENTRAL_AGENT_INSTRUCTIONS.md
workspace-agent/orchestrator-decision-action.openapi.yaml
openapi/aios-orchestrator-gateway.openapi.yaml
)
while IFS= read -r path; do SOURCE_FILES+=("$path"); done < <(
cd "$SOURCE_ROOT"
find services/aios_orchestrator_gateway -type f ! -path '*/__pycache__/*' ! -name '*.pyc' -print | sort
)
for path in "${SOURCE_FILES[@]}"; do
[[ -f "$SOURCE_ROOT/$path" && ! -L "$SOURCE_ROOT/$path" ]] || fail "INVALID_SOURCE_FILE_${path}"
done
SOURCE_DIGEST="$(
cd "$SOURCE_ROOT"
printf '%s\0' "${SOURCE_FILES[@]}" | sort -z | xargs -0 sha256sum \
| sha256sum \
| awk '{print $1}'
)"
[[ "$SOURCE_DIGEST" =~ ^[0-9a-f]{64}$ ]] || fail SOURCE_DIGEST_INVALID
RELEASE_DIR="$INSTALL_ROOT/releases/$SOURCE_DIGEST"
if [[ "$DRY_RUN" -eq 1 ]]; then
printf 'AIOS_ORCHESTRATOR_INSTALL_DRY_RUN=PASS digest=%s release=%s\n' \
"$SOURCE_DIGEST" "$RELEASE_DIR"
exit 0
fi
systemd-sysusers "$SOURCE_ROOT/systemd/aios-orchestrator-gateway.sysusers"
install -d -m 0755 "$INSTALL_ROOT/releases"
install -d -o root -g aios-orchestrator -m 0750 "$CONFIG_ROOT"
install -d -m 0700 "$STATE_ROOT"
systemd-tmpfiles --create "$SOURCE_ROOT/systemd/aios-orchestrator-gateway.tmpfiles"
if [[ ! -d "$RELEASE_DIR" ]]; then
TMP_RELEASE="$INSTALL_ROOT/releases/.tmp-$SOURCE_DIGEST-$$"
install -d -m 0755 "$TMP_RELEASE"
for path in "${SOURCE_FILES[@]}"; do
install -d -m 0755 "$TMP_RELEASE/$(dirname "$path")"
cp -a "$SOURCE_ROOT/$path" "$TMP_RELEASE/$path"
done
printf '%s\n' "$SOURCE_DIGEST" > "$TMP_RELEASE/SOURCE_DIGEST"
chmod -R go-w "$TMP_RELEASE"
mv "$TMP_RELEASE" "$RELEASE_DIR"
fi
PREVIOUS_TARGET=""
if [[ -L "$INSTALL_ROOT/current" ]]; then
PREVIOUS_TARGET="$(readlink -f "$INSTALL_ROOT/current")"
fi
install -m 0644 "$SOURCE_ROOT/systemd/aios-orchestrator-gateway.service" \
"$UNIT_ROOT/aios-orchestrator-gateway.service"
install -m 0644 "$SOURCE_ROOT/systemd/aios-orchestrator-gateway-poll.service" \
"$UNIT_ROOT/aios-orchestrator-gateway-poll.service"
install -m 0644 "$SOURCE_ROOT/systemd/aios-orchestrator-gateway-poll.timer" \
"$UNIT_ROOT/aios-orchestrator-gateway-poll.timer"
install -m 0644 "$SOURCE_ROOT/systemd/aios-orchestrator-gateway.sysusers" \
"$SYSUSERS_ROOT/aios-orchestrator-gateway.conf"
install -m 0644 "$SOURCE_ROOT/systemd/aios-orchestrator-gateway.tmpfiles" \
"$TMPFILES_ROOT/aios-orchestrator-gateway.conf"
if [[ ! -f "$CONFIG_ROOT/inbound.token" ]]; then
python3 - <<'PY' > "$CONFIG_ROOT/inbound.token"
import secrets, sys
sys.stdout.write(secrets.token_urlsafe(48))
PY
fi
[[ ! -L "$CONFIG_ROOT/inbound.token" ]] || fail INBOUND_TOKEN_SYMLINK
chown root:aios-orchestrator "$CONFIG_ROOT/inbound.token"
chmod 0640 "$CONFIG_ROOT/inbound.token"
if [[ ! -f "$CONFIG_ROOT/callback.token" ]]; then
python3 - <<'PY' > "$CONFIG_ROOT/callback.token"
import secrets, sys
sys.stdout.write(secrets.token_urlsafe(48))
PY
fi
[[ ! -L "$CONFIG_ROOT/callback.token" ]] || fail CALLBACK_TOKEN_SYMLINK
chown root:aios-orchestrator "$CONFIG_ROOT/callback.token"
chmod 0640 "$CONFIG_ROOT/callback.token"
# Historical installers appended one LF. Remove only that exact legacy byte in
# place; reject every other newline form without ever copying or printing data.
python3 - "$CONFIG_ROOT/config.json" "$CONFIG_ROOT/inbound.token" "$CONFIG_ROOT/callback.token" <<'PY'
import json, os, pathlib, stat, sys
config_path = pathlib.Path(sys.argv[1])
names = list(sys.argv[2:])
if config_path.is_file() and not config_path.is_symlink():
config = json.loads(config_path.read_text())
candidates = [
config.get("inbound_token_file"),
config.get("callback_token_file"),
(config.get("workspace_agent") or {}).get("access_token_file"),
(config.get("responses_fallback") or {}).get("api_key_file"),
(config.get("openclaw_sync") or {}).get("bearer_token_file"),
]
names.extend(name for name in candidates if isinstance(name, str) and name)
for name in dict.fromkeys(names):
path = pathlib.Path(name)
if not path.is_absolute() or path.is_symlink() or path.resolve() != path:
raise SystemExit("invalid token path")
fd = os.open(name, os.O_RDWR | getattr(os, "O_NOFOLLOW", 0))
try:
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_size <= 0 or info.st_size > 16384:
raise SystemExit("invalid token metadata")
data = os.read(fd, info.st_size)
if data.endswith(b"\n") and b"\n" not in data[:-1] and b"\r" not in data:
os.ftruncate(fd, info.st_size - 1)
os.fsync(fd)
elif b"\n" in data or b"\r" in data:
raise SystemExit("invalid token format")
finally:
os.close(fd)
PY
if [[ ! -f "$CONFIG_ROOT/config.json" ]]; then
install -m 0600 \
"$SOURCE_ROOT/config/aios-orchestrator-gateway.example.json" \
"$CONFIG_ROOT/config.json.example"
printf 'AIOS_ORCHESTRATOR_INSTALL=BLOCKED reason=CONFIGURATION_REQUIRED digest=%s\n' \
"$SOURCE_DIGEST"
exit 2
fi
chown root:aios-orchestrator "$CONFIG_ROOT/config.json"
chmod 0640 "$CONFIG_ROOT/config.json"
python3 -m json.tool "$CONFIG_ROOT/config.json" >/dev/null
AIOS_ORCHESTRATOR_CONFIG_OWNER_UID=0 \
AIOS_ORCHESTRATOR_SECRET_OWNER_UID=0 \
PYTHONPATH="$RELEASE_DIR" \
python3 - "$CONFIG_ROOT/config.json" <<'PY'
import sys
from pathlib import Path
from services.aios_orchestrator_gateway.config import Config
cfg = Config.from_file(Path(sys.argv[1]))
cfg.inbound_token()
cfg.callback_token()
if cfg.workspace_trigger_id:
cfg.workspace_token()
if cfg.responses_api_key_file:
cfg.responses_api_key()
if cfg.openclaw_sync_endpoint:
cfg.openclaw_sync_token()
print("CONFIG_VALIDATE=PASS")
PY
# Commit the release pointer only after every candidate/config/secret check has
# succeeded. A failed validation therefore leaves the running release intact.
ln -s "$RELEASE_DIR" "$INSTALL_ROOT/.current-new-$$"
mv -Tf "$INSTALL_ROOT/.current-new-$$" "$INSTALL_ROOT/current"
python3 - "$STATE_ROOT/install-receipt.json" "$SOURCE_DIGEST" "$RELEASE_DIR" "$PREVIOUS_TARGET" <<'PY'
import json, os, sys, tempfile
from pathlib import Path
path = Path(sys.argv[1])
data = {
"schema_version": 1,
"source_digest": sys.argv[2],
"current_release": sys.argv[3],
"previous_release": sys.argv[4] or None,
"last_rollback": None,
}
path.parent.mkdir(parents=True, exist_ok=True)
fd, temp = tempfile.mkstemp(prefix=".install-receipt-", dir=path.parent)
try:
with os.fdopen(fd, "w") as handle:
json.dump(data, handle, indent=2, sort_keys=True)
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
os.chmod(temp, 0o600)
os.replace(temp, path)
directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(directory)
finally:
os.close(directory)
finally:
try:
os.unlink(temp)
except FileNotFoundError:
pass
PY
chown root:root "$STATE_ROOT/install-receipt.json"
chmod 0600 "$STATE_ROOT/install-receipt.json"
systemctl daemon-reload
systemctl enable aios-orchestrator-gateway.service
systemctl enable aios-orchestrator-gateway-poll.timer
systemctl stop aios-orchestrator-gateway-poll.timer || true
systemctl stop aios-orchestrator-gateway-poll.service || true
rollback_activation() {
if [[ -n "$PREVIOUS_TARGET" && -d "$PREVIOUS_TARGET" ]]; then
bash "$RELEASE_DIR/scripts/rollback_aios_orchestrator_gateway.sh" || true
else
systemctl stop aios-orchestrator-gateway.service || true
fi
fail ACTIVATION_FAILED_ROLLED_BACK
}
systemctl restart aios-orchestrator-gateway.service || rollback_activation
python3 - <<'PY' || rollback_activation
import json, time, urllib.request
for _ in range(30):
try:
with urllib.request.urlopen("http://127.0.0.1:18920/healthz", timeout=1) as response:
body = json.loads(response.read(65536))
if response.status == 200 and body.get("status") == "active":
raise SystemExit(0)
except Exception:
pass
time.sleep(0.5)
raise SystemExit(1)
PY
systemctl start aios-orchestrator-gateway-poll.timer || rollback_activation
systemctl is-active --quiet aios-orchestrator-gateway.service
systemctl is-active --quiet aios-orchestrator-gateway-poll.timer
AIOS_ORCHESTRATOR_CONFIG_OWNER_UID=0 \
AIOS_ORCHESTRATOR_SECRET_OWNER_UID=0 \
PYTHONPATH="$INSTALL_ROOT/current" \
python3 "$INSTALL_ROOT/current/scripts/bootstrap_orchestration_loop.py" \
--config "$CONFIG_ROOT/config.json"
systemctl start aios-orchestrator-gateway-poll.service || rollback_activation
printf 'AIOS_ORCHESTRATOR_INSTALL=PASS digest=%s previous=%s\n' \
"$SOURCE_DIGEST" "${PREVIOUS_TARGET:-NONE}"