toolcraft 0.0.808 → 0.0.810
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 +2 -2
- package/dist/composition.json +2 -2
- package/node_modules/@poe-code/safe-fs/README.md +1 -1
- package/node_modules/@poe-code/safe-fs/dist/contracts/filesystem.d.ts +9 -1
- package/node_modules/@poe-code/safe-fs/dist/fs/capabilities.js +7 -4
- package/node_modules/@poe-code/safe-fs/dist/fs/devices/index.d.ts +3 -1
- package/node_modules/@poe-code/safe-fs/dist/fs/devices/index.js +7 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/memory/index.d.ts +2 -1
- package/node_modules/@poe-code/safe-fs/dist/fs/memory/index.js +3 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/mount/index.d.ts +4 -1
- package/node_modules/@poe-code/safe-fs/dist/fs/mount/index.js +55 -25
- package/node_modules/@poe-code/safe-fs/dist/fs/overlay/index.d.ts +2 -1
- package/node_modules/@poe-code/safe-fs/dist/fs/overlay/index.js +2 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/real/index.d.ts +3 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/real/index.js +78 -3
- package/node_modules/@poe-code/safe-fs/dist/fs/scoped.js +2 -2
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-design/dist/explorer/demo.js +3 -3
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +2 -2
package/composition.json
CHANGED
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"name": "toolcraft",
|
|
141
|
-
"version": "0.0.
|
|
141
|
+
"version": "0.0.810",
|
|
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.
|
|
151
|
+
"version": "0.0.810",
|
|
152
152
|
"license": "MIT"
|
|
153
153
|
},
|
|
154
154
|
{
|
package/dist/composition.json
CHANGED
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"name": "toolcraft",
|
|
141
|
-
"version": "0.0.
|
|
141
|
+
"version": "0.0.810",
|
|
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.
|
|
151
|
+
"version": "0.0.810",
|
|
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.
|
|
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?:
|
|
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
|
-
|
|
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?:
|
|
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
|
-
|
|
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,65 @@ export class MountFileSystem {
|
|
|
152
152
|
...(retainedRead === undefined ? {} : { retainedRead }),
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
|
-
async
|
|
156
|
-
options.
|
|
157
|
-
const
|
|
158
|
-
const
|
|
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
|
-
|
|
164
|
-
|
|
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
|
+
const owner = property === "prepareDirectoryAncestry" ? observations : target;
|
|
201
|
+
const value = Reflect.get(owner, property);
|
|
202
|
+
return typeof value === "function" ? value.bind(owner) : value;
|
|
203
|
+
},
|
|
204
|
+
}));
|
|
167
205
|
}
|
|
168
|
-
if (!root.confineExtraction)
|
|
169
|
-
fail("ENOTSUP");
|
|
170
|
-
const confined = await root.confineExtraction(captured, options);
|
|
171
206
|
options.signal?.throwIfAborted();
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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)])) });
|
|
207
|
+
return new MountFileSystem({ root: confined.get(this.select("/")), mounts: Object.fromEntries(this.mounts.filter(mount => mount.path !== "/").map(mount => [mount.path, confined.get(mount)])) });
|
|
208
|
+
}
|
|
209
|
+
confineTrustedExtraction(roots, options = {}) {
|
|
210
|
+
return this.confineArchive(roots, options, true);
|
|
211
|
+
}
|
|
212
|
+
confineExtraction(roots, options = {}) {
|
|
213
|
+
return this.confineArchive(roots, options, false);
|
|
184
214
|
}
|
|
185
215
|
select(path) {
|
|
186
216
|
return this.mounts.find((mount) => within(mount.path, path));
|
|
@@ -221,7 +251,7 @@ export class MountFileSystem {
|
|
|
221
251
|
&& await this.supportsDirectoryValidation(location.path.slice(0, location.path.lastIndexOf("/")) || "/", options, true)
|
|
222
252
|
: this.capabilities.conditionalChmod;
|
|
223
253
|
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 }) };
|
|
254
|
+
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
255
|
const capabilities = location.synthetic ? { ...withOpen, open: false, retainedRead: false }
|
|
226
256
|
: retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, withOpen));
|
|
227
257
|
if (location.synthetic)
|
|
@@ -240,7 +270,7 @@ export class MountFileSystem {
|
|
|
240
270
|
async supportsStagingAncestry(target, capabilities, options) {
|
|
241
271
|
options.signal?.throwIfAborted();
|
|
242
272
|
if (target.synthetic || target.stat !== undefined && (target.stat.type !== "file" || compareIdentity(target.stat, target.stat) !== "same")
|
|
243
|
-
|| capabilities.atomicFileStaging !== true || capabilities.atomicStagingAncestry !== true
|
|
273
|
+
|| (capabilities.atomicFileStaging !== true || capabilities.atomicStagingAncestry !== true) && (capabilities.trustedOwnedStaging !== true || capabilities.trustedStagingAncestry !== true)
|
|
244
274
|
|| capabilities.guardedStagingPublication !== true || capabilities.readOnly === true)
|
|
245
275
|
return false;
|
|
246
276
|
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?:
|
|
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
|
-
|
|
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) => {
|
|
@@ -223,7 +223,7 @@ function buildReviewDetail(slowDetail) {
|
|
|
223
223
|
{
|
|
224
224
|
id: "resolve-comment",
|
|
225
225
|
label: "Resolve comment",
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
+
accelerator: "e",
|
|
260
260
|
destructive: true,
|
|
261
261
|
showInFooter: true,
|
|
262
262
|
handler: (ctx) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.810",
|
|
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.
|
|
180
|
+
"toolcraft-schema": "0.0.810",
|
|
181
181
|
"toolcraft-design": "*",
|
|
182
182
|
"@poe-code/frontmatter": "*",
|
|
183
183
|
"@poe-code/agent-mcp-config": "*",
|