From ba6707545ee50e08ffd854757e70b46a3562726c Mon Sep 17 00:00:00 2001 From: Scott Sunarto Date: Mon, 24 Aug 2026 23:35:12 -0700 Subject: [PATCH 1/2] test(agent-proxy): pin unquoted systemd directive values (repro for #66) --- plugins/agent-proxy/test/core-process.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/agent-proxy/test/core-process.test.ts b/plugins/agent-proxy/test/core-process.test.ts index 84dd95b..2501a63 100644 --- a/plugins/agent-proxy/test/core-process.test.ts +++ b/plugins/agent-proxy/test/core-process.test.ts @@ -360,6 +360,17 @@ test("renders and parses a persistent user systemd service", () => { assert.match(unit, /Restart=always/); assert.match(unit, /UMask=0077/); assert.match(unit, /ExecStart="\/home\/test\/Agent Proxy\/proxy" "--config"/); + assert.match(unit, /^WorkingDirectory=\/home\/test\/Agent Proxy$/m); + assert.match(unit, /^StandardOutput=append:\/home\/test\/Agent Proxy\/core\.log$/m); + assert.match(unit, /^StandardError=append:\/home\/test\/Agent Proxy\/core\.log$/m); + const percentUnit = renderSystemdUserUnit({ + label: "com.example.proxy", + binPath: "/home/test/100% sure/proxy", + configPath: "/home/test/100% sure/config.yaml", + logPath: "/home/test/100% sure/core.log", + }); + assert.match(percentUnit, /^WorkingDirectory=\/home\/test\/100%% sure$/m); + assert.match(percentUnit, /^StandardOutput=append:\/home\/test\/100%% sure\/core\.log$/m); assert.deepEqual( parseSystemctlShow( systemctlOutput({ From 0262d74ac8408729032bfd054946108efab59499 Mon Sep 17 00:00:00 2001 From: Scott Sunarto Date: Mon, 24 Aug 2026 23:36:55 -0700 Subject: [PATCH 2/2] fix(agent-proxy): emit systemd path directives unquoted systemd only defines quoting for command lines such as ExecStart=. A quoted WorkingDirectory= is a fatal unit error on Linux, and quoted StandardOutput=/StandardError= values are silently ignored, so the service never loaded and logs went to the journal. Keep %-specifier escaping, drop the quotes for plain assignment values. Fixes #66. --- .changeset/systemd-unit-unquoted-paths.md | 5 +++++ plugins/agent-proxy/lib/core-process.ts | 14 ++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 .changeset/systemd-unit-unquoted-paths.md diff --git a/.changeset/systemd-unit-unquoted-paths.md b/.changeset/systemd-unit-unquoted-paths.md new file mode 100644 index 0000000..0351494 --- /dev/null +++ b/.changeset/systemd-unit-unquoted-paths.md @@ -0,0 +1,5 @@ +--- +"@smsunarto/bb-plugin-agent-proxy": patch +--- + +Emit `WorkingDirectory=`, `StandardOutput=`, and `StandardError=` unquoted in the generated systemd user unit. systemd rejects a quoted `WorkingDirectory=` as a fatal unit error, so the core service never loaded on Linux, and quoted output directives silently sent core logs to the journal instead of `core.log`. diff --git a/plugins/agent-proxy/lib/core-process.ts b/plugins/agent-proxy/lib/core-process.ts index 69b368f..b6b501c 100644 --- a/plugins/agent-proxy/lib/core-process.ts +++ b/plugins/agent-proxy/lib/core-process.ts @@ -500,10 +500,16 @@ interface SystemdJobInfo { exitCode: number | null; } -function systemdQuote(value: string): string { +function systemdValue(value: string): string { if (/\r|\n|\0/.test(value)) throw new Error("systemd service values cannot contain control characters"); - return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%")}"`; + return value.replaceAll("%", "%%"); +} + +// Quoting is only defined for command-line directives such as ExecStart=; +// plain assignments like WorkingDirectory= take their value literally. +function systemdQuote(value: string): string { + return `"${systemdValue(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`; } export function renderSystemdUserUnit(options: { @@ -513,7 +519,7 @@ export function renderSystemdUserUnit(options: { logPath: string; }): string { const command = [options.binPath, "--config", options.configPath].map(systemdQuote).join(" "); - const logTarget = systemdQuote(`append:${options.logPath}`); + const logTarget = systemdValue(`append:${options.logPath}`); return `[Unit] Description=Agent Proxy (${options.label}) After=network-online.target @@ -522,7 +528,7 @@ Wants=network-online.target [Service] Type=simple ExecStart=${command} -WorkingDirectory=${systemdQuote(dirname(options.configPath))} +WorkingDirectory=${systemdValue(dirname(options.configPath))} UMask=0077 Restart=always RestartSec=2