feat(@projects/@claire): ✨ add systemd agent deployment
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
3057182717
commit
2a2e99ab2b
4 changed files with 145 additions and 0 deletions
38
app.manifest.yaml
Normal file
38
app.manifest.yaml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
name: claire-agent
|
||||
description: Headless Claire peer-node daemon — sync + supervisor + telemetry
|
||||
type: service
|
||||
version: 0.1.0
|
||||
|
||||
# The orchestrator (claire serve) + tray run on plum; this manifest covers the
|
||||
# Linux peer-node agent that runs on the worker hosts.
|
||||
platforms:
|
||||
apricot:
|
||||
os: linux
|
||||
service:
|
||||
type: systemd-user
|
||||
unit: claire-agent.service
|
||||
bind: "127.0.0.1"
|
||||
default_port: 8766
|
||||
deploy:
|
||||
script: scripts/deploy-agent.sh
|
||||
args: ["apricot"]
|
||||
status:
|
||||
command: "ssh apricot 'systemctl --user is-active claire-agent.service'"
|
||||
type: process
|
||||
logs:
|
||||
command: "ssh apricot 'journalctl --user -u claire-agent.service -f'"
|
||||
black:
|
||||
os: linux
|
||||
service:
|
||||
type: systemd-user
|
||||
unit: claire-agent.service
|
||||
bind: "127.0.0.1"
|
||||
default_port: 8766
|
||||
deploy:
|
||||
script: scripts/deploy-agent.sh
|
||||
args: ["black"]
|
||||
status:
|
||||
command: "ssh black 'systemctl --user is-active claire-agent.service'"
|
||||
type: process
|
||||
logs:
|
||||
command: "ssh black 'journalctl --user -u claire-agent.service -f'"
|
||||
20
deployments/systemd/claire-agent.service
Normal file
20
deployments/systemd/claire-agent.service
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[Unit]
|
||||
Description=Claire peer-node daemon (sync + supervisor + telemetry)
|
||||
Documentation=https://forge.black.lan/lilith/claire
|
||||
# Clock sync is a hard precondition: peer sync uses HMAC with a 300s skew
|
||||
# window, so a drifting clock breaks auth. Wait for time-sync before start.
|
||||
After=time-sync.target network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# Use the venv binary directly — no dependency on a ~/.local/bin symlink.
|
||||
ExecStart=%h/Code/@projects/@claire/.venv/bin/claire agent run
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=claire-agent
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
67
scripts/deploy-agent.sh
Executable file
67
scripts/deploy-agent.sh
Executable file
|
|
@ -0,0 +1,67 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Deploy the headless `claire agent` peer node to a Linux host (apricot|black).
|
||||
# Runs FROM plum. Idempotent. Code + systemd unit + peer config (injects plum's
|
||||
# sync_secret so the host can sync to plum).
|
||||
#
|
||||
# scripts/deploy-agent.sh apricot
|
||||
#
|
||||
# Requires: `remote-run` on PATH (~/Code/@scripts/session-tools), ssh access,
|
||||
# uv + python3.12+ on the remote, and NTP-synced clocks (HMAC skew window 300s).
|
||||
set -euo pipefail
|
||||
|
||||
HOST="${1:?usage: deploy-agent.sh <host>}"
|
||||
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
REMOTE_DIR="Code/@projects/@claire" # relative to remote $HOME
|
||||
PLUM_TOML="${CLAIRE_TOML:-$HOME/.config/claire/claire.toml}"
|
||||
|
||||
say() { printf '\033[1;35m▸\033[0m %s\n' "$*"; }
|
||||
|
||||
# Plum's bind + sync_secret — the peer host signs sync requests to plum with it.
|
||||
read -r PLUM_URL PLUM_SECRET <<EOF
|
||||
$("$SRC/.venv/bin/python" - "$PLUM_TOML" <<'PY'
|
||||
import sys, tomllib, pathlib
|
||||
c = tomllib.loads(pathlib.Path(sys.argv[1]).read_text())
|
||||
web = c.get("web", {})
|
||||
host = web.get("host", "127.0.0.1")
|
||||
if host in ("0.0.0.0", "::", ""):
|
||||
host = "127.0.0.1"
|
||||
print(f"http://{host}:{web.get('port', 8765)}", c.get("sync_secret", ""))
|
||||
PY
|
||||
)
|
||||
EOF
|
||||
[ -n "$PLUM_SECRET" ] || { echo "ERROR: plum has no sync_secret in $PLUM_TOML" >&2; exit 1; }
|
||||
say "plum peer URL = $PLUM_URL (secret ${PLUM_SECRET:0:4}…)"
|
||||
|
||||
say "[$HOST] reachability + clock"
|
||||
ssh -o ConnectTimeout=8 -o BatchMode=yes "$HOST" 'true' \
|
||||
|| { echo "ERROR: cannot ssh $HOST" >&2; exit 1; }
|
||||
ssh "$HOST" 'timedatectl show -p NTPSynchronized --value 2>/dev/null || echo unknown'
|
||||
|
||||
say "[$HOST] rsync source"
|
||||
ssh "$HOST" "mkdir -p ~/$REMOTE_DIR"
|
||||
rsync -az --delete \
|
||||
--exclude='.venv/' --exclude='.git/' --exclude='__pycache__/' \
|
||||
--exclude='*.pyc' --exclude='.pytest_cache/' --exclude='.ruff_cache/' \
|
||||
--exclude='claire.toml' \
|
||||
--exclude='src/claire/web/app/node_modules/' \
|
||||
--exclude='src/claire/web/app/dist/' \
|
||||
"$SRC/" "${HOST}:${REMOTE_DIR}/"
|
||||
|
||||
say "[$HOST] install (uv) + init"
|
||||
remote-run "$HOST" "cd ~/$REMOTE_DIR && { [ -d .venv ] || uv venv; } && uv pip install -e . && .venv/bin/claire init"
|
||||
|
||||
say "[$HOST] configure peer (idempotent — points this host at plum)"
|
||||
remote-run "$HOST" "cd ~/$REMOTE_DIR && .venv/bin/claire agent add-peer --url '$PLUM_URL' --secret '$PLUM_SECRET'"
|
||||
|
||||
say "[$HOST] install + enable systemd --user unit"
|
||||
remote-run "$HOST" "
|
||||
mkdir -p ~/.config/systemd/user
|
||||
cp ~/$REMOTE_DIR/deployments/systemd/claire-agent.service ~/.config/systemd/user/
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now claire-agent.service
|
||||
loginctl enable-linger \$(whoami) 2>/dev/null || true
|
||||
sleep 2
|
||||
systemctl --user --no-pager status claire-agent.service | head -5
|
||||
"
|
||||
say "[$HOST] done."
|
||||
|
|
@ -150,6 +150,26 @@ def agent_run(
|
|||
)
|
||||
|
||||
|
||||
@agent_app.command("add-peer")
|
||||
def agent_add_peer(
|
||||
url: Annotated[str, typer.Option("--url", help="Peer base URL, e.g. http://10.9.0.3:8767")],
|
||||
secret: Annotated[str | None, typer.Option("--secret", help="HMAC secret to sign requests TO this peer (= the peer's sync_secret)")] = None,
|
||||
) -> None:
|
||||
"""Idempotently add a sync peer to this host's config + ensure [agent] is set.
|
||||
|
||||
Used by deploy-agent.sh to point a worker host at plum. Re-running with the
|
||||
same URL is a no-op (updates the secret if it changed).
|
||||
"""
|
||||
from .config import PeerConfig, _serialize, default_config_path
|
||||
|
||||
cfg = load_or_init()
|
||||
peers = [p for p in cfg.peers if p.url != url] # replace existing entry for url
|
||||
peers.append(PeerConfig(url=url, secret=secret))
|
||||
cfg = cfg.model_copy(update={"peers": peers})
|
||||
default_config_path().write_text(_serialize(cfg), encoding="utf-8")
|
||||
console.print(f"[green]✓[/green] peers: {[p.url for p in peers]}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SPA build management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue