[Unit] Description=lchat bot sidecar for %i After=network-online.target lchat@%i.service Wants=network-online.target [Service] # add-bot writes /data/%i-bot.env (0600) with LCHAT_BASE, the three agent invites, # session paths, and (optionally) ANTHROPIC_API_KEY. EnvironmentFile=-/data/%i-bot.env # The sidecar is the SAME versioned Go binary as its instance: exec the per-host # live symlink (/opt/lchat/live/%i → versions//lchat) that lchat@%i.service # runs, so the bot's code stays in lock-step with the app by construction. `bot run` # with no positional args reads LCHAT_BASE + the LCHAT_*_SESSION paths from the # EnvironmentFile above, running whichever identities are set (a full trio or an # Eleven-only box both work). A deploy/update repoints that symlink and restarts this # unit (deployHostVersion → restartBotSidecar), so the bot always tracks the version. ExecStart=/opt/lchat/live/%i bot run Restart=always RestartSec=2 # --- hardening (see docs/instance-router.md § Hardening) --- # Pure outbound WebSocket client: no listening socket, no capabilities. User=lchat Group=lchat CapabilityBoundingSet= NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes # Writes only its persisted agent sessions under /data/%i-bot/. HOME lands in the # private /tmp so nothing reaches for a real home dir. ReadWritePaths=/data Environment=HOME=/tmp PrivateTmp=yes PrivateDevices=yes ProtectClock=yes ProtectHostname=yes ProtectKernelTunables=yes ProtectKernelModules=yes ProtectKernelLogs=yes ProtectControlGroups=yes ProtectProc=invisible RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX RestrictNamespaces=yes RestrictRealtime=yes RestrictSUIDSGID=yes LockPersonality=yes MemoryDenyWriteExecute=yes SystemCallFilter=@system-service SystemCallErrorNumber=EPERM SystemCallArchitectures=native MemoryMax=512M TasksMax=512 [Install] WantedBy=multi-user.target