diff --git a/.repository-projection.json b/.repository-projection.json index 6a593ee..ef809db 100644 --- a/.repository-projection.json +++ b/.repository-projection.json @@ -3,11 +3,11 @@ "projection": "endpoint", "projectionSchemaVersion": 1, "sourceRepository": "dx-corp/mono", - "sourceSha": "be57256872b9a0b021c1da667b7803f806e8f289", + "sourceSha": "fc21676e9136ce5ddca1260a75b4044e3c3b8bc3", "destinationRepository": "dx-corp/endpoint", - "priorProjectedBase": "f906d29bae651484eca29bf35d5577b0919a50df", + "priorProjectedBase": "2243d823a7fbcbb69382c681cc7858bda33136d4", "definitionDigest": "8068fb5528eff3a9256419584bb34a9722ea322c288ee4cfda088ff93fb60ec6", "toolDigest": "898e8657d9153a2a51d7c283bf83bb3350b5d1e6", - "contentDigest": "306e18a97c82824dfe39bd5e021dfbf2fa6ca4f28b2eb200ed1224cc052615bc", + "contentDigest": "9f84e7f599492777fd805f10e8f2cab5c9d2772d1f68134bebfe089edadc7e20", "publicationEligible": true } diff --git a/Cargo.lock b/Cargo.lock index b3f32ae..206eda6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -815,9 +815,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "log", "once_cell", @@ -839,9 +839,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", diff --git a/macos/Sources/MerlinMacOS/Inventory.swift b/macos/Sources/MerlinMacOS/Inventory.swift index cb995ff..46f0eb9 100644 --- a/macos/Sources/MerlinMacOS/Inventory.swift +++ b/macos/Sources/MerlinMacOS/Inventory.swift @@ -221,9 +221,7 @@ func collectMacAgentDiscovery(homes: [String], systemBins: [String]) -> (clis: [ ] for (client, kind, relative, format) in assetDirs { let directory = "\(home)/\(relative)" - var directoryInfo = stat() - guard lstat(directory, &directoryInfo) == 0, (directoryInfo.st_mode & mode_t(S_IFMT)) == mode_t(S_IFDIR) else { continue } - for entry in ((try? FileManager.default.contentsOfDirectory(atPath: directory)) ?? []).sorted().prefix(256) { + for entry in boundedAgentDirectoryEntries(directory) { let path = "\(directory)/\(entry)" var info = stat() guard lstat(path, &info) == 0 else { continue } @@ -308,6 +306,21 @@ func collectMacAgentDiscovery(homes: [String], systemBins: [String]) -> (clis: [ return (clis, servers, assets) } +private func boundedAgentDirectoryEntries(_ path: String) -> [String] { + let fd = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK) + guard fd >= 0 else { return [] } + guard let directory = fdopendir(fd) else { close(fd); return [] } + defer { closedir(directory) } + var names = [String]() + while names.count < 256, let entry = readdir(directory) { + let name = withUnsafePointer(to: &entry.pointee.d_name) { pointer in + pointer.withMemoryRebound(to: CChar.self, capacity: 1) { String(cString: $0) } + } + if name != "." && name != ".." { names.append(name) } + } + return names.sorted() +} + private func safeAgentAssetName(_ name: String) -> Bool { !name.isEmpty && name.utf8.count <= 128 && !name.hasPrefix(".") && !name.contains("/") && !name.contains("\\") && diff --git a/macos/Tests/MerlinMacOSTests/SyncTests.swift b/macos/Tests/MerlinMacOSTests/SyncTests.swift index f30db72..f113b9c 100644 --- a/macos/Tests/MerlinMacOSTests/SyncTests.swift +++ b/macos/Tests/MerlinMacOSTests/SyncTests.swift @@ -143,6 +143,8 @@ struct SyncTests { try FileManager.default.createDirectory(atPath: home + "/.cursor", withIntermediateDirectories: true) try FileManager.default.createDirectory(atPath: home + "/.local/bin", withIntermediateDirectories: true) try FileManager.default.createDirectory(atPath: home + "/.agents/skills/review", withIntermediateDirectories: true) + try FileManager.default.createDirectory(atPath: home + "/.pi/agent", withIntermediateDirectories: true) + try FileManager.default.createSymbolicLink(atPath: home + "/.pi/agent/skills", withDestinationPath: home + "/.agents/skills") try FileManager.default.createDirectory(atPath: home + "/.gemini/extensions/workspace", withIntermediateDirectories: true) try FileManager.default.createDirectory(atPath: home + "/.gemini/extensions/not-extension", withIntermediateDirectories: true) try FileManager.default.createDirectory(atPath: home + "/.claude/agents", withIntermediateDirectories: true) @@ -170,6 +172,7 @@ struct SyncTests { #expect(discovered.assets.contains { $0.client == "opencode" && $0.kind == "plugin" && $0.name == "trace" }) #expect(!discovered.assets.contains { $0.name == "off@marketplace" }) #expect(!discovered.assets.contains { $0.name == "not-extension" }) + #expect(!discovered.assets.contains { $0.client == "pi" && $0.name == "review" }) let encoded = try JSONEncoder().encode(discovered.servers) #expect(!String(decoding: encoded, as: UTF8.self).contains("secret")) #expect(!String(decoding: try JSONEncoder().encode(discovered.assets), as: UTF8.self).contains("secret")) diff --git a/merlin/src/sync.rs b/merlin/src/sync.rs index fa01bef..56f7531 100644 --- a/merlin/src/sync.rs +++ b/merlin/src/sync.rs @@ -2283,6 +2283,9 @@ mod tests { "secret instructions", ) .unwrap(); + fs::create_dir_all(home.join(".pi/agent")).unwrap(); + std::os::unix::fs::symlink(home.join(".agents/skills"), home.join(".pi/agent/skills")) + .unwrap(); fs::create_dir_all(home.join(".gemini/extensions/workspace")).unwrap(); fs::write( home.join(".gemini/extensions/workspace/gemini-extension.json"), @@ -2365,6 +2368,11 @@ mod tests { && item.name == "trace")); assert!(!assets.iter().any(|item| item.name == "off@marketplace")); assert!(!assets.iter().any(|item| item.name == "not-extension")); + assert!( + !assets + .iter() + .any(|item| item.client == "pi" && item.name == "review") + ); assert!(!serde_json::to_string(&assets).unwrap().contains("secret")); fs::remove_file(home.join(".cursor/mcp.json")).unwrap(); std::os::unix::fs::symlink(