@openclaw/fs-safe 0.13.1 → 0.14.0
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/CHANGELOG.md +14 -1
- package/dist/file-store-sync-directory.d.ts.map +1 -1
- package/dist/file-store-sync-directory.js +14 -7
- package/dist/guest-dispatch-python.js +1 -1
- package/dist/native-binding.d.ts +6 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +195 -161
- package/dist/native-policy-directory-observation.d.ts +12 -0
- package/dist/native-policy-directory-observation.d.ts.map +1 -0
- package/dist/native-policy-directory-observation.js +61 -0
- package/dist/pinned-mutation-admission.d.ts.map +1 -1
- package/dist/pinned-mutation-admission.js +13 -24
- package/dist/pinned-mutation-observation.d.ts +7 -1
- package/dist/pinned-mutation-observation.d.ts.map +1 -1
- package/dist/pinned-mutation-observation.js +87 -5
- package/dist/root-directory-creation.d.ts.map +1 -1
- package/dist/root-directory-creation.js +5 -4
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +2 -1
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +1 -2
- package/dist/root-write-compatibility.d.ts +1 -2
- package/dist/root-write-compatibility.d.ts.map +1 -1
- package/dist/root-write-compatibility.js +17 -68
- package/dist/root-write-complete-parent.d.ts.map +1 -1
- package/dist/root-write-complete-parent.js +3 -1
- package/dist/root-write-lock-binding.d.ts +15 -0
- package/dist/root-write-lock-binding.d.ts.map +1 -0
- package/dist/root-write-lock-binding.js +162 -0
- package/dist/staged-directory.d.ts +5 -2
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +37 -1
- package/dist/temp-workspace-admission.d.ts.map +1 -1
- package/dist/temp-workspace-admission.js +28 -4
- package/docs/guest.md +1 -1
- package/docs/native.md +8 -0
- package/docs/writing.md +6 -0
- package/package.json +8 -8
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { inspectDirectoryIdentitySync } from "./directory-guard.js";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { hasNodeErrorCode, isNotFoundPathError } from "./path.js";
|
|
6
|
+
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
7
|
+
import { realpathSync } from "./realpath.js";
|
|
8
|
+
const lockObservations = new WeakMap();
|
|
9
|
+
export function canReuseParentWithMutationAssertion(assertion, rootPath, targetPath) {
|
|
10
|
+
if (assertion === undefined)
|
|
11
|
+
return true;
|
|
12
|
+
const binding = lockObservations.get(assertion);
|
|
13
|
+
return binding?.rootPath === rootPath && binding.targetPath === targetPath;
|
|
14
|
+
}
|
|
15
|
+
function unsupportedSpelling() {
|
|
16
|
+
return new FsSafeError("path-alias", "Windows compatibility writes require lower-case ASCII destination components");
|
|
17
|
+
}
|
|
18
|
+
// Missing names have no portable case/Unicode lock key. Keep this admission
|
|
19
|
+
// narrower than general Root paths and never change keys after acquisition.
|
|
20
|
+
function effectiveDestination(rootPath, targetPath, rootIdentity) {
|
|
21
|
+
let effective;
|
|
22
|
+
let missingHint;
|
|
23
|
+
try {
|
|
24
|
+
effective = realpathSync.native(targetPath);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (!isNotFoundPathError(error))
|
|
28
|
+
throw error;
|
|
29
|
+
let cursor = targetPath;
|
|
30
|
+
const missing = [];
|
|
31
|
+
for (;;) {
|
|
32
|
+
try {
|
|
33
|
+
fs.lstatSync(cursor);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (!isNotFoundPathError(error) || cursor === rootPath)
|
|
38
|
+
throw error;
|
|
39
|
+
const parent = path.dirname(cursor);
|
|
40
|
+
if (parent === cursor)
|
|
41
|
+
throw error;
|
|
42
|
+
missing.unshift(path.basename(cursor));
|
|
43
|
+
cursor = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Existing dangling links must fail resolution, not become missing names.
|
|
47
|
+
const canonicalParent = realpathSync.native(cursor);
|
|
48
|
+
effective = path.join(canonicalParent, ...missing);
|
|
49
|
+
// The hint must describe the selected route exactly. The full resolver can
|
|
50
|
+
// normalize a missing raw `..` component out of the eventual lock path.
|
|
51
|
+
if (missing.length > 1 && path.resolve(canonicalParent) === canonicalParent &&
|
|
52
|
+
missing.every(component => /^[a-z0-9._-]+$/.test(component) &&
|
|
53
|
+
component !== "." && component !== ".." && !component.endsWith("."))) {
|
|
54
|
+
missingHint = Object.freeze({ parentPath: canonicalParent, parts: Object.freeze(missing), nextIndex: 0 });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const admitted = admitPathInsideRoot({ rootPath, candidatePath: effective, rootIdentity });
|
|
58
|
+
if (!admitted)
|
|
59
|
+
throw unsupportedSpelling();
|
|
60
|
+
const relative = admitted.relativePath;
|
|
61
|
+
if (!relative || !relative.split(path.sep).every(component => /^[a-z0-9._-]+$/.test(component) && !component.endsWith(".")))
|
|
62
|
+
throw unsupportedSpelling();
|
|
63
|
+
return {
|
|
64
|
+
path: admitted.path,
|
|
65
|
+
// Case-folded or rebased root spellings keep the full resolver and its
|
|
66
|
+
// existing root-identity admission at every mutation.
|
|
67
|
+
missing: admitted.admission === "exact" && admitted.path === effective ? missingHint : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// The hint stores only a location. Each call freshly binds the nearest existing
|
|
71
|
+
// prefix and its first absent child; no identity or authorization crosses calls.
|
|
72
|
+
function observeMissingDestination(hint) {
|
|
73
|
+
// A complete parent needs only the ordinary leaf check, not a prefix fence.
|
|
74
|
+
if (hint.parts.length - hint.nextIndex < 2)
|
|
75
|
+
return undefined;
|
|
76
|
+
try {
|
|
77
|
+
let parentPath = hint.parentPath;
|
|
78
|
+
let parent = inspectDirectoryIdentitySync(parentPath);
|
|
79
|
+
for (let nextIndex = hint.nextIndex; nextIndex < hint.parts.length; nextIndex++) {
|
|
80
|
+
const childPath = path.join(parentPath, hint.parts[nextIndex]);
|
|
81
|
+
let child;
|
|
82
|
+
try {
|
|
83
|
+
child = fs.lstatSync(childPath, { bigint: true });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (!hasNodeErrorCode(error, "ENOENT"))
|
|
87
|
+
return undefined;
|
|
88
|
+
if (realpathSync.native(parentPath) !== parentPath)
|
|
89
|
+
return undefined;
|
|
90
|
+
inspectDirectoryIdentitySync(parentPath, parent);
|
|
91
|
+
try {
|
|
92
|
+
fs.lstatSync(childPath);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (!hasNodeErrorCode(error, "ENOENT"))
|
|
96
|
+
return undefined;
|
|
97
|
+
// Absence can follow a renamed ancestor through a new alias while
|
|
98
|
+
// preserving the directory's identity. Finish at its current name.
|
|
99
|
+
if (realpathSync.native(parentPath) !== parentPath)
|
|
100
|
+
return undefined;
|
|
101
|
+
return Object.freeze({ parentPath, parts: hint.parts, nextIndex });
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
// An existing leaf always receives the historical full resolution,
|
|
106
|
+
// including final links, collisions and unsupported canonical spellings.
|
|
107
|
+
if (nextIndex === hint.parts.length - 1)
|
|
108
|
+
return undefined;
|
|
109
|
+
parent = inspectDirectoryIdentitySync(childPath, undefined, child);
|
|
110
|
+
parentPath = childPath;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Removal, redirection and uncertain observations only discard the hint.
|
|
115
|
+
// The full resolver below still determines the destination and error.
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
export function assertRootFallbackWritePath(expected, actual) {
|
|
120
|
+
if (expected !== undefined && actual !== expected) {
|
|
121
|
+
throw new FsSafeError("path-mismatch", "compatibility write destination changed after lock selection");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function createRootWriteLockBinding(params) {
|
|
125
|
+
const { rootPath } = params;
|
|
126
|
+
const rootIdentity = params.rootIdentity && Object.freeze({
|
|
127
|
+
dev: params.rootIdentity.dev, ino: params.rootIdentity.ino,
|
|
128
|
+
});
|
|
129
|
+
const selected = effectiveDestination(rootPath, params.targetPath, rootIdentity);
|
|
130
|
+
const targetPath = selected.path;
|
|
131
|
+
let missingHint = selected.missing;
|
|
132
|
+
const relativePath = path.relative(rootPath, targetPath);
|
|
133
|
+
let active = true;
|
|
134
|
+
const assertCurrent = () => {
|
|
135
|
+
if (!active)
|
|
136
|
+
throw new FsSafeError("path-mismatch", "compatibility lock observation expired");
|
|
137
|
+
const observed = missingHint && observeMissingDestination(missingHint);
|
|
138
|
+
if (observed) {
|
|
139
|
+
missingHint = observed;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
missingHint = undefined;
|
|
143
|
+
const current = effectiveDestination(rootPath, targetPath, rootIdentity);
|
|
144
|
+
assertRootFallbackWritePath(targetPath, current.path);
|
|
145
|
+
missingHint = current.missing;
|
|
146
|
+
};
|
|
147
|
+
// Only this library-owned, observation-only closure permits parent reuse.
|
|
148
|
+
// Callers cannot obtain that status by setting properties on their callback.
|
|
149
|
+
lockObservations.set(assertCurrent, Object.freeze({ rootPath, targetPath }));
|
|
150
|
+
return Object.freeze({
|
|
151
|
+
targetPath,
|
|
152
|
+
relativePath,
|
|
153
|
+
// The existing lock protocol uses slashes; the writer needs native spelling.
|
|
154
|
+
relativeLockPath: relativePath.split(path.sep).join("/"),
|
|
155
|
+
assertCurrent,
|
|
156
|
+
dispose() {
|
|
157
|
+
active = false;
|
|
158
|
+
missingHint = undefined;
|
|
159
|
+
lockObservations.delete(assertCurrent);
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { type BigIntStats } from "node:fs";
|
|
2
2
|
import type { DirectoryReceipt } from "./directory-durability.js";
|
|
3
3
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
4
|
-
import {
|
|
4
|
+
import type { NativeBinding } from "./native-binding.js";
|
|
5
|
+
import { type MutationDirectoryObservation, type MutationDirectoryObserver } from "./pinned-mutation-observation.js";
|
|
5
6
|
import type { StagedFileReceipt } from "./staged-file-types.js";
|
|
6
7
|
export type StagedDirectorySnapshot = StagedFileReceipt["directory"];
|
|
7
8
|
export type PolicyStagedDirectory = Readonly<{
|
|
8
9
|
directory: StagedDirectorySnapshot;
|
|
9
10
|
observation: MutationDirectoryObservation;
|
|
10
11
|
stat: BigIntStats;
|
|
12
|
+
observeCurrent?: MutationDirectoryObserver;
|
|
13
|
+
disposeObservation?(): void;
|
|
11
14
|
}>;
|
|
12
15
|
export declare function exactIdentityMatches(expected: FileIdentityStat, actual: Readonly<{
|
|
13
16
|
dev: bigint;
|
|
@@ -15,7 +18,7 @@ export declare function exactIdentityMatches(expected: FileIdentityStat, actual:
|
|
|
15
18
|
}>): boolean;
|
|
16
19
|
export declare function describeStagedDirectory(fd: number, pathname: string): StagedDirectorySnapshot;
|
|
17
20
|
export declare function assertStagedDirectoryCurrent(receipt: StagedDirectorySnapshot): BigIntStats;
|
|
18
|
-
export declare function describePolicyStagedDirectory(fd: number, pathname: string): PolicyStagedDirectory;
|
|
21
|
+
export declare function describePolicyStagedDirectory(fd: number, pathname: string, binding?: NativeBinding): PolicyStagedDirectory;
|
|
19
22
|
export declare function assertPolicyStagedDirectoryCurrent(captured: PolicyStagedDirectory): BigIntStats;
|
|
20
23
|
export declare function refreshPolicyStagedDirectoryObservation(captured: PolicyStagedDirectory): MutationDirectoryObservation;
|
|
21
24
|
export declare function openStagedDirectory(directory: string | DirectoryReceipt): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staged-directory.d.ts","sourceRoot":"","sources":["../src/staged-directory.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,EAEL,KAAK,4BAA4B,
|
|
1
|
+
{"version":3,"file":"staged-directory.d.ts","sourceRoot":"","sources":["../src/staged-directory.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAEL,KAAK,4BAA4B,EACjC,KAAK,yBAAyB,EAE/B,MAAM,kCAAkC,CAAC;AAE1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAGhE,MAAM,MAAM,uBAAuB,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;AAErE,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,SAAS,EAAE,uBAAuB,CAAC;IACnC,WAAW,EAAE,4BAA4B,CAAC;IAC1C,IAAI,EAAE,WAAW,CAAC;IAClB,cAAc,CAAC,EAAE,yBAAyB,CAAC;IAC3C,kBAAkB,CAAC,IAAI,IAAI,CAAC;CAC7B,CAAC,CAAC;AAEH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,GAC7C,OAAO,CAKT;AAED,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,uBAAuB,CAY7F;AAED,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,uBAAuB,GAAG,WAAW,CAS1F;AASD,wBAAgB,6BAA6B,CAC3C,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,aAAa,GACtB,qBAAqB,CA2CvB;AAED,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,qBAAqB,GAC9B,WAAW,CAeb;AAED,wBAAgB,uCAAuC,CACrD,QAAQ,EAAE,qBAAqB,GAC9B,4BAA4B,CAoB9B;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,GAAG;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,uBAAuB,CAAC;CAClC,CAiCA"}
|
package/dist/staged-directory.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs, {} from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { inspectDirectoryIdentitySync } from "./directory-guard.js";
|
|
5
|
+
import { nativePolicyDirectoryObserver, NativePolicyDirectoryMismatch } from "./native-policy-directory-observation.js";
|
|
5
6
|
import { checkedMutationDirectory, } from "./pinned-mutation-observation.js";
|
|
6
7
|
import { realpathSync } from "./realpath.js";
|
|
7
8
|
import { resolvePathPreservingWindowsRoot } from "./windows-path-alias.js";
|
|
@@ -38,12 +39,31 @@ function sameDirectoryMetadata(left, right) {
|
|
|
38
39
|
}
|
|
39
40
|
// The policy hot path is Node/POSIX-only. It deliberately uses native
|
|
40
41
|
// canonicalization so a successful capture contains no JS realpath work.
|
|
41
|
-
export function describePolicyStagedDirectory(fd, pathname) {
|
|
42
|
+
export function describePolicyStagedDirectory(fd, pathname, binding) {
|
|
42
43
|
const resolved = path.resolve(pathname);
|
|
43
44
|
const descriptor = fs.fstatSync(fd, { bigint: true });
|
|
44
45
|
if (!descriptor.isDirectory()) {
|
|
45
46
|
throw new FsSafeError("not-file", "staging parent must be a directory");
|
|
46
47
|
}
|
|
48
|
+
const observeCurrent = nativePolicyDirectoryObserver(binding, fd, resolved);
|
|
49
|
+
const observed = observeCurrent?.();
|
|
50
|
+
if (observed) {
|
|
51
|
+
if (!sameDirectoryMetadata(descriptor, observed.identity)) {
|
|
52
|
+
throw new NativePolicyDirectoryMismatch();
|
|
53
|
+
}
|
|
54
|
+
const directory = Object.freeze({
|
|
55
|
+
path: resolved,
|
|
56
|
+
realPath: observed.canonicalPath,
|
|
57
|
+
identity: Object.freeze({ dev: descriptor.dev, ino: descriptor.ino }),
|
|
58
|
+
});
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
directory,
|
|
61
|
+
observation: checkedMutationDirectory(resolved, observed.canonicalPath, observed.identity, observeCurrent),
|
|
62
|
+
stat: descriptor,
|
|
63
|
+
observeCurrent,
|
|
64
|
+
disposeObservation: observeCurrent.dispose,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
47
67
|
const before = inspectDirectoryIdentitySync(resolved);
|
|
48
68
|
const canonicalPath = path.resolve(realpathSync.native(resolved));
|
|
49
69
|
const after = inspectDirectoryIdentitySync(resolved, descriptor);
|
|
@@ -64,6 +84,14 @@ export function describePolicyStagedDirectory(fd, pathname) {
|
|
|
64
84
|
});
|
|
65
85
|
}
|
|
66
86
|
export function assertPolicyStagedDirectoryCurrent(captured) {
|
|
87
|
+
const observed = captured.observeCurrent?.();
|
|
88
|
+
if (observed) {
|
|
89
|
+
if (!sameDirectoryMetadata(captured.stat, observed.identity) ||
|
|
90
|
+
observed.canonicalPath !== captured.directory.realPath) {
|
|
91
|
+
throw new FsSafeError("path-mismatch", "staging directory pathname changed");
|
|
92
|
+
}
|
|
93
|
+
return captured.stat;
|
|
94
|
+
}
|
|
67
95
|
const current = inspectDirectoryIdentitySync(captured.directory.path, captured.directory.identity);
|
|
68
96
|
if (!sameDirectoryMetadata(captured.stat, current) ||
|
|
69
97
|
path.resolve(realpathSync.native(captured.directory.path)) !== captured.directory.realPath) {
|
|
@@ -72,6 +100,14 @@ export function assertPolicyStagedDirectoryCurrent(captured) {
|
|
|
72
100
|
return current;
|
|
73
101
|
}
|
|
74
102
|
export function refreshPolicyStagedDirectoryObservation(captured) {
|
|
103
|
+
const observed = captured.observeCurrent?.();
|
|
104
|
+
if (observed) {
|
|
105
|
+
if (!exactIdentityMatches(captured.directory.identity, observed.identity) ||
|
|
106
|
+
observed.canonicalPath !== captured.directory.realPath) {
|
|
107
|
+
throw new FsSafeError("path-mismatch", "staging directory pathname changed");
|
|
108
|
+
}
|
|
109
|
+
return checkedMutationDirectory(captured.directory.path, observed.canonicalPath, observed.identity, captured.observeCurrent);
|
|
110
|
+
}
|
|
75
111
|
const current = inspectDirectoryIdentitySync(captured.directory.path, captured.directory.identity);
|
|
76
112
|
const canonicalPath = path.resolve(realpathSync.native(captured.directory.path));
|
|
77
113
|
if (canonicalPath !== captured.directory.realPath) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"temp-workspace-admission.d.ts","sourceRoot":"","sources":["../src/temp-workspace-admission.ts"],"names":[],"mappings":"AA6BA,KAAK,aAAa,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAiB5D,MAAM,MAAM,0BAA0B,GAAG;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,aAAa,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,oBAAoB,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClD,aAAa,IAAI,IAAI,CAAC;IACtB,cAAc,IAAI,IAAI,CAAC;IACvB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/C,CAAC;
|
|
1
|
+
{"version":3,"file":"temp-workspace-admission.d.ts","sourceRoot":"","sources":["../src/temp-workspace-admission.ts"],"names":[],"mappings":"AA6BA,KAAK,aAAa,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAiB5D,MAAM,MAAM,0BAA0B,GAAG;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,aAAa,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,oBAAoB,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClD,aAAa,IAAI,IAAI,CAAC;IACtB,cAAc,IAAI,IAAI,CAAC;IACvB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/C,CAAC;AAsWF,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,GAAG,0BAA0B,CAM1F;AAED,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAyBjG;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,0BAA0B,CAsBtF"}
|
|
@@ -116,6 +116,30 @@ function assertSnapshot(entry, uid) {
|
|
|
116
116
|
assertTrustedTempWorkspaceDirectory(stat, uid);
|
|
117
117
|
assertCanonicalRoot(entry);
|
|
118
118
|
}
|
|
119
|
+
function snapshotMissingRootComponent(parent, dir, ownerUid) {
|
|
120
|
+
if (parent.dir !== parent.realPath) {
|
|
121
|
+
assertSnapshot(parent, ownerUid);
|
|
122
|
+
return snapshot(dir, ownerUid);
|
|
123
|
+
}
|
|
124
|
+
const current = inspectSnapshotIdentity(parent);
|
|
125
|
+
assertTrustedTempWorkspaceDirectory(current, ownerUid);
|
|
126
|
+
let realPath;
|
|
127
|
+
try {
|
|
128
|
+
realPath = canonicalTempWorkspacePath(dir);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
// Keep parent resolution failures ahead of a missing or unreadable child.
|
|
132
|
+
assertCanonicalRoot(parent);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
if (path.dirname(realPath) !== parent.realPath) {
|
|
136
|
+
assertCanonicalRoot(parent);
|
|
137
|
+
return snapshot(dir, ownerUid);
|
|
138
|
+
}
|
|
139
|
+
// The child's canonical parent confirms the same parent name at this phase.
|
|
140
|
+
// Its exact non-symlink/owner observation still precedes mode initialization.
|
|
141
|
+
return snapshot(dir, ownerUid, realPath);
|
|
142
|
+
}
|
|
119
143
|
function assertChain(chain, uid) {
|
|
120
144
|
for (const entry of chain) {
|
|
121
145
|
const current = inspectSnapshotIdentity(entry);
|
|
@@ -321,6 +345,7 @@ export async function admitTempWorkspaceRoot(rootDir) {
|
|
|
321
345
|
return admission;
|
|
322
346
|
const guardedChain = chain;
|
|
323
347
|
for (const segment of missing) {
|
|
348
|
+
const parentEntry = guardedChain[guardedChain.length - 1];
|
|
324
349
|
const parent = rootAdmission(guardedChain, ownerUid);
|
|
325
350
|
const dir = path.join(parent.dir, segment);
|
|
326
351
|
assertNoWindowsPathAlias(dir, "filesystem");
|
|
@@ -334,8 +359,7 @@ export async function admitTempWorkspaceRoot(rootDir) {
|
|
|
334
359
|
if (error.code !== "EEXIST")
|
|
335
360
|
throw error;
|
|
336
361
|
}
|
|
337
|
-
|
|
338
|
-
const observed = snapshot(dir, ownerUid);
|
|
362
|
+
const observed = snapshotMissingRootComponent(parentEntry, dir, ownerUid);
|
|
339
363
|
if (created) {
|
|
340
364
|
const modeInitialization = admitTempWorkspaceChild(dir, observed.stat, parent, 0o700);
|
|
341
365
|
if (modeInitialization)
|
|
@@ -351,6 +375,7 @@ export function admitTempWorkspaceRootSync(rootDir) {
|
|
|
351
375
|
return admission;
|
|
352
376
|
const guardedChain = chain;
|
|
353
377
|
for (const segment of missing) {
|
|
378
|
+
const parentEntry = guardedChain[guardedChain.length - 1];
|
|
354
379
|
const parent = rootAdmission(guardedChain, ownerUid);
|
|
355
380
|
const dir = path.join(parent.dir, segment);
|
|
356
381
|
assertNoWindowsPathAlias(dir, "filesystem");
|
|
@@ -364,8 +389,7 @@ export function admitTempWorkspaceRootSync(rootDir) {
|
|
|
364
389
|
if (error.code !== "EEXIST")
|
|
365
390
|
throw error;
|
|
366
391
|
}
|
|
367
|
-
|
|
368
|
-
const observed = snapshot(dir, ownerUid);
|
|
392
|
+
const observed = snapshotMissingRootComponent(parentEntry, dir, ownerUid);
|
|
369
393
|
if (created)
|
|
370
394
|
admitTempWorkspaceChildSync(dir, observed.stat, parent, 0o700);
|
|
371
395
|
guardedChain.push(observed.entry);
|
package/docs/guest.md
CHANGED
|
@@ -80,7 +80,7 @@ disables. Do not omit required fields.
|
|
|
80
80
|
| Rename | `rename srcRoot srcParent srcBasename dstRoot dstParent dstBasename mkdir` | Rename with cross-device copy/delete fallback. |
|
|
81
81
|
| Remove | `remove root parent basename recursive force` | Removes the leaf, or recursively removes its tree. |
|
|
82
82
|
| Make directories | `mkdirp root directory` | Creates missing relative directory components. |
|
|
83
|
-
| List directory | `readdir root directory` | JSON array of `{ name, isDirectory }`; no sorting guarantee. |
|
|
83
|
+
| List directory | `readdir root directory` | JSON array of `{ name, isDirectory, isFile }`; both kind fields are false for symlinks and special entries; no sorting guarantee. |
|
|
84
84
|
|
|
85
85
|
The complete program rejects empty, `.`, `..`, slash-containing, and NUL
|
|
86
86
|
basenames before opening roots or creating parents. Both leaf operands of copy
|
package/docs/native.md
CHANGED
|
@@ -206,6 +206,14 @@ that fallback does not apply to POSIX no-read modes.
|
|
|
206
206
|
|
|
207
207
|
## JavaScript fallback guarantees and delta
|
|
208
208
|
|
|
209
|
+
Policy-bound parent creation can refresh exact directory facts through the
|
|
210
|
+
retained POSIX descriptor. The optional native observer compares descriptor and
|
|
211
|
+
no-follow pathname metadata and verifies the descriptor's canonical path before
|
|
212
|
+
and after the observation. Ordinary paths can share that observation within one
|
|
213
|
+
synchronous admission phase; callbacks, mutations, and later phases require fresh
|
|
214
|
+
evidence. Unsupported helpers retain the guarded pathname checks. Policy and
|
|
215
|
+
denied-path decisions remain in TypeScript.
|
|
216
|
+
|
|
209
217
|
Public policy does not change with the selected mechanism: traversal and link
|
|
210
218
|
rejection, archive filters/limits/modes, exclusive target creation, source and
|
|
211
219
|
target identity fencing, publication cleanup receipts, and secret/lock policy
|
package/docs/writing.md
CHANGED
|
@@ -537,6 +537,12 @@ Windows `Root.write()` and `Root.writeJson()` honor this policy both as a Root d
|
|
|
537
537
|
|
|
538
538
|
The Windows buffered compatibility path resolves permitted in-root aliases before choosing its lock and binds publication to that effective destination. With the existing lock protocol, effective path components beneath the Root must contain only lower-case ASCII letters, digits, `.`, `_`, or `-`, with no trailing `.`. Unsupported spellings, including missing upper-case or non-ASCII names, fail with `path-alias` before mutation; no filesystem case-sensitivity or Unicode-folding behavior is guessed. This restriction does not apply to strict writes. Opaque Windows pathname identities still use strict verification against the retained original descriptor: they never, by themselves, authorize content-based acceptance of a replacement.
|
|
539
539
|
|
|
540
|
+
The library's own lock-destination check permits the writer to reuse parent
|
|
541
|
+
admission within that operation. The lock key and destination checks remain
|
|
542
|
+
unchanged, and evidence is revoked when the locked operation finishes. Supplying
|
|
543
|
+
an `assertBeforeMutation` callback retains full parent admission because caller
|
|
544
|
+
code can change the filesystem before dispatch.
|
|
545
|
+
|
|
540
546
|
Lock recovery is fail-closed. If a process crashes and leaves the root-level `.fs-safe-write-<sha256>.lock`, a later write reports the stale lock instead of deleting it based on a host-local PID. Recover only under external authority that excludes every competing writer; see [File lock](sidecar-lock.md#stale-recovery-guarded-remove-if-unchanged).
|
|
541
547
|
|
|
542
548
|
## See also
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/fs-safe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"filesystem",
|
|
@@ -167,13 +167,13 @@
|
|
|
167
167
|
"archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
|
|
168
168
|
},
|
|
169
169
|
"optionalDependencies": {
|
|
170
|
-
"@openclaw/fs-safe-darwin-arm64": "0.
|
|
171
|
-
"@openclaw/fs-safe-darwin-x64": "0.
|
|
172
|
-
"@openclaw/fs-safe-linux-arm64-gnu": "0.
|
|
173
|
-
"@openclaw/fs-safe-linux-arm64-musl": "0.
|
|
174
|
-
"@openclaw/fs-safe-linux-x64-gnu": "0.
|
|
175
|
-
"@openclaw/fs-safe-linux-x64-musl": "0.
|
|
176
|
-
"@openclaw/fs-safe-win32-x64-msvc": "0.
|
|
170
|
+
"@openclaw/fs-safe-darwin-arm64": "0.14.0",
|
|
171
|
+
"@openclaw/fs-safe-darwin-x64": "0.14.0",
|
|
172
|
+
"@openclaw/fs-safe-linux-arm64-gnu": "0.14.0",
|
|
173
|
+
"@openclaw/fs-safe-linux-arm64-musl": "0.14.0",
|
|
174
|
+
"@openclaw/fs-safe-linux-x64-gnu": "0.14.0",
|
|
175
|
+
"@openclaw/fs-safe-linux-x64-musl": "0.14.0",
|
|
176
|
+
"@openclaw/fs-safe-win32-x64-msvc": "0.14.0",
|
|
177
177
|
"jszip": "^3.10.2"
|
|
178
178
|
},
|
|
179
179
|
"devDependencies": {
|