feat: workspace fence toggle (workspaceFence pref + one-click off in error surfaces)
- New persisted built-in pref workspaceFence (default true) under the files card's secondary settings modal; host reads it per request via the settings seam (fenceEnabledOf — absent/malformed stays ON). - ensureWorkspacePath/ensureWorkspaceWritePath gain a fence param: disarmed skips the containment assert but keeps realpath resolution; wired through fs.tree/fs.read/fs.write, the media + html routes, and the upload path (writeWorkspaceUpload fence input). - Editor load failures and file-tree level errors show the localized reason plus a one-click global off (FenceErrorNotice) that flips the pref, adopts it into the store, and auto-retries the failed operation (reloadSeq / retryDir) — the raw wire text is never shown. - GitView's two client-side pre-checks (hidden open action + no-op defense) respect the pref; store threaded through TreePanel/FileTree. - i18n: 4 new keys across all 21 dictionaries; tests: fence-off route coverage, prefs parsing, schema default, editor toggles list; docs: guide §8/§8.1 + plans/2026-08-29 design record.
This commit is contained in:
@@ -32,6 +32,8 @@ export interface WorkspaceUploadInput {
|
||||
chunks: AsyncIterable<string | Uint8Array>
|
||||
/** Byte cap; an oversized upload is refused without touching the target. */
|
||||
limit: number
|
||||
/** Whether workspace containment is enforced (the `workspaceFence` setting; on by default). */
|
||||
fence?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,9 +48,9 @@ export interface WorkspaceUploadInput {
|
||||
* failures; the temp file is always removed on failure.
|
||||
*/
|
||||
export async function writeWorkspaceUpload(input: WorkspaceUploadInput): Promise<{ path: string; size: number }> {
|
||||
const { cwd, dir, relativePath, chunks, limit } = input
|
||||
const { cwd, dir, relativePath, chunks, limit, fence = true } = input
|
||||
const base = requireAbsolute(dir)
|
||||
await ensureWorkspacePath(cwd, base)
|
||||
await ensureWorkspacePath(cwd, base, fence)
|
||||
if (relativePath === '' || relativePath.startsWith('/') || relativePath.startsWith('\\')) {
|
||||
throw new SidebarError('bad-request', 'relativePath must stay below the upload directory', 400)
|
||||
}
|
||||
@@ -57,7 +59,7 @@ export async function writeWorkspaceUpload(input: WorkspaceUploadInput): Promise
|
||||
throw new SidebarError('bad-request', 'relativePath must stay below the upload directory', 400)
|
||||
}
|
||||
const target = join(base, ...segments)
|
||||
const safeTarget = await ensureWorkspaceWritePath(cwd, target)
|
||||
const safeTarget = await ensureWorkspaceWritePath(cwd, target, fence)
|
||||
const tmp = join(dirname(safeTarget), `.${basename(safeTarget)}.dsh-upload-${randomUUID()}.tmp`)
|
||||
await mkdir(dirname(safeTarget), { recursive: true })
|
||||
const stream = createWriteStream(tmp, { flags: 'wx' })
|
||||
|
||||
Reference in New Issue
Block a user