| 1 | [Unit] |
| 2 | Description=lchat bot sidecar for %i |
| 3 | After=network-online.target lchat@%i.service |
| 4 | Wants=network-online.target |
| 5 | |
| 6 | [Service] |
| 7 | # add-bot writes /data/%i-bot.env (0600) with LCHAT_BASE, the three agent invites, |
| 8 | # session paths, and (optionally) ANTHROPIC_API_KEY. |
| 9 | EnvironmentFile=-/data/%i-bot.env |
| 10 | # The sidecar is the SAME versioned Go binary as its instance: exec the per-host |
| 11 | # live symlink (/opt/lchat/live/%i → versions/<ver>/lchat) that lchat@%i.service |
| 12 | # runs, so the bot's code stays in lock-step with the app by construction. `bot run` |
| 13 | # with no positional args reads LCHAT_BASE + the LCHAT_*_SESSION paths from the |
| 14 | # EnvironmentFile above, running whichever identities are set (a full trio or an |
| 15 | # Eleven-only box both work). A deploy/update repoints that symlink and restarts this |
| 16 | # unit (deployHostVersion → restartBotSidecar), so the bot always tracks the version. |
| 17 | ExecStart=/opt/lchat/live/%i bot run |
| 18 | Restart=always |
| 19 | RestartSec=2 |
| 20 | |
| 21 | # --- hardening (see docs/instance-router.md § Hardening) --- |
| 22 | # Pure outbound WebSocket client: no listening socket, no capabilities. |
| 23 | User=lchat |
| 24 | Group=lchat |
| 25 | CapabilityBoundingSet= |
| 26 | NoNewPrivileges=yes |
| 27 | ProtectSystem=strict |
| 28 | ProtectHome=yes |
| 29 | # Writes only its persisted agent sessions under /data/%i-bot/. HOME lands in the |
| 30 | # private /tmp so nothing reaches for a real home dir. |
| 31 | ReadWritePaths=/data |
| 32 | Environment=HOME=/tmp |
| 33 | PrivateTmp=yes |
| 34 | PrivateDevices=yes |
| 35 | ProtectClock=yes |
| 36 | ProtectHostname=yes |
| 37 | ProtectKernelTunables=yes |
| 38 | ProtectKernelModules=yes |
| 39 | ProtectKernelLogs=yes |
| 40 | ProtectControlGroups=yes |
| 41 | ProtectProc=invisible |
| 42 | RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX |
| 43 | RestrictNamespaces=yes |
| 44 | RestrictRealtime=yes |
| 45 | RestrictSUIDSGID=yes |
| 46 | LockPersonality=yes |
| 47 | MemoryDenyWriteExecute=yes |
| 48 | SystemCallFilter=@system-service |
| 49 | SystemCallErrorNumber=EPERM |
| 50 | SystemCallArchitectures=native |
| 51 | MemoryMax=512M |
| 52 | TasksMax=512 |
| 53 | |
| 54 | [Install] |
| 55 | WantedBy=multi-user.target |
| 56 | |