Files
cleveragents-core/tools/controller/deploy/systemd/cleveragents-controller-master.service
T
drew a19b609554 feat(controller): Phase 1l — systemd units + ops runbook
Deployment surface for the controller. Operators copy the unit files
+ env examples to /etc/, customize, and `systemctl enable --now`.

Layout (under tools/controller/deploy/):
- systemd/cleveragents-controller-master.service — master singleton.
  One unit per (owner, repo); plan v9 singleton constraint enforced
  by deployment.
- systemd/cleveragents-controller-worker@.service — worker template.
  `systemctl enable cleveragents-controller-worker@implementer-1`
  spins up one instance; scale by adding instances. Per-instance
  env override at /etc/cleveragents/worker.<inst>.env (optional)
  specialises CLEVERAGENTS_WORKER_ROLES per instance.
- systemd/master.env.example — every env var the master reads,
  documented. Copy to /etc/cleveragents/master.env, mode 0640.
- systemd/worker.env.example — every env var the workers read.
- RUNBOOK.md — operator guide:
  * Prereqs (Linux + systemd 245+, Python 3.13 + uv, Postgres 14+,
    OpenCode server, dedicated cleveragents user).
  * First-time setup (7 ordered steps from useradd to first PR).
  * Day-to-day ops (where logs live, healthy queries, role pool
    sizing, label-based pause, clean restart).
  * Incident response (6 named scenarios: STUCK workflows, MERGING
    retry exhaustion, no-OpenCode, dequeue starvation, DB loss,
    operator-unstick procedure).
  * Migration playbook (Phase 2: opt one PR in via label,
    monitor controller_events, escalate to full management).
  * Tunables cheat sheet covering all 8 env vars + their tradeoffs.
- README.md — index linking the above.

Hardening on both units: NoNewPrivileges, PrivateTmp,
ProtectSystem=strict, ProtectHome, narrow ReadWritePaths, kernel
+ control-group protections. RestartPreventExitStatus=2 prevents
systemd loop-restart on misconfiguration (exit 2 = bad env).

No code changes; verified via `systemd-analyze verify` (unit syntax
parses; only "venv path doesn't exist on dev host" warnings, which
are expected). Test suite: 569/569 pass, 0 regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:49:00 -04:00

59 lines
1.8 KiB
Desktop File

[Unit]
Description=cleveragents controller — master singleton (per repo)
Documentation=file:///etc/cleveragents/RUNBOOK.md
After=network-online.target postgresql.service opencode.service
Wants=network-online.target
# Master MUST be a singleton per (owner, repo). The systemd unit is the
# singleton enforcement (one unit instance per deployment).
[Service]
Type=simple
User=cleveragents
Group=cleveragents
WorkingDirectory=/opt/cleveragents-core
# Environment: required vars below. Override per-deployment by editing
# /etc/cleveragents/master.env (NOT this unit file).
EnvironmentFile=/etc/cleveragents/master.env
# --owner / --repo come from EnvironmentFile via $OWNER / $REPO so
# the same unit file works for every (owner, repo) deployment by
# swapping the env file.
ExecStart=/opt/cleveragents-core/.venv/bin/python -m tools.controller.master \
--owner=${OWNER} \
--repo=${REPO} \
--opencode-url=${CONTROLLER_OPENCODE_URL} \
--log-level=${CONTROLLER_LOG_LEVEL}
# Signal handling: __main__.py wires SIGTERM → stop_event (clean
# shutdown). Give it 30s before SIGKILL.
KillSignal=SIGTERM
TimeoutStopSec=30s
Restart=always
RestartSec=10s
# Don't loop-restart on misconfiguration (exit code 2 = bad env).
RestartPreventExitStatus=2
# Resource limits — pin file descriptors high enough for the DB pool
# + OpenCode HTTP connections + per-attempt subprocesses.
LimitNOFILE=8192
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/cleveragents /var/log/cleveragents
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
# Journald is the canonical log sink — read with:
# journalctl -u cleveragents-controller-master -f
StandardOutput=journal
StandardError=journal
SyslogIdentifier=cleveragents-master
[Install]
WantedBy=multi-user.target