paperclip/packages/plugins/sandbox-providers/cloudflare/src/config.ts
Devin Foley 1bd44c8a0d
Harden Cloudflare sandbox execution (#5967)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - Remote-managed adapters need sandbox/environment execution to behave
like real agent runs, not just local host probes.
> - The Cloudflare sandbox path was the weakest leg in the SSH +
Cloudflare QA matrix because bridge execution could truncate output,
time out long-running installs, and under-provision the worker instance.
> - That made several adapters fail for reasons unrelated to their
actual business logic, which blocks confidence in Paperclip's non-local
environment model.
> - This pull request hardens the Cloudflare bridge/runtime path and
adjusts sandbox probe budgets so adapter verification matches the
measured behavior of the fixed environment.
> - It also corrects the Pi sandbox install command so the QA matrix
exercises a real, supported install path.
> - The benefit is a materially more reliable SSH + Cloudflare adapter
matrix with fewer false negatives and clearer failure boundaries.

## What Changed

- Switched the Cloudflare bridge worker instance type to `standard-2`
for the QA-matrix execution path.
- Raised Cloudflare bridge/plugin-worker timeout budgets and added SSE
keepalives so long-running install/exec calls can complete instead of
dying at the transport layer.
- Fixed Cloudflare bridge-channel command handling to avoid dropped
final stdout chunks on short-lived execs.
- Made Claude, OpenCode, and Cursor sandbox probe timeouts
configurable/sandbox-aware, then tightened the defaults to the measured
post-fix range.
- Updated the Pi sandbox install command to use the package currently
installed by the official `pi.dev` installer, pinned to a specific npm
version.
- Added/updated tests around Cloudflare bridge behavior and adapter
sandbox probe paths.

## Verification

- `pnpm --filter @paperclipai/adapter-claude-local typecheck`
- `pnpm --filter @paperclipai/adapter-opencode-local typecheck`
- `pnpm --filter @paperclipai/adapter-cursor-local typecheck`
- `pnpm vitest run packages/adapters/cursor-local
packages/adapters/claude-local packages/adapters/opencode-local
packages/adapters/pi-local packages/plugins/sandbox-providers/cloudflare
server/src/services/__tests__/plugin-worker-manager.test.ts`
- Manual QA on the dedicated dev instance using the SSH + Cloudflare
environment matrix (`ENV-29` through `ENV-40`). Clean end-to-end passes:
SSH `claude_local`, `codex_local`, `cursor`, `gemini_local`; Cloudflare
`claude_local`, `codex_local`, `cursor`, `gemini_local`.

## Risks

- Cloudflare sandbox cost increases because the bridge worker now runs
on `standard-2` instead of `lite`.
- Higher timeout ceilings can delay surfacing truly hung Cloudflare
bridge calls, even though they remove transport-level false negatives.
- The manual heartbeat matrix still exposed follow-on
execution/sync/disposition bugs in `opencode_local` and `pi_local`;
those are not fixed by this PR.

## Model Used

- OpenAI `gpt-5.4` via Paperclip `codex_local`, reasoning effort `high`,
tool use enabled, repo search enabled.

## 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
- [x] If this change affects the UI, I have included before/after
screenshots (not applicable)
- [x] I have updated relevant documentation to reflect my changes (not
applicable)
- [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>
2026-05-13 22:00:10 -07:00

84 lines
3.1 KiB
TypeScript

import type { CloudflareDriverConfig } from "./types.js";
const DEFAULT_REQUESTED_CWD = "/workspace/paperclip";
const DEFAULT_SLEEP_AFTER = "10m";
const DEFAULT_TIMEOUT_MS = 300_000;
const DEFAULT_BRIDGE_REQUEST_TIMEOUT_MS = 300_000;
const LOCALHOST_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1"]);
function readTrimmedString(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function readBoolean(value: unknown, fallback: boolean): boolean {
return value === undefined ? fallback : value === true;
}
function readInteger(value: unknown, fallback: number): number {
const parsed = Number(value);
return Number.isFinite(parsed) ? Math.trunc(parsed) : fallback;
}
function isLocalBridgeHost(url: URL): boolean {
return LOCALHOST_HOSTNAMES.has(url.hostname);
}
export function parseCloudflareDriverConfig(raw: Record<string, unknown>): CloudflareDriverConfig {
return {
bridgeBaseUrl: readTrimmedString(raw.bridgeBaseUrl) ?? "",
bridgeAuthToken: readTrimmedString(raw.bridgeAuthToken) ?? "",
reuseLease: readBoolean(raw.reuseLease, false),
keepAlive: readBoolean(raw.keepAlive, false),
sleepAfter: readTrimmedString(raw.sleepAfter) ?? DEFAULT_SLEEP_AFTER,
normalizeId: readBoolean(raw.normalizeId, true),
requestedCwd: readTrimmedString(raw.requestedCwd) ?? DEFAULT_REQUESTED_CWD,
sessionStrategy: raw.sessionStrategy === "default" ? "default" : "named",
sessionId: readTrimmedString(raw.sessionId) ?? "paperclip",
timeoutMs: readInteger(raw.timeoutMs, DEFAULT_TIMEOUT_MS),
bridgeRequestTimeoutMs: readInteger(raw.bridgeRequestTimeoutMs, DEFAULT_BRIDGE_REQUEST_TIMEOUT_MS),
previewHostname: readTrimmedString(raw.previewHostname),
};
}
export function validateCloudflareDriverConfig(config: CloudflareDriverConfig): string[] {
const errors: string[] = [];
if (!config.bridgeBaseUrl) {
errors.push("Cloudflare sandbox environments require bridgeBaseUrl.");
} else {
try {
const url = new URL(config.bridgeBaseUrl);
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocalBridgeHost(url))) {
errors.push("bridgeBaseUrl must use HTTPS unless it points at localhost.");
}
} catch {
errors.push("bridgeBaseUrl must be a valid URL.");
}
}
if (!config.bridgeAuthToken) {
errors.push("Cloudflare sandbox environments require bridgeAuthToken.");
}
if (config.reuseLease && !config.keepAlive) {
errors.push("reuseLease requires keepAlive for Cloudflare sandboxes.");
}
if (config.timeoutMs < 1 || config.timeoutMs > 86_400_000) {
errors.push("timeoutMs must be between 1 and 86400000.");
}
if (config.bridgeRequestTimeoutMs < 1 || config.bridgeRequestTimeoutMs > 86_400_000) {
errors.push("bridgeRequestTimeoutMs must be between 1 and 86400000.");
}
if (!config.requestedCwd.startsWith("/")) {
errors.push("requestedCwd must be an absolute POSIX path.");
}
if (config.sessionStrategy === "named" && config.sessionId.trim().length === 0) {
errors.push("sessionId is required when sessionStrategy is named.");
}
return errors;
}