From 787aa10599ff89f77ba5cef9cc5fa9fe3244452c Mon Sep 17 00:00:00 2001 From: Chris Scott <99081550+chriswritescode-dev@users.noreply.github.com> Date: Sat, 1 Aug 2026 19:46:01 -0400 Subject: [PATCH] fix(sandbox): canonicalize paths when detecting mount overlap Mount planning mixed two path realms. Git paths come from `git rev-parse --git-dir`/`--git-common-dir`, which are symlink-resolved, while the worktree and `sourceProjectDir` go through `resolve()`, which never resolves symlinks. Overlap detection is string prefix matching, so on macOS (where /var and /tmp are symlinks to /private/...) it could not see that /var/X is an ancestor of /private/var/X/.git. The read-only source project mount was therefore not dropped, and the same directory was mounted twice with conflicting permissions. Canonicalize both operands inside `containerPathsOverlap`, the single choke point behind every overlap check, and apply the same comparison to the two containment guards in `detectGitMount`. Emitted hostDir and containerDir values are unchanged: sbx mounts at the identical host path and opencode reports unresolved host paths, while the git mounts must keep git's resolved paths because the linked worktree records them. --- src/sandbox/manager.ts | 10 +++++----- src/sandbox/path.ts | 10 ++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/sandbox/manager.ts b/src/sandbox/manager.ts index f636984d9..95d23cbed 100644 --- a/src/sandbox/manager.ts +++ b/src/sandbox/manager.ts @@ -4,7 +4,7 @@ import type { Logger, SandboxResources, SandboxMountConfig } from '../types' import { resolve, join, isAbsolute, posix as posixPath } from 'path' import { mkdirSync, existsSync, writeFileSync, chmodSync, rmSync } from 'fs' import { defaultGitService, type GitService } from '../utils/git-service' -import { isSameOrDescendantPath, type SandboxMount } from './path' +import { canonicalizePath, isSameOrDescendantPath, type SandboxMount } from './path' export interface SandboxManagerConfig { image: string @@ -45,8 +45,8 @@ function normalizeContainerPath(path: string): string { } function containerPathsOverlap(a: string, b: string): boolean { - const left = normalizeContainerPath(a) - const right = normalizeContainerPath(b) + const left = normalizeContainerPath(canonicalizePath(a)) + const right = normalizeContainerPath(canonicalizePath(b)) return isSameOrDescendantPath(left, right) || isSameOrDescendantPath(right, left) } @@ -279,11 +279,11 @@ export function createSandboxManager( const resolvedGitDir = resolve(projectDir, gitDirResult.stdout.trim()) const resolvedCommonDir = resolve(projectDir, commonDirResult.stdout.trim()) - if (!resolvedGitDir.startsWith(projectDir + '/')) { + if (!isSameOrDescendantPath(canonicalizePath(resolvedGitDir), canonicalizePath(projectDir))) { paths.add(resolvedGitDir) } - if (!resolvedCommonDir.startsWith(projectDir + '/')) { + if (!isSameOrDescendantPath(canonicalizePath(resolvedCommonDir), canonicalizePath(projectDir))) { paths.add(resolvedCommonDir) } diff --git a/src/sandbox/path.ts b/src/sandbox/path.ts index a198ec882..c02e61cab 100644 --- a/src/sandbox/path.ts +++ b/src/sandbox/path.ts @@ -1,9 +1,19 @@ +import { realpathSync } from 'fs' + export interface SandboxMount { hostDir: string containerDir: string readOnly?: boolean } +export function canonicalizePath(path: string): string { + try { + return realpathSync(path) + } catch { + return path + } +} + export function isSameOrDescendantPath(path: string, prefix: string): boolean { if (path === prefix) return true return path.startsWith(prefix + '/')