From 9e211cbd1cf577582b745c6b8dd3b9fe82cc2e29 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 30 Sep 2026 01:07:38 -0700 Subject: [PATCH 01/11] test(worktree-seed): cover the per-pattern walk budget and dangling-link escapes Co-Authored-By: Claude Opus 5.5 (1M context) --- .../WorktreeSeedFileTests.swift | 4 -- .../WorktreeSeedPatternTests.swift | 4 +- .../WorktreeSeedPlannerTests.swift | 48 +++++++++++++++++-- .../WorktreeSeedRepositoryTests.swift | 18 +++++++ 4 files changed, 65 insertions(+), 9 deletions(-) diff --git a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedFileTests.swift b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedFileTests.swift index 3710fe0e9c33..8d94dcc8e656 100644 --- a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedFileTests.swift +++ b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedFileTests.swift @@ -4,10 +4,6 @@ import CMUXAgentLaunch @Suite("worktreeinclude files") struct WorktreeSeedFileTests { - @Test func theFileNameIsTheOneTheRepositoryRootUses() { - #expect(WorktreeSeedFile.fileName == ".worktreeinclude") - } - @Test func patternsKeepFileOrderAndLineNumbers() { let file = WorktreeSeedFile.parse( """ diff --git a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedPatternTests.swift b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedPatternTests.swift index 8b7b1e85d8c9..dfb095992446 100644 --- a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedPatternTests.swift +++ b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedPatternTests.swift @@ -28,9 +28,9 @@ struct WorktreeSeedPatternTests { init(_ description: String) { self.description = description } } - @Test func blankAndCommentLinesSelectNothing() { + @Test func blankAndCommentLinesSelectNothing() throws { for line in ["", " ", "\t", "# a comment", " # indented comment"] { - #expect(try! parse(line).get() == nil, "\(line.debugDescription) should be skipped") + #expect(try parse(line).get() == nil, "\(line.debugDescription) should be skipped") } } diff --git a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedPlannerTests.swift b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedPlannerTests.swift index 157683ba8717..a111a3f80a16 100644 --- a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedPlannerTests.swift +++ b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedPlannerTests.swift @@ -94,8 +94,7 @@ struct WorktreeSeedPlannerTests { @Test func questionMarkMatchesOneCharacter() { let repository = WorktreeSeedFakeRepository(["a.env", "ab.env", ".env"]) - #expect(plan("?.env", WorktreeSeedFakeRepository(["a.env", "ab.env", ".env"])).entries.map(\.relativePath) == ["a.env"]) - _ = repository + #expect(plan("?.env", repository).entries.map(\.relativePath) == ["a.env"]) } @Test func doubleStarIsHowAPatternOptsIntoAWalk() { @@ -113,6 +112,19 @@ struct WorktreeSeedPlannerTests { #expect(plan.shadowed.map(\.relativePath) == ["secrets/b/c"]) } + @Test func aTrailingDoubleStarNegationDoesNotDisarmTheDirectoryItself() { + let repository = WorktreeSeedFakeRepository(["secrets/", "secrets/a"]) + let plan = plan("secrets\n!secrets/**\n", repository) + #expect(plan.entries.map(\.relativePath) == ["secrets"]) + } + + @Test func aTrailingDoubleStarSlashSelectsOnlyDirectories() { + let repository = WorktreeSeedFakeRepository(["a/", "a/f", "a/d/", "a/d/g"]) + let plan = plan("a/**/", repository) + #expect(plan.entries.allSatisfy { $0.isDirectory }) + #expect(!plan.entries.contains { $0.relativePath == "a/f" }) + } + @Test func aTrailingSlashSelectsDirectoriesOnly() { let repository = WorktreeSeedFakeRepository(["build", "build-dir/", "build-dir/x"]) let plan = plan("build*/", repository) @@ -264,7 +276,37 @@ struct WorktreeSeedPlannerTests { let repository = WorktreeSeedFakeRepository(paths) let plan = plan("**/.env", repository, maximumVisitedDirectories: 5) #expect(plan.reachedWalkLimit) - #expect(repository.listedDirectories.count <= 5) + #expect(repository.listedDirectories.count == 5) + } + + @Test func aPatternAfterABudgetExhaustingOneIsStillTried() { + var paths = [".env"] + var prefix = "a" + for _ in 0..<8 { + paths.append(prefix + "/") + paths.append(prefix + "/.env") + prefix += "/a" + } + let repository = WorktreeSeedFakeRepository(paths) + let plan = plan("**/.env\n.env\n", repository, maximumVisitedDirectories: 3) + #expect(plan.truncated.map(\.glob) == ["**/.env"]) + #expect(!plan.unmatched.contains { $0.glob == ".env" }) + #expect(plan.entries.map(\.relativePath) == [".env"]) + } + + @Test func aBudgetExhaustingPatternIsNotReportedAsMatchingNothing() { + var paths: [String] = [] + var prefix = "a" + for _ in 0..<8 { + paths.append(prefix + "/") + paths.append(prefix + "/.env") + prefix += "/a" + } + let repository = WorktreeSeedFakeRepository(paths) + let plan = plan("**/.env\n", repository, maximumVisitedDirectories: 3) + #expect(plan.reachedWalkLimit) + #expect(plan.truncated.map(\.glob) == ["**/.env"]) + #expect(plan.unmatched.isEmpty) } @Test func aPlanThatStaysInBudgetDoesNotClaimALimit() { diff --git a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift index b7c5ae69d7c8..f091b62c06b4 100644 --- a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift +++ b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift @@ -91,6 +91,24 @@ struct WorktreeSeedRepositoryTests { #expect(entry.escapesRepository) } + @Test func aDanglingSymlinkOutOfTheRepositoryIsAnEscape() throws { + let tree = try WorktreeSeedTemporaryTree() + let target = URL(fileURLWithPath: "/nonexistent-\(UUID().uuidString)/secret") + try tree.symlink("gone", to: target) + let listing = WorktreeSeedRepository(root: tree.root).listing("") + let entry = try #require(listing.first { $0.name == "gone" }) + #expect(entry.escapesRepository) + } + + @Test func aDanglingSymlinkInsideTheRepositoryIsNotAnEscape() throws { + let tree = try WorktreeSeedTemporaryTree() + let target = tree.root.appendingPathComponent("future/secret") + try tree.symlink("future-link", to: target) + let listing = WorktreeSeedRepository(root: tree.root).listing("") + let entry = try #require(listing.first { $0.name == "future-link" }) + #expect(!entry.escapesRepository) + } + @Test func aSiblingDirectoryWithTheRootAsAPrefixIsOutside() throws { let parent = try WorktreeSeedTemporaryTree("prefix") let root = try parent.directory("repo") From 6fbc221daae1385a74c54940cc4f0e45557ad040 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 30 Sep 2026 01:10:18 -0700 Subject: [PATCH 02/11] fix(worktree-seed): budget each pattern's walk and refuse dangling links out of the repository Co-Authored-By: Claude Opus 5.5 (1M context) --- .../WorktreeSeed/WorktreeSeedPlan.swift | 8 +++++++ .../WorktreeSeed/WorktreeSeedPlanner.swift | 17 +++++++++++--- .../WorktreeSeed/WorktreeSeedRepository.swift | 22 +++++++++++++++++-- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedPlan.swift b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedPlan.swift index da8608492dfa..6a841c31c75a 100644 --- a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedPlan.swift +++ b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedPlan.swift @@ -72,6 +72,12 @@ public struct WorktreeSeedPlan: Sendable, Equatable { public var alreadyPresent: [WorktreeSeedDecision] /// Patterns that matched nothing, so their author can delete or fix them. public var unmatched: [WorktreeSeedPattern] + /// Patterns whose walk ran out of directory budget before it finished. + /// + /// Kept apart from `unmatched`: a pattern that was cut short may well match + /// something, and telling its author it matched nothing reads as advice to + /// delete the line. + public var truncated: [WorktreeSeedPattern] /// `!` patterns whose only matches sit inside a wholesale-selected directory. /// /// Those cannot be honored, and a silent no-op would read as an exclusion @@ -88,6 +94,7 @@ public struct WorktreeSeedPlan: Sendable, Equatable { shadowed: [WorktreeSeedShadow] = [], alreadyPresent: [WorktreeSeedDecision] = [], unmatched: [WorktreeSeedPattern] = [], + truncated: [WorktreeSeedPattern] = [], ineffectiveNegations: [WorktreeSeedShadow] = [], reachedWalkLimit: Bool = false ) { @@ -97,6 +104,7 @@ public struct WorktreeSeedPlan: Sendable, Equatable { self.shadowed = shadowed self.alreadyPresent = alreadyPresent self.unmatched = unmatched + self.truncated = truncated self.ineffectiveNegations = ineffectiveNegations self.reachedWalkLimit = reachedWalkLimit } diff --git a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedPlanner.swift b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedPlanner.swift index fd64cfcb879b..e54a6fbfeecf 100644 --- a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedPlanner.swift +++ b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedPlanner.swift @@ -40,7 +40,10 @@ public struct WorktreeSeedPlanner: Sendable { /// Returns the children of a repository-relative directory. `""` is the root. public typealias Listing = @Sendable (String) -> [WorktreeSeedListedEntry] - /// How many directories a single expansion may list before the plan gives up. + /// How many directories a single expansion may list before it gives up. + /// + /// Counted per pattern, not per file, so one greedy line cannot starve the + /// rest. A pattern that hits it is reported in `WorktreeSeedPlan.truncated`. public var maximumVisitedDirectories: Int private let listing: Listing @@ -59,12 +62,20 @@ public struct WorktreeSeedPlanner: Sendable { public func plan(for file: WorktreeSeedFile, alreadyPresent: Set = []) -> WorktreeSeedPlan { var plan = WorktreeSeedPlan() var matchesByLine: [Int: [String: WorktreeSeedListedEntry]] = [:] - var visited = 0 for pattern in file.patterns { + // The budget is per pattern. Sharing one across the file made a + // single `**` starve every line after it, and those lines then + // looked like patterns that matched nothing, which is advice to + // delete a line that was never tried. + var visited = 0 let expansion = expand(pattern, visited: &visited) - if expansion.reachedLimit { plan.reachedWalkLimit = true } matchesByLine[pattern.line] = expansion.matches + if expansion.reachedLimit { + plan.reachedWalkLimit = true + plan.truncated.append(pattern) + continue + } if expansion.matches.isEmpty { plan.unmatched.append(pattern) } } diff --git a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift index 3f0f9f2f1bd0..32d1880b48cd 100644 --- a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift +++ b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift @@ -74,12 +74,30 @@ public struct WorktreeSeedRepository: Sendable { } } - /// Whether a path resolves to somewhere under the resolved repository root. + /// Whether the symlink at `url` points to somewhere under the resolved + /// repository root. + /// + /// The target is read and resolved by hand rather than by resolving the link + /// itself. `resolvingSymlinksInPath()` leaves a link whose target does not + /// exist looking like the link's own path, so a dangling + /// `gone -> /elsewhere/secret` read as inside the repository and was + /// reproduced in the new worktree, where it becomes a live link out of the + /// tree the moment the target appears. /// /// The comparison adds the separator so `/repo-backup` does not read as being /// inside `/repo`. private static func isInside(_ url: URL, resolvedRootPath: String) -> Bool { - let resolved = url.resolvingSymlinksInPath().standardizedFileURL.path + let target: URL + if let destination = try? FileManager.default.destinationOfSymbolicLink(atPath: url.path) { + target = URL(fileURLWithPath: destination, relativeTo: url.deletingLastPathComponent()) + .standardizedFileURL + } else { + target = url + } + // The parent is resolved, not the target: the target may not exist, and + // every real component above it does. + let parent = target.deletingLastPathComponent().resolvingSymlinksInPath().standardizedFileURL + let resolved = parent.appendingPathComponent(target.lastPathComponent).standardizedFileURL.path if resolved == resolvedRootPath { return true } return resolved.hasPrefix(resolvedRootPath.hasSuffix("/") ? resolvedRootPath : resolvedRootPath + "/") } From 86cc61270519efbdfc1f8c24b9877144982b466a Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 30 Sep 2026 01:45:14 -0700 Subject: [PATCH 03/11] test(worktree-seed): cover chained symlink escapes Co-Authored-By: Claude Opus 5.5 (1M context) --- .../WorktreeSeedRepositoryTests.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift index f091b62c06b4..402ea3e5a17b 100644 --- a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift +++ b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift @@ -100,6 +100,17 @@ struct WorktreeSeedRepositoryTests { #expect(entry.escapesRepository) } + @Test func aChainedSymlinkOutOfTheRepositoryIsAnEscape() throws { + let tree = try WorktreeSeedTemporaryTree() + let outside = try WorktreeSeedTemporaryTree("outside") + let target = try outside.file("secret") + try tree.symlink("redirect", to: target) + try tree.symlink("selected", to: URL(fileURLWithPath: "redirect")) + let listing = WorktreeSeedRepository(root: tree.root).listing("") + let entry = try #require(listing.first { $0.name == "selected" }) + #expect(entry.escapesRepository) + } + @Test func aDanglingSymlinkInsideTheRepositoryIsNotAnEscape() throws { let tree = try WorktreeSeedTemporaryTree() let target = tree.root.appendingPathComponent("future/secret") From 52b96185b1e91d576e7ae2dd1ae013497b83b8a2 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 30 Sep 2026 01:45:14 -0700 Subject: [PATCH 04/11] fix(worktree-seed): resolve chained symlinks before boundary checks Co-Authored-By: Claude Opus 5.5 (1M context) --- .../WorktreeSeed/WorktreeSeedRepository.swift | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift index 32d1880b48cd..724822d75ed4 100644 --- a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift +++ b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift @@ -5,7 +5,7 @@ import Foundation /// This is the only part of seeding that touches the source repository, so it is /// also where "inside the repository" is decided. `root` is resolved once, and a /// child is reported as escaping when it is a symlink whose target resolves -/// outside that resolved root. + /// outside that resolved root. public struct WorktreeSeedRepository: Sendable { /// The repository root, as given. public let root: URL @@ -87,18 +87,32 @@ public struct WorktreeSeedRepository: Sendable { /// The comparison adds the separator so `/repo-backup` does not read as being /// inside `/repo`. private static func isInside(_ url: URL, resolvedRootPath: String) -> Bool { - let target: URL - if let destination = try? FileManager.default.destinationOfSymbolicLink(atPath: url.path) { - target = URL(fileURLWithPath: destination, relativeTo: url.deletingLastPathComponent()) - .standardizedFileURL - } else { - target = url - } - // The parent is resolved, not the target: the target may not exist, and - // every real component above it does. - let parent = target.deletingLastPathComponent().resolvingSymlinksInPath().standardizedFileURL - let resolved = parent.appendingPathComponent(target.lastPathComponent).standardizedFileURL.path + guard let resolved = resolveSymlinksPreservingMissingLeaf(url)?.path else { return false } if resolved == resolvedRootPath { return true } return resolved.hasPrefix(resolvedRootPath.hasSuffix("/") ? resolvedRootPath : resolvedRootPath + "/") } + + private static func resolveSymlinksPreservingMissingLeaf(_ url: URL) -> URL? { + var current = url.standardizedFileURL + var followed: Set = [] + + while true { + let components = current.pathComponents + var rebuilt = URL(fileURLWithPath: components[0], isDirectory: true) + var foundSymlink = false + + for component in components.dropFirst() { + rebuilt.appendPathComponent(component) + if let destination = try? FileManager.default.destinationOfSymbolicLink(atPath: rebuilt.path) { + guard followed.insert(rebuilt.path).inserted else { return nil } + current = URL(fileURLWithPath: destination, relativeTo: rebuilt.deletingLastPathComponent()) + .standardizedFileURL + foundSymlink = true + break + } + } + + if !foundSymlink { return rebuilt.standardizedFileURL } + } + } } From dc7684a2c891bb83768bff097e195594eaa11582 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 30 Sep 2026 02:20:59 -0700 Subject: [PATCH 05/11] test(worktree-seed): cover symlinked target parents Co-Authored-By: Claude Opus 5.5 (1M context) --- .../WorktreeSeedRepositoryTests.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift index 402ea3e5a17b..68067722944e 100644 --- a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift +++ b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift @@ -105,7 +105,20 @@ struct WorktreeSeedRepositoryTests { let outside = try WorktreeSeedTemporaryTree("outside") let target = try outside.file("secret") try tree.symlink("redirect", to: target) - try tree.symlink("selected", to: URL(fileURLWithPath: "redirect")) + try tree.symlink("selected", to: tree.root.appendingPathComponent("redirect")) + let listing = WorktreeSeedRepository(root: tree.root).listing("") + let entry = try #require(listing.first { $0.name == "selected" }) + #expect(entry.escapesRepository) + } + + @Test func aSymlinkedTargetParentDoesNotHideAnEscapingLeaf() throws { + let tree = try WorktreeSeedTemporaryTree() + let outside = try WorktreeSeedTemporaryTree("outside") + let target = try outside.file("secret") + let directory = try tree.directory("config") + try tree.symlink("config/redirect", to: target) + try tree.symlink("alias", to: directory) + try tree.symlink("selected", to: tree.root.appendingPathComponent("alias/redirect")) let listing = WorktreeSeedRepository(root: tree.root).listing("") let entry = try #require(listing.first { $0.name == "selected" }) #expect(entry.escapesRepository) From 4e70cdfb0df29282181230d977b529d5ac15e536 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 30 Sep 2026 02:20:59 -0700 Subject: [PATCH 06/11] fix(worktree-seed): preserve suffixes after symlink resolution Co-Authored-By: Claude Opus 5.5 (1M context) --- .../WorktreeSeed/WorktreeSeedRepository.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift index 724822d75ed4..550e950468e3 100644 --- a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift +++ b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift @@ -101,12 +101,15 @@ public struct WorktreeSeedRepository: Sendable { var rebuilt = URL(fileURLWithPath: components[0], isDirectory: true) var foundSymlink = false - for component in components.dropFirst() { + for (offset, component) in components.dropFirst().enumerated() { rebuilt.appendPathComponent(component) if let destination = try? FileManager.default.destinationOfSymbolicLink(atPath: rebuilt.path) { guard followed.insert(rebuilt.path).inserted else { return nil } current = URL(fileURLWithPath: destination, relativeTo: rebuilt.deletingLastPathComponent()) - .standardizedFileURL + for suffix in components.dropFirst(offset + 2) { + current.appendPathComponent(suffix) + } + current = current.standardizedFileURL foundSymlink = true break } From 7ecb1b69d52f3524cc1736c2638ec48d0fc8b67c Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 30 Sep 2026 02:40:30 -0700 Subject: [PATCH 07/11] test(worktree-seed): cover valid symlink alias revisits Co-Authored-By: Claude Opus 5.5 (1M context) --- .../WorktreeSeedRepositoryTests.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift index 68067722944e..719d8c341297 100644 --- a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift +++ b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift @@ -124,6 +124,18 @@ struct WorktreeSeedRepositoryTests { #expect(entry.escapesRepository) } + @Test func aValidSymlinkChainCanRevisitAnAlias() throws { + let tree = try WorktreeSeedTemporaryTree() + let directory = try tree.directory("real") + try tree.file("real/file") + try tree.symlink("alias", to: directory) + try tree.symlink("real/redirect", to: tree.root.appendingPathComponent("alias/file")) + try tree.symlink("selected", to: tree.root.appendingPathComponent("alias/redirect")) + let listing = WorktreeSeedRepository(root: tree.root).listing("") + let entry = try #require(listing.first { $0.name == "selected" }) + #expect(!entry.escapesRepository) + } + @Test func aDanglingSymlinkInsideTheRepositoryIsNotAnEscape() throws { let tree = try WorktreeSeedTemporaryTree() let target = tree.root.appendingPathComponent("future/secret") From d272e3f56305aa445a6bcb145a89598dd78aebcb Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 30 Sep 2026 02:40:30 -0700 Subject: [PATCH 08/11] fix(worktree-seed): detect cycles by resolved path state Co-Authored-By: Claude Opus 5.5 (1M context) --- .../CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift index 550e950468e3..1a37f27304a4 100644 --- a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift +++ b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift @@ -94,9 +94,10 @@ public struct WorktreeSeedRepository: Sendable { private static func resolveSymlinksPreservingMissingLeaf(_ url: URL) -> URL? { var current = url.standardizedFileURL - var followed: Set = [] + var seen: Set = [] while true { + guard seen.insert(current.path).inserted else { return nil } let components = current.pathComponents var rebuilt = URL(fileURLWithPath: components[0], isDirectory: true) var foundSymlink = false @@ -104,7 +105,6 @@ public struct WorktreeSeedRepository: Sendable { for (offset, component) in components.dropFirst().enumerated() { rebuilt.appendPathComponent(component) if let destination = try? FileManager.default.destinationOfSymbolicLink(atPath: rebuilt.path) { - guard followed.insert(rebuilt.path).inserted else { return nil } current = URL(fileURLWithPath: destination, relativeTo: rebuilt.deletingLastPathComponent()) for suffix in components.dropFirst(offset + 2) { current.appendPathComponent(suffix) From afc4c3374f3fd0ac9dd7b91e3819ecca931c6ea6 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 30 Sep 2026 03:18:20 -0700 Subject: [PATCH 09/11] test(worktree-seed): bound self-expanding symlink resolution Co-Authored-By: Claude Opus 5.5 (1M context) --- .../WorktreeSeedRepositoryTests.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift index 719d8c341297..fb3dd6bae8cb 100644 --- a/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift +++ b/Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/WorktreeSeedRepositoryTests.swift @@ -136,6 +136,14 @@ struct WorktreeSeedRepositoryTests { #expect(!entry.escapesRepository) } + @Test func aSelfExpandingSymlinkIsRefusedWithoutHanging() throws { + let tree = try WorktreeSeedTemporaryTree() + try tree.symlink("a", to: tree.root.appendingPathComponent("a/child")) + let listing = WorktreeSeedRepository(root: tree.root).listing("") + let entry = try #require(listing.first { $0.name == "a" }) + #expect(entry.escapesRepository) + } + @Test func aDanglingSymlinkInsideTheRepositoryIsNotAnEscape() throws { let tree = try WorktreeSeedTemporaryTree() let target = tree.root.appendingPathComponent("future/secret") From 92f98e9f55b9c7b1dd1545a42b6f5a12c548b8a4 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 30 Sep 2026 03:18:20 -0700 Subject: [PATCH 10/11] fix(worktree-seed): cap symlink resolution hops Co-Authored-By: Claude Opus 5.5 (1M context) --- .../CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift index 1a37f27304a4..48311a6e0f9d 100644 --- a/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift +++ b/Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift @@ -10,6 +10,7 @@ public struct WorktreeSeedRepository: Sendable { /// The repository root, as given. public let root: URL private let resolvedRootPath: String + private static let maximumSymlinkResolutions = 64 /// Creates a reader for a repository root. public init(root: URL) { @@ -95,6 +96,7 @@ public struct WorktreeSeedRepository: Sendable { private static func resolveSymlinksPreservingMissingLeaf(_ url: URL) -> URL? { var current = url.standardizedFileURL var seen: Set = [] + var resolutions = 0 while true { guard seen.insert(current.path).inserted else { return nil } @@ -105,6 +107,8 @@ public struct WorktreeSeedRepository: Sendable { for (offset, component) in components.dropFirst().enumerated() { rebuilt.appendPathComponent(component) if let destination = try? FileManager.default.destinationOfSymbolicLink(atPath: rebuilt.path) { + guard resolutions < maximumSymlinkResolutions else { return nil } + resolutions += 1 current = URL(fileURLWithPath: destination, relativeTo: rebuilt.deletingLastPathComponent()) for suffix in components.dropFirst(offset + 2) { current.appendPathComponent(suffix) From dfded72b7f6dc8a3653e1a2e1eb10b338bc7cd98 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 30 Sep 2026 08:22:58 -0700 Subject: [PATCH 11/11] Align bonsplit pin with current main Keep the catch-up branch on the bonsplit revision required by main's terminal sizing sources. Co-Authored-By: Claude Opus 5.5 (1M context) --- vendor/bonsplit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/bonsplit b/vendor/bonsplit index bd340add9076..83857fa043bd 160000 --- a/vendor/bonsplit +++ b/vendor/bonsplit @@ -1 +1 @@ -Subproject commit bd340add9076addccaf9d4b991e8f142d90877bf +Subproject commit 83857fa043bd00caf20600d379c07d9c33e33b89