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:
+11
-5
@@ -21,19 +21,23 @@ function assertWithinWorkspace(workspace: string, target: string): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an existing workspace path through symlinks and enforce containment.
|
||||
* Resolve an existing workspace path through symlinks and (unless disarmed)
|
||||
* enforce containment.
|
||||
*
|
||||
* @param cwd - Session workspace directory.
|
||||
* @param target - Client-supplied absolute path.
|
||||
* @param fence - Whether containment is enforced (the settings-page
|
||||
* `workspaceFence` switch). Even when false the paths are still resolved
|
||||
* through symlinks so callers always receive the canonical target.
|
||||
* @returns The canonical absolute path used for the filesystem operation.
|
||||
*/
|
||||
export async function ensureWorkspacePath(cwd: string, target: string): Promise<string> {
|
||||
export async function ensureWorkspacePath(cwd: string, target: string, fence = true): Promise<string> {
|
||||
const absolute = requireAbsolute(target)
|
||||
const [realCwd, realTarget] = await Promise.all([
|
||||
resolveRealPath(cwd, 'workspace'),
|
||||
resolveRealPath(absolute, 'target'),
|
||||
])
|
||||
assertWithinWorkspace(realCwd, realTarget)
|
||||
if (fence) assertWithinWorkspace(realCwd, realTarget)
|
||||
return realTarget
|
||||
}
|
||||
|
||||
@@ -46,9 +50,11 @@ export async function ensureWorkspacePath(cwd: string, target: string): Promise<
|
||||
*
|
||||
* @param cwd - Session workspace directory.
|
||||
* @param target - Client-supplied absolute destination path.
|
||||
* @param fence - Whether containment is enforced (the settings-page
|
||||
* `workspaceFence` switch). Resolution/canonicalization is identical either way.
|
||||
* @returns A canonical path for an existing target or its nearest existing ancestor.
|
||||
*/
|
||||
export async function ensureWorkspaceWritePath(cwd: string, target: string): Promise<string> {
|
||||
export async function ensureWorkspaceWritePath(cwd: string, target: string, fence = true): Promise<string> {
|
||||
const absolute = requireAbsolute(target)
|
||||
const realCwd = await resolveRealPath(cwd, 'workspace')
|
||||
let existingPath = absolute
|
||||
@@ -57,7 +63,7 @@ export async function ensureWorkspaceWritePath(cwd: string, target: string): Pro
|
||||
for (;;) {
|
||||
try {
|
||||
const realTarget = await realpath(existingPath)
|
||||
assertWithinWorkspace(realCwd, realTarget)
|
||||
if (fence) assertWithinWorkspace(realCwd, realTarget)
|
||||
return missingSegments.reduce((path, segment) => join(path, segment), realTarget)
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
|
||||
Reference in New Issue
Block a user