From f6d5e2c19b4d1dff108fd8c568cdfc2f40fb32e3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 1 Aug 2026 08:50:10 -0700 Subject: [PATCH] fix(systemd): parse all inline environment assignments --- src/daemon/systemd-unit.ts | 2 +- src/daemon/systemd.test.ts | 33 +++++++++++++++++++++++++++++++++ src/daemon/systemd.ts | 4 +--- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/daemon/systemd-unit.ts b/src/daemon/systemd-unit.ts index 6802ba7aef63..c1a61fd4480f 100644 --- a/src/daemon/systemd-unit.ts +++ b/src/daemon/systemd-unit.ts @@ -104,7 +104,7 @@ export function parseSystemdExecStart(value: string): string[] { return splitArgsPreservingQuotes(value, { escapeMode: "backslash" }); } -export function parseSystemdEnvAssignment(raw: string): { key: string; value: string } | null { +function parseSystemdEnvAssignment(raw: string): { key: string; value: string } | null { const trimmed = raw.trim(); if (!trimmed) { return null; diff --git a/src/daemon/systemd.test.ts b/src/daemon/systemd.test.ts index bdbf55a6ba34..5f196b972e54 100644 --- a/src/daemon/systemd.test.ts +++ b/src/daemon/systemd.test.ts @@ -1265,6 +1265,39 @@ describe("readSystemdServiceExecStart", () => { expect(command?.environmentValueSources?.OPENCLAW_GATEWAY_TOKEN).toBe("inline-and-file"); }); + it("reads all inline assignments in order before EnvironmentFile overrides", async () => { + mockReadGatewayServiceFile( + [ + "[Service]", + "ExecStart=/usr/bin/openclaw gateway run", + 'Environment=PLAIN=first "QUOTED=value with spaces" REPEATED=first', + "Environment='REPEATED=last value' INLINE_FILE=inline", + "EnvironmentFile=%h/.openclaw/.env", + ], + { + [`${TEST_SERVICE_HOME}/.openclaw/.env`]: ["INLINE_FILE=file", "FILE_ONLY=from-file"].join( + "\n", + ), + }, + ); + + const command = await readSystemdServiceExecStart({ HOME: TEST_SERVICE_HOME }); + expect(command?.environment).toEqual({ + PLAIN: "first", + QUOTED: "value with spaces", + REPEATED: "last value", + INLINE_FILE: "file", + FILE_ONLY: "from-file", + }); + expect(command?.environmentValueSources).toEqual({ + PLAIN: "inline", + QUOTED: "inline", + REPEATED: "inline", + INLINE_FILE: "inline-and-file", + FILE_ONLY: "file", + }); + }); + it("ignores missing optional EnvironmentFile entries", async () => { await expectExecStartWithoutEnvironment("EnvironmentFile=-%h/.openclaw/missing.env"); }); diff --git a/src/daemon/systemd.ts b/src/daemon/systemd.ts index 477c7604f200..63665bdc09d4 100644 --- a/src/daemon/systemd.ts +++ b/src/daemon/systemd.ts @@ -58,7 +58,6 @@ import { } from "./systemd-unavailable.js"; import { buildSystemdUnit, - parseSystemdEnvAssignment, parseSystemdEnvAssignments, parseSystemdExecStart, renderSystemdEnvAssignment, @@ -328,8 +327,7 @@ export async function readSystemdServiceExecStart( workingDirectory = line.slice("WorkingDirectory=".length).trim(); } else if (line.startsWith("Environment=")) { const raw = line.slice("Environment=".length).trim(); - const parsed = parseSystemdEnvAssignment(raw); - if (parsed) { + for (const parsed of parseSystemdEnvAssignments(raw)) { inlineEnvironment[parsed.key] = parsed.value; } } else if (line.startsWith("EnvironmentFile=")) {