From e8dee462e2fd09cf858be0a02368805346ec4f3b Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 28 Sep 2026 16:01:36 -0500 Subject: [PATCH 1/4] Admit versioned ACP profile snapshots for the Pi error contract Allow profile version 3 without changing existing provider identities. Reject unsupported and malformed versions, and retain exact runtime identity verification. Co-Authored-By: Paperclip --- .../src/contracts/native-execution.test.ts | 11 +++++++++-- .../src/contracts/native-execution.ts | 4 ++-- .../src/drivers/acpx/qualified-profiles.ts | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/paperclip-runner/src/contracts/native-execution.test.ts b/packages/paperclip-runner/src/contracts/native-execution.test.ts index 8d1ddee0a15..06b8c83a84e 100644 --- a/packages/paperclip-runner/src/contracts/native-execution.test.ts +++ b/packages/paperclip-runner/src/contracts/native-execution.test.ts @@ -280,7 +280,7 @@ describe("NativeExecutionInputV1", () => { })).toThrow("eventExpiryDays"); }); - it("accepts only a closed ACPX profile matching the driver and agent", () => { + it.each([1, 2, 3] as const)("accepts only a closed ACPX profile matching the driver and agent at profile version %s", (agentProfileVersion) => { const provider = { kind: "acpx", agent: "pi", @@ -291,7 +291,7 @@ describe("NativeExecutionInputV1", () => { protocolVersion: 1, acpxVersion: "0.13.1", agent: "pi", - agentProfileVersion: 1, + agentProfileVersion, agentServerPackage: "pi-acp", agentServerVersion: "0.0.33", agentRuntimePackage: "@earendil-works/pi-coding-agent", @@ -312,6 +312,13 @@ describe("NativeExecutionInputV1", () => { profile: provider.profile, }); expect(parseNativeExecutionInput(parsed)).toEqual(parsed); + for (const unsupportedVersion of [0, 4, 1.5, "3", null]) { + expect(() => parseNativeExecutionInput({ + ...input, + session: { ...input.session, driverKind: "acpx_runtime" }, + provider: { ...provider, profile: { ...provider.profile, agentProfileVersion: unsupportedVersion } }, + })).toThrow("qualified ACPX v1 profile"); + } expect(buildNativeModelEnvelope(parsed).workspace).toEqual({ cwd: "/safe/workspace" }); expect(() => parseNativeExecutionInput({ ...input, diff --git a/packages/paperclip-runner/src/contracts/native-execution.ts b/packages/paperclip-runner/src/contracts/native-execution.ts index ab8aac4ed76..0416a4cf603 100644 --- a/packages/paperclip-runner/src/contracts/native-execution.ts +++ b/packages/paperclip-runner/src/contracts/native-execution.ts @@ -90,7 +90,7 @@ export interface NativeAcpxProfileSnapshot { protocolVersion: 1; acpxVersion: "0.13.1"; agent: NativeAcpxAgent; - agentProfileVersion: 1 | 2; + agentProfileVersion: 1 | 2 | 3; agentServerPackage: string; agentServerVersion: string; agentRuntimePackage: string | null; @@ -596,7 +596,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput || profile.protocolVersion !== 1 || profile.acpxVersion !== "0.13.1" || profile.agent !== provider.agent - || (profile.agentProfileVersion !== 1 && profile.agentProfileVersion !== 2) + || (profile.agentProfileVersion !== 1 && profile.agentProfileVersion !== 2 && profile.agentProfileVersion !== 3) ) { throw new NativeExecutionInputError("input.provider.profile does not match the qualified ACPX v1 profile"); } diff --git a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts index aff023bb8f1..902707dea44 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts @@ -11,7 +11,7 @@ export interface QualifiedAcpxProfile { readonly protocolVersion: typeof ACPX_DRIVER_PROTOCOL_VERSION; readonly acpxVersion: typeof QUALIFIED_ACPX_VERSION; readonly agent: QualifiedAcpxAgent; - readonly agentProfileVersion: 1 | 2; + readonly agentProfileVersion: 1 | 2 | 3; readonly qualificationStatus?: "pending"; readonly modelPolicy?: "explicit-provider-verified"; /** Wire identity: an npm package name or a runner-owned builtin: identifier. */ From a88056eee95146dc035abede5311e3f407df009c Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 28 Sep 2026 16:09:38 -0500 Subject: [PATCH 2/4] Stabilize feedback route mocks during serialized validation Keep the route service factories bound to hoisted test doubles and assert exact calls while preserving both company-boundary 404 checks. Co-Authored-By: Paperclip --- .../__tests__/issue-feedback-routes.test.ts | 112 ++++++++---------- 1 file changed, 52 insertions(+), 60 deletions(-) diff --git a/server/src/__tests__/issue-feedback-routes.test.ts b/server/src/__tests__/issue-feedback-routes.test.ts index 31e625e6fe4..e59fe6d1e91 100644 --- a/server/src/__tests__/issue-feedback-routes.test.ts +++ b/server/src/__tests__/issue-feedback-routes.test.ts @@ -72,61 +72,60 @@ const mockIssueReferenceService = vi.hoisted(() => ({ syncIssue: vi.fn(async () => undefined), })); -function registerModuleMocks() { - vi.doMock("@paperclipai/shared/telemetry", () => ({ - trackAgentTaskCompleted: vi.fn(), - trackErrorHandlerCrash: vi.fn(), - })); +// Keep service factories stable while each request uses the hoisted test doubles. +vi.mock("@paperclipai/shared/telemetry", () => ({ + trackAgentTaskCompleted: vi.fn(), + trackErrorHandlerCrash: vi.fn(), +})); - vi.doMock("../telemetry.js", () => ({ - getTelemetryClient: vi.fn(() => ({ track: vi.fn() })), - })); +vi.mock("../telemetry.js", () => ({ + getTelemetryClient: vi.fn(() => ({ track: vi.fn() })), +})); - vi.doMock("../services/index.js", () => ({ - companyService: () => ({ - getById: vi.fn(async () => ({ id: "company-1" })), - }), - accessService: () => mockAccessService, - agentService: () => mockAgentService, - companySkillService: () => ({ - completeTestRunForIssue: vi.fn(async () => null), - }), - documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }), - documentService: () => ({}), - executionWorkspaceService: () => mockExecutionWorkspaceService, - goalService: () => ({}), - heartbeatService: () => mockHeartbeatService, - issueApprovalService: () => ({}), - issueReferenceService: () => mockIssueReferenceService, - issueRecoveryActionService: () => ({ - getActiveForIssue: vi.fn(async () => null), - listActiveForIssues: vi.fn(async () => new Map()), - }), - issueService: () => mockIssueService, - issueThreadInteractionService: () => mockIssueThreadInteractionService, - logActivity: mockLogActivity, - projectService: () => ({}), - routineService: () => mockRoutineService, - workProductService: () => ({}), - })); +vi.mock("../services/index.js", () => ({ + companyService: () => ({ + getById: vi.fn(async () => ({ id: "company-1" })), + }), + accessService: () => mockAccessService, + agentService: () => mockAgentService, + companySkillService: () => ({ + completeTestRunForIssue: vi.fn(async () => null), + }), + documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }), + documentService: () => ({}), + executionWorkspaceService: () => mockExecutionWorkspaceService, + goalService: () => ({}), + heartbeatService: () => mockHeartbeatService, + issueApprovalService: () => ({}), + issueReferenceService: () => mockIssueReferenceService, + issueRecoveryActionService: () => ({ + getActiveForIssue: vi.fn(async () => null), + listActiveForIssues: vi.fn(async () => new Map()), + }), + issueService: () => mockIssueService, + issueThreadInteractionService: () => mockIssueThreadInteractionService, + logActivity: mockLogActivity, + projectService: () => ({}), + routineService: () => mockRoutineService, + workProductService: () => ({}), +})); - vi.doMock("../services/environments.js", () => ({ - environmentService: () => mockEnvironmentService, - })); +vi.mock("../services/environments.js", () => ({ + environmentService: () => mockEnvironmentService, +})); - vi.doMock("../services/execution-workspaces.js", () => ({ - executionWorkspaceService: () => mockExecutionWorkspaceService, - STALE_REOPEN_PENDING_CONSUMPTION_GRACE_MS: 5 * 60 * 1000, - })); +vi.mock("../services/execution-workspaces.js", () => ({ + executionWorkspaceService: () => mockExecutionWorkspaceService, + STALE_REOPEN_PENDING_CONSUMPTION_GRACE_MS: 5 * 60 * 1000, +})); - vi.doMock("../services/feedback.js", () => ({ - feedbackService: () => mockFeedbackService, - })); +vi.mock("../services/feedback.js", () => ({ + feedbackService: () => mockFeedbackService, +})); - vi.doMock("../services/instance-settings.js", () => ({ - instanceSettingsService: () => mockInstanceSettingsService, - })); -} +vi.mock("../services/instance-settings.js", () => ({ + instanceSettingsService: () => mockInstanceSettingsService, +})); async function createApp(actor: Record) { const [{ issueRoutes }, { errorHandler }] = await Promise.all([ @@ -152,17 +151,6 @@ async function createApp(actor: Record) { describe("issue feedback trace routes", () => { beforeEach(() => { - vi.resetModules(); - vi.doUnmock("@paperclipai/shared/telemetry"); - vi.doUnmock("../telemetry.js"); - vi.doUnmock("../services/index.js"); - vi.doUnmock("../services/environments.js"); - vi.doUnmock("../services/execution-workspaces.js"); - vi.doUnmock("../services/feedback.js"); - vi.doUnmock("../services/instance-settings.js"); - vi.doUnmock("../routes/issues.js"); - vi.doUnmock("../middleware/index.js"); - registerModuleMocks(); vi.clearAllMocks(); mockFeedbackExportService.flushPendingFeedbackTraces.mockResolvedValue({ attempted: 1, @@ -260,6 +248,8 @@ describe("issue feedback trace routes", () => { const res = await request(app).get("/api/feedback-traces/trace-1"); expect(res.status, JSON.stringify({ body: res.body, errors: app.locals.routeErrors })).toBe(404); + expect(mockFeedbackService.getFeedbackTraceById).toHaveBeenCalledExactlyOnceWith("trace-1", true); + expect(mockFeedbackService.getFeedbackTraceBundle).not.toHaveBeenCalled(); }); it("returns 404 for bundle fetches when a board user lacks access to the trace company", async () => { @@ -280,5 +270,7 @@ describe("issue feedback trace routes", () => { const res = await request(app).get("/api/feedback-traces/trace-1/bundle"); expect(res.status, JSON.stringify({ body: res.body, errors: app.locals.routeErrors })).toBe(404); + expect(mockFeedbackService.getFeedbackTraceBundle).toHaveBeenCalledExactlyOnceWith("trace-1"); + expect(mockFeedbackService.getFeedbackTraceById).not.toHaveBeenCalled(); }); }); From ee0836203f536303259995ad6135463df3629395 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 28 Sep 2026 16:11:34 -0500 Subject: [PATCH 3/4] Refresh reviewed Daytona workspace resolution pin Record the independently reproduced resolved lock digest after the integrated mainline dependency changes. Candidate image builds continue to supply their own verified lock digest. Co-Authored-By: Paperclip --- docker/daytona-runner/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/daytona-runner/Dockerfile b/docker/daytona-runner/Dockerfile index 6d544f6e02c..50c22ec37c0 100644 --- a/docker/daytona-runner/Dockerfile +++ b/docker/daytona-runner/Dockerfile @@ -28,7 +28,7 @@ COPY cli/package.json ./cli/package.json # The complete resolved lock (including transitive integrity hashes) is reviewed. # Reject registry-time drift BEFORE installing packages or running lifecycle code. # Refresh this digest together with source/provider dependency changes. -ARG PAPERCLIP_RUNNER_LOCK_SHA256=9c163adf40fe1c6a65abda6207bd9f5acfc8196bc94f81348b8bf1b2769e050e +ARG PAPERCLIP_RUNNER_LOCK_SHA256=2c46a68811ba1d504a41b6f4d3f642bc93f4a1c615097754c9c6486881dba8e6 RUN printf '%s pnpm-lock.yaml\n' "${PAPERCLIP_RUNNER_LOCK_SHA256}" > /tmp/provider-lock.sha256 \ && sha256sum -c /tmp/provider-lock.sha256 \ && pnpm install --frozen-lockfile --filter '@paperclipai/paperclip-runner...' From 915adc4d331fb19e2207814efafdcaabc4395915 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 28 Sep 2026 16:22:19 -0500 Subject: [PATCH 4/4] Keep real Git streaming stress coverage portable on macOS Retain forty thousand files and four filename lanes above 32 MiB while fitting the macOS path limit. Allow 300 seconds for real filesystem staging/deletion on macOS and retain the 180-second bound elsewhere. Co-Authored-By: Paperclip --- .../workspace-git-snapshot-streaming.test.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/server/src/services/workspace-git-snapshot-streaming.test.ts b/server/src/services/workspace-git-snapshot-streaming.test.ts index 19bfa648872..fa6a8247cb6 100644 --- a/server/src/services/workspace-git-snapshot-streaming.test.ts +++ b/server/src/services/workspace-git-snapshot-streaming.test.ts @@ -51,8 +51,10 @@ function localClient(commands: string[]): SandboxManagedRuntimeClient { }; } +// macOS adds per-file process and filesystem overhead to the real deletion lane. +const realGitTimeoutMs = process.platform === "darwin" ? 300_000 : 180_000; it("streams and stages all four real Git filename lanes above 32 MiB through the shared scheduler", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-streaming-real-git-")); + const root = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "pc-stream-git-"))); directories.push(root); const repo = path.join(root, "repo"); await fs.mkdir(repo); @@ -60,7 +62,9 @@ it("streams and stages all four real Git filename lanes above 32 MiB through the const git = (args: string[]) => runLocalGit(repo, args, { env, timeout: 120_000, maxBuffer: 64 * 1024 }); await git(["init"]); await git(["commit", "--allow-empty", "-qm", "fixture"]); - const parent = path.join(repo, ...Array.from({ length: 4 }, () => "nested-".repeat(30)), "storybook-output"); + // Leave room for the fixture/staging root below macOS's 1,024-byte path + // limit while keeping each 40,000-name Git lane above the 32 MiB boundary. + const parent = path.join(repo, ...Array.from({ length: 3 }, () => "nested-".repeat(30)), "storybook-output"); await fs.mkdir(parent, { recursive: true }); const fileName = (index: number) => `${"asset-".repeat(36)}${index}.js`; const writeFiles = async (content: string) => { @@ -92,7 +96,8 @@ it("streams and stages all four real Git filename lanes above 32 MiB through the bytes += Buffer.byteLength(relative) + 1; } expect(count).toBe(40_000); - expect(bytes).toBe(43_428_890); + expect(bytes).toBe(34_988_890); + expect(bytes).toBeGreaterThan(32 * 1024 * 1024); }; // Standalone callers must use the same streaming semantics. setExpensiveWorkspaceGitExecutor(null); @@ -147,4 +152,4 @@ it("streams and stages all four real Git filename lanes above 32 MiB through the expect(remaining).toBe(0); expect(commands.every((command) => command.length < 16 * 1024)).toBe(true); expect(scheduler.snapshot()).toMatchObject({ activeCount: 0, queuedCount: 0, inFlightCount: 0, cacheBytes: 0 }); -}, 180_000); +}, realGitTimeoutMs);