Harden work-folder recovery and add deployed staging acceptance tools

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-07 12:28:17 -05:00
1 parent a13bb6b395
commit 6c7d1bbbf0
23 files changed
+656 -38

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
import { describe, expect, it } from "vitest";
import { isStagingOrigin } from "./deployed-stack.js";
describe("deployed stack target", () => {
it("requires an explicit HTTPS staging tenant and rejects credential-bearing URLs", () => {
expect(isStagingOrigin("https://work-folders-qa.staging.paperclip.app")).toBe(true);
for (const value of ["http://localhost:3100", "https://tenant.paperclip.app", "https://staging.paperclip.app.attacker.test",
"https://token@tenant.staging.paperclip.app", "https://tenant.staging.paperclip.app/path", "https://tenant.staging.paperclip.app?secret=x"]) {
expect(isStagingOrigin(value), value).toBe(false);
}
});
});
+73
View File
@@ -0,0 +1,73 @@
import fs from "node:fs";
import assert from "node:assert/strict";
// A separate target contract deliberately has no local-server fallback.
export function isStagingOrigin(value: string) {
try {
const url = new URL(value);
return url.protocol === "https:" && url.hostname.endsWith(".staging.paperclip.app")
&& !url.username && !url.password && url.pathname === "/" && !url.search && !url.hash;
} catch { return false; }
}
export interface DeployedStack {
baseURL: string; stackId: string; commit: string; appImage: string; migratorVersion: string; sandboxImage: string;
companyId: string; taskId: string; agentId: string; projectId: string; userId: string;
profiles: Array<{ id: string; adapterType: string; engine: string; model: string; qualification: string; agentId: string }>;
}
export function loadDeployedStack(): DeployedStack {
const filename = process.env.PAPERCLIP_DEPLOYED_STACK_MANIFEST;
if (!filename) throw new Error("PAPERCLIP_DEPLOYED_STACK_MANIFEST is required");
const manifest = JSON.parse(fs.readFileSync(filename, "utf8")) as DeployedStack;
assert(manifest && typeof manifest === "object", "Invalid manifest");
for (const key of ["baseURL", "stackId", "commit", "appImage", "migratorVersion", "sandboxImage", "companyId", "taskId", "agentId", "projectId", "userId"] as const) {
assert(typeof manifest[key] === "string" && manifest[key].length > 0, `Missing manifest ${key}`);
}
assert(isStagingOrigin(manifest.baseURL), "Use the dedicated HTTPS staging tenant origin");
assert(/^[a-f0-9]{40}$/.test(manifest.commit), "Expected full commit SHA");
assert(/@sha256:[a-f0-9]{64}$/.test(manifest.sandboxImage), "Expected immutable sandbox image");
const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
for (const key of ["companyId", "taskId", "agentId", "projectId"] as const) assert(uuid.test(manifest[key]), `Invalid ${key}`);
assert(Array.isArray(manifest.profiles) && manifest.profiles.length >= 7, "The seven baseline profiles are required");
for (const profile of manifest.profiles) {
for (const key of ["id", "adapterType", "engine", "model", "qualification", "agentId"] as const) {
assert(typeof profile[key] === "string" && profile[key].length > 0, `Missing profile ${key}`);
}
assert(uuid.test(profile.agentId), "Invalid profile agent ID");
}
if (new Set(manifest.profiles.map((profile) => profile.id)).size !== manifest.profiles.length) {
throw new Error("Duplicate deployed profile IDs");
}
return manifest;
}
export class DeployedStackApi {
private readonly token: string;
constructor(readonly stack: DeployedStack) {
const filename = process.env.PAPERCLIP_DEPLOYED_STACK_AUTH;
if (!filename || (fs.statSync(filename).mode & 0o077) !== 0) {
throw new Error("PAPERCLIP_DEPLOYED_STACK_AUTH must name a private (0600) credentials file");
}
const auth = JSON.parse(fs.readFileSync(filename, "utf8"));
assert(typeof auth?.baseURL === "string" && typeof auth.boardApiToken === "string" && auth.boardApiToken.length > 0, "Invalid stack credentials");
if (new URL(auth.baseURL).origin !== new URL(stack.baseURL).origin) throw new Error("Credentials belong to another stack");
this.token = auth.boardApiToken;
}
// Node fetch keeps credentials out of Playwright traces, request attachments,
// and serialized reporter configuration. Error bodies may contain run secrets.
async request(path: string, options: RequestInit = {}): Promise<Response> {
if (!path.startsWith("/api/") || path.includes("\\") || path.includes("..")) throw new Error("Invalid tenant API path");
const headers = new Headers(options.headers);
headers.set("Authorization", `Bearer ${this.token}`);
return fetch(new URL(path, this.stack.baseURL), {
...options, headers, redirect: "error", signal: AbortSignal.timeout(60_000),
});
}
async json<T>(path: string, method = "GET", body?: unknown): Promise<T> {
const response = await this.request(path, { method,
...(body === undefined ? {} : { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }),
});
if (!response.ok) throw new Error(`${method} ${path.split("?")[0]} returned ${response.status}; body withheld`);
return response.json() as Promise<T>;
}
}
@@ -0,0 +1,91 @@
import { randomUUID } from "node:crypto";
import { test, expect } from "@playwright/test";
import type { EnvironmentCapabilities } from "../../packages/shared/src/environment-support.js";
import type { WorkFolderListing, WorkFolderSyncStatus } from "../../packages/shared/src/work-folders.js";
import { QUALIFIED_ACPX_PROFILES } from "../../packages/paperclip-runner/src/drivers/acpx/qualified-profiles.js";
import { pollUntil } from "./api.js";
import { DeployedStackApi, loadDeployedStack } from "./deployed-stack.js";
const stack = loadDeployedStack();
const api = new DeployedStackApi(stack);
const folder = (scope: string, ownerId: string) => `/api/companies/${stack.companyId}/work-folders/${scope}/${encodeURIComponent(ownerId)}`;
test.describe.configure({ mode: "serial" });
test("deployed candidate and complete supported adapter inventory", async ({}, info) => {
const health = await api.json<{ commit: string }>("/api/health");
expect(health.commit).toBe(stack.commit);
const capabilities = await api.json<EnvironmentCapabilities>(`/api/companies/${stack.companyId}/environments/capabilities`);
expect(capabilities.sandboxProviders.daytona?.supportsRunExecution).toBe(true);
const adapters = await api.json<Array<{ type: string; disabled: boolean; capabilities: { supportsAcp: boolean } }>>("/api/adapters");
const required: string[] = [];
for (const adapter of adapters.filter((entry) => !entry.disabled)) {
if (capabilities.adapters.find((entry) => entry.adapterType === adapter.type)?.drivers.sandbox !== "supported") continue;
if (adapter.type === "paperclip_runner") {
required.push("paperclip_runner:codex", "paperclip_runner:opencode",
...Object.keys(QUALIFIED_ACPX_PROFILES).map((name) => `paperclip_runner:acpx:${name}`));
} else {
required.push(`${adapter.type}:cli`);
if (adapter.capabilities.supportsAcp) required.push(`${adapter.type}:acp`);
}
}
const configured = new Set(stack.profiles.map((profile) => `${profile.adapterType}:${profile.engine}`));
expect(required.filter((key) => !configured.has(key)), "Every exposed sandbox adapter/engine requires a qualified fixture").toEqual([]);
for (const profile of stack.profiles) {
const agent = await api.json<{ adapterType: string; adapterConfig: Record<string, unknown> }>(`/api/agents/${profile.agentId}`);
expect(agent.adapterType).toBe(profile.adapterType);
expect(agent.adapterConfig.model).toBe(profile.model);
}
await info.attach("deployed-candidate-and-inventory", { contentType: "application/json", body: Buffer.from(JSON.stringify({ stack, required }, null, 2)) });
});
for (const [scope, owner] of [["task", stack.taskId], ["agent", stack.agentId], ["project", stack.projectId], ["user", stack.userId]]) {
test(`${scope} nested empty executable files, retry, trash and restoration`, async () => {
const base = folder(scope!, owner!);
const filename = `acceptance/${randomUUID()}/empty.sh`;
const key = randomUUID();
const write = () => api.request(`${base}/content?path=${encodeURIComponent(filename)}`, {
method: "PUT", headers: { "Content-Type": "application/octet-stream", "X-File-Executable": "true", "Idempotency-Key": key }, body: "",
});
expect((await write()).ok).toBe(true);
expect((await write()).ok).toBe(true);
const listing = await api.json<WorkFolderListing>(base);
const file = listing.files.find((entry) => entry.path === filename)!;
expect(file).toMatchObject({ byteSize: 0, executable: true, deletedAt: null });
const download = await api.request(`${base}/content?path=${encodeURIComponent(filename)}`);
expect(download.status).toBe(200); expect((await download.arrayBuffer()).byteLength).toBe(0);
await api.json(`${base}/operations`, "POST", { action: "delete", path: filename });
expect((await api.request(`${base}/content?path=${encodeURIComponent(filename)}`)).status).toBe(404);
const trash = await api.json<WorkFolderListing>(`${base}?trash=true`);
expect(trash.files.some((entry) => entry.id === file.id)).toBe(true);
await api.json(`${base}/operations`, "POST", { action: "restore", fileId: file.id });
expect((await api.request(`${base}/content?path=${encodeURIComponent(filename)}`)).status).toBe(200);
});
}
for (const profile of stack.profiles) {
test(`${profile.id} creates durable work from the actual sandbox home`, async ({}, info) => {
const nonce = randomUUID();
const issue = await api.json<{ id: string; identifier: string }>(`/api/companies/${stack.companyId}/issues`, "POST", {
title: `Work folder acceptance ${profile.id} ${nonce}`, projectId: stack.projectId,
assigneeAgentId: profile.agentId, status: "todo",
description: [
"Perform this sandbox acceptance task using real filesystem tools.",
"Verify cwd equals the operating-system HOME and task, agent, user, project, repos, .codex, .cache are directories beneath it.",
"Verify repos contains at least two independent Git checkouts. Fail the task with the actual error if either assertion fails.",
`Write exactly '${nonce}' without a newline into $HOME/task/acceptance.txt and $HOME/agent/acceptance-${nonce}.txt.`,
"Then complete this task successfully. Do not print credentials or modify unrelated files.",
].join("\n"),
});
const base = folder("task", issue.id);
await pollUntil({ label: `${profile.id} completed run and durable task file`, deadlineAt: Date.now() + 840_000,
intervalMs: 5_000,
load: async () => ({ issue: await api.json<{ status: string }>(`/api/issues/${issue.id}`),
saves: await api.json<WorkFolderSyncStatus[]>(`${base}/sync`) }),
accept: (state) => state.issue.status === "done" && state.saves.some((save) => !save.active && save.state === "saved" && save.lastSavedAt !== null),
reject: (state) => state.saves.some((save) => save.state === "failed") ? "Work-folder save failed" : undefined,
});
const content = await api.request(`${base}/content?path=acceptance.txt`);
expect(content.status).toBe(200); expect(await content.text()).toBe(nonce);
await info.attach("task", { contentType: "application/json", body: Buffer.from(JSON.stringify({ profile: profile.id, ...issue })) });
});
}
@@ -0,0 +1,16 @@
import path from "node:path";
import { defineConfig } from "@playwright/test";
import { loadDeployedStack } from "./deployed-stack.js";
const stack = loadDeployedStack();
const output = process.env.PAPERCLIP_DEPLOYED_STACK_EVIDENCE;
if (!output || !path.isAbsolute(output)) throw new Error("PAPERCLIP_DEPLOYED_STACK_EVIDENCE must be an absolute output directory");
export default defineConfig({
testDir: ".", testMatch: "deployed-work-folders.spec.ts",
fullyParallel: false, workers: 1, retries: 0, timeout: 900_000,
use: { baseURL: stack.baseURL, trace: "off", video: "off" },
// No webServer: every operation reaches the deployed tenant and database.
outputDir: path.join(output, "results"),
reporter: [["list"], ["json", { outputFile: path.join(output, "results.json") }]],
});