fix(host): require complete UNC forms in the fully-qualified path check
ds-review-bot round 5: '\\' and '\\server' satisfy win32.isAbsolute and the previous two-separator test, yet resolve() collapses them to drive-relative roots. The UNC arm now requires server and share components; incomplete prefixes reject with the business codes, covered per-platform.
This commit is contained in:
5 files changed
+13
-8
No files matched your search
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/host/directory-picker-browse/README.md
|
||||
README.md: 160a12a8594400c9e6c565881d8e9ca0517b4e24
|
||||
README.zh.md: 4cfc4a611fb17cb30ac841c8af8498b51a6388b0
|
||||
README.md: 9655fdb538da1addb4d10dbee6210965400394c8
|
||||
README.zh.md: 1c7b0c36cbb73286f3d4e6a48748c4415f9ff0e4
|
||||
@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
The **in-app browsing backend** of the [directory-picker seam](../directory-picker/README.md): `BrowseDirectoryPicker` registers `ctx.directoryPicker` with the `browse` capability — one-level directory listing and child-directory creation over Node's stdlib, which already carries the per-OS adaptation. Nothing renders on the host display, so this backend serves remote clients the dialog backend cannot.
|
||||
|
||||
Behavior facts: listings return **directories only**, name-sorted, with symlinks-to-directories followed (broken/cyclic links skipped — the probe `stat` failing means "not enterable") and a host-owned `hidden` flag (POSIX dot convention) left for the client to act on; `crumbs` is the root-to-target ancestor chain, the root crumb labeled by its full path (`/`, `C:\`); an absent `list` path means the host account's home directory. `createDirectory` is non-recursive (a missing parent is a real failure, not a level to invent) and validates the name as a single non-blank segment even when called directly, mirroring the wire schema's fence. Both primitives reject an explicit path that is not fully qualified — relative forms, and on Windows the rooted drive-less forms (`\foo`, `/foo`) that `isAbsolute` accepts — with `directory-unreadable`/`directory-create-failed`, instead of letting `resolve` rebase it under the host process cwd or current drive. Failures throw the seam's typed `DirectoryPickerError`. Policy rationale: [the directory-picker capability seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.md).
|
||||
Behavior facts: listings return **directories only**, name-sorted, with symlinks-to-directories followed (broken/cyclic links skipped — the probe `stat` failing means "not enterable") and a host-owned `hidden` flag (POSIX dot convention) left for the client to act on; `crumbs` is the root-to-target ancestor chain, the root crumb labeled by its full path (`/`, `C:\`); an absent `list` path means the host account's home directory. `createDirectory` is non-recursive (a missing parent is a real failure, not a level to invent) and validates the name as a single non-blank segment even when called directly, mirroring the wire schema's fence. Both primitives reject an explicit path that is not fully qualified — relative forms, and on Windows the rooted drive-less forms (`\foo`, `/foo`) and incomplete UNC prefixes (`\\`, `\\server`) that `isAbsolute` accepts — with `directory-unreadable`/`directory-create-failed`, instead of letting `resolve` rebase it under the host process cwd or current drive. Failures throw the seam's typed `DirectoryPickerError`. Policy rationale: [the directory-picker capability seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.md).
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
[目录选择 seam](../directory-picker/README.md) 的**应用内浏览后端**:`BrowseDirectoryPicker` 以 `browse` 能力注册 `ctx.directoryPicker`——基于 Node 标准库(跨 OS 适配本就由它承担)提供单层目录列举与子目录创建。宿主屏幕上不渲染任何东西,因此该后端能服务 dialog 后端无法触及的远程客户端。
|
||||
|
||||
行为事实:列举**只返回目录**、按名称排序,指向目录的符号链接会被跟随(断链/循环链接被跳过——探测 `stat` 失败即"不可进入"),并携带宿主判定的 `hidden` 标志(POSIX 点前缀约定),展示决策留给客户端;`crumbs` 是从根到目标的祖先链,根 crumb 以完整路径标注(`/`、`C:\`);`list` 不带路径即列举宿主账户的家目录。`createDirectory` 不递归(父目录缺失是真实失败,不是要补造的层级),且即便被直接调用也把名称校验为单个非空段,与协议 schema 的栅栏一致。两个原语都拒绝非完全限定的显式路径——相对形态,以及 Windows 上 `isAbsolute` 会放行的无盘符有根形态(`\foo`、`/foo`)——报 `directory-unreadable`/`directory-create-failed`,而不是任由 `resolve` 把它重定位到宿主进程 cwd 或当前盘符之下。失败抛出 seam 的类型化 `DirectoryPickerError`。策略依据:[目录选择能力 seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.md)。
|
||||
行为事实:列举**只返回目录**、按名称排序,指向目录的符号链接会被跟随(断链/循环链接被跳过——探测 `stat` 失败即"不可进入"),并携带宿主判定的 `hidden` 标志(POSIX 点前缀约定),展示决策留给客户端;`crumbs` 是从根到目标的祖先链,根 crumb 以完整路径标注(`/`、`C:\`);`list` 不带路径即列举宿主账户的家目录。`createDirectory` 不递归(父目录缺失是真实失败,不是要补造的层级),且即便被直接调用也把名称校验为单个非空段,与协议 schema 的栅栏一致。两个原语都拒绝非完全限定的显式路径——相对形态,以及 Windows 上 `isAbsolute` 会放行的无盘符有根形态(`\foo`、`/foo`)与不完整的 UNC 前缀(`\\`、`\\server`)——报 `directory-unreadable`/`directory-create-failed`,而不是任由 `resolve` 把它重定位到宿主进程 cwd 或当前盘符之下。失败抛出 seam 的类型化 `DirectoryPickerError`。策略依据:[目录选择能力 seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.md)。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -38,16 +38,16 @@ function ancestryCrumbs(target: string): DirectoryEntry[] {
|
||||
/**
|
||||
* True when the path names one fixed filesystem location regardless of
|
||||
* process state: POSIX-absolute on POSIX; on Windows only drive-qualified
|
||||
* (`C:\…`) or UNC (`\\server\…`) forms — rooted drive-less forms (`\foo`,
|
||||
* `/foo`) pass `isAbsolute` yet still resolve against the process's current
|
||||
* drive.
|
||||
* (`C:\…`) or complete UNC (`\\server\share…`) forms. Rooted drive-less
|
||||
* forms (`\foo`, `/foo`) and incomplete UNC prefixes (`\\`, `\\server`)
|
||||
* pass `isAbsolute` yet still resolve against the process's current drive.
|
||||
* @param path - candidate path.
|
||||
* @param platform - replaces `process.platform` for deterministic tests.
|
||||
* @returns whether the path is fully qualified on the platform.
|
||||
*/
|
||||
export function fullyQualified(path: string, platform: NodeJS.Platform = process.platform): boolean {
|
||||
return platform === 'win32'
|
||||
? win32.isAbsolute(path) && /^(?:[A-Za-z]:[\\/]|[\\/]{2})/.test(path)
|
||||
? win32.isAbsolute(path) && /^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/]+[^\\/]+)/.test(path)
|
||||
: posix.isAbsolute(path)
|
||||
}
|
||||
|
||||
|
||||
@@ -76,11 +76,16 @@ describe('BrowseDirectoryPicker', () => {
|
||||
expect(fullyQualified('C:\\projects', 'win32')).toBe(true)
|
||||
expect(fullyQualified('C:/projects', 'win32')).toBe(true)
|
||||
expect(fullyQualified('\\\\server\\share', 'win32')).toBe(true)
|
||||
expect(fullyQualified('//server/share/deep', 'win32')).toBe(true)
|
||||
// Rooted but drive-less: isAbsolute accepts these, yet resolve() would
|
||||
// inject the process's current drive.
|
||||
expect(fullyQualified('\\foo', 'win32')).toBe(false)
|
||||
expect(fullyQualified('/foo', 'win32')).toBe(false)
|
||||
expect(fullyQualified('C:relative', 'win32')).toBe(false)
|
||||
// Incomplete UNC prefixes collapse to drive-relative roots under resolve().
|
||||
expect(fullyQualified('\\\\', 'win32')).toBe(false)
|
||||
expect(fullyQualified('\\\\server', 'win32')).toBe(false)
|
||||
expect(fullyQualified('\\\\server\\', 'win32')).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects non-absolute paths instead of rebasing them under the process cwd', async () => {
|
||||
|
||||
Reference in New Issue
Block a user