Skip to content

Commit e427262

Browse files
clawsweeper[bot]samzongTakhoffman
authored
[Fix] Keep node systemd tokens out of unit files (#84815)
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 head f626b66. - Required merge gates passed before the squash merge. Prepared head SHA: f626b66 Review: #84815 (comment) 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>
1 parent 9ec9fbf commit e427262

8 files changed

Lines changed: 539 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
1111
### Fixes
1212

1313
- Media/audio: skip empty structured sherpa-onnx transcripts instead of treating the raw JSON payload as spoken text. (#84667) Thanks @TurboTheTurtle.
14+
- Node/Linux: keep `OPENCLAW_GATEWAY_TOKEN` out of generated systemd unit files by writing node service token values to a node-specific env file. (#84408)
1415
- Memory-core/dreaming: reuse stable narrative subagent session keys per workspace and phase while keeping per-run idempotency and bounded cleanup, so stale `dreaming-narrative-*` sessions do not accumulate. Fixes #68252, #69187, and #70402. (#70464) Thanks @chiyouYCH.
1516
- Trajectory/support: tolerate partial skill snapshot entries when building support metadata so rejected skill path scans no longer abort trajectory capture. (#71185) Thanks @lukeboyett.
1617
- Agents/Pi: disable the embedded pi-coding-agent runtime auto-retry so OpenClaw's own retry and failover loop does not replay failed tool calls through a nested SDK retry. Fixes #73781. (#74434) Thanks @yelog.

src/cli/node-cli/daemon.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) {
136136
const tlsFingerprint =
137137
normalizeOptionalString(opts.tlsFingerprint) || config?.gateway?.tlsFingerprint;
138138
const tls = Boolean(opts.tls) || Boolean(tlsFingerprint) || Boolean(config?.gateway?.tls);
139-
const { programArguments, workingDirectory, environment, description } =
139+
const { programArguments, workingDirectory, environment, environmentValueSources, description } =
140140
await buildNodeInstallPlan({
141141
env: process.env,
142142
host,
@@ -168,6 +168,7 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) {
168168
programArguments,
169169
workingDirectory,
170170
environment,
171+
environmentValueSources,
171172
description,
172173
});
173174
},

src/commands/node-daemon-install-helpers.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ describe("buildNodeInstallPlan", () => {
5555
expect(plan.environment).toEqual({
5656
OPENCLAW_SERVICE_VERSION: "2026.3.22",
5757
});
58+
expect(plan.environmentValueSources).toEqual({
59+
OPENCLAW_GATEWAY_TOKEN: "file",
60+
});
5861
expect(mocks.resolvePreferredNodePath).not.toHaveBeenCalled();
5962
expect(mocks.buildNodeServiceEnvironment).toHaveBeenCalledWith({
6063
env: {},
@@ -90,4 +93,33 @@ describe("buildNodeInstallPlan", () => {
9093
extraPathDirs: undefined,
9194
});
9295
});
96+
97+
it("marks node gateway tokens as file-backed service env", async () => {
98+
mocks.resolveNodeProgramArguments.mockResolvedValue({
99+
programArguments: ["node", "node-host"],
100+
workingDirectory: "/Users/me",
101+
});
102+
mocks.resolveSystemNodeInfo.mockResolvedValue({
103+
path: "/usr/bin/node",
104+
version: "22.0.0",
105+
supported: true,
106+
});
107+
mocks.renderSystemNodeWarning.mockReturnValue(undefined);
108+
mocks.buildNodeServiceEnvironment.mockReturnValue({
109+
OPENCLAW_GATEWAY_TOKEN: "node-token",
110+
OPENCLAW_SERVICE_VERSION: "2026.3.22",
111+
});
112+
113+
const plan = await buildNodeInstallPlan({
114+
env: { OPENCLAW_GATEWAY_TOKEN: "node-token" },
115+
host: "127.0.0.1",
116+
port: 18789,
117+
runtime: "node",
118+
});
119+
120+
expect(plan.environment.OPENCLAW_GATEWAY_TOKEN).toBe("node-token");
121+
expect(plan.environmentValueSources).toEqual({
122+
OPENCLAW_GATEWAY_TOKEN: "file",
123+
});
124+
});
93125
});

src/commands/node-daemon-install-helpers.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { formatNodeServiceDescription } from "../daemon/constants.js";
22
import { resolveNodeProgramArguments } from "../daemon/program-args.js";
33
import { buildNodeServiceEnvironment } from "../daemon/service-env.js";
4+
import type { GatewayServiceEnvironmentValueSource } from "../daemon/service-types.js";
45
import {
56
emitDaemonInstallRuntimeWarning,
67
resolveDaemonInstallRuntimeInputs,
@@ -13,9 +14,19 @@ type NodeInstallPlan = {
1314
programArguments: string[];
1415
workingDirectory?: string;
1516
environment: Record<string, string | undefined>;
17+
environmentValueSources?: Record<string, GatewayServiceEnvironmentValueSource | undefined>;
1618
description?: string;
1719
};
1820

21+
function buildNodeInstallEnvironmentValueSources(): Record<
22+
string,
23+
GatewayServiceEnvironmentValueSource | undefined
24+
> {
25+
return {
26+
OPENCLAW_GATEWAY_TOKEN: "file",
27+
};
28+
}
29+
1930
export async function buildNodeInstallPlan(params: {
2031
env: Record<string, string | undefined>;
2132
host: string;
@@ -65,5 +76,11 @@ export async function buildNodeInstallPlan(params: {
6576
version: environment.OPENCLAW_SERVICE_VERSION,
6677
});
6778

68-
return { programArguments, workingDirectory, environment, description };
79+
return {
80+
programArguments,
81+
workingDirectory,
82+
environment,
83+
environmentValueSources: buildNodeInstallEnvironmentValueSources(),
84+
description,
85+
};
6986
}

src/daemon/arg-split.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
type ArgSplitEscapeMode = "none" | "backslash" | "backslash-quote-only";
2+
type ArgSplitQuoteChar = '"' | "'";
3+
type ArgSplitQuoteStart = "anywhere" | "item-start";
24

35
export function splitArgsPreservingQuotes(
46
value: string,
5-
options?: { escapeMode?: ArgSplitEscapeMode },
7+
options?: {
8+
escapeMode?: ArgSplitEscapeMode;
9+
quoteChars?: readonly ArgSplitQuoteChar[];
10+
quoteStart?: ArgSplitQuoteStart;
11+
},
612
): string[] {
713
const args: string[] = [];
814
let current = "";
9-
let inQuotes = false;
15+
let quoteChar: ArgSplitQuoteChar | null = null;
1016
const escapeMode = options?.escapeMode ?? "none";
17+
const quoteChars = new Set<ArgSplitQuoteChar>(options?.quoteChars ?? ['"']);
18+
const quoteStart = options?.quoteStart ?? "anywhere";
1119

1220
for (let i = 0; i < value.length; i++) {
1321
const char = value[i];
@@ -28,11 +36,18 @@ export function splitArgsPreservingQuotes(
2836
i++;
2937
continue;
3038
}
31-
if (char === '"') {
32-
inQuotes = !inQuotes;
33-
continue;
39+
if (quoteChars.has(char as ArgSplitQuoteChar)) {
40+
if (quoteChar === char) {
41+
quoteChar = null;
42+
continue;
43+
}
44+
const canOpenQuote = quoteStart === "anywhere" || current.length === 0;
45+
if (!quoteChar && canOpenQuote) {
46+
quoteChar = char as ArgSplitQuoteChar;
47+
continue;
48+
}
3449
}
35-
if (!inQuotes && /\s/.test(char)) {
50+
if (!quoteChar && /\s/.test(char)) {
3651
if (current) {
3752
args.push(current);
3853
current = "";

src/daemon/systemd-unit.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ export function parseSystemdEnvAssignment(raw: string): { key: string; value: st
106106
}
107107

108108
const unquoted = (() => {
109-
if (!(trimmed.startsWith('"') && trimmed.endsWith('"'))) {
109+
const quote = trimmed[0];
110+
if (!((quote === '"' || quote === "'") && trimmed.endsWith(quote))) {
110111
return trimmed;
111112
}
112113
let out = "";
@@ -137,3 +138,18 @@ export function parseSystemdEnvAssignment(raw: string): { key: string; value: st
137138
const value = unquoted.slice(eq + 1);
138139
return { key, value };
139140
}
141+
142+
export function parseSystemdEnvAssignments(raw: string): Array<{ key: string; value: string }> {
143+
return splitArgsPreservingQuotes(raw, {
144+
escapeMode: "backslash",
145+
quoteChars: ['"', "'"],
146+
quoteStart: "item-start",
147+
}).flatMap((entry) => {
148+
const parsed = parseSystemdEnvAssignment(entry);
149+
return parsed ? [parsed] : [];
150+
});
151+
}
152+
153+
export function renderSystemdEnvAssignment(key: string, value: string): string {
154+
return systemdEscapeArg(`${key}=${value}`);
155+
}

0 commit comments

Comments
 (0)