mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 06:59:56 +00:00
Summary: - This replacement PR marks the Linux node daemon gateway token as file-backed, writes it to `node.systemd.env`, sanitizes and migrates systemd env artifacts, adds regression tests, and updates the changelog. - Reproducibility: yes. from source inspection: current `main` copies `OPENCLAW_GATEWAY_TOKEN` into the node s ... e-backed before systemd rendering. I did not run a local live systemd install during this read-only review. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(systemd): scrub single-quoted env tokens - PR branch already contained follow-up commit before automerge: [Fix] Keep node systemd tokens out of unit files Validation: - ClawSweeper review passed for headf626b66c09. - Required merge gates passed before the squash merge. Prepared head SHA:f626b66c09Review: https://github.com/openclaw/openclaw/pull/84815#issuecomment-4505012292 Co-authored-by: samzong <samzong.lu@gmail.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
156 lines
4.4 KiB
TypeScript
156 lines
4.4 KiB
TypeScript
import { splitArgsPreservingQuotes } from "./arg-split.js";
|
|
import type { GatewayServiceRenderArgs } from "./service-types.js";
|
|
|
|
const SYSTEMD_LINE_BREAKS = /[\r\n]/;
|
|
|
|
function assertNoSystemdLineBreaks(value: string, label: string): void {
|
|
if (SYSTEMD_LINE_BREAKS.test(value)) {
|
|
throw new Error(`${label} cannot contain CR or LF characters.`);
|
|
}
|
|
}
|
|
|
|
function systemdEscapeArg(value: string): string {
|
|
assertNoSystemdLineBreaks(value, "Systemd unit values");
|
|
if (!/[\s"\\]/.test(value)) {
|
|
return value;
|
|
}
|
|
return `"${value.replace(/\\\\/g, "\\\\\\\\").replace(/"/g, '\\\\"')}"`;
|
|
}
|
|
|
|
function renderEnvLines(env: Record<string, string | undefined> | undefined): string[] {
|
|
if (!env) {
|
|
return [];
|
|
}
|
|
const entries = Object.entries(env).filter(
|
|
([, value]) => typeof value === "string" && value.trim(),
|
|
);
|
|
if (entries.length === 0) {
|
|
return [];
|
|
}
|
|
return entries.map(([key, value]) => {
|
|
const rawValue = value ?? "";
|
|
assertNoSystemdLineBreaks(key, "Systemd environment variable names");
|
|
assertNoSystemdLineBreaks(rawValue, "Systemd environment variable values");
|
|
return `Environment=${systemdEscapeArg(`${key}=${rawValue.trim()}`)}`;
|
|
});
|
|
}
|
|
|
|
function renderEnvironmentFileLines(environmentFiles: string[] | undefined): string[] {
|
|
if (!environmentFiles) {
|
|
return [];
|
|
}
|
|
return environmentFiles
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean)
|
|
.map((entry) => {
|
|
assertNoSystemdLineBreaks(entry, "Systemd EnvironmentFile values");
|
|
return `EnvironmentFile=-${systemdEscapeArg(entry)}`;
|
|
});
|
|
}
|
|
|
|
export function buildSystemdUnit({
|
|
description,
|
|
programArguments,
|
|
workingDirectory,
|
|
environment,
|
|
environmentFiles,
|
|
}: GatewayServiceRenderArgs): string {
|
|
const execStart = programArguments.map(systemdEscapeArg).join(" ");
|
|
const descriptionValue = description?.trim() || "OpenClaw Gateway";
|
|
assertNoSystemdLineBreaks(descriptionValue, "Systemd Description");
|
|
const descriptionLine = `Description=${descriptionValue}`;
|
|
const workingDirLine = workingDirectory
|
|
? `WorkingDirectory=${systemdEscapeArg(workingDirectory)}`
|
|
: null;
|
|
const envLines = renderEnvLines(environment);
|
|
const environmentFileLines = renderEnvironmentFileLines(environmentFiles);
|
|
return [
|
|
"[Unit]",
|
|
descriptionLine,
|
|
"After=network-online.target",
|
|
"Wants=network-online.target",
|
|
"StartLimitBurst=5",
|
|
"StartLimitIntervalSec=60",
|
|
"",
|
|
"[Service]",
|
|
`ExecStart=${execStart}`,
|
|
"Restart=always",
|
|
"RestartSec=5",
|
|
"RestartPreventExitStatus=78",
|
|
"TimeoutStopSec=30",
|
|
"TimeoutStartSec=30",
|
|
"SuccessExitStatus=0 143",
|
|
// Keep service children in the same lifecycle so restarts do not leave
|
|
// orphan ACP/runtime workers behind.
|
|
"KillMode=control-group",
|
|
workingDirLine,
|
|
...environmentFileLines,
|
|
...envLines,
|
|
"",
|
|
"[Install]",
|
|
"WantedBy=default.target",
|
|
"",
|
|
]
|
|
.filter((line) => line !== null)
|
|
.join("\n");
|
|
}
|
|
|
|
export function parseSystemdExecStart(value: string): string[] {
|
|
return splitArgsPreservingQuotes(value, { escapeMode: "backslash" });
|
|
}
|
|
|
|
export function parseSystemdEnvAssignment(raw: string): { key: string; value: string } | null {
|
|
const trimmed = raw.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
|
|
const unquoted = (() => {
|
|
const quote = trimmed[0];
|
|
if (!((quote === '"' || quote === "'") && trimmed.endsWith(quote))) {
|
|
return trimmed;
|
|
}
|
|
let out = "";
|
|
let escapeNext = false;
|
|
for (const ch of trimmed.slice(1, -1)) {
|
|
if (escapeNext) {
|
|
out += ch;
|
|
escapeNext = false;
|
|
continue;
|
|
}
|
|
if (ch === "\\\\") {
|
|
escapeNext = true;
|
|
continue;
|
|
}
|
|
out += ch;
|
|
}
|
|
return out;
|
|
})();
|
|
|
|
const eq = unquoted.indexOf("=");
|
|
if (eq <= 0) {
|
|
return null;
|
|
}
|
|
const key = unquoted.slice(0, eq).trim();
|
|
if (!key) {
|
|
return null;
|
|
}
|
|
const value = unquoted.slice(eq + 1);
|
|
return { key, value };
|
|
}
|
|
|
|
export function parseSystemdEnvAssignments(raw: string): Array<{ key: string; value: string }> {
|
|
return splitArgsPreservingQuotes(raw, {
|
|
escapeMode: "backslash",
|
|
quoteChars: ['"', "'"],
|
|
quoteStart: "item-start",
|
|
}).flatMap((entry) => {
|
|
const parsed = parseSystemdEnvAssignment(entry);
|
|
return parsed ? [parsed] : [];
|
|
});
|
|
}
|
|
|
|
export function renderSystemdEnvAssignment(key: string, value: string): string {
|
|
return systemdEscapeArg(`${key}=${value}`);
|
|
}
|