mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 10:50:38 +09:00
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Heartbeats run inside managed sandboxes (E2B, Cloudflare Sandbox), and each run begins by uploading the agent's workspace as a tar archive > - PAPA-381's E2B runs were failing at 5 and 11 minutes — two distinct failure modes were entangled: workspace tar extraction errors on Linux, and sandbox idle/lease timeouts during normal heartbeat gaps > - Workspace tar extraction failed because macOS bsdtar embeds `LIBARCHIVE.xattr.*` PAX headers that GNU tar on Linux rejects with "This does not look like a tar archive"; the existing `COPYFILE_DISABLE=1` only suppresses AppleDouble `._*` sidecars, not inline PAX xattr entries > - E2B sandboxes also expired between heartbeats because `timeoutMs` defaulted to a short window and was never refreshed per execute, and Cloudflare sandboxes idled out because `sleepAfter` defaulted to 10 minutes > - This pull request adds `--no-xattrs` to the workspace tar invocation, refreshes the E2B sandbox lifetime on each execute and bumps the default `timeoutMs` to 1h, and raises the Cloudflare `sleepAfter` default to 1h > - The benefit is that long-running heartbeat-driven runs (Claude, Codex, etc.) survive across both their initial workspace upload and the natural idle gaps between executes on both E2B and Cloudflare ## What Changed - `packages/adapter-utils/src/sandbox-managed-runtime.ts`: added `--no-xattrs` to `createTarballFromDirectory` so macOS bsdtar produces a clean POSIX tar that GNU tar on Linux can extract, with an inline comment explaining why `COPYFILE_DISABLE=1` alone is insufficient. - `packages/plugins/sandbox-providers/e2b/src/plugin.ts`: refresh the sandbox lifetime on every execute (so long runs don't expire mid-job) and raised the default `timeoutMs` to 1h. - `packages/plugins/sandbox-providers/e2b/src/manifest.ts` and `plugin.test.ts`: updated manifest defaults and added regression coverage for the new behavior. - `packages/plugins/sandbox-providers/cloudflare/src/config.ts`, `manifest.ts`, `plugin.test.ts`: raised default `sleepAfter` from 10m to 1h, mirroring the E2B 1h default, and added a regression test asserting the acquire-lease request body sends `sleepAfter: "1h"` when not overridden. ## Verification - `pnpm --filter @paperclipai/plugin-e2b test` - `pnpm --filter @paperclipai/plugin-cloudflare-sandbox test` - Locally cherry-picked the `--no-xattrs` fix onto master and confirmed end-to-end via a real PAPA-381-style heartbeat-driven E2B run that the workspace upload now extracts cleanly on Linux. The user (board operator) tested this on master and reported "Ok, that worked." - Manual reviewer steps: trigger an E2B heartbeat from a macOS host (this is where the bsdtar xattr headers come from), confirm the workspace tar extracts on the Linux sandbox side; run a long (>15 min) Cloudflare sandbox flow and confirm no lost-lease/idle errors between executes. ## Risks - Low risk overall. - `--no-xattrs` is widely supported by both macOS bsdtar and GNU tar (Linux). Worst case it silently no-ops on a future host that doesn't support it; in that case the existing failure mode reappears, it doesn't introduce a new one. - Raising default `timeoutMs` (E2B) and `sleepAfter` (Cloudflare) from short values to 1h means sandboxes stay alive longer between executes by default. This is the intended behavior — operators that want a tighter idle window can still override via plugin config. - E2B per-execute sandbox lifetime refresh adds a small API call per execute; it is bounded by the same client that already handles execute traffic, so no new dependencies or retry semantics. ## Model Used - Claude (Anthropic), `claude-opus-4-7`, extended thinking enabled, tool use enabled (file/grep/git tools and Paperclip control-plane API). Used to diagnose the dual failure mode (workspace tar PAX xattr headers + sandbox lifetime), write the fixes and tests, and drive the verification loop with the board operator. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots (N/A — no UI changes) - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
98 lines
3.5 KiB
TypeScript
98 lines
3.5 KiB
TypeScript
import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
|
|
|
|
const PLUGIN_ID = "paperclip.cloudflare-sandbox-provider";
|
|
const PLUGIN_VERSION = "0.1.0";
|
|
|
|
const manifest: PaperclipPluginManifestV1 = {
|
|
id: PLUGIN_ID,
|
|
apiVersion: 1,
|
|
version: PLUGIN_VERSION,
|
|
displayName: "Cloudflare Sandbox Provider",
|
|
description:
|
|
"First-party sandbox provider plugin that provisions Cloudflare sandboxes through an operator-deployed Worker bridge.",
|
|
author: "Paperclip",
|
|
categories: ["automation"],
|
|
capabilities: ["environment.drivers.register"],
|
|
entrypoints: {
|
|
worker: "./dist/worker.js",
|
|
},
|
|
environmentDrivers: [
|
|
{
|
|
driverKey: "cloudflare",
|
|
kind: "sandbox_provider",
|
|
displayName: "Cloudflare Sandbox",
|
|
description:
|
|
"Runs Paperclip sandbox environments through a Cloudflare Worker bridge backed by the Sandbox SDK and Durable Objects.",
|
|
configSchema: {
|
|
type: "object",
|
|
properties: {
|
|
bridgeBaseUrl: {
|
|
type: "string",
|
|
format: "uri",
|
|
description: "Base URL of the operator-deployed Cloudflare Worker bridge.",
|
|
},
|
|
bridgeAuthToken: {
|
|
type: "string",
|
|
format: "secret-ref",
|
|
description:
|
|
"Bearer token used by the provider plugin when calling the Cloudflare bridge. Pasted values are stored as company secrets.",
|
|
},
|
|
reuseLease: {
|
|
type: "boolean",
|
|
default: false,
|
|
description: "Reuse a sandbox by environment ID instead of creating one per run.",
|
|
},
|
|
keepAlive: {
|
|
type: "boolean",
|
|
default: false,
|
|
description: "Prevent Cloudflare from idling the container between requests.",
|
|
},
|
|
sleepAfter: {
|
|
type: "string",
|
|
default: "1h",
|
|
description:
|
|
"Idle timeout passed to getSandbox() on lease creation. Defaults to 1 hour so a fresh sandbox survives normal Claude/Codex heartbeats. Ignored when keepAlive is true.",
|
|
},
|
|
normalizeId: {
|
|
type: "boolean",
|
|
default: true,
|
|
description: "Lowercase and normalize sandbox IDs for operator-friendly naming.",
|
|
},
|
|
requestedCwd: {
|
|
type: "string",
|
|
default: "/workspace/paperclip",
|
|
description: "Workspace directory to create inside the sandbox lease.",
|
|
},
|
|
sessionStrategy: {
|
|
type: "string",
|
|
enum: ["named", "default"],
|
|
default: "named",
|
|
description: "Whether to run commands in a stable named session or the default session.",
|
|
},
|
|
sessionId: {
|
|
type: "string",
|
|
default: "paperclip",
|
|
description: "Named Cloudflare session ID used when sessionStrategy is named.",
|
|
},
|
|
timeoutMs: {
|
|
type: "number",
|
|
default: 300000,
|
|
description: "Default per-command timeout passed through to the bridge.",
|
|
},
|
|
bridgeRequestTimeoutMs: {
|
|
type: "number",
|
|
default: 30000,
|
|
description: "HTTP timeout for plugin-to-bridge requests.",
|
|
},
|
|
previewHostname: {
|
|
type: "string",
|
|
description: "Optional hostname reserved for future preview URL support.",
|
|
},
|
|
},
|
|
required: ["bridgeBaseUrl", "bridgeAuthToken"],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
export default manifest;
|