Daemon: servicio del sistema
Responsabilidad
resolveGatewayService (resolveGatewayService:370-375) es la entrada unificada con la que OpenClaw instala el gateway como «servicio del sistema (daemon) residente»: en macOS es un LaunchAgent (gestionado por launchd), en Linux es una systemd user unit (gestionada por systemd --user), en Windows es una Scheduled Task (tarea registrada con schtasks disparada por login). Los comandos de cada plataforma se envuelven en la misma interfaz GatewayService (GatewayService:75-87): stage/install/uninstall/stop/restart/isLoaded/readCommand/readRuntime, la capa CLI no se preocupa por las diferencias de plataforma.
Lo que hace no es «correr el gateway» — el gateway en sí sigue siendo node openclaw.mjs gateway. La capa Daemon hace escribir ese comando en el manifiesto del servicio del sistema (plist/unit/XML) y dejar que el gestor de servicios del sistema lo levante, monitorice y reinicie automáticamente. Por eso la mayor parte del código del módulo daemon está en el rendering de plantillas plist/unit/XML y en la invocación de los CLI launchctl/systemctl/schtasks; el ciclo de vida real del proceso gateway lo controla el gestor de servicios del sistema.
Motivación de diseño
¿Por qué no directamente nohup openclaw gateway &?
- Auto-arranque en boot + reinicio tras crash:
KeepAlive=truede launchd,Restart=alwaysde systemd,LogonTriggerde schtasks son mecanismos a nivel sistema que levantan el gateway automáticamente al reiniciar la máquina o si el proceso cae inesperadamente.nohupsolo «ignora SIGHUP», en un reinicio de la máquina se pierde. - Configuración unificada + aislamiento de profile:
OPENCLAW_PROFILEpermite correr varias instancias de gateway en la misma máquina (por ejemplo, dev/prod separadas), yresolveGatewayLaunchAgentLabel(resolveGatewayLaunchAgentLabel:33-39) compone el profile en el service label —ai.openclaw.gateway(default) oai.openclaw.<profile>; systemd y schtasks añaden el sufijo correspondiente. Cada profile tiene su propio plist/unit/XML, sin interferencias. - future config guard:
withFutureConfigGuard(withFutureConfigGuard:336-362) invocaassertFutureConfigActionAllowedantes de cada operación de escritura (stage/install/uninstall/stop/restart); si detecta que la versión actual de OpenClaw es más nueva que la que escribió el servicio (o viceversa, config generada por versión futura), rechaza la reescritura — una red de seguridad para prevenir «un downgrade arranque y corrompa los archivos de servicio del nuevo formato». - Detección de repair:
collectGatewayServiceStartRepairIssues(collectGatewayServiceStartRepairIssues:138-172) chequea antes derestartelOPENCLAW_SERVICE_VERSIONdel servicio cargado, si el program path apunta a un directorio temporal (isTemporaryProgramPath) y si el archivo program todavía existe (isMissingProgramPath). Drift de versión o path inválido exige reinstalar en lugar de fingir un restart exitoso.
Archivos clave
GatewayService tipo:75-87— interfaz unificada con 8 métodos.GATEWAY_SERVICE_REGISTRY:294-334— tres adaptersdarwin/linux/win32, cada uno apuntando a un módulo de implementación por plataforma.withFutureConfigGuard:336-362— envuelve stage/install/uninstall/stop/restart, prepende el chequeoassertFutureConfigActionAllowed.resolveGatewayService:370-375— elige adapter porprocess.platform, plataforma desconocida devuelvecreateUnsupportedGatewayService(todos los métodos throwunsupported).isTemporaryProgramPath:115-136— detección de paths/tmp//var/tmp/os.tmpdir(), servicios que apunten a directorios temporales se marcan para repair.collectGatewayServiceStartRepairIssues:138-172— tres tipos de issues: drift deOPENCLAW_SERVICE_VERSION, path temporal, archivo ausente.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 naming:33-60—resolveGatewayLaunchAgentLabel/resolveGatewaySystemdServiceName/resolveGatewayWindowsTaskName, reglas de sufijo de profile.installLaunchAgent:1013-1030—writeLaunchAgentPlist+activateLaunchAgent, instalación en macOS.launchd constants:5-13—LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS=10,LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS=20,LAUNCH_AGENT_UMASK_DECIMAL=0o077(archivos owner-only).buildLaunchAgentPlist:267-295— render del plist:RunAtLoad=true/KeepAlive=true/ExitTimeOut=20/ThrottleInterval=10/Umask=0o077/StandardOutPath/StandardErrorPath.installSystemdService:1107-1131—writeSystemdUnit+activateSystemdService.buildSystemdUnit:52-101— tres secciones[Unit]/[Service]/[Install],Restart=always/RestartSec=5/TimeoutStopSec=30/OOMPolicy=continue/KillMode=control-group.readSystemdUserLingerStatus:28-50— despliegues headless necesitanloginctl enable-linger, aquí se lee el estado.buildScheduledTaskXml:139-192— XML de Windows Task Scheduler,LogonTrigger/LeastPrivilege/DisallowStartIfOnBatteries=false.installScheduledTask:1313-1325—writeScheduledTaskScript+activateScheduledTask.writeTaskXmlTempFile:194-203—schtasks /XMLexige UTF-16 LE BOM, Nodeutf16le+ BOMFFFEescrito a mano.GATEWAY_DIST_ENTRYPOINT_BASENAMES:5-22—index.js/index.mjs/entry.js/entry.mjs, elige entrypoint disponible desde el dist.
Flujo de datos
resolveGatewayService (resolveGatewayService:370) elige adapter desde el registro de plataforma:
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 envuelve todos los métodos de escritura con assertFutureConfigActionAllowed, una intercepción unificada de «chequea antes de escribir» — evita que una versión vieja de OpenClaw sobrescriba los archivos de servicio generados por una versión nueva y corrompa el formato.
La plantilla plist de launchd (buildLaunchAgentPlist:267) es la más compleja:
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>
`;Cuatro keys clave: RunAtLoad=true (arranca en carga), KeepAlive=true (reinicia ante cualquier salida), ExitTimeOut=20 (tras SIGTERM espera 20s antes de SIGKILL), ThrottleInterval=10 (protección de crash loop builtin de launchd, mínimo 10s entre arranques). Umask=0o077 hace que todos los archivos que el gateway crea sean owner-only por defecto — evita que group/other lean secrets. envXml embebe variables como OPENCLAW_STATE_DIR/OPENCLAW_PROFILE dentro del plist, así no hace falta un env-file separado.
La unit de systemd (buildSystemdUnit:68) usa tres secciones estándar:
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: no reiniciar en error de config, evita loop
"TimeoutStopSec=30",
"TimeoutStartSec=30",
"SuccessExitStatus=0 143", // 143 = SIGTERM cuenta como salida normal
"OOMPolicy=continue", // si un subproceso cae por OOM, el servicio principal sigue
"KillMode=control-group", // al reiniciar, mata todos los subprocesos juntos, evita huérfanos ACP worker
workingDirLine,
...environmentFileLines,
...envLines,
"",
"[Install]",
"WantedBy=default.target",
"",
].filter((line) => line !== null).join("\n");OOMPolicy=continue y KillMode=control-group son dos valores no default — el primero garantiza que un OOM de un subproceso no arrastre todo el gateway, el segundo garantiza que al reiniciar el gateway no queden workers ACP/agent huérfanos. StartLimitBurst=5/StartLimitIntervalSec=60 es la protección de crash loop builtin de systemd: si cae 5 veces consecutivas en 60s se detiene el reinicio (requiere intervención manual).
El XML de Windows schtasks (buildScheduledTaskXml:152) usa LogonTrigger (dispara al iniciar sesión), LeastPrivilege (sin admin), DisallowStartIfOnBatteries=false (corre también en portátil con batería). Un pozo oculto: schtasks /XML exige UTF-16 LE BOM (writeTaskXmlTempFile:194), la codificación utf16le de Node no añade BOM automáticamente, el código monta manualmente Buffer.from([0xff, 0xfe]) + body.
Límites y fallos
- Plataformas no soportadas degradan: todos los métodos de escritura de
createUnsupportedGatewayService(createUnsupportedGatewayService:275-292) hacenthrow createUnsupportedGatewayServiceError(),readCommanddevuelve null,readRuntimedevuelve{ status: "unknown", detail: ... }. Sistemas conprocess.platformfuera de la tabla (FreeBSD/OpenBSD, etc.) reciben este servicio degradado. - Detección de path temporal:
TEMP_PROGRAM_ROOTS = [os.tmpdir(), "/tmp", "/private/tmp", "/var/tmp"](TEMP_PROGRAM_ROOTS:115). Si el program path del servicio cargado apunta a uno de estos,collectGatewayServiceStartRepairIssuesmarca issue — el directorio temporal puede ser limpiado por el sistema, el servicio de repente no encuentra el binario. Este estado exige reinstalar, no un simple restart. - Protección contra drift de versión:
serviceVersion !== VERSIONtambién es issue (OPENCLAW_SERVICE_VERSION check:146-150).OPENCLAW_SERVICE_VERSIONse escribe como variable de entorno en el plist/unit/XML al instalar; en runtime se lee y se compara con la versión actual de OpenClaw — una inconsistencia indica que el servicio lo instaló una versión vieja, que podría referenciar binarios eliminados o un layout viejo de directorios. - launchd sin kickstart -k:
installLaunchAgent(avoid kickstart -k:1018-1020) comenta explícitamente quebootstrapya tiene RunAtLoad, no hace faltakickstart -k— en macOS VMs lentos,kickstart -kmanda SIGTERM al gateway recién arrancado, empujando el listener real de arranque más allá del deadline del health check. - launchd spawn throttle:
LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS = 10(LAUNCH_AGENT_THROTTLE_INTERVAL_SECONDS:8) es el default builtin de launchd, se escribe explícitamente en el plist para que un crash loop (crash loop) sea como mínimo cada 10s y no cada segundo.LAUNCH_AGENT_UMASK_DECIMAL = 0o077se renderiza explícitamente como decimal63— los campos enteros de plist de launchd no aceptan la sintaxis0o077. - systemd ExitStatus 78:
RestartPreventExitStatus=78(RestartPreventExitStatus:80) es EX_CONFIG — cuando la config está rota, el proceso del gateway sale con 78 y systemd no reinicia. Evita el loop de «config rota, systemd reinicia sin parar y cae». - systemd linger:
readSystemdUserLingerStatus(readSystemdUserLingerStatus:28) leeloginctl show-user <user> -p Linger. En despliegues headless (sin sesión SSH activa), la instancia systemd user no arranca y el gateway no corre, hace faltaloginctl enable-linger <user>— el módulo daemon lee el estado pero no lo modifica, lo deja a decisión del administrador. - schtasks UTF-16 BOM:
writeTaskXmlTempFile(writeTaskXmlTempFile:194) debe escribir el BOMFFFE, si no, Task Scheduler MMC rechaza la importación en algunos locales. Es un detalle de compatibilidad de la toolchain de Windows, con comentario explícito en el código. - schtasks upgrade Change + XML:
updateExistingScheduledTask(updateExistingScheduledTask:1042) primero hace/Changepara modificar/TR(task command), luego/Create /F /XMLpara sobrescribir todo el XML — garantiza que una task instalada por una versión vieja al actualizar herede los nuevos campos como<DisallowStartIfOnBatteries>false</...>(#59299). Ambos pasos son best-effort, si/Changefalla se conservan los settings viejos y no se pierde la task.
Resumen
El módulo Daemon es el wrapper delgado de OpenClaw frente a tres gestores de servicio del sistema (launchd/systemd/schtasks): no corre el gateway, solo escribe plist/unit/XML + invoca el CLI del sistema para cargar/descargar/reiniciar. resolveGatewayService elige adapter por process.platform, withFutureConfigGuard protege las operaciones de escritura contra desalineación de versiones, collectGatewayServiceStartRepairIssues detecta tres tipos de issues (drift de versión, path inválido, directorio temporal) antes de un restart. La lógica concreta de arranque del gateway en Núcleo del gateway; despliegue en contenedores (sin gestor de servicios del sistema) en Despliegue con Docker y Fly; cambio de profile y variables de entorno del servicio en openclaw.json.