Skip to content

fix(sandbox): canonicalize paths when detecting mount overlap - #83

Merged
chriswritescode-dev merged 1 commit into
mainfrom
fix/sandbox-mount-path-canonicalization
Aug 2, 2026
Merged

fix(sandbox): canonicalize paths when detecting mount overlap#83
chriswritescode-dev merged 1 commit into
mainfrom
fix/sandbox-mount-path-canonicalization

Conversation

@chriswritescode-dev

@chriswritescode-devchriswritescode-dev commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Fixes the one test failing on main:

test/sandbox-manager.test.ts > SandboxManager > start > "git mounts inside sourceProjectDir survive read-write while the overlapping project workspace is dropped"

Root cause

Mount planning mixed two path realms that were never normalized against each other:

SourceValueSymlinks resolved?
git rev-parse --git-common-dir (detectGitMount)/private/var/.../main-project/.gitYes — git realpaths
resolve(sourceProjectDir) from config/var/.../main-projectNo — resolve() never touches symlinks

Overlap detection is pure string prefix matching (isSameOrDescendantPath), so /var/X is not seen as an ancestor of /private/var/X/.git. On macOS /var and /tmp are symlinks to /private/..., which is what the test hits.

Consequence: the read-only source project mount was not dropped, so the same directory was mounted twice — once read-only, once read-write — instead of the project mount yielding to the writable git mounts.

Latent in production rather than universal: it needs a project or worktree path that traverses a symlink. Forge's default worktree root is not symlinked, but a symlinked home, project path, or external volume triggers it.

Fix

Canonicalize comparisons only; emitted mount paths are unchanged.

  • New canonicalizePath in src/sandbox/path.tsrealpathSync with fallback to the input on throw, since sourceProjectDir, custom mounts, and tool-output/temp dirs are not guaranteed to exist. Never throws.
  • containerPathsOverlap canonicalizes both operands. It is the single choke point behind every overlap check (findContainerPathCollision for custom mounts, the workspace-dedupe loop, and the buildMountPlan candidate loop), so one change covers all call sites.
  • The two containment guards in detectGitMount use the same comparison instead of raw startsWith.

Why emitted paths are deliberately left alone

sbx mounts every workspace at its identical host path, and opencode reports absolute host paths in the unresolved realm — realpathing the worktree mount would desync the sandbox path from the path opencode uses and break file/search tools. Conversely the git mounts must keep git's resolved paths, because the linked worktree's .git file records that exact path and git inside the sandbox has to find it there.

Validation

  • pnpm typecheck — pass
  • pnpm lint — pass
  • pnpm test2902/2902 pass (was 2901/2902 on main)
  • The failing test passes unmodified; no test files were changed.

Summary by CodeRabbit

  • Bug Fixes
    • Improved sandbox path handling for equivalent path representations.
    • Improved filtering of Git directory mounts, preventing incorrect matches caused by similar path prefixes.
    • Added fallback handling when a path cannot be resolved.

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.
@coderabbitai

coderabbitaiBot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Sandbox path comparisons now canonicalize existing paths before normalization. Git mount filtering uses canonicalized descendant checks instead of raw string-prefix checks. Unresolvable paths retain their original values.

Changes

Sandbox path handling

Layer / File(s)Summary
Canonicalize sandbox path checks
src/sandbox/path.ts, src/sandbox/manager.ts
Adds canonicalizePath with fallback behavior when resolution fails. Container overlap and Git mount checks use canonicalized paths.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely describes the main change: canonicalizing paths for sandbox mount-overlap detection.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sandbox-mount-path-canonicalization

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/sandbox/manager.ts`:
- Around line 48-49: Update the workspace-boundary guards in
resolveToolOutputMount and resolveTempMount to canonicalize the configured paths
and use isSameOrDescendantPath instead of raw startsWith checks, ensuring
symlinked paths resolving outside workspaceDir are rejected before mounting.
Preserve the original configured paths in emitted mounts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ec23a590-d549-40c3-8449-b57b91817bd1

📥 Commits

Reviewing files that changed from the base of the PR and between a3b5bda and 787aa10.

📒 Files selected for processing (2)
  • src/sandbox/manager.ts
  • src/sandbox/path.ts

Comment on lines +48 to +49
const left = normalizeContainerPath(canonicalizePath(a))
const right = normalizeContainerPath(canonicalizePath(b))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use canonical paths for all workspace-boundary checks.

resolveToolOutputMount at Line [249] and resolveTempMount at Line [263] still use raw startsWith checks. If a configured path is a symlink under workspaceDir but resolves outside it, these guards skip the mount before the canonical overlap check runs. Reuse canonicalizePath and isSameOrDescendantPath in both guards. Keep the original path in emitted mounts.

Proposed fix
- if (resolved === workspaceDir || resolved.startsWith(workspaceDir + '/')) return undefined+ if (isSameOrDescendantPath(canonicalizePath(resolved), canonicalizePath(workspaceDir))) {+ return undefined+ }

Apply the same change in both workspace-boundary guards.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/sandbox/manager.ts` around lines 48 - 49, Update the workspace-boundary
guards in resolveToolOutputMount and resolveTempMount to canonicalize the
configured paths and use isSameOrDescendantPath instead of raw startsWith
checks, ensuring symlinked paths resolving outside workspaceDir are rejected
before mounting. Preserve the original configured paths in emitted mounts.

@chriswritescode-dev
chriswritescode-dev merged commit bef4718 into mainAug 2, 2026
2 checks passed
@chriswritescode-dev
chriswritescode-dev deleted the fix/sandbox-mount-path-canonicalization branch August 2, 2026 00:40
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@chriswritescode-dev