paperclipai/paperclip · error

Invalid CreateOS transfer timeout.

Error message

Invalid CreateOS transfer timeout.

What it means

postUploadCommands may specify an optional timeoutMs for a remote command. The plugin validates it is an integer between 1 and 86,400,000 ms (24 hours). Missing, non-integer, zero, negative, or excessively large values throw this error during pre-flight validation.

Solutions

  1. Set timeoutMs to a positive integer of milliseconds within 1..86_400_000, e.g. 30_000.
  2. Omit timeoutMs entirely to use the plugin's default signal handling instead of a sentinel value.
  3. Coerce and validate numeric config values (Number(), Number.isInteger) before constructing operations.

Example fix

// before
{ cmd: "./postprocess.sh", timeoutMs: "30s" }
// after
{ cmd: "./postprocess.sh", timeoutMs: 30_000 }
Defensive patterns

Strategy: validation

Validate before calling

const isValidTimeout = (t: unknown) =>
  Number.isInteger(t) && (t as number) >= 1 && (t as number) <= 86_400_000;
for (const c of op.postUploadCommands ?? []) {
  if (c.timeoutMs != null && !isValidTimeout(c.timeoutMs)) throw new Error("bad timeoutMs");
}

Type guard

const isTimeoutMs = (v: unknown): v is number =>
  typeof v === "number" && Number.isInteger(v) && v >= 1 && v <= 86_400_000;

Prevention

When it happens

Trigger: A post-upload command defines timeoutMs as 0, a float, a negative number, a string like "30000", or a value exceeding one day (e.g. Number.MAX_SAFE_INTEGER intending "no timeout").

Common situations: Using Infinity/very large sentinels to mean unlimited; config parsing yields strings; unit confusion (seconds written where milliseconds are expected, e.g. 30 instead of 30_000).

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/cda5238318ba18b8. Report an issue: GitHub.

Appendix: source

Thrown at packages/plugins/sandbox-providers/createos/src/file-sync.ts:101

  };
  const download = async (remote: string, local: string, mode = 0o600) => {
    const response = await client.request(`/sandboxes/${id}/files?path=${encodeURIComponent(remote)}`, { signal });
    if (!response.body) throw new Error("CreateOS file download has no body.");
    await pipeline(response.body, createWriteStream(local, { flags: "wx", mode }), { signal });
  };

  // Validate every mapping before beginning side effects. Host paths are
  // orchestrator-authored and checked by its source/target-root guard.
  for (const operation of params.operations) {
    for (const mapping of operation.files) {
      if (!["file", "directory"].includes(mapping.kind)) throw new Error("Unsupported CreateOS transfer kind.");
      if (!path.isAbsolute(direction === "in" ? mapping.sourcePath : mapping.targetPath)) throw new Error("CreateOS transfer requires an absolute host path.");
      if (mapping.mode != null && (!Number.isInteger(mapping.mode) || mapping.mode < 0 || mapping.mode > 0o777)) throw new Error("Invalid CreateOS file mode.");
      assertRemotePath(direction === "in" ? mapping.targetPath : mapping.sourcePath);
    }
    for (const command of operation.postUploadCommands ?? []) {
      assertRemotePath(command.cwd ?? ROOT);
      if (command.timeoutMs != null && (!Number.isInteger(command.timeoutMs) || command.timeoutMs < 1 || command.timeoutMs > 86_400_000)) throw new Error("Invalid CreateOS transfer timeout.");
    }
    if (direction === "out" && operation.postUploadCommands?.length) throw new Error("Outbound CreateOS transfers cannot run post-upload commands.");
  }

  for (const operation of params.operations) {
    let bytesTransferred = 0;
    let filesTransferred = 0;
    for (const mapping of operation.files) {
      signal.throwIfAborted();
      const local = direction === "in" ? mapping.sourcePath : mapping.targetPath;
      const remote = direction === "in" ? mapping.targetPath : mapping.sourcePath;
      const scratch = `/tmp/paperclip-createos-transfer-${randomUUID()}`;
      // Outbound temporary files are on the target filesystem for atomic rename.
      const parent = direction === "out" ? path.dirname(local) : os.tmpdir();
      await fs.mkdir(parent, { recursive: true });
      const temp = await fs.mkdtemp(path.join(parent, ".paperclip-createos-"));
      const transferFile = path.join(temp, "data");
      try {

View on GitHub (pinned to 3f1d897a7c)