@openclaw/fs-safe 0.1.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 +15 -0
- package/LICENSE +21 -0
- package/README.md +390 -0
- package/SECURITY.md +12 -0
- package/dist/absolute-path.d.ts +19 -0
- package/dist/absolute-path.d.ts.map +1 -0
- package/dist/absolute-path.js +98 -0
- package/dist/advanced.d.ts +22 -0
- package/dist/advanced.d.ts.map +1 -0
- package/dist/advanced.js +24 -0
- package/dist/archive-entry.d.ts +13 -0
- package/dist/archive-entry.d.ts.map +1 -0
- package/dist/archive-entry.js +47 -0
- package/dist/archive-kind.d.ts +8 -0
- package/dist/archive-kind.d.ts.map +1 -0
- package/dist/archive-kind.js +58 -0
- package/dist/archive-limits.d.ts +40 -0
- package/dist/archive-limits.d.ts.map +1 -0
- package/dist/archive-limits.js +92 -0
- package/dist/archive-staging.d.ts +26 -0
- package/dist/archive-staging.d.ts.map +1 -0
- package/dist/archive-staging.js +171 -0
- package/dist/archive-tar.d.ts +14 -0
- package/dist/archive-tar.d.ts.map +1 -0
- package/dist/archive-tar.js +52 -0
- package/dist/archive-utils.d.ts +3 -0
- package/dist/archive-utils.d.ts.map +1 -0
- package/dist/archive-utils.js +26 -0
- package/dist/archive-zip-preflight.d.ts +7 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -0
- package/dist/archive-zip-preflight.js +170 -0
- package/dist/archive.d.ts +24 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +251 -0
- package/dist/async-lock.d.ts +2 -0
- package/dist/async-lock.d.ts.map +1 -0
- package/dist/async-lock.js +17 -0
- package/dist/atomic.d.ts +5 -0
- package/dist/atomic.d.ts.map +1 -0
- package/dist/atomic.js +4 -0
- package/dist/boundary-file-read.d.ts +44 -0
- package/dist/boundary-file-read.d.ts.map +1 -0
- package/dist/boundary-file-read.js +129 -0
- package/dist/boundary-path.d.ts +39 -0
- package/dist/boundary-path.d.ts.map +1 -0
- package/dist/boundary-path.js +598 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +20 -0
- package/dist/file-identity.d.ts +6 -0
- package/dist/file-identity.d.ts.map +1 -0
- package/dist/file-identity.js +14 -0
- package/dist/file-lock.d.ts +20 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +34 -0
- package/dist/file-store.d.ts +66 -0
- package/dist/file-store.d.ts.map +1 -0
- package/dist/file-store.js +436 -0
- package/dist/file-url.d.ts +2 -0
- package/dist/file-url.d.ts.map +1 -0
- package/dist/file-url.js +1 -0
- package/dist/filename.d.ts +2 -0
- package/dist/filename.d.ts.map +1 -0
- package/dist/filename.js +25 -0
- package/dist/fs-pinned-path-helper.d.ts +7 -0
- package/dist/fs-pinned-path-helper.d.ts.map +1 -0
- package/dist/fs-pinned-path-helper.js +182 -0
- package/dist/fs-pinned-write-helper.d.ts +21 -0
- package/dist/fs-pinned-write-helper.d.ts.map +1 -0
- package/dist/fs-pinned-write-helper.js +263 -0
- package/dist/fs.d.ts +13 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +30 -0
- package/dist/hardlink-guards.d.ts +7 -0
- package/dist/hardlink-guards.d.ts.map +1 -0
- package/dist/hardlink-guards.js +30 -0
- package/dist/home-dir.d.ts +22 -0
- package/dist/home-dir.d.ts.map +1 -0
- package/dist/home-dir.js +107 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/install-path.d.ts +20 -0
- package/dist/install-path.d.ts.map +1 -0
- package/dist/install-path.js +94 -0
- package/dist/install-safe-path.d.ts +20 -0
- package/dist/install-safe-path.d.ts.map +1 -0
- package/dist/install-safe-path.js +94 -0
- package/dist/json-document-store.d.ts +30 -0
- package/dist/json-document-store.d.ts.map +1 -0
- package/dist/json-document-store.js +68 -0
- package/dist/json-file.d.ts +3 -0
- package/dist/json-file.d.ts.map +1 -0
- package/dist/json-file.js +123 -0
- package/dist/json-files.d.ts +20 -0
- package/dist/json-files.d.ts.map +1 -0
- package/dist/json-files.js +153 -0
- package/dist/json-store.d.ts +9 -0
- package/dist/json-store.d.ts.map +1 -0
- package/dist/json-store.js +12 -0
- package/dist/json.d.ts +17 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +198 -0
- package/dist/local-file-access.d.ts +8 -0
- package/dist/local-file-access.d.ts.map +1 -0
- package/dist/local-file-access.js +73 -0
- package/dist/local-roots.d.ts +26 -0
- package/dist/local-roots.d.ts.map +1 -0
- package/dist/local-roots.js +150 -0
- package/dist/mode.d.ts +2 -0
- package/dist/mode.d.ts.map +1 -0
- package/dist/mode.js +3 -0
- package/dist/move-path.d.ts +6 -0
- package/dist/move-path.d.ts.map +1 -0
- package/dist/move-path.js +18 -0
- package/dist/path-alias-guards.d.ts +19 -0
- package/dist/path-alias-guards.d.ts.map +1 -0
- package/dist/path-alias-guards.js +21 -0
- package/dist/path-guards.d.ts +7 -0
- package/dist/path-guards.d.ts.map +1 -0
- package/dist/path-guards.js +49 -0
- package/dist/path-policy.d.ts +25 -0
- package/dist/path-policy.d.ts.map +1 -0
- package/dist/path-policy.js +50 -0
- package/dist/path-safety.d.ts +12 -0
- package/dist/path-safety.d.ts.map +1 -0
- package/dist/path-safety.js +50 -0
- package/dist/path-scope.d.ts +2 -0
- package/dist/path-scope.d.ts.map +1 -0
- package/dist/path-scope.js +1 -0
- package/dist/path.d.ts +18 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +123 -0
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -0
- package/dist/permissions-public.js +1 -0
- package/dist/permissions.d.ts +86 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +435 -0
- package/dist/pinned-helper.d.ts +9 -0
- package/dist/pinned-helper.d.ts.map +1 -0
- package/dist/pinned-helper.js +19 -0
- package/dist/pinned-open.d.ts +24 -0
- package/dist/pinned-open.d.ts.map +1 -0
- package/dist/pinned-open.js +71 -0
- package/dist/pinned-path.d.ts +7 -0
- package/dist/pinned-path.d.ts.map +1 -0
- package/dist/pinned-path.js +21 -0
- package/dist/pinned-python-config.d.ts +9 -0
- package/dist/pinned-python-config.d.ts.map +1 -0
- package/dist/pinned-python-config.js +39 -0
- package/dist/pinned-python.d.ts +12 -0
- package/dist/pinned-python.d.ts.map +1 -0
- package/dist/pinned-python.js +599 -0
- package/dist/pinned-write.d.ts +33 -0
- package/dist/pinned-write.d.ts.map +1 -0
- package/dist/pinned-write.js +198 -0
- package/dist/private-file-store.d.ts +60 -0
- package/dist/private-file-store.d.ts.map +1 -0
- package/dist/private-file-store.js +233 -0
- package/dist/private-temp-workspace.d.ts +39 -0
- package/dist/private-temp-workspace.d.ts.map +1 -0
- package/dist/private-temp-workspace.js +185 -0
- package/dist/regular-file.d.ts +38 -0
- package/dist/regular-file.d.ts.map +1 -0
- package/dist/regular-file.js +239 -0
- package/dist/replace-directory.d.ts +7 -0
- package/dist/replace-directory.d.ts.map +1 -0
- package/dist/replace-directory.js +32 -0
- package/dist/replace-file.d.ts +41 -0
- package/dist/replace-file.d.ts.map +1 -0
- package/dist/replace-file.js +325 -0
- package/dist/root-file.d.ts +44 -0
- package/dist/root-file.d.ts.map +1 -0
- package/dist/root-file.js +129 -0
- package/dist/root-path.d.ts +39 -0
- package/dist/root-path.d.ts.map +1 -0
- package/dist/root-path.js +598 -0
- package/dist/root-paths.d.ts +92 -0
- package/dist/root-paths.d.ts.map +1 -0
- package/dist/root-paths.js +338 -0
- package/dist/root.d.ts +108 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +1332 -0
- package/dist/safe-open-sync.d.ts +24 -0
- package/dist/safe-open-sync.d.ts.map +1 -0
- package/dist/safe-open-sync.js +71 -0
- package/dist/safe-root.d.ts +123 -0
- package/dist/safe-root.d.ts.map +1 -0
- package/dist/safe-root.js +1060 -0
- package/dist/secret-file.d.ts +17 -0
- package/dist/secret-file.d.ts.map +1 -0
- package/dist/secret-file.js +232 -0
- package/dist/secret.d.ts +2 -0
- package/dist/secret.d.ts.map +1 -0
- package/dist/secret.js +1 -0
- package/dist/secure-file.d.ts +32 -0
- package/dist/secure-file.d.ts.map +1 -0
- package/dist/secure-file.js +163 -0
- package/dist/secure-temp-dir.d.ts +27 -0
- package/dist/secure-temp-dir.d.ts.map +1 -0
- package/dist/secure-temp-dir.js +155 -0
- package/dist/secure-temp-workspace.d.ts +25 -0
- package/dist/secure-temp-workspace.d.ts.map +1 -0
- package/dist/secure-temp-workspace.js +136 -0
- package/dist/sibling-temp-file.d.ts +16 -0
- package/dist/sibling-temp-file.d.ts.map +1 -0
- package/dist/sibling-temp-file.js +73 -0
- package/dist/sibling-temp-write.d.ts +8 -0
- package/dist/sibling-temp-write.d.ts.map +1 -0
- package/dist/sibling-temp-write.js +40 -0
- package/dist/sibling-temp.d.ts +23 -0
- package/dist/sibling-temp.d.ts.map +1 -0
- package/dist/sibling-temp.js +119 -0
- package/dist/sidecar-lock.d.ts +50 -0
- package/dist/sidecar-lock.d.ts.map +1 -0
- package/dist/sidecar-lock.js +235 -0
- package/dist/store.d.ts +3 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +2 -0
- package/dist/string-coerce.d.ts +14 -0
- package/dist/string-coerce.d.ts.map +1 -0
- package/dist/string-coerce.js +72 -0
- package/dist/symlink-parents.d.ts +12 -0
- package/dist/symlink-parents.d.ts.map +1 -0
- package/dist/symlink-parents.js +78 -0
- package/dist/temp-cleanup.d.ts +6 -0
- package/dist/temp-cleanup.d.ts.map +1 -0
- package/dist/temp-cleanup.js +42 -0
- package/dist/temp-target.d.ts +28 -0
- package/dist/temp-target.d.ts.map +1 -0
- package/dist/temp-target.js +84 -0
- package/dist/temp.d.ts +3 -0
- package/dist/temp.d.ts.map +1 -0
- package/dist/temp.js +2 -0
- package/dist/test-hooks.d.ts +9 -0
- package/dist/test-hooks.d.ts.map +1 -0
- package/dist/test-hooks.js +13 -0
- package/dist/text-atomic.d.ts +7 -0
- package/dist/text-atomic.d.ts.map +1 -0
- package/dist/text-atomic.js +13 -0
- package/dist/timing.d.ts +6 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +24 -0
- package/dist/trash.d.ts +5 -0
- package/dist/trash.d.ts.map +1 -0
- package/dist/trash.js +127 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/walk.d.ts +26 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +169 -0
- package/dist/write-queue.d.ts +2 -0
- package/dist/write-queue.d.ts.map +1 -0
- package/dist/write-queue.js +18 -0
- package/package.json +117 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.js";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const INHERIT_FLAGS = new Set(["I", "OI", "CI", "IO", "NP"]);
|
|
9
|
+
const WORLD_PRINCIPALS = new Set([
|
|
10
|
+
"everyone",
|
|
11
|
+
"users",
|
|
12
|
+
"builtin\\users",
|
|
13
|
+
"authenticated users",
|
|
14
|
+
"nt authority\\authenticated users",
|
|
15
|
+
]);
|
|
16
|
+
const TRUSTED_BASE = new Set([
|
|
17
|
+
"nt authority\\system",
|
|
18
|
+
"system",
|
|
19
|
+
"builtin\\administrators",
|
|
20
|
+
"creator owner",
|
|
21
|
+
"autorite nt\\système",
|
|
22
|
+
"nt-autorität\\system",
|
|
23
|
+
"autoridad nt\\system",
|
|
24
|
+
"autoridade nt\\system",
|
|
25
|
+
]);
|
|
26
|
+
const WORLD_SUFFIXES = ["\\users", "\\authenticated users"];
|
|
27
|
+
const TRUSTED_SUFFIXES = ["\\administrators", "\\system", "\\système"];
|
|
28
|
+
const SID_RE = /^\*?s-\d+-\d+(-\d+)+$/i;
|
|
29
|
+
const TRUSTED_SIDS = new Set([
|
|
30
|
+
"s-1-5-18",
|
|
31
|
+
"s-1-5-32-544",
|
|
32
|
+
"s-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464",
|
|
33
|
+
]);
|
|
34
|
+
const WORLD_SIDS = new Set(["s-1-1-0", "s-1-5-11", "s-1-5-32-545"]);
|
|
35
|
+
const STATUS_PREFIXES = [
|
|
36
|
+
"successfully processed",
|
|
37
|
+
"processed",
|
|
38
|
+
"failed processing",
|
|
39
|
+
"no mapping between account names",
|
|
40
|
+
];
|
|
41
|
+
const normalize = (value) => normalizeLowercaseStringOrEmpty(value);
|
|
42
|
+
const defaultWindowsUserInfo = () => os.userInfo();
|
|
43
|
+
function defaultPermissionExec(command, args) {
|
|
44
|
+
return execFileAsync(command, args, {
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
windowsHide: true,
|
|
47
|
+
maxBuffer: 1024 * 1024,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export async function safeStat(targetPath) {
|
|
51
|
+
try {
|
|
52
|
+
const lst = await fs.lstat(targetPath);
|
|
53
|
+
return {
|
|
54
|
+
ok: true,
|
|
55
|
+
isSymlink: lst.isSymbolicLink(),
|
|
56
|
+
isDir: lst.isDirectory(),
|
|
57
|
+
mode: typeof lst.mode === "number" ? lst.mode : null,
|
|
58
|
+
uid: typeof lst.uid === "number" ? lst.uid : null,
|
|
59
|
+
gid: typeof lst.gid === "number" ? lst.gid : null,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
isSymlink: false,
|
|
66
|
+
isDir: false,
|
|
67
|
+
mode: null,
|
|
68
|
+
uid: null,
|
|
69
|
+
gid: null,
|
|
70
|
+
error: String(err),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function inspectPathPermissions(targetPath, opts) {
|
|
75
|
+
const st = await safeStat(targetPath);
|
|
76
|
+
if (!st.ok) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
isSymlink: false,
|
|
80
|
+
isDir: false,
|
|
81
|
+
mode: null,
|
|
82
|
+
bits: null,
|
|
83
|
+
source: "unknown",
|
|
84
|
+
worldWritable: false,
|
|
85
|
+
groupWritable: false,
|
|
86
|
+
worldReadable: false,
|
|
87
|
+
groupReadable: false,
|
|
88
|
+
error: st.error,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
let effectiveMode = st.mode;
|
|
92
|
+
let effectiveIsDir = st.isDir;
|
|
93
|
+
if (st.isSymlink) {
|
|
94
|
+
try {
|
|
95
|
+
const target = await fs.stat(targetPath);
|
|
96
|
+
effectiveMode = typeof target.mode === "number" ? target.mode : st.mode;
|
|
97
|
+
effectiveIsDir = target.isDirectory();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Keep lstat metadata when the symlink target cannot be inspected.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const bits = modeBits(effectiveMode);
|
|
104
|
+
const platform = opts?.platform ?? process.platform;
|
|
105
|
+
if (platform === "win32") {
|
|
106
|
+
const acl = await inspectWindowsAcl(targetPath, { env: opts?.env, exec: opts?.exec });
|
|
107
|
+
if (!acl.ok) {
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
isSymlink: st.isSymlink,
|
|
111
|
+
isDir: effectiveIsDir,
|
|
112
|
+
mode: effectiveMode,
|
|
113
|
+
bits,
|
|
114
|
+
source: "unknown",
|
|
115
|
+
worldWritable: false,
|
|
116
|
+
groupWritable: false,
|
|
117
|
+
worldReadable: false,
|
|
118
|
+
groupReadable: false,
|
|
119
|
+
error: acl.error,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
isSymlink: st.isSymlink,
|
|
125
|
+
isDir: effectiveIsDir,
|
|
126
|
+
mode: effectiveMode,
|
|
127
|
+
bits,
|
|
128
|
+
source: "windows-acl",
|
|
129
|
+
worldWritable: acl.untrustedWorld.some((entry) => entry.canWrite),
|
|
130
|
+
groupWritable: acl.untrustedGroup.some((entry) => entry.canWrite),
|
|
131
|
+
worldReadable: acl.untrustedWorld.some((entry) => entry.canRead),
|
|
132
|
+
groupReadable: acl.untrustedGroup.some((entry) => entry.canRead),
|
|
133
|
+
aclSummary: formatWindowsAclSummary(acl),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
isSymlink: st.isSymlink,
|
|
139
|
+
isDir: effectiveIsDir,
|
|
140
|
+
mode: effectiveMode,
|
|
141
|
+
bits,
|
|
142
|
+
source: "posix",
|
|
143
|
+
worldWritable: isWorldWritable(bits),
|
|
144
|
+
groupWritable: isGroupWritable(bits),
|
|
145
|
+
worldReadable: isWorldReadable(bits),
|
|
146
|
+
groupReadable: isGroupReadable(bits),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function formatPermissionDetail(targetPath, perms) {
|
|
150
|
+
if (perms.source === "windows-acl") {
|
|
151
|
+
return `${targetPath} acl=${perms.aclSummary ?? "unknown"}`;
|
|
152
|
+
}
|
|
153
|
+
return `${targetPath} mode=${formatOctal(perms.bits)}`;
|
|
154
|
+
}
|
|
155
|
+
export function formatPermissionRemediation(params) {
|
|
156
|
+
if (params.perms.source === "windows-acl") {
|
|
157
|
+
return formatIcaclsResetCommand(params.targetPath, {
|
|
158
|
+
isDir: params.isDir,
|
|
159
|
+
env: params.env,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return `chmod ${params.posixMode.toString(8).padStart(3, "0")} ${params.targetPath}`;
|
|
163
|
+
}
|
|
164
|
+
export function modeBits(mode) {
|
|
165
|
+
return mode == null ? null : mode & 0o777;
|
|
166
|
+
}
|
|
167
|
+
export function formatOctal(bits) {
|
|
168
|
+
return bits == null ? "unknown" : bits.toString(8).padStart(3, "0");
|
|
169
|
+
}
|
|
170
|
+
export function isWorldWritable(bits) {
|
|
171
|
+
return bits != null && (bits & 0o002) !== 0;
|
|
172
|
+
}
|
|
173
|
+
export function isGroupWritable(bits) {
|
|
174
|
+
return bits != null && (bits & 0o020) !== 0;
|
|
175
|
+
}
|
|
176
|
+
export function isWorldReadable(bits) {
|
|
177
|
+
return bits != null && (bits & 0o004) !== 0;
|
|
178
|
+
}
|
|
179
|
+
export function isGroupReadable(bits) {
|
|
180
|
+
return bits != null && (bits & 0o040) !== 0;
|
|
181
|
+
}
|
|
182
|
+
function normalizeSid(value) {
|
|
183
|
+
const normalized = normalize(value);
|
|
184
|
+
return normalized.startsWith("*") ? normalized.slice(1) : normalized;
|
|
185
|
+
}
|
|
186
|
+
export function resolveWindowsUserPrincipal(env, userInfo = defaultWindowsUserInfo) {
|
|
187
|
+
const username = env?.USERNAME?.trim() || userInfo().username?.trim();
|
|
188
|
+
if (!username) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const domain = env?.USERDOMAIN?.trim();
|
|
192
|
+
return domain ? `${domain}\\${username}` : username;
|
|
193
|
+
}
|
|
194
|
+
function buildTrustedPrincipals(env) {
|
|
195
|
+
const trusted = new Set(TRUSTED_BASE);
|
|
196
|
+
const principal = resolveWindowsUserPrincipal(env);
|
|
197
|
+
if (principal) {
|
|
198
|
+
trusted.add(normalize(principal));
|
|
199
|
+
const userOnly = principal.split("\\").at(-1);
|
|
200
|
+
if (userOnly) {
|
|
201
|
+
trusted.add(normalize(userOnly));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const userSid = normalizeSid(env?.USERSID ?? "");
|
|
205
|
+
if (userSid && SID_RE.test(userSid) && !WORLD_SIDS.has(userSid)) {
|
|
206
|
+
trusted.add(userSid);
|
|
207
|
+
}
|
|
208
|
+
return trusted;
|
|
209
|
+
}
|
|
210
|
+
function getEnvValueCaseInsensitive(env, name) {
|
|
211
|
+
const direct = env[name];
|
|
212
|
+
if (direct !== undefined) {
|
|
213
|
+
return direct;
|
|
214
|
+
}
|
|
215
|
+
const lower = name.toLowerCase();
|
|
216
|
+
for (const [key, value] of Object.entries(env)) {
|
|
217
|
+
if (key.toLowerCase() === lower) {
|
|
218
|
+
return value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
function normalizeWindowsInstallRoot(value) {
|
|
224
|
+
const trimmed = value?.trim();
|
|
225
|
+
if (!trimmed || !path.win32.isAbsolute(trimmed)) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return trimmed.replace(/[\\/]+$/, "");
|
|
229
|
+
}
|
|
230
|
+
function resolveWindowsSystemRoot(env) {
|
|
231
|
+
const source = env ?? process.env;
|
|
232
|
+
return (normalizeWindowsInstallRoot(getEnvValueCaseInsensitive(source, "SystemRoot")) ??
|
|
233
|
+
normalizeWindowsInstallRoot(getEnvValueCaseInsensitive(source, "WINDIR")) ??
|
|
234
|
+
"C:\\Windows");
|
|
235
|
+
}
|
|
236
|
+
function resolveWindowsSystemCommand(command, env) {
|
|
237
|
+
const root = resolveWindowsSystemRoot(env);
|
|
238
|
+
return path.win32.join(root, "System32", command);
|
|
239
|
+
}
|
|
240
|
+
function classifyPrincipal(principal, trustedPrincipals) {
|
|
241
|
+
const normalized = normalize(principal);
|
|
242
|
+
if (SID_RE.test(normalized)) {
|
|
243
|
+
const sid = normalizeSid(normalized);
|
|
244
|
+
if (WORLD_SIDS.has(sid))
|
|
245
|
+
return "world";
|
|
246
|
+
if (TRUSTED_SIDS.has(sid) || trustedPrincipals.has(sid))
|
|
247
|
+
return "trusted";
|
|
248
|
+
return "group";
|
|
249
|
+
}
|
|
250
|
+
if (trustedPrincipals.has(normalized) ||
|
|
251
|
+
TRUSTED_SUFFIXES.some((suffix) => normalized.endsWith(suffix))) {
|
|
252
|
+
return "trusted";
|
|
253
|
+
}
|
|
254
|
+
if (WORLD_PRINCIPALS.has(normalized) ||
|
|
255
|
+
WORLD_SUFFIXES.some((suffix) => normalized.endsWith(suffix))) {
|
|
256
|
+
return "world";
|
|
257
|
+
}
|
|
258
|
+
const stripped = normalized.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
259
|
+
if (stripped !== normalized &&
|
|
260
|
+
(TRUSTED_BASE.has(stripped) ||
|
|
261
|
+
TRUSTED_SUFFIXES.some((suffix) => {
|
|
262
|
+
const strippedSuffix = suffix.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
263
|
+
return stripped.endsWith(strippedSuffix);
|
|
264
|
+
}))) {
|
|
265
|
+
return "trusted";
|
|
266
|
+
}
|
|
267
|
+
return "group";
|
|
268
|
+
}
|
|
269
|
+
function rightsFromTokens(tokens) {
|
|
270
|
+
const upper = tokens.join("").toUpperCase();
|
|
271
|
+
return {
|
|
272
|
+
canWrite: upper.includes("F") || upper.includes("M") || upper.includes("W") || upper.includes("D"),
|
|
273
|
+
canRead: upper.includes("F") || upper.includes("M") || upper.includes("R"),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function stripTargetPrefix(params) {
|
|
277
|
+
if (params.lowerLine.startsWith(params.lowerTarget)) {
|
|
278
|
+
return params.trimmedLine.slice(params.normalizedTarget.length).trim();
|
|
279
|
+
}
|
|
280
|
+
if (params.lowerLine.startsWith(params.quotedLower)) {
|
|
281
|
+
return params.trimmedLine.slice(params.quotedTarget.length).trim();
|
|
282
|
+
}
|
|
283
|
+
return params.trimmedLine;
|
|
284
|
+
}
|
|
285
|
+
function parseAceEntry(entry) {
|
|
286
|
+
if (!entry.includes("("))
|
|
287
|
+
return null;
|
|
288
|
+
const idx = entry.indexOf(":");
|
|
289
|
+
if (idx === -1)
|
|
290
|
+
return null;
|
|
291
|
+
const principal = entry.slice(0, idx).trim();
|
|
292
|
+
const rawRights = entry.slice(idx + 1).trim();
|
|
293
|
+
const tokens = rawRights
|
|
294
|
+
.match(/\(([^)]+)\)/g)
|
|
295
|
+
?.map((token) => token.slice(1, -1).trim())
|
|
296
|
+
.filter(Boolean) ?? [];
|
|
297
|
+
if (tokens.some((token) => token.toUpperCase() === "DENY"))
|
|
298
|
+
return null;
|
|
299
|
+
const rights = tokens.filter((token) => !INHERIT_FLAGS.has(token.toUpperCase()));
|
|
300
|
+
if (rights.length === 0)
|
|
301
|
+
return null;
|
|
302
|
+
return { principal, rights, rawRights, ...rightsFromTokens(rights) };
|
|
303
|
+
}
|
|
304
|
+
export function parseIcaclsOutput(output, targetPath) {
|
|
305
|
+
const entries = [];
|
|
306
|
+
const normalizedTarget = targetPath.trim();
|
|
307
|
+
const lowerTarget = normalizedTarget.toLowerCase();
|
|
308
|
+
const quotedTarget = `"${normalizedTarget}"`;
|
|
309
|
+
const quotedLower = quotedTarget.toLowerCase();
|
|
310
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
311
|
+
const line = rawLine.trimEnd();
|
|
312
|
+
if (!line.trim())
|
|
313
|
+
continue;
|
|
314
|
+
const trimmed = line.trim();
|
|
315
|
+
const lower = trimmed.toLowerCase();
|
|
316
|
+
if (STATUS_PREFIXES.some((prefix) => lower.startsWith(prefix)))
|
|
317
|
+
continue;
|
|
318
|
+
const parsed = parseAceEntry(stripTargetPrefix({
|
|
319
|
+
trimmedLine: trimmed,
|
|
320
|
+
lowerLine: lower,
|
|
321
|
+
normalizedTarget,
|
|
322
|
+
lowerTarget,
|
|
323
|
+
quotedTarget,
|
|
324
|
+
quotedLower,
|
|
325
|
+
}));
|
|
326
|
+
if (parsed)
|
|
327
|
+
entries.push(parsed);
|
|
328
|
+
}
|
|
329
|
+
return entries;
|
|
330
|
+
}
|
|
331
|
+
export function summarizeWindowsAcl(entries, env) {
|
|
332
|
+
const trustedPrincipals = buildTrustedPrincipals(env);
|
|
333
|
+
const trusted = [];
|
|
334
|
+
const untrustedWorld = [];
|
|
335
|
+
const untrustedGroup = [];
|
|
336
|
+
for (const entry of entries) {
|
|
337
|
+
const classification = classifyPrincipal(entry.principal, trustedPrincipals);
|
|
338
|
+
if (classification === "trusted")
|
|
339
|
+
trusted.push(entry);
|
|
340
|
+
else if (classification === "world")
|
|
341
|
+
untrustedWorld.push(entry);
|
|
342
|
+
else
|
|
343
|
+
untrustedGroup.push(entry);
|
|
344
|
+
}
|
|
345
|
+
return { trusted, untrustedWorld, untrustedGroup };
|
|
346
|
+
}
|
|
347
|
+
async function resolveCurrentUserSid(exec, env) {
|
|
348
|
+
try {
|
|
349
|
+
const { stdout, stderr } = await exec(resolveWindowsSystemCommand("whoami.exe", env), [
|
|
350
|
+
"/user",
|
|
351
|
+
"/fo",
|
|
352
|
+
"csv",
|
|
353
|
+
"/nh",
|
|
354
|
+
]);
|
|
355
|
+
const match = `${stdout}\n${stderr}`.match(/\*?S-\d+-\d+(?:-\d+)+/i);
|
|
356
|
+
return match ? normalizeSid(match[0]) : null;
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
export async function inspectWindowsAcl(targetPath, opts) {
|
|
363
|
+
const exec = opts?.exec ?? defaultPermissionExec;
|
|
364
|
+
try {
|
|
365
|
+
const { stdout, stderr } = await exec(resolveWindowsSystemCommand("icacls.exe", opts?.env), [
|
|
366
|
+
targetPath,
|
|
367
|
+
"/sid",
|
|
368
|
+
]);
|
|
369
|
+
const entries = parseIcaclsOutput(`${stdout}\n${stderr}`.trim(), targetPath);
|
|
370
|
+
let effectiveEnv = opts?.env;
|
|
371
|
+
let { trusted, untrustedWorld, untrustedGroup } = summarizeWindowsAcl(entries, effectiveEnv);
|
|
372
|
+
const needsUserSidResolution = !effectiveEnv?.USERSID &&
|
|
373
|
+
untrustedGroup.some((entry) => SID_RE.test(normalize(entry.principal)));
|
|
374
|
+
if (needsUserSidResolution) {
|
|
375
|
+
const currentUserSid = await resolveCurrentUserSid(exec, effectiveEnv);
|
|
376
|
+
if (currentUserSid) {
|
|
377
|
+
effectiveEnv = { ...effectiveEnv, USERSID: currentUserSid };
|
|
378
|
+
({ trusted, untrustedWorld, untrustedGroup } = summarizeWindowsAcl(entries, effectiveEnv));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return { ok: true, entries, trusted, untrustedWorld, untrustedGroup };
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
return {
|
|
385
|
+
ok: false,
|
|
386
|
+
entries: [],
|
|
387
|
+
trusted: [],
|
|
388
|
+
untrustedWorld: [],
|
|
389
|
+
untrustedGroup: [],
|
|
390
|
+
error: String(err),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
export function formatWindowsAclSummary(summary) {
|
|
395
|
+
if (!summary.ok)
|
|
396
|
+
return "unknown";
|
|
397
|
+
const untrusted = [...summary.untrustedWorld, ...summary.untrustedGroup];
|
|
398
|
+
return untrusted.length === 0
|
|
399
|
+
? "trusted-only"
|
|
400
|
+
: untrusted.map((entry) => `${entry.principal}:${entry.rawRights}`).join(", ");
|
|
401
|
+
}
|
|
402
|
+
export function formatIcaclsResetCommand(targetPath, opts) {
|
|
403
|
+
const command = resolveWindowsSystemCommand("icacls.exe", opts.env);
|
|
404
|
+
const user = resolveWindowsUserPrincipal(opts.env, opts.userInfo) ?? "%USERNAME%";
|
|
405
|
+
const grant = opts.isDir ? "(OI)(CI)F" : "F";
|
|
406
|
+
return [
|
|
407
|
+
command,
|
|
408
|
+
`"${targetPath}"`,
|
|
409
|
+
"/inheritance:r",
|
|
410
|
+
"/grant:r",
|
|
411
|
+
`"${user}:${grant}"`,
|
|
412
|
+
"/grant:r",
|
|
413
|
+
`"*S-1-5-18:${grant}"`,
|
|
414
|
+
].join(" ");
|
|
415
|
+
}
|
|
416
|
+
export function createIcaclsResetCommand(targetPath, opts) {
|
|
417
|
+
const user = resolveWindowsUserPrincipal(opts.env, opts.userInfo);
|
|
418
|
+
if (!user) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
const grant = opts.isDir ? "(OI)(CI)F" : "F";
|
|
422
|
+
const args = [
|
|
423
|
+
targetPath,
|
|
424
|
+
"/inheritance:r",
|
|
425
|
+
"/grant:r",
|
|
426
|
+
`${user}:${grant}`,
|
|
427
|
+
"/grant:r",
|
|
428
|
+
`*S-1-5-18:${grant}`,
|
|
429
|
+
];
|
|
430
|
+
return {
|
|
431
|
+
command: resolveWindowsSystemCommand("icacls.exe", opts.env),
|
|
432
|
+
args,
|
|
433
|
+
display: formatIcaclsResetCommand(targetPath, opts),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DirEntry, PathStat } from "./types.js";
|
|
2
|
+
import { isPinnedHelperUnavailable } from "./pinned-python.js";
|
|
3
|
+
type HelperOperation = "stat" | "readdir" | "mkdirp" | "remove" | "rename";
|
|
4
|
+
export { isPinnedHelperUnavailable };
|
|
5
|
+
export declare function runPinnedHelper<T>(operation: HelperOperation, rootDir: string, payload: Record<string, unknown>): Promise<T>;
|
|
6
|
+
export declare function helperStat(rootDir: string, relativePath: string): Promise<PathStat>;
|
|
7
|
+
export declare function helperReaddir(rootDir: string, relativePath: string, withFileTypes: false): Promise<string[]>;
|
|
8
|
+
export declare function helperReaddir(rootDir: string, relativePath: string, withFileTypes: true): Promise<DirEntry[]>;
|
|
9
|
+
//# sourceMappingURL=pinned-helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinned-helper.d.ts","sourceRoot":"","sources":["../src/pinned-helper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EACL,yBAAyB,EAG1B,MAAM,oBAAoB,CAAC;AAE5B,KAAK,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE3E,OAAO,EAAE,yBAAyB,EAAE,CAAC;AAErC,wBAAsB,eAAe,CAAC,CAAC,EACrC,SAAS,EAAE,eAAe,EAC1B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEzF;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,KAAK,GACnB,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AACrB,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,IAAI,GAClB,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isPinnedHelperUnavailable, runPinnedPythonOperation, validatePinnedOperationPayload, } from "./pinned-python.js";
|
|
2
|
+
export { isPinnedHelperUnavailable };
|
|
3
|
+
export async function runPinnedHelper(operation, rootDir, payload) {
|
|
4
|
+
validatePinnedOperationPayload(payload);
|
|
5
|
+
return await runPinnedPythonOperation({
|
|
6
|
+
operation,
|
|
7
|
+
rootPath: rootDir,
|
|
8
|
+
payload,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export async function helperStat(rootDir, relativePath) {
|
|
12
|
+
return await runPinnedHelper("stat", rootDir, { relativePath });
|
|
13
|
+
}
|
|
14
|
+
export async function helperReaddir(rootDir, relativePath, withFileTypes) {
|
|
15
|
+
return await runPinnedHelper("readdir", rootDir, {
|
|
16
|
+
relativePath,
|
|
17
|
+
withFileTypes,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
export type PinnedOpenSyncFailureReason = "path" | "validation" | "io";
|
|
3
|
+
export type PinnedOpenSyncResult = {
|
|
4
|
+
ok: true;
|
|
5
|
+
path: string;
|
|
6
|
+
fd: number;
|
|
7
|
+
stat: fs.Stats;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
reason: PinnedOpenSyncFailureReason;
|
|
11
|
+
error?: unknown;
|
|
12
|
+
};
|
|
13
|
+
export type PinnedOpenSyncAllowedType = "file" | "directory";
|
|
14
|
+
export type PinnedOpenSyncFs = Pick<typeof fs, "constants" | "lstatSync" | "realpathSync" | "openSync" | "fstatSync" | "closeSync">;
|
|
15
|
+
export declare function openPinnedFileSync(params: {
|
|
16
|
+
filePath: string;
|
|
17
|
+
resolvedPath?: string;
|
|
18
|
+
rejectPathSymlink?: boolean;
|
|
19
|
+
rejectHardlinks?: boolean;
|
|
20
|
+
maxBytes?: number;
|
|
21
|
+
allowedType?: PinnedOpenSyncAllowedType;
|
|
22
|
+
ioFs?: PinnedOpenSyncFs;
|
|
23
|
+
}): PinnedOpenSyncResult;
|
|
24
|
+
//# sourceMappingURL=pinned-open.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAGzB,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7D,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,OAAO,EAAE,EACT,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CACpF,CAAC;AAYF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,GAAG,oBAAoB,CA2DvB"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { sameFileIdentity as hasSameFileIdentity } from "./file-identity.js";
|
|
3
|
+
function isExpectedPathError(error) {
|
|
4
|
+
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
|
|
5
|
+
return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
|
|
6
|
+
}
|
|
7
|
+
function sameFileIdentity(left, right) {
|
|
8
|
+
return hasSameFileIdentity(left, right);
|
|
9
|
+
}
|
|
10
|
+
export function openPinnedFileSync(params) {
|
|
11
|
+
const ioFs = params.ioFs ?? fs;
|
|
12
|
+
const allowedType = params.allowedType ?? "file";
|
|
13
|
+
const openReadFlags = ioFs.constants.O_RDONLY |
|
|
14
|
+
(typeof ioFs.constants.O_NOFOLLOW === "number" ? ioFs.constants.O_NOFOLLOW : 0);
|
|
15
|
+
let fd = null;
|
|
16
|
+
try {
|
|
17
|
+
if (params.rejectPathSymlink) {
|
|
18
|
+
const candidateStat = ioFs.lstatSync(params.filePath);
|
|
19
|
+
if (candidateStat.isSymbolicLink()) {
|
|
20
|
+
return { ok: false, reason: "validation" };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const realPath = params.resolvedPath ?? ioFs.realpathSync(params.filePath);
|
|
24
|
+
const preOpenStat = ioFs.lstatSync(realPath);
|
|
25
|
+
if (!isAllowedType(preOpenStat, allowedType)) {
|
|
26
|
+
return { ok: false, reason: "validation" };
|
|
27
|
+
}
|
|
28
|
+
if (params.rejectHardlinks && preOpenStat.isFile() && preOpenStat.nlink > 1) {
|
|
29
|
+
return { ok: false, reason: "validation" };
|
|
30
|
+
}
|
|
31
|
+
if (params.maxBytes !== undefined &&
|
|
32
|
+
preOpenStat.isFile() &&
|
|
33
|
+
preOpenStat.size > params.maxBytes) {
|
|
34
|
+
return { ok: false, reason: "validation" };
|
|
35
|
+
}
|
|
36
|
+
fd = ioFs.openSync(realPath, openReadFlags);
|
|
37
|
+
const openedStat = ioFs.fstatSync(fd);
|
|
38
|
+
if (!isAllowedType(openedStat, allowedType)) {
|
|
39
|
+
return { ok: false, reason: "validation" };
|
|
40
|
+
}
|
|
41
|
+
if (params.rejectHardlinks && openedStat.isFile() && openedStat.nlink > 1) {
|
|
42
|
+
return { ok: false, reason: "validation" };
|
|
43
|
+
}
|
|
44
|
+
if (params.maxBytes !== undefined && openedStat.isFile() && openedStat.size > params.maxBytes) {
|
|
45
|
+
return { ok: false, reason: "validation" };
|
|
46
|
+
}
|
|
47
|
+
if (!sameFileIdentity(preOpenStat, openedStat)) {
|
|
48
|
+
return { ok: false, reason: "validation" };
|
|
49
|
+
}
|
|
50
|
+
const opened = { ok: true, path: realPath, fd, stat: openedStat };
|
|
51
|
+
fd = null;
|
|
52
|
+
return opened;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (isExpectedPathError(error)) {
|
|
56
|
+
return { ok: false, reason: "path", error };
|
|
57
|
+
}
|
|
58
|
+
return { ok: false, reason: "io", error };
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
if (fd !== null) {
|
|
62
|
+
ioFs.closeSync(fd);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function isAllowedType(stat, allowedType) {
|
|
67
|
+
if (allowedType === "directory") {
|
|
68
|
+
return stat.isDirectory();
|
|
69
|
+
}
|
|
70
|
+
return stat.isFile();
|
|
71
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function isPinnedPathHelperSpawnError(error: unknown): boolean;
|
|
2
|
+
export declare function runPinnedPathHelper(params: {
|
|
3
|
+
operation: "mkdirp" | "remove";
|
|
4
|
+
rootPath: string;
|
|
5
|
+
relativePath: string;
|
|
6
|
+
}): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=pinned-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinned-path.d.ts","sourceRoot":"","sources":["../src/pinned-path.ts"],"names":[],"mappings":"AAIA,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEpE;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,SAAS,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAahB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import { canFallbackFromPythonError } from "./pinned-python-config.js";
|
|
3
|
+
import { runPinnedHelper } from "./pinned-helper.js";
|
|
4
|
+
export function isPinnedPathHelperSpawnError(error) {
|
|
5
|
+
return canFallbackFromPythonError(error);
|
|
6
|
+
}
|
|
7
|
+
export async function runPinnedPathHelper(params) {
|
|
8
|
+
try {
|
|
9
|
+
await runPinnedHelper(params.operation, params.rootPath, {
|
|
10
|
+
relativePath: params.relativePath,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error instanceof FsSafeError) {
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
throw new FsSafeError("helper-failed", "pinned path helper failed", {
|
|
18
|
+
cause: error instanceof Error ? error : undefined,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type FsSafePythonMode = "auto" | "off" | "require";
|
|
2
|
+
export type FsSafePythonConfig = {
|
|
3
|
+
mode: FsSafePythonMode;
|
|
4
|
+
pythonPath?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function configureFsSafePython(config: Partial<FsSafePythonConfig>): void;
|
|
7
|
+
export declare function getFsSafePythonConfig(): FsSafePythonConfig;
|
|
8
|
+
export declare function canFallbackFromPythonError(error: unknown): boolean;
|
|
9
|
+
//# sourceMappingURL=pinned-python-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinned-python-config.d.ts","sourceRoot":"","sources":["../src/pinned-python-config.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;AAE1D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,gBAAgB,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAqBF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAE/E;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,CAc1D;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAOlE"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
let overrideConfig = {};
|
|
2
|
+
function parseMode(value) {
|
|
3
|
+
if (!value) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
const normalized = value.trim().toLowerCase();
|
|
7
|
+
if (normalized === "0" || normalized === "false" || normalized === "off" || normalized === "never") {
|
|
8
|
+
return "off";
|
|
9
|
+
}
|
|
10
|
+
if (normalized === "1" || normalized === "true" || normalized === "on" || normalized === "auto") {
|
|
11
|
+
return "auto";
|
|
12
|
+
}
|
|
13
|
+
if (normalized === "required" || normalized === "require") {
|
|
14
|
+
return "require";
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
export function configureFsSafePython(config) {
|
|
19
|
+
overrideConfig = { ...overrideConfig, ...config };
|
|
20
|
+
}
|
|
21
|
+
export function getFsSafePythonConfig() {
|
|
22
|
+
return {
|
|
23
|
+
mode: overrideConfig.mode ??
|
|
24
|
+
parseMode(process.env.FS_SAFE_PYTHON_MODE) ??
|
|
25
|
+
parseMode(process.env.OPENCLAW_FS_SAFE_PYTHON_MODE) ??
|
|
26
|
+
"auto",
|
|
27
|
+
pythonPath: overrideConfig.pythonPath ??
|
|
28
|
+
process.env.FS_SAFE_PYTHON ??
|
|
29
|
+
process.env.OPENCLAW_FS_SAFE_PYTHON ??
|
|
30
|
+
process.env.OPENCLAW_PINNED_PYTHON ??
|
|
31
|
+
process.env.OPENCLAW_PINNED_WRITE_PYTHON,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function canFallbackFromPythonError(error) {
|
|
35
|
+
return (getFsSafePythonConfig().mode !== "require" &&
|
|
36
|
+
error instanceof Error &&
|
|
37
|
+
"code" in error &&
|
|
38
|
+
error.code === "helper-unavailable");
|
|
39
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type PinnedPythonOperation = "copy" | "stat" | "readdir" | "mkdirp" | "remove" | "rename" | "write";
|
|
2
|
+
export declare function __resetPinnedPythonWorkerForTest(): void;
|
|
3
|
+
export declare function runPinnedPythonOperation<T>(params: {
|
|
4
|
+
operation: PinnedPythonOperation;
|
|
5
|
+
rootPath: string;
|
|
6
|
+
payload: Record<string, unknown>;
|
|
7
|
+
}): Promise<T>;
|
|
8
|
+
export declare function assertPinnedPythonOperationAvailable(): void;
|
|
9
|
+
export declare function validatePinnedOperationPayload(payload: Record<string, unknown>): void;
|
|
10
|
+
export declare function isPinnedHelperUnavailable(error: unknown): boolean;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=pinned-python.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinned-python.d.ts","sourceRoot":"","sources":["../src/pinned-python.ts"],"names":[],"mappings":"AAyUA,KAAK,qBAAqB,GACtB,MAAM,GACN,MAAM,GACN,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,OAAO,CAAC;AAiBZ,wBAAgB,gCAAgC,IAAI,IAAI,CAQvD;AAiND,wBAAsB,wBAAwB,CAAC,CAAC,EAAE,MAAM,EAAE;IACxD,SAAS,EAAE,qBAAqB,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC,GAAG,OAAO,CAAC,CAAC,CAAC,CA4Bb;AAED,wBAAgB,oCAAoC,IAAI,IAAI,CAK3D;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAarF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAIjE"}
|