paperclipai/paperclip · critical

sync operation ${label} path escapes its confinement root: $

Error message

sync operation ${label} path escapes its confinement root: ${candidate}

What it means

The second confinement check in assertSyncOperationsConfined: the path is a clean absolute POSIX path but does not fall under any of the allowed roots supplied for that label (source or target). It fails closed to prevent a sync operation from reading or writing outside orchestrator-owned directories, blocking absolute-path escapes that are technically valid but unauthorized.

Source

Thrown at packages/adapter-utils/src/sandbox-managed-runtime.ts:281

 * Absolute escapes and `..` traversal are rejected fail-closed. Sandbox and host
 * paths on the server are POSIX.
 */
export function assertSyncOperationsConfined(
  operations: SandboxSyncOperation[],
  roots: { sourceRoots: string[]; targetRoots: string[] },
): void {
  const confine = (candidate: string, allowed: string[], label: string): void => {
    const normalized = path.posix.normalize(candidate);
    if (!path.posix.isAbsolute(normalized) || normalized === ".." || normalized.includes("/../") || normalized.endsWith("/..")) {
      throw new Error(`sync operation ${label} path is not a confined absolute path: ${candidate}`);
    }
    const within = allowed.some((root) => {
      const normalizedRoot = path.posix.normalize(root);
      const prefix = normalizedRoot.endsWith("/") ? normalizedRoot : `${normalizedRoot}/`;
      return normalized === normalizedRoot || normalized.startsWith(prefix);
    });
    if (!within) {
      throw new Error(`sync operation ${label} path escapes its confinement root: ${candidate}`);
    }
  };
  for (const operation of operations) {
    for (const mapping of operation.files) {
      confine(mapping.sourcePath, roots.sourceRoots, "source");
      confine(mapping.targetPath, roots.targetRoots, "target");
    }
  }
}

export interface PreparedSandboxManagedRuntime {
  spec: SandboxRemoteExecutionSpec;
  workspaceLocalDir: string;
  workspaceRemoteDir: string;
  runtimeRootDir: string;
  assetDirs: Record<string, string>;
  /**
   * Remote directory of each additional (referenced) project that staged

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Compare the offending candidate (logged in the message) against roots.sourceRoots/targetRoots and add the missing owning root.
  2. Canonicalize paths with fs.realpath before submitting so symlinks resolve to their true root before the check.
  3. Confirm runtimeRootDir/tempDir used to build targetPaths are included in the targetRoots you pass.
  4. Restrict caller-supplied paths to known prefixes at the API boundary so they never escape.
Defensive patterns

Strategy: validation

Validate before calling

function assertWithinRoots(candidate: string, roots: string[]): void {
  const n = path.posix.normalize(candidate);
  const ok = roots.some((r) => {
    const nr = path.posix.normalize(r);
    const prefix = nr.endsWith('/') ? nr : nr + '/';
    return n === nr || n.startsWith(prefix);
  });
  if (!ok) throw new Error(`path escapes confinement roots: ${candidate}`);
}

Prevention

When it happens

Trigger: Calling assertSyncOperationsConfined where a mapping path is absolute and traversal-free but starts with a root not present in roots.sourceRoots (for source) or roots.targetRoots (for target). E.g. targetPath under /etc while targetRoots only lists the runtime root.

Common situations: roots arrays were not updated when a new staging directory was introduced; a symlink-resolved real path points outside the declared root; misconfigured runtimeRootDir or tempDir; caller passed an asset dir outside the asset root.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/db2232eb93b38f3a. Report an issue: GitHub.