From c8429cfde1131c571e9395d9de38d3e9fa0ffd75 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Wed, 29 Apr 2026 22:15:15 -0400 Subject: [PATCH] fix: write logs to /paperclip/instances/default/data/run-logs/ to match server PVC layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.2.1 introduced filesystem-tail log delivery with buildPodLogPath() returning /paperclip/instances/default/run-logs/... but the paperclip server creates and tails from /paperclip/instances/default/data/run-logs/ on the shared PVC. The missing /data/ segment meant: 1. The init container's mkdir -p /paperclip/instances/... ran in a directory busybox UID 1000 can't write to — it's the init container's ephemeral rootfs, since the PVC is only mounted in the main container. Init exited 1, the && short-circuited, and the prompt copy never happened. Job failed with "Init container 'write-prompt' failed with exit code 1". 2. Even if the mkdir had worked, the main container's tee would have written to a path the server doesn't tail. Fix: drop the misplaced mkdir from both init container variants and correct buildPodLogPath() to include /data/. The directory already exists on the PVC because the paperclip server creates it; both containers run as UID 1000 with fsGroup 1000, so the main container's tee writes to the pre-existing path with no setup needed. Bump to 0.2.2. Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- src/server/job-manifest.test.ts | 15 ++++++--------- src/server/job-manifest.ts | 6 +++--- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index b0faac0..6e666d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paperclip-adapter-claude-k8s", - "version": "0.2.0", + "version": "0.2.2", "description": "Paperclip adapter plugin that runs Claude Code agents as Kubernetes Jobs", "license": "MIT", "repository": { diff --git a/src/server/job-manifest.test.ts b/src/server/job-manifest.test.ts index 12bf8a7..6020000 100644 --- a/src/server/job-manifest.test.ts +++ b/src/server/job-manifest.test.ts @@ -301,8 +301,7 @@ describe("buildJobManifest", () => { const init = job.spec?.template?.spec?.initContainers?.[0]; expect(init?.command?.[0]).toBe("sh"); expect(init?.command?.[1]).toBe("-c"); - expect(init?.command?.[2]).toContain("mkdir -p /paperclip/instances/default/run-logs/"); - expect(init?.command?.[2]).toContain("printf '%s' \"$PROMPT_CONTENT\" > /tmp/prompt/prompt.txt"); + expect(init?.command?.[2]).toBe("printf '%s' \"$PROMPT_CONTENT\" > /tmp/prompt/prompt.txt"); }); it("write-prompt mounts prompt volume", () => { @@ -808,28 +807,26 @@ describe("buildJobManifest", () => { const { job } = buildJobManifest({ ctx, selfPod }); const cmd = job.spec?.template?.spec?.containers[0]?.command?.[2] ?? ""; expect(cmd).toContain("| tee"); - expect(cmd).toContain("/paperclip/instances/default/run-logs/"); + expect(cmd).toContain("/paperclip/instances/default/data/run-logs/"); }); it("podLogPath is returned from buildJobManifest", () => { const result = buildJobManifest({ ctx, selfPod }); expect(result.podLogPath).toBe( - "/paperclip/instances/default/run-logs/co1/agent-abc/run-abc12345.pod.ndjson", + "/paperclip/instances/default/data/run-logs/co1/agent-abc/run-abc12345.pod.ndjson", ); }); it("buildPodLogPath returns correctly formatted path", () => { expect(buildPodLogPath("co1", "agent-abc", "run-abc12345")).toBe( - "/paperclip/instances/default/run-logs/co1/agent-abc/run-abc12345.pod.ndjson", + "/paperclip/instances/default/data/run-logs/co1/agent-abc/run-abc12345.pod.ndjson", ); }); - it("init container creates log directory", () => { + it("init container does not create log directory (server pre-creates it on shared PVC)", () => { const { job } = buildJobManifest({ ctx, selfPod }); const initCmd = job.spec?.template?.spec?.initContainers?.[0]?.command; - expect(initCmd?.[0]).toBe("sh"); - expect(initCmd?.[1]).toBe("-c"); - expect(initCmd?.[2]).toContain("mkdir -p /paperclip/instances/default/run-logs/"); + expect(initCmd?.[2]).not.toContain("mkdir -p /paperclip"); }); it("sanitizes companyId with / to valid path component for log path", () => { diff --git a/src/server/job-manifest.ts b/src/server/job-manifest.ts index 1970edf..2469ce0 100644 --- a/src/server/job-manifest.ts +++ b/src/server/job-manifest.ts @@ -23,7 +23,7 @@ function sanitizeForK8sPath(value: string): string { } export function buildPodLogPath(companyId: string, agentId: string, runId: string): string { - return `/paperclip/instances/default/run-logs/${companyId}/${agentId}/${runId}.pod.ndjson`; + return `/paperclip/instances/default/data/run-logs/${companyId}/${agentId}/${runId}.pod.ndjson`; } /** Prompts above this size (bytes) are staged via a Secret instead of an @@ -504,7 +504,7 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult { name: "write-prompt", image: "busybox:1.36", imagePullPolicy: "IfNotPresent", - command: ["sh", "-c", `mkdir -p /paperclip/instances/default/run-logs/${agent.companyId}/${agent.id} && cp /tmp/prompt-secret/prompt.txt /tmp/prompt/prompt.txt`], + command: ["sh", "-c", "cp /tmp/prompt-secret/prompt.txt /tmp/prompt/prompt.txt"], volumeMounts: [ { name: "prompt", mountPath: "/tmp/prompt" }, { name: "prompt-secret", mountPath: "/tmp/prompt-secret", readOnly: true }, @@ -519,7 +519,7 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult { name: "write-prompt", image: "busybox:1.36", imagePullPolicy: "IfNotPresent", - command: ["sh", "-c", `mkdir -p /paperclip/instances/default/run-logs/${agent.companyId}/${agent.id} && printf '%s' "$PROMPT_CONTENT" > /tmp/prompt/prompt.txt`], + command: ["sh", "-c", `printf '%s' "$PROMPT_CONTENT" > /tmp/prompt/prompt.txt`], env: [{ name: "PROMPT_CONTENT", value: prompt }], volumeMounts: [{ name: "prompt", mountPath: "/tmp/prompt" }], securityContext,