Daemon: service système
Responsabilités
resolveGatewayService (resolveGatewayService:370-375) est l'entrée unifiée d'OpenClaw pour installer la gateway en « service système résident (daemon) »: LaunchAgent sur macOS (géré par launchd), unit systemd user sur Linux (géré par systemd --user), Scheduled Task sur Windows (tâche enregistrée via schtasks au login). Les commandes des trois plateformes sont encapsulées dans une même interface GatewayService (GatewayService:75-87): stage/install/uninstall/stop/restart/isLoaded/readCommand/readRuntime; la couche CLI ne se soucie pas des différences de plateforme.
Ce qu'elle gère n'est pas « faire tourner la gateway » — la gateway elle-même reste node openclaw.mjs gateway. La couche Daemon fait l'écriture de cette commande dans le manifeste du service système (plist/unit/XML), puis laisse le gestionnaire de services système la lancer, surveiller, redémarrer automatiquement selon ses règles. Donc le module daemon comporte beaucoup de code pour le rendu de templates plist/unit/XML et les appels CLI launchctl/systemctl/schtasks; le cycle de vie réel du process gateway est contrôlé par le gestionnaire de services système.
Motivation de conception
Pourquoi ne pas simplement nohup openclaw gateway &?
- Démarrage auto + restart sur crash:
KeepAlive=truede launchd,Restart=alwaysde systemd,LogonTriggerde schtasks sont des mécanismes système; au redémarrage de la machine ou à la sortie inattendue du process OpenClaw, ils relancent automatiquement.nohupne fait qu'« ignorer SIGHUP », inutile au redémarrage machine. - Config unifiée + isolation profile:
OPENCLAW_PROFILEpermet à une même machine de faire tourner plusieurs gateways (par exemple dev/prod séparés),resolveGatewayLaunchAgentLabel(resolveGatewayLaunchAgentLabel:33-39) encode le profile dans le label du service —ai.openclaw.gateway(default) ouai.openclaw.<profile>, systemd et schtasks ajoutent aussi le suffixe. Chaque profile a son propre plist/unit/XML, sans interférence. - future config guard:
withFutureConfigGuard(withFutureConfigGuard:336-362) appelleassertFutureConfigActionAllowedavant chaque opération d'écriture stage/install/uninstall/stop/restart; si la version OpenClaw courante est plus récente que celle qui a écrit la config du service (ou inversement, la config a été générée par une version future), refuse la réécriture — filet de sécurité contre « un démarrage dégradé qui écrase un fichier de service au format neuf ». - Détection repair:
collectGatewayServiceStartRepairIssues(collectGatewayServiceStartRepairIssues:138-172) avantrestartvérifie leOPENCLAW_SERVICE_VERSIONdu service chargé, si le program path pointe vers un répertoire temporaire (isTemporaryProgramPath), si le fichier program existe encore (isMissingProgramPath). Une dérive de version ou un chemin périmé exige une réinstallation, plutôt que de prétendre un restart réussi.
Fichiers clés
GatewayService type:75-87— Interface unifiée à 8 méthodes.GATEWAY_SERVICE_REGISTRY:294-334— Trois adaptersdarwin/linux/win32, chacun pointant vers un module de plateforme.withFutureConfigGuard:336-362— Enveloppe stage/install/uninstall/stop/restart, appelleassertFutureConfigActionAllowedavant.resolveGatewayService:370-375— Sélectionne l'adapter selonprocess.platform, plateforme inconnue renvoiecreateUnsupportedGatewayService(toutes les méthodes throwunsupported).isTemporaryProgramPath:115-136— Détection de chemin/tmp//var/tmp/os.tmpdir(), un service pointant vers un répertoire temporaire est marqué à réparer.collectGatewayServiceStartRepairIssues:138-172— Trois types d'issues start repair: dériveOPENCLAW_SERVICE_VERSION, chemin temporaire, fichier manquant.service label constants:5-17—GATEWAY_LAUNCH_AGENT_LABEL="ai.openclaw.gateway",GATEWAY_SYSTEMD_SERVICE_NAME="openclaw-gateway",GATEWAY_WINDOWS_TASK_NAME="OpenClaw Gateway", legacyclawdbot-gateway.profile-aware names:33-60—resolveGatewayLaunchAgentLabel/resolveGatewaySystemdServiceName/resolveGatewayWindowsTaskName, règles de suffixe par profile.installLaunchAgent:1013-1030—writeLaunchAgentPlist+activateLaunchAgent, chargement macOS.launchd constants:5-13—LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS=10,LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS=20,LAUNCH_AGENT_UMASK_DECIMAL=0o077(fichiers owner-only).buildLaunchAgentPlist:267-295— Rendu plist:RunAtLoad=true/KeepAlive=true/ExitTimeOut=20/ThrottleInterval=10/Umask=0o077/StandardOutPath/StandardErrorPath.installSystemdService:1107-1131—writeSystemdUnit+activateSystemdService.buildSystemdUnit:52-101— Trois sections[Unit]/[Service]/[Install],Restart=always/RestartSec=5/TimeoutStopSec=30/OOMPolicy=continue/KillMode=control-group.readSystemdUserLingerStatus:28-50— Déploiement headless exigeloginctl enable-linger, ici on lit le statut.buildScheduledTaskXml:139-192— XML Task Scheduler Windows,LogonTrigger/LeastPrivilege/DisallowStartIfOnBatteries=false.installScheduledTask:1313-1325—writeScheduledTaskScript+activateScheduledTask.writeTaskXmlTempFile:194-203—schtasks /XMLrequiert UTF-16 LE BOM, Nodeutf16le+ écriture manuelle du BOMFFFE.GATEWAY_DIST_ENTRYPOINT_BASENAMES:5-22—index.js/index.mjs/entry.js/entry.mjs, sélectionne l'entrypoint disponible dans dist.
Flux de données
resolveGatewayService (resolveGatewayService:370) sélectionne l'adapter via le registry plateforme:
const GATEWAY_SERVICE_REGISTRY: Record<SupportedGatewayServicePlatform, GatewayService> = {
darwin: {
label: "LaunchAgent",
loadedText: "loaded",
notLoadedText: "not loaded",
stage: ignoreServiceWriteResult(stageLaunchAgent),
install: ignoreServiceWriteResult(installLaunchAgent),
uninstall: uninstallLaunchAgent,
stop: stopLaunchAgent,
restart: restartLaunchAgent,
isLoaded: isLaunchAgentLoaded,
readCommand: readLaunchAgentProgramArguments,
readRuntime: readLaunchAgentRuntime,
},
linux: { label: "systemd user", /* ...systemd impl */ },
win32: { label: "Scheduled Task", /* ...schtasks impl */ },
};
export function resolveGatewayService(): GatewayService {
if (isSupportedGatewayServicePlatform(process.platform)) {
return withFutureConfigGuard(GATEWAY_SERVICE_REGISTRY[process.platform]);
}
return createUnsupportedGatewayService();
}withFutureConfigGuard enveloppe toutes les méthodes d'écriture d'un assertFutureConfigActionAllowed — interception unifiée « vérifier avant d'écrire » pour éviter qu'une vieille version OpenClaw ne réécrive un fichier de service au format généré par une version plus récente.
Le template plist de launchd (buildLaunchAgentPlist:267) est le plus complexe:
return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${plistEscape(label)}</string>
${commentXml}
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ExitTimeOut</key><integer>${LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS}</integer>
<key>ProcessType</key><string>${LAUNCH_AGENT_PROCESS_TYPE}</string>
<key>ThrottleInterval</key><integer>${LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS}</integer>
<key>Umask</key><integer>${LAUNCH_AGENT_UMASK_DECIMAL}</integer>
<key>ProgramArguments</key>
<array>${argsXml}
</array>
${workingDirXml}
<key>StandardInPath</key><string>/dev/null</string>
<key>StandardOutPath</key><string>${plistEscape(stdoutPath)}</string>
<key>StandardErrorPath</key><string>${plistEscape(stderrPath)}</string>${envXml}
</dict>
</plist>
`;Quatre clés critiques: RunAtLoad=true (démarre immédiatement au chargement), KeepAlive=true (relance à toute sortie), ExitTimeOut=20 (attend 20s après SIGTERM avant SIGKILL), ThrottleInterval=10 (protection launchd contre crash loop, au moins 10s entre deux démarrages). Umask=0o077 fait que tous les fichiers créés par la gateway sont owner-only par défaut — pour éviter que group/other ne lisent les secrets. envXml embarque OPENCLAW_STATE_DIR/OPENCLAW_PROFILE etc. dans le plist, sans fichier env séparé.
L'unit systemd (buildSystemdUnit:68) suit la structure standard trois sections:
return [
"[Unit]",
descriptionLine,
"After=network-online.target",
"Wants=network-online.target",
"StartLimitBurst=5",
"StartLimitIntervalSec=60",
"",
"[Service]",
`ExecStart=${execStart}`,
"Restart=always",
"RestartSec=5",
"RestartPreventExitStatus=78", // EX_CONFIG: ne pas restart sur erreur de config, anti-boucle
"TimeoutStopSec=30",
"TimeoutStartSec=30",
"SuccessExitStatus=0 143", // 143 = terminaison par SIGTERM compte comme sortie normale
"OOMPolicy=continue", // sous-process OOM tué ne stoppe pas le service principal
"KillMode=control-group", // au restart, KILL tous les sous-process, anti orphelins ACP worker
workingDirLine,
...environmentFileLines,
...envLines,
"",
"[Install]",
"WantedBy=default.target",
"",
].filter((line) => line !== null).join("\n");OOMPolicy=continue et KillMode=control-group sont deux valeurs non-default — le premier garantit qu'un sous-process OOM ne fait pas tomber toute la gateway, le second garantit qu'au restart gateway pas d'orphelins ACP/agent worker. StartLimitBurst=5/StartLimitIntervalSec=60 est la protection systemd contre crash loop, après 5 crashes consécutifs en 60s, systemd arrête de relancer (intervention humaine requise).
Le XML schtasks Windows (buildScheduledTaskXml:152) utilise LogonTrigger (déclenchement au login utilisateur), LeastPrivilege (pas admin), DisallowStartIfOnBatteries=false (tourne aussi sur batterie). Piège caché: schtasks /XML requiert UTF-16 LE BOM (writeTaskXmlTempFile:194); l'encodage utf16le de Node n'ajoute pas automatiquement de BOM, le code concatène explicitement Buffer.from([0xff, 0xfe]) + body.
Limites et modes d'échec
- Plateformes non supportées dégradées:
createUnsupportedGatewayService(createUnsupportedGatewayService:275-292) fait que toutes les méthodes d'écriturethrow createUnsupportedGatewayServiceError(),readCommandretourne null,readRuntimeretourne{ status: "unknown", detail: ... }. FreeBSD/OpenBSD etc. dontprocess.platformn'est pas dans la table obtiennent ce service dégradé. - Détection de chemin temporaire:
TEMP_PROGRAM_ROOTS = [os.tmpdir(), "/tmp", "/private/tmp", "/var/tmp"](TEMP_PROGRAM_ROOTS:115). Si le program path d'un service chargé pointe vers l'un de ces répertoires,collectGatewayServiceStartRepairIssueslève un issue — le répertoire temporaire peut être nettoyé par le système, et le service perd soudainement son exécutable. Cet état exige une réinstall plutôt qu'un simple restart. - Protection dérive version:
serviceVersion !== VERSIONcompte aussi comme issue (OPENCLAW_SERVICE_VERSION check:146-150).OPENCLAW_SERVICE_VERSIONest écrite dans l'environnement du plist/unit/XML à l'install du service, relue au runtime et comparée à la version OpenClaw courante — une incohérence indique que le service a été installé par une ancienne version, qui peut référencer un chemin binaire supprimé ou une vieille layout de working directory. - launchd sans kickstart -k:
installLaunchAgent(avoid kickstart -k:1018-1020) commente explicitement quebootstrapfait déjà RunAtLoad, ne pas appelerkickstart -k— sur macOS VM lent,kickstart -kSIGTERM la gateway qui vient de démarrer, repoussant le démarrage du vrai listener au-delà du deadline de health check du setup. - launchd spawn throttle:
LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS = 10(LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS:8) est la valeur par défaut launchd, écrite explicitement dans le plist pour qu'un crash loop soit espacé d'au moins 10s, plutôt que d'être relancé chaque seconde.LAUNCH_AGENT_UMASK_DECIMAL = 0o077est rendu explicitement en décimal63— le champ integer du plist launchd n'accepte pas0o077. - systemd ExitStatus 78:
RestartPreventExitStatus=78(RestartPreventExitStatus:80) est EX_CONFIG — quand la gateway quitte avec le code 78 pour erreur de config, systemd ne restart pas. Cela évite la boucle « config cassée, systemd restart crash à l'infini ». - systemd linger:
readSystemdUserLingerStatus(readSystemdUserLingerStatus:28) litloginctl show-user <user> -p Linger. En déploiement headless (sans session SSH active), l'instance systemd user ne démarre pas et la gateway ne se lance pas; il fautloginctl enable-linger <user>— le module daemon lit l'état sans modifier, laisse l'admin décider. - schtasks UTF-16 BOM:
writeTaskXmlTempFile(writeTaskXmlTempFile:194) doit écrire le BOMFFFE, sinon Task Scheduler MMC refuse l'import dans certaines locales. Détail de compat outil Windows, commenté explicitement dans le code. - schtasks upgrade Change + XML:
updateExistingScheduledTask(updateExistingScheduledTask:1042) fait d'abord/Changepour modifier/TR(task command), puis/Create /F /XMLpour écraser tout le XML — cela garantit qu'au passage de version une task installée par ancienne version puisse hériter des nouveaux champs comme<DisallowStartIfOnBatteries>false</...>(#59299). Les deux étapes sont best-effort,/Changeéchoué conserve les vieux réglages, ne perd pas la task. cap_dropetno-new-privilegesen conteneur: le mode daemon physique n'offre pas ce niveau d'isolation — cf. Docker et Fly déploiement pour la défense en profondeur des conteneurs.
Résumé
Le module Daemon est une fine enveloppe d'OpenClaw sur les trois gestionnaires de services système (launchd/systemd/schtasks): il ne fait pas tourner la gateway, il ne fait qu'écrire plist/unit/XML + appeler les CLI système pour charger/décharger/restart. resolveGatewayService choisit l'adapter selon process.platform, withFutureConfigGuard protège les écritures contre les décalages de version, collectGatewayServiceStartRepairIssues détecte avant restart trois types de problèmes: dérive version, chemin périmé, répertoire temporaire. La logique de démarrage de la gateway elle-même dans cœur de la Gateway, le déploiement conteneurisé (sans gestionnaire de services système) dans déploiement Docker et Fly, le switch profile et les variables d'environnement du service dans openclaw.json.