diff --git a/cli/src/__tests__/service-manager.test.ts b/cli/src/__tests__/service-manager.test.ts index f5f98b5ee15..f64a3125521 100644 --- a/cli/src/__tests__/service-manager.test.ts +++ b/cli/src/__tests__/service-manager.test.ts @@ -48,6 +48,14 @@ describe("service definition generation", () => { expect(unit).toContain('Environment="PAPERCLIP_HOME=/home/$$USER/%%i/.paperclip"'); }); + it.each([ + ["instanceId", { instanceId: "team-a\nExecStartPre=/tmp/attack", shimPath: "/home/alice/.local/bin/paperclipai", homeDir: "/home/alice/.paperclip" }], + ["shimPath", { instanceId: "team-a", shimPath: "/home/alice/bin/paperclipai\r\nExecStartPre=/tmp/attack", homeDir: "/home/alice/.paperclip" }], + ["homeDir", { instanceId: "team-a", shimPath: "/home/alice/.local/bin/paperclipai", homeDir: "/home/alice/.paperclip\nEnvironment=ATTACK=1" }], + ])("rejects line breaks in the systemd %s", (_field, input) => { + expect(() => renderSystemdUnit(input)).toThrow("Systemd service values must not contain line breaks"); + }); + it("generates a launchd agent with keepalive and instance logs", () => { const plist = renderLaunchdPlist({ instanceId: "team-a", shimPath: "/Users/alice/.local/bin/paperclipai", homeDir: "/Users/alice/.paperclip", stdoutPath: "/Users/alice/.paperclip/instances/team-a/logs/service.log", stderrPath: "/Users/alice/.paperclip/instances/team-a/logs/service.err.log" }); expect(plist).toContain("ing.paperclip.paperclipai.team-a"); diff --git a/cli/src/services/service-manager.ts b/cli/src/services/service-manager.ts index 57c608a642b..f284afa59b4 100644 --- a/cli/src/services/service-manager.ts +++ b/cli/src/services/service-manager.ts @@ -53,6 +53,9 @@ export const defaultCommandRunner: CommandRunner = async (command, args, options }; function escapeSystemd(value: string): string { + if (/\r|\n/.test(value)) { + throw new Error("Systemd service values must not contain line breaks"); + } return value .replaceAll("\\", "\\\\") .replaceAll('"', '\\"') @@ -82,7 +85,7 @@ export function launchdServiceName(instanceId: string): string { export function renderSystemdUnit(input: { instanceId: string; shimPath: string; homeDir: string }): string { return `[Unit] -Description=Paperclip AI (${input.instanceId}) +Description=Paperclip AI (${escapeSystemd(input.instanceId)}) After=network.target StartLimitIntervalSec=60 StartLimitBurst=5