From 1eccb71213207b72377dd0f666c2d90035999130 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 13 May 2026 14:47:48 -0500 Subject: [PATCH] fix(plugin): guard kubernetes upload edge cases --- .../kubernetes/src/plugin.ts | 8 +++++-- .../kubernetes/src/upload-interceptor.ts | 10 +++++++++ .../kubernetes/test/unit/plugin.test.ts | 7 +++++++ .../test/unit/upload-interceptor.test.ts | 21 +++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/plugins/sandbox-providers/kubernetes/src/plugin.ts b/packages/plugins/sandbox-providers/kubernetes/src/plugin.ts index 5d0321d8..03950e95 100644 --- a/packages/plugins/sandbox-providers/kubernetes/src/plugin.ts +++ b/packages/plugins/sandbox-providers/kubernetes/src/plugin.ts @@ -132,6 +132,11 @@ export function buildSandboxExecCommand( return ["/bin/sh", "-l"]; } +export function deriveUploadTargetDir(targetPath: string): string { + const slashIndex = targetPath.lastIndexOf("/"); + return slashIndex >= 0 ? targetPath.slice(0, slashIndex) || "/" : "."; +} + export function buildSandboxExecShellCommand( params: Pick, ): string { @@ -552,8 +557,7 @@ const plugin = definePlugin({ } if (decision.action === "flush") { const base64Body = decision.flush.payload.toString("base64"); - const slashIndex = decision.flush.targetPath.lastIndexOf("/"); - const dir = slashIndex > 0 ? decision.flush.targetPath.slice(0, slashIndex) : "."; + const dir = deriveUploadTargetDir(decision.flush.targetPath); const script = `mkdir -p ${shellQuoteArg(dir)} && ` + `base64 -d > ${shellQuoteArg(decision.flush.targetPath)}`; diff --git a/packages/plugins/sandbox-providers/kubernetes/src/upload-interceptor.ts b/packages/plugins/sandbox-providers/kubernetes/src/upload-interceptor.ts index 42a90035..8e567d0a 100644 --- a/packages/plugins/sandbox-providers/kubernetes/src/upload-interceptor.ts +++ b/packages/plugins/sandbox-providers/kubernetes/src/upload-interceptor.ts @@ -38,6 +38,7 @@ interface BufferedUpload { targetPath: string; chunks: string[]; totalBase64Chars: number; + sawPaddedChunk: boolean; } export class FastUploadInterceptor { @@ -67,6 +68,7 @@ export class FastUploadInterceptor { targetPath, chunks: [], totalBase64Chars: 0, + sawPaddedChunk: false, }); return { action: "ack", reason: `init upload to ${targetPath}` }; } @@ -80,6 +82,13 @@ export class FastUploadInterceptor { if (!upload) { return { action: "passthrough", reason: "chunk without prior init" }; } + if (upload.sawPaddedChunk) { + this.buffers.delete(tempPath); + return { + action: "error", + message: `Fast upload received data after a padded chunk for ${upload.targetPath}; retry the upload from the beginning.`, + }; + } if (upload.totalBase64Chars + base64Chunk.length > (this.maxBufferBytes * 4) / 3) { this.buffers.delete(tempPath); @@ -91,6 +100,7 @@ export class FastUploadInterceptor { upload.chunks.push(base64Chunk); upload.totalBase64Chars += base64Chunk.length; + upload.sawPaddedChunk = base64Chunk.endsWith("="); return { action: "ack", reason: `buffered ${base64Chunk.length} base64 chars` }; } diff --git a/packages/plugins/sandbox-providers/kubernetes/test/unit/plugin.test.ts b/packages/plugins/sandbox-providers/kubernetes/test/unit/plugin.test.ts index be098949..c70d67f1 100644 --- a/packages/plugins/sandbox-providers/kubernetes/test/unit/plugin.test.ts +++ b/packages/plugins/sandbox-providers/kubernetes/test/unit/plugin.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest"; import plugin, { buildSandboxExecCommand, buildSandboxExecShellCommand, + deriveUploadTargetDir, extractAdapterEnvFromProcess, } from "../../src/plugin.js"; @@ -179,4 +180,10 @@ describe("plugin", () => { }), ).toEqual(["/bin/sh", "-lc", "pnpm test -- --runInBand"]); }); + + it("derives upload target directories for root and nested paths", () => { + expect(deriveUploadTargetDir("/file")).toBe("/"); + expect(deriveUploadTargetDir("/workspace/file")).toBe("/workspace"); + expect(deriveUploadTargetDir("relative-file")).toBe("."); + }); }); diff --git a/packages/plugins/sandbox-providers/kubernetes/test/unit/upload-interceptor.test.ts b/packages/plugins/sandbox-providers/kubernetes/test/unit/upload-interceptor.test.ts index 3c20959a..dcfc2ec9 100644 --- a/packages/plugins/sandbox-providers/kubernetes/test/unit/upload-interceptor.test.ts +++ b/packages/plugins/sandbox-providers/kubernetes/test/unit/upload-interceptor.test.ts @@ -62,6 +62,27 @@ describe("FastUploadInterceptor", () => { expect(interceptor.pendingCount).toBe(1); }); + it("fails fast when data arrives after a padded chunk", () => { + const interceptor = new FastUploadInterceptor(); + const target = "/workspace/file.bin"; + + expect( + interceptor.decide( + `mkdir -p '/workspace' && rm -f '${target}.paperclip-upload.b64' && : > '${target}.paperclip-upload.b64'`, + ), + ).toMatchObject({ action: "ack" }); + expect( + interceptor.decide(`printf '%s' 'aGVs=' >> '${target}.paperclip-upload.b64'`), + ).toMatchObject({ action: "ack" }); + + const decision = interceptor.decide(`printf '%s' 'bG8=' >> '${target}.paperclip-upload.b64'`); + expect(decision).toMatchObject({ + action: "error", + message: expect.stringContaining("received data after a padded chunk"), + }); + expect(interceptor.pendingCount).toBe(0); + }); + it("falls through when the init command does not match the target parent directory", () => { const interceptor = new FastUploadInterceptor();