@openclaw/fs-safe 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +114 -0
- package/LICENSE +1 -0
- package/README.md +39 -6
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +3 -2
- package/dist/advanced.d.ts +5 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +5 -1
- package/dist/archive-durability.d.ts +6 -6
- package/dist/archive-durability.d.ts.map +1 -1
- package/dist/archive-durability.js +1 -1
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +8 -8
- package/dist/archive-gzip-tail.d.ts +1 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +16 -9
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +4 -2
- package/dist/archive-merge.d.ts +5 -1
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +15 -12
- package/dist/archive-native.js +4 -4
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +54 -42
- package/dist/archive-staging.d.ts +6 -3
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +42 -22
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +7 -6
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +16 -13
- package/dist/archive-zip-admission.d.ts +1 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +48 -12
- package/dist/archive-zip-loader.d.ts +6 -0
- package/dist/archive-zip-loader.d.ts.map +1 -0
- package/dist/archive-zip-loader.js +38 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +26 -9
- package/dist/archive-zip-preflight.d.ts +2 -3
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +2 -34
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +12 -10
- package/dist/bounded-read.d.ts +5 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +18 -11
- package/dist/copy-file-input.d.ts +0 -1
- package/dist/copy-file-input.d.ts.map +1 -1
- package/dist/copy-file-input.js +4 -26
- package/dist/copy-tree-portable.d.ts.map +1 -1
- package/dist/copy-tree-portable.js +57 -26
- package/dist/copy.d.ts +1 -1
- package/dist/copy.d.ts.map +1 -1
- package/dist/copy.js +3 -1
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +5 -3
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +5 -4
- package/dist/directory-guard.d.ts +11 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +53 -11
- package/dist/durability.d.ts +1 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +1 -1
- package/dist/file-handle-transfer.d.ts +14 -0
- package/dist/file-handle-transfer.d.ts.map +1 -0
- package/dist/file-handle-transfer.js +64 -0
- package/dist/file-hash.d.ts +3 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +99 -32
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +8 -4
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +7 -5
- package/dist/file-store-path.d.ts +3 -0
- package/dist/file-store-path.d.ts.map +1 -0
- package/dist/file-store-path.js +27 -0
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +15 -5
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +56 -44
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +2 -18
- package/dist/filename.d.ts +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +32 -14
- package/dist/guarded-mkdir.d.ts +2 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +52 -16
- package/dist/guest-dispatch-python.d.ts +2 -0
- package/dist/guest-dispatch-python.d.ts.map +1 -0
- package/dist/guest-dispatch-python.js +117 -0
- package/dist/guest-native-python.d.ts +4 -0
- package/dist/guest-native-python.d.ts.map +1 -0
- package/dist/guest-native-python.js +135 -0
- package/dist/guest.d.ts +9 -0
- package/dist/guest.d.ts.map +1 -0
- package/dist/guest.js +421 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts +6 -0
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +16 -2
- package/dist/json-durable-queue-directory.js +3 -3
- package/dist/json-durable-queue-ownership.d.ts +2 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +47 -3
- package/dist/json-durable-queue-read.d.ts +6 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -0
- package/dist/json-durable-queue-read.js +59 -0
- package/dist/json-durable-queue.d.ts +1 -1
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +43 -84
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +2 -1
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +2 -1
- package/dist/move-path-stage.d.ts.map +1 -1
- package/dist/move-path-stage.js +2 -1
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +4 -3
- package/dist/mutation-authority.d.ts +1 -0
- package/dist/mutation-authority.d.ts.map +1 -1
- package/dist/mutation-authority.js +4 -4
- package/dist/native-binding.d.ts +14 -2
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +3 -2
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +2 -1
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +5 -4
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +2 -0
- package/dist/overwrite-file-handle.d.ts +8 -0
- package/dist/overwrite-file-handle.d.ts.map +1 -0
- package/dist/overwrite-file-handle.js +42 -0
- package/dist/path-case.d.ts +7 -0
- package/dist/path-case.d.ts.map +1 -0
- package/dist/path-case.js +136 -0
- package/dist/path-scope-lexical.d.ts +14 -0
- package/dist/path-scope-lexical.d.ts.map +1 -0
- package/dist/path-scope-lexical.js +27 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +22 -3
- package/dist/permissions-windows.d.ts +1 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +48 -6
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +3 -1
- package/dist/pinned-write.d.ts +2 -2
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +6 -4
- package/dist/realpath.d.ts +4 -0
- package/dist/realpath.d.ts.map +1 -0
- package/dist/realpath.js +43 -0
- package/dist/recursive-mkdir-path.d.ts +3 -0
- package/dist/recursive-mkdir-path.d.ts.map +1 -0
- package/dist/recursive-mkdir-path.js +8 -0
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +2 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +23 -31
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +7 -12
- package/dist/replace-file-mode.d.ts +3 -0
- package/dist/replace-file-mode.d.ts.map +1 -0
- package/dist/replace-file-mode.js +10 -0
- package/dist/replace-file.d.ts +1 -0
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +14 -8
- package/dist/root-boundary.d.ts +25 -0
- package/dist/root-boundary.d.ts.map +1 -0
- package/dist/root-boundary.js +177 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +40 -13
- package/dist/root-create-input.d.ts +10 -0
- package/dist/root-create-input.d.ts.map +1 -0
- package/dist/root-create-input.js +80 -0
- package/dist/root-directory-list.d.ts +3 -1
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +31 -5
- package/dist/root-entries.d.ts +11 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +61 -0
- package/dist/root-errors.d.ts +5 -5
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +13 -12
- package/dist/root-impl.d.ts +13 -3
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +93 -70
- package/dist/root-move-preflight.d.ts +8 -0
- package/dist/root-move-preflight.d.ts.map +1 -0
- package/dist/root-move-preflight.js +16 -0
- package/dist/root-options.d.ts +12 -1
- package/dist/root-options.d.ts.map +1 -1
- package/dist/root-path-existing.d.ts +2 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +14 -5
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +3 -2
- package/dist/root-path.d.ts +2 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +54 -26
- package/dist/root-paths.d.ts +2 -6
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +24 -32
- package/dist/root-remove.d.ts +5 -0
- package/dist/root-remove.d.ts.map +1 -0
- package/dist/root-remove.js +286 -0
- package/dist/root-symlink-policy.d.ts +2 -1
- package/dist/root-symlink-policy.d.ts.map +1 -1
- package/dist/root-symlink-policy.js +2 -2
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +2 -1
- package/dist/root-write-mode.d.ts +2 -0
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +21 -7
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +12 -3
- package/dist/root.d.ts +2 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +3 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +2 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +2 -1
- package/dist/secure-file-windows.d.ts +10 -0
- package/dist/secure-file-windows.d.ts.map +1 -0
- package/dist/secure-file-windows.js +186 -0
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +27 -7
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +2 -1
- package/dist/sibling-staged-file.d.ts +2 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +49 -8
- package/dist/sibling-temp.d.ts +2 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +8 -5
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +16 -5
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +17 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -3
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +4 -3
- package/dist/temp-target.d.ts +14 -12
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +15 -7
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +7 -5
- package/dist/unicode-path.d.ts +3 -0
- package/dist/unicode-path.d.ts.map +1 -0
- package/dist/unicode-path.js +13 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +14 -12
- package/dist/write-file-handle.d.ts +1 -0
- package/dist/write-file-handle.d.ts.map +1 -1
- package/dist/write-file-handle.js +3 -2
- package/docs/advanced.md +7 -1
- package/docs/archive.md +31 -5
- package/docs/atomic.md +17 -1
- package/docs/config.md +1 -0
- package/docs/contributing.md +33 -2
- package/docs/copy.md +75 -6
- package/docs/directory-identity.md +85 -0
- package/docs/durability.md +40 -3
- package/docs/entries.md +109 -0
- package/docs/errors.md +3 -3
- package/docs/file-store.md +21 -0
- package/docs/filename.md +9 -2
- package/docs/guest.md +146 -0
- package/docs/in-place-write.md +81 -0
- package/docs/index.md +2 -0
- package/docs/install-path.md +59 -13
- package/docs/install.md +34 -0
- package/docs/native-helper.md +14 -5
- package/docs/native.md +18 -2
- package/docs/output.md +32 -6
- package/docs/path-case.md +64 -0
- package/docs/path-scope.md +1 -1
- package/docs/path.md +1 -1
- package/docs/permissions.md +37 -3
- package/docs/public-api.md +36 -2
- package/docs/root.md +33 -3
- package/docs/secure-file.md +17 -14
- package/docs/security-model.md +1 -1
- package/docs/sidecar-lock.md +12 -3
- package/docs/store.md +22 -1
- package/docs/temp.md +39 -6
- package/docs/types.md +1 -1
- package/docs/writing.md +153 -3
- package/package.json +14 -8
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export const GUEST_FILESYSTEM_CREATE_STAGING_PYTHON = [
|
|
2
|
+
"def create_staging_dir(parent_fd):",
|
|
3
|
+
" # This helper guarantees descriptor-relative confinement and no-replace",
|
|
4
|
+
" # publication, not content integrity against same-UID peers; they can",
|
|
5
|
+
" # also rewrite the destination immediately after publication.",
|
|
6
|
+
" prefix = '.openclaw-create-'",
|
|
7
|
+
" for _ in range(128):",
|
|
8
|
+
" candidate = prefix + secrets.token_hex(6)",
|
|
9
|
+
" try:",
|
|
10
|
+
" os.mkdir(candidate, 0o700, dir_fd=parent_fd)",
|
|
11
|
+
" except FileExistsError:",
|
|
12
|
+
" continue",
|
|
13
|
+
" created_identity = entry_identity(os.lstat(candidate, dir_fd=parent_fd))",
|
|
14
|
+
" staging_fd = None",
|
|
15
|
+
" try:",
|
|
16
|
+
" staging_fd = open_dir(candidate, dir_fd=parent_fd)",
|
|
17
|
+
" if not same_identity(created_identity, os.fstat(staging_fd)):",
|
|
18
|
+
" raise OSError(errno.ESTALE, 'create staging directory changed', candidate)",
|
|
19
|
+
" return candidate, staging_fd",
|
|
20
|
+
" except Exception:",
|
|
21
|
+
" if staging_fd is not None:",
|
|
22
|
+
" os.close(staging_fd)",
|
|
23
|
+
" try:",
|
|
24
|
+
" current = os.lstat(candidate, dir_fd=parent_fd)",
|
|
25
|
+
" if same_identity(created_identity, current):",
|
|
26
|
+
" os.rmdir(candidate, dir_fd=parent_fd)",
|
|
27
|
+
" os.fsync(parent_fd)",
|
|
28
|
+
" except FileNotFoundError:",
|
|
29
|
+
" pass",
|
|
30
|
+
" raise",
|
|
31
|
+
" raise RuntimeError('failed to allocate sandbox create staging directory')",
|
|
32
|
+
].join("\n");
|
|
33
|
+
export const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON = [
|
|
34
|
+
"def rename_no_replace(src_parent_fd, src_basename, dst_parent_fd, dst_basename):",
|
|
35
|
+
" libc = ctypes.CDLL(None, use_errno=True)",
|
|
36
|
+
" is_linux = sys.platform.startswith('linux')",
|
|
37
|
+
" if is_linux:",
|
|
38
|
+
" rename_fn = getattr(libc, 'renameat2', None)",
|
|
39
|
+
" if rename_fn is None:",
|
|
40
|
+
" os.link(",
|
|
41
|
+
" src_basename,",
|
|
42
|
+
" dst_basename,",
|
|
43
|
+
" src_dir_fd=src_parent_fd,",
|
|
44
|
+
" dst_dir_fd=dst_parent_fd,",
|
|
45
|
+
" follow_symlinks=False,",
|
|
46
|
+
" )",
|
|
47
|
+
" return",
|
|
48
|
+
" flags = 1 # RENAME_NOREPLACE",
|
|
49
|
+
" elif sys.platform == 'darwin':",
|
|
50
|
+
" rename_fn = getattr(libc, 'renameatx_np', None)",
|
|
51
|
+
" flags = 0x00000004 # RENAME_EXCL",
|
|
52
|
+
" else:",
|
|
53
|
+
" rename_fn = None",
|
|
54
|
+
" flags = 0",
|
|
55
|
+
" if rename_fn is None:",
|
|
56
|
+
" raise OSError(errno.ENOSYS, 'atomic no-replace rename is unavailable')",
|
|
57
|
+
" rename_fn.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]",
|
|
58
|
+
" rename_fn.restype = ctypes.c_int",
|
|
59
|
+
" result = rename_fn(",
|
|
60
|
+
" src_parent_fd,",
|
|
61
|
+
" os.fsencode(src_basename),",
|
|
62
|
+
" dst_parent_fd,",
|
|
63
|
+
" os.fsencode(dst_basename),",
|
|
64
|
+
" flags,",
|
|
65
|
+
" )",
|
|
66
|
+
" if result != 0:",
|
|
67
|
+
" error_code = ctypes.get_errno()",
|
|
68
|
+
" unsupported_codes = {errno.ENOSYS, errno.EINVAL, errno.ENOTSUP}",
|
|
69
|
+
" if hasattr(errno, 'EOPNOTSUPP'):",
|
|
70
|
+
" unsupported_codes.add(errno.EOPNOTSUPP)",
|
|
71
|
+
" if is_linux and error_code in unsupported_codes:",
|
|
72
|
+
" os.link(",
|
|
73
|
+
" src_basename,",
|
|
74
|
+
" dst_basename,",
|
|
75
|
+
" src_dir_fd=src_parent_fd,",
|
|
76
|
+
" dst_dir_fd=dst_parent_fd,",
|
|
77
|
+
" follow_symlinks=False,",
|
|
78
|
+
" )",
|
|
79
|
+
" return",
|
|
80
|
+
" raise OSError(error_code, os.strerror(error_code), dst_basename)",
|
|
81
|
+
].join("\n");
|
|
82
|
+
export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
|
|
83
|
+
"def create_exclusive(parent_fd, basename, stdin_buffer):",
|
|
84
|
+
" staging_fd = None",
|
|
85
|
+
" staging_name = None",
|
|
86
|
+
" temp_fd = None",
|
|
87
|
+
" temp_name = 'payload'",
|
|
88
|
+
" temp_inode = None",
|
|
89
|
+
" try:",
|
|
90
|
+
" try:",
|
|
91
|
+
" os.lstat(basename, dir_fd=parent_fd)",
|
|
92
|
+
" except FileNotFoundError:",
|
|
93
|
+
" pass",
|
|
94
|
+
" else:",
|
|
95
|
+
" raise FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), basename)",
|
|
96
|
+
" staging_name, staging_fd = create_staging_dir(parent_fd)",
|
|
97
|
+
" temp_fd = os.open(temp_name, WRITE_FLAGS, 0o600, dir_fd=staging_fd)",
|
|
98
|
+
" while True:",
|
|
99
|
+
" chunk = stdin_buffer.read(65536)",
|
|
100
|
+
" if not chunk:",
|
|
101
|
+
" break",
|
|
102
|
+
" write_all(temp_fd, chunk)",
|
|
103
|
+
" # exclusive create payload is durable before publication",
|
|
104
|
+
" os.fsync(temp_fd)",
|
|
105
|
+
" temp_inode = inode_identity(os.fstat(temp_fd))",
|
|
106
|
+
" # Publish with a native atomic no-replace rename.",
|
|
107
|
+
" rename_no_replace(staging_fd, temp_name, parent_fd, basename)",
|
|
108
|
+
" target_stat = os.lstat(basename, dir_fd=parent_fd)",
|
|
109
|
+
" if temp_inode != inode_identity(target_stat):",
|
|
110
|
+
" raise OSError(errno.ESTALE, 'exclusive publication source changed', basename)",
|
|
111
|
+
" os.fsync(parent_fd)",
|
|
112
|
+
" finally:",
|
|
113
|
+
" if temp_fd is not None:",
|
|
114
|
+
" os.close(temp_fd)",
|
|
115
|
+
" if staging_fd is not None:",
|
|
116
|
+
" # Cleanup stays relative to the private pinned directory, so a",
|
|
117
|
+
" # parent-path substitution cannot redirect payload deletion.",
|
|
118
|
+
" try:",
|
|
119
|
+
" os.unlink(temp_name, dir_fd=staging_fd)",
|
|
120
|
+
" except FileNotFoundError:",
|
|
121
|
+
" pass",
|
|
122
|
+
" staging_identity = entry_identity(os.fstat(staging_fd))",
|
|
123
|
+
" os.close(staging_fd)",
|
|
124
|
+
" staging_removed = False",
|
|
125
|
+
" if staging_name is not None:",
|
|
126
|
+
" try:",
|
|
127
|
+
" current_staging = os.lstat(staging_name, dir_fd=parent_fd)",
|
|
128
|
+
" if same_identity(staging_identity, current_staging):",
|
|
129
|
+
" os.rmdir(staging_name, dir_fd=parent_fd)",
|
|
130
|
+
" staging_removed = True",
|
|
131
|
+
" except FileNotFoundError:",
|
|
132
|
+
" pass",
|
|
133
|
+
" if staging_removed:",
|
|
134
|
+
" os.fsync(parent_fd)",
|
|
135
|
+
].join("\n");
|
package/dist/guest.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON } from "./guest-native-python.js";
|
|
2
|
+
/** Exclusive create found an existing destination, including a lost publication race. */
|
|
3
|
+
export declare const GUEST_FILESYSTEM_CREATE_EXISTS_EXIT_CODE = 17;
|
|
4
|
+
/** Read found a missing parent or file after the admitted root was opened. */
|
|
5
|
+
export declare const GUEST_FILESYSTEM_READ_NOT_FOUND_EXIT_CODE = 2;
|
|
6
|
+
/** Python 3 source for the descriptor-relative guest filesystem protocol. See docs/guest.md. */
|
|
7
|
+
export declare const GUEST_FILESYSTEM_PYTHON: string;
|
|
8
|
+
export { GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON };
|
|
9
|
+
//# sourceMappingURL=guest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guest.d.ts","sourceRoot":"","sources":["../src/guest.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,yCAAyC,EAC1C,MAAM,0BAA0B,CAAC;AAElC,yFAAyF;AACzF,eAAO,MAAM,wCAAwC,KAAK,CAAC;AAC3D,8EAA8E;AAC9E,eAAO,MAAM,yCAAyC,IAAI,CAAC;AAE3D,gGAAgG;AAChG,eAAO,MAAM,uBAAuB,QA2ZxB,CAAC;AAEb,OAAO,EAAE,yCAAyC,EAAE,CAAC"}
|
package/dist/guest.js
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
// Adapted from OpenClaw's guest filesystem program. Copyright OpenClaw Foundation; MIT.
|
|
2
|
+
import { GUEST_FILESYSTEM_DISPATCH_PYTHON } from "./guest-dispatch-python.js";
|
|
3
|
+
import { GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON, GUEST_FILESYSTEM_CREATE_STAGING_PYTHON, GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON, } from "./guest-native-python.js";
|
|
4
|
+
/** Exclusive create found an existing destination, including a lost publication race. */
|
|
5
|
+
export const GUEST_FILESYSTEM_CREATE_EXISTS_EXIT_CODE = 17;
|
|
6
|
+
/** Read found a missing parent or file after the admitted root was opened. */
|
|
7
|
+
export const GUEST_FILESYSTEM_READ_NOT_FOUND_EXIT_CODE = 2;
|
|
8
|
+
/** Python 3 source for the descriptor-relative guest filesystem protocol. See docs/guest.md. */
|
|
9
|
+
export const GUEST_FILESYSTEM_PYTHON = [
|
|
10
|
+
`SANDBOX_CREATE_EXISTS_EXIT_CODE = ${GUEST_FILESYSTEM_CREATE_EXISTS_EXIT_CODE}`,
|
|
11
|
+
`SANDBOX_READ_NOT_FOUND_EXIT_CODE = ${GUEST_FILESYSTEM_READ_NOT_FOUND_EXIT_CODE}`,
|
|
12
|
+
"import ctypes",
|
|
13
|
+
"import errno",
|
|
14
|
+
"import json",
|
|
15
|
+
"import os",
|
|
16
|
+
"import secrets",
|
|
17
|
+
"import stat",
|
|
18
|
+
"import sys",
|
|
19
|
+
"",
|
|
20
|
+
"operation = sys.argv[1]",
|
|
21
|
+
"",
|
|
22
|
+
"DIR_FLAGS = os.O_RDONLY",
|
|
23
|
+
"if hasattr(os, 'O_DIRECTORY'):",
|
|
24
|
+
" DIR_FLAGS |= os.O_DIRECTORY",
|
|
25
|
+
"if hasattr(os, 'O_NOFOLLOW'):",
|
|
26
|
+
" DIR_FLAGS |= os.O_NOFOLLOW",
|
|
27
|
+
"",
|
|
28
|
+
"READ_FLAGS = os.O_RDONLY",
|
|
29
|
+
"if hasattr(os, 'O_NOFOLLOW'):",
|
|
30
|
+
" READ_FLAGS |= os.O_NOFOLLOW",
|
|
31
|
+
"if hasattr(os, 'O_NONBLOCK'):",
|
|
32
|
+
" READ_FLAGS |= os.O_NONBLOCK",
|
|
33
|
+
"",
|
|
34
|
+
"WRITE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL",
|
|
35
|
+
"if hasattr(os, 'O_NOFOLLOW'):",
|
|
36
|
+
" WRITE_FLAGS |= os.O_NOFOLLOW",
|
|
37
|
+
"",
|
|
38
|
+
"def split_relative(path_value):",
|
|
39
|
+
" segments = []",
|
|
40
|
+
" for segment in path_value.split('/'):",
|
|
41
|
+
" if not segment or segment == '.':",
|
|
42
|
+
" continue",
|
|
43
|
+
" if segment == '..':",
|
|
44
|
+
" raise OSError(errno.EPERM, 'path traversal is not allowed', segment)",
|
|
45
|
+
" segments.append(segment)",
|
|
46
|
+
" return segments",
|
|
47
|
+
"",
|
|
48
|
+
"def open_dir(path_value, dir_fd=None):",
|
|
49
|
+
" return os.open(path_value, DIR_FLAGS, dir_fd=dir_fd)",
|
|
50
|
+
"",
|
|
51
|
+
"def walk_dir(root_fd, rel_path, mkdir_enabled):",
|
|
52
|
+
" current_fd = os.dup(root_fd)",
|
|
53
|
+
" try:",
|
|
54
|
+
" for segment in split_relative(rel_path):",
|
|
55
|
+
" try:",
|
|
56
|
+
" next_fd = open_dir(segment, dir_fd=current_fd)",
|
|
57
|
+
" except FileNotFoundError:",
|
|
58
|
+
" if not mkdir_enabled:",
|
|
59
|
+
" raise",
|
|
60
|
+
" os.mkdir(segment, 0o777, dir_fd=current_fd)",
|
|
61
|
+
" next_fd = open_dir(segment, dir_fd=current_fd)",
|
|
62
|
+
" os.close(current_fd)",
|
|
63
|
+
" current_fd = next_fd",
|
|
64
|
+
" return current_fd",
|
|
65
|
+
" except Exception:",
|
|
66
|
+
" os.close(current_fd)",
|
|
67
|
+
" raise",
|
|
68
|
+
"",
|
|
69
|
+
"def create_temp_file(parent_fd):",
|
|
70
|
+
" prefix = '.openclaw-write-'",
|
|
71
|
+
" for _ in range(128):",
|
|
72
|
+
" candidate = prefix + secrets.token_hex(6)",
|
|
73
|
+
" try:",
|
|
74
|
+
" fd = os.open(candidate, WRITE_FLAGS, 0o600, dir_fd=parent_fd)",
|
|
75
|
+
" return candidate, fd",
|
|
76
|
+
" except FileExistsError:",
|
|
77
|
+
" continue",
|
|
78
|
+
" raise RuntimeError('failed to allocate sandbox temp file')",
|
|
79
|
+
"",
|
|
80
|
+
"def create_temp_dir(parent_fd, mode):",
|
|
81
|
+
" prefix = '.openclaw-move-'",
|
|
82
|
+
" for _ in range(128):",
|
|
83
|
+
" candidate = prefix + secrets.token_hex(6)",
|
|
84
|
+
" try:",
|
|
85
|
+
" os.mkdir(candidate, mode, dir_fd=parent_fd)",
|
|
86
|
+
" return candidate",
|
|
87
|
+
" except FileExistsError:",
|
|
88
|
+
" continue",
|
|
89
|
+
" raise RuntimeError('failed to allocate sandbox temp directory')",
|
|
90
|
+
"",
|
|
91
|
+
GUEST_FILESYSTEM_CREATE_STAGING_PYTHON,
|
|
92
|
+
"",
|
|
93
|
+
"def existing_regular_file_mode(parent_fd, basename):",
|
|
94
|
+
" try:",
|
|
95
|
+
" target_stat = os.lstat(basename, dir_fd=parent_fd)",
|
|
96
|
+
" except FileNotFoundError:",
|
|
97
|
+
" return None",
|
|
98
|
+
" if stat.S_ISREG(target_stat.st_mode):",
|
|
99
|
+
" return stat.S_IMODE(target_stat.st_mode)",
|
|
100
|
+
" return None",
|
|
101
|
+
"",
|
|
102
|
+
"def write_all(file_fd, data):",
|
|
103
|
+
" view = memoryview(data)",
|
|
104
|
+
" while view:",
|
|
105
|
+
" written = os.write(file_fd, view)",
|
|
106
|
+
" if written <= 0:",
|
|
107
|
+
" raise OSError(errno.EIO, 'failed to write copied file')",
|
|
108
|
+
" view = view[written:]",
|
|
109
|
+
"",
|
|
110
|
+
GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON,
|
|
111
|
+
"",
|
|
112
|
+
"def copy_regular_file(src_parent_fd, src_basename, dst_parent_fd, dst_basename):",
|
|
113
|
+
" src_fd = os.open(src_basename, READ_FLAGS, dir_fd=src_parent_fd)",
|
|
114
|
+
" dst_fd = None",
|
|
115
|
+
" destination_created = False",
|
|
116
|
+
" copy_completed = False",
|
|
117
|
+
" try:",
|
|
118
|
+
" src_stat = os.fstat(src_fd)",
|
|
119
|
+
" if not stat.S_ISREG(src_stat.st_mode):",
|
|
120
|
+
" raise OSError(errno.EPERM, 'only regular files are allowed', src_basename)",
|
|
121
|
+
" if src_stat.st_nlink > 1:",
|
|
122
|
+
" raise OSError(errno.EPERM, 'hardlinked file is not allowed', src_basename)",
|
|
123
|
+
" dst_fd = os.open(dst_basename, WRITE_FLAGS, stat.S_IMODE(src_stat.st_mode), dir_fd=dst_parent_fd)",
|
|
124
|
+
" destination_created = True",
|
|
125
|
+
" while True:",
|
|
126
|
+
" chunk = os.read(src_fd, 65536)",
|
|
127
|
+
" if not chunk:",
|
|
128
|
+
" break",
|
|
129
|
+
" write_all(dst_fd, chunk)",
|
|
130
|
+
" try:",
|
|
131
|
+
" os.fchmod(dst_fd, stat.S_IMODE(src_stat.st_mode))",
|
|
132
|
+
" except AttributeError:",
|
|
133
|
+
" pass",
|
|
134
|
+
" os.fsync(dst_fd)",
|
|
135
|
+
" copy_completed = True",
|
|
136
|
+
" return entry_identity(src_stat)",
|
|
137
|
+
" finally:",
|
|
138
|
+
" if dst_fd is not None:",
|
|
139
|
+
" os.close(dst_fd)",
|
|
140
|
+
" if destination_created and not copy_completed:",
|
|
141
|
+
" try:",
|
|
142
|
+
" os.unlink(dst_basename, dir_fd=dst_parent_fd)",
|
|
143
|
+
" except FileNotFoundError:",
|
|
144
|
+
" pass",
|
|
145
|
+
" os.close(src_fd)",
|
|
146
|
+
"",
|
|
147
|
+
"def copy_file_atomic(src_parent_fd, src_basename, dst_parent_fd, dst_basename):",
|
|
148
|
+
" prefix = '.openclaw-copy-'",
|
|
149
|
+
" temp_name = None",
|
|
150
|
+
" try:",
|
|
151
|
+
" for _ in range(128):",
|
|
152
|
+
" candidate = prefix + secrets.token_hex(6)",
|
|
153
|
+
" try:",
|
|
154
|
+
" copy_regular_file(src_parent_fd, src_basename, dst_parent_fd, candidate)",
|
|
155
|
+
" temp_name = candidate",
|
|
156
|
+
" break",
|
|
157
|
+
" except FileExistsError:",
|
|
158
|
+
" continue",
|
|
159
|
+
" if temp_name is None:",
|
|
160
|
+
" raise RuntimeError('failed to allocate sandbox copy temp file')",
|
|
161
|
+
" os.replace(temp_name, dst_basename, src_dir_fd=dst_parent_fd, dst_dir_fd=dst_parent_fd)",
|
|
162
|
+
" temp_name = None",
|
|
163
|
+
" os.fsync(dst_parent_fd)",
|
|
164
|
+
" finally:",
|
|
165
|
+
" if temp_name is not None:",
|
|
166
|
+
" try:",
|
|
167
|
+
" os.unlink(temp_name, dir_fd=dst_parent_fd)",
|
|
168
|
+
" except FileNotFoundError:",
|
|
169
|
+
" pass",
|
|
170
|
+
"",
|
|
171
|
+
"def write_atomic(parent_fd, basename, stdin_buffer):",
|
|
172
|
+
" target_mode = existing_regular_file_mode(parent_fd, basename)",
|
|
173
|
+
" temp_fd = None",
|
|
174
|
+
" temp_name = None",
|
|
175
|
+
" try:",
|
|
176
|
+
" temp_name, temp_fd = create_temp_file(parent_fd)",
|
|
177
|
+
" while True:",
|
|
178
|
+
" chunk = stdin_buffer.read(65536)",
|
|
179
|
+
" if not chunk:",
|
|
180
|
+
" break",
|
|
181
|
+
" write_all(temp_fd, chunk)",
|
|
182
|
+
" if target_mode is not None:",
|
|
183
|
+
" try:",
|
|
184
|
+
" os.fchmod(temp_fd, target_mode)",
|
|
185
|
+
" except AttributeError:",
|
|
186
|
+
" pass",
|
|
187
|
+
" os.fsync(temp_fd)",
|
|
188
|
+
" os.close(temp_fd)",
|
|
189
|
+
" temp_fd = None",
|
|
190
|
+
" os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)",
|
|
191
|
+
" temp_name = None",
|
|
192
|
+
" os.fsync(parent_fd)",
|
|
193
|
+
" finally:",
|
|
194
|
+
" if temp_fd is not None:",
|
|
195
|
+
" os.close(temp_fd)",
|
|
196
|
+
" if temp_name is not None:",
|
|
197
|
+
" try:",
|
|
198
|
+
" os.unlink(temp_name, dir_fd=parent_fd)",
|
|
199
|
+
" except FileNotFoundError:",
|
|
200
|
+
" pass",
|
|
201
|
+
"",
|
|
202
|
+
GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON,
|
|
203
|
+
"",
|
|
204
|
+
"def read_file_impl(parent_fd, basename, max_bytes):",
|
|
205
|
+
" file_fd = os.open(basename, READ_FLAGS, dir_fd=parent_fd)",
|
|
206
|
+
" try:",
|
|
207
|
+
" file_stat = os.fstat(file_fd)",
|
|
208
|
+
" if not stat.S_ISREG(file_stat.st_mode):",
|
|
209
|
+
" raise OSError(errno.EPERM, 'only regular files are allowed', basename)",
|
|
210
|
+
" if file_stat.st_nlink > 1:",
|
|
211
|
+
" raise OSError(errno.EPERM, 'hardlinked file is not allowed', basename)",
|
|
212
|
+
" if max_bytes is not None and file_stat.st_size > max_bytes:",
|
|
213
|
+
" raise OSError(errno.EFBIG, 'file exceeds bounded read limit', basename)",
|
|
214
|
+
" bytes_read = 0",
|
|
215
|
+
" while True:",
|
|
216
|
+
" read_size = 65536 if max_bytes is None else min(65536, max_bytes - bytes_read + 1)",
|
|
217
|
+
" chunk = os.read(file_fd, read_size)",
|
|
218
|
+
" if not chunk:",
|
|
219
|
+
" break",
|
|
220
|
+
" bytes_read += len(chunk)",
|
|
221
|
+
" if max_bytes is not None and bytes_read > max_bytes:",
|
|
222
|
+
" raise OSError(errno.EFBIG, 'file exceeds bounded read limit', basename)",
|
|
223
|
+
" write_all(1, chunk)",
|
|
224
|
+
" finally:",
|
|
225
|
+
" os.close(file_fd)",
|
|
226
|
+
"",
|
|
227
|
+
"def read_file(parent_fd, basename):",
|
|
228
|
+
" read_file_impl(parent_fd, basename, None)",
|
|
229
|
+
"",
|
|
230
|
+
"def read_file_bounded(parent_fd, basename, max_bytes):",
|
|
231
|
+
" if max_bytes < 0:",
|
|
232
|
+
" raise OSError(errno.EINVAL, 'read limit must be non-negative', basename)",
|
|
233
|
+
" read_file_impl(parent_fd, basename, max_bytes)",
|
|
234
|
+
"",
|
|
235
|
+
"def remove_tree(parent_fd, basename):",
|
|
236
|
+
" entry_stat = os.lstat(basename, dir_fd=parent_fd)",
|
|
237
|
+
" if not stat.S_ISDIR(entry_stat.st_mode) or stat.S_ISLNK(entry_stat.st_mode):",
|
|
238
|
+
" os.unlink(basename, dir_fd=parent_fd)",
|
|
239
|
+
" return",
|
|
240
|
+
" dir_fd = open_dir(basename, dir_fd=parent_fd)",
|
|
241
|
+
" try:",
|
|
242
|
+
" for child in os.listdir(dir_fd):",
|
|
243
|
+
" remove_tree(dir_fd, child)",
|
|
244
|
+
" finally:",
|
|
245
|
+
" os.close(dir_fd)",
|
|
246
|
+
" os.rmdir(basename, dir_fd=parent_fd)",
|
|
247
|
+
"",
|
|
248
|
+
"def entry_identity(entry_stat):",
|
|
249
|
+
" return (",
|
|
250
|
+
" entry_stat.st_dev,",
|
|
251
|
+
" entry_stat.st_ino,",
|
|
252
|
+
" entry_stat.st_mode,",
|
|
253
|
+
" entry_stat.st_size,",
|
|
254
|
+
" getattr(entry_stat, 'st_mtime_ns', int(entry_stat.st_mtime * 1000000000)),",
|
|
255
|
+
" getattr(entry_stat, 'st_ctime_ns', int(entry_stat.st_ctime * 1000000000)),",
|
|
256
|
+
" )",
|
|
257
|
+
"",
|
|
258
|
+
"def inode_identity(entry_stat):",
|
|
259
|
+
" return (entry_stat.st_dev, entry_stat.st_ino)",
|
|
260
|
+
"",
|
|
261
|
+
"def same_identity(expected, entry_stat):",
|
|
262
|
+
" return expected == entry_identity(entry_stat)",
|
|
263
|
+
"",
|
|
264
|
+
"def source_changed_error(basename):",
|
|
265
|
+
" return OSError(getattr(errno, 'ESTALE', errno.EIO), 'source changed during move fallback cleanup', basename)",
|
|
266
|
+
"",
|
|
267
|
+
"def copy_entry(src_parent_fd, src_basename, dst_parent_fd, dst_basename):",
|
|
268
|
+
" src_stat = os.lstat(src_basename, dir_fd=src_parent_fd)",
|
|
269
|
+
" if stat.S_ISDIR(src_stat.st_mode) and not stat.S_ISLNK(src_stat.st_mode):",
|
|
270
|
+
" os.mkdir(dst_basename, stat.S_IMODE(src_stat.st_mode) or 0o755, dir_fd=dst_parent_fd)",
|
|
271
|
+
" copied_children = []",
|
|
272
|
+
" src_dir_fd = None",
|
|
273
|
+
" dst_dir_fd = None",
|
|
274
|
+
" try:",
|
|
275
|
+
" src_dir_fd = open_dir(src_basename, dir_fd=src_parent_fd)",
|
|
276
|
+
" src_stat = os.fstat(src_dir_fd)",
|
|
277
|
+
" dst_dir_fd = open_dir(dst_basename, dir_fd=dst_parent_fd)",
|
|
278
|
+
" for child in os.listdir(src_dir_fd):",
|
|
279
|
+
" copied_children.append((child, copy_entry(src_dir_fd, child, dst_dir_fd, child)))",
|
|
280
|
+
" except Exception:",
|
|
281
|
+
" if dst_dir_fd is not None:",
|
|
282
|
+
" os.close(dst_dir_fd)",
|
|
283
|
+
" dst_dir_fd = None",
|
|
284
|
+
" try:",
|
|
285
|
+
" remove_tree(dst_parent_fd, dst_basename)",
|
|
286
|
+
" except FileNotFoundError:",
|
|
287
|
+
" pass",
|
|
288
|
+
" raise",
|
|
289
|
+
" finally:",
|
|
290
|
+
" if src_dir_fd is not None:",
|
|
291
|
+
" os.close(src_dir_fd)",
|
|
292
|
+
" if dst_dir_fd is not None:",
|
|
293
|
+
" os.close(dst_dir_fd)",
|
|
294
|
+
" return ('dir', entry_identity(src_stat), copied_children)",
|
|
295
|
+
" if stat.S_ISLNK(src_stat.st_mode):",
|
|
296
|
+
" link_target = os.readlink(src_basename, dir_fd=src_parent_fd)",
|
|
297
|
+
" os.symlink(link_target, dst_basename, dir_fd=dst_parent_fd)",
|
|
298
|
+
" return ('leaf', entry_identity(src_stat), None)",
|
|
299
|
+
" return ('leaf', copy_regular_file(src_parent_fd, src_basename, dst_parent_fd, dst_basename), None)",
|
|
300
|
+
"",
|
|
301
|
+
"def remove_copied_entry(parent_fd, basename, manifest):",
|
|
302
|
+
" kind, expected_identity, children = manifest",
|
|
303
|
+
" current_stat = os.lstat(basename, dir_fd=parent_fd)",
|
|
304
|
+
" if not same_identity(expected_identity, current_stat):",
|
|
305
|
+
" raise source_changed_error(basename)",
|
|
306
|
+
" if kind != 'dir':",
|
|
307
|
+
" os.unlink(basename, dir_fd=parent_fd)",
|
|
308
|
+
" return",
|
|
309
|
+
" dir_fd = open_dir(basename, dir_fd=parent_fd)",
|
|
310
|
+
" try:",
|
|
311
|
+
" for child, child_manifest in children:",
|
|
312
|
+
" remove_copied_entry(dir_fd, child, child_manifest)",
|
|
313
|
+
" finally:",
|
|
314
|
+
" os.close(dir_fd)",
|
|
315
|
+
" os.rmdir(basename, dir_fd=parent_fd)",
|
|
316
|
+
"",
|
|
317
|
+
"def move_entry(src_parent_fd, src_basename, dst_parent_fd, dst_basename):",
|
|
318
|
+
" try:",
|
|
319
|
+
" os.rename(src_basename, dst_basename, src_dir_fd=src_parent_fd, dst_dir_fd=dst_parent_fd)",
|
|
320
|
+
" os.fsync(dst_parent_fd)",
|
|
321
|
+
" os.fsync(src_parent_fd)",
|
|
322
|
+
" return",
|
|
323
|
+
" except OSError as err:",
|
|
324
|
+
" if err.errno != errno.EXDEV:",
|
|
325
|
+
" raise",
|
|
326
|
+
" src_stat = os.lstat(src_basename, dir_fd=src_parent_fd)",
|
|
327
|
+
" if stat.S_ISDIR(src_stat.st_mode) and not stat.S_ISLNK(src_stat.st_mode):",
|
|
328
|
+
" temp_dir_name = create_temp_dir(dst_parent_fd, stat.S_IMODE(src_stat.st_mode) or 0o755)",
|
|
329
|
+
" copied_children = []",
|
|
330
|
+
" temp_dir_fd = None",
|
|
331
|
+
" src_dir_fd = None",
|
|
332
|
+
" try:",
|
|
333
|
+
" temp_dir_fd = open_dir(temp_dir_name, dir_fd=dst_parent_fd)",
|
|
334
|
+
" src_dir_fd = open_dir(src_basename, dir_fd=src_parent_fd)",
|
|
335
|
+
" src_stat = os.fstat(src_dir_fd)",
|
|
336
|
+
" for child in os.listdir(src_dir_fd):",
|
|
337
|
+
" copied_children.append((child, copy_entry(src_dir_fd, child, temp_dir_fd, child)))",
|
|
338
|
+
" os.close(src_dir_fd)",
|
|
339
|
+
" src_dir_fd = None",
|
|
340
|
+
" os.close(temp_dir_fd)",
|
|
341
|
+
" temp_dir_fd = None",
|
|
342
|
+
" os.rename(temp_dir_name, dst_basename, src_dir_fd=dst_parent_fd, dst_dir_fd=dst_parent_fd)",
|
|
343
|
+
" except Exception:",
|
|
344
|
+
" if src_dir_fd is not None:",
|
|
345
|
+
" os.close(src_dir_fd)",
|
|
346
|
+
" if temp_dir_fd is not None:",
|
|
347
|
+
" os.close(temp_dir_fd)",
|
|
348
|
+
" try:",
|
|
349
|
+
" remove_tree(dst_parent_fd, temp_dir_name)",
|
|
350
|
+
" except FileNotFoundError:",
|
|
351
|
+
" pass",
|
|
352
|
+
" raise",
|
|
353
|
+
" remove_copied_entry(src_parent_fd, src_basename, ('dir', entry_identity(src_stat), copied_children))",
|
|
354
|
+
" os.fsync(dst_parent_fd)",
|
|
355
|
+
" os.fsync(src_parent_fd)",
|
|
356
|
+
" return",
|
|
357
|
+
" if stat.S_ISLNK(src_stat.st_mode):",
|
|
358
|
+
" link_target = os.readlink(src_basename, dir_fd=src_parent_fd)",
|
|
359
|
+
" temp_dir_name, temp_dir_fd = create_staging_dir(dst_parent_fd)",
|
|
360
|
+
" try:",
|
|
361
|
+
" os.symlink(link_target, 'link', dir_fd=temp_dir_fd)",
|
|
362
|
+
" os.replace('link', dst_basename, src_dir_fd=temp_dir_fd, dst_dir_fd=dst_parent_fd)",
|
|
363
|
+
" finally:",
|
|
364
|
+
" try:",
|
|
365
|
+
" try:",
|
|
366
|
+
" os.unlink('link', dir_fd=temp_dir_fd)",
|
|
367
|
+
" except FileNotFoundError:",
|
|
368
|
+
" pass",
|
|
369
|
+
" finally:",
|
|
370
|
+
" os.close(temp_dir_fd)",
|
|
371
|
+
" os.rmdir(temp_dir_name, dir_fd=dst_parent_fd)",
|
|
372
|
+
" os.unlink(src_basename, dir_fd=src_parent_fd)",
|
|
373
|
+
" os.fsync(dst_parent_fd)",
|
|
374
|
+
" os.fsync(src_parent_fd)",
|
|
375
|
+
" return",
|
|
376
|
+
" src_fd = os.open(src_basename, READ_FLAGS, dir_fd=src_parent_fd)",
|
|
377
|
+
" temp_fd = None",
|
|
378
|
+
" temp_name = None",
|
|
379
|
+
" try:",
|
|
380
|
+
" src_file_stat = os.fstat(src_fd)",
|
|
381
|
+
" if not stat.S_ISREG(src_file_stat.st_mode):",
|
|
382
|
+
" raise OSError(errno.EPERM, 'only regular files are allowed', src_basename)",
|
|
383
|
+
" if src_file_stat.st_nlink > 1:",
|
|
384
|
+
" raise OSError(errno.EPERM, 'hardlinked file is not allowed', src_basename)",
|
|
385
|
+
" temp_name, temp_fd = create_temp_file(dst_parent_fd)",
|
|
386
|
+
" while True:",
|
|
387
|
+
" chunk = os.read(src_fd, 65536)",
|
|
388
|
+
" if not chunk:",
|
|
389
|
+
" break",
|
|
390
|
+
" write_all(temp_fd, chunk)",
|
|
391
|
+
" try:",
|
|
392
|
+
" os.fchmod(temp_fd, stat.S_IMODE(src_stat.st_mode))",
|
|
393
|
+
" except AttributeError:",
|
|
394
|
+
" pass",
|
|
395
|
+
" os.fsync(temp_fd)",
|
|
396
|
+
" os.close(temp_fd)",
|
|
397
|
+
" temp_fd = None",
|
|
398
|
+
" os.replace(temp_name, dst_basename, src_dir_fd=dst_parent_fd, dst_dir_fd=dst_parent_fd)",
|
|
399
|
+
" temp_name = None",
|
|
400
|
+
" os.unlink(src_basename, dir_fd=src_parent_fd)",
|
|
401
|
+
" os.fsync(dst_parent_fd)",
|
|
402
|
+
" os.fsync(src_parent_fd)",
|
|
403
|
+
" finally:",
|
|
404
|
+
" if temp_fd is not None:",
|
|
405
|
+
" os.close(temp_fd)",
|
|
406
|
+
" if temp_name is not None:",
|
|
407
|
+
" try:",
|
|
408
|
+
" os.unlink(temp_name, dir_fd=dst_parent_fd)",
|
|
409
|
+
" except FileNotFoundError:",
|
|
410
|
+
" pass",
|
|
411
|
+
" os.close(src_fd)",
|
|
412
|
+
"",
|
|
413
|
+
"basename_indexes = (4, 7) if operation in ('copy', 'rename') else (4,) if operation in ('read', 'write', 'create', 'remove') else ()",
|
|
414
|
+
"for basename_index in basename_indexes:",
|
|
415
|
+
" basename = sys.argv[basename_index]",
|
|
416
|
+
" if not basename or basename in ('.', '..') or '/' in basename or '\\x00' in basename:",
|
|
417
|
+
" raise OSError(errno.EINVAL, 'invalid basename', basename)",
|
|
418
|
+
"",
|
|
419
|
+
GUEST_FILESYSTEM_DISPATCH_PYTHON,
|
|
420
|
+
].join("\n");
|
|
421
|
+
export { GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { FsSafeError, categorizeFsSafeError, type FsSafeErrorCategory, type FsSafeErrorCode, type FsSafeErrorDetails, } from "./errors.js";
|
|
2
|
-
export { DEFAULT_ROOT_MAX_BYTES, root, type DenyMutationPolicy, type HardlinkPolicy, type RenameIdentityPolicy, type OpenResult, type ReadResult, type Root, type RootAppendOptions, type RootCopyOptions, type RootCopySource, type RootCopyPublicationReceipt, type CopyCloneMode, type RootCreateJsonOptions, type RootCreateOptions, type RootDefaults, type RootMkdirOptions, type RootMoveOptions, type RootOpenOptions, type RootOpenWritableOptions, type RootOptions, type RootReadOptions, type RootRemoveOptions, type RootWriteJsonOptions, type RootWriteOptions, type RootWalkDataEntry, type RootWalkDataEntryKind, type RootWalkDirectoryErrorBehavior, type RootWalkEntry, type RootWalkEntryFilter, type RootWalkEntryFilterResult, type RootWalkEntryKind, type RootWalkLimitBehavior, type RootWalkOptions, type RootWalkSymlinkPolicy, type SymlinkPolicy, type MutationSymlinkPolicy, type WritableOpenMode, type WritableOpenResult, } from "./root.js";
|
|
2
|
+
export { DEFAULT_ROOT_MAX_BYTES, root, type DenyMutationPolicy, type HardlinkPolicy, type RenameIdentityPolicy, type OpenResult, type ReadResult, type Root, type RootAppendOptions, type RootCopyOptions, type RootCopySource, type RootCopyPublicationReceipt, type CopyCloneMode, type RootCreateJsonOptions, type RootCreateOptions, type RootCreateStreamOptions, type RootDefaults, type RootEntriesOptions, type RootMkdirOptions, type RootMoveOptions, type RootOpenOptions, type RootOpenWritableOptions, type RootOptions, type RootReadOptions, type RootRemoveOptions, type RootWriteJsonOptions, type RootWriteOptions, type RootWalkDataEntry, type RootWalkDataEntryKind, type RootWalkDirectoryErrorBehavior, type RootWalkEntry, type RootWalkEntryFilter, type RootWalkEntryFilterResult, type RootWalkEntryKind, type RootWalkLimitBehavior, type RootWalkOptions, type RootWalkSymlinkPolicy, type SymlinkPolicy, type MutationSymlinkPolicy, type WritableOpenMode, type WritableOpenResult, } from "./root.js";
|
|
3
3
|
export type { ContainmentGuarantee } from "./containment.js";
|
|
4
4
|
export { configureFsSafePython, configureFsSafeNative, getFsSafeNativeConfig, type FsSafePythonConfig, type FsSafeNativeConfig, type FsSafeNativeMode, } from "./native-config.js";
|
|
5
5
|
export { writeExternalFileWithinRoot, type ExternalFileWriteOptions, type ExternalFileWriteResult, } from "./output.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,sBAAsB,EACtB,IAAI,EACJ,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,0BAA0B,EAC/B,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,8BAA8B,EACnC,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,2BAA2B,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC7B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,sBAAsB,EACtB,IAAI,EACJ,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,0BAA0B,EAC/B,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,8BAA8B,EACnC,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,2BAA2B,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC7B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC"}
|
package/dist/install-path.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export declare function safeDirName(input: string): string;
|
|
2
|
+
/** Legacy readable encoding; distinct IDs can share a result. Use V2 for untrusted IDs. */
|
|
2
3
|
export declare function safePathSegmentHashed(input: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Versioned install-ID encoding: every trimmed ID receives a domain-separated
|
|
6
|
+
* SHA-256 digest. Only surrounding whitespace is intentionally equivalent.
|
|
7
|
+
*/
|
|
8
|
+
export declare function safePathSegmentHashedV2(input: string): string;
|
|
3
9
|
export declare function resolveSafeInstallDir(params: {
|
|
4
10
|
baseDir: string;
|
|
5
11
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install-path.d.ts","sourceRoot":"","sources":["../src/install-path.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"install-path.d.ts","sourceRoot":"","sources":["../src/install-path.ts"],"names":[],"mappings":"AAMA,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED,2FAA2F;AAC3F,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiB3D;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO7D;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAe5D;AAED,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDhB"}
|
package/dist/install-path.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { isPathInside } from "./path.js";
|
|
5
|
+
import { realpathSync } from "./realpath.js";
|
|
5
6
|
export function safeDirName(input) {
|
|
6
7
|
const trimmed = input.trim();
|
|
7
8
|
if (!trimmed) {
|
|
@@ -9,6 +10,7 @@ export function safeDirName(input) {
|
|
|
9
10
|
}
|
|
10
11
|
return trimmed.replaceAll("/", "__").replaceAll("\\", "__");
|
|
11
12
|
}
|
|
13
|
+
/** Legacy readable encoding; distinct IDs can share a result. Use V2 for untrusted IDs. */
|
|
12
14
|
export function safePathSegmentHashed(input) {
|
|
13
15
|
const trimmed = input.trim();
|
|
14
16
|
const base = trimmed
|
|
@@ -25,6 +27,18 @@ export function safePathSegmentHashed(input) {
|
|
|
25
27
|
}
|
|
26
28
|
return safe;
|
|
27
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Versioned install-ID encoding: every trimmed ID receives a domain-separated
|
|
32
|
+
* SHA-256 digest. Only surrounding whitespace is intentionally equivalent.
|
|
33
|
+
*/
|
|
34
|
+
export function safePathSegmentHashedV2(input) {
|
|
35
|
+
const hash = createHash("sha256")
|
|
36
|
+
.update("@openclaw/fs-safe:install-path:v2\0", "utf8")
|
|
37
|
+
// UTF-16LE preserves every JavaScript code unit, including lone surrogates.
|
|
38
|
+
.update(input.trim(), "utf16le")
|
|
39
|
+
.digest("hex");
|
|
40
|
+
return `id-v2-${hash}`;
|
|
41
|
+
}
|
|
28
42
|
export function resolveSafeInstallDir(params) {
|
|
29
43
|
const encodedName = (params.nameEncoder ?? safeDirName)(params.id);
|
|
30
44
|
const targetDir = path.join(params.baseDir, encodedName);
|
|
@@ -55,7 +69,7 @@ export async function assertCanonicalPathWithinBase(params) {
|
|
|
55
69
|
else if (!baseLstat.isDirectory()) {
|
|
56
70
|
throw new Error(`Invalid ${params.boundaryLabel}: base directory must be a directory`);
|
|
57
71
|
}
|
|
58
|
-
const baseRealPath =
|
|
72
|
+
const baseRealPath = realpathSync.native(baseDir);
|
|
59
73
|
const validateDirectory = async (dirPath) => {
|
|
60
74
|
const resolvedDirPath = path.resolve(dirPath);
|
|
61
75
|
const dirLstat = fsSync.lstatSync(dirPath);
|
|
@@ -71,7 +85,7 @@ export async function assertCanonicalPathWithinBase(params) {
|
|
|
71
85
|
else if (!dirLstat.isDirectory()) {
|
|
72
86
|
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
|
|
73
87
|
}
|
|
74
|
-
const dirRealPath =
|
|
88
|
+
const dirRealPath = realpathSync.native(dirPath);
|
|
75
89
|
if (!isPathInside(baseRealPath, dirRealPath)) {
|
|
76
90
|
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
|
|
77
91
|
}
|