Skip to content

The CodeSurface worktree locator rejects every macOS temp path #697

Description

@drewstone

What happens

verifyCodeSurfaceWithGit refuses any worktree whose path has a symlinked ancestor, not merely a symlinked locator:

src/campaign/worktree/index.ts:554-562

const lexicalRoot = resolve(path)
const canonicalRoot = realpathSync(path)
if (
  lstatSync(lexicalRoot).isSymbolicLink() ||
  (process.platform !== 'win32' && lexicalRoot !== canonicalRoot)
) {
  throw new WorktreeAdapterError(
    `CodeSurface worktree locator must not contain a symbolic link: ${path}`,
  )
}

resolve() is lexical; realpathSync() resolves the whole prefix. On macOS os.tmpdir() is /var/folders/..., and /var is a symlink to /private/var, so the two always differ and the throw is unconditional for anything under a temp root. /tmp/private/tmp has the same effect.

Reached from the public verifyCodeSurface (:780), resolveWorktreePath (:790), and finalize (:744).

Measured

In @tangle-network/agent-runtime's suite on macOS (Darwin 25.1.0, full pnpm test), this is the second-largest failure cluster — 8 occurrences:

8  Error: CodeSurface worktree locator must not contain a symbolic link: /var/folders/wk/qcfly3h940s4cfmlpxbyf21w0000gn/…

This package's own worktree suite builds on exactly the failing shape — tests/campaign/worktree.test.ts:37:

repoRoot = mkdtempSync(join(tmpdir(), 'wt-repo-'))

No realpathSync wrapper, so on macOS every case in that file takes the throw. tests/sandbox-harness.test.ts:171,186,187 does wrap mkdtempSync in realpathSync, so the pattern is known here — it just was not applied.

The part that makes this hard to notice

Two tests assert this exact message and therefore pass for the wrong reason on macOS — they cannot distinguish a real detection from the tmpdir false positive:

  • tests/campaign/worktree.test.ts:351 — "rejects a symbolic worktree locator"
  • tests/campaign/worktree.test.ts:366 — "rejects a worktree locator that traverses a symbolic-link parent"

The second one names the behavior as intended, which is why I have not sent a patch: rejecting a symlinked parent looks deliberate, and guessing at the intent of a security check is worse than leaving it. This issue is the question, not a fix.

The shape a fix could take

lstatSync(lexicalRoot).isSymbolicLink() already expresses "the locator itself is not a link". The very next check in the same function compares canonical to canonical and is unaffected:

const repoRoot = gitText(git, ['rev-parse', '--show-toplevel'], path)
const canonicalRepoRoot = realpathSync(repoRoot)
if (canonicalRepoRoot !== canonicalRoot) {  }

So one option is to drop the lexicalRoot !== canonicalRoot operand and carry canonicalRoot forward, which keeps "the locator is not a symlink" and "the locator is the repository root" while letting an OS-supplied symlinked prefix through. If instead the symlinked-parent rejection is a property you want to keep, then the tests need a realpathSync wrapper so they stop passing for the wrong reason, and the constraint should be documented — because as written the worktree adapter cannot be used under os.tmpdir() on macOS at all.

Either way the two rejection tests above should be made to fail on macOS for the right reason before the behavior is settled.

Environment

  • macOS Darwin 25.1.0, arm64, Node v24.11.1
  • agent-eval origin/main @ 85068915 (0.170.0)
  • os.tmpdir()/var/folders/wk/…/T; realpathSync/private/var/folders/wk/…/T

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions