@lousy-agents/agent-shell 5.14.1 → 5.14.2
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/dist/index.js +3804 -54
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire as __rspack_createRequire } from "node:module";
|
|
3
3
|
const __rspack_createRequire_require = __rspack_createRequire(import.meta.url);
|
|
4
4
|
var __webpack_modules__ = ({
|
|
5
|
-
|
|
5
|
+
391(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
|
|
6
6
|
|
|
7
7
|
;// CONCATENATED MODULE: external "node:child_process"
|
|
8
8
|
const external_node_child_process_namespaceObject = __rspack_createRequire_require("node:child_process");
|
|
@@ -77,6 +77,3706 @@ function defaultExecutor(env) {
|
|
|
77
77
|
}
|
|
78
78
|
/** Process-scoped singleton for production use. */ const git_utils_getRepositoryRoot = createGetRepositoryRoot();
|
|
79
79
|
|
|
80
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/errors.js
|
|
81
|
+
const OPERATIONAL_CODES = new Set([
|
|
82
|
+
"helper-failed",
|
|
83
|
+
"helper-unavailable",
|
|
84
|
+
"permission-unverified",
|
|
85
|
+
"timeout",
|
|
86
|
+
"unsupported-platform",
|
|
87
|
+
]);
|
|
88
|
+
function categorizeFsSafeError(code) {
|
|
89
|
+
return OPERATIONAL_CODES.has(code) ? "operational" : "policy";
|
|
90
|
+
}
|
|
91
|
+
class errors_FsSafeError extends Error {
|
|
92
|
+
code;
|
|
93
|
+
category;
|
|
94
|
+
constructor(code, message, options = {}) {
|
|
95
|
+
super(message, options);
|
|
96
|
+
this.name = "FsSafeError";
|
|
97
|
+
this.code = code;
|
|
98
|
+
this.category = categorizeFsSafeError(code);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
;// CONCATENATED MODULE: external "node:fs"
|
|
103
|
+
const external_node_fs_namespaceObject = __rspack_createRequire_require("node:fs");
|
|
104
|
+
;// CONCATENATED MODULE: external "node:stream/promises"
|
|
105
|
+
const external_node_stream_promises_namespaceObject = __rspack_createRequire_require("node:stream/promises");
|
|
106
|
+
;// CONCATENATED MODULE: external "node:stream"
|
|
107
|
+
const external_node_stream_namespaceObject = __rspack_createRequire_require("node:stream");
|
|
108
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/bounded-read-stream.js
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
function createMaxBytesTransform(maxBytes) {
|
|
112
|
+
let bytes = 0;
|
|
113
|
+
return new external_node_stream_namespaceObject.Transform({
|
|
114
|
+
transform(chunk, _encoding, callback) {
|
|
115
|
+
const buffer = chunk instanceof Buffer ? chunk : Buffer.from(chunk);
|
|
116
|
+
bytes += buffer.byteLength;
|
|
117
|
+
if (bytes > maxBytes) {
|
|
118
|
+
callback(new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
callback(null, buffer);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function createBoundedReadStream(opened, maxBytes) {
|
|
126
|
+
const stream = opened.handle.createReadStream();
|
|
127
|
+
return maxBytes === undefined ? stream : stream.pipe(createMaxBytesTransform(maxBytes));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/file-identity.js
|
|
131
|
+
function isZero(value) {
|
|
132
|
+
return value === 0 || value === 0n;
|
|
133
|
+
}
|
|
134
|
+
function file_identity_sameFileIdentity(left, right, platform = process.platform) {
|
|
135
|
+
if (left.ino !== right.ino) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
// On Windows, path-based stat calls can report dev=0 while fd-based stat
|
|
139
|
+
// reports a real volume serial; treat either-side dev=0 as "unknown device".
|
|
140
|
+
if (left.dev === right.dev) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
return platform === "win32" && (isZero(left.dev) || isZero(right.dev));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/string-coerce.js
|
|
147
|
+
function readStringValue(value) {
|
|
148
|
+
return typeof value === "string" ? value : undefined;
|
|
149
|
+
}
|
|
150
|
+
function normalizeNullableString(value) {
|
|
151
|
+
if (typeof value !== "string") {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const trimmed = value.trim();
|
|
155
|
+
return trimmed ? trimmed : null;
|
|
156
|
+
}
|
|
157
|
+
function normalizeOptionalString(value) {
|
|
158
|
+
return normalizeNullableString(value) ?? undefined;
|
|
159
|
+
}
|
|
160
|
+
function normalizeStringifiedOptionalString(value) {
|
|
161
|
+
if (typeof value === "string") {
|
|
162
|
+
return normalizeOptionalString(value);
|
|
163
|
+
}
|
|
164
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
165
|
+
return normalizeOptionalString(String(value));
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
function normalizeOptionalLowercaseString(value) {
|
|
170
|
+
return normalizeOptionalString(value)?.toLowerCase();
|
|
171
|
+
}
|
|
172
|
+
function normalizeLowercaseStringOrEmpty(value) {
|
|
173
|
+
return normalizeOptionalLowercaseString(value) ?? "";
|
|
174
|
+
}
|
|
175
|
+
function normalizeFastMode(raw) {
|
|
176
|
+
if (typeof raw === "boolean") {
|
|
177
|
+
return raw;
|
|
178
|
+
}
|
|
179
|
+
if (!raw) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const key = normalizeLowercaseStringOrEmpty(raw);
|
|
183
|
+
if (["off", "false", "no", "0", "disable", "disabled", "normal"].includes(key)) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
if (["on", "true", "yes", "1", "enable", "enabled", "fast"].includes(key)) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
function lowercasePreservingWhitespace(value) {
|
|
192
|
+
return value.toLowerCase();
|
|
193
|
+
}
|
|
194
|
+
function localeLowercasePreservingWhitespace(value) {
|
|
195
|
+
return value.toLocaleLowerCase();
|
|
196
|
+
}
|
|
197
|
+
function resolvePrimaryStringValue(value) {
|
|
198
|
+
if (typeof value === "string") {
|
|
199
|
+
return normalizeOptionalString(value);
|
|
200
|
+
}
|
|
201
|
+
if (!value || typeof value !== "object") {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
return normalizeOptionalString(value.primary);
|
|
205
|
+
}
|
|
206
|
+
function normalizeOptionalThreadValue(value) {
|
|
207
|
+
if (typeof value === "number") {
|
|
208
|
+
return Number.isFinite(value) ? Math.trunc(value) : undefined;
|
|
209
|
+
}
|
|
210
|
+
return normalizeOptionalString(value);
|
|
211
|
+
}
|
|
212
|
+
function normalizeOptionalStringifiedId(value) {
|
|
213
|
+
const normalized = normalizeOptionalThreadValue(value);
|
|
214
|
+
return normalized == null ? undefined : String(normalized);
|
|
215
|
+
}
|
|
216
|
+
function hasNonEmptyString(value) {
|
|
217
|
+
return normalizeOptionalString(value) !== undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/path.js
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
|
|
226
|
+
const SYMLINK_OPEN_CODES = new Set(["ELOOP", "EINVAL", "ENOTSUP"]);
|
|
227
|
+
const POSIX_SEPARATOR_CHAR_CODE = 0x2f;
|
|
228
|
+
function normalizeWindowsPathForComparison(input) {
|
|
229
|
+
let normalized = external_node_path_namespaceObject.win32.normalize(input);
|
|
230
|
+
if (normalized.startsWith("\\\\?\\")) {
|
|
231
|
+
normalized = normalized.slice(4);
|
|
232
|
+
if (normalized.toUpperCase().startsWith("UNC\\")) {
|
|
233
|
+
normalized = `\\\\${normalized.slice(4)}`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return normalizeLowercaseStringOrEmpty(normalized.replaceAll("/", "\\"));
|
|
237
|
+
}
|
|
238
|
+
function isNodeError(value) {
|
|
239
|
+
return Boolean(value && typeof value === "object" && "code" in value);
|
|
240
|
+
}
|
|
241
|
+
function hasNodeErrorCode(value, code) {
|
|
242
|
+
return isNodeError(value) && value.code === code;
|
|
243
|
+
}
|
|
244
|
+
function path_assertNoNulPathInput(filePath, message = "path contains a NUL byte") {
|
|
245
|
+
if (filePath.includes("\0")) {
|
|
246
|
+
throw new errors_FsSafeError("invalid-path", message);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function path_isNotFoundPathError(value) {
|
|
250
|
+
return isNodeError(value) && typeof value.code === "string" && NOT_FOUND_CODES.has(value.code);
|
|
251
|
+
}
|
|
252
|
+
function isSymlinkOpenError(value) {
|
|
253
|
+
return isNodeError(value) && typeof value.code === "string" && SYMLINK_OPEN_CODES.has(value.code);
|
|
254
|
+
}
|
|
255
|
+
function path_isPathInside(root, target) {
|
|
256
|
+
if (process.platform === "win32") {
|
|
257
|
+
const rootForCompare = normalizeWindowsPathForComparison(external_node_path_namespaceObject.win32.resolve(root));
|
|
258
|
+
const targetForCompare = normalizeWindowsPathForComparison(external_node_path_namespaceObject.win32.resolve(target));
|
|
259
|
+
const relative = external_node_path_namespaceObject.win32.relative(rootForCompare, targetForCompare);
|
|
260
|
+
const firstSegment = relative.split(external_node_path_namespaceObject.win32.sep)[0];
|
|
261
|
+
return (relative === "" || (firstSegment !== ".." && !external_node_path_namespaceObject.win32.isAbsolute(relative)));
|
|
262
|
+
}
|
|
263
|
+
if (root.length > 0 &&
|
|
264
|
+
root.charCodeAt(0) === POSIX_SEPARATOR_CHAR_CODE &&
|
|
265
|
+
target.length >= root.length &&
|
|
266
|
+
target.charCodeAt(0) === POSIX_SEPARATOR_CHAR_CODE &&
|
|
267
|
+
!target.includes("/..") &&
|
|
268
|
+
(target === root ||
|
|
269
|
+
(target.startsWith(root) && target.charCodeAt(root.length) === POSIX_SEPARATOR_CHAR_CODE))) {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
const resolvedRoot = external_node_path_namespaceObject.resolve(root);
|
|
273
|
+
const resolvedTarget = external_node_path_namespaceObject.resolve(target);
|
|
274
|
+
const relative = external_node_path_namespaceObject.relative(resolvedRoot, resolvedTarget);
|
|
275
|
+
const firstSegment = relative.split(external_node_path_namespaceObject.posix.sep)[0];
|
|
276
|
+
return relative === "" || (firstSegment !== ".." && !external_node_path_namespaceObject.isAbsolute(relative));
|
|
277
|
+
}
|
|
278
|
+
function resolveSafeBaseDir(rootDir) {
|
|
279
|
+
const resolved = path.resolve(rootDir);
|
|
280
|
+
return resolved.endsWith(path.sep) ? resolved : `${resolved}${path.sep}`;
|
|
281
|
+
}
|
|
282
|
+
function isWithinDir(rootDir, targetPath) {
|
|
283
|
+
return path_isPathInside(rootDir, targetPath);
|
|
284
|
+
}
|
|
285
|
+
function safeRealpathSync(targetPath, cache) {
|
|
286
|
+
const cached = cache?.get(targetPath);
|
|
287
|
+
if (cached) {
|
|
288
|
+
return cached;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const resolved = fs.realpathSync(targetPath);
|
|
292
|
+
cache?.set(targetPath, resolved);
|
|
293
|
+
cache?.set(resolved, resolved);
|
|
294
|
+
return resolved;
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function isPathInsideWithRealpath(basePath, candidatePath, opts) {
|
|
301
|
+
if (!path_isPathInside(basePath, candidatePath)) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
const baseReal = safeRealpathSync(basePath, opts?.cache);
|
|
305
|
+
const candidateReal = safeRealpathSync(candidatePath, opts?.cache);
|
|
306
|
+
if (!baseReal || !candidateReal) {
|
|
307
|
+
return opts?.requireRealpath === false;
|
|
308
|
+
}
|
|
309
|
+
return path_isPathInside(baseReal, candidateReal);
|
|
310
|
+
}
|
|
311
|
+
function safeStatSync(targetPath) {
|
|
312
|
+
try {
|
|
313
|
+
return fs.statSync(targetPath);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function splitSafeRelativePath(relativePath) {
|
|
320
|
+
if (relativePath.length === 0 || relativePath === ".") {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
path_assertNoNulPathInput(relativePath, "relative path contains a NUL byte");
|
|
324
|
+
if (relativePath.includes("\\")) {
|
|
325
|
+
throw new FsSafeError("invalid-path", "relative path must use forward slashes");
|
|
326
|
+
}
|
|
327
|
+
if (path.posix.isAbsolute(relativePath) ||
|
|
328
|
+
path.win32.isAbsolute(relativePath) ||
|
|
329
|
+
relativePath.startsWith("//")) {
|
|
330
|
+
throw new FsSafeError("invalid-path", "relative path must not be absolute");
|
|
331
|
+
}
|
|
332
|
+
const segments = relativePath.split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
333
|
+
for (const segment of segments) {
|
|
334
|
+
if (segment === "..") {
|
|
335
|
+
throw new FsSafeError("invalid-path", "relative path must not contain '..'");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return segments;
|
|
339
|
+
}
|
|
340
|
+
function resolveSafeRelativePath(rootDir, relativePath) {
|
|
341
|
+
const root = path.resolve(rootDir);
|
|
342
|
+
const target = path.resolve(root, ...splitSafeRelativePath(relativePath));
|
|
343
|
+
if (target !== root && !target.startsWith(root + path.sep)) {
|
|
344
|
+
throw new FsSafeError("outside-workspace", "relative path escapes root");
|
|
345
|
+
}
|
|
346
|
+
return target;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/directory-guard.js
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
async function directory_guard_createAsyncDirectoryGuard(dir) {
|
|
357
|
+
const stat = await promises_namespaceObject.lstat(dir);
|
|
358
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
359
|
+
throw new errors_FsSafeError("not-file", "directory component must be a directory");
|
|
360
|
+
}
|
|
361
|
+
return { dir, realPath: await promises_namespaceObject.realpath(dir), stat };
|
|
362
|
+
}
|
|
363
|
+
async function assertAsyncDirectoryGuard(guard) {
|
|
364
|
+
const stat = await promises_namespaceObject.lstat(guard.dir);
|
|
365
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
366
|
+
throw new errors_FsSafeError("not-file", "directory component must be a directory");
|
|
367
|
+
}
|
|
368
|
+
if (!file_identity_sameFileIdentity(stat, guard.stat) || (await promises_namespaceObject.realpath(guard.dir)) !== guard.realPath) {
|
|
369
|
+
throw new errors_FsSafeError("path-mismatch", "directory changed during operation");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function directory_guard_createSyncDirectoryGuard(dir) {
|
|
373
|
+
const stat = fsSync.lstatSync(dir);
|
|
374
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
375
|
+
throw new FsSafeError("not-file", "directory component must be a directory");
|
|
376
|
+
}
|
|
377
|
+
return { dir, realPath: fsSync.realpathSync(dir), stat };
|
|
378
|
+
}
|
|
379
|
+
function directory_guard_assertSyncDirectoryGuard(guard) {
|
|
380
|
+
const stat = fsSync.lstatSync(guard.dir);
|
|
381
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
382
|
+
throw new FsSafeError("not-file", "directory component must be a directory");
|
|
383
|
+
}
|
|
384
|
+
if (!sameFileIdentity(stat, guard.stat) || fsSync.realpathSync(guard.dir) !== guard.realPath) {
|
|
385
|
+
throw new FsSafeError("path-mismatch", "directory changed during operation");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async function directory_guard_createNearestExistingDirectoryGuard(rootReal, targetPath) {
|
|
389
|
+
let current = external_node_path_namespaceObject.resolve(targetPath);
|
|
390
|
+
const root = external_node_path_namespaceObject.resolve(rootReal);
|
|
391
|
+
while (current !== root) {
|
|
392
|
+
try {
|
|
393
|
+
return await directory_guard_createAsyncDirectoryGuard(current);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
if (!path_isNotFoundPathError(error)) {
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
current = external_node_path_namespaceObject.dirname(current);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return await directory_guard_createAsyncDirectoryGuard(root);
|
|
403
|
+
}
|
|
404
|
+
function directory_guard_createNearestExistingSyncDirectoryGuard(rootReal, targetPath) {
|
|
405
|
+
let current = path.resolve(targetPath);
|
|
406
|
+
const root = path.resolve(rootReal);
|
|
407
|
+
while (current !== root) {
|
|
408
|
+
try {
|
|
409
|
+
return directory_guard_createSyncDirectoryGuard(current);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
if (!isNotFoundPathError(error)) {
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
current = path.dirname(current);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return directory_guard_createSyncDirectoryGuard(root);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
function isSameOrChildPath(candidate, parent) {
|
|
427
|
+
return candidate === parent || candidate.startsWith(`${parent}${external_node_path_namespaceObject.sep}`);
|
|
428
|
+
}
|
|
429
|
+
function isPathEscape(relativePath) {
|
|
430
|
+
return relativePath === ".." || relativePath.startsWith(`..${external_node_path_namespaceObject.sep}`) || external_node_path_namespaceObject.isAbsolute(relativePath);
|
|
431
|
+
}
|
|
432
|
+
async function mkdirPathComponentsWithGuards(params) {
|
|
433
|
+
const root = external_node_path_namespaceObject.resolve(params.rootReal);
|
|
434
|
+
const target = external_node_path_namespaceObject.resolve(params.targetPath);
|
|
435
|
+
const relative = external_node_path_namespaceObject.relative(root, target);
|
|
436
|
+
if (isPathEscape(relative)) {
|
|
437
|
+
throw new errors_FsSafeError("outside-workspace", "directory is outside workspace root");
|
|
438
|
+
}
|
|
439
|
+
let current = root;
|
|
440
|
+
for (const part of relative.split(external_node_path_namespaceObject.sep).filter(Boolean)) {
|
|
441
|
+
const next = external_node_path_namespaceObject.join(current, part);
|
|
442
|
+
const parentGuard = await directory_guard_createAsyncDirectoryGuard(current);
|
|
443
|
+
await params.beforeComponent?.(next);
|
|
444
|
+
await assertAsyncDirectoryGuard(parentGuard);
|
|
445
|
+
try {
|
|
446
|
+
await promises_namespaceObject.mkdir(next);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const stat = await promises_namespaceObject.lstat(next);
|
|
454
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
455
|
+
throw new errors_FsSafeError("not-file", "directory component must be a directory");
|
|
456
|
+
}
|
|
457
|
+
// Node's recursive mkdir follows symlinks in missing components. Build one
|
|
458
|
+
// segment at a time and realpath-check each segment before descending.
|
|
459
|
+
if (!isSameOrChildPath(external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(next)), root)) {
|
|
460
|
+
throw new errors_FsSafeError("outside-workspace", "directory escaped workspace root");
|
|
461
|
+
}
|
|
462
|
+
await directory_guard_createAsyncDirectoryGuard(next);
|
|
463
|
+
await assertAsyncDirectoryGuard(parentGuard);
|
|
464
|
+
current = next;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/guarded-mutation.js
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
async function withAsyncDirectoryGuards(guards, mutate, options = {}) {
|
|
474
|
+
for (const guard of guards) {
|
|
475
|
+
await assertAsyncDirectoryGuard(guard);
|
|
476
|
+
}
|
|
477
|
+
const result = await mutate();
|
|
478
|
+
if (options.verifyAfter !== false) {
|
|
479
|
+
try {
|
|
480
|
+
for (const guard of guards) {
|
|
481
|
+
await assertAsyncDirectoryGuard(guard);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
if (options.onPostGuardFailure) {
|
|
486
|
+
try {
|
|
487
|
+
// The mutation may have returned an owned resource before the post-guard
|
|
488
|
+
// check detected a swapped directory. Give callers one chance to close
|
|
489
|
+
// handles without letting cleanup hide the boundary failure.
|
|
490
|
+
await options.onPostGuardFailure(result, error);
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
// Preserve the boundary failure. Cleanup is best-effort.
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
function withSyncDirectoryGuards(guards, mutate, options = {}) {
|
|
502
|
+
for (const guard of guards) {
|
|
503
|
+
assertSyncDirectoryGuard(guard);
|
|
504
|
+
}
|
|
505
|
+
const result = mutate();
|
|
506
|
+
if (options.verifyAfter !== false) {
|
|
507
|
+
for (const guard of guards) {
|
|
508
|
+
assertSyncDirectoryGuard(guard);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
513
|
+
async function guardedRename(params) {
|
|
514
|
+
const sourceGuard = await createAsyncDirectoryGuard(path.dirname(params.from));
|
|
515
|
+
const targetGuard = params.targetRoot
|
|
516
|
+
? await createNearestExistingDirectoryGuard(params.targetRoot, path.dirname(params.to))
|
|
517
|
+
: await createAsyncDirectoryGuard(path.dirname(params.to));
|
|
518
|
+
await withAsyncDirectoryGuards([sourceGuard, targetGuard], async () => {
|
|
519
|
+
await fs.rename(params.from, params.to);
|
|
520
|
+
}, { verifyAfter: params.verifyAfter });
|
|
521
|
+
}
|
|
522
|
+
function guardedRenameSync(params) {
|
|
523
|
+
const sourceGuard = createSyncDirectoryGuard(path.dirname(params.from));
|
|
524
|
+
const targetGuard = params.targetRoot
|
|
525
|
+
? createNearestExistingSyncDirectoryGuard(params.targetRoot, path.dirname(params.to))
|
|
526
|
+
: createSyncDirectoryGuard(path.dirname(params.to));
|
|
527
|
+
withSyncDirectoryGuards([sourceGuard, targetGuard], () => fsSync.renameSync(params.from, params.to), { verifyAfter: params.verifyAfter });
|
|
528
|
+
}
|
|
529
|
+
async function guardedRm(params) {
|
|
530
|
+
const guard = await createAsyncDirectoryGuard(path.dirname(params.target));
|
|
531
|
+
await withAsyncDirectoryGuards([guard], async () => {
|
|
532
|
+
await fs.rm(params.target, {
|
|
533
|
+
...(params.recursive !== undefined ? { recursive: params.recursive } : {}),
|
|
534
|
+
...(params.force !== undefined ? { force: params.force } : {}),
|
|
535
|
+
});
|
|
536
|
+
}, { verifyAfter: params.verifyAfter });
|
|
537
|
+
}
|
|
538
|
+
function guardedRmSync(params) {
|
|
539
|
+
const guard = createSyncDirectoryGuard(path.dirname(params.target));
|
|
540
|
+
withSyncDirectoryGuards([guard], () => fsSync.rmSync(params.target, {
|
|
541
|
+
...(params.recursive !== undefined ? { recursive: params.recursive } : {}),
|
|
542
|
+
...(params.force !== undefined ? { force: params.force } : {}),
|
|
543
|
+
}), { verifyAfter: params.verifyAfter });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-python-config.js
|
|
547
|
+
let overrideConfig = {};
|
|
548
|
+
function parseMode(value) {
|
|
549
|
+
if (!value) {
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
const normalized = value.trim().toLowerCase();
|
|
553
|
+
if (normalized === "0" || normalized === "false" || normalized === "off" || normalized === "never") {
|
|
554
|
+
return "off";
|
|
555
|
+
}
|
|
556
|
+
if (normalized === "1" || normalized === "true" || normalized === "on" || normalized === "auto") {
|
|
557
|
+
return "auto";
|
|
558
|
+
}
|
|
559
|
+
if (normalized === "required" || normalized === "require") {
|
|
560
|
+
return "require";
|
|
561
|
+
}
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
function configureFsSafePython(config) {
|
|
565
|
+
overrideConfig = { ...overrideConfig, ...config };
|
|
566
|
+
}
|
|
567
|
+
function getFsSafePythonConfig() {
|
|
568
|
+
return {
|
|
569
|
+
mode: overrideConfig.mode ??
|
|
570
|
+
parseMode(process.env.FS_SAFE_PYTHON_MODE) ??
|
|
571
|
+
parseMode(process.env.OPENCLAW_FS_SAFE_PYTHON_MODE) ??
|
|
572
|
+
"auto",
|
|
573
|
+
pythonPath: overrideConfig.pythonPath ??
|
|
574
|
+
process.env.FS_SAFE_PYTHON ??
|
|
575
|
+
process.env.OPENCLAW_FS_SAFE_PYTHON ??
|
|
576
|
+
process.env.OPENCLAW_PINNED_PYTHON ??
|
|
577
|
+
process.env.OPENCLAW_PINNED_WRITE_PYTHON,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function canFallbackFromPythonError(error) {
|
|
581
|
+
return (getFsSafePythonConfig().mode !== "require" &&
|
|
582
|
+
error instanceof Error &&
|
|
583
|
+
"code" in error &&
|
|
584
|
+
error.code === "helper-unavailable");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-python.js
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
const PINNED_PYTHON_WORKER_SOURCE = String.raw `
|
|
593
|
+
import base64
|
|
594
|
+
import errno
|
|
595
|
+
import json
|
|
596
|
+
import os
|
|
597
|
+
import secrets
|
|
598
|
+
import stat
|
|
599
|
+
import sys
|
|
600
|
+
|
|
601
|
+
DIR_FLAGS = os.O_RDONLY
|
|
602
|
+
if hasattr(os, "O_DIRECTORY"):
|
|
603
|
+
DIR_FLAGS |= os.O_DIRECTORY
|
|
604
|
+
if hasattr(os, "O_NOFOLLOW"):
|
|
605
|
+
DIR_FLAGS |= os.O_NOFOLLOW
|
|
606
|
+
READ_FLAGS = os.O_RDONLY
|
|
607
|
+
if hasattr(os, "O_NOFOLLOW"):
|
|
608
|
+
READ_FLAGS |= os.O_NOFOLLOW
|
|
609
|
+
WRITE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
610
|
+
if hasattr(os, "O_NOFOLLOW"):
|
|
611
|
+
WRITE_FLAGS |= os.O_NOFOLLOW
|
|
612
|
+
|
|
613
|
+
def split_relative(value):
|
|
614
|
+
if value in ("", "."):
|
|
615
|
+
return []
|
|
616
|
+
if "\x00" in value or value.startswith("/") or value.startswith("//"):
|
|
617
|
+
raise OSError(errno.EPERM, "invalid relative path")
|
|
618
|
+
if value.startswith("..\\"):
|
|
619
|
+
raise OSError(errno.EPERM, "path traversal is not allowed")
|
|
620
|
+
parts = [part for part in value.split("/") if part and part != "."]
|
|
621
|
+
for part in parts:
|
|
622
|
+
if part == "..":
|
|
623
|
+
raise OSError(errno.EPERM, "path traversal is not allowed")
|
|
624
|
+
return parts
|
|
625
|
+
|
|
626
|
+
def open_dir(path_value, dir_fd=None):
|
|
627
|
+
return os.open(path_value, DIR_FLAGS, dir_fd=dir_fd)
|
|
628
|
+
|
|
629
|
+
def walk_dir(root_fd, segments, mkdir_enabled=False):
|
|
630
|
+
current_fd = os.dup(root_fd)
|
|
631
|
+
try:
|
|
632
|
+
for segment in segments:
|
|
633
|
+
try:
|
|
634
|
+
next_fd = open_dir(segment, dir_fd=current_fd)
|
|
635
|
+
except FileNotFoundError:
|
|
636
|
+
if not mkdir_enabled:
|
|
637
|
+
raise
|
|
638
|
+
os.mkdir(segment, 0o777, dir_fd=current_fd)
|
|
639
|
+
next_fd = open_dir(segment, dir_fd=current_fd)
|
|
640
|
+
os.close(current_fd)
|
|
641
|
+
current_fd = next_fd
|
|
642
|
+
return current_fd
|
|
643
|
+
except Exception:
|
|
644
|
+
os.close(current_fd)
|
|
645
|
+
raise
|
|
646
|
+
|
|
647
|
+
def parent_and_basename(root_fd, relative):
|
|
648
|
+
segments = split_relative(relative)
|
|
649
|
+
if not segments:
|
|
650
|
+
raise OSError(errno.EPERM, "operation requires a non-root path")
|
|
651
|
+
parent_fd = walk_dir(root_fd, segments[:-1])
|
|
652
|
+
return parent_fd, segments[-1]
|
|
653
|
+
|
|
654
|
+
def encode_stat(st):
|
|
655
|
+
mode = st.st_mode
|
|
656
|
+
return {
|
|
657
|
+
"dev": st.st_dev,
|
|
658
|
+
"gid": st.st_gid,
|
|
659
|
+
"ino": st.st_ino,
|
|
660
|
+
"isDirectory": stat.S_ISDIR(mode),
|
|
661
|
+
"isFile": stat.S_ISREG(mode),
|
|
662
|
+
"isSymbolicLink": stat.S_ISLNK(mode),
|
|
663
|
+
"mode": mode,
|
|
664
|
+
"mtimeMs": st.st_mtime * 1000,
|
|
665
|
+
"nlink": st.st_nlink,
|
|
666
|
+
"size": st.st_size,
|
|
667
|
+
"uid": st.st_uid,
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
def reject_unsafe_endpoint(st):
|
|
671
|
+
mode = st.st_mode
|
|
672
|
+
if stat.S_ISLNK(mode):
|
|
673
|
+
raise OSError(errno.ELOOP, "symlink endpoint is not allowed")
|
|
674
|
+
if stat.S_ISREG(mode) and st.st_nlink > 1:
|
|
675
|
+
raise OSError(errno.EPERM, "hardlinked file endpoint is not allowed")
|
|
676
|
+
|
|
677
|
+
def stat_path(root_fd, payload):
|
|
678
|
+
relative = payload.get("relativePath", "")
|
|
679
|
+
segments = split_relative(relative)
|
|
680
|
+
if not segments:
|
|
681
|
+
return encode_stat(os.fstat(root_fd))
|
|
682
|
+
parent_fd, basename = parent_and_basename(root_fd, relative)
|
|
683
|
+
try:
|
|
684
|
+
st = os.lstat(basename, dir_fd=parent_fd)
|
|
685
|
+
if payload.get("rejectSymlink", True) and stat.S_ISLNK(st.st_mode):
|
|
686
|
+
raise OSError(errno.ELOOP, "symlink endpoint is not allowed")
|
|
687
|
+
return encode_stat(st)
|
|
688
|
+
finally:
|
|
689
|
+
os.close(parent_fd)
|
|
690
|
+
|
|
691
|
+
def readdir_path(root_fd, payload):
|
|
692
|
+
dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")))
|
|
693
|
+
try:
|
|
694
|
+
names = sorted(os.listdir(dir_fd))
|
|
695
|
+
if not payload.get("withFileTypes", False):
|
|
696
|
+
return names
|
|
697
|
+
entries = []
|
|
698
|
+
for name in names:
|
|
699
|
+
st = os.lstat(name, dir_fd=dir_fd)
|
|
700
|
+
entry = encode_stat(st)
|
|
701
|
+
entry["name"] = name
|
|
702
|
+
entries.append(entry)
|
|
703
|
+
return entries
|
|
704
|
+
finally:
|
|
705
|
+
os.close(dir_fd)
|
|
706
|
+
|
|
707
|
+
def mkdirp_path(root_fd, payload):
|
|
708
|
+
dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")), mkdir_enabled=True)
|
|
709
|
+
os.close(dir_fd)
|
|
710
|
+
return None
|
|
711
|
+
|
|
712
|
+
def remove_tree(parent_fd, basename):
|
|
713
|
+
st = os.lstat(basename, dir_fd=parent_fd)
|
|
714
|
+
if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
|
|
715
|
+
dir_fd = open_dir(basename, dir_fd=parent_fd)
|
|
716
|
+
try:
|
|
717
|
+
for child in os.listdir(dir_fd):
|
|
718
|
+
remove_tree(dir_fd, child)
|
|
719
|
+
finally:
|
|
720
|
+
os.close(dir_fd)
|
|
721
|
+
os.rmdir(basename, dir_fd=parent_fd)
|
|
722
|
+
else:
|
|
723
|
+
os.unlink(basename, dir_fd=parent_fd)
|
|
724
|
+
|
|
725
|
+
def remove_path(root_fd, payload):
|
|
726
|
+
parent_fd, basename = parent_and_basename(root_fd, payload.get("relativePath", ""))
|
|
727
|
+
try:
|
|
728
|
+
try:
|
|
729
|
+
st = os.lstat(basename, dir_fd=parent_fd)
|
|
730
|
+
except FileNotFoundError:
|
|
731
|
+
if payload.get("force", True):
|
|
732
|
+
return None
|
|
733
|
+
raise
|
|
734
|
+
if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
|
|
735
|
+
if payload.get("recursive", False):
|
|
736
|
+
remove_tree(parent_fd, basename)
|
|
737
|
+
else:
|
|
738
|
+
os.rmdir(basename, dir_fd=parent_fd)
|
|
739
|
+
else:
|
|
740
|
+
os.unlink(basename, dir_fd=parent_fd)
|
|
741
|
+
return None
|
|
742
|
+
finally:
|
|
743
|
+
os.close(parent_fd)
|
|
744
|
+
|
|
745
|
+
def rename_path(root_fd, payload):
|
|
746
|
+
from_parent_fd, from_base = parent_and_basename(root_fd, payload["from"])
|
|
747
|
+
to_parent_fd, to_base = parent_and_basename(root_fd, payload["to"])
|
|
748
|
+
try:
|
|
749
|
+
from_stat = os.lstat(from_base, dir_fd=from_parent_fd)
|
|
750
|
+
reject_unsafe_endpoint(from_stat)
|
|
751
|
+
if not payload.get("overwrite", True):
|
|
752
|
+
try:
|
|
753
|
+
os.lstat(to_base, dir_fd=to_parent_fd)
|
|
754
|
+
raise FileExistsError(errno.EEXIST, "destination exists", to_base)
|
|
755
|
+
except FileNotFoundError:
|
|
756
|
+
pass
|
|
757
|
+
os.rename(from_base, to_base, src_dir_fd=from_parent_fd, dst_dir_fd=to_parent_fd)
|
|
758
|
+
os.fsync(from_parent_fd)
|
|
759
|
+
if from_parent_fd != to_parent_fd:
|
|
760
|
+
os.fsync(to_parent_fd)
|
|
761
|
+
return None
|
|
762
|
+
finally:
|
|
763
|
+
os.close(from_parent_fd)
|
|
764
|
+
os.close(to_parent_fd)
|
|
765
|
+
|
|
766
|
+
def create_temp_file(parent_fd, basename, mode):
|
|
767
|
+
prefix = "." + basename + "."
|
|
768
|
+
for _ in range(128):
|
|
769
|
+
candidate = prefix + secrets.token_hex(6) + ".tmp"
|
|
770
|
+
try:
|
|
771
|
+
fd = os.open(candidate, WRITE_FLAGS, mode, dir_fd=parent_fd)
|
|
772
|
+
return candidate, fd
|
|
773
|
+
except FileExistsError:
|
|
774
|
+
continue
|
|
775
|
+
raise RuntimeError("failed to allocate pinned temp file")
|
|
776
|
+
|
|
777
|
+
def write_path(root_fd, payload):
|
|
778
|
+
parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
|
|
779
|
+
temp_fd = None
|
|
780
|
+
temp_name = None
|
|
781
|
+
basename = payload["basename"]
|
|
782
|
+
mode = int(payload.get("mode", 0o600))
|
|
783
|
+
overwrite = bool(payload.get("overwrite", True))
|
|
784
|
+
max_bytes = int(payload.get("maxBytes", -1))
|
|
785
|
+
data = base64.b64decode(payload.get("base64", ""))
|
|
786
|
+
try:
|
|
787
|
+
if max_bytes >= 0 and len(data) > max_bytes:
|
|
788
|
+
raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, len(data)))
|
|
789
|
+
if not overwrite:
|
|
790
|
+
try:
|
|
791
|
+
os.lstat(basename, dir_fd=parent_fd)
|
|
792
|
+
raise FileExistsError(errno.EEXIST, "destination exists", basename)
|
|
793
|
+
except FileNotFoundError:
|
|
794
|
+
pass
|
|
795
|
+
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
|
796
|
+
view = memoryview(data)
|
|
797
|
+
while view:
|
|
798
|
+
written = os.write(temp_fd, view)
|
|
799
|
+
if written <= 0:
|
|
800
|
+
raise OSError(errno.EIO, "short write")
|
|
801
|
+
view = view[written:]
|
|
802
|
+
os.fsync(temp_fd)
|
|
803
|
+
os.close(temp_fd)
|
|
804
|
+
temp_fd = None
|
|
805
|
+
os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
|
|
806
|
+
temp_name = None
|
|
807
|
+
os.fsync(parent_fd)
|
|
808
|
+
result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
|
|
809
|
+
return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
|
|
810
|
+
finally:
|
|
811
|
+
if temp_fd is not None:
|
|
812
|
+
os.close(temp_fd)
|
|
813
|
+
if temp_name is not None:
|
|
814
|
+
try:
|
|
815
|
+
os.unlink(temp_name, dir_fd=parent_fd)
|
|
816
|
+
except FileNotFoundError:
|
|
817
|
+
pass
|
|
818
|
+
os.close(parent_fd)
|
|
819
|
+
|
|
820
|
+
def copy_path(root_fd, payload):
|
|
821
|
+
source_fd = os.open(payload["sourcePath"], READ_FLAGS)
|
|
822
|
+
parent_fd = None
|
|
823
|
+
temp_fd = None
|
|
824
|
+
temp_name = None
|
|
825
|
+
try:
|
|
826
|
+
source_stat = os.fstat(source_fd)
|
|
827
|
+
if not stat.S_ISREG(source_stat.st_mode):
|
|
828
|
+
raise RuntimeError("fs-safe-not-file")
|
|
829
|
+
if source_stat.st_dev != int(payload["sourceDev"]) or source_stat.st_ino != int(payload["sourceIno"]):
|
|
830
|
+
raise RuntimeError("fs-safe-source-mismatch")
|
|
831
|
+
basename = payload["basename"]
|
|
832
|
+
mode = int(payload.get("mode", 0o600))
|
|
833
|
+
overwrite = bool(payload.get("overwrite", True))
|
|
834
|
+
max_bytes = int(payload.get("maxBytes", -1))
|
|
835
|
+
if max_bytes >= 0 and source_stat.st_size > max_bytes:
|
|
836
|
+
raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
|
|
837
|
+
parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
|
|
838
|
+
if not overwrite:
|
|
839
|
+
try:
|
|
840
|
+
os.lstat(basename, dir_fd=parent_fd)
|
|
841
|
+
raise FileExistsError(errno.EEXIST, "destination exists", basename)
|
|
842
|
+
except FileNotFoundError:
|
|
843
|
+
pass
|
|
844
|
+
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
|
845
|
+
written_bytes = 0
|
|
846
|
+
while True:
|
|
847
|
+
chunk = os.read(source_fd, 65536)
|
|
848
|
+
if not chunk:
|
|
849
|
+
break
|
|
850
|
+
written_bytes += len(chunk)
|
|
851
|
+
if max_bytes >= 0 and written_bytes > max_bytes:
|
|
852
|
+
raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, written_bytes))
|
|
853
|
+
view = memoryview(chunk)
|
|
854
|
+
while view:
|
|
855
|
+
written = os.write(temp_fd, view)
|
|
856
|
+
if written <= 0:
|
|
857
|
+
raise OSError(errno.EIO, "short write")
|
|
858
|
+
view = view[written:]
|
|
859
|
+
os.fsync(temp_fd)
|
|
860
|
+
os.close(temp_fd)
|
|
861
|
+
temp_fd = None
|
|
862
|
+
os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
|
|
863
|
+
temp_name = None
|
|
864
|
+
os.fsync(parent_fd)
|
|
865
|
+
result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
|
|
866
|
+
return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
|
|
867
|
+
finally:
|
|
868
|
+
os.close(source_fd)
|
|
869
|
+
if temp_fd is not None:
|
|
870
|
+
os.close(temp_fd)
|
|
871
|
+
if temp_name is not None and parent_fd is not None:
|
|
872
|
+
try:
|
|
873
|
+
os.unlink(temp_name, dir_fd=parent_fd)
|
|
874
|
+
except FileNotFoundError:
|
|
875
|
+
pass
|
|
876
|
+
if parent_fd is not None:
|
|
877
|
+
os.close(parent_fd)
|
|
878
|
+
|
|
879
|
+
def run_operation(operation, root_path, payload):
|
|
880
|
+
root_fd = open_dir(root_path)
|
|
881
|
+
try:
|
|
882
|
+
if operation == "stat":
|
|
883
|
+
return stat_path(root_fd, payload)
|
|
884
|
+
if operation == "readdir":
|
|
885
|
+
return readdir_path(root_fd, payload)
|
|
886
|
+
if operation == "mkdirp":
|
|
887
|
+
return mkdirp_path(root_fd, payload)
|
|
888
|
+
if operation == "remove":
|
|
889
|
+
return remove_path(root_fd, payload)
|
|
890
|
+
if operation == "rename":
|
|
891
|
+
return rename_path(root_fd, payload)
|
|
892
|
+
if operation == "write":
|
|
893
|
+
return write_path(root_fd, payload)
|
|
894
|
+
if operation == "copy":
|
|
895
|
+
return copy_path(root_fd, payload)
|
|
896
|
+
raise RuntimeError("unknown operation: " + operation)
|
|
897
|
+
finally:
|
|
898
|
+
os.close(root_fd)
|
|
899
|
+
|
|
900
|
+
for line in sys.stdin:
|
|
901
|
+
try:
|
|
902
|
+
request = json.loads(line)
|
|
903
|
+
result = run_operation(request["operation"], request["rootPath"], request.get("payload") or {})
|
|
904
|
+
response = {"id": request["id"], "ok": True, "result": result}
|
|
905
|
+
except Exception as exc:
|
|
906
|
+
response = {
|
|
907
|
+
"id": request.get("id") if isinstance(locals().get("request"), dict) else None,
|
|
908
|
+
"ok": False,
|
|
909
|
+
"code": exc.__class__.__name__,
|
|
910
|
+
"errno": getattr(exc, "errno", None),
|
|
911
|
+
"message": str(exc),
|
|
912
|
+
}
|
|
913
|
+
print(json.dumps(response, separators=(",", ":")), flush=True)
|
|
914
|
+
`;
|
|
915
|
+
let nextRequestId = 1;
|
|
916
|
+
let worker = null;
|
|
917
|
+
function __resetPinnedPythonWorkerForTest() {
|
|
918
|
+
const currentWorker = worker;
|
|
919
|
+
worker = null;
|
|
920
|
+
if (!currentWorker) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
currentWorker.pending.clear();
|
|
924
|
+
currentWorker.child.kill("SIGTERM");
|
|
925
|
+
}
|
|
926
|
+
const PYTHON_CANDIDATE_DEFAULTS = [
|
|
927
|
+
"/usr/bin/python3",
|
|
928
|
+
"/opt/homebrew/bin/python3",
|
|
929
|
+
"/usr/local/bin/python3",
|
|
930
|
+
];
|
|
931
|
+
function canExecute(binPath) {
|
|
932
|
+
try {
|
|
933
|
+
external_node_fs_namespaceObject.accessSync(binPath, external_node_fs_namespaceObject.constants.X_OK);
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
catch {
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function resolvePython() {
|
|
941
|
+
const configured = getFsSafePythonConfig().pythonPath;
|
|
942
|
+
if (configured) {
|
|
943
|
+
return configured;
|
|
944
|
+
}
|
|
945
|
+
for (const candidate of PYTHON_CANDIDATE_DEFAULTS) {
|
|
946
|
+
if (canExecute(candidate)) {
|
|
947
|
+
return candidate;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return "python3";
|
|
951
|
+
}
|
|
952
|
+
function assertPinnedHelperSupported() {
|
|
953
|
+
if (process.platform === "win32") {
|
|
954
|
+
throw new errors_FsSafeError("unsupported-platform", "fd-relative pinned filesystem operations are not available on Windows");
|
|
955
|
+
}
|
|
956
|
+
if (getFsSafePythonConfig().mode === "off") {
|
|
957
|
+
throw new errors_FsSafeError("helper-unavailable", "Python helper is disabled");
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function isSpawnUnavailable(error) {
|
|
961
|
+
if (!(error instanceof Error)) {
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
const maybeErrno = error;
|
|
965
|
+
return (typeof maybeErrno.syscall === "string" &&
|
|
966
|
+
maybeErrno.syscall.startsWith("spawn") &&
|
|
967
|
+
["EACCES", "ENOENT", "ENOEXEC"].includes(maybeErrno.code ?? ""));
|
|
968
|
+
}
|
|
969
|
+
function mapWorkerError(response) {
|
|
970
|
+
const code = typeof response.code === "string" ? response.code : "";
|
|
971
|
+
const errno = typeof response.errno === "number" ? response.errno : undefined;
|
|
972
|
+
const message = typeof response.message === "string" && response.message
|
|
973
|
+
? response.message
|
|
974
|
+
: "pinned helper failed";
|
|
975
|
+
const tooLarge = message.match(/fs-safe-too-large:(\d+):(\d+)/);
|
|
976
|
+
if (tooLarge) {
|
|
977
|
+
const [, limit, got] = tooLarge;
|
|
978
|
+
return new errors_FsSafeError("too-large", `file exceeds limit of ${limit} bytes (got at least ${got})`);
|
|
979
|
+
}
|
|
980
|
+
if (message.includes("fs-safe-not-file")) {
|
|
981
|
+
return new errors_FsSafeError("not-file", "not a file");
|
|
982
|
+
}
|
|
983
|
+
if (message.includes("fs-safe-source-mismatch")) {
|
|
984
|
+
return new errors_FsSafeError("path-mismatch", "source path changed during copy");
|
|
985
|
+
}
|
|
986
|
+
if (code === "FileNotFoundError" || errno === 2) {
|
|
987
|
+
return new errors_FsSafeError("not-found", "file not found");
|
|
988
|
+
}
|
|
989
|
+
if (code === "FileExistsError" || errno === 17) {
|
|
990
|
+
return new errors_FsSafeError("already-exists", message);
|
|
991
|
+
}
|
|
992
|
+
if (errno === 39) {
|
|
993
|
+
return new errors_FsSafeError("not-empty", "directory is not empty");
|
|
994
|
+
}
|
|
995
|
+
if (errno === 1 || errno === 13 || errno === 21) {
|
|
996
|
+
return new errors_FsSafeError("not-removable", "path is not removable under root");
|
|
997
|
+
}
|
|
998
|
+
if (code === "NotADirectoryError" || code === "OSError" || errno === 20 || errno === 40) {
|
|
999
|
+
return new errors_FsSafeError("path-alias", message);
|
|
1000
|
+
}
|
|
1001
|
+
return new errors_FsSafeError("helper-failed", message);
|
|
1002
|
+
}
|
|
1003
|
+
function rejectPending(error) {
|
|
1004
|
+
if (!worker) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
setWorkerRef(worker, false);
|
|
1008
|
+
for (const pending of worker.pending.values()) {
|
|
1009
|
+
pending.reject(error);
|
|
1010
|
+
}
|
|
1011
|
+
worker.pending.clear();
|
|
1012
|
+
worker = null;
|
|
1013
|
+
}
|
|
1014
|
+
function handleWorkerLine(line) {
|
|
1015
|
+
if (!worker || !line.trim()) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
let decoded;
|
|
1019
|
+
try {
|
|
1020
|
+
decoded = JSON.parse(line);
|
|
1021
|
+
}
|
|
1022
|
+
catch {
|
|
1023
|
+
rejectPending(new errors_FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`));
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (typeof decoded !== "object" || decoded === null || !("id" in decoded)) {
|
|
1027
|
+
rejectPending(new errors_FsSafeError("helper-failed", "pinned helper returned invalid response"));
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const response = decoded;
|
|
1031
|
+
const id = typeof response.id === "number" ? response.id : undefined;
|
|
1032
|
+
if (id === undefined) {
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
const pending = worker.pending.get(id);
|
|
1036
|
+
if (!pending) {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
worker.pending.delete(id);
|
|
1040
|
+
if (worker.pending.size === 0) {
|
|
1041
|
+
setWorkerRef(worker, false);
|
|
1042
|
+
}
|
|
1043
|
+
if (response.ok === true) {
|
|
1044
|
+
pending.resolve(response.result);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
pending.reject(mapWorkerError(decoded));
|
|
1048
|
+
}
|
|
1049
|
+
function getWorker() {
|
|
1050
|
+
assertPinnedHelperSupported();
|
|
1051
|
+
if (worker) {
|
|
1052
|
+
return worker;
|
|
1053
|
+
}
|
|
1054
|
+
const child = (0,external_node_child_process_namespaceObject.spawn)(resolvePython(), ["-u", "-c", PINNED_PYTHON_WORKER_SOURCE], {
|
|
1055
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1056
|
+
});
|
|
1057
|
+
worker = { child, pending: new Map(), stderr: "", stdoutBuffer: "" };
|
|
1058
|
+
child.stdout.setEncoding("utf8");
|
|
1059
|
+
child.stderr.setEncoding("utf8");
|
|
1060
|
+
child.stdout.on("data", (chunk) => {
|
|
1061
|
+
const current = worker;
|
|
1062
|
+
if (!current) {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
current.stdoutBuffer += chunk;
|
|
1066
|
+
for (;;) {
|
|
1067
|
+
const newline = current.stdoutBuffer.indexOf("\n");
|
|
1068
|
+
if (newline < 0) {
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
const line = current.stdoutBuffer.slice(0, newline);
|
|
1072
|
+
current.stdoutBuffer = current.stdoutBuffer.slice(newline + 1);
|
|
1073
|
+
handleWorkerLine(line);
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
child.stderr.on("data", (chunk) => {
|
|
1077
|
+
if (worker) {
|
|
1078
|
+
worker.stderr = `${worker.stderr}${chunk}`.slice(-4096);
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
child.once("error", (error) => {
|
|
1082
|
+
const mapped = isSpawnUnavailable(error)
|
|
1083
|
+
? new errors_FsSafeError("helper-unavailable", "Python helper is unavailable", { cause: error })
|
|
1084
|
+
: error instanceof Error
|
|
1085
|
+
? error
|
|
1086
|
+
: new Error(String(error));
|
|
1087
|
+
rejectPending(mapped);
|
|
1088
|
+
});
|
|
1089
|
+
child.once("close", (code, signal) => {
|
|
1090
|
+
const stderr = worker?.stderr.trim();
|
|
1091
|
+
rejectPending(new errors_FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`));
|
|
1092
|
+
});
|
|
1093
|
+
process.once("exit", () => {
|
|
1094
|
+
child.kill("SIGTERM");
|
|
1095
|
+
});
|
|
1096
|
+
setWorkerRef(worker, false);
|
|
1097
|
+
return worker;
|
|
1098
|
+
}
|
|
1099
|
+
function setRefable(value, ref) {
|
|
1100
|
+
if (!value) {
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const method = ref ? "ref" : "unref";
|
|
1104
|
+
const refable = value;
|
|
1105
|
+
refable[method]?.();
|
|
1106
|
+
}
|
|
1107
|
+
function setWorkerRef(currentWorker, ref) {
|
|
1108
|
+
setRefable(currentWorker.child, ref);
|
|
1109
|
+
setRefable(currentWorker.child.stdin, ref);
|
|
1110
|
+
setRefable(currentWorker.child.stdout, ref);
|
|
1111
|
+
setRefable(currentWorker.child.stderr, ref);
|
|
1112
|
+
}
|
|
1113
|
+
async function runPinnedPythonOperation(params) {
|
|
1114
|
+
const requestId = nextRequestId++;
|
|
1115
|
+
const currentWorker = getWorker();
|
|
1116
|
+
if (typeof currentWorker.child.stdin?.write !== "function") {
|
|
1117
|
+
throw new errors_FsSafeError("helper-unavailable", "Python helper stdin is unavailable");
|
|
1118
|
+
}
|
|
1119
|
+
setWorkerRef(currentWorker, true);
|
|
1120
|
+
return await new Promise((resolve, reject) => {
|
|
1121
|
+
currentWorker.pending.set(requestId, {
|
|
1122
|
+
reject,
|
|
1123
|
+
resolve: (value) => resolve(value),
|
|
1124
|
+
});
|
|
1125
|
+
const request = JSON.stringify({
|
|
1126
|
+
id: requestId,
|
|
1127
|
+
operation: params.operation,
|
|
1128
|
+
rootPath: params.rootPath,
|
|
1129
|
+
payload: params.payload,
|
|
1130
|
+
});
|
|
1131
|
+
currentWorker.child.stdin.write(`${request}\n`, (error) => {
|
|
1132
|
+
if (error) {
|
|
1133
|
+
currentWorker.pending.delete(requestId);
|
|
1134
|
+
if (currentWorker.pending.size === 0) {
|
|
1135
|
+
setWorkerRef(currentWorker, false);
|
|
1136
|
+
}
|
|
1137
|
+
reject(error);
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
function assertPinnedPythonOperationAvailable() {
|
|
1143
|
+
const currentWorker = getWorker();
|
|
1144
|
+
if (typeof currentWorker.child.stdin?.write !== "function") {
|
|
1145
|
+
throw new errors_FsSafeError("helper-unavailable", "Python helper stdin is unavailable");
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
function validatePinnedOperationPayload(payload) {
|
|
1149
|
+
if (typeof payload.relativePath === "string") {
|
|
1150
|
+
validatePinnedRelativePath(payload.relativePath);
|
|
1151
|
+
}
|
|
1152
|
+
if (typeof payload.relativeParentPath === "string") {
|
|
1153
|
+
validatePinnedRelativePath(payload.relativeParentPath);
|
|
1154
|
+
}
|
|
1155
|
+
if (typeof payload.from === "string") {
|
|
1156
|
+
validatePinnedRelativePath(payload.from);
|
|
1157
|
+
}
|
|
1158
|
+
if (typeof payload.to === "string") {
|
|
1159
|
+
validatePinnedRelativePath(payload.to);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
function isPinnedHelperUnavailable(error) {
|
|
1163
|
+
return error instanceof Error &&
|
|
1164
|
+
"code" in error &&
|
|
1165
|
+
error.code === "helper-unavailable";
|
|
1166
|
+
}
|
|
1167
|
+
function validatePinnedRelativePath(relativePath) {
|
|
1168
|
+
if (relativePath.length === 0 || relativePath === ".") {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
if (relativePath.includes("\0")) {
|
|
1172
|
+
throw new errors_FsSafeError("invalid-path", "relative path contains a NUL byte");
|
|
1173
|
+
}
|
|
1174
|
+
if (relativePath.startsWith("/") ||
|
|
1175
|
+
relativePath.startsWith("//") ||
|
|
1176
|
+
relativePath === ".." ||
|
|
1177
|
+
relativePath.startsWith("../") ||
|
|
1178
|
+
relativePath.startsWith("..\\")) {
|
|
1179
|
+
throw new errors_FsSafeError("invalid-path", "relative path must not escape root");
|
|
1180
|
+
}
|
|
1181
|
+
for (const segment of relativePath.split("/")) {
|
|
1182
|
+
if (segment === "..") {
|
|
1183
|
+
throw new errors_FsSafeError("invalid-path", "relative path must not contain '..'");
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-helper.js
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
async function runPinnedHelper(operation, rootDir, payload) {
|
|
1192
|
+
validatePinnedOperationPayload(payload);
|
|
1193
|
+
return await runPinnedPythonOperation({
|
|
1194
|
+
operation,
|
|
1195
|
+
rootPath: rootDir,
|
|
1196
|
+
payload,
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
async function helperStat(rootDir, relativePath) {
|
|
1200
|
+
return await runPinnedHelper("stat", rootDir, { relativePath });
|
|
1201
|
+
}
|
|
1202
|
+
async function helperReaddir(rootDir, relativePath, withFileTypes) {
|
|
1203
|
+
return await runPinnedHelper("readdir", rootDir, {
|
|
1204
|
+
relativePath,
|
|
1205
|
+
withFileTypes,
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-path.js
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
function isPinnedPathHelperSpawnError(error) {
|
|
1214
|
+
return canFallbackFromPythonError(error);
|
|
1215
|
+
}
|
|
1216
|
+
async function runPinnedPathHelper(params) {
|
|
1217
|
+
try {
|
|
1218
|
+
await runPinnedHelper(params.operation, params.rootPath, {
|
|
1219
|
+
relativePath: params.relativePath,
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
catch (error) {
|
|
1223
|
+
if (error instanceof errors_FsSafeError) {
|
|
1224
|
+
throw error;
|
|
1225
|
+
}
|
|
1226
|
+
throw new errors_FsSafeError("helper-failed", "pinned path helper failed", {
|
|
1227
|
+
cause: error instanceof Error ? error : undefined,
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-write.js
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
function byteLength(input, encoding) {
|
|
1245
|
+
return typeof input === "string"
|
|
1246
|
+
? Buffer.byteLength(input, encoding ?? "utf8")
|
|
1247
|
+
: input.byteLength;
|
|
1248
|
+
}
|
|
1249
|
+
function assertSafeBasename(basename) {
|
|
1250
|
+
if (!basename ||
|
|
1251
|
+
basename === "." ||
|
|
1252
|
+
basename === ".." ||
|
|
1253
|
+
basename.includes("/") ||
|
|
1254
|
+
basename.includes("\0")) {
|
|
1255
|
+
throw new errors_FsSafeError("invalid-path", "invalid target path");
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
function assertWithinMaxBytes(bytes, maxBytes) {
|
|
1259
|
+
if (maxBytes !== undefined && bytes > maxBytes) {
|
|
1260
|
+
throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function pinned_write_createMaxBytesTransform(maxBytes) {
|
|
1264
|
+
if (maxBytes === undefined) {
|
|
1265
|
+
return undefined;
|
|
1266
|
+
}
|
|
1267
|
+
let bytes = 0;
|
|
1268
|
+
return new external_node_stream_namespaceObject.Transform({
|
|
1269
|
+
transform(chunk, _encoding, callback) {
|
|
1270
|
+
bytes += chunk.byteLength;
|
|
1271
|
+
if (bytes > maxBytes) {
|
|
1272
|
+
callback(new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`));
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
callback(null, chunk);
|
|
1276
|
+
},
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
async function pipelineWithMaxBytes(stream, destination, maxBytes) {
|
|
1280
|
+
const limiter = pinned_write_createMaxBytesTransform(maxBytes);
|
|
1281
|
+
if (limiter) {
|
|
1282
|
+
await (0,external_node_stream_promises_namespaceObject.pipeline)(stream, limiter, destination);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
await (0,external_node_stream_promises_namespaceObject.pipeline)(stream, destination);
|
|
1286
|
+
}
|
|
1287
|
+
async function inputToBase64(input, maxBytes) {
|
|
1288
|
+
if (input.kind === "buffer") {
|
|
1289
|
+
assertWithinMaxBytes(byteLength(input.data, input.encoding), maxBytes);
|
|
1290
|
+
return (typeof input.data === "string"
|
|
1291
|
+
? Buffer.from(input.data, input.encoding ?? "utf8")
|
|
1292
|
+
: input.data).toString("base64");
|
|
1293
|
+
}
|
|
1294
|
+
const chunks = [];
|
|
1295
|
+
let bytes = 0;
|
|
1296
|
+
for await (const chunk of input.stream) {
|
|
1297
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1298
|
+
bytes += buffer.byteLength;
|
|
1299
|
+
assertWithinMaxBytes(bytes, maxBytes);
|
|
1300
|
+
chunks.push(buffer);
|
|
1301
|
+
}
|
|
1302
|
+
return Buffer.concat(chunks, bytes).toString("base64");
|
|
1303
|
+
}
|
|
1304
|
+
async function runPinnedWriteHelper(params) {
|
|
1305
|
+
assertSafeBasename(params.basename);
|
|
1306
|
+
validatePinnedOperationPayload({
|
|
1307
|
+
relativeParentPath: params.relativeParentPath,
|
|
1308
|
+
});
|
|
1309
|
+
if (getFsSafePythonConfig().mode === "off") {
|
|
1310
|
+
return await runPinnedWriteFallback(params);
|
|
1311
|
+
}
|
|
1312
|
+
if (params.input.kind === "stream") {
|
|
1313
|
+
try {
|
|
1314
|
+
assertPinnedPythonOperationAvailable();
|
|
1315
|
+
}
|
|
1316
|
+
catch (error) {
|
|
1317
|
+
if (canFallbackFromPythonError(error)) {
|
|
1318
|
+
return await runPinnedWriteFallback(params);
|
|
1319
|
+
}
|
|
1320
|
+
throw error;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
const payload = {
|
|
1324
|
+
base64: await inputToBase64(params.input, params.maxBytes),
|
|
1325
|
+
basename: params.basename,
|
|
1326
|
+
maxBytes: params.maxBytes ?? -1,
|
|
1327
|
+
mkdir: params.mkdir,
|
|
1328
|
+
mode: params.mode || 0o600,
|
|
1329
|
+
overwrite: params.overwrite !== false,
|
|
1330
|
+
relativeParentPath: params.relativeParentPath,
|
|
1331
|
+
};
|
|
1332
|
+
try {
|
|
1333
|
+
return await runPinnedPythonOperation({
|
|
1334
|
+
operation: "write",
|
|
1335
|
+
rootPath: params.rootPath,
|
|
1336
|
+
payload,
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
catch (error) {
|
|
1340
|
+
if (canFallbackFromPythonError(error)) {
|
|
1341
|
+
return await runPinnedWriteFallback(params);
|
|
1342
|
+
}
|
|
1343
|
+
throw error;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
async function runPinnedCopyHelper(params) {
|
|
1347
|
+
assertSafeBasename(params.basename);
|
|
1348
|
+
validatePinnedOperationPayload({
|
|
1349
|
+
relativeParentPath: params.relativeParentPath,
|
|
1350
|
+
});
|
|
1351
|
+
return await runPinnedPythonOperation({
|
|
1352
|
+
operation: "copy",
|
|
1353
|
+
rootPath: params.rootPath,
|
|
1354
|
+
payload: {
|
|
1355
|
+
basename: params.basename,
|
|
1356
|
+
maxBytes: params.maxBytes ?? -1,
|
|
1357
|
+
mkdir: params.mkdir,
|
|
1358
|
+
mode: params.mode || 0o600,
|
|
1359
|
+
overwrite: params.overwrite !== false,
|
|
1360
|
+
relativeParentPath: params.relativeParentPath,
|
|
1361
|
+
sourceDev: params.sourceIdentity.dev,
|
|
1362
|
+
sourceIno: params.sourceIdentity.ino,
|
|
1363
|
+
sourcePath: params.sourcePath,
|
|
1364
|
+
},
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
async function runPinnedWriteFallback(params) {
|
|
1368
|
+
const parentPath = params.relativeParentPath
|
|
1369
|
+
? external_node_path_namespaceObject.join(params.rootPath, ...params.relativeParentPath.split("/"))
|
|
1370
|
+
: params.rootPath;
|
|
1371
|
+
const parentGuard = await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
|
|
1372
|
+
if (params.mkdir) {
|
|
1373
|
+
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
1374
|
+
await promises_namespaceObject.mkdir(parentPath, { recursive: true });
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
const targetPath = external_node_path_namespaceObject.join(parentPath, params.basename);
|
|
1378
|
+
if (params.overwrite === false) {
|
|
1379
|
+
let handle = await withAsyncDirectoryGuards([parentGuard], async () => await promises_namespaceObject.open(targetPath, external_node_fs_namespaceObject.constants.O_WRONLY | external_node_fs_namespaceObject.constants.O_CREAT | external_node_fs_namespaceObject.constants.O_EXCL, params.mode), {
|
|
1380
|
+
onPostGuardFailure: async (openedHandle) => {
|
|
1381
|
+
// The parent failed verification, so targetPath may now resolve
|
|
1382
|
+
// somewhere else. Close the fd, but do not clean up by path.
|
|
1383
|
+
await openedHandle.close().catch(() => undefined);
|
|
1384
|
+
},
|
|
1385
|
+
});
|
|
1386
|
+
let created = true;
|
|
1387
|
+
try {
|
|
1388
|
+
if (params.input.kind === "buffer") {
|
|
1389
|
+
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
1390
|
+
if (typeof params.input.data === "string") {
|
|
1391
|
+
await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
|
|
1392
|
+
}
|
|
1393
|
+
else {
|
|
1394
|
+
await handle.writeFile(params.input.data);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
await pipelineWithMaxBytes(params.input.stream, handle.createWriteStream(), params.maxBytes);
|
|
1399
|
+
}
|
|
1400
|
+
const stat = await handle.stat();
|
|
1401
|
+
created = false;
|
|
1402
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
1403
|
+
}
|
|
1404
|
+
finally {
|
|
1405
|
+
await handle.close().catch(() => undefined);
|
|
1406
|
+
if (created) {
|
|
1407
|
+
await promises_namespaceObject.rm(targetPath, { force: true }).catch(() => undefined);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const tempPath = external_node_path_namespaceObject.join(parentPath, `.${params.basename}.${(0,external_node_crypto_namespaceObject.randomUUID)()}.fallback.tmp`);
|
|
1412
|
+
const tempFlags = external_node_fs_namespaceObject.constants.O_WRONLY |
|
|
1413
|
+
external_node_fs_namespaceObject.constants.O_CREAT |
|
|
1414
|
+
external_node_fs_namespaceObject.constants.O_EXCL |
|
|
1415
|
+
(process.platform !== "win32" && "O_NOFOLLOW" in external_node_fs_namespaceObject.constants
|
|
1416
|
+
? external_node_fs_namespaceObject.constants.O_NOFOLLOW
|
|
1417
|
+
: 0);
|
|
1418
|
+
let handle;
|
|
1419
|
+
let handleClosedByStream = false;
|
|
1420
|
+
try {
|
|
1421
|
+
handle = await promises_namespaceObject.open(tempPath, tempFlags, params.mode);
|
|
1422
|
+
if (params.input.kind === "buffer") {
|
|
1423
|
+
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
1424
|
+
if (typeof params.input.data === "string") {
|
|
1425
|
+
await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
await handle.writeFile(params.input.data);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
const writable = handle.createWriteStream();
|
|
1433
|
+
writable.once("close", () => {
|
|
1434
|
+
handleClosedByStream = true;
|
|
1435
|
+
});
|
|
1436
|
+
await pipelineWithMaxBytes(params.input.stream, writable, params.maxBytes);
|
|
1437
|
+
}
|
|
1438
|
+
if (!handleClosedByStream) {
|
|
1439
|
+
await handle.close().catch(() => undefined);
|
|
1440
|
+
handle = undefined;
|
|
1441
|
+
}
|
|
1442
|
+
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
1443
|
+
await promises_namespaceObject.rename(tempPath, targetPath);
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
catch (error) {
|
|
1447
|
+
if (handle && !handleClosedByStream) {
|
|
1448
|
+
await handle.close().catch(() => undefined);
|
|
1449
|
+
}
|
|
1450
|
+
await promises_namespaceObject.rm(tempPath, { force: true }).catch(() => undefined);
|
|
1451
|
+
throw error;
|
|
1452
|
+
}
|
|
1453
|
+
const stat = await promises_namespaceObject.stat(targetPath);
|
|
1454
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
;// CONCATENATED MODULE: external "node:os"
|
|
1458
|
+
const external_node_os_namespaceObject = __rspack_createRequire_require("node:os");
|
|
1459
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-path.js
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
const ROOT_PATH_ALIAS_POLICIES = {
|
|
1466
|
+
strict: Object.freeze({
|
|
1467
|
+
allowFinalSymlinkForUnlink: false,
|
|
1468
|
+
allowFinalHardlinkForUnlink: false,
|
|
1469
|
+
}),
|
|
1470
|
+
unlinkTarget: Object.freeze({
|
|
1471
|
+
allowFinalSymlinkForUnlink: true,
|
|
1472
|
+
allowFinalHardlinkForUnlink: true,
|
|
1473
|
+
}),
|
|
1474
|
+
};
|
|
1475
|
+
async function resolveRootPath(params) {
|
|
1476
|
+
const rootPath = external_node_path_namespaceObject.resolve(params.rootPath);
|
|
1477
|
+
const absolutePath = external_node_path_namespaceObject.resolve(params.absolutePath);
|
|
1478
|
+
const rootCanonicalPath = params.rootCanonicalPath
|
|
1479
|
+
? external_node_path_namespaceObject.resolve(params.rootCanonicalPath)
|
|
1480
|
+
: await resolvePathViaExistingAncestor(rootPath);
|
|
1481
|
+
const context = createBoundaryResolutionContext({
|
|
1482
|
+
resolveParams: params,
|
|
1483
|
+
rootPath,
|
|
1484
|
+
absolutePath,
|
|
1485
|
+
rootCanonicalPath,
|
|
1486
|
+
outsideLexicalCanonicalPath: await resolveOutsideLexicalCanonicalPathAsync({
|
|
1487
|
+
rootPath,
|
|
1488
|
+
absolutePath,
|
|
1489
|
+
}),
|
|
1490
|
+
});
|
|
1491
|
+
const outsideResult = await resolveOutsideRootPathAsync({
|
|
1492
|
+
boundaryLabel: params.boundaryLabel,
|
|
1493
|
+
context,
|
|
1494
|
+
});
|
|
1495
|
+
if (outsideResult) {
|
|
1496
|
+
return outsideResult;
|
|
1497
|
+
}
|
|
1498
|
+
return resolveRootPathLexicalAsync({
|
|
1499
|
+
params,
|
|
1500
|
+
absolutePath: context.absolutePath,
|
|
1501
|
+
rootPath: context.rootPath,
|
|
1502
|
+
rootCanonicalPath: context.rootCanonicalPath,
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
function resolveRootPathSync(params) {
|
|
1506
|
+
const rootPath = path.resolve(params.rootPath);
|
|
1507
|
+
const absolutePath = path.resolve(params.absolutePath);
|
|
1508
|
+
const rootCanonicalPath = params.rootCanonicalPath
|
|
1509
|
+
? path.resolve(params.rootCanonicalPath)
|
|
1510
|
+
: resolvePathViaExistingAncestorSync(rootPath);
|
|
1511
|
+
const context = createBoundaryResolutionContext({
|
|
1512
|
+
resolveParams: params,
|
|
1513
|
+
rootPath,
|
|
1514
|
+
absolutePath,
|
|
1515
|
+
rootCanonicalPath,
|
|
1516
|
+
outsideLexicalCanonicalPath: resolveOutsideLexicalCanonicalPathSync({
|
|
1517
|
+
rootPath,
|
|
1518
|
+
absolutePath,
|
|
1519
|
+
}),
|
|
1520
|
+
});
|
|
1521
|
+
const outsideResult = resolveOutsideRootPathSync({
|
|
1522
|
+
boundaryLabel: params.boundaryLabel,
|
|
1523
|
+
context,
|
|
1524
|
+
});
|
|
1525
|
+
if (outsideResult) {
|
|
1526
|
+
return outsideResult;
|
|
1527
|
+
}
|
|
1528
|
+
return resolveRootPathLexicalSync({
|
|
1529
|
+
params,
|
|
1530
|
+
absolutePath: context.absolutePath,
|
|
1531
|
+
rootPath: context.rootPath,
|
|
1532
|
+
rootCanonicalPath: context.rootCanonicalPath,
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
function isPromiseLike(value) {
|
|
1536
|
+
return Boolean(value &&
|
|
1537
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
1538
|
+
"then" in value &&
|
|
1539
|
+
typeof value.then === "function");
|
|
1540
|
+
}
|
|
1541
|
+
function createLexicalTraversalState(params) {
|
|
1542
|
+
const relative = external_node_path_namespaceObject.relative(params.rootPath, params.absolutePath);
|
|
1543
|
+
return {
|
|
1544
|
+
segments: relative.split(external_node_path_namespaceObject.sep).filter(Boolean),
|
|
1545
|
+
allowFinalSymlink: params.params.policy?.allowFinalSymlinkForUnlink === true,
|
|
1546
|
+
canonicalCursor: params.rootCanonicalPath,
|
|
1547
|
+
lexicalCursor: params.rootPath,
|
|
1548
|
+
preserveFinalSymlink: false,
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
function assertLexicalCursorInsideBoundary(params) {
|
|
1552
|
+
assertInsideBoundary({
|
|
1553
|
+
boundaryLabel: params.params.boundaryLabel,
|
|
1554
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1555
|
+
candidatePath: params.candidatePath,
|
|
1556
|
+
absolutePath: params.absolutePath,
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
function applyMissingSuffixToCanonicalCursor(params) {
|
|
1560
|
+
const missingSuffix = params.state.segments.slice(params.missingFromIndex);
|
|
1561
|
+
params.state.canonicalCursor = external_node_path_namespaceObject.resolve(params.state.canonicalCursor, ...missingSuffix);
|
|
1562
|
+
assertLexicalCursorInsideBoundary({
|
|
1563
|
+
params: params.params,
|
|
1564
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1565
|
+
candidatePath: params.state.canonicalCursor,
|
|
1566
|
+
absolutePath: params.absolutePath,
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
function advanceCanonicalCursorForSegment(params) {
|
|
1570
|
+
params.state.canonicalCursor = external_node_path_namespaceObject.resolve(params.state.canonicalCursor, params.segment);
|
|
1571
|
+
assertLexicalCursorInsideBoundary({
|
|
1572
|
+
params: params.params,
|
|
1573
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1574
|
+
candidatePath: params.state.canonicalCursor,
|
|
1575
|
+
absolutePath: params.absolutePath,
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
function finalizeLexicalResolution(params) {
|
|
1579
|
+
assertLexicalCursorInsideBoundary({
|
|
1580
|
+
params: params.params,
|
|
1581
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1582
|
+
candidatePath: params.state.canonicalCursor,
|
|
1583
|
+
absolutePath: params.absolutePath,
|
|
1584
|
+
});
|
|
1585
|
+
return buildResolvedRootPath({
|
|
1586
|
+
absolutePath: params.absolutePath,
|
|
1587
|
+
canonicalPath: params.state.canonicalCursor,
|
|
1588
|
+
rootPath: params.rootPath,
|
|
1589
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1590
|
+
kind: params.kind,
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
function handleLexicalLstatFailure(params) {
|
|
1594
|
+
if (!path_isNotFoundPathError(params.error)) {
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1597
|
+
applyMissingSuffixToCanonicalCursor({
|
|
1598
|
+
state: params.state,
|
|
1599
|
+
missingFromIndex: params.missingFromIndex,
|
|
1600
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1601
|
+
params: params.resolveParams,
|
|
1602
|
+
absolutePath: params.absolutePath,
|
|
1603
|
+
});
|
|
1604
|
+
return true;
|
|
1605
|
+
}
|
|
1606
|
+
function handleLexicalStatReadFailure(params) {
|
|
1607
|
+
if (handleLexicalLstatFailure({
|
|
1608
|
+
error: params.error,
|
|
1609
|
+
state: params.state,
|
|
1610
|
+
missingFromIndex: params.missingFromIndex,
|
|
1611
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1612
|
+
resolveParams: params.resolveParams,
|
|
1613
|
+
absolutePath: params.absolutePath,
|
|
1614
|
+
})) {
|
|
1615
|
+
return null;
|
|
1616
|
+
}
|
|
1617
|
+
throw params.error;
|
|
1618
|
+
}
|
|
1619
|
+
function handleLexicalStatDisposition(params) {
|
|
1620
|
+
if (!params.isSymbolicLink) {
|
|
1621
|
+
advanceCanonicalCursorForSegment({
|
|
1622
|
+
state: params.state,
|
|
1623
|
+
segment: params.segment,
|
|
1624
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1625
|
+
params: params.resolveParams,
|
|
1626
|
+
absolutePath: params.absolutePath,
|
|
1627
|
+
});
|
|
1628
|
+
return "continue";
|
|
1629
|
+
}
|
|
1630
|
+
if (params.state.allowFinalSymlink && params.isLast) {
|
|
1631
|
+
params.state.preserveFinalSymlink = true;
|
|
1632
|
+
advanceCanonicalCursorForSegment({
|
|
1633
|
+
state: params.state,
|
|
1634
|
+
segment: params.segment,
|
|
1635
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1636
|
+
params: params.resolveParams,
|
|
1637
|
+
absolutePath: params.absolutePath,
|
|
1638
|
+
});
|
|
1639
|
+
return "break";
|
|
1640
|
+
}
|
|
1641
|
+
return "resolve-link";
|
|
1642
|
+
}
|
|
1643
|
+
function applyResolvedSymlinkHop(params) {
|
|
1644
|
+
if (!path_isPathInside(params.rootCanonicalPath, params.linkCanonical)) {
|
|
1645
|
+
throw symlinkEscapeError({
|
|
1646
|
+
boundaryLabel: params.boundaryLabel,
|
|
1647
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1648
|
+
symlinkPath: params.state.lexicalCursor,
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
params.state.canonicalCursor = params.linkCanonical;
|
|
1652
|
+
params.state.lexicalCursor = params.linkCanonical;
|
|
1653
|
+
}
|
|
1654
|
+
function readLexicalStat(params) {
|
|
1655
|
+
try {
|
|
1656
|
+
const stat = params.read(params.state.lexicalCursor);
|
|
1657
|
+
if (isPromiseLike(stat)) {
|
|
1658
|
+
return Promise.resolve(stat).catch((error) => handleLexicalStatReadFailure({ ...params, error }));
|
|
1659
|
+
}
|
|
1660
|
+
return stat;
|
|
1661
|
+
}
|
|
1662
|
+
catch (error) {
|
|
1663
|
+
return handleLexicalStatReadFailure({ ...params, error });
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function resolveAndApplySymlinkHop(params) {
|
|
1667
|
+
const linkCanonical = params.resolveLinkCanonical(params.state.lexicalCursor);
|
|
1668
|
+
if (isPromiseLike(linkCanonical)) {
|
|
1669
|
+
return Promise.resolve(linkCanonical).then((value) => applyResolvedSymlinkHop({
|
|
1670
|
+
state: params.state,
|
|
1671
|
+
linkCanonical: value,
|
|
1672
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1673
|
+
boundaryLabel: params.boundaryLabel,
|
|
1674
|
+
}));
|
|
1675
|
+
}
|
|
1676
|
+
applyResolvedSymlinkHop({
|
|
1677
|
+
state: params.state,
|
|
1678
|
+
linkCanonical,
|
|
1679
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1680
|
+
boundaryLabel: params.boundaryLabel,
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
function* iterateLexicalTraversal(state) {
|
|
1684
|
+
for (let idx = 0; idx < state.segments.length; idx += 1) {
|
|
1685
|
+
const segment = state.segments[idx] ?? "";
|
|
1686
|
+
const isLast = idx === state.segments.length - 1;
|
|
1687
|
+
state.lexicalCursor = external_node_path_namespaceObject.join(state.lexicalCursor, segment);
|
|
1688
|
+
yield { idx, segment, isLast };
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
async function resolveRootPathLexicalAsync(params) {
|
|
1692
|
+
const state = createLexicalTraversalState(params);
|
|
1693
|
+
const sharedStepParams = {
|
|
1694
|
+
state,
|
|
1695
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1696
|
+
resolveParams: params.params,
|
|
1697
|
+
absolutePath: params.absolutePath,
|
|
1698
|
+
};
|
|
1699
|
+
for (const { idx, segment, isLast } of iterateLexicalTraversal(state)) {
|
|
1700
|
+
const stat = await readLexicalStat({
|
|
1701
|
+
...sharedStepParams,
|
|
1702
|
+
missingFromIndex: idx,
|
|
1703
|
+
read: (cursor) => promises_namespaceObject.lstat(cursor),
|
|
1704
|
+
});
|
|
1705
|
+
if (!stat) {
|
|
1706
|
+
break;
|
|
1707
|
+
}
|
|
1708
|
+
const disposition = handleLexicalStatDisposition({
|
|
1709
|
+
...sharedStepParams,
|
|
1710
|
+
isSymbolicLink: stat.isSymbolicLink(),
|
|
1711
|
+
segment,
|
|
1712
|
+
isLast,
|
|
1713
|
+
});
|
|
1714
|
+
if (disposition === "continue") {
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
if (disposition === "break") {
|
|
1718
|
+
break;
|
|
1719
|
+
}
|
|
1720
|
+
await resolveAndApplySymlinkHop({
|
|
1721
|
+
state,
|
|
1722
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1723
|
+
boundaryLabel: params.params.boundaryLabel,
|
|
1724
|
+
resolveLinkCanonical: (cursor) => resolveSymlinkHopPath(cursor),
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
const kind = await getPathKind(params.absolutePath, state.preserveFinalSymlink);
|
|
1728
|
+
return finalizeLexicalResolution({
|
|
1729
|
+
...params,
|
|
1730
|
+
state,
|
|
1731
|
+
kind,
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
function resolveRootPathLexicalSync(params) {
|
|
1735
|
+
const state = createLexicalTraversalState(params);
|
|
1736
|
+
for (let idx = 0; idx < state.segments.length; idx += 1) {
|
|
1737
|
+
const segment = state.segments[idx] ?? "";
|
|
1738
|
+
const isLast = idx === state.segments.length - 1;
|
|
1739
|
+
state.lexicalCursor = path.join(state.lexicalCursor, segment);
|
|
1740
|
+
const maybeStat = readLexicalStat({
|
|
1741
|
+
state,
|
|
1742
|
+
missingFromIndex: idx,
|
|
1743
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1744
|
+
resolveParams: params.params,
|
|
1745
|
+
absolutePath: params.absolutePath,
|
|
1746
|
+
read: (cursor) => fs.lstatSync(cursor),
|
|
1747
|
+
});
|
|
1748
|
+
if (isPromiseLike(maybeStat)) {
|
|
1749
|
+
throw new Error("Unexpected async lexical stat");
|
|
1750
|
+
}
|
|
1751
|
+
const stat = maybeStat;
|
|
1752
|
+
if (!stat) {
|
|
1753
|
+
break;
|
|
1754
|
+
}
|
|
1755
|
+
const disposition = handleLexicalStatDisposition({
|
|
1756
|
+
state,
|
|
1757
|
+
isSymbolicLink: stat.isSymbolicLink(),
|
|
1758
|
+
segment,
|
|
1759
|
+
isLast,
|
|
1760
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1761
|
+
resolveParams: params.params,
|
|
1762
|
+
absolutePath: params.absolutePath,
|
|
1763
|
+
});
|
|
1764
|
+
if (disposition === "continue") {
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
if (disposition === "break") {
|
|
1768
|
+
break;
|
|
1769
|
+
}
|
|
1770
|
+
const maybeApplied = resolveAndApplySymlinkHop({
|
|
1771
|
+
state,
|
|
1772
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1773
|
+
boundaryLabel: params.params.boundaryLabel,
|
|
1774
|
+
resolveLinkCanonical: (cursor) => resolveSymlinkHopPathSync(cursor),
|
|
1775
|
+
});
|
|
1776
|
+
if (isPromiseLike(maybeApplied)) {
|
|
1777
|
+
throw new Error("Unexpected async symlink resolution");
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
const kind = getPathKindSync(params.absolutePath, state.preserveFinalSymlink);
|
|
1781
|
+
return finalizeLexicalResolution({
|
|
1782
|
+
...params,
|
|
1783
|
+
state,
|
|
1784
|
+
kind,
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
function resolveCanonicalOutsideLexicalPath(params) {
|
|
1788
|
+
return params.outsideLexicalCanonicalPath ?? params.absolutePath;
|
|
1789
|
+
}
|
|
1790
|
+
function createBoundaryResolutionContext(params) {
|
|
1791
|
+
const lexicalInside = path_isPathInside(params.rootPath, params.absolutePath);
|
|
1792
|
+
const canonicalOutsideLexicalPath = resolveCanonicalOutsideLexicalPath({
|
|
1793
|
+
absolutePath: params.absolutePath,
|
|
1794
|
+
outsideLexicalCanonicalPath: params.outsideLexicalCanonicalPath,
|
|
1795
|
+
});
|
|
1796
|
+
assertLexicalBoundaryOrCanonicalAlias({
|
|
1797
|
+
skipLexicalRootCheck: params.resolveParams.skipLexicalRootCheck,
|
|
1798
|
+
lexicalInside,
|
|
1799
|
+
canonicalOutsideLexicalPath,
|
|
1800
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1801
|
+
boundaryLabel: params.resolveParams.boundaryLabel,
|
|
1802
|
+
rootPath: params.rootPath,
|
|
1803
|
+
absolutePath: params.absolutePath,
|
|
1804
|
+
});
|
|
1805
|
+
return {
|
|
1806
|
+
rootPath: params.rootPath,
|
|
1807
|
+
absolutePath: params.absolutePath,
|
|
1808
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1809
|
+
lexicalInside,
|
|
1810
|
+
canonicalOutsideLexicalPath,
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
async function resolveOutsideRootPathAsync(params) {
|
|
1814
|
+
if (params.context.lexicalInside) {
|
|
1815
|
+
return null;
|
|
1816
|
+
}
|
|
1817
|
+
const kind = await getPathKind(params.context.absolutePath, false);
|
|
1818
|
+
return buildOutsideRootPathFromContext({
|
|
1819
|
+
boundaryLabel: params.boundaryLabel,
|
|
1820
|
+
context: params.context,
|
|
1821
|
+
kind,
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
function resolveOutsideRootPathSync(params) {
|
|
1825
|
+
if (params.context.lexicalInside) {
|
|
1826
|
+
return null;
|
|
1827
|
+
}
|
|
1828
|
+
const kind = getPathKindSync(params.context.absolutePath, false);
|
|
1829
|
+
return buildOutsideRootPathFromContext({
|
|
1830
|
+
boundaryLabel: params.boundaryLabel,
|
|
1831
|
+
context: params.context,
|
|
1832
|
+
kind,
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
function buildOutsideRootPathFromContext(params) {
|
|
1836
|
+
return buildOutsideLexicalRootPath({
|
|
1837
|
+
boundaryLabel: params.boundaryLabel,
|
|
1838
|
+
rootCanonicalPath: params.context.rootCanonicalPath,
|
|
1839
|
+
absolutePath: params.context.absolutePath,
|
|
1840
|
+
canonicalOutsideLexicalPath: params.context.canonicalOutsideLexicalPath,
|
|
1841
|
+
rootPath: params.context.rootPath,
|
|
1842
|
+
kind: params.kind,
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
async function resolveOutsideLexicalCanonicalPathAsync(params) {
|
|
1846
|
+
if (path_isPathInside(params.rootPath, params.absolutePath)) {
|
|
1847
|
+
return undefined;
|
|
1848
|
+
}
|
|
1849
|
+
return await resolvePathViaExistingAncestor(params.absolutePath);
|
|
1850
|
+
}
|
|
1851
|
+
function resolveOutsideLexicalCanonicalPathSync(params) {
|
|
1852
|
+
if (isPathInside(params.rootPath, params.absolutePath)) {
|
|
1853
|
+
return undefined;
|
|
1854
|
+
}
|
|
1855
|
+
return resolvePathViaExistingAncestorSync(params.absolutePath);
|
|
1856
|
+
}
|
|
1857
|
+
function buildOutsideLexicalRootPath(params) {
|
|
1858
|
+
assertInsideBoundary({
|
|
1859
|
+
boundaryLabel: params.boundaryLabel,
|
|
1860
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1861
|
+
candidatePath: params.canonicalOutsideLexicalPath,
|
|
1862
|
+
absolutePath: params.absolutePath,
|
|
1863
|
+
});
|
|
1864
|
+
return buildResolvedRootPath({
|
|
1865
|
+
absolutePath: params.absolutePath,
|
|
1866
|
+
canonicalPath: params.canonicalOutsideLexicalPath,
|
|
1867
|
+
rootPath: params.rootPath,
|
|
1868
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1869
|
+
kind: params.kind,
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
function assertLexicalBoundaryOrCanonicalAlias(params) {
|
|
1873
|
+
if (params.skipLexicalRootCheck || params.lexicalInside) {
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (path_isPathInside(params.rootCanonicalPath, params.canonicalOutsideLexicalPath)) {
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
throw pathEscapeError({
|
|
1880
|
+
boundaryLabel: params.boundaryLabel,
|
|
1881
|
+
rootPath: params.rootPath,
|
|
1882
|
+
absolutePath: params.absolutePath,
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
function buildResolvedRootPath(params) {
|
|
1886
|
+
return {
|
|
1887
|
+
absolutePath: params.absolutePath,
|
|
1888
|
+
canonicalPath: params.canonicalPath,
|
|
1889
|
+
rootPath: params.rootPath,
|
|
1890
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
1891
|
+
relativePath: relativeInsideRoot(params.rootCanonicalPath, params.canonicalPath),
|
|
1892
|
+
exists: params.kind.exists,
|
|
1893
|
+
kind: params.kind.kind,
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
async function resolvePathViaExistingAncestor(targetPath) {
|
|
1897
|
+
const normalized = external_node_path_namespaceObject.resolve(targetPath);
|
|
1898
|
+
let cursor = normalized;
|
|
1899
|
+
const missingSuffix = [];
|
|
1900
|
+
while (!isFilesystemRoot(cursor) && !(await pathExists(cursor))) {
|
|
1901
|
+
missingSuffix.unshift(external_node_path_namespaceObject.basename(cursor));
|
|
1902
|
+
const parent = external_node_path_namespaceObject.dirname(cursor);
|
|
1903
|
+
if (parent === cursor) {
|
|
1904
|
+
break;
|
|
1905
|
+
}
|
|
1906
|
+
cursor = parent;
|
|
1907
|
+
}
|
|
1908
|
+
if (!(await pathExists(cursor))) {
|
|
1909
|
+
return normalized;
|
|
1910
|
+
}
|
|
1911
|
+
try {
|
|
1912
|
+
const resolvedAncestor = external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(cursor));
|
|
1913
|
+
if (missingSuffix.length === 0) {
|
|
1914
|
+
return resolvedAncestor;
|
|
1915
|
+
}
|
|
1916
|
+
return external_node_path_namespaceObject.resolve(resolvedAncestor, ...missingSuffix);
|
|
1917
|
+
}
|
|
1918
|
+
catch {
|
|
1919
|
+
return normalized;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
function resolvePathViaExistingAncestorSync(targetPath) {
|
|
1923
|
+
const normalized = path.resolve(targetPath);
|
|
1924
|
+
let cursor = normalized;
|
|
1925
|
+
const missingSuffix = [];
|
|
1926
|
+
while (!isFilesystemRoot(cursor) && !fs.existsSync(cursor)) {
|
|
1927
|
+
missingSuffix.unshift(path.basename(cursor));
|
|
1928
|
+
const parent = path.dirname(cursor);
|
|
1929
|
+
if (parent === cursor) {
|
|
1930
|
+
break;
|
|
1931
|
+
}
|
|
1932
|
+
cursor = parent;
|
|
1933
|
+
}
|
|
1934
|
+
if (!fs.existsSync(cursor)) {
|
|
1935
|
+
return normalized;
|
|
1936
|
+
}
|
|
1937
|
+
try {
|
|
1938
|
+
// Keep sync behavior aligned with async (`fsp.realpath`) to avoid
|
|
1939
|
+
// platform-specific canonical alias drift (notably on Windows).
|
|
1940
|
+
const resolvedAncestor = path.resolve(fs.realpathSync(cursor));
|
|
1941
|
+
if (missingSuffix.length === 0) {
|
|
1942
|
+
return resolvedAncestor;
|
|
1943
|
+
}
|
|
1944
|
+
return path.resolve(resolvedAncestor, ...missingSuffix);
|
|
1945
|
+
}
|
|
1946
|
+
catch {
|
|
1947
|
+
return normalized;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
async function getPathKind(absolutePath, preserveFinalSymlink) {
|
|
1951
|
+
try {
|
|
1952
|
+
const stat = preserveFinalSymlink
|
|
1953
|
+
? await promises_namespaceObject.lstat(absolutePath)
|
|
1954
|
+
: await promises_namespaceObject.stat(absolutePath);
|
|
1955
|
+
return { exists: true, kind: toResolvedKind(stat) };
|
|
1956
|
+
}
|
|
1957
|
+
catch (error) {
|
|
1958
|
+
if (path_isNotFoundPathError(error)) {
|
|
1959
|
+
return { exists: false, kind: "missing" };
|
|
1960
|
+
}
|
|
1961
|
+
throw error;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
function getPathKindSync(absolutePath, preserveFinalSymlink) {
|
|
1965
|
+
try {
|
|
1966
|
+
const stat = preserveFinalSymlink ? fs.lstatSync(absolutePath) : fs.statSync(absolutePath);
|
|
1967
|
+
return { exists: true, kind: toResolvedKind(stat) };
|
|
1968
|
+
}
|
|
1969
|
+
catch (error) {
|
|
1970
|
+
if (isNotFoundPathError(error)) {
|
|
1971
|
+
return { exists: false, kind: "missing" };
|
|
1972
|
+
}
|
|
1973
|
+
throw error;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
function toResolvedKind(stat) {
|
|
1977
|
+
if (stat.isFile()) {
|
|
1978
|
+
return "file";
|
|
1979
|
+
}
|
|
1980
|
+
if (stat.isDirectory()) {
|
|
1981
|
+
return "directory";
|
|
1982
|
+
}
|
|
1983
|
+
if (stat.isSymbolicLink()) {
|
|
1984
|
+
return "symlink";
|
|
1985
|
+
}
|
|
1986
|
+
return "other";
|
|
1987
|
+
}
|
|
1988
|
+
function relativeInsideRoot(rootPath, targetPath) {
|
|
1989
|
+
const relative = external_node_path_namespaceObject.relative(external_node_path_namespaceObject.resolve(rootPath), external_node_path_namespaceObject.resolve(targetPath));
|
|
1990
|
+
if (!relative || relative === ".") {
|
|
1991
|
+
return "";
|
|
1992
|
+
}
|
|
1993
|
+
if (relative.startsWith("..") || external_node_path_namespaceObject.isAbsolute(relative)) {
|
|
1994
|
+
return "";
|
|
1995
|
+
}
|
|
1996
|
+
return relative;
|
|
1997
|
+
}
|
|
1998
|
+
function assertInsideBoundary(params) {
|
|
1999
|
+
if (path_isPathInside(params.rootCanonicalPath, params.candidatePath)) {
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
throw new Error(`Path resolves outside ${params.boundaryLabel} (${shortPath(params.rootCanonicalPath)}): ${shortPath(params.absolutePath)}`);
|
|
2003
|
+
}
|
|
2004
|
+
function pathEscapeError(params) {
|
|
2005
|
+
return new Error(`Path escapes ${params.boundaryLabel} (${shortPath(params.rootPath)}): ${shortPath(params.absolutePath)}`);
|
|
2006
|
+
}
|
|
2007
|
+
function symlinkEscapeError(params) {
|
|
2008
|
+
return new Error(`Symlink escapes ${params.boundaryLabel} (${shortPath(params.rootCanonicalPath)}): ${shortPath(params.symlinkPath)}`);
|
|
2009
|
+
}
|
|
2010
|
+
function shortPath(value) {
|
|
2011
|
+
const home = external_node_os_namespaceObject.homedir();
|
|
2012
|
+
if (value.startsWith(home)) {
|
|
2013
|
+
return `~${value.slice(home.length)}`;
|
|
2014
|
+
}
|
|
2015
|
+
return value;
|
|
2016
|
+
}
|
|
2017
|
+
function isFilesystemRoot(candidate) {
|
|
2018
|
+
return external_node_path_namespaceObject.parse(candidate).root === candidate;
|
|
2019
|
+
}
|
|
2020
|
+
async function pathExists(targetPath) {
|
|
2021
|
+
try {
|
|
2022
|
+
await promises_namespaceObject.lstat(targetPath);
|
|
2023
|
+
return true;
|
|
2024
|
+
}
|
|
2025
|
+
catch (error) {
|
|
2026
|
+
if (path_isNotFoundPathError(error)) {
|
|
2027
|
+
return false;
|
|
2028
|
+
}
|
|
2029
|
+
throw error;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
async function resolveSymlinkHopPath(symlinkPath) {
|
|
2033
|
+
try {
|
|
2034
|
+
return external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(symlinkPath));
|
|
2035
|
+
}
|
|
2036
|
+
catch (error) {
|
|
2037
|
+
if (!path_isNotFoundPathError(error)) {
|
|
2038
|
+
throw error;
|
|
2039
|
+
}
|
|
2040
|
+
const linkTarget = await promises_namespaceObject.readlink(symlinkPath);
|
|
2041
|
+
const linkAbsolute = external_node_path_namespaceObject.resolve(external_node_path_namespaceObject.dirname(symlinkPath), linkTarget);
|
|
2042
|
+
return resolvePathViaExistingAncestor(linkAbsolute);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
function resolveSymlinkHopPathSync(symlinkPath) {
|
|
2046
|
+
try {
|
|
2047
|
+
return path.resolve(fs.realpathSync(symlinkPath));
|
|
2048
|
+
}
|
|
2049
|
+
catch (error) {
|
|
2050
|
+
if (!isNotFoundPathError(error)) {
|
|
2051
|
+
throw error;
|
|
2052
|
+
}
|
|
2053
|
+
const linkTarget = fs.readlinkSync(symlinkPath);
|
|
2054
|
+
const linkAbsolute = path.resolve(path.dirname(symlinkPath), linkTarget);
|
|
2055
|
+
return resolvePathViaExistingAncestorSync(linkAbsolute);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/path-policy.js
|
|
2060
|
+
|
|
2061
|
+
|
|
2062
|
+
|
|
2063
|
+
|
|
2064
|
+
const PATH_ALIAS_POLICIES = ROOT_PATH_ALIAS_POLICIES;
|
|
2065
|
+
async function assertNoPathAliasEscape(params) {
|
|
2066
|
+
const resolved = await resolveRootPath({
|
|
2067
|
+
absolutePath: params.absolutePath,
|
|
2068
|
+
rootPath: params.rootPath,
|
|
2069
|
+
boundaryLabel: params.boundaryLabel,
|
|
2070
|
+
policy: params.policy,
|
|
2071
|
+
});
|
|
2072
|
+
const allowFinalSymlink = params.policy?.allowFinalSymlinkForUnlink === true;
|
|
2073
|
+
if (allowFinalSymlink && resolved.kind === "symlink") {
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
await assertNoHardlinkedFinalPath({
|
|
2077
|
+
filePath: resolved.absolutePath,
|
|
2078
|
+
root: resolved.rootPath,
|
|
2079
|
+
boundaryLabel: params.boundaryLabel,
|
|
2080
|
+
allowFinalHardlinkForUnlink: params.policy?.allowFinalHardlinkForUnlink,
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
async function assertNoHardlinkedFinalPath(params) {
|
|
2084
|
+
if (params.allowFinalHardlinkForUnlink) {
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
let stat;
|
|
2088
|
+
try {
|
|
2089
|
+
stat = await promises_namespaceObject.stat(params.filePath);
|
|
2090
|
+
}
|
|
2091
|
+
catch (err) {
|
|
2092
|
+
if (path_isNotFoundPathError(err)) {
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
throw err;
|
|
2096
|
+
}
|
|
2097
|
+
if (!stat.isFile()) {
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
if (stat.nlink > 1) {
|
|
2101
|
+
throw new Error(`Hardlinked path is not allowed under ${params.boundaryLabel} (${path_policy_shortPath(params.root)}): ${path_policy_shortPath(params.filePath)}`);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
function path_policy_shortPath(value) {
|
|
2105
|
+
if (value.startsWith(external_node_os_namespaceObject.homedir())) {
|
|
2106
|
+
return `~${value.slice(external_node_os_namespaceObject.homedir().length)}`;
|
|
2107
|
+
}
|
|
2108
|
+
return value;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/path-stat.js
|
|
2112
|
+
function pathStatFromStats(stat) {
|
|
2113
|
+
return {
|
|
2114
|
+
dev: Number(stat.dev),
|
|
2115
|
+
gid: Number(stat.gid),
|
|
2116
|
+
ino: Number(stat.ino),
|
|
2117
|
+
isDirectory: stat.isDirectory(),
|
|
2118
|
+
isFile: stat.isFile(),
|
|
2119
|
+
isSymbolicLink: stat.isSymbolicLink(),
|
|
2120
|
+
mode: stat.mode,
|
|
2121
|
+
mtimeMs: stat.mtimeMs,
|
|
2122
|
+
nlink: stat.nlink,
|
|
2123
|
+
size: stat.size,
|
|
2124
|
+
uid: stat.uid,
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/home-dir.js
|
|
2129
|
+
|
|
2130
|
+
|
|
2131
|
+
|
|
2132
|
+
function normalize(value) {
|
|
2133
|
+
const trimmed = normalizeOptionalString(value);
|
|
2134
|
+
if (!trimmed) {
|
|
2135
|
+
return undefined;
|
|
2136
|
+
}
|
|
2137
|
+
if (trimmed === "undefined" || trimmed === "null") {
|
|
2138
|
+
return undefined;
|
|
2139
|
+
}
|
|
2140
|
+
return trimmed;
|
|
2141
|
+
}
|
|
2142
|
+
function resolveEffectiveHomeDir(env = process.env, homedir = external_node_os_namespaceObject.homedir) {
|
|
2143
|
+
const raw = resolveRawHomeDir(env, homedir);
|
|
2144
|
+
return raw ? external_node_path_namespaceObject.resolve(raw) : undefined;
|
|
2145
|
+
}
|
|
2146
|
+
function resolveOsHomeDir(env = process.env, homedir = os.homedir) {
|
|
2147
|
+
const raw = resolveRawOsHomeDir(env, homedir);
|
|
2148
|
+
return raw ? path.resolve(raw) : undefined;
|
|
2149
|
+
}
|
|
2150
|
+
function resolveRawHomeDir(env, homedir) {
|
|
2151
|
+
const explicitHome = normalize(env.OPENCLAW_HOME);
|
|
2152
|
+
if (!explicitHome) {
|
|
2153
|
+
return resolveRawOsHomeDir(env, homedir);
|
|
2154
|
+
}
|
|
2155
|
+
const segments = external_node_path_namespaceObject.normalize(explicitHome).split(external_node_path_namespaceObject.sep);
|
|
2156
|
+
if (segments[0] !== "~") {
|
|
2157
|
+
return explicitHome;
|
|
2158
|
+
}
|
|
2159
|
+
// OPENCLAW_HOME starts with "~"; expand against the os home dir. Fall
|
|
2160
|
+
// back to undefined when there is no os home to expand against rather
|
|
2161
|
+
// than returning a raw "~"-prefixed path the caller cannot use.
|
|
2162
|
+
const fallbackHome = resolveRawOsHomeDir(env, homedir);
|
|
2163
|
+
if (!fallbackHome) {
|
|
2164
|
+
return undefined;
|
|
2165
|
+
}
|
|
2166
|
+
return expandHomePrefix(explicitHome, { home: fallbackHome });
|
|
2167
|
+
}
|
|
2168
|
+
function resolveRawOsHomeDir(env, homedir) {
|
|
2169
|
+
const envHome = normalize(env.HOME);
|
|
2170
|
+
if (envHome) {
|
|
2171
|
+
return envHome;
|
|
2172
|
+
}
|
|
2173
|
+
const userProfile = normalize(env.USERPROFILE);
|
|
2174
|
+
if (userProfile) {
|
|
2175
|
+
return userProfile;
|
|
2176
|
+
}
|
|
2177
|
+
return normalizeSafe(homedir);
|
|
2178
|
+
}
|
|
2179
|
+
function normalizeSafe(homedir) {
|
|
2180
|
+
try {
|
|
2181
|
+
return normalize(homedir());
|
|
2182
|
+
}
|
|
2183
|
+
catch {
|
|
2184
|
+
return undefined;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
function resolveRequiredHomeDir(env = process.env, homedir = os.homedir) {
|
|
2188
|
+
return resolveEffectiveHomeDir(env, homedir) ?? path.resolve(process.cwd());
|
|
2189
|
+
}
|
|
2190
|
+
function resolveRequiredOsHomeDir(env = process.env, homedir = os.homedir) {
|
|
2191
|
+
return resolveOsHomeDir(env, homedir) ?? path.resolve(process.cwd());
|
|
2192
|
+
}
|
|
2193
|
+
function expandHomePrefix(input, opts) {
|
|
2194
|
+
const segments = external_node_path_namespaceObject.normalize(input).split(external_node_path_namespaceObject.sep);
|
|
2195
|
+
if (segments[0] !== "~") {
|
|
2196
|
+
return input;
|
|
2197
|
+
}
|
|
2198
|
+
const home = normalize(opts?.home) ??
|
|
2199
|
+
resolveEffectiveHomeDir(opts?.env ?? process.env, opts?.homedir ?? external_node_os_namespaceObject.homedir);
|
|
2200
|
+
if (!home) {
|
|
2201
|
+
return input;
|
|
2202
|
+
}
|
|
2203
|
+
return external_node_path_namespaceObject.join(home, ...segments.slice(1));
|
|
2204
|
+
}
|
|
2205
|
+
function resolveHomeRelativePath(input, opts) {
|
|
2206
|
+
if (!input) {
|
|
2207
|
+
return input;
|
|
2208
|
+
}
|
|
2209
|
+
const segments = path.normalize(input).split(path.sep);
|
|
2210
|
+
if (segments[0] !== "~") {
|
|
2211
|
+
return path.resolve(input);
|
|
2212
|
+
}
|
|
2213
|
+
const expanded = expandHomePrefix(input, {
|
|
2214
|
+
home: resolveRequiredHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
|
|
2215
|
+
env: opts?.env,
|
|
2216
|
+
homedir: opts?.homedir,
|
|
2217
|
+
});
|
|
2218
|
+
return path.resolve(expanded);
|
|
2219
|
+
}
|
|
2220
|
+
function resolveUserPath(input, optsOrEnv, homedir) {
|
|
2221
|
+
const opts = optsOrEnv && ("env" in optsOrEnv || "homedir" in optsOrEnv)
|
|
2222
|
+
? optsOrEnv
|
|
2223
|
+
: { env: optsOrEnv, homedir };
|
|
2224
|
+
return resolveHomeRelativePath(input, opts);
|
|
2225
|
+
}
|
|
2226
|
+
function resolveOsHomeRelativePath(input, opts) {
|
|
2227
|
+
if (!input) {
|
|
2228
|
+
return input;
|
|
2229
|
+
}
|
|
2230
|
+
const segments = path.normalize(input).split(path.sep);
|
|
2231
|
+
if (segments[0] !== "~") {
|
|
2232
|
+
return path.resolve(input);
|
|
2233
|
+
}
|
|
2234
|
+
const expanded = expandHomePrefix(input, {
|
|
2235
|
+
home: resolveRequiredOsHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
|
|
2236
|
+
env: opts?.env,
|
|
2237
|
+
homedir: opts?.homedir,
|
|
2238
|
+
});
|
|
2239
|
+
return path.resolve(expanded);
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-context.js
|
|
2243
|
+
|
|
2244
|
+
|
|
2245
|
+
|
|
2246
|
+
|
|
2247
|
+
|
|
2248
|
+
|
|
2249
|
+
const ensureTrailingSep = (value) => value.endsWith(external_node_path_namespaceObject.sep) ? value : value + external_node_path_namespaceObject.sep;
|
|
2250
|
+
function assertValidRootRelativePath(relativePath) {
|
|
2251
|
+
path_assertNoNulPathInput(relativePath, "relative path contains a NUL byte");
|
|
2252
|
+
}
|
|
2253
|
+
let cachedHomePath;
|
|
2254
|
+
async function expandRelativePathWithHome(relativePath) {
|
|
2255
|
+
const rawHome = process.env.HOME || process.env.USERPROFILE || external_node_os_namespaceObject.homedir();
|
|
2256
|
+
if (cachedHomePath?.raw !== rawHome) {
|
|
2257
|
+
let realHome = rawHome;
|
|
2258
|
+
try {
|
|
2259
|
+
realHome = await promises_namespaceObject.realpath(rawHome);
|
|
2260
|
+
}
|
|
2261
|
+
catch {
|
|
2262
|
+
// If the home dir cannot be canonicalized, keep lexical expansion behavior.
|
|
2263
|
+
}
|
|
2264
|
+
cachedHomePath = { raw: rawHome, real: realHome };
|
|
2265
|
+
}
|
|
2266
|
+
return expandHomePrefix(relativePath, { home: cachedHomePath.real });
|
|
2267
|
+
}
|
|
2268
|
+
async function resolveRootContext(rootDir) {
|
|
2269
|
+
path_assertNoNulPathInput(rootDir, "root dir contains a NUL byte");
|
|
2270
|
+
let rootReal;
|
|
2271
|
+
try {
|
|
2272
|
+
rootReal = await promises_namespaceObject.realpath(rootDir);
|
|
2273
|
+
const rootStat = await promises_namespaceObject.stat(rootReal);
|
|
2274
|
+
if (!rootStat.isDirectory()) {
|
|
2275
|
+
throw new errors_FsSafeError("invalid-path", "root dir is not a directory");
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
catch (err) {
|
|
2279
|
+
if (err instanceof errors_FsSafeError) {
|
|
2280
|
+
throw err;
|
|
2281
|
+
}
|
|
2282
|
+
if (path_isNotFoundPathError(err)) {
|
|
2283
|
+
throw new errors_FsSafeError("not-found", "root dir not found");
|
|
2284
|
+
}
|
|
2285
|
+
throw err;
|
|
2286
|
+
}
|
|
2287
|
+
return {
|
|
2288
|
+
rootDir: external_node_path_namespaceObject.resolve(rootDir),
|
|
2289
|
+
rootReal,
|
|
2290
|
+
rootWithSep: ensureTrailingSep(rootReal),
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
async function resolvePathInRoot(root, relativePath) {
|
|
2294
|
+
assertValidRootRelativePath(relativePath);
|
|
2295
|
+
const expanded = await expandRelativePathWithHome(relativePath);
|
|
2296
|
+
const resolved = external_node_path_namespaceObject.resolve(root.rootWithSep, expanded);
|
|
2297
|
+
if (!path_isPathInside(root.rootWithSep, resolved)) {
|
|
2298
|
+
throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
|
|
2299
|
+
}
|
|
2300
|
+
return { rootReal: root.rootReal, rootWithSep: root.rootWithSep, resolved };
|
|
2301
|
+
}
|
|
2302
|
+
async function resolvePathWithinRoot(params) {
|
|
2303
|
+
return await resolvePathInRoot(await resolveRootContext(params.rootDir), params.relativePath);
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-errors.js
|
|
2307
|
+
|
|
2308
|
+
|
|
2309
|
+
function isAlreadyExistsError(error) {
|
|
2310
|
+
return hasNodeErrorCode(error, "EEXIST") || /File exists|EEXIST/i.test(String(error));
|
|
2311
|
+
}
|
|
2312
|
+
function normalizePinnedWriteError(error) {
|
|
2313
|
+
if (error instanceof errors_FsSafeError) {
|
|
2314
|
+
return error;
|
|
2315
|
+
}
|
|
2316
|
+
return new errors_FsSafeError("invalid-path", "path is not a regular file under root", {
|
|
2317
|
+
cause: error instanceof Error ? error : undefined,
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
function normalizePinnedPathError(error) {
|
|
2321
|
+
if (error instanceof errors_FsSafeError) {
|
|
2322
|
+
return error;
|
|
2323
|
+
}
|
|
2324
|
+
return new errors_FsSafeError("path-alias", "path is not under root", {
|
|
2325
|
+
cause: error instanceof Error ? error : undefined,
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/test-hooks.js
|
|
2330
|
+
let test_hooks_fsSafeTestHooks;
|
|
2331
|
+
function allowFsSafeTestHooks() {
|
|
2332
|
+
return false || process.env.VITEST === "true";
|
|
2333
|
+
}
|
|
2334
|
+
function getFsSafeTestHooks() {
|
|
2335
|
+
return test_hooks_fsSafeTestHooks;
|
|
2336
|
+
}
|
|
2337
|
+
function __setFsSafeTestHooksForTest(hooks) {
|
|
2338
|
+
if (hooks && !allowFsSafeTestHooks()) {
|
|
2339
|
+
throw new Error("__setFsSafeTestHooksForTest is only available in tests");
|
|
2340
|
+
}
|
|
2341
|
+
test_hooks_fsSafeTestHooks = hooks;
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/temp-cleanup.js
|
|
2345
|
+
|
|
2346
|
+
const tempCleanupEntries = new Map();
|
|
2347
|
+
let cleanupRegistered = false;
|
|
2348
|
+
function cleanupRegisteredTempPathsSync() {
|
|
2349
|
+
for (const entry of tempCleanupEntries.values()) {
|
|
2350
|
+
try {
|
|
2351
|
+
external_node_fs_namespaceObject.rmSync(entry.path, { force: true, recursive: entry.recursive });
|
|
2352
|
+
}
|
|
2353
|
+
catch {
|
|
2354
|
+
// Process-exit cleanup is best-effort.
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
tempCleanupEntries.clear();
|
|
2358
|
+
}
|
|
2359
|
+
function registerTempPathForExit(tempPath, options) {
|
|
2360
|
+
if (!cleanupRegistered) {
|
|
2361
|
+
cleanupRegistered = true;
|
|
2362
|
+
process.once("exit", cleanupRegisteredTempPathsSync);
|
|
2363
|
+
}
|
|
2364
|
+
tempCleanupEntries.set(tempPath, {
|
|
2365
|
+
path: tempPath,
|
|
2366
|
+
recursive: options?.recursive === true,
|
|
2367
|
+
});
|
|
2368
|
+
return () => {
|
|
2369
|
+
tempCleanupEntries.delete(tempPath);
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
function __cleanupRegisteredTempPathsForTest() {
|
|
2373
|
+
cleanupRegisteredTempPathsSync();
|
|
2374
|
+
}
|
|
2375
|
+
function __cleanupRegisteredTempPathForTest(tempPath) {
|
|
2376
|
+
const entry = tempCleanupEntries.get(tempPath);
|
|
2377
|
+
if (!entry) {
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
try {
|
|
2381
|
+
fsSync.rmSync(entry.path, { force: true, recursive: entry.recursive });
|
|
2382
|
+
}
|
|
2383
|
+
finally {
|
|
2384
|
+
tempCleanupEntries.delete(tempPath);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/write-queue.js
|
|
2389
|
+
const writeQueues = new Map();
|
|
2390
|
+
async function serializePathWrite(key, run) {
|
|
2391
|
+
const previous = writeQueues.get(key) ?? Promise.resolve();
|
|
2392
|
+
const task = (async () => {
|
|
2393
|
+
await previous.catch(() => undefined);
|
|
2394
|
+
return await run();
|
|
2395
|
+
})();
|
|
2396
|
+
const done = task.then(() => undefined, () => undefined);
|
|
2397
|
+
writeQueues.set(key, done);
|
|
2398
|
+
try {
|
|
2399
|
+
return await task;
|
|
2400
|
+
}
|
|
2401
|
+
finally {
|
|
2402
|
+
if (writeQueues.get(key) === done) {
|
|
2403
|
+
writeQueues.delete(key);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-impl.js
|
|
2409
|
+
|
|
2410
|
+
|
|
2411
|
+
|
|
2412
|
+
|
|
2413
|
+
|
|
2414
|
+
|
|
2415
|
+
|
|
2416
|
+
|
|
2417
|
+
|
|
2418
|
+
|
|
2419
|
+
|
|
2420
|
+
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
|
|
2424
|
+
|
|
2425
|
+
|
|
2426
|
+
|
|
2427
|
+
|
|
2428
|
+
|
|
2429
|
+
|
|
2430
|
+
|
|
2431
|
+
|
|
2432
|
+
|
|
2433
|
+
function logWarn(message) {
|
|
2434
|
+
if (process.env.FS_SAFE_DEBUG_WARNINGS === "1") {
|
|
2435
|
+
console.warn(message);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in external_node_fs_namespaceObject.constants;
|
|
2439
|
+
const NONBLOCK_OPEN_FLAG = "O_NONBLOCK" in external_node_fs_namespaceObject.constants ? external_node_fs_namespaceObject.constants.O_NONBLOCK : 0;
|
|
2440
|
+
const OPEN_READ_FLAGS = external_node_fs_namespaceObject.constants.O_RDONLY | (SUPPORTS_NOFOLLOW ? external_node_fs_namespaceObject.constants.O_NOFOLLOW : 0);
|
|
2441
|
+
const OPEN_READ_NONBLOCK_FLAGS = OPEN_READ_FLAGS | NONBLOCK_OPEN_FLAG;
|
|
2442
|
+
const OPEN_READ_FOLLOW_FLAGS = external_node_fs_namespaceObject.constants.O_RDONLY;
|
|
2443
|
+
const OPEN_READ_FOLLOW_NONBLOCK_FLAGS = OPEN_READ_FOLLOW_FLAGS | NONBLOCK_OPEN_FLAG;
|
|
2444
|
+
const OPEN_WRITE_EXISTING_FLAGS = external_node_fs_namespaceObject.constants.O_WRONLY | (SUPPORTS_NOFOLLOW ? external_node_fs_namespaceObject.constants.O_NOFOLLOW : 0);
|
|
2445
|
+
const OPEN_WRITE_CREATE_FLAGS = external_node_fs_namespaceObject.constants.O_WRONLY |
|
|
2446
|
+
external_node_fs_namespaceObject.constants.O_CREAT |
|
|
2447
|
+
external_node_fs_namespaceObject.constants.O_EXCL |
|
|
2448
|
+
(SUPPORTS_NOFOLLOW ? external_node_fs_namespaceObject.constants.O_NOFOLLOW : 0);
|
|
2449
|
+
const OPEN_APPEND_EXISTING_FLAGS = external_node_fs_namespaceObject.constants.O_RDWR | external_node_fs_namespaceObject.constants.O_APPEND | (SUPPORTS_NOFOLLOW ? external_node_fs_namespaceObject.constants.O_NOFOLLOW : 0);
|
|
2450
|
+
const OPEN_APPEND_CREATE_FLAGS = external_node_fs_namespaceObject.constants.O_RDWR |
|
|
2451
|
+
external_node_fs_namespaceObject.constants.O_APPEND |
|
|
2452
|
+
external_node_fs_namespaceObject.constants.O_CREAT |
|
|
2453
|
+
external_node_fs_namespaceObject.constants.O_EXCL |
|
|
2454
|
+
(SUPPORTS_NOFOLLOW ? external_node_fs_namespaceObject.constants.O_NOFOLLOW : 0);
|
|
2455
|
+
const DEFAULT_ROOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
2456
|
+
function closeHandleForDispose(handle) {
|
|
2457
|
+
return handle.close().catch(() => undefined);
|
|
2458
|
+
}
|
|
2459
|
+
function openResult(params) {
|
|
2460
|
+
return {
|
|
2461
|
+
handle: params.handle,
|
|
2462
|
+
realPath: params.realPath,
|
|
2463
|
+
stat: params.stat,
|
|
2464
|
+
[Symbol.asyncDispose]: async () => {
|
|
2465
|
+
await closeHandleForDispose(params.handle);
|
|
2466
|
+
},
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
async function openVerifiedLocalFile(filePath, options) {
|
|
2470
|
+
const fsSafeTestHooks = getFsSafeTestHooks();
|
|
2471
|
+
// Reject directories before opening so we never surface EISDIR to callers (e.g. tool
|
|
2472
|
+
// results that get sent to messaging channels). See openclaw/openclaw#31186.
|
|
2473
|
+
try {
|
|
2474
|
+
const preStat = await promises_namespaceObject.lstat(filePath);
|
|
2475
|
+
if (preStat.isDirectory()) {
|
|
2476
|
+
throw new errors_FsSafeError("not-file", "not a file");
|
|
2477
|
+
}
|
|
2478
|
+
await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
|
|
2479
|
+
}
|
|
2480
|
+
catch (err) {
|
|
2481
|
+
if (err instanceof errors_FsSafeError) {
|
|
2482
|
+
throw err;
|
|
2483
|
+
}
|
|
2484
|
+
// ENOENT and other lstat errors: fall through and let fs.open handle.
|
|
2485
|
+
}
|
|
2486
|
+
let handle;
|
|
2487
|
+
try {
|
|
2488
|
+
const openFlags = options?.symlinks === "follow-within-root"
|
|
2489
|
+
? options?.nonBlockingRead
|
|
2490
|
+
? OPEN_READ_FOLLOW_NONBLOCK_FLAGS
|
|
2491
|
+
: OPEN_READ_FOLLOW_FLAGS
|
|
2492
|
+
: options?.nonBlockingRead
|
|
2493
|
+
? OPEN_READ_NONBLOCK_FLAGS
|
|
2494
|
+
: OPEN_READ_FLAGS;
|
|
2495
|
+
await fsSafeTestHooks?.beforeOpen?.(filePath, openFlags);
|
|
2496
|
+
handle = await promises_namespaceObject.open(filePath, openFlags);
|
|
2497
|
+
try {
|
|
2498
|
+
await fsSafeTestHooks?.afterOpen?.(filePath, handle);
|
|
2499
|
+
}
|
|
2500
|
+
catch (err) {
|
|
2501
|
+
await handle.close().catch(() => { });
|
|
2502
|
+
throw err;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
catch (err) {
|
|
2506
|
+
if (path_isNotFoundPathError(err)) {
|
|
2507
|
+
throw new errors_FsSafeError("not-found", "file not found");
|
|
2508
|
+
}
|
|
2509
|
+
if (isSymlinkOpenError(err)) {
|
|
2510
|
+
throw new errors_FsSafeError("symlink", "symlink open blocked", { cause: err });
|
|
2511
|
+
}
|
|
2512
|
+
// Defensive: if open still throws EISDIR (e.g. race), sanitize so it never leaks.
|
|
2513
|
+
if (hasNodeErrorCode(err, "EISDIR")) {
|
|
2514
|
+
throw new errors_FsSafeError("not-file", "not a file");
|
|
2515
|
+
}
|
|
2516
|
+
throw err;
|
|
2517
|
+
}
|
|
2518
|
+
try {
|
|
2519
|
+
const stat = await handle.stat();
|
|
2520
|
+
if (!stat.isFile()) {
|
|
2521
|
+
throw new errors_FsSafeError("not-file", "not a file");
|
|
2522
|
+
}
|
|
2523
|
+
if (options?.hardlinks === "reject" && stat.nlink > 1) {
|
|
2524
|
+
throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
|
|
2525
|
+
}
|
|
2526
|
+
if (options?.symlinks === "follow-within-root") {
|
|
2527
|
+
const pathStat = await promises_namespaceObject.stat(filePath);
|
|
2528
|
+
if (!file_identity_sameFileIdentity(stat, pathStat)) {
|
|
2529
|
+
throw new errors_FsSafeError("path-mismatch", "path changed during read");
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
else {
|
|
2533
|
+
const pathStat = await promises_namespaceObject.lstat(filePath);
|
|
2534
|
+
if (pathStat.isSymbolicLink()) {
|
|
2535
|
+
throw new errors_FsSafeError("symlink", "symlink not allowed");
|
|
2536
|
+
}
|
|
2537
|
+
if (!file_identity_sameFileIdentity(stat, pathStat)) {
|
|
2538
|
+
throw new errors_FsSafeError("path-mismatch", "path changed during read");
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
const realPath = await resolveOpenedFileRealPathForHandle(handle, filePath);
|
|
2542
|
+
const realStat = await promises_namespaceObject.stat(realPath);
|
|
2543
|
+
if (options?.hardlinks === "reject" && realStat.nlink > 1) {
|
|
2544
|
+
throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
|
|
2545
|
+
}
|
|
2546
|
+
if (!file_identity_sameFileIdentity(stat, realStat)) {
|
|
2547
|
+
throw new errors_FsSafeError("path-mismatch", "path mismatch");
|
|
2548
|
+
}
|
|
2549
|
+
return openResult({ handle, realPath, stat });
|
|
2550
|
+
}
|
|
2551
|
+
catch (err) {
|
|
2552
|
+
await handle.close().catch(() => { });
|
|
2553
|
+
if (err instanceof errors_FsSafeError) {
|
|
2554
|
+
throw err;
|
|
2555
|
+
}
|
|
2556
|
+
if (path_isNotFoundPathError(err)) {
|
|
2557
|
+
throw new errors_FsSafeError("not-found", "file not found");
|
|
2558
|
+
}
|
|
2559
|
+
throw err;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
class RootHandle {
|
|
2563
|
+
rootDir;
|
|
2564
|
+
rootReal;
|
|
2565
|
+
rootWithSep;
|
|
2566
|
+
defaults;
|
|
2567
|
+
constructor(context, defaults = {}) {
|
|
2568
|
+
this.rootDir = context.rootDir;
|
|
2569
|
+
this.rootReal = context.rootReal;
|
|
2570
|
+
this.rootWithSep = context.rootWithSep;
|
|
2571
|
+
this.defaults = defaults;
|
|
2572
|
+
}
|
|
2573
|
+
get context() {
|
|
2574
|
+
return {
|
|
2575
|
+
rootDir: this.rootDir,
|
|
2576
|
+
rootReal: this.rootReal,
|
|
2577
|
+
rootWithSep: this.rootWithSep,
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
async resolve(relativePath) {
|
|
2581
|
+
return (await resolvePathInRoot(this.context, relativePath)).resolved;
|
|
2582
|
+
}
|
|
2583
|
+
async open(relativePath, options = {}) {
|
|
2584
|
+
return await openFileInRoot(this.context, {
|
|
2585
|
+
relativePath,
|
|
2586
|
+
...readDefaults(this.defaults),
|
|
2587
|
+
...options,
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
async read(relativePath, options = {}) {
|
|
2591
|
+
return await readFileInRoot(this.context, {
|
|
2592
|
+
relativePath,
|
|
2593
|
+
...readDefaults(this.defaults),
|
|
2594
|
+
...options,
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
async readBytes(relativePath, options = {}) {
|
|
2598
|
+
return (await this.read(relativePath, options)).buffer;
|
|
2599
|
+
}
|
|
2600
|
+
async readText(relativePath, options = {}) {
|
|
2601
|
+
const { encoding = "utf8", ...readOptions } = options;
|
|
2602
|
+
return (await this.read(relativePath, readOptions)).buffer.toString(encoding);
|
|
2603
|
+
}
|
|
2604
|
+
async readJson(relativePath, options = {}) {
|
|
2605
|
+
return JSON.parse(await this.readText(relativePath, options));
|
|
2606
|
+
}
|
|
2607
|
+
async readAbsolute(filePath, options = {}) {
|
|
2608
|
+
return await readPathInRoot(this.context, {
|
|
2609
|
+
filePath,
|
|
2610
|
+
...readDefaults(this.defaults),
|
|
2611
|
+
...options,
|
|
2612
|
+
});
|
|
2613
|
+
}
|
|
2614
|
+
reader(options = {}) {
|
|
2615
|
+
return async (filePath) => {
|
|
2616
|
+
return (await this.readAbsolute(filePath, options)).buffer;
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
async openWritable(relativePath, options = {}) {
|
|
2620
|
+
const writeMode = options.writeMode ?? "replace";
|
|
2621
|
+
return await openWritableFileInRoot(this.context, {
|
|
2622
|
+
relativePath,
|
|
2623
|
+
mkdir: this.defaults.mkdir,
|
|
2624
|
+
mode: this.defaults.mode,
|
|
2625
|
+
...options,
|
|
2626
|
+
append: writeMode === "append",
|
|
2627
|
+
truncateExisting: writeMode === "replace",
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
async append(relativePath, data, options = {}) {
|
|
2631
|
+
await appendFileInRoot(this.context, {
|
|
2632
|
+
relativePath,
|
|
2633
|
+
data,
|
|
2634
|
+
mkdir: this.defaults.mkdir,
|
|
2635
|
+
mode: this.defaults.mode,
|
|
2636
|
+
...options,
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
async remove(relativePath) {
|
|
2640
|
+
assertValidRootRelativePath(relativePath);
|
|
2641
|
+
await removePathInRoot(this.context, relativePath);
|
|
2642
|
+
}
|
|
2643
|
+
async mkdir(relativePath) {
|
|
2644
|
+
assertValidRootRelativePath(relativePath);
|
|
2645
|
+
await mkdirPathInRoot(this.context, { relativePath });
|
|
2646
|
+
}
|
|
2647
|
+
async ensureRoot() {
|
|
2648
|
+
await mkdirPathInRoot(this.context, { relativePath: "", allowRoot: true });
|
|
2649
|
+
}
|
|
2650
|
+
async write(relativePath, data, options = {}) {
|
|
2651
|
+
await writeFileInRoot(this.context, {
|
|
2652
|
+
relativePath,
|
|
2653
|
+
data,
|
|
2654
|
+
mkdir: this.defaults.mkdir,
|
|
2655
|
+
mode: this.defaults.mode,
|
|
2656
|
+
...options,
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
async create(relativePath, data, options = {}) {
|
|
2660
|
+
await writeFileInRoot(this.context, {
|
|
2661
|
+
relativePath,
|
|
2662
|
+
data,
|
|
2663
|
+
mkdir: this.defaults.mkdir,
|
|
2664
|
+
mode: this.defaults.mode,
|
|
2665
|
+
...options,
|
|
2666
|
+
overwrite: false,
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
async writeJson(relativePath, data, options = {}) {
|
|
2670
|
+
const { replacer, space, trailingNewline = true, ...writeOptions } = options;
|
|
2671
|
+
const json = JSON.stringify(data, replacer, space);
|
|
2672
|
+
await this.write(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
|
|
2673
|
+
}
|
|
2674
|
+
async createJson(relativePath, data, options = {}) {
|
|
2675
|
+
const { replacer, space, trailingNewline = true, ...writeOptions } = options;
|
|
2676
|
+
const json = JSON.stringify(data, replacer, space);
|
|
2677
|
+
await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
|
|
2678
|
+
}
|
|
2679
|
+
async copyIn(relativePath, sourcePath, options = {}) {
|
|
2680
|
+
assertValidRootRelativePath(relativePath);
|
|
2681
|
+
await copyFileInRoot(this.context, {
|
|
2682
|
+
sourcePath,
|
|
2683
|
+
relativePath,
|
|
2684
|
+
maxBytes: this.defaults.maxBytes,
|
|
2685
|
+
mkdir: this.defaults.mkdir,
|
|
2686
|
+
mode: this.defaults.mode,
|
|
2687
|
+
...options,
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
async exists(relativePath) {
|
|
2691
|
+
try {
|
|
2692
|
+
await this.stat(relativePath);
|
|
2693
|
+
return true;
|
|
2694
|
+
}
|
|
2695
|
+
catch (err) {
|
|
2696
|
+
if (err instanceof errors_FsSafeError && err.code === "not-found") {
|
|
2697
|
+
return false;
|
|
2698
|
+
}
|
|
2699
|
+
throw err;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
async stat(relativePath) {
|
|
2703
|
+
assertValidRootRelativePath(relativePath);
|
|
2704
|
+
try {
|
|
2705
|
+
return await helperStat(this.rootReal, relativePath);
|
|
2706
|
+
}
|
|
2707
|
+
catch (error) {
|
|
2708
|
+
if (canFallbackFromPythonError(error)) {
|
|
2709
|
+
return await statPathFallback(this.context, relativePath);
|
|
2710
|
+
}
|
|
2711
|
+
throw error;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
async list(relativePath, options = {}) {
|
|
2715
|
+
assertValidRootRelativePath(relativePath);
|
|
2716
|
+
try {
|
|
2717
|
+
return options.withFileTypes === true
|
|
2718
|
+
? await helperReaddir(this.rootReal, relativePath, true)
|
|
2719
|
+
: await helperReaddir(this.rootReal, relativePath, false);
|
|
2720
|
+
}
|
|
2721
|
+
catch (error) {
|
|
2722
|
+
if (canFallbackFromPythonError(error)) {
|
|
2723
|
+
return await listPathFallback(this.context, relativePath, options.withFileTypes === true);
|
|
2724
|
+
}
|
|
2725
|
+
throw error;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
async move(fromRelative, toRelative, options = {}) {
|
|
2729
|
+
assertValidRootRelativePath(fromRelative);
|
|
2730
|
+
assertValidRootRelativePath(toRelative);
|
|
2731
|
+
try {
|
|
2732
|
+
await runPinnedHelper("rename", this.rootReal, {
|
|
2733
|
+
from: fromRelative,
|
|
2734
|
+
overwrite: options.overwrite ?? false,
|
|
2735
|
+
to: toRelative,
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
catch (error) {
|
|
2739
|
+
if (canFallbackFromPythonError(error)) {
|
|
2740
|
+
await movePathFallback(this.context, {
|
|
2741
|
+
fromRelative,
|
|
2742
|
+
overwrite: options.overwrite ?? false,
|
|
2743
|
+
toRelative,
|
|
2744
|
+
});
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
throw error;
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
function readDefaults(defaults) {
|
|
2752
|
+
return {
|
|
2753
|
+
hardlinks: defaults.hardlinks,
|
|
2754
|
+
maxBytes: defaults.maxBytes ?? DEFAULT_ROOT_MAX_BYTES,
|
|
2755
|
+
nonBlockingRead: defaults.nonBlockingRead,
|
|
2756
|
+
symlinks: defaults.symlinks,
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
async function root_impl_root(rootDir, defaults = {}) {
|
|
2760
|
+
return new RootHandle(await resolveRootContext(rootDir), defaults);
|
|
2761
|
+
}
|
|
2762
|
+
async function openFileInRoot(root, params) {
|
|
2763
|
+
const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath);
|
|
2764
|
+
let opened;
|
|
2765
|
+
try {
|
|
2766
|
+
opened = await openVerifiedLocalFile(resolved, {
|
|
2767
|
+
nonBlockingRead: params.nonBlockingRead,
|
|
2768
|
+
symlinks: params.symlinks,
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
catch (err) {
|
|
2772
|
+
if (err instanceof errors_FsSafeError) {
|
|
2773
|
+
throw err;
|
|
2774
|
+
}
|
|
2775
|
+
throw err;
|
|
2776
|
+
}
|
|
2777
|
+
if (params.hardlinks !== "allow" && opened.stat.nlink > 1) {
|
|
2778
|
+
await opened.handle.close().catch(() => { });
|
|
2779
|
+
throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
|
|
2780
|
+
}
|
|
2781
|
+
if (!path_isPathInside(rootWithSep, opened.realPath)) {
|
|
2782
|
+
await opened.handle.close().catch(() => { });
|
|
2783
|
+
throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
|
|
2784
|
+
}
|
|
2785
|
+
return opened;
|
|
2786
|
+
}
|
|
2787
|
+
async function readFileInRoot(root, params) {
|
|
2788
|
+
const opened = await openFileInRoot(root, params);
|
|
2789
|
+
try {
|
|
2790
|
+
return await readOpenedFileSafely({ opened, maxBytes: params.maxBytes });
|
|
2791
|
+
}
|
|
2792
|
+
finally {
|
|
2793
|
+
await opened.handle.close().catch(() => { });
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
async function readPathInRoot(root, params) {
|
|
2797
|
+
const rootDir = root.rootDir;
|
|
2798
|
+
const candidatePath = external_node_path_namespaceObject.isAbsolute(params.filePath)
|
|
2799
|
+
? external_node_path_namespaceObject.resolve(params.filePath)
|
|
2800
|
+
: external_node_path_namespaceObject.resolve(rootDir, params.filePath);
|
|
2801
|
+
const relativePath = external_node_path_namespaceObject.relative(rootDir, candidatePath);
|
|
2802
|
+
return await readFileInRoot(root, {
|
|
2803
|
+
relativePath,
|
|
2804
|
+
hardlinks: params.hardlinks,
|
|
2805
|
+
maxBytes: params.maxBytes,
|
|
2806
|
+
nonBlockingRead: params.nonBlockingRead,
|
|
2807
|
+
symlinks: params.symlinks,
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
async function readLocalFileSafely(params) {
|
|
2811
|
+
const opened = await openLocalFileSafely({ filePath: params.filePath });
|
|
2812
|
+
try {
|
|
2813
|
+
return await readOpenedFileSafely({ opened, maxBytes: params.maxBytes });
|
|
2814
|
+
}
|
|
2815
|
+
finally {
|
|
2816
|
+
await opened.handle.close().catch(() => { });
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
async function openLocalFileSafely(params) {
|
|
2820
|
+
assertNoNulPathInput(params.filePath, "file path contains a NUL byte");
|
|
2821
|
+
return await openVerifiedLocalFile(params.filePath);
|
|
2822
|
+
}
|
|
2823
|
+
async function readOpenedFileSafely(params) {
|
|
2824
|
+
if (params.maxBytes !== undefined && params.opened.stat.size > params.maxBytes) {
|
|
2825
|
+
throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${params.opened.stat.size})`);
|
|
2826
|
+
}
|
|
2827
|
+
const buffer = await params.opened.handle.readFile();
|
|
2828
|
+
if (params.maxBytes !== undefined && buffer.byteLength > params.maxBytes) {
|
|
2829
|
+
throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${buffer.byteLength})`);
|
|
2830
|
+
}
|
|
2831
|
+
return {
|
|
2832
|
+
buffer,
|
|
2833
|
+
realPath: params.opened.realPath,
|
|
2834
|
+
stat: params.opened.stat,
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
function emitWriteBoundaryWarning(reason) {
|
|
2838
|
+
logWarn(`security: fs-safe write boundary warning (${reason})`);
|
|
2839
|
+
}
|
|
2840
|
+
function buildAtomicWriteTempPath(targetPath) {
|
|
2841
|
+
const dir = external_node_path_namespaceObject.dirname(targetPath);
|
|
2842
|
+
const base = external_node_path_namespaceObject.basename(targetPath);
|
|
2843
|
+
return external_node_path_namespaceObject.join(dir, `.${base}.${process.pid}.${(0,external_node_crypto_namespaceObject.randomUUID)()}.tmp`);
|
|
2844
|
+
}
|
|
2845
|
+
function rootWriteQueueKey(root, relativePath) {
|
|
2846
|
+
return `${root.rootReal}\0${relativePath}`;
|
|
2847
|
+
}
|
|
2848
|
+
async function writeTempFileForAtomicReplace(params) {
|
|
2849
|
+
const tempHandle = await promises_namespaceObject.open(params.tempPath, OPEN_WRITE_CREATE_FLAGS, params.mode);
|
|
2850
|
+
try {
|
|
2851
|
+
if (typeof params.data === "string") {
|
|
2852
|
+
await tempHandle.writeFile(params.data, params.encoding ?? "utf8");
|
|
2853
|
+
}
|
|
2854
|
+
else {
|
|
2855
|
+
await tempHandle.writeFile(params.data);
|
|
2856
|
+
}
|
|
2857
|
+
return await tempHandle.stat();
|
|
2858
|
+
}
|
|
2859
|
+
finally {
|
|
2860
|
+
await tempHandle.close().catch(() => { });
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
async function verifyAtomicWriteResult(params) {
|
|
2864
|
+
const opened = await openVerifiedLocalFile(params.targetPath, { hardlinks: "reject" });
|
|
2865
|
+
try {
|
|
2866
|
+
if (!file_identity_sameFileIdentity(opened.stat, params.expectedIdentity)) {
|
|
2867
|
+
throw new errors_FsSafeError("path-mismatch", "path changed during write");
|
|
2868
|
+
}
|
|
2869
|
+
if (!path_isPathInside(params.root.rootWithSep, opened.realPath)) {
|
|
2870
|
+
throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
finally {
|
|
2874
|
+
await opened.handle.close().catch(() => { });
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
async function resolveOpenedFileRealPathForHandle(handle, ioPath) {
|
|
2878
|
+
const handleStat = await handle.stat();
|
|
2879
|
+
const fdCandidates = process.platform === "linux"
|
|
2880
|
+
? [`/proc/self/fd/${handle.fd}`, `/dev/fd/${handle.fd}`]
|
|
2881
|
+
: process.platform === "win32"
|
|
2882
|
+
? []
|
|
2883
|
+
: [`/dev/fd/${handle.fd}`];
|
|
2884
|
+
for (const fdPath of fdCandidates) {
|
|
2885
|
+
try {
|
|
2886
|
+
const fdRealPath = await promises_namespaceObject.realpath(fdPath);
|
|
2887
|
+
const fdRealStat = await promises_namespaceObject.stat(fdRealPath);
|
|
2888
|
+
if (file_identity_sameFileIdentity(handleStat, fdRealStat)) {
|
|
2889
|
+
return fdRealPath;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
catch {
|
|
2893
|
+
// try next fd path
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
try {
|
|
2897
|
+
const ioRealPath = await promises_namespaceObject.realpath(ioPath);
|
|
2898
|
+
const ioRealStat = await promises_namespaceObject.stat(ioRealPath);
|
|
2899
|
+
if (file_identity_sameFileIdentity(handleStat, ioRealStat)) {
|
|
2900
|
+
return ioRealPath;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
catch (err) {
|
|
2904
|
+
if (!path_isNotFoundPathError(err)) {
|
|
2905
|
+
throw err;
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
const parentResolved = await resolveOpenedFileRealPathFromParent(handleStat, ioPath);
|
|
2909
|
+
if (parentResolved) {
|
|
2910
|
+
return parentResolved;
|
|
2911
|
+
}
|
|
2912
|
+
throw new errors_FsSafeError("path-mismatch", "unable to resolve opened file path");
|
|
2913
|
+
}
|
|
2914
|
+
async function resolveOpenedFileRealPathFromParent(handleStat, ioPath) {
|
|
2915
|
+
let parentReal;
|
|
2916
|
+
try {
|
|
2917
|
+
parentReal = await promises_namespaceObject.realpath(external_node_path_namespaceObject.dirname(ioPath));
|
|
2918
|
+
}
|
|
2919
|
+
catch (err) {
|
|
2920
|
+
if (path_isNotFoundPathError(err)) {
|
|
2921
|
+
return null;
|
|
2922
|
+
}
|
|
2923
|
+
throw err;
|
|
2924
|
+
}
|
|
2925
|
+
let entries;
|
|
2926
|
+
try {
|
|
2927
|
+
entries = await promises_namespaceObject.readdir(parentReal);
|
|
2928
|
+
}
|
|
2929
|
+
catch (err) {
|
|
2930
|
+
if (path_isNotFoundPathError(err)) {
|
|
2931
|
+
return null;
|
|
2932
|
+
}
|
|
2933
|
+
throw err;
|
|
2934
|
+
}
|
|
2935
|
+
for (const entry of entries.toSorted()) {
|
|
2936
|
+
const candidatePath = external_node_path_namespaceObject.join(parentReal, entry);
|
|
2937
|
+
try {
|
|
2938
|
+
const candidateStat = await promises_namespaceObject.lstat(candidatePath);
|
|
2939
|
+
if (candidateStat.isFile() && file_identity_sameFileIdentity(handleStat, candidateStat)) {
|
|
2940
|
+
return await promises_namespaceObject.realpath(candidatePath);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
catch (err) {
|
|
2944
|
+
if (!path_isNotFoundPathError(err)) {
|
|
2945
|
+
throw err;
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
return null;
|
|
2950
|
+
}
|
|
2951
|
+
async function openWritableFileInRoot(root, params) {
|
|
2952
|
+
const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath);
|
|
2953
|
+
try {
|
|
2954
|
+
await assertNoPathAliasEscape({
|
|
2955
|
+
absolutePath: resolved,
|
|
2956
|
+
rootPath: rootReal,
|
|
2957
|
+
boundaryLabel: "root",
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
catch (err) {
|
|
2961
|
+
throw new errors_FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
2962
|
+
}
|
|
2963
|
+
if (params.mkdir !== false) {
|
|
2964
|
+
const parentGuard = await directory_guard_createNearestExistingDirectoryGuard(rootReal, external_node_path_namespaceObject.dirname(resolved));
|
|
2965
|
+
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
2966
|
+
await promises_namespaceObject.mkdir(external_node_path_namespaceObject.dirname(resolved), { recursive: true });
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
let ioPath = resolved;
|
|
2970
|
+
try {
|
|
2971
|
+
const resolvedRealPath = await promises_namespaceObject.realpath(resolved);
|
|
2972
|
+
if (!path_isPathInside(rootWithSep, resolvedRealPath)) {
|
|
2973
|
+
throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
|
|
2974
|
+
}
|
|
2975
|
+
ioPath = resolvedRealPath;
|
|
2976
|
+
}
|
|
2977
|
+
catch (err) {
|
|
2978
|
+
if (err instanceof errors_FsSafeError) {
|
|
2979
|
+
throw err;
|
|
2980
|
+
}
|
|
2981
|
+
if (!path_isNotFoundPathError(err)) {
|
|
2982
|
+
throw err;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
const mode = params.mode ?? 0o600;
|
|
2986
|
+
let handle;
|
|
2987
|
+
let createdForWrite = false;
|
|
2988
|
+
const existingFlags = params.append ? OPEN_APPEND_EXISTING_FLAGS : OPEN_WRITE_EXISTING_FLAGS;
|
|
2989
|
+
const createFlags = params.append ? OPEN_APPEND_CREATE_FLAGS : OPEN_WRITE_CREATE_FLAGS;
|
|
2990
|
+
try {
|
|
2991
|
+
try {
|
|
2992
|
+
handle = await promises_namespaceObject.open(ioPath, existingFlags, mode);
|
|
2993
|
+
}
|
|
2994
|
+
catch (err) {
|
|
2995
|
+
if (!path_isNotFoundPathError(err)) {
|
|
2996
|
+
throw err;
|
|
2997
|
+
}
|
|
2998
|
+
handle = await promises_namespaceObject.open(ioPath, createFlags, mode);
|
|
2999
|
+
createdForWrite = true;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
catch (err) {
|
|
3003
|
+
if (path_isNotFoundPathError(err)) {
|
|
3004
|
+
throw new errors_FsSafeError("not-found", "file not found");
|
|
3005
|
+
}
|
|
3006
|
+
if (isSymlinkOpenError(err)) {
|
|
3007
|
+
throw new errors_FsSafeError("symlink", "symlink open blocked", { cause: err });
|
|
3008
|
+
}
|
|
3009
|
+
if (hasNodeErrorCode(err, "EISDIR")) {
|
|
3010
|
+
throw new errors_FsSafeError("not-file", "not a file", { cause: err });
|
|
3011
|
+
}
|
|
3012
|
+
throw err;
|
|
3013
|
+
}
|
|
3014
|
+
let realPathForCleanup = null;
|
|
3015
|
+
try {
|
|
3016
|
+
const stat = await handle.stat();
|
|
3017
|
+
if (!stat.isFile()) {
|
|
3018
|
+
throw new errors_FsSafeError("invalid-path", "path is not a regular file under root");
|
|
3019
|
+
}
|
|
3020
|
+
if (stat.nlink > 1) {
|
|
3021
|
+
throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
|
|
3022
|
+
}
|
|
3023
|
+
try {
|
|
3024
|
+
const lstat = await promises_namespaceObject.lstat(ioPath);
|
|
3025
|
+
if (lstat.isSymbolicLink() || !lstat.isFile()) {
|
|
3026
|
+
throw new errors_FsSafeError(lstat.isSymbolicLink() ? "symlink" : "not-file", "path is not a regular file under root");
|
|
3027
|
+
}
|
|
3028
|
+
if (!file_identity_sameFileIdentity(stat, lstat)) {
|
|
3029
|
+
throw new errors_FsSafeError("path-mismatch", "path changed during write");
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
catch (err) {
|
|
3033
|
+
if (!path_isNotFoundPathError(err)) {
|
|
3034
|
+
throw err;
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
const realPath = await resolveOpenedFileRealPathForHandle(handle, ioPath);
|
|
3038
|
+
realPathForCleanup = realPath;
|
|
3039
|
+
const realStat = await promises_namespaceObject.stat(realPath);
|
|
3040
|
+
if (!file_identity_sameFileIdentity(stat, realStat)) {
|
|
3041
|
+
throw new errors_FsSafeError("path-mismatch", "path mismatch");
|
|
3042
|
+
}
|
|
3043
|
+
if (realStat.nlink > 1) {
|
|
3044
|
+
throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
|
|
3045
|
+
}
|
|
3046
|
+
if (!path_isPathInside(rootWithSep, realPath)) {
|
|
3047
|
+
throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
|
|
3048
|
+
}
|
|
3049
|
+
// Truncate only after boundary and identity checks complete. This avoids
|
|
3050
|
+
// irreversible side effects if a symlink target changes before validation.
|
|
3051
|
+
if (params.append !== true && params.truncateExisting !== false && !createdForWrite) {
|
|
3052
|
+
await handle.truncate(0);
|
|
3053
|
+
}
|
|
3054
|
+
return {
|
|
3055
|
+
handle,
|
|
3056
|
+
createdForWrite,
|
|
3057
|
+
realPath,
|
|
3058
|
+
stat,
|
|
3059
|
+
[Symbol.asyncDispose]: async () => {
|
|
3060
|
+
await closeHandleForDispose(handle);
|
|
3061
|
+
},
|
|
3062
|
+
};
|
|
3063
|
+
}
|
|
3064
|
+
catch (err) {
|
|
3065
|
+
const cleanupCreatedPath = createdForWrite && err instanceof errors_FsSafeError;
|
|
3066
|
+
const cleanupPath = realPathForCleanup ?? ioPath;
|
|
3067
|
+
await handle.close().catch(() => { });
|
|
3068
|
+
if (cleanupCreatedPath) {
|
|
3069
|
+
await promises_namespaceObject.rm(cleanupPath, { force: true }).catch(() => { });
|
|
3070
|
+
}
|
|
3071
|
+
throw err;
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
async function appendFileInRoot(root, params) {
|
|
3075
|
+
const target = await openWritableFileInRoot(root, {
|
|
3076
|
+
relativePath: params.relativePath,
|
|
3077
|
+
mkdir: params.mkdir,
|
|
3078
|
+
mode: params.mode,
|
|
3079
|
+
truncateExisting: false,
|
|
3080
|
+
append: true,
|
|
3081
|
+
});
|
|
3082
|
+
try {
|
|
3083
|
+
let prefix = "";
|
|
3084
|
+
if (params.prependNewlineIfNeeded === true &&
|
|
3085
|
+
!target.createdForWrite &&
|
|
3086
|
+
target.stat.size > 0 &&
|
|
3087
|
+
((typeof params.data === "string" && !params.data.startsWith("\n")) ||
|
|
3088
|
+
(Buffer.isBuffer(params.data) && params.data.length > 0 && params.data[0] !== 0x0a))) {
|
|
3089
|
+
const lastByte = Buffer.alloc(1);
|
|
3090
|
+
const { bytesRead } = await target.handle.read(lastByte, 0, 1, target.stat.size - 1);
|
|
3091
|
+
if (bytesRead === 1 && lastByte[0] !== 0x0a) {
|
|
3092
|
+
prefix = "\n";
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
if (typeof params.data === "string") {
|
|
3096
|
+
await target.handle.appendFile(`${prefix}${params.data}`, params.encoding ?? "utf8");
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
const payload = prefix.length > 0 ? Buffer.concat([Buffer.from(prefix, "utf8"), params.data]) : params.data;
|
|
3100
|
+
await target.handle.appendFile(payload);
|
|
3101
|
+
}
|
|
3102
|
+
finally {
|
|
3103
|
+
await target.handle.close().catch(() => { });
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
async function removePathInRoot(root, relativePath) {
|
|
3107
|
+
const resolved = await resolvePinnedRemovePathInRoot(root, relativePath);
|
|
3108
|
+
if (process.platform === "win32") {
|
|
3109
|
+
await removePathFallback(resolved);
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
try {
|
|
3113
|
+
await runPinnedPathHelper({
|
|
3114
|
+
operation: "remove",
|
|
3115
|
+
rootPath: resolved.rootReal,
|
|
3116
|
+
relativePath: resolved.relativePosix,
|
|
3117
|
+
});
|
|
3118
|
+
}
|
|
3119
|
+
catch (error) {
|
|
3120
|
+
if (isPinnedPathHelperSpawnError(error)) {
|
|
3121
|
+
await removePathFallback(resolved);
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
throw normalizePinnedPathError(error);
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
async function mkdirPathInRoot(root, params) {
|
|
3128
|
+
const resolved = await resolvePinnedPathInRoot(root, params);
|
|
3129
|
+
if (process.platform === "win32") {
|
|
3130
|
+
await mkdirPathFallback(resolved);
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
try {
|
|
3134
|
+
await runPinnedPathHelper({
|
|
3135
|
+
operation: "mkdirp",
|
|
3136
|
+
rootPath: resolved.rootReal,
|
|
3137
|
+
relativePath: resolved.relativePosix,
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
catch (error) {
|
|
3141
|
+
if (isPinnedPathHelperSpawnError(error)) {
|
|
3142
|
+
await mkdirPathFallback(resolved);
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
throw normalizePinnedPathError(error);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
async function writeFileInRoot(root, params) {
|
|
3149
|
+
if (process.platform === "win32") {
|
|
3150
|
+
await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
|
|
3151
|
+
await writeFileFallback(root, params);
|
|
3152
|
+
});
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode);
|
|
3156
|
+
await serializePathWrite(pinned.targetPath, async () => {
|
|
3157
|
+
let identity;
|
|
3158
|
+
try {
|
|
3159
|
+
identity = await runPinnedWriteHelper({
|
|
3160
|
+
rootPath: pinned.rootReal,
|
|
3161
|
+
relativeParentPath: pinned.relativeParentPath,
|
|
3162
|
+
basename: pinned.basename,
|
|
3163
|
+
mkdir: params.mkdir !== false,
|
|
3164
|
+
mode: params.mode ?? pinned.mode,
|
|
3165
|
+
overwrite: params.overwrite,
|
|
3166
|
+
input: {
|
|
3167
|
+
kind: "buffer",
|
|
3168
|
+
data: params.data,
|
|
3169
|
+
encoding: params.encoding,
|
|
3170
|
+
},
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
catch (error) {
|
|
3174
|
+
if (params.overwrite === false && isAlreadyExistsError(error)) {
|
|
3175
|
+
throw new errors_FsSafeError("already-exists", "file already exists", {
|
|
3176
|
+
cause: error instanceof Error ? error : undefined,
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
throw normalizePinnedWriteError(error);
|
|
3180
|
+
}
|
|
3181
|
+
try {
|
|
3182
|
+
await verifyAtomicWriteResult({
|
|
3183
|
+
root,
|
|
3184
|
+
targetPath: pinned.targetPath,
|
|
3185
|
+
expectedIdentity: identity,
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
catch (err) {
|
|
3189
|
+
emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
|
|
3190
|
+
throw err;
|
|
3191
|
+
}
|
|
3192
|
+
});
|
|
3193
|
+
}
|
|
3194
|
+
async function copyFileInRoot(root, params) {
|
|
3195
|
+
assertValidRootRelativePath(params.relativePath);
|
|
3196
|
+
path_assertNoNulPathInput(params.sourcePath, "source path contains a NUL byte");
|
|
3197
|
+
const source = await openVerifiedLocalFile(params.sourcePath, {
|
|
3198
|
+
hardlinks: params.sourceHardlinks,
|
|
3199
|
+
});
|
|
3200
|
+
if (params.maxBytes !== undefined && source.stat.size > params.maxBytes) {
|
|
3201
|
+
await source.handle.close().catch(() => { });
|
|
3202
|
+
throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`);
|
|
3203
|
+
}
|
|
3204
|
+
try {
|
|
3205
|
+
if (process.platform === "win32") {
|
|
3206
|
+
await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
|
|
3207
|
+
await copyFileFallback(root, params, source);
|
|
3208
|
+
});
|
|
3209
|
+
return;
|
|
3210
|
+
}
|
|
3211
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode);
|
|
3212
|
+
await serializePathWrite(pinned.targetPath, async () => {
|
|
3213
|
+
let identity;
|
|
3214
|
+
try {
|
|
3215
|
+
if (getFsSafePythonConfig().mode === "off") {
|
|
3216
|
+
await copyFileFallback(root, params, source);
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3219
|
+
identity = await runPinnedCopyHelper({
|
|
3220
|
+
rootPath: pinned.rootReal,
|
|
3221
|
+
relativeParentPath: pinned.relativeParentPath,
|
|
3222
|
+
basename: pinned.basename,
|
|
3223
|
+
mkdir: params.mkdir !== false,
|
|
3224
|
+
mode: pinned.mode,
|
|
3225
|
+
overwrite: true,
|
|
3226
|
+
maxBytes: params.maxBytes,
|
|
3227
|
+
sourcePath: source.realPath,
|
|
3228
|
+
sourceIdentity: { dev: source.stat.dev, ino: source.stat.ino },
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
3231
|
+
catch (error) {
|
|
3232
|
+
if (canFallbackFromPythonError(error)) {
|
|
3233
|
+
await copyFileFallback(root, params, source);
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3236
|
+
throw normalizePinnedWriteError(error);
|
|
3237
|
+
}
|
|
3238
|
+
try {
|
|
3239
|
+
await verifyAtomicWriteResult({
|
|
3240
|
+
root,
|
|
3241
|
+
targetPath: pinned.targetPath,
|
|
3242
|
+
expectedIdentity: identity,
|
|
3243
|
+
});
|
|
3244
|
+
}
|
|
3245
|
+
catch (err) {
|
|
3246
|
+
emitWriteBoundaryWarning(`post-copy verification failed: ${String(err)}`);
|
|
3247
|
+
throw err;
|
|
3248
|
+
}
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
finally {
|
|
3252
|
+
await source.handle.close().catch(() => { });
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode) {
|
|
3256
|
+
const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, relativePath);
|
|
3257
|
+
try {
|
|
3258
|
+
await assertNoPathAliasEscape({
|
|
3259
|
+
absolutePath: resolved,
|
|
3260
|
+
rootPath: rootReal,
|
|
3261
|
+
boundaryLabel: "root",
|
|
3262
|
+
});
|
|
3263
|
+
}
|
|
3264
|
+
catch (err) {
|
|
3265
|
+
throw new errors_FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
3266
|
+
}
|
|
3267
|
+
// resolvePathInRoot already enforces isPathInside, so any actual escape
|
|
3268
|
+
// is rejected upstream.
|
|
3269
|
+
const relativeResolved = external_node_path_namespaceObject.relative(rootReal, resolved);
|
|
3270
|
+
if (external_node_path_namespaceObject.isAbsolute(relativeResolved)) {
|
|
3271
|
+
throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
|
|
3272
|
+
}
|
|
3273
|
+
const relativePosix = relativeResolved
|
|
3274
|
+
? relativeResolved.split(external_node_path_namespaceObject.sep).join(external_node_path_namespaceObject.posix.sep)
|
|
3275
|
+
: "";
|
|
3276
|
+
const basename = external_node_path_namespaceObject.posix.basename(relativePosix);
|
|
3277
|
+
if (!basename || basename === "." || basename === "/") {
|
|
3278
|
+
throw new errors_FsSafeError("invalid-path", "invalid target path");
|
|
3279
|
+
}
|
|
3280
|
+
let mode = requestedMode ?? 0o600;
|
|
3281
|
+
try {
|
|
3282
|
+
const opened = await openFileInRoot(root, {
|
|
3283
|
+
relativePath,
|
|
3284
|
+
hardlinks: "reject",
|
|
3285
|
+
nonBlockingRead: true,
|
|
3286
|
+
});
|
|
3287
|
+
try {
|
|
3288
|
+
mode = requestedMode ?? (opened.stat.mode & 0o777);
|
|
3289
|
+
if (!path_isPathInside(rootWithSep, opened.realPath)) {
|
|
3290
|
+
throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
finally {
|
|
3294
|
+
await opened.handle.close().catch(() => { });
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
catch (err) {
|
|
3298
|
+
if (!(err instanceof errors_FsSafeError) || err.code !== "not-found") {
|
|
3299
|
+
throw err;
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
return {
|
|
3303
|
+
rootReal,
|
|
3304
|
+
targetPath: resolved,
|
|
3305
|
+
relativeParentPath: external_node_path_namespaceObject.posix.dirname(relativePosix) === "." ? "" : external_node_path_namespaceObject.posix.dirname(relativePosix),
|
|
3306
|
+
basename,
|
|
3307
|
+
mode: mode || 0o600,
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
3310
|
+
async function resolvePinnedPathInRoot(root, params) {
|
|
3311
|
+
return await resolvePinnedOperationPathInRoot(root, {
|
|
3312
|
+
allowRoot: params.allowRoot,
|
|
3313
|
+
relativePath: params.relativePath,
|
|
3314
|
+
policy: PATH_ALIAS_POLICIES.strict,
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
async function resolvePinnedRemovePathInRoot(root, relativePath) {
|
|
3318
|
+
return await resolvePinnedOperationPathInRoot(root, {
|
|
3319
|
+
relativePath,
|
|
3320
|
+
policy: PATH_ALIAS_POLICIES.unlinkTarget,
|
|
3321
|
+
});
|
|
3322
|
+
}
|
|
3323
|
+
async function resolvePinnedOperationPathInRoot(root, params) {
|
|
3324
|
+
const resolved = await resolvePinnedRootPathInRoot(root, {
|
|
3325
|
+
relativePath: params.relativePath,
|
|
3326
|
+
policy: params.policy,
|
|
3327
|
+
});
|
|
3328
|
+
const relativeResolved = external_node_path_namespaceObject.relative(resolved.rootReal, resolved.canonicalPath);
|
|
3329
|
+
if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
|
|
3330
|
+
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix: "" };
|
|
3331
|
+
}
|
|
3332
|
+
const firstSegment = relativeResolved.split(external_node_path_namespaceObject.sep)[0];
|
|
3333
|
+
if (relativeResolved === "" ||
|
|
3334
|
+
relativeResolved === "." ||
|
|
3335
|
+
firstSegment === ".." ||
|
|
3336
|
+
external_node_path_namespaceObject.isAbsolute(relativeResolved)) {
|
|
3337
|
+
throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
|
|
3338
|
+
}
|
|
3339
|
+
const relativePosix = relativeResolved.split(external_node_path_namespaceObject.sep).join(external_node_path_namespaceObject.posix.sep);
|
|
3340
|
+
if (!path_isPathInside(resolved.rootWithSep, resolved.canonicalPath)) {
|
|
3341
|
+
throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
|
|
3342
|
+
}
|
|
3343
|
+
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix };
|
|
3344
|
+
}
|
|
3345
|
+
async function resolvePinnedRootPathInRoot(root, params) {
|
|
3346
|
+
const rootReal = root.rootReal;
|
|
3347
|
+
let resolved;
|
|
3348
|
+
try {
|
|
3349
|
+
resolved = await resolveRootPath({
|
|
3350
|
+
absolutePath: external_node_path_namespaceObject.resolve(rootReal, await expandRelativePathWithHome(params.relativePath)),
|
|
3351
|
+
rootPath: rootReal,
|
|
3352
|
+
rootCanonicalPath: rootReal,
|
|
3353
|
+
boundaryLabel: "root",
|
|
3354
|
+
policy: params.policy,
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
catch (err) {
|
|
3358
|
+
throw new errors_FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
3359
|
+
}
|
|
3360
|
+
const rootWithSep = ensureTrailingSep(resolved.rootCanonicalPath);
|
|
3361
|
+
return {
|
|
3362
|
+
rootReal: resolved.rootCanonicalPath,
|
|
3363
|
+
rootWithSep,
|
|
3364
|
+
canonicalPath: resolved.canonicalPath,
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
async function removePathFallback(resolved) {
|
|
3368
|
+
const guard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(resolved.resolved));
|
|
3369
|
+
await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", resolved.resolved);
|
|
3370
|
+
await assertAsyncDirectoryGuard(guard);
|
|
3371
|
+
await ((await promises_namespaceObject.lstat(resolved.resolved)).isDirectory() ? promises_namespaceObject.rmdir(resolved.resolved) : promises_namespaceObject.rm(resolved.resolved));
|
|
3372
|
+
await assertAsyncDirectoryGuard(guard).catch(() => undefined);
|
|
3373
|
+
}
|
|
3374
|
+
async function mkdirPathFallback(resolved) {
|
|
3375
|
+
await mkdirPathComponentsWithGuards({
|
|
3376
|
+
rootReal: resolved.rootReal, targetPath: resolved.resolved,
|
|
3377
|
+
beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
|
|
3378
|
+
});
|
|
3379
|
+
}
|
|
3380
|
+
async function statPathFallback(root, relativePath) {
|
|
3381
|
+
const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
|
|
3382
|
+
try {
|
|
3383
|
+
return pathStatFromStats(await promises_namespaceObject.lstat(resolved.resolved));
|
|
3384
|
+
}
|
|
3385
|
+
catch (error) {
|
|
3386
|
+
if (path_isNotFoundPathError(error)) {
|
|
3387
|
+
throw new errors_FsSafeError("not-found", "file not found", {
|
|
3388
|
+
cause: error instanceof Error ? error : undefined,
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
throw error;
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
async function listPathFallback(root, relativePath, withFileTypes) {
|
|
3395
|
+
const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
|
|
3396
|
+
try {
|
|
3397
|
+
const names = await promises_namespaceObject.readdir(resolved.resolved);
|
|
3398
|
+
const sortedNames = names.toSorted();
|
|
3399
|
+
if (!withFileTypes) {
|
|
3400
|
+
return sortedNames;
|
|
3401
|
+
}
|
|
3402
|
+
const entries = [];
|
|
3403
|
+
for (const name of sortedNames) {
|
|
3404
|
+
entries.push({
|
|
3405
|
+
name,
|
|
3406
|
+
...pathStatFromStats(await promises_namespaceObject.lstat(external_node_path_namespaceObject.join(resolved.resolved, name))),
|
|
3407
|
+
});
|
|
3408
|
+
}
|
|
3409
|
+
return entries;
|
|
3410
|
+
}
|
|
3411
|
+
catch (error) {
|
|
3412
|
+
if (path_isNotFoundPathError(error)) {
|
|
3413
|
+
throw new errors_FsSafeError("not-found", "directory not found", {
|
|
3414
|
+
cause: error instanceof Error ? error : undefined,
|
|
3415
|
+
});
|
|
3416
|
+
}
|
|
3417
|
+
throw error;
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
async function movePathFallback(root, params) {
|
|
3421
|
+
const source = await resolvePathInRoot(root, params.fromRelative);
|
|
3422
|
+
await resolvePinnedRootPathInRoot(root, {
|
|
3423
|
+
relativePath: params.fromRelative,
|
|
3424
|
+
policy: PATH_ALIAS_POLICIES.strict,
|
|
3425
|
+
});
|
|
3426
|
+
const target = await resolvePathInRoot(root, params.toRelative);
|
|
3427
|
+
await resolvePinnedRootPathInRoot(root, {
|
|
3428
|
+
relativePath: params.toRelative,
|
|
3429
|
+
policy: PATH_ALIAS_POLICIES.unlinkTarget,
|
|
3430
|
+
});
|
|
3431
|
+
try {
|
|
3432
|
+
await assertNoPathAliasEscape({
|
|
3433
|
+
absolutePath: target.resolved,
|
|
3434
|
+
rootPath: target.rootReal,
|
|
3435
|
+
boundaryLabel: "root",
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
catch (error) {
|
|
3439
|
+
throw new errors_FsSafeError("path-alias", "path alias escape blocked", {
|
|
3440
|
+
cause: error instanceof Error ? error : undefined,
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
let sourceStat;
|
|
3444
|
+
try {
|
|
3445
|
+
sourceStat = await promises_namespaceObject.lstat(source.resolved);
|
|
3446
|
+
}
|
|
3447
|
+
catch (error) {
|
|
3448
|
+
if (path_isNotFoundPathError(error)) {
|
|
3449
|
+
throw new errors_FsSafeError("not-found", "file not found", {
|
|
3450
|
+
cause: error instanceof Error ? error : undefined,
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
throw error;
|
|
3454
|
+
}
|
|
3455
|
+
if (sourceStat.isSymbolicLink()) {
|
|
3456
|
+
throw new errors_FsSafeError("symlink", "symlink not allowed");
|
|
3457
|
+
}
|
|
3458
|
+
if (sourceStat.isFile() && sourceStat.nlink > 1) {
|
|
3459
|
+
throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
|
|
3460
|
+
}
|
|
3461
|
+
if (!params.overwrite) {
|
|
3462
|
+
try {
|
|
3463
|
+
await promises_namespaceObject.lstat(target.resolved);
|
|
3464
|
+
throw new errors_FsSafeError("already-exists", "destination exists");
|
|
3465
|
+
}
|
|
3466
|
+
catch (error) {
|
|
3467
|
+
if (error instanceof errors_FsSafeError) {
|
|
3468
|
+
throw error;
|
|
3469
|
+
}
|
|
3470
|
+
if (!path_isNotFoundPathError(error)) {
|
|
3471
|
+
throw error;
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
const sourceParentGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(source.resolved));
|
|
3476
|
+
const targetParentGuard = await directory_guard_createNearestExistingDirectoryGuard(target.rootReal, external_node_path_namespaceObject.dirname(target.resolved));
|
|
3477
|
+
await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("move", target.resolved);
|
|
3478
|
+
await assertAsyncDirectoryGuard(sourceParentGuard);
|
|
3479
|
+
await assertAsyncDirectoryGuard(targetParentGuard);
|
|
3480
|
+
try {
|
|
3481
|
+
await promises_namespaceObject.rename(source.resolved, target.resolved);
|
|
3482
|
+
}
|
|
3483
|
+
catch (error) {
|
|
3484
|
+
if (path_isNotFoundPathError(error)) {
|
|
3485
|
+
throw new errors_FsSafeError("not-found", "file not found", {
|
|
3486
|
+
cause: error instanceof Error ? error : undefined,
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3489
|
+
if (hasNodeErrorCode(error, "EEXIST")) {
|
|
3490
|
+
throw new errors_FsSafeError("already-exists", "destination exists", {
|
|
3491
|
+
cause: error instanceof Error ? error : undefined,
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
throw error;
|
|
3495
|
+
}
|
|
3496
|
+
await assertAsyncDirectoryGuard(targetParentGuard).catch(() => undefined);
|
|
3497
|
+
}
|
|
3498
|
+
async function writeFileFallback(root, params) {
|
|
3499
|
+
if (params.overwrite === false) {
|
|
3500
|
+
await writeMissingFileFallback(root, params);
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
const target = await openWritableFileInRoot(root, {
|
|
3504
|
+
relativePath: params.relativePath,
|
|
3505
|
+
mkdir: params.mkdir,
|
|
3506
|
+
mode: params.mode,
|
|
3507
|
+
truncateExisting: false,
|
|
3508
|
+
});
|
|
3509
|
+
const destinationPath = target.realPath;
|
|
3510
|
+
const mode = params.mode ?? (target.stat.mode & 0o777);
|
|
3511
|
+
await target.handle.close().catch(() => { });
|
|
3512
|
+
const destinationGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(destinationPath));
|
|
3513
|
+
let tempPath = null;
|
|
3514
|
+
let unregisterTempPath = null;
|
|
3515
|
+
try {
|
|
3516
|
+
tempPath = buildAtomicWriteTempPath(destinationPath);
|
|
3517
|
+
unregisterTempPath = registerTempPathForExit(tempPath);
|
|
3518
|
+
const writtenStat = await writeTempFileForAtomicReplace({
|
|
3519
|
+
tempPath,
|
|
3520
|
+
data: params.data,
|
|
3521
|
+
encoding: params.encoding,
|
|
3522
|
+
mode: mode || 0o600,
|
|
3523
|
+
});
|
|
3524
|
+
const commitTempPath = tempPath;
|
|
3525
|
+
await withAsyncDirectoryGuards([destinationGuard], async () => {
|
|
3526
|
+
await promises_namespaceObject.rename(commitTempPath, destinationPath);
|
|
3527
|
+
});
|
|
3528
|
+
tempPath = null;
|
|
3529
|
+
unregisterTempPath();
|
|
3530
|
+
unregisterTempPath = null;
|
|
3531
|
+
try {
|
|
3532
|
+
await verifyAtomicWriteResult({
|
|
3533
|
+
root,
|
|
3534
|
+
targetPath: destinationPath,
|
|
3535
|
+
expectedIdentity: writtenStat,
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
catch (err) {
|
|
3539
|
+
emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
|
|
3540
|
+
throw err;
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
finally {
|
|
3544
|
+
if (tempPath) {
|
|
3545
|
+
await promises_namespaceObject.rm(tempPath, { force: true }).catch(() => { });
|
|
3546
|
+
}
|
|
3547
|
+
unregisterTempPath?.();
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
async function writeMissingFileFallback(root, params) {
|
|
3551
|
+
const { rootReal, resolved } = await resolvePathInRoot(root, params.relativePath);
|
|
3552
|
+
try {
|
|
3553
|
+
await assertNoPathAliasEscape({
|
|
3554
|
+
absolutePath: resolved,
|
|
3555
|
+
rootPath: rootReal,
|
|
3556
|
+
boundaryLabel: "root",
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3559
|
+
catch (err) {
|
|
3560
|
+
throw new errors_FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
3561
|
+
}
|
|
3562
|
+
if (params.mkdir !== false) {
|
|
3563
|
+
await promises_namespaceObject.mkdir(external_node_path_namespaceObject.dirname(resolved), { recursive: true });
|
|
3564
|
+
}
|
|
3565
|
+
const parentGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(resolved));
|
|
3566
|
+
let created = false;
|
|
3567
|
+
try {
|
|
3568
|
+
const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
3569
|
+
const handle = await promises_namespaceObject.open(resolved, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600);
|
|
3570
|
+
created = true;
|
|
3571
|
+
try {
|
|
3572
|
+
if (typeof params.data === "string") {
|
|
3573
|
+
await handle.writeFile(params.data, params.encoding ?? "utf8");
|
|
3574
|
+
}
|
|
3575
|
+
else {
|
|
3576
|
+
await handle.writeFile(params.data);
|
|
3577
|
+
}
|
|
3578
|
+
return { handle, writtenStat: await handle.stat() };
|
|
3579
|
+
}
|
|
3580
|
+
catch (error) {
|
|
3581
|
+
await handle.close().catch(() => undefined);
|
|
3582
|
+
throw error;
|
|
3583
|
+
}
|
|
3584
|
+
}, {
|
|
3585
|
+
onPostGuardFailure: async ({ handle }) => {
|
|
3586
|
+
created = false; // Parent is untrusted now; skip outer path cleanup by name.
|
|
3587
|
+
await handle.close().catch(() => undefined);
|
|
3588
|
+
},
|
|
3589
|
+
});
|
|
3590
|
+
await handle.close();
|
|
3591
|
+
await verifyAtomicWriteResult({
|
|
3592
|
+
root,
|
|
3593
|
+
targetPath: resolved,
|
|
3594
|
+
expectedIdentity: writtenStat,
|
|
3595
|
+
});
|
|
3596
|
+
created = false;
|
|
3597
|
+
}
|
|
3598
|
+
catch (err) {
|
|
3599
|
+
if (hasNodeErrorCode(err, "EEXIST")) {
|
|
3600
|
+
throw new errors_FsSafeError("already-exists", "file already exists", {
|
|
3601
|
+
cause: err instanceof Error ? err : undefined,
|
|
3602
|
+
});
|
|
3603
|
+
}
|
|
3604
|
+
throw err;
|
|
3605
|
+
}
|
|
3606
|
+
finally {
|
|
3607
|
+
if (created) {
|
|
3608
|
+
await promises_namespaceObject.rm(resolved, { force: true }).catch(() => undefined);
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
async function copyFileFallback(root, params, source) {
|
|
3613
|
+
let target = null;
|
|
3614
|
+
let sourceClosedByStream = false;
|
|
3615
|
+
let targetClosedByUs = false;
|
|
3616
|
+
let tempHandle = null;
|
|
3617
|
+
let tempPath = null;
|
|
3618
|
+
let unregisterTempPath = null;
|
|
3619
|
+
let tempClosedByStream = false;
|
|
3620
|
+
try {
|
|
3621
|
+
target = await openWritableFileInRoot(root, {
|
|
3622
|
+
relativePath: params.relativePath,
|
|
3623
|
+
mkdir: params.mkdir,
|
|
3624
|
+
mode: params.mode,
|
|
3625
|
+
truncateExisting: false,
|
|
3626
|
+
});
|
|
3627
|
+
const destinationPath = target.realPath;
|
|
3628
|
+
const mode = params.mode ?? (target.stat.mode & 0o777);
|
|
3629
|
+
await target.handle.close().catch(() => { });
|
|
3630
|
+
targetClosedByUs = true;
|
|
3631
|
+
const destinationGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(destinationPath));
|
|
3632
|
+
tempPath = buildAtomicWriteTempPath(destinationPath);
|
|
3633
|
+
unregisterTempPath = registerTempPathForExit(tempPath);
|
|
3634
|
+
tempHandle = await promises_namespaceObject.open(tempPath, OPEN_WRITE_CREATE_FLAGS, mode || 0o600);
|
|
3635
|
+
const sourceStream = createBoundedReadStream(source, params.maxBytes);
|
|
3636
|
+
const targetStream = tempHandle.createWriteStream();
|
|
3637
|
+
sourceStream.once("close", () => {
|
|
3638
|
+
sourceClosedByStream = true;
|
|
3639
|
+
});
|
|
3640
|
+
targetStream.once("close", () => {
|
|
3641
|
+
tempClosedByStream = true;
|
|
3642
|
+
});
|
|
3643
|
+
await (0,external_node_stream_promises_namespaceObject.pipeline)(sourceStream, targetStream);
|
|
3644
|
+
const writtenStat = await promises_namespaceObject.stat(tempPath);
|
|
3645
|
+
if (!tempClosedByStream) {
|
|
3646
|
+
await tempHandle.close().catch(() => { });
|
|
3647
|
+
tempClosedByStream = true;
|
|
3648
|
+
}
|
|
3649
|
+
tempHandle = null;
|
|
3650
|
+
const commitTempPath = tempPath;
|
|
3651
|
+
await withAsyncDirectoryGuards([destinationGuard], async () => {
|
|
3652
|
+
await promises_namespaceObject.rename(commitTempPath, destinationPath);
|
|
3653
|
+
});
|
|
3654
|
+
tempPath = null;
|
|
3655
|
+
unregisterTempPath();
|
|
3656
|
+
unregisterTempPath = null;
|
|
3657
|
+
try {
|
|
3658
|
+
await verifyAtomicWriteResult({
|
|
3659
|
+
root,
|
|
3660
|
+
targetPath: destinationPath,
|
|
3661
|
+
expectedIdentity: writtenStat,
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
catch (err) {
|
|
3665
|
+
emitWriteBoundaryWarning(`post-copy verification failed: ${String(err)}`);
|
|
3666
|
+
throw err;
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
catch (err) {
|
|
3670
|
+
if (target?.createdForWrite) {
|
|
3671
|
+
await promises_namespaceObject.rm(target.realPath, { force: true }).catch(() => { });
|
|
3672
|
+
}
|
|
3673
|
+
throw err;
|
|
3674
|
+
}
|
|
3675
|
+
finally {
|
|
3676
|
+
if (!sourceClosedByStream) {
|
|
3677
|
+
await source.handle.close().catch(() => { });
|
|
3678
|
+
}
|
|
3679
|
+
if (tempHandle && !tempClosedByStream) {
|
|
3680
|
+
await tempHandle.close().catch(() => { });
|
|
3681
|
+
}
|
|
3682
|
+
if (tempPath) {
|
|
3683
|
+
await promises_namespaceObject.rm(tempPath, { force: true }).catch(() => { });
|
|
3684
|
+
}
|
|
3685
|
+
unregisterTempPath?.();
|
|
3686
|
+
if (target && !targetClosedByUs) {
|
|
3687
|
+
await target.handle.close().catch(() => { });
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
;// CONCATENATED MODULE: ./src/lib/safe-fs.ts
|
|
3693
|
+
|
|
3694
|
+
function mapFsSafeError(error, relativePath) {
|
|
3695
|
+
if (error instanceof errors_FsSafeError) {
|
|
3696
|
+
switch(error.code){
|
|
3697
|
+
case "outside-workspace":
|
|
3698
|
+
case "invalid-path":
|
|
3699
|
+
throw new Error(`Resolved path is outside target directory: ${relativePath}`, {
|
|
3700
|
+
cause: error
|
|
3701
|
+
});
|
|
3702
|
+
case "path-alias":
|
|
3703
|
+
case "symlink":
|
|
3704
|
+
throw new Error(`Symlinks are not allowed: path contains symbolic link: ${relativePath}`, {
|
|
3705
|
+
cause: error
|
|
3706
|
+
});
|
|
3707
|
+
case "too-large":
|
|
3708
|
+
throw new Error(`File ${relativePath} exceeds size limit: ${error.message}`, {
|
|
3709
|
+
cause: error
|
|
3710
|
+
});
|
|
3711
|
+
default:
|
|
3712
|
+
throw error;
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
throw error;
|
|
3716
|
+
}
|
|
3717
|
+
async function createSafeRoot(targetDir, maxBytes) {
|
|
3718
|
+
return root_impl_root(targetDir, {
|
|
3719
|
+
hardlinks: "reject",
|
|
3720
|
+
maxBytes,
|
|
3721
|
+
symlinks: "reject"
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
function toSafeDirEntry(entry) {
|
|
3725
|
+
return {
|
|
3726
|
+
name: entry.name,
|
|
3727
|
+
isDirectory: ()=>entry.isDirectory,
|
|
3728
|
+
isFile: ()=>entry.isFile,
|
|
3729
|
+
isSymbolicLink: ()=>entry.isSymbolicLink
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
async function readBytesWithinRoot(targetDir, relativePath, maxBytes) {
|
|
3733
|
+
try {
|
|
3734
|
+
const safeRoot = await createSafeRoot(targetDir, maxBytes);
|
|
3735
|
+
return await safeRoot.readBytes(relativePath, {
|
|
3736
|
+
maxBytes
|
|
3737
|
+
});
|
|
3738
|
+
} catch (error) {
|
|
3739
|
+
mapFsSafeError(error, relativePath);
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
async function readTextWithinRoot(targetDir, relativePath, maxBytes) {
|
|
3743
|
+
try {
|
|
3744
|
+
const safeRoot = await createSafeRoot(targetDir, maxBytes);
|
|
3745
|
+
return await safeRoot.readText(relativePath, {
|
|
3746
|
+
maxBytes
|
|
3747
|
+
});
|
|
3748
|
+
} catch (error) {
|
|
3749
|
+
mapFsSafeError(error, relativePath);
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
async function listDirectoryWithinRoot(targetDir, relativePath) {
|
|
3753
|
+
try {
|
|
3754
|
+
const safeRoot = await createSafeRoot(targetDir);
|
|
3755
|
+
const entries = await safeRoot.list(relativePath, {
|
|
3756
|
+
withFileTypes: true
|
|
3757
|
+
});
|
|
3758
|
+
return entries.map(toSafeDirEntry);
|
|
3759
|
+
} catch (error) {
|
|
3760
|
+
mapFsSafeError(error, relativePath);
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
async function statWithinRoot(targetDir, relativePath) {
|
|
3764
|
+
try {
|
|
3765
|
+
const safeRoot = await createSafeRoot(targetDir);
|
|
3766
|
+
return await safeRoot.stat(relativePath);
|
|
3767
|
+
} catch (error) {
|
|
3768
|
+
mapFsSafeError(error, relativePath);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
async function pathExistsWithinRoot(targetDir, relativePath) {
|
|
3772
|
+
try {
|
|
3773
|
+
const safeRoot = await createSafeRoot(targetDir);
|
|
3774
|
+
return await safeRoot.exists(relativePath);
|
|
3775
|
+
} catch (error) {
|
|
3776
|
+
mapFsSafeError(error, relativePath);
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
|
|
80
3780
|
;// CONCATENATED MODULE: ./src/gateways/project-scanner.ts
|
|
81
3781
|
|
|
82
3782
|
|
|
@@ -97,33 +3797,39 @@ const LANGUAGE_MARKERS = {
|
|
|
97
3797
|
"build.gradle": "java",
|
|
98
3798
|
"build.gradle.kts": "java"
|
|
99
3799
|
};
|
|
100
|
-
async function fileExists(
|
|
3800
|
+
async function fileExists(rootDir, relativePath) {
|
|
101
3801
|
try {
|
|
102
|
-
|
|
103
|
-
|
|
3802
|
+
if (!await pathExistsWithinRoot(rootDir, relativePath)) {
|
|
3803
|
+
return false;
|
|
3804
|
+
}
|
|
3805
|
+
const s = await statWithinRoot(rootDir, relativePath);
|
|
3806
|
+
return s.isFile;
|
|
104
3807
|
} catch {
|
|
105
3808
|
return false;
|
|
106
3809
|
}
|
|
107
3810
|
}
|
|
108
|
-
async function dirExists(
|
|
3811
|
+
async function dirExists(rootDir, relativePath) {
|
|
109
3812
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
3813
|
+
if (!await pathExistsWithinRoot(rootDir, relativePath)) {
|
|
3814
|
+
return false;
|
|
3815
|
+
}
|
|
3816
|
+
const s = await statWithinRoot(rootDir, relativePath);
|
|
3817
|
+
return s.isDirectory;
|
|
112
3818
|
} catch {
|
|
113
3819
|
return false;
|
|
114
3820
|
}
|
|
115
3821
|
}
|
|
116
3822
|
async function discoverScripts(targetDir) {
|
|
117
|
-
const packageJsonPath =
|
|
118
|
-
if (!await fileExists(packageJsonPath)) {
|
|
3823
|
+
const packageJsonPath = "package.json";
|
|
3824
|
+
if (!await fileExists(targetDir, packageJsonPath)) {
|
|
119
3825
|
return [];
|
|
120
3826
|
}
|
|
121
3827
|
try {
|
|
122
|
-
const fileStat = await (
|
|
3828
|
+
const fileStat = await statWithinRoot(targetDir, packageJsonPath);
|
|
123
3829
|
if (fileStat.size > MAX_FILE_SIZE_BYTES) {
|
|
124
3830
|
return [];
|
|
125
3831
|
}
|
|
126
|
-
const content = await (
|
|
3832
|
+
const content = await readTextWithinRoot(targetDir, packageJsonPath, MAX_FILE_SIZE_BYTES);
|
|
127
3833
|
const parsed = JSON.parse(content);
|
|
128
3834
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
129
3835
|
return [];
|
|
@@ -148,13 +3854,14 @@ async function discoverScripts(targetDir) {
|
|
|
148
3854
|
}
|
|
149
3855
|
}
|
|
150
3856
|
async function discoverWorkflowCommands(targetDir) {
|
|
151
|
-
const workflowsDir = (0,external_node_path_namespaceObject.join)(
|
|
152
|
-
if (!await dirExists(workflowsDir)) {
|
|
3857
|
+
const workflowsDir = (0,external_node_path_namespaceObject.join)(".github", "workflows");
|
|
3858
|
+
if (!await dirExists(targetDir, workflowsDir)) {
|
|
153
3859
|
return [];
|
|
154
3860
|
}
|
|
155
3861
|
let files;
|
|
156
3862
|
try {
|
|
157
|
-
|
|
3863
|
+
const entries = await listDirectoryWithinRoot(targetDir, workflowsDir);
|
|
3864
|
+
files = entries.filter((entry)=>entry.isFile()).map((entry)=>entry.name);
|
|
158
3865
|
} catch {
|
|
159
3866
|
return [];
|
|
160
3867
|
}
|
|
@@ -163,11 +3870,11 @@ async function discoverWorkflowCommands(targetDir) {
|
|
|
163
3870
|
for (const file of yamlFiles){
|
|
164
3871
|
try {
|
|
165
3872
|
const filePath = (0,external_node_path_namespaceObject.join)(workflowsDir, file);
|
|
166
|
-
const fileStat = await (
|
|
3873
|
+
const fileStat = await statWithinRoot(targetDir, filePath);
|
|
167
3874
|
if (fileStat.size > MAX_FILE_SIZE_BYTES) {
|
|
168
3875
|
continue;
|
|
169
3876
|
}
|
|
170
|
-
const content = await (
|
|
3877
|
+
const content = await readTextWithinRoot(targetDir, filePath, MAX_FILE_SIZE_BYTES);
|
|
171
3878
|
const commands = extractRunCommandsFromYaml(content);
|
|
172
3879
|
allCommands.push(...commands);
|
|
173
3880
|
} catch {}
|
|
@@ -299,16 +4006,16 @@ function isUsefulCommand(cmd) {
|
|
|
299
4006
|
* Parse mise.toml to extract task definitions.
|
|
300
4007
|
* Uses simple line-based parsing for [tasks.*] sections.
|
|
301
4008
|
*/ async function discoverMiseTasks(targetDir) {
|
|
302
|
-
const miseTomlPath =
|
|
303
|
-
if (!await fileExists(miseTomlPath)) {
|
|
4009
|
+
const miseTomlPath = "mise.toml";
|
|
4010
|
+
if (!await fileExists(targetDir, miseTomlPath)) {
|
|
304
4011
|
return [];
|
|
305
4012
|
}
|
|
306
4013
|
try {
|
|
307
|
-
const fileStat = await (
|
|
4014
|
+
const fileStat = await statWithinRoot(targetDir, miseTomlPath);
|
|
308
4015
|
if (fileStat.size > MAX_FILE_SIZE_BYTES) {
|
|
309
4016
|
return [];
|
|
310
4017
|
}
|
|
311
|
-
const content = await (
|
|
4018
|
+
const content = await readTextWithinRoot(targetDir, miseTomlPath, MAX_FILE_SIZE_BYTES);
|
|
312
4019
|
return parseMiseTomlTasks(content);
|
|
313
4020
|
} catch {
|
|
314
4021
|
return [];
|
|
@@ -373,7 +4080,7 @@ function parseMiseTomlTasks(content) {
|
|
|
373
4080
|
async function detectLanguages(targetDir) {
|
|
374
4081
|
const detected = new Set();
|
|
375
4082
|
for (const [filename, language] of Object.entries(LANGUAGE_MARKERS)){
|
|
376
|
-
if (await fileExists(
|
|
4083
|
+
if (await fileExists(targetDir, filename)) {
|
|
377
4084
|
detected.add(language);
|
|
378
4085
|
}
|
|
379
4086
|
}
|
|
@@ -727,7 +4434,7 @@ function jsonStringifyReplacer(_, value) {
|
|
|
727
4434
|
return value.toString();
|
|
728
4435
|
return value;
|
|
729
4436
|
}
|
|
730
|
-
function
|
|
4437
|
+
function util_cached(getter) {
|
|
731
4438
|
const set = false;
|
|
732
4439
|
return {
|
|
733
4440
|
get value() {
|
|
@@ -843,7 +4550,7 @@ const captureStackTrace = ("captureStackTrace" in Error ? Error.captureStackTrac
|
|
|
843
4550
|
function util_isObject(data) {
|
|
844
4551
|
return typeof data === "object" && data !== null && !Array.isArray(data);
|
|
845
4552
|
}
|
|
846
|
-
const util_allowsEval = /* @__PURE__*/
|
|
4553
|
+
const util_allowsEval = /* @__PURE__*/ util_cached(() => {
|
|
847
4554
|
// Skip the probe under `jitless`: strict CSPs report the caught `new Function`
|
|
848
4555
|
// as a `securitypolicyviolation` even though the throw is swallowed.
|
|
849
4556
|
if (globalConfig.jitless) {
|
|
@@ -3093,7 +6800,7 @@ const $ZodObject = /*@__PURE__*/ $constructor("$ZodObject", (inst, def) => {
|
|
|
3093
6800
|
},
|
|
3094
6801
|
});
|
|
3095
6802
|
}
|
|
3096
|
-
const _normalized =
|
|
6803
|
+
const _normalized = util_cached(() => normalizeDef(def));
|
|
3097
6804
|
defineLazy(inst._zod, "propValues", () => {
|
|
3098
6805
|
const shape = def.shape;
|
|
3099
6806
|
const propValues = {};
|
|
@@ -3147,7 +6854,7 @@ const $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
3147
6854
|
// requires cast because technically $ZodObject doesn't extend
|
|
3148
6855
|
$ZodObject.init(inst, def);
|
|
3149
6856
|
const superParse = inst._zod.parse;
|
|
3150
|
-
const _normalized =
|
|
6857
|
+
const _normalized = util_cached(() => normalizeDef(def));
|
|
3151
6858
|
const generateFastpass = (shape) => {
|
|
3152
6859
|
const doc = new Doc(["shape", "payload", "ctx"]);
|
|
3153
6860
|
const normalized = _normalized.value;
|
|
@@ -3421,7 +7128,7 @@ $constructor("$ZodDiscriminatedUnion", (inst, def) => {
|
|
|
3421
7128
|
}
|
|
3422
7129
|
return propValues;
|
|
3423
7130
|
});
|
|
3424
|
-
const disc =
|
|
7131
|
+
const disc = util_cached(() => {
|
|
3425
7132
|
const opts = def.options;
|
|
3426
7133
|
const map = new Map();
|
|
3427
7134
|
for (const o of opts) {
|
|
@@ -8797,8 +12504,6 @@ function hasProtoKey(value) {
|
|
|
8797
12504
|
return hasProtoKeyAtDepth(value, 0);
|
|
8798
12505
|
}
|
|
8799
12506
|
|
|
8800
|
-
;// CONCATENATED MODULE: external "node:fs"
|
|
8801
|
-
const external_node_fs_namespaceObject = __rspack_createRequire_require("node:fs");
|
|
8802
12507
|
;// CONCATENATED MODULE: external "node:module"
|
|
8803
12508
|
const external_node_module_namespaceObject = __rspack_createRequire_require("node:module");
|
|
8804
12509
|
;// CONCATENATED MODULE: external "node:url"
|
|
@@ -8984,6 +12689,7 @@ ${miseList}
|
|
|
8984
12689
|
|
|
8985
12690
|
const DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
8986
12691
|
const MAX_FILE_READ_BYTES = 102_400;
|
|
12692
|
+
const MAX_PROJECT_FILE_BYTES = 20 * 1024 * 1024;
|
|
8987
12693
|
const AnalysisResponseSchema = schemas_object({
|
|
8988
12694
|
additionalAllowRules: schemas_array(schemas_string().max(512)).max(100),
|
|
8989
12695
|
suggestions: schemas_array(schemas_string().max(1024)).max(100)
|
|
@@ -9044,28 +12750,33 @@ const AnalysisResponseSchema = schemas_object({
|
|
|
9044
12750
|
error: "Path is outside the repository"
|
|
9045
12751
|
};
|
|
9046
12752
|
}
|
|
12753
|
+
const cleanRoot = repoRoot.replace(/\/+$/, "") || "/";
|
|
12754
|
+
const relPath = (0,external_node_path_namespaceObject.relative)(cleanRoot, safePath);
|
|
9047
12755
|
let fileBuffer;
|
|
12756
|
+
let truncated = false;
|
|
9048
12757
|
try {
|
|
9049
|
-
const
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
12758
|
+
const fileStat = await statWithinRoot(repoRoot, relPath);
|
|
12759
|
+
truncated = fileStat.size > MAX_FILE_READ_BYTES;
|
|
12760
|
+
if (fileStat.size > MAX_PROJECT_FILE_BYTES) {
|
|
12761
|
+
return {
|
|
12762
|
+
error: "File not found or unreadable"
|
|
12763
|
+
};
|
|
12764
|
+
}
|
|
12765
|
+
fileBuffer = await readBytesWithinRoot(repoRoot, relPath, MAX_PROJECT_FILE_BYTES);
|
|
12766
|
+
} catch (error) {
|
|
12767
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12768
|
+
if (message.includes("symbolic link")) {
|
|
9056
12769
|
return {
|
|
9057
12770
|
error: "Path is outside the repository"
|
|
9058
12771
|
};
|
|
9059
12772
|
}
|
|
9060
|
-
fileBuffer = await (0,promises_namespaceObject.readFile)(realPath);
|
|
9061
|
-
} catch {
|
|
9062
12773
|
return {
|
|
9063
12774
|
error: "File not found or unreadable"
|
|
9064
12775
|
};
|
|
9065
12776
|
}
|
|
9066
12777
|
// Buffer processing outside I/O catch — programming bugs in
|
|
9067
12778
|
// findUtf8Boundary propagate instead of being silently swallowed.
|
|
9068
|
-
if (
|
|
12779
|
+
if (!truncated) {
|
|
9069
12780
|
return {
|
|
9070
12781
|
content: fileBuffer.toString("utf-8"),
|
|
9071
12782
|
truncated: false
|
|
@@ -9574,9 +13285,9 @@ function detectExistingFeatures(config) {
|
|
|
9574
13285
|
hasPostToolUse: hasHookCommand(config.hooks.postToolUse, AGENT_SHELL_RECORD)
|
|
9575
13286
|
};
|
|
9576
13287
|
}
|
|
9577
|
-
async function loadExistingHooksConfig(hooksPath, deps) {
|
|
13288
|
+
async function loadExistingHooksConfig(repoRoot, hooksPath, deps) {
|
|
9578
13289
|
try {
|
|
9579
|
-
const raw = await deps.readFile(hooksPath, "utf-8");
|
|
13290
|
+
const raw = deps.readFileWithinRoot ? await deps.readFileWithinRoot(repoRoot, init_command_HOOKS_SUBPATH, 524_288) : await deps.readFile(hooksPath, "utf-8");
|
|
9580
13291
|
const parsed = JSON.parse(raw);
|
|
9581
13292
|
if (hasProtoKey(parsed)) {
|
|
9582
13293
|
throw new Error("hooks schema validation failed");
|
|
@@ -9720,7 +13431,7 @@ async function writeConfigFile(repoRoot, subpath, content, deps) {
|
|
|
9720
13431
|
async function handleInit(flags, deps) {
|
|
9721
13432
|
const repoRoot = deps.getRepositoryRoot();
|
|
9722
13433
|
const hooksPath = (0,external_node_path_namespaceObject.join)(repoRoot, init_command_HOOKS_SUBPATH);
|
|
9723
|
-
const { config: existingConfig, error: loadError } = await loadExistingHooksConfig(hooksPath, deps);
|
|
13434
|
+
const { config: existingConfig, error: loadError } = await loadExistingHooksConfig(repoRoot, hooksPath, deps);
|
|
9724
13435
|
if (loadError) {
|
|
9725
13436
|
return false;
|
|
9726
13437
|
}
|
|
@@ -9813,7 +13524,7 @@ async function handleInit(flags, deps) {
|
|
|
9813
13524
|
const policyPath = (0,external_node_path_namespaceObject.join)(repoRoot, init_command_POLICY_SUBPATH);
|
|
9814
13525
|
let existingContent = null;
|
|
9815
13526
|
try {
|
|
9816
|
-
existingContent = await deps.readFile(policyPath, "utf-8");
|
|
13527
|
+
existingContent = deps.readFileWithinRoot ? await deps.readFileWithinRoot(repoRoot, init_command_POLICY_SUBPATH, 524_288) : await deps.readFile(policyPath, "utf-8");
|
|
9817
13528
|
} catch (error) {
|
|
9818
13529
|
if (!isPathNotFoundError(error)) {
|
|
9819
13530
|
throw error;
|
|
@@ -9905,7 +13616,7 @@ async function resolveReadEventsDir(env, deps) {
|
|
|
9905
13616
|
if (logDir !== undefined && logDir !== "") {
|
|
9906
13617
|
const projectRootReal = await deps.realpath(projectRoot);
|
|
9907
13618
|
const candidate = (0,external_node_path_namespaceObject.resolve)(projectRoot, logDir);
|
|
9908
|
-
if (!isWithinProjectRoot(candidate, projectRoot) && !isWithinProjectRoot(candidate, projectRootReal)) {
|
|
13619
|
+
if (!(0,external_node_path_namespaceObject.isAbsolute)(logDir) && !isWithinProjectRoot(candidate, projectRoot) && !isWithinProjectRoot(candidate, projectRootReal)) {
|
|
9909
13620
|
return {
|
|
9910
13621
|
dir: "",
|
|
9911
13622
|
error: "AGENTSHELL_LOG_DIR resolves outside project root"
|
|
@@ -10161,6 +13872,8 @@ function formatEventsJson(events) {
|
|
|
10161
13872
|
|
|
10162
13873
|
|
|
10163
13874
|
|
|
13875
|
+
|
|
13876
|
+
|
|
10164
13877
|
function parseLogArgs(args) {
|
|
10165
13878
|
const options = {
|
|
10166
13879
|
failures: false,
|
|
@@ -10209,19 +13922,42 @@ function parseLogArgs(args) {
|
|
|
10209
13922
|
}
|
|
10210
13923
|
return options;
|
|
10211
13924
|
}
|
|
13925
|
+
function relativePathUnderRoot(rootDir, path) {
|
|
13926
|
+
const relativePath = (0,external_node_path_namespaceObject.relative)(rootDir, path);
|
|
13927
|
+
if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${external_node_path_namespaceObject.sep}`) || (0,external_node_path_namespaceObject.isAbsolute)(relativePath)) {
|
|
13928
|
+
throw new Error("Path resolves outside project root");
|
|
13929
|
+
}
|
|
13930
|
+
return relativePath;
|
|
13931
|
+
}
|
|
13932
|
+
async function* readSafeFileLines(rootDir, path) {
|
|
13933
|
+
const relativePath = relativePathUnderRoot(rootDir, path);
|
|
13934
|
+
await statWithinRoot(rootDir, relativePath);
|
|
13935
|
+
const rl = (0,external_node_readline_namespaceObject.createInterface)({
|
|
13936
|
+
input: (0,external_node_fs_namespaceObject.createReadStream)((0,external_node_path_namespaceObject.join)(rootDir, relativePath)),
|
|
13937
|
+
crlfDelay: Infinity
|
|
13938
|
+
});
|
|
13939
|
+
for await (const line of rl){
|
|
13940
|
+
yield line;
|
|
13941
|
+
}
|
|
13942
|
+
}
|
|
10212
13943
|
function createDefaultQueryDeps() {
|
|
13944
|
+
const rootDir = process.cwd();
|
|
10213
13945
|
return {
|
|
10214
|
-
readdir: (path)=>
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
13946
|
+
readdir: async (path)=>{
|
|
13947
|
+
const relativePath = relativePathUnderRoot(rootDir, path);
|
|
13948
|
+
const entries = await listDirectoryWithinRoot(rootDir, relativePath);
|
|
13949
|
+
return entries.map((entry)=>entry.name);
|
|
13950
|
+
},
|
|
13951
|
+
stat: async (path)=>{
|
|
13952
|
+
const relativePath = relativePathUnderRoot(rootDir, path);
|
|
13953
|
+
const s = await statWithinRoot(rootDir, relativePath);
|
|
13954
|
+
return {
|
|
13955
|
+
mtimeMs: s.mtimeMs
|
|
13956
|
+
};
|
|
13957
|
+
},
|
|
10218
13958
|
realpath: (path)=>(0,promises_namespaceObject.realpath)(path),
|
|
10219
13959
|
cwd: ()=>process.cwd(),
|
|
10220
|
-
readFileLines: (path)=>(
|
|
10221
|
-
input: (0,external_node_fs_namespaceObject.createReadStream)(path, {
|
|
10222
|
-
encoding: "utf-8"
|
|
10223
|
-
})
|
|
10224
|
-
}),
|
|
13960
|
+
readFileLines: (path)=>readSafeFileLines(rootDir, path),
|
|
10225
13961
|
writeStderr: (msg)=>{
|
|
10226
13962
|
process.stderr.write(msg);
|
|
10227
13963
|
}
|
|
@@ -10390,6 +14126,13 @@ function resolvePolicyPath(env, repoRoot) {
|
|
|
10390
14126
|
isOverride: false
|
|
10391
14127
|
};
|
|
10392
14128
|
}
|
|
14129
|
+
function policy_relativePathUnderRoot(repoRoot, path) {
|
|
14130
|
+
const relativePath = (0,external_node_path_namespaceObject.relative)(repoRoot, path);
|
|
14131
|
+
if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${external_node_path_namespaceObject.sep}`) || (0,external_node_path_namespaceObject.isAbsolute)(relativePath)) {
|
|
14132
|
+
return null;
|
|
14133
|
+
}
|
|
14134
|
+
return relativePath;
|
|
14135
|
+
}
|
|
10393
14136
|
async function loadPolicy(env, deps) {
|
|
10394
14137
|
const rawRepoRoot = deps.getRepositoryRoot();
|
|
10395
14138
|
const repoRoot = await deps.realpath(rawRepoRoot);
|
|
@@ -10410,8 +14153,12 @@ async function loadPolicy(env, deps) {
|
|
|
10410
14153
|
throw new Error(`Policy file path resolves outside the repository root: ${sanitizePath(resolvedPath)}`);
|
|
10411
14154
|
}
|
|
10412
14155
|
let content;
|
|
14156
|
+
const relativePolicyPath = policy_relativePathUnderRoot(repoRoot, candidatePath);
|
|
14157
|
+
if (relativePolicyPath === null) {
|
|
14158
|
+
throw new Error(`Policy file path resolves outside the repository root: ${sanitizePath(candidatePath)}`);
|
|
14159
|
+
}
|
|
10413
14160
|
try {
|
|
10414
|
-
content = await deps.readFile(resolvedPath, "utf-8");
|
|
14161
|
+
content = deps.readFileWithinRoot ? await deps.readFileWithinRoot(repoRoot, relativePolicyPath, 524_288) : await deps.readFile(resolvedPath, "utf-8");
|
|
10415
14162
|
} catch (error) {
|
|
10416
14163
|
if (isPathNotFoundError(error)) {
|
|
10417
14164
|
if (isOverride) {
|
|
@@ -10691,6 +14438,7 @@ async function handleRecord(deps) {
|
|
|
10691
14438
|
|
|
10692
14439
|
|
|
10693
14440
|
|
|
14441
|
+
|
|
10694
14442
|
const VERSION = "0.1.0";
|
|
10695
14443
|
const USAGE = `Usage: agent-shell -c <command>
|
|
10696
14444
|
agent-shell init [--flight-recorder] [--policy] [--no-flight-recorder] [--no-policy]
|
|
@@ -10735,6 +14483,7 @@ async function main() {
|
|
|
10735
14483
|
policyDeps: {
|
|
10736
14484
|
realpath: (path)=>(0,promises_namespaceObject.realpath)(path),
|
|
10737
14485
|
readFile: (path, encoding)=>(0,promises_namespaceObject.readFile)(path, encoding),
|
|
14486
|
+
readFileWithinRoot: (rootDir, relativePath, maxBytes)=>readTextWithinRoot(rootDir, relativePath, maxBytes),
|
|
10738
14487
|
getRepositoryRoot
|
|
10739
14488
|
},
|
|
10740
14489
|
telemetryDeps: createDefaultDeps()
|
|
@@ -10818,6 +14567,7 @@ async function main() {
|
|
|
10818
14567
|
writeStdout: (data)=>process.stdout.write(data),
|
|
10819
14568
|
writeStderr: (data)=>process.stderr.write(data),
|
|
10820
14569
|
readFile: (path, encoding)=>(0,promises_namespaceObject.readFile)(path, encoding),
|
|
14570
|
+
readFileWithinRoot: (rootDir, relativePath, maxBytes)=>readTextWithinRoot(rootDir, relativePath, maxBytes),
|
|
10821
14571
|
writeFile: (path, content, options)=>(0,promises_namespaceObject.writeFile)(path, content, options),
|
|
10822
14572
|
rename: (oldPath, newPath)=>(0,promises_namespaceObject.rename)(oldPath, newPath),
|
|
10823
14573
|
unlink: (path)=>(0,promises_namespaceObject.unlink)(path),
|
|
@@ -11033,4 +14783,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
|
|
|
11033
14783
|
// module factories are used so entry inlining is disabled
|
|
11034
14784
|
// startup
|
|
11035
14785
|
// Load entry module and return exports
|
|
11036
|
-
var __webpack_exports__ = __webpack_require__(
|
|
14786
|
+
var __webpack_exports__ = __webpack_require__(391);
|