toolcraft 0.0.809 → 0.0.811

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/composition.json CHANGED
@@ -138,7 +138,7 @@
138
138
  },
139
139
  {
140
140
  "name": "toolcraft",
141
- "version": "0.0.809",
141
+ "version": "0.0.811",
142
142
  "license": "MIT"
143
143
  },
144
144
  {
@@ -148,7 +148,7 @@
148
148
  },
149
149
  {
150
150
  "name": "toolcraft-schema",
151
- "version": "0.0.809",
151
+ "version": "0.0.811",
152
152
  "license": "MIT"
153
153
  },
154
154
  {
@@ -138,7 +138,7 @@
138
138
  },
139
139
  {
140
140
  "name": "toolcraft",
141
- "version": "0.0.809",
141
+ "version": "0.0.811",
142
142
  "license": "MIT"
143
143
  },
144
144
  {
@@ -148,7 +148,7 @@
148
148
  },
149
149
  {
150
150
  "name": "toolcraft-schema",
151
- "version": "0.0.809",
151
+ "version": "0.0.811",
152
152
  "license": "MIT"
153
153
  },
154
154
  {
@@ -54,7 +54,7 @@ rename nor inode/version identity from placeholder native metadata. Retained
54
54
  reads remain available, with native seek reporting `ENOTSUP`. Ordinary browser
55
55
  bundles cannot import this backend. Node keeps its POSIX host profile.
56
56
 
57
- For host storage, use `await createRealFileSystem({ root: "/absolute/existing/directory" })` instead. The root must already exist; virtual `/` maps to that directory. ZIP creation and updates can use this adapter's private owned staging in an isolated host tree. Archive extraction requires atomic ancestry verification. MemoryFileSystem supports it, including a writable root composed with read-only mounts such as `/skills/.system`; the real adapter refuses it. Mount extraction delegates confinement to the root backend and keeps other mounts read-only in the extraction view. Extraction roots inside a separate mount are rejected because directory and metadata mutations cannot atomically guard ancestors on another backend. Staged file publication supports nested writable mounts when every ancestor provides authoritative validation. Its `trustedOwnedStaging` capability checks original entries before publication and cleanup, and preserves foreign staging children. It does not advertise atomic conditional mutations: keep external writers and other in-flight writes away from the tree during these operations. Read the safety boundary below before exposing it to untrusted code.
57
+ For host storage, use `await createRealFileSystem({ root: "/absolute/existing/directory" })` instead. The root must already exist; virtual `/` maps to that directory. ZIP creation and updates can use this adapter's private owned staging in an isolated host tree. Tar extraction can use `confineTrustedExtraction` on an externally isolated host tree. This view retains extraction roots and checks full ancestry at publication without claiming OS-atomic staging. Atomic archive extraction requires atomic ancestry verification. MemoryFileSystem supports it, including a writable root composed with read-only mounts such as `/skills/.system`; the real adapter refuses it. Mount extraction delegates confinement to each selected backend and keeps other mounts read-only. Extraction inside a separate mount requires synchronous outer-ancestor guards at every mutation; backends that cannot honor those guards refuse it. Staged file publication supports nested writable mounts when every ancestor provides authoritative validation. Its `trustedOwnedStaging` capability checks original entries before publication and cleanup, and preserves foreign staging children. It does not advertise atomic conditional mutations: keep external writers and other in-flight writes away from the tree during these operations. Read the safety boundary below before exposing it to untrusted code.
58
58
 
59
59
  To create new outputs with Safe Bash's `dos2unix`, `unix2dos`, or compression commands, host adapters need atomic no-replace publication. Supply `createRealFileSystem({ root, renameNoReplace })` only when that callback binds a qualified native primitive such as Linux `renameat2` with `RENAME_NOREPLACE`. It receives resolved absolute host paths and an optional signal in its third argument. Existing destinations must reject `EEXIST` atomically; existence checks followed by rename and copy/delete are insufficient. Without this binding the adapter explicitly refuses those operations. See the [no-replace contract](src/contracts/filesystem.md#atomic-no-replace-rename).
60
60
 
@@ -83,6 +83,8 @@ export interface FileSystemCapabilities {
83
83
  readonly retainedStagingWrite?: boolean;
84
84
  /** Atomically verifies every supplied root-to-parent directory identity at publication. */
85
85
  readonly atomicStagingAncestry?: boolean;
86
+ /** Full ancestry validation serialized with publication on an externally isolated host. */
87
+ readonly trustedStagingAncestry?: boolean;
86
88
  /** Prepares directory guards that validate without yielding or mutating state. */
87
89
  readonly synchronousDirectoryValidation?: boolean;
88
90
  /** Captures a followed destination path with a synchronous commit validator. */
@@ -323,12 +325,18 @@ export interface ConditionalFilePublicationOptions extends FsOptions {
323
325
  readonly mode?: number;
324
326
  readonly mtimeMs?: number;
325
327
  }
328
+ export interface ExtractionOptions extends FsOptions {
329
+ /** Validate outer mount bindings synchronously at every mutation; reject unsupported guards. */
330
+ readonly commitGuard?: () => true;
331
+ }
326
332
  export interface FileSystem {
327
333
  /** Retain each extraction root and enforce no-symlink ancestry atomically with
328
334
  * every mutation (including metadata and both hardlink paths). Returned views
329
335
  * must refuse unsupported mutations; no check-then-write emulation is allowed.
330
336
  * Roots and their ancestors must remain the retained directories. */
331
- confineExtraction?(roots: readonly string[], options?: FsOptions): Promise<FileSystem>;
337
+ confineExtraction?(roots: readonly string[], options?: ExtractionOptions): Promise<FileSystem>;
338
+ /** Trusted-host counterpart; requires external tree isolation, and never promises OS-atomic confinement. */
339
+ confineTrustedExtraction?(roots: readonly string[], options?: ExtractionOptions): Promise<FileSystem>;
332
340
  /** Explicit retained-object capability for qualified byte-path backends. */
333
341
  readonly objects?: ObjectFileSystem;
334
342
  /** Consume the complete source privately, then atomically compare/publish.
@@ -144,7 +144,7 @@ export function quotaCapabilities(capabilities) {
144
144
  const streamingWrite = requireCapabilities(capabilities.write, capabilities.append, capabilities.streamingAppend, !capabilities.readOnly);
145
145
  const streamingAppend = requireCapabilities(capabilities.append, capabilities.streamingAppend, !capabilities.readOnly);
146
146
  const { streamingWrite: ignoredWrite, streamingAppend: ignoredAppend, ...rest } = capabilities;
147
- return Object.freeze({ ...rest, synchronousStagingResolution: false, synchronousFollowedStagingResolution: false, retainedStagingCleanup: false, retainedStagingWrite: false, atomicStagingAncestry: false, synchronousDirectoryValidation: false, guardedStagingPublication: false, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicStagedFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
147
+ return Object.freeze({ ...rest, synchronousStagingResolution: false, synchronousFollowedStagingResolution: false, retainedStagingCleanup: false, retainedStagingWrite: false, atomicStagingAncestry: false, trustedStagingAncestry: false, synchronousDirectoryValidation: false, guardedStagingPublication: false, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicStagedFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
148
148
  ...(streamingWrite === undefined ? {} : { streamingWrite }),
149
149
  ...(streamingAppend === undefined ? {} : { streamingAppend }),
150
150
  });
@@ -181,7 +181,7 @@ export function ownedMutationCapabilities(filesystem, capabilities = filesystem.
181
181
  (unavailable ??= {}).synchronousStagingResolution = false;
182
182
  if (capabilities.synchronousFollowedStagingResolution === true && (capabilities.synchronousStagingResolution !== true || unavailable?.synchronousStagingResolution === false))
183
183
  (unavailable ??= {}).synchronousFollowedStagingResolution = false;
184
- if (capabilities.guardedStagingPublication === true && (capabilities.atomicFileStaging !== true || unavailable?.atomicFileStaging === false))
184
+ if (capabilities.guardedStagingPublication === true && ((capabilities.atomicFileStaging !== true || unavailable?.atomicFileStaging === false) && capabilities.trustedOwnedStaging !== true))
185
185
  (unavailable ??= {}).guardedStagingPublication = false;
186
186
  if (capabilities.atomicDirectoryMetadata === true && (capabilities.readOnly === true || typeof filesystem.prepareDirectory !== "function"))
187
187
  (unavailable ??= {}).atomicDirectoryMetadata = false;
@@ -195,8 +195,11 @@ export function ownedMutationCapabilities(filesystem, capabilities = filesystem.
195
195
  }
196
196
  }
197
197
  }
198
- if (missing)
198
+ if (missing) {
199
199
  (unavailable ??= {}).trustedOwnedStaging = false;
200
+ unavailable.trustedStagingAncestry = false;
201
+ unavailable.guardedStagingPublication = false;
202
+ }
200
203
  }
201
204
  return unavailable ? { ...capabilities, ...unavailable } : capabilities;
202
205
  }
@@ -206,7 +209,7 @@ export async function requireOwnedMutation(filesystem, path, capability, options
206
209
  const query = create ? { ...options, create: true } : options;
207
210
  const capabilities = ownedMutationCapabilities(filesystem, await filesystem.capabilitiesFor?.(path, capability === "atomicStagingAncestry" ? { ...query, stagingAncestry: true } : query) ?? filesystem.capabilities);
208
211
  options.signal?.throwIfAborted();
209
- if (capabilities[capability] !== true && !(capabilities.trustedOwnedStaging === true && ["atomicFileStaging", "atomicFileMutation", "atomicDirectoryMetadata"].includes(capability)))
212
+ if (capabilities[capability] !== true && !(capability === "atomicStagingAncestry" && capabilities.trustedStagingAncestry === true && capabilities.trustedOwnedStaging === true) && !(capabilities.trustedOwnedStaging === true && ["atomicFileStaging", "atomicFileMutation", "atomicDirectoryMetadata"].includes(capability)))
210
213
  throw new FsError("ENOTSUP", { path, syscall: capability });
211
214
  }
212
215
  catch (error) {
@@ -1,3 +1,4 @@
1
+ import type { ExtractionOptions } from "../../contracts/filesystem.js";
1
2
  import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
2
3
  import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PrepareStagingResolutionOptions, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
3
4
  import type { ByteSource } from "../../contracts/io.js";
@@ -51,7 +52,8 @@ export declare class DeviceFileSystem implements FileSystem {
51
52
  access(path: string, mode?: number, options?: FsOptions): Promise<void>;
52
53
  readlink(path: string, options?: FsOptions): Promise<string>;
53
54
  symlink(target: string, path: string, options?: FsOptions): Promise<void>;
54
- confineExtraction(roots: readonly string[], options?: FsOptions): Promise<FileSystem>;
55
+ confineTrustedExtraction(roots: readonly string[], options?: ExtractionOptions): Promise<FileSystem>;
56
+ confineExtraction(roots: readonly string[], options?: ExtractionOptions): Promise<FileSystem>;
55
57
  link(existingPath: string, newPath: string, options?: FsOptions): Promise<void>;
56
58
  chmod(path: string, mode: number, options?: FsOptions): Promise<void>;
57
59
  utimes(path: string, atimeMs: number, mtimeMs: number, options?: FsOptions): Promise<void>;
@@ -873,6 +873,13 @@ export class DeviceFileSystem {
873
873
  throw new FsError("ENOTSUP", { path });
874
874
  await this.#filesystem.symlink(target, path, options);
875
875
  }
876
+ async confineTrustedExtraction(roots, options = {}) {
877
+ options.signal?.throwIfAborted();
878
+ const operation = this.#filesystem.confineTrustedExtraction ?? this.#filesystem.confineExtraction;
879
+ if (!operation)
880
+ throw new FsError("ENOTSUP", { syscall: "confineTrustedExtraction" });
881
+ return operation.call(this.#filesystem, roots, options);
882
+ }
876
883
  async confineExtraction(roots, options = {}) {
877
884
  options.signal?.throwIfAborted();
878
885
  if (!this.#filesystem.confineExtraction)
@@ -1,3 +1,4 @@
1
+ import type { ExtractionOptions } from "../../contracts/filesystem.js";
1
2
  import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, FileDescriptor, OpenFileOptions, OpenReadFileOptions, OpenResizeFileOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PrepareStagingResolutionOptions, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
3
  import type { ByteSource } from "../../contracts/io.js";
3
4
  import { type MemoryAtomicView } from "./atomic-view.js";
@@ -77,7 +78,7 @@ export declare class MemoryFileSystem implements FileSystem {
77
78
  constructor(options?: MemoryFileSystemOptions);
78
79
  compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
79
80
  canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
80
- confineExtraction(roots: readonly string[], options?: FsOptions): Promise<FileSystem>;
81
+ confineExtraction(roots: readonly string[], options?: ExtractionOptions): Promise<FileSystem>;
81
82
  private metadata;
82
83
  private directory;
83
84
  private addDirectoryNode;
@@ -738,6 +738,7 @@ export class MemoryFileSystem {
738
738
  }
739
739
  async confineExtraction(roots, options = {}) {
740
740
  options.signal?.throwIfAborted();
741
+ const commitGuard = options.commitGuard;
741
742
  const retained = new Map();
742
743
  for (const root of roots) {
743
744
  this.validatePath(root, "confineExtraction");
@@ -757,6 +758,8 @@ export class MemoryFileSystem {
757
758
  const allowed = new Set(["mkdir", "rm", "rmdir", "rename", "symlink", "link", "chmod", "utimes", "writeFile", "appendFile", "writeStream", "writeFileConditional", "removeFileConditional", "removeEntryConditional", "prepareDirectory", "createStagedFile", "publishStagedFile", "removeStagedFile"]);
758
759
  const reads = new Set(["access", "capabilitiesFor", "compareEntry", "lstat", "stat", "readFile", "readStream", "readdir", "readlink", "realpath"]);
759
760
  const check = (path, followFinal) => {
761
+ if (commitGuard)
762
+ runStagingGuard(commitGuard);
760
763
  if (!roots.some(root => root === "/" || path === root || path.startsWith(`${root}/`)))
761
764
  this.fail("EPERM", "confineExtraction", path);
762
765
  const components = path.split("/").filter(Boolean);
@@ -1,3 +1,4 @@
1
+ import type { ExtractionOptions } from "../../contracts/filesystem.js";
1
2
  import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PrepareStagingResolutionOptions, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
3
  import type { ByteSource } from "../../contracts/io.js";
3
4
  import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
@@ -12,7 +13,9 @@ export declare class MountFileSystem implements FileSystem {
12
13
  private namespaceMutation;
13
14
  private readonly namespaceWaiters;
14
15
  constructor(options: MountFileSystemOptions);
15
- confineExtraction(roots: readonly string[], options?: FsOptions): Promise<FileSystem>;
16
+ private confineArchive;
17
+ confineTrustedExtraction(roots: readonly string[], options?: ExtractionOptions): Promise<FileSystem>;
18
+ confineExtraction(roots: readonly string[], options?: ExtractionOptions): Promise<FileSystem>;
16
19
  private select;
17
20
  capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
18
21
  private supportsStagingAncestry;
@@ -152,35 +152,66 @@ export class MountFileSystem {
152
152
  ...(retainedRead === undefined ? {} : { retainedRead }),
153
153
  });
154
154
  }
155
- async confineExtraction(roots, options = {}) {
156
- options.signal?.throwIfAborted();
157
- const root = this.select("/").backend;
158
- const captured = [...roots];
159
- for (const path of captured) {
155
+ async confineArchive(roots, options, trusted) {
156
+ const callerGuard = options.commitGuard;
157
+ const groups = new Map();
158
+ for (const path of [...roots]) {
160
159
  validatePath(path);
161
160
  if (normalizePath(globalPath(path)) !== path)
162
161
  fail("EINVAL");
163
- // A backing confinement retains its own root-to-destination ancestry.
164
- // It cannot retain directories belonging to another mounted backend.
165
- if (this.select(path).path !== "/")
162
+ const location = await this.resolve(path, options, { followFinal: false, entry: true });
163
+ if (location.path !== path || location.stat?.type !== "directory" || location.synthetic)
166
164
  fail("ENOTSUP");
165
+ const paths = groups.get(location.mount) ?? [];
166
+ paths.push(location.local);
167
+ groups.set(location.mount, paths);
168
+ }
169
+ const confined = new Map();
170
+ for (const mount of this.mounts) {
171
+ const paths = groups.get(mount);
172
+ const backend = mount.backend;
173
+ const operation = trusted ? backend.confineTrustedExtraction ?? backend.confineExtraction : backend.confineExtraction;
174
+ if (paths && !operation)
175
+ fail("ENOTSUP");
176
+ let outerGuard;
177
+ if (paths && mount.path !== "/") {
178
+ const parent = mount.path.slice(0, mount.path.lastIndexOf("/")) || "/";
179
+ const ancestors = await Promise.all(directoryAncestryPaths(parent).map(async (path) => ({ path, stat: await this.lstat(path, options) })));
180
+ outerGuard = await this.prepareAncestry(ancestors, options);
181
+ }
182
+ const controls = { ...options, ...(outerGuard || callerGuard ? { commitGuard: () => {
183
+ if (callerGuard)
184
+ runStagingGuard(callerGuard);
185
+ if (outerGuard)
186
+ runStagingGuard(outerGuard);
187
+ return true;
188
+ } } : {}) };
189
+ const view = paths ? await operation.call(backend, paths, controls) : createReadOnlyFileSystem(backend);
190
+ const observations = paths ? view : backend;
191
+ confined.set(mount, new Proxy(view, {
192
+ get(target, property) {
193
+ if (property === "capabilities")
194
+ return { ...target.capabilities, synchronousDirectoryValidation: observations.capabilities.synchronousDirectoryValidation };
195
+ if (property === "capabilitiesFor")
196
+ return async (path, query = {}) => ({
197
+ ...await target.capabilitiesFor?.(path, query) ?? target.capabilities,
198
+ synchronousDirectoryValidation: (await observations.capabilitiesFor?.(path, query) ?? observations.capabilities).synchronousDirectoryValidation,
199
+ });
200
+ // These capture bindings; mutations still use the confined view.
201
+ const owner = property === "prepareDirectoryAncestry" || property === "prepareStagingResolution" ? backend : target;
202
+ const value = Reflect.get(owner, property);
203
+ return typeof value === "function" ? value.bind(owner) : value;
204
+ },
205
+ }));
167
206
  }
168
- if (!root.confineExtraction)
169
- fail("ENOTSUP");
170
- const confined = await root.confineExtraction(captured, options);
171
207
  options.signal?.throwIfAborted();
172
- const observations = new Set(["prepareDirectoryAncestry", "prepareStagingResolution"]);
173
- const backing = new Proxy(confined, {
174
- get(target, key) {
175
- // These only capture/validate bindings. Mutations must always use the
176
- // backend's confined view, including creation, metadata and hardlinks.
177
- const owner = observations.has(key) ? root : target;
178
- const value = Reflect.get(owner, key);
179
- return typeof value === "function" ? value.bind(owner) : value;
180
- },
181
- });
182
- return new MountFileSystem({ root: backing, mounts: Object.fromEntries(this.mounts.filter(mount => mount.path !== "/").map(mount => [mount.path,
183
- mount.backend.capabilities.readOnly === true ? mount.backend : createReadOnlyFileSystem(mount.backend)])) });
208
+ return new MountFileSystem({ root: confined.get(this.select("/")), mounts: Object.fromEntries(this.mounts.filter(mount => mount.path !== "/").map(mount => [mount.path, confined.get(mount)])) });
209
+ }
210
+ confineTrustedExtraction(roots, options = {}) {
211
+ return this.confineArchive(roots, options, true);
212
+ }
213
+ confineExtraction(roots, options = {}) {
214
+ return this.confineArchive(roots, options, false);
184
215
  }
185
216
  select(path) {
186
217
  return this.mounts.find((mount) => within(mount.path, path));
@@ -221,7 +252,7 @@ export class MountFileSystem {
221
252
  && await this.supportsDirectoryValidation(location.path.slice(0, location.path.lastIndexOf("/")) || "/", options, true)
222
253
  : this.capabilities.conditionalChmod;
223
254
  const { synchronousDirectoryValidation: ignoredValidation, synchronousStagingResolution: ignoredResolution, synchronousFollowedStagingResolution: ignoredFollowedResolution, ...ordinary } = resize;
224
- const withOpen = { ...ordinary, ...(conditionalChmod === undefined ? {} : { conditionalChmod }), ...(resolution === undefined ? {} : { synchronousStagingResolution: resolution, ...(options.followFinalSymlink === true ? { synchronousFollowedStagingResolution: resolution } : {}) }), ...(validation === undefined ? {} : { synchronousDirectoryValidation: validation }), atomicStagingAncestry: ancestry, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
255
+ const withOpen = { ...ordinary, ...(conditionalChmod === undefined ? {} : { conditionalChmod }), ...(resolution === undefined ? {} : { synchronousStagingResolution: resolution, ...(options.followFinalSymlink === true ? { synchronousFollowedStagingResolution: resolution } : {}) }), ...(validation === undefined ? {} : { synchronousDirectoryValidation: validation }), atomicStagingAncestry: ancestry && resize.atomicStagingAncestry === true, trustedStagingAncestry: ancestry && resize.trustedStagingAncestry === true, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
225
256
  const capabilities = location.synthetic ? { ...withOpen, open: false, retainedRead: false }
226
257
  : retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, withOpen));
227
258
  if (location.synthetic)
@@ -240,7 +271,7 @@ export class MountFileSystem {
240
271
  async supportsStagingAncestry(target, capabilities, options) {
241
272
  options.signal?.throwIfAborted();
242
273
  if (target.synthetic || target.stat !== undefined && (target.stat.type !== "file" || compareIdentity(target.stat, target.stat) !== "same")
243
- || capabilities.atomicFileStaging !== true || capabilities.atomicStagingAncestry !== true
274
+ || (capabilities.atomicFileStaging !== true || capabilities.atomicStagingAncestry !== true) && (capabilities.trustedOwnedStaging !== true || capabilities.trustedStagingAncestry !== true)
244
275
  || capabilities.guardedStagingPublication !== true || capabilities.readOnly === true)
245
276
  return false;
246
277
  const parent = target.path.slice(0, target.path.lastIndexOf("/")) || "/";
@@ -1,3 +1,4 @@
1
+ import type { ExtractionOptions } from "../../contracts/filesystem.js";
1
2
  import type { ByteSource } from "../../contracts/io.js";
2
3
  import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
3
4
  import type { ConditionalWriteFileOptions, ConditionalRemoveFileOptions, PrepareDirectoryOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, StagedFileContent, AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, OpenReadFileOptions, FileReadHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
@@ -19,7 +20,7 @@ export declare class OverlayFileSystem implements FileSystem {
19
20
  private readonly publication;
20
21
  private queue;
21
22
  constructor(options: OverlayFileSystemOptions);
22
- confineExtraction(roots: readonly string[], options?: FsOptions): Promise<FileSystem>;
23
+ confineExtraction(roots: readonly string[], options?: ExtractionOptions): Promise<FileSystem>;
23
24
  createStagedFile(directory: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
24
25
  publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
25
26
  removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
@@ -148,6 +148,8 @@ export class OverlayFileSystem {
148
148
  Object.defineProperty(this, "capabilities", { writable: false, configurable: false });
149
149
  }
150
150
  confineExtraction(roots, options = {}) {
151
+ if (options.commitGuard !== undefined)
152
+ return Promise.reject(new FsError("ENOTSUP"));
151
153
  return this.run(options, async () => this.publication.confine(roots, options, (options, operation) => this.run(options, operation, false), this, this.maxBufferBytes), false);
152
154
  }
153
155
  createStagedFile(directory, name, content, options) {
@@ -1,3 +1,4 @@
1
+ import type { ExtractionOptions } from "../../contracts/filesystem.js";
1
2
  import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
2
3
  import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ChmodOptions, FileStagingEntry, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent, OpenReadFileOptions, OpenResizeFileOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/index.js";
3
4
  export interface RealFileSystemOptions {
@@ -54,6 +55,7 @@ export declare class RealFileSystem implements FileSystem {
54
55
  readonly capabilities: FileSystemCapabilities;
55
56
  private readonly configuredRoot;
56
57
  private readonly renameNoReplace;
58
+ private extractionGuard?;
57
59
  private rootPromise;
58
60
  constructor(options: RealFileSystemOptions | string);
59
61
  private root;
@@ -62,6 +64,7 @@ export declare class RealFileSystem implements FileSystem {
62
64
  private path;
63
65
  private operation;
64
66
  private protectRoot;
67
+ confineTrustedExtraction(roots: readonly string[], options?: ExtractionOptions): Promise<FileSystem>;
65
68
  private stagingSnapshot;
66
69
  private expectStaging;
67
70
  private stagingMetadata;
@@ -2,7 +2,7 @@ import { plainDevicePathResolvers } from "../devices/plain-path.js";
2
2
  import { platform } from "#safe-fs-platform";
3
3
  import { snapshotStagingCreation } from "../staging-cleanup.js";
4
4
  import { snapshotConditionalChmod } from "../conditional-chmod.js";
5
- import { runStagingGuard, snapshotDirectoryAncestry } from "../staging-ancestry.js";
5
+ import { directoryAncestryPaths, runStagingGuard, snapshotDirectoryAncestry } from "../staging-ancestry.js";
6
6
  import { constants } from "node:fs";
7
7
  import * as immediate from "node:fs";
8
8
  import * as native from "node:fs/promises";
@@ -111,6 +111,7 @@ export class RealFileSystem {
111
111
  });
112
112
  configuredRoot;
113
113
  renameNoReplace;
114
+ extractionGuard;
114
115
  rootPromise;
115
116
  constructor(options) {
116
117
  const root = typeof options === "string" ? options : options.root;
@@ -313,6 +314,57 @@ export class RealFileSystem {
313
314
  // These critical sections contain no await. They serialize with all JavaScript
314
315
  // callers, under this adapter's existing trusted-host (externally isolated tree)
315
316
  // boundary. They deliberately do not advertise the stronger atomic capabilities.
317
+ async confineTrustedExtraction(roots, options = {}) {
318
+ if (!platform.nativeFileSystem.trustedOwnedStaging || !roots.length)
319
+ throw new FsError("ENOTSUP");
320
+ const root = await this.root(options);
321
+ const retained = new Map();
322
+ const captured = [...roots];
323
+ const commitGuard = options.commitGuard;
324
+ for (const path of captured) {
325
+ for (const ancestor of directoryAncestryPaths(path)) {
326
+ const host = join(root, ancestor.slice(1));
327
+ const stat = this.stagingSnapshot(host);
328
+ if (stat?.type !== "directory")
329
+ throw new FsError("ENOTDIR", { path: ancestor });
330
+ this.expectStaging(host, stat, true);
331
+ retained.set(host, stat);
332
+ }
333
+ }
334
+ options.signal?.throwIfAborted();
335
+ const view = new RealFileSystem({ root });
336
+ Object.defineProperty(view, "capabilities", { value: Object.freeze({ ...view.capabilities, trustedStagingAncestry: true, guardedStagingPublication: true, synchronousDirectoryValidation: true }) });
337
+ view.extractionGuard = host => {
338
+ if (commitGuard)
339
+ runStagingGuard(commitGuard);
340
+ const path = `/${relative(root, host)}`;
341
+ if (!captured.some(base => base === "/" || path === base || path.startsWith(`${base}/`)))
342
+ throw new FsError("EPERM");
343
+ for (const [entry, stat] of retained)
344
+ view.expectStaging(entry, stat, true);
345
+ for (const ancestor of directoryAncestryPaths(dirname(path))) {
346
+ const entry = join(root, ancestor.slice(1));
347
+ if (view.stagingSnapshot(entry)?.type !== "directory")
348
+ throw new FsError("EAGAIN");
349
+ if (immediate.realpathSync(entry) !== entry)
350
+ throw new FsError("EAGAIN");
351
+ }
352
+ };
353
+ const reads = new Set(["access", "capabilitiesFor", "lstat", "stat", "readFile", "readStream", "openReadFile", "readdir", "readlink", "realpath", "prepareDirectoryAncestry"]);
354
+ const mutations = new Set(["prepareDirectory", "createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "link"]);
355
+ return new Proxy(view, {
356
+ get(target, property) {
357
+ if (property === "confineTrustedExtraction" || property === "confineExtraction")
358
+ return undefined;
359
+ const value = Reflect.get(target, property);
360
+ if (typeof value !== "function")
361
+ return value;
362
+ if (reads.has(String(property)) || mutations.has(String(property)))
363
+ return value.bind(target);
364
+ return () => { throw new FsError("ENOTSUP", { syscall: String(property) }); };
365
+ },
366
+ });
367
+ }
316
368
  stagingSnapshot(path) {
317
369
  try {
318
370
  return fileStat(immediate.lstatSync(path, { bigint: true }));
@@ -391,6 +443,7 @@ export class RealFileSystem {
391
443
  options.signal?.throwIfAborted();
392
444
  this.protectTerminal(directoryPath);
393
445
  this.expectStaging(dirname(directory), options.parent, true);
446
+ this.extractionGuard?.(directory);
394
447
  immediate.mkdirSync(directory, { mode: 0o700 });
395
448
  const file = join(directory, name);
396
449
  try {
@@ -447,8 +500,12 @@ export class RealFileSystem {
447
500
  throw new FsError("ENOTSUP", { path: destination });
448
501
  return this.operation("publishStagedFile", staging.file.path, options, async () => {
449
502
  options.signal?.throwIfAborted();
450
- if (options.ancestors !== undefined || options.commitGuard !== undefined)
503
+ if (!this.extractionGuard && (options.ancestors !== undefined || options.commitGuard !== undefined))
451
504
  throw new FsError("ENOTSUP");
505
+ const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
506
+ if (ancestors && ancestors.at(-1).path !== dirname(destination))
507
+ throw new FsError("EINVAL");
508
+ const root = await this.root(options);
452
509
  const paths = await this.stagingPaths(staging, options);
453
510
  const target = await this.path(destination, { ...options, followFinal: false, missing: "final" });
454
511
  options.signal?.throwIfAborted();
@@ -462,6 +519,12 @@ export class RealFileSystem {
462
519
  throw new FsError("EINVAL");
463
520
  if (existing && existing.type !== "file")
464
521
  throw new FsError("EAGAIN");
522
+ if (ancestors)
523
+ for (const entry of ancestors)
524
+ this.expectStaging(join(root, entry.path.slice(1)), entry.stat, true);
525
+ if (options.commitGuard)
526
+ runStagingGuard(options.commitGuard);
527
+ this.extractionGuard?.(target);
465
528
  immediate.renameSync(paths.file, target);
466
529
  }, destination, true);
467
530
  }
@@ -476,6 +539,7 @@ export class RealFileSystem {
476
539
  const children = immediate.readdirSync(paths.directory);
477
540
  if (children.length !== (file ? 1 : 0) || file && children[0] !== basename(paths.file))
478
541
  throw new FsError("ENOTEMPTY");
542
+ this.extractionGuard?.(paths.directory);
479
543
  if (file)
480
544
  immediate.unlinkSync(paths.file);
481
545
  immediate.rmdirSync(paths.directory);
@@ -493,6 +557,7 @@ export class RealFileSystem {
493
557
  const existing = this.expectStaging(target, options.expected);
494
558
  if (existing && existing.type !== "file")
495
559
  throw new FsError("EAGAIN");
560
+ this.extractionGuard?.(target);
496
561
  const fd = immediate.openSync(target, constants.O_WRONLY | constants.O_NOFOLLOW | (existing ? 0 : constants.O_CREAT | constants.O_EXCL), options.mode ?? 0o666);
497
562
  try {
498
563
  if (!options.append)
@@ -531,6 +596,7 @@ export class RealFileSystem {
531
596
  const file = this.expectStaging(target, options.expected);
532
597
  if (file?.type !== "file" || file.nlink !== 1)
533
598
  throw new FsError("EAGAIN");
599
+ this.extractionGuard?.(target);
534
600
  immediate.unlinkSync(target);
535
601
  });
536
602
  }
@@ -545,6 +611,7 @@ export class RealFileSystem {
545
611
  const existing = this.expectStaging(target, options.expected, true);
546
612
  if (existing && existing.type !== "directory")
547
613
  throw new FsError("EAGAIN");
614
+ this.extractionGuard?.(target);
548
615
  if (!existing)
549
616
  immediate.mkdirSync(target, { mode: options.mode ?? 0o777 });
550
617
  if (options.mode !== undefined)
@@ -883,7 +950,15 @@ export class RealFileSystem {
883
950
  const source = await this.path(existingPath, { ...options, followFinal: false });
884
951
  const destination = await this.path(newPath, { ...options, followFinal: false, missing: "final" });
885
952
  options.signal?.throwIfAborted();
886
- await native.link(source, destination);
953
+ if (this.extractionGuard) {
954
+ this.extractionGuard(source);
955
+ this.extractionGuard(destination);
956
+ if (this.stagingSnapshot(source)?.type !== "file")
957
+ throw new FsError("EAGAIN");
958
+ immediate.linkSync(source, destination);
959
+ }
960
+ else
961
+ await native.link(source, destination);
887
962
  }, newPath);
888
963
  }
889
964
  async chmod(path, mode, options = {}) {
@@ -20,7 +20,7 @@ export function retargetScopedFileSystem(scoped, charge, signal, cleanupCharge =
20
20
  const operations = new Set([
21
21
  "prepareDirectoryAncestry", "prepareStagingResolution", "publishStagedFileSet",
22
22
  "publishFileConditional", "removeEntryConditional", "removeTreeConditional", "writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory",
23
- "confineExtraction", "access", "appendFile", "canonicalizeMissingTarget", "capabilitiesFor", "chmod", "compareEntry",
23
+ "confineExtraction", "confineTrustedExtraction", "access", "appendFile", "canonicalizeMissingTarget", "capabilitiesFor", "chmod", "compareEntry",
24
24
  "copyFile", "link", "lstat", "mkdir", "openReadFile", "openResizeFile", "readFile", "readStream", "readdir", "iterateDirectory",
25
25
  "readlink", "realpath", "rename", "resizeFile", "rm", "rmdir", "unlink", "stat", "symlink", "truncate", "utimes",
26
26
  "writeFile", "writeStream",
@@ -507,7 +507,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
507
507
  throw error;
508
508
  }
509
509
  }
510
- : property === "confineExtraction"
510
+ : (property === "confineExtraction" || property === "confineTrustedExtraction")
511
511
  ? async (...args) => scopeFileSystem(await dispatch(...args), charge, signal, cleanupCharge, options)
512
512
  : property === "open"
513
513
  ? async (path, options) => {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.809",
11
+ "version": "0.0.811",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -223,7 +223,7 @@ function buildReviewDetail(slowDetail) {
223
223
  {
224
224
  id: "resolve-comment",
225
225
  label: "Resolve comment",
226
- key: "x",
226
+ accelerator: "x",
227
227
  showInFooter: true,
228
228
  handler: (ctx) => {
229
229
  ctx.toast(`Resolved ${ctx.item?.title ?? ctx.row.title}`, "success");
@@ -246,7 +246,7 @@ function demoActions() {
246
246
  {
247
247
  id: "refresh",
248
248
  label: "Refresh",
249
- key: "r",
249
+ accelerator: "r",
250
250
  showInFooter: true,
251
251
  handler: async (ctx) => {
252
252
  await ctx.refresh();
@@ -256,7 +256,7 @@ function demoActions() {
256
256
  {
257
257
  id: "archive",
258
258
  label: () => "Archive selected",
259
- key: "a",
259
+ accelerator: "e",
260
260
  destructive: true,
261
261
  showInFooter: true,
262
262
  handler: (ctx) => {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.809",
3
+ "version": "0.0.811",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.809",
3
+ "version": "0.0.811",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -177,7 +177,7 @@
177
177
  "saxes"
178
178
  ],
179
179
  "optionalDependencies": {
180
- "toolcraft-schema": "0.0.809",
180
+ "toolcraft-schema": "0.0.811",
181
181
  "toolcraft-design": "*",
182
182
  "@poe-code/frontmatter": "*",
183
183
  "@poe-code/agent-mcp-config": "*",