[Unit] Description=lchat webhook-ingest sidecar for %i After=network-online.target lchat@%i.service Wants=network-online.target [Service] # add-hooks writes /data/%i-hooks.env (0600) with LCHAT_BASE, the service # identity's one-time agent invite, the session path, and the socket path. EnvironmentFile=-/data/%i-hooks.env # Same versioned Go binary as the instance (the per-host live symlink), so the # ingest sidecar's code stays in lock-step with the app by construction — # exactly the lchat-bot@ arrangement. `hooks run` reads everything from the # EnvironmentFile above. ExecStart=/opt/lchat/live/%i hooks run Restart=always RestartSec=2 # --- hardening (see docs/instance-router.md § Hardening) --- # Unlike the bot sidecar this process DOES listen — on a unix socket under # /run/lchat that only the router dials; it still binds no TCP port and needs # no capabilities. It is the inverse of the bot: inbound requests come in, # encrypted posts go out, and nothing here can read a conversation. User=lchat Group=lchat CapabilityBoundingSet= NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes # Writes its service-identity session under /data/%i-hooks/ and its listening # socket under /run/lchat. HOME lands in the private /tmp. ReadWritePaths=/data /run/lchat 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=256M TasksMax=256 [Install] WantedBy=multi-user.target