From a3c44d53d18db5635ef4a006cbda9a85aada60bc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 4 Jun 2026 13:57:21 -0400 Subject: [PATCH] docs: document daemon audit helpers --- src/daemon/runtime-hints.ts | 5 +++++ src/daemon/runtime-parse.ts | 1 + src/daemon/runtime-paths.ts | 7 ++++++- src/daemon/schtasks-exec.ts | 3 +++ src/daemon/schtasks.ts | 6 ++++++ src/daemon/service-audit.ts | 4 ++++ src/daemon/service-env-plan.ts | 5 +++++ src/daemon/service-env-render-policy.ts | 3 +++ 8 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/daemon/runtime-hints.ts b/src/daemon/runtime-hints.ts index 389ab67be7b6..b8b8c984dd71 100644 --- a/src/daemon/runtime-hints.ts +++ b/src/daemon/runtime-hints.ts @@ -1,3 +1,4 @@ +/** Builds platform-specific log and start hints for daemon status output. */ import { toPosixPath } from "./output.js"; import { resolveGatewayRestartLogPath, resolveGatewaySupervisorLogPaths } from "./restart-logs.js"; @@ -16,6 +17,8 @@ export function buildPlatformRuntimeLogHints(params: { const env = { ...process.env, ...params.env }; if (platform === "darwin") { const logs = resolveGatewaySupervisorLogPaths(env, { platform }); + // Display launchd paths as POSIX-style paths even in cross-platform tests + // where mocked env values may carry Windows drive prefixes. return [ `Launchd stdout (if installed): ${toDarwinDisplayPath(logs.stdoutPath)}`, "Launchd stderr (if installed): suppressed", @@ -47,6 +50,8 @@ export function buildPlatformServiceStartHints(params: { }): string[] { const platform = params.platform ?? process.platform; const base = [params.installCommand, params.startCommand]; + // Native service-manager commands are supplemental hints; the OpenClaw + // commands stay first because they know the generated profile/env paths. switch (platform) { case "darwin": return [...base, `launchctl bootstrap gui/$UID ${params.launchAgentPlistPath}`]; diff --git a/src/daemon/runtime-parse.ts b/src/daemon/runtime-parse.ts index b05b33875f4f..ed7b92ec4515 100644 --- a/src/daemon/runtime-parse.ts +++ b/src/daemon/runtime-parse.ts @@ -1,3 +1,4 @@ +/** Parses daemon runtime command output into normalized key-value maps. */ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; /** Parses command output key-value lines using a caller-supplied separator. */ diff --git a/src/daemon/runtime-paths.ts b/src/daemon/runtime-paths.ts index bc70b1c711f5..c60a0bc496cf 100644 --- a/src/daemon/runtime-paths.ts +++ b/src/daemon/runtime-paths.ts @@ -1,3 +1,4 @@ +/** Selects stable Node runtime paths for daemon installs across platforms. */ import { execFile } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; @@ -44,6 +45,8 @@ function buildSystemNodeCandidates( env: Record, platform: NodeJS.Platform, ): string[] { + // Prefer system package-manager Node paths over shell-managed shims; daemons + // launch without interactive shell init files. if (platform === "darwin") { return [ "/opt/homebrew/bin/node", @@ -104,6 +107,7 @@ async function isVersionManagedRealNodePath( ): Promise { try { const realPath = await fs.realpath(nodePath); + // Symlinks in /usr/local/bin can resolve into version-manager trees. return isVersionManagedNodePath(realPath, platform); } catch { return false; @@ -218,7 +222,8 @@ export async function resolvePreferredNodePath(params: { if (!isVersionManagedNodePath(currentExecPath, platform)) { return stableCurrentPath; } - // Prefer system Node over a version-manager shim so daemon launch survives shell setup. + // Prefer system Node over a version-manager shim so daemon launch survives + // shell setup differences and package manager upgrades. const systemNode = await resolveSystemNodeInfo({ env: params.env, platform, diff --git a/src/daemon/schtasks-exec.ts b/src/daemon/schtasks-exec.ts index 959b7eb33a2c..ea25058eccc3 100644 --- a/src/daemon/schtasks-exec.ts +++ b/src/daemon/schtasks-exec.ts @@ -1,3 +1,4 @@ +/** Executes Windows Task Scheduler commands with daemon-friendly timeouts. */ import { runCommandWithTimeout } from "../process/exec.js"; const SCHTASKS_TIMEOUT_MS = 15_000; @@ -17,6 +18,8 @@ export async function execSchtasks( : result.termination === "no-output-timeout" ? `schtasks produced no output for ${SCHTASKS_NO_OUTPUT_TIMEOUT_MS}ms` : ""; + // schtasks can hang without output on some Windows hosts; convert both timeout + // modes into ordinary process-like failures for service fallback logic. return { stdout: result.stdout, stderr: result.stderr || timeoutDetail, diff --git a/src/daemon/schtasks.ts b/src/daemon/schtasks.ts index a7aa4063ad2b..1573f8608929 100644 --- a/src/daemon/schtasks.ts +++ b/src/daemon/schtasks.ts @@ -1,3 +1,4 @@ +/** Windows Task Scheduler installer, startup fallback, and lifecycle controls. */ import { spawn, spawnSync } from "node:child_process"; import fs from "node:fs/promises"; import os from "node:os"; @@ -43,6 +44,8 @@ function resolveTaskName(env: GatewayServiceEnv): string { } function shouldFallbackToStartupEntry(params: { code: number; detail: string }): boolean { + // Permission failures and hung schtasks calls can still be served by the + // per-user Startup folder fallback. return ( params.code === 1 || /(?:access is denied|acceso denegado)/i.test(params.detail) || @@ -93,6 +96,8 @@ function resolveStartupEntryPath(env: GatewayServiceEnv, extension?: "cmd" | "vb function resolveStartupEntryPaths(env: GatewayServiceEnv): string[] { const primaryPath = resolveStartupEntryPath(env); const legacyCmdPath = resolveStartupEntryPath(env, "cmd"); + // Hidden VBS launchers supersede cmd launchers, but uninstall must remove the + // legacy cmd path from older installs too. return uniqueStrings([primaryPath, legacyCmdPath]); } @@ -257,6 +262,7 @@ export async function readScheduledTaskCommand( if (lower.startsWith("set ")) { const assignment = parseCmdSetAssignment(line.slice(4)); if (assignment) { + // Generated cmd launchers inline service env before the final command. environment[assignment.key] = assignment.value; } continue; diff --git a/src/daemon/service-audit.ts b/src/daemon/service-audit.ts index 2828d36ffc34..15ddf83b7074 100644 --- a/src/daemon/service-audit.ts +++ b/src/daemon/service-audit.ts @@ -1,3 +1,4 @@ +/** Audits installed daemon service definitions for drift and repair candidates. */ import fs from "node:fs/promises"; import path from "node:path"; import { @@ -73,6 +74,7 @@ export const SERVICE_AUDIT_CODES = { systemdKillModeProcessOrNone: "systemd-kill-mode-process-or-none", } as const; +/** Returns whether audit issues require migrating a daemon to a stable Node runtime. */ export function needsNodeRuntimeMigration(issues: ServiceConfigIssue[]): boolean { return issues.some( (issue) => @@ -96,6 +98,8 @@ function parseSystemdUnit(content: string): { let restartSec: string | undefined; let killMode: string | undefined; + // Parse only unit keys relevant to service resilience; this is not a full + // systemd parser and intentionally ignores sections. for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim(); if (!line) { diff --git a/src/daemon/service-env-plan.ts b/src/daemon/service-env-plan.ts index 2ce80c95afbc..3ac61d686fb3 100644 --- a/src/daemon/service-env-plan.ts +++ b/src/daemon/service-env-plan.ts @@ -1,3 +1,4 @@ +/** Builds normalized environment plans for managed daemon service rendering. */ import { normalizeEnvVarKey } from "../infra/host-env-security.js"; import type { GatewayServiceEnvironmentValueSource } from "./service-types.js"; @@ -53,6 +54,8 @@ export function addServiceEnvPlanEntries( for (const [rawKey, rawValue] of Object.entries(entries)) { if (typeof rawValue !== "string" || !rawValue.trim()) { if (options.includeRawKeys) { + // Preserve explicit blank raw keys only when callers need round-trip + // visibility in generated service env. plan.environment[rawKey] = rawValue; plan.environmentValueSources[rawKey] = "inline"; } @@ -69,6 +72,8 @@ export function addServiceEnvPlanEntries( ? options.valueSource({ rawKey, normalizedKey }) : options.valueSource; plan.environmentValueSources[rawKey] = valueSource ?? "inline"; + // Last writer wins per normalized key so later, higher-priority env sources + // can decide render policy without scanning duplicate casing. plan.entriesByNormalizedKey.set(normalizedKey, { rawKey, normalizedKey, diff --git a/src/daemon/service-env-render-policy.ts b/src/daemon/service-env-render-policy.ts index 9defaae94c66..20160b370f86 100644 --- a/src/daemon/service-env-render-policy.ts +++ b/src/daemon/service-env-render-policy.ts @@ -1,3 +1,4 @@ +/** Applies platform render policy for managed daemon service environment values. */ import type { MutableServiceEnvPlan } from "./service-env-plan.js"; import { readManagedServiceEnvKeysFromEnvironment, @@ -38,6 +39,8 @@ export function applyManagedServiceEnvRenderPolicy(params: { if (entry.source !== "state-dotenv" || !managedKeys.has(entry.normalizedKey)) { continue; } + // launchd does not read shell dotenv files; inline only the managed dotenv + // keys declared for this service. params.plan.environment[entry.rawKey] = entry.value; params.plan.environmentValueSources[entry.rawKey] = "inline"; }