fix(host,client): gate the picker affordance on the advertised kind; reject non-absolute browse paths

ds-review-bot round 2. The workspace UI never consulted the advertised
directoryPicker kind: under a browse (or merge-added) backend it still
rendered 'Open local folder…' and called pickDirectory(), which the host
answers with directory-picker-unavailable. The create flow now reads
directoryPickerKind() per menu open and renders the dialog affordance only
under 'dialog' — browse (until its in-app browser UI lands) and unknown
kinds hide the entry, realizing the seam's documented default; a keyless
workspace-flow snapshot pins the hidden entry over the browse fixture.

The browse backend also resolved wire paths, silently rebasing '' or
relative parents under the host process cwd; both primitives now reject
non-absolute explicit paths with their business codes, and the seam JSDoc
carries the contract.
This commit is contained in:
creatixchu
2026-07-28 17:39:15 +08:00
parent 6e299cd55a
commit cd7aa3c7d8
20 changed files with 146 additions and 37 deletions

View File

@@ -11,7 +11,7 @@
import { mkdir, readdir, stat } from 'node:fs/promises'
import { homedir } from 'node:os'
import { basename, dirname, join, resolve } from 'node:path'
import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
import {
DirectoryPicker, DirectoryPickerError,
} from '@deepseek-ai/dsh-host-directory-picker'
@@ -81,6 +81,11 @@ export default class BrowseDirectoryPicker extends DirectoryPicker {
private async list(path?: string): Promise<DirectoryListing> {
const home = homedir()
// The seam contract takes absolute paths only; resolve() would silently
// rebase a relative or empty wire value under the host process cwd.
if (path !== undefined && !isAbsolute(path)) {
throw new DirectoryPickerError('directory-unreadable', path, `cannot list "${path}": not an absolute path`)
}
const target = resolve(path ?? home)
let names: { name: string; isDirectory: boolean; isSymbolicLink: boolean }[]
try {
@@ -100,6 +105,10 @@ export default class BrowseDirectoryPicker extends DirectoryPicker {
}
private async createDirectory(path: string, name: string): Promise<string> {
// Same absolute-path fence as list: never rebase a parent under the cwd.
if (!isAbsolute(path)) {
throw new DirectoryPickerError('directory-create-failed', path, `cannot create under "${path}": not an absolute parent path`)
}
const parent = resolve(path)
// The backend owns segment validation (the wire schema also refuses these,
// but direct service consumers must hit the same fence).