mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-20 12:30:38 +09:00
## 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>
84 lines
3.1 KiB
TypeScript
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;
|
|
}
|