docs: document daemon audit helpers

This commit is contained in:
Peter Steinberger
2026-06-04 13:57:21 -04:00
parent feeaff20ab
commit a3c44d53d1
8 changed files with 33 additions and 1 deletions
+5
View File
@@ -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}`];
+1
View File
@@ -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. */
+6 -1
View File
@@ -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<string, string | undefined>,
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<boolean> {
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,
+3
View File
@@ -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,
+6
View File
@@ -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;
+4
View File
@@ -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) {
+5
View File
@@ -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,
+3
View File
@@ -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";
}