@j0hanz/filesystem-mcp 1.19.0 → 2.0.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.
Files changed (301) hide show
  1. package/README.md +333 -838
  2. package/dist/cli-help.d.ts +3 -0
  3. package/dist/cli-help.d.ts.map +1 -0
  4. package/dist/cli-help.js +169 -0
  5. package/dist/cli-help.js.map +1 -0
  6. package/dist/cli.d.ts +28 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +168 -123
  9. package/dist/cli.js.map +1 -0
  10. package/dist/core/concurrency.d.ts +70 -0
  11. package/dist/core/concurrency.d.ts.map +1 -0
  12. package/dist/core/concurrency.js +173 -0
  13. package/dist/core/concurrency.js.map +1 -0
  14. package/dist/core/config.d.ts +19 -0
  15. package/dist/core/config.d.ts.map +1 -0
  16. package/dist/core/config.js +11 -0
  17. package/dist/core/config.js.map +1 -0
  18. package/dist/core/cursor.d.ts +23 -0
  19. package/dist/core/cursor.d.ts.map +1 -0
  20. package/dist/core/cursor.js +62 -0
  21. package/dist/core/cursor.js.map +1 -0
  22. package/dist/core/errors.d.ts +96 -0
  23. package/dist/core/errors.d.ts.map +1 -0
  24. package/dist/core/errors.js +356 -0
  25. package/dist/core/errors.js.map +1 -0
  26. package/dist/core/file-uri.d.ts +24 -0
  27. package/dist/core/file-uri.d.ts.map +1 -0
  28. package/dist/core/file-uri.js +52 -0
  29. package/dist/core/file-uri.js.map +1 -0
  30. package/dist/core/fmt.d.ts +38 -0
  31. package/dist/core/fmt.d.ts.map +1 -0
  32. package/dist/core/fmt.js +109 -0
  33. package/dist/core/fmt.js.map +1 -0
  34. package/dist/core/fs.d.ts +108 -0
  35. package/dist/core/fs.d.ts.map +1 -0
  36. package/dist/core/fs.js +243 -0
  37. package/dist/core/fs.js.map +1 -0
  38. package/dist/core/glob.d.ts +41 -0
  39. package/dist/core/glob.d.ts.map +1 -0
  40. package/dist/core/glob.js +399 -0
  41. package/dist/core/glob.js.map +1 -0
  42. package/dist/core/input-required.d.ts +147 -0
  43. package/dist/core/input-required.d.ts.map +1 -0
  44. package/dist/core/input-required.js +261 -0
  45. package/dist/core/input-required.js.map +1 -0
  46. package/dist/core/mime.d.ts +15 -0
  47. package/dist/core/mime.d.ts.map +1 -0
  48. package/dist/core/mime.js +188 -0
  49. package/dist/core/mime.js.map +1 -0
  50. package/dist/core/observability.d.ts +10 -0
  51. package/dist/core/observability.d.ts.map +1 -0
  52. package/dist/core/observability.js +76 -0
  53. package/dist/core/observability.js.map +1 -0
  54. package/dist/core/page-store.d.ts +41 -0
  55. package/dist/core/page-store.d.ts.map +1 -0
  56. package/dist/core/page-store.js +74 -0
  57. package/dist/core/page-store.js.map +1 -0
  58. package/dist/core/path-completer.d.ts +10 -0
  59. package/dist/core/path-completer.d.ts.map +1 -0
  60. package/dist/core/path-completer.js +227 -0
  61. package/dist/core/path-completer.js.map +1 -0
  62. package/dist/core/path-discovery.d.ts +8 -0
  63. package/dist/core/path-discovery.d.ts.map +1 -0
  64. package/dist/core/path-discovery.js +105 -0
  65. package/dist/core/path-discovery.js.map +1 -0
  66. package/dist/core/path-utils.d.ts +9 -0
  67. package/dist/core/path-utils.d.ts.map +1 -0
  68. package/dist/core/path-utils.js +121 -0
  69. package/dist/core/path-utils.js.map +1 -0
  70. package/dist/core/path.d.ts +148 -0
  71. package/dist/core/path.d.ts.map +1 -0
  72. package/dist/core/path.js +674 -0
  73. package/dist/core/path.js.map +1 -0
  74. package/dist/core/primitives.d.ts +23 -0
  75. package/dist/core/primitives.d.ts.map +1 -0
  76. package/dist/core/primitives.js +54 -0
  77. package/dist/core/primitives.js.map +1 -0
  78. package/dist/core/read.d.ts +74 -0
  79. package/dist/core/read.d.ts.map +1 -0
  80. package/dist/core/read.js +562 -0
  81. package/dist/core/read.js.map +1 -0
  82. package/dist/core/schema.d.ts +139 -0
  83. package/dist/core/schema.d.ts.map +1 -0
  84. package/dist/core/schema.js +345 -0
  85. package/dist/core/schema.js.map +1 -0
  86. package/dist/core/search.d.ts +100 -0
  87. package/dist/core/search.d.ts.map +1 -0
  88. package/dist/core/search.js +248 -0
  89. package/dist/core/search.js.map +1 -0
  90. package/dist/core/sensitive.d.ts +6 -0
  91. package/dist/core/sensitive.d.ts.map +1 -0
  92. package/dist/core/sensitive.js +142 -0
  93. package/dist/core/sensitive.js.map +1 -0
  94. package/dist/core/store.d.ts +57 -0
  95. package/dist/core/store.d.ts.map +1 -0
  96. package/dist/core/store.js +162 -0
  97. package/dist/core/store.js.map +1 -0
  98. package/dist/core/util.d.ts +26 -0
  99. package/dist/core/util.d.ts.map +1 -0
  100. package/dist/core/util.js +66 -0
  101. package/dist/core/util.js.map +1 -0
  102. package/dist/core/watcher-registry.d.ts +68 -0
  103. package/dist/core/watcher-registry.d.ts.map +1 -0
  104. package/dist/core/watcher-registry.js +312 -0
  105. package/dist/core/watcher-registry.js.map +1 -0
  106. package/dist/http-policy.d.ts +121 -0
  107. package/dist/http-policy.d.ts.map +1 -0
  108. package/dist/http-policy.js +340 -0
  109. package/dist/http-policy.js.map +1 -0
  110. package/dist/index.d.ts +1 -0
  111. package/dist/index.d.ts.map +1 -0
  112. package/dist/index.js +77 -41
  113. package/dist/index.js.map +1 -0
  114. package/dist/instructions.d.ts +12 -0
  115. package/dist/instructions.d.ts.map +1 -0
  116. package/dist/instructions.js +71 -0
  117. package/dist/instructions.js.map +1 -0
  118. package/dist/prompts.d.ts +8 -6
  119. package/dist/prompts.d.ts.map +1 -0
  120. package/dist/prompts.js +74 -168
  121. package/dist/prompts.js.map +1 -0
  122. package/dist/resources.d.ts +73 -9
  123. package/dist/resources.d.ts.map +1 -0
  124. package/dist/resources.js +392 -163
  125. package/dist/resources.js.map +1 -0
  126. package/dist/server.d.ts +62 -1
  127. package/dist/server.d.ts.map +1 -0
  128. package/dist/server.js +139 -1
  129. package/dist/server.js.map +1 -0
  130. package/dist/tools/batch.d.ts +89 -0
  131. package/dist/tools/batch.d.ts.map +1 -0
  132. package/dist/tools/batch.js +157 -0
  133. package/dist/tools/batch.js.map +1 -0
  134. package/dist/tools/create.d.ts +2 -0
  135. package/dist/tools/create.d.ts.map +1 -0
  136. package/dist/tools/create.js +142 -0
  137. package/dist/tools/create.js.map +1 -0
  138. package/dist/tools/define.d.ts +110 -0
  139. package/dist/tools/define.d.ts.map +1 -0
  140. package/dist/tools/define.js +394 -0
  141. package/dist/tools/define.js.map +1 -0
  142. package/dist/tools/delete-file.d.ts +2 -4
  143. package/dist/tools/delete-file.d.ts.map +1 -0
  144. package/dist/tools/delete-file.js +330 -87
  145. package/dist/tools/delete-file.js.map +1 -0
  146. package/dist/tools/diff.d.ts +2 -0
  147. package/dist/tools/diff.d.ts.map +1 -0
  148. package/dist/tools/diff.js +94 -0
  149. package/dist/tools/diff.js.map +1 -0
  150. package/dist/tools/edit.d.ts +2 -0
  151. package/dist/tools/edit.d.ts.map +1 -0
  152. package/dist/tools/edit.js +442 -0
  153. package/dist/tools/edit.js.map +1 -0
  154. package/dist/tools/index.d.ts +26 -0
  155. package/dist/tools/index.d.ts.map +1 -0
  156. package/dist/tools/index.js +52 -0
  157. package/dist/tools/index.js.map +1 -0
  158. package/dist/tools/list.d.ts +2 -0
  159. package/dist/tools/list.d.ts.map +1 -0
  160. package/dist/tools/list.js +291 -0
  161. package/dist/tools/list.js.map +1 -0
  162. package/dist/tools/move.d.ts +2 -0
  163. package/dist/tools/move.d.ts.map +1 -0
  164. package/dist/tools/move.js +321 -0
  165. package/dist/tools/move.js.map +1 -0
  166. package/dist/tools/patch.d.ts +2 -0
  167. package/dist/tools/patch.d.ts.map +1 -0
  168. package/dist/tools/patch.js +155 -0
  169. package/dist/tools/patch.js.map +1 -0
  170. package/dist/tools/progress.d.ts +52 -0
  171. package/dist/tools/progress.d.ts.map +1 -0
  172. package/dist/tools/progress.js +172 -0
  173. package/dist/tools/progress.js.map +1 -0
  174. package/dist/tools/read.d.ts +2 -4
  175. package/dist/tools/read.d.ts.map +1 -0
  176. package/dist/tools/read.js +398 -152
  177. package/dist/tools/read.js.map +1 -0
  178. package/dist/tools/replace-in-files.d.ts +2 -4
  179. package/dist/tools/replace-in-files.d.ts.map +1 -0
  180. package/dist/tools/replace-in-files.js +435 -264
  181. package/dist/tools/replace-in-files.js.map +1 -0
  182. package/dist/tools/roots.d.ts +2 -4
  183. package/dist/tools/roots.d.ts.map +1 -0
  184. package/dist/tools/roots.js +29 -52
  185. package/dist/tools/roots.js.map +1 -0
  186. package/dist/tools/search-content.d.ts +2 -4
  187. package/dist/tools/search-content.d.ts.map +1 -0
  188. package/dist/tools/search-content.js +262 -228
  189. package/dist/tools/search-content.js.map +1 -0
  190. package/dist/tools/search-files.d.ts +2 -4
  191. package/dist/tools/search-files.d.ts.map +1 -0
  192. package/dist/tools/search-files.js +162 -161
  193. package/dist/tools/search-files.js.map +1 -0
  194. package/dist/tools/stat.d.ts +2 -4
  195. package/dist/tools/stat.d.ts.map +1 -0
  196. package/dist/tools/stat.js +202 -62
  197. package/dist/tools/stat.js.map +1 -0
  198. package/dist/transport/http.d.ts +5 -0
  199. package/dist/transport/http.d.ts.map +1 -0
  200. package/dist/transport/http.js +276 -0
  201. package/dist/transport/http.js.map +1 -0
  202. package/dist/transport/shared.d.ts +45 -0
  203. package/dist/transport/shared.d.ts.map +1 -0
  204. package/dist/transport/shared.js +65 -0
  205. package/dist/transport/shared.js.map +1 -0
  206. package/dist/transport/stdio.d.ts +32 -0
  207. package/dist/transport/stdio.d.ts.map +1 -0
  208. package/dist/transport/stdio.js +279 -0
  209. package/dist/transport/stdio.js.map +1 -0
  210. package/dist/transport.d.ts +5 -0
  211. package/dist/transport.d.ts.map +1 -0
  212. package/dist/transport.js +4 -0
  213. package/dist/transport.js.map +1 -0
  214. package/package.json +37 -46
  215. package/dist/assets/logo.svg +0 -35
  216. package/dist/completions.d.ts +0 -2
  217. package/dist/completions.js +0 -572
  218. package/dist/config.d.ts +0 -119
  219. package/dist/config.js +0 -31
  220. package/dist/lib/abort.d.ts +0 -7
  221. package/dist/lib/abort.js +0 -81
  222. package/dist/lib/constants.d.ts +0 -42
  223. package/dist/lib/constants.js +0 -313
  224. package/dist/lib/errors.d.ts +0 -28
  225. package/dist/lib/errors.js +0 -301
  226. package/dist/lib/file-operations/core.d.ts +0 -53
  227. package/dist/lib/file-operations/core.js +0 -144
  228. package/dist/lib/file-operations/metadata.d.ts +0 -77
  229. package/dist/lib/file-operations/metadata.js +0 -878
  230. package/dist/lib/file-operations/search.d.ts +0 -46
  231. package/dist/lib/file-operations/search.js +0 -1194
  232. package/dist/lib/file-operations/traversal.d.ts +0 -40
  233. package/dist/lib/file-operations/traversal.js +0 -310
  234. package/dist/lib/fs-helpers.d.ts +0 -46
  235. package/dist/lib/fs-helpers.js +0 -606
  236. package/dist/lib/globs.d.ts +0 -2
  237. package/dist/lib/globs.js +0 -19
  238. package/dist/lib/logger.d.ts +0 -27
  239. package/dist/lib/logger.js +0 -91
  240. package/dist/lib/observability.d.ts +0 -33
  241. package/dist/lib/observability.js +0 -373
  242. package/dist/lib/paths.d.ts +0 -35
  243. package/dist/lib/paths.js +0 -634
  244. package/dist/lib/resource-store.d.ts +0 -28
  245. package/dist/lib/resource-store.js +0 -195
  246. package/dist/lib/utils.d.ts +0 -7
  247. package/dist/lib/utils.js +0 -37
  248. package/dist/lib/zod-codecs.d.ts +0 -2
  249. package/dist/lib/zod-codecs.js +0 -18
  250. package/dist/pkg-info.d.ts +0 -7
  251. package/dist/pkg-info.js +0 -9
  252. package/dist/resources/generated-instructions.d.ts +0 -1
  253. package/dist/resources/generated-instructions.js +0 -105
  254. package/dist/resources/tool-catalog.d.ts +0 -2
  255. package/dist/resources/tool-catalog.js +0 -78
  256. package/dist/resources/tool-info.d.ts +0 -9
  257. package/dist/resources/tool-info.js +0 -206
  258. package/dist/resources/workflows.d.ts +0 -1
  259. package/dist/resources/workflows.js +0 -72
  260. package/dist/schemas.d.ts +0 -505
  261. package/dist/schemas.js +0 -676
  262. package/dist/server/bootstrap.d.ts +0 -6
  263. package/dist/server/bootstrap.js +0 -563
  264. package/dist/server/event-store.d.ts +0 -18
  265. package/dist/server/event-store.js +0 -71
  266. package/dist/server/roots-manager.d.ts +0 -28
  267. package/dist/server/roots-manager.js +0 -227
  268. package/dist/server/task-store.d.ts +0 -10
  269. package/dist/server/task-store.js +0 -91
  270. package/dist/tools/apply-patch.d.ts +0 -4
  271. package/dist/tools/apply-patch.js +0 -235
  272. package/dist/tools/calculate-hash.d.ts +0 -4
  273. package/dist/tools/calculate-hash.js +0 -187
  274. package/dist/tools/contract.d.ts +0 -50
  275. package/dist/tools/contract.js +0 -1
  276. package/dist/tools/create-directory.d.ts +0 -4
  277. package/dist/tools/create-directory.js +0 -68
  278. package/dist/tools/diff-files.d.ts +0 -4
  279. package/dist/tools/diff-files.js +0 -136
  280. package/dist/tools/edit-file.d.ts +0 -4
  281. package/dist/tools/edit-file.js +0 -256
  282. package/dist/tools/icons.d.ts +0 -15
  283. package/dist/tools/icons.js +0 -24
  284. package/dist/tools/list-directory.d.ts +0 -4
  285. package/dist/tools/list-directory.js +0 -218
  286. package/dist/tools/move-file.d.ts +0 -4
  287. package/dist/tools/move-file.js +0 -178
  288. package/dist/tools/read-multiple.d.ts +0 -4
  289. package/dist/tools/read-multiple.js +0 -207
  290. package/dist/tools/shared.d.ts +0 -172
  291. package/dist/tools/shared.js +0 -599
  292. package/dist/tools/stat-many.d.ts +0 -4
  293. package/dist/tools/stat-many.js +0 -104
  294. package/dist/tools/task-support.d.ts +0 -29
  295. package/dist/tools/task-support.js +0 -472
  296. package/dist/tools/tree.d.ts +0 -4
  297. package/dist/tools/tree.js +0 -102
  298. package/dist/tools/write-file.d.ts +0 -4
  299. package/dist/tools/write-file.js +0 -60
  300. package/dist/tools.d.ts +0 -5
  301. package/dist/tools.js +0 -54
@@ -0,0 +1,674 @@
1
+ import { lstat, readlink, realpath, stat } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path';
4
+ import { timedSignal, withAbort } from './concurrency.js';
5
+ import { cli } from './config.js';
6
+ import { ERRNO_MAP, ErrorCode, formatUnknownErrorMessage, FsError, isFsError, isNodeError, isNotFoundErrno, rethrowIfAborted, SKIPPABLE_ERRNOS, SKIPPABLE_FS_CODES, } from './errors.js';
7
+ import { Logger } from './observability.js';
8
+ import { findProjectRoot, isUnsafeCwdPath, resolveConfiguredDirs } from './path-discovery.js';
9
+ import { getReservedDeviceNameForPath, isPathWithinDirectories, isSamePath, isWindowsDriveRelativePath, normalizeAllowedDirectory, normalizePath, } from './path-utils.js';
10
+ import { isSlash, parseTrueEnvFlag, toPosixPath } from './primitives.js';
11
+ import { SensitiveMatcher } from './sensitive.js';
12
+ import { ROOTS_TIMEOUT_MS } from './util.js';
13
+ // Re-exported for path-completer.ts, the one consumer that reaches these
14
+ // through this module rather than through primitives.ts directly.
15
+ export { isSlash, toPosixPath };
16
+ /** True when `normalizedRoot` really resolves inside `bounds` (FS_ROOT_BOUNDARY). */
17
+ async function isRootWithin(normalizedRoot, bounds, label, signal) {
18
+ try {
19
+ signal?.throwIfAborted();
20
+ const realPath = await withAbort(realpath(normalizedRoot), signal);
21
+ return isPathWithinDirectories(normalizePath(realPath), bounds);
22
+ }
23
+ catch (error) {
24
+ rethrowIfAborted(error);
25
+ if (isNotFoundErrno(error)) {
26
+ return false;
27
+ }
28
+ Logger.warn(`${label}: realpath failed unexpectedly`, {
29
+ root: normalizedRoot,
30
+ error: String(error),
31
+ });
32
+ return false;
33
+ }
34
+ }
35
+ async function filterRootsWithin(roots, bounds, label, signal) {
36
+ const normalizedBounds = normalizeAllowedDirectories(bounds);
37
+ const normalizedRoots = roots.map(normalizePath);
38
+ if (normalizedRoots.length === 0) {
39
+ return [];
40
+ }
41
+ const results = await Promise.allSettled(normalizedRoots.map((root) => isRootWithin(root, normalizedBounds, label, signal)));
42
+ return normalizedRoots.filter((root, i) => {
43
+ const result = results[i];
44
+ if (result?.status === 'rejected') {
45
+ Logger.warn(`${label}: root check threw unexpectedly`, {
46
+ root,
47
+ error: String(result.reason),
48
+ });
49
+ return false;
50
+ }
51
+ return result?.status === 'fulfilled' && result.value;
52
+ });
53
+ }
54
+ /** `path.relative` with forward slashes, so displayed paths match across platforms. */
55
+ export function toPosixRelative(from, to) {
56
+ return toPosixPath(relative(from, to));
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // Resolver pipeline
60
+ // ---------------------------------------------------------------------------
61
+ export function normalizeAllowedDirectories(dirs) {
62
+ const normalized = [];
63
+ for (const dir of dirs) {
64
+ const entry = normalizeAllowedDirectory(dir);
65
+ if (entry.length > 0) {
66
+ normalized.push(entry);
67
+ }
68
+ }
69
+ return [...new Set(normalized)];
70
+ }
71
+ export async function resolveRealPath(normalized, signal) {
72
+ try {
73
+ signal?.throwIfAborted();
74
+ const realPath = await withAbort(realpath(normalized), signal);
75
+ return normalizeAllowedDirectory(realPath);
76
+ }
77
+ catch (error) {
78
+ rethrowIfAborted(error);
79
+ // Only suppress ENOENT — the path genuinely does not exist.
80
+ // EACCES, EIO, and other unexpected errors are rethrown so callers
81
+ // cannot silently operate with a narrowed allowed-directory set.
82
+ if (isNotFoundErrno(error)) {
83
+ return null;
84
+ }
85
+ throw error;
86
+ }
87
+ }
88
+ async function expandAllowedDirectories(primaryDirs, signal) {
89
+ const realPaths = await Promise.all(primaryDirs.map((dir) => resolveRealPath(dir, signal)));
90
+ const expanded = [];
91
+ for (let i = 0; i < primaryDirs.length; i++) {
92
+ const primary = primaryDirs[i];
93
+ if (!primary) {
94
+ continue;
95
+ }
96
+ expanded.push(primary);
97
+ const real = realPaths[i];
98
+ if (real && !isSamePath(real, primary)) {
99
+ expanded.push(real);
100
+ }
101
+ }
102
+ return [...new Set(expanded)];
103
+ }
104
+ async function resolveAllowedDirectoriesState(dirs, signal) {
105
+ const primary = normalizeAllowedDirectories(dirs);
106
+ const expanded = await expandAllowedDirectories(primary, signal);
107
+ return { primary, expanded };
108
+ }
109
+ // ---------------------------------------------------------------------------
110
+ // Windows helpers
111
+ // ---------------------------------------------------------------------------
112
+ /**
113
+ * Accepted risk: validation methods resolve/verify a path (symlinks, boundaries,
114
+ * sensitivity) and then return a plain string; the actual fs operation happens
115
+ * afterward as a separate syscall (classic TOCTOU). A symlink swapped in that
116
+ * exact window could redirect the follow-up operation. This is mitigated by
117
+ * re-validating the resolved real path (not just the requested path) and by
118
+ * walking ancestors to catch escapes before the target exists, but it is not
119
+ * eliminated — doing so would require fd-based operations (open with
120
+ * O_NOFOLLOW / operate on the resolved fd) throughout core/fs.ts. Acceptable
121
+ * tradeoff for a local, single-user filesystem server today.
122
+ */
123
+ export class PathGuard {
124
+ allowedDirectoriesState;
125
+ sensitive = new SensitiveMatcher();
126
+ /**
127
+ * Directories added by an accepted access grant (R8) — from the tool
128
+ * executor's grant round-trip, or from a client's declared workspace roots on
129
+ * the legacy stdio leg. Held separately from the configured baseline because
130
+ * `recomputeAllowedDirectories` rebuilds that baseline from CLI args and env
131
+ * on every call: anything merged into it would be recomputed away.
132
+ *
133
+ * These are NOT filtered against the baseline. A granted directory is
134
+ * out-of-baseline by definition, so a baseline filter dropped every one while
135
+ * `applyGrant` still reported success — the whole round-trip prompted the
136
+ * user and then changed nothing. FS_ROOT_BOUNDARY and the unsafe-path denylist
137
+ * remain the limits, both checked in `applyGrant` — the only writer of this
138
+ * field — so the recompute can rebuild from it without re-filtering.
139
+ */
140
+ grantedDirectories = [];
141
+ rootBoundaries = [];
142
+ // ponytail: one mutex per PathGuard. If per-session grant throughput ever
143
+ // matters, split into per-grant-dir locks; a single lock is correct for the
144
+ // stdio + InMemoryEventStore single-process model.
145
+ #mutex = Promise.resolve();
146
+ options;
147
+ /**
148
+ * True when this guard backs a live MCP server rather than a one-shot CLI
149
+ * invocation. Operator-facing configuration warnings are suppressed unless
150
+ * it is set, so `--print-config` and unit construction stay quiet.
151
+ */
152
+ isServerContext;
153
+ constructor(options, isServerContext = false) {
154
+ this.options = options;
155
+ this.isServerContext = isServerContext;
156
+ }
157
+ static async fromAllowedDirectories(dirs, signal) {
158
+ const state = await resolveAllowedDirectoriesState(dirs, signal);
159
+ const guard = new PathGuard();
160
+ guard.initialize(state);
161
+ return guard;
162
+ }
163
+ initialize(state) {
164
+ this.allowedDirectoriesState = {
165
+ primary: [...new Set(state.primary)],
166
+ expanded: normalizeAllowedDirectories(state.expanded),
167
+ };
168
+ }
169
+ isInitialized() {
170
+ return this.allowedDirectoriesState !== undefined;
171
+ }
172
+ /**
173
+ * Run `fn` as the next holder of the guard's single mutation lock. Every
174
+ * change to the allowed set goes through this so concurrent grants cannot
175
+ * interleave their read-`await`-write and lose a grant (GRANT-1).
176
+ */
177
+ async runExclusive(fn) {
178
+ const result = this.#mutex.then(fn, fn);
179
+ this.#mutex = result.then(() => undefined, () => undefined);
180
+ return result;
181
+ }
182
+ getAllowedDirectories() {
183
+ if (!this.allowedDirectoriesState) {
184
+ return [];
185
+ }
186
+ return [...this.allowedDirectoriesState.expanded];
187
+ }
188
+ getRootBoundaries() {
189
+ return [...this.rootBoundaries];
190
+ }
191
+ isSensitive(filePath) {
192
+ return this.sensitive.isSensitive(filePath);
193
+ }
194
+ /**
195
+ * True when `entryPath` is both within the guard's allowed directories and
196
+ * not sensitive, checking the requested AND resolved paths. EVERY entry is
197
+ * realpath-resolved via validateExistingPathDetailed: a symlinked ancestor
198
+ * directory pointing outside the sandbox would otherwise pass the lexical
199
+ * containment check (fs.glob follows symlinks and yields external entries as
200
+ * non-symlink dirents). validateExistingPathDetailed re-checks containment on
201
+ * the real path against this.allowedDirectoriesState.expanded (the guard's full
202
+ * allowed set — a superset of the single-root `bounds` callers pass) and
203
+ * re-checks sensitivity on the resolved target, throwing ACCESS_DENIED for
204
+ * escapes/sensitive, which the catch below turns into a filter. `_entryType`
205
+ * and `_bounds` are retained for call-site compatibility while the guard's own
206
+ * allowed set is the real gate. Skippable errno/fs errors return false (the
207
+ * entry is filtered, not fatal). The accepted TOCTOU window is documented at
208
+ * the class docstring above.
209
+ */
210
+ async isEntryAccessible(entryPath, _entryType, _bounds) {
211
+ const isSensitive = (requestedPath, resolvedPath) => this.isSensitive(requestedPath) || this.isSensitive(resolvedPath);
212
+ try {
213
+ const validated = await this.validateExistingPathDetailed(entryPath);
214
+ return !isSensitive(validated.requestedPath, validated.resolvedPath);
215
+ }
216
+ catch (error) {
217
+ if (isFsError(error)) {
218
+ if (SKIPPABLE_FS_CODES.has(error.code))
219
+ return false;
220
+ throw error;
221
+ }
222
+ if (isNodeError(error) && error.code !== undefined && SKIPPABLE_ERRNOS.has(error.code))
223
+ return false;
224
+ throw error;
225
+ }
226
+ }
227
+ async validateExistingPath(requestedPath) {
228
+ const details = await this.validateExistingPathDetailed(requestedPath);
229
+ return details.resolvedPath;
230
+ }
231
+ /** True when a normalized path equals its own filesystem root (`C:\`, `/`). */
232
+ isFilesystemRoot(normalizedPath) {
233
+ return isSamePath(normalizedPath, parse(normalizedPath).root);
234
+ }
235
+ /**
236
+ * True when a grant must never admit `targetDir` — a bare filesystem root, or
237
+ * an unsafe path (home, /etc, C:\Windows, ...). Checked on the lexical path
238
+ * AND on the one it resolves to: `expandAllowedDirectories` pushes each root's
239
+ * realpath into the allowed set, so a lexical-only check let a symlink or
240
+ * junction aliasing $HOME/C:\Windows in under an innocuous name — and the
241
+ * confirmation prompt showed the alias, not the target. A target that cannot
242
+ * be resolved is judged lexically; it does not exist to escape into.
243
+ */
244
+ async isUnsafeGrantTarget(targetDir) {
245
+ const isRefused = (dir) => this.isFilesystemRoot(dir) || isUnsafeCwdPath(dir);
246
+ const normalized = normalizePath(targetDir);
247
+ if (isRefused(normalized))
248
+ return true;
249
+ let resolved;
250
+ try {
251
+ resolved = normalizePath(await realpath(normalized));
252
+ }
253
+ catch {
254
+ return false;
255
+ }
256
+ return !isSamePath(resolved, normalized) && isRefused(resolved);
257
+ }
258
+ /** Walk up from a blocked path to the closest existing ancestor directory. */
259
+ async resolveGrantTargetDir(blockedPath) {
260
+ let targetDir = blockedPath;
261
+ for (;;) {
262
+ const parent = dirname(targetDir);
263
+ try {
264
+ // An existing directory is the grant target; an existing file grants
265
+ // its parent directory.
266
+ return (await stat(targetDir)).isDirectory() ? targetDir : parent;
267
+ }
268
+ catch {
269
+ // Missing — keep walking up.
270
+ }
271
+ if (parent === targetDir)
272
+ return targetDir;
273
+ targetDir = parent;
274
+ }
275
+ }
276
+ /**
277
+ * Pre-check (no mutation, no callback): given the paths a tool is about to
278
+ * operate on, return the sorted, de-duplicated grant-target directories for
279
+ * those that are outside the allowed roots AND grantable (within FS_ROOT_BOUNDARY
280
+ * when it is configured). A path whose nearest existing ancestor escapes the
281
+ * boundary is NOT grantable and is omitted — the operation will fail with
282
+ * ACCESS_DENIED for it rather than prompt. The caller returns an
283
+ * `input_required` result carrying exactly this set (R7), and on retry applies
284
+ * each accepted grant via {@link applyGrant} (R8). `requestState` binds this
285
+ * set so a grant accepted for X cannot authorize Y (R9).
286
+ */
287
+ async precheckAccess(paths) {
288
+ if (!this.allowedDirectoriesState || paths.length === 0) {
289
+ return [];
290
+ }
291
+ const allowedDirs = this.allowedDirectoriesState.expanded;
292
+ const grantDirs = [];
293
+ for (const requested of paths) {
294
+ if (!requested) {
295
+ continue;
296
+ }
297
+ const normalized = normalizePath(requested);
298
+ if (isPathWithinDirectories(normalized, allowedDirs)) {
299
+ continue;
300
+ }
301
+ const targetDir = normalizePath(await this.resolveGrantTargetDir(normalized));
302
+ // Never offer a grant into a bare filesystem root or an unsafe path (home,
303
+ // /etc, C:\Windows, ...). Without FS_ROOT_BOUNDARY, isWithinBoundary returns
304
+ // true for everything, so this is the one guard that still rejects roots a
305
+ // misleading grant could reach. Mirrors the check gating --allow-cwd.
306
+ if (await this.isUnsafeGrantTarget(targetDir)) {
307
+ continue;
308
+ }
309
+ if (grantDirs.includes(targetDir)) {
310
+ continue;
311
+ }
312
+ if (!(await this.isWithinBoundary(targetDir))) {
313
+ continue;
314
+ }
315
+ grantDirs.push(targetDir);
316
+ }
317
+ return grantDirs.sort();
318
+ }
319
+ /**
320
+ * Apply an accepted access grant: enforce FS_ROOT_BOUNDARY again (a TOCTOU
321
+ * re-check against the boundary resolved at config time), then extend the
322
+ * allowed roots for the remainder of the session (R8, A4). Returns false when
323
+ * the boundary or the unsafe-path denylist blocks the grant, AND when the
324
+ * recompute did not actually admit the directory — the return value now
325
+ * reports what the guard's allowed set really holds, not merely that the
326
+ * pre-checks passed. The caller leaves a refused path to fail with
327
+ * ACCESS_DENIED during the operation. Idempotent: re-granting an already
328
+ * allowed directory is a no-op via the dedup in `initialize`.
329
+ */
330
+ async applyGrant(targetDir) {
331
+ // Defense-in-depth: even a tampered/accepted grant cannot extend roots into
332
+ // an unsafe path. The precheckAccess guard already refuses to offer these;
333
+ // this catches a grant that arrived by another route — the legacy stdio
334
+ // `roots/list` seeding, which has no confirmation round-trip at all.
335
+ if (await this.isUnsafeGrantTarget(targetDir)) {
336
+ return false;
337
+ }
338
+ if (!(await this.isWithinBoundary(targetDir))) {
339
+ return false;
340
+ }
341
+ // Read + write under the mutation lock so a concurrent grant cannot
342
+ // interleave and lose this grant (GRANT-1). runExclusive is not reentrant:
343
+ // nothing in this body may take the lock again.
344
+ return this.runExclusive(async () => {
345
+ const previous = this.grantedDirectories;
346
+ this.grantedDirectories = [...previous, normalizePath(targetDir)];
347
+ try {
348
+ await this.recomputeAllowedDirectories();
349
+ }
350
+ catch (error) {
351
+ // A failed recompute leaves the guard with its previous, consistent view.
352
+ this.grantedDirectories = previous;
353
+ throw error;
354
+ }
355
+ // Verify rather than assume: a boundary that no longer covers this
356
+ // directory would drop it in the recompute above, and reporting success
357
+ // for a grant that did not land is exactly the failure this replaces.
358
+ if (isPathWithinDirectories(normalizePath(targetDir), this.getAllowedDirectories())) {
359
+ return true;
360
+ }
361
+ this.grantedDirectories = previous;
362
+ await this.recomputeAllowedDirectories();
363
+ return false;
364
+ });
365
+ }
366
+ // The guard already realpath-resolved FS_ROOT_BOUNDARY into rootBoundaries
367
+ // during recomputeAllowedDirectories. Reuse that single source of truth so
368
+ // grant paths check the same boundary the rest of the guard enforces,
369
+ // instead of re-reading the env and re-resolving each entry.
370
+ async isWithinBoundary(targetDir) {
371
+ if (this.rootBoundaries.length === 0) {
372
+ return true;
373
+ }
374
+ let resolved;
375
+ try {
376
+ resolved = normalizePath(await realpath(targetDir));
377
+ }
378
+ catch {
379
+ resolved = normalizePath(targetDir);
380
+ }
381
+ return isPathWithinDirectories(resolved, this.rootBoundaries);
382
+ }
383
+ validateAccessAndSensitivity(requestedPath) {
384
+ const result = this.validateAccess(requestedPath);
385
+ this.assertNotSensitiveFile(requestedPath, requestedPath);
386
+ this.assertNotSensitiveFile(result.normalizedRequested, requestedPath);
387
+ return result;
388
+ }
389
+ // Synchronous since the access-grant round-trip moved to the executor's
390
+ // pre-check: validateAccess only does lexical containment math and throws,
391
+ // no async I/O remains. Callers still `await` it for uniform control-flow;
392
+ // awaiting a non-thenable returns it unchanged.
393
+ validateAccess(requestedPath) {
394
+ if (!this.allowedDirectoriesState) {
395
+ throw new FsError(ErrorCode.UNKNOWN, 'PathGuard not initialized. Call initialize() first.', requestedPath);
396
+ }
397
+ if (isWindowsDriveRelativePath(requestedPath)) {
398
+ throw new FsError(ErrorCode.INVALID_INPUT, 'Drive-relative paths are not allowed. Use C:\\path or C:/path instead of C:path.', requestedPath);
399
+ }
400
+ const reservedDevice = getReservedDeviceNameForPath(requestedPath);
401
+ if (reservedDevice) {
402
+ throw new FsError(ErrorCode.ACCESS_DENIED, `Reserved Windows device name not allowed: ${reservedDevice}.`, requestedPath);
403
+ }
404
+ const normalizedRequested = normalizePath(requestedPath);
405
+ const allowedDirs = this.allowedDirectoriesState.expanded;
406
+ const accessDeniedHint = allowedDirs.length > 0
407
+ ? `Allowed: ${allowedDirs.join(', ')}`
408
+ : 'No allowed directories configured.';
409
+ if (!isPathWithinDirectories(normalizedRequested, allowedDirs)) {
410
+ // Out of root. The access-grant `input_required` round-trip is driven by
411
+ // the executor's pre-check (precheckAccess) BEFORE the operation runs, so
412
+ // by the time validation reaches here any grantable out-of-root path has
413
+ // already been accepted and added to the allowed set. A path still out of
414
+ // root here was either declined, ungrantable (outside FS_ROOT_BOUNDARY), or
415
+ // never pre-checked — fail closed.
416
+ this.throwAccessDenied(requestedPath, accessDeniedHint);
417
+ }
418
+ return {
419
+ normalizedRequested,
420
+ allowedDirs: this.allowedDirectoriesState.expanded,
421
+ accessDeniedHint,
422
+ };
423
+ }
424
+ async handleRealpathError(error, normalizedRequested, allowedDirs, accessDeniedHint, requestedPath) {
425
+ if (isNotFoundErrno(error)) {
426
+ // Resolve the nearest existing ancestor to detect out-of-sandbox symlinks.
427
+ // e.g. if `link -> C:\external` and path is `link\nonexistent.txt`, the
428
+ // ancestor resolves outside allowed dirs → ACCESS_DENIED, not NOT_FOUND.
429
+ try {
430
+ const { realAncestor, resolvedTarget } = await this.resolveNearestExistingAncestor(requestedPath, normalizedRequested);
431
+ if (!isPathWithinDirectories(realAncestor, allowedDirs) ||
432
+ !isPathWithinDirectories(resolvedTarget, allowedDirs)) {
433
+ this.throwAccessDenied(requestedPath, accessDeniedHint);
434
+ }
435
+ }
436
+ catch (ancestorErr) {
437
+ // Rethrow any FsError — collapsing e.g. UNKNOWN to NOT_FOUND would mask
438
+ // incomplete sandbox checks and make bugs invisible to callers.
439
+ if (isFsError(ancestorErr)) {
440
+ throw ancestorErr;
441
+ }
442
+ }
443
+ throw new FsError(ErrorCode.NOT_FOUND, 'Path not found', requestedPath, { originalError: error.message }, error);
444
+ }
445
+ const mapped = isNodeError(error) && error.code !== undefined ? ERRNO_MAP[error.code] : undefined;
446
+ throw new FsError(mapped ?? ErrorCode.UNKNOWN, 'Cannot access path', requestedPath, {
447
+ originalError: formatUnknownErrorMessage(error),
448
+ }, error instanceof Error ? error : undefined);
449
+ }
450
+ async validateExistingPathDetailed(requestedPath) {
451
+ const { normalizedRequested, allowedDirs, accessDeniedHint } = this.validateAccessAndSensitivity(requestedPath);
452
+ let realPath;
453
+ try {
454
+ realPath = await realpath(normalizedRequested);
455
+ }
456
+ catch (error) {
457
+ realPath = await this.handleRealpathError(error, normalizedRequested, allowedDirs, accessDeniedHint, requestedPath);
458
+ }
459
+ const normalizedReal = normalizePath(realPath);
460
+ if (!isPathWithinDirectories(normalizedReal, allowedDirs)) {
461
+ this.throwAccessDenied(requestedPath, accessDeniedHint);
462
+ }
463
+ // Re-check the resolved real path: a symlink inside an allowed root may
464
+ // point at a sensitive file (e.g. link -> .env). The early check above only
465
+ // sees the requested/normalized path, not the symlink target.
466
+ this.assertNotSensitiveFile(normalizedReal, requestedPath);
467
+ return {
468
+ requestedPath: normalizedRequested,
469
+ resolvedPath: normalizedReal,
470
+ isSymlink: !isSamePath(normalizedRequested, normalizedReal),
471
+ };
472
+ }
473
+ async validateExistingDirectory(requestedPath) {
474
+ const details = await this.validateExistingPathDetailed(requestedPath);
475
+ let stats;
476
+ try {
477
+ stats = await stat(details.resolvedPath);
478
+ }
479
+ catch (error) {
480
+ throw new FsError(ErrorCode.UNKNOWN, 'Cannot access directory', requestedPath, {
481
+ originalError: formatUnknownErrorMessage(error),
482
+ }, error instanceof Error ? error : undefined);
483
+ }
484
+ if (!stats.isDirectory()) {
485
+ throw new FsError(ErrorCode.NOT_DIRECTORY, 'Not a directory', requestedPath);
486
+ }
487
+ return details.resolvedPath;
488
+ }
489
+ resolvePathOrRoot(pathValue) {
490
+ if (pathValue && pathValue.trim().length > 0) {
491
+ return pathValue;
492
+ }
493
+ const roots = this.getAllowedDirectories();
494
+ if (roots.length === 0) {
495
+ throw new FsError(ErrorCode.ACCESS_DENIED, 'No roots configured. Use the roots tool or --allow-cwd.');
496
+ }
497
+ if (roots.length > 1) {
498
+ throw new FsError(ErrorCode.INVALID_INPUT, 'Multiple roots configured. Provide an explicit path.');
499
+ }
500
+ const root = roots[0];
501
+ if (!root) {
502
+ throw new FsError(ErrorCode.ACCESS_DENIED, 'Workspace root is unexpectedly undefined');
503
+ }
504
+ return root;
505
+ }
506
+ isAllowedRoot(normalizedPath) {
507
+ // isSamePath case-folds on case-insensitive filesystems (win + darwin),
508
+ // matching the containment checks used everywhere else in the guard. The
509
+ // previous IS_WINDOWS-only fold left darwin doing exact-case compares here
510
+ // while isPathInsideDirectory folded — a root matched case-insensitively
511
+ // everywhere else was missed.
512
+ return this.getAllowedDirectories().some((dir) => isSamePath(dir, normalizedPath));
513
+ }
514
+ // Checks ONLY the sensitive-file denylist. Root containment and symlink
515
+ // resolution must be verified separately (e.g. via validateExistingPath).
516
+ assertNotSensitiveFile(checkPath, requestedPath) {
517
+ if (this.isSensitive(checkPath)) {
518
+ throw new FsError(ErrorCode.ACCESS_DENIED, 'Sensitive file blocked. Set ALLOW_SENSITIVE=1 to override.', requestedPath);
519
+ }
520
+ }
521
+ throwAccessDenied(requestedPath, hint) {
522
+ throw new FsError(ErrorCode.ACCESS_DENIED, hint ? `Outside allowed directories. ${hint}` : 'Outside allowed directories.', requestedPath);
523
+ }
524
+ async resolveNearestExistingAncestor(requestedPath, currentPath) {
525
+ const missingSegments = [];
526
+ let current = currentPath;
527
+ for (;;) {
528
+ try {
529
+ const realAncestor = normalizePath(await realpath(current));
530
+ const resolvedTarget = missingSegments.length === 0
531
+ ? realAncestor
532
+ : normalizePath(join(realAncestor, ...missingSegments.reverse()));
533
+ return { realAncestor, resolvedTarget };
534
+ }
535
+ catch (error) {
536
+ try {
537
+ const stats = await lstat(current);
538
+ if (stats.isSymbolicLink()) {
539
+ const target = await readlink(current);
540
+ const resolvedTarget = isAbsolute(target) ? target : resolve(dirname(current), target);
541
+ const normalizedTarget = normalizePath(resolvedTarget);
542
+ const allowedDirs = this.getAllowedDirectories();
543
+ if (!isPathWithinDirectories(normalizedTarget, allowedDirs)) {
544
+ this.throwAccessDenied(requestedPath);
545
+ }
546
+ }
547
+ }
548
+ catch (lstatErr) {
549
+ if (isFsError(lstatErr) && lstatErr.code === ErrorCode.ACCESS_DENIED) {
550
+ throw lstatErr;
551
+ }
552
+ // ENOENT is expected during ancestor walk — the entry simply doesn't exist.
553
+ // Any other error (EACCES, EIO, ELOOP) is unexpected; fail safe.
554
+ if (!isNodeError(lstatErr) || lstatErr.code !== 'ENOENT') {
555
+ throw new FsError(ErrorCode.UNKNOWN, 'Cannot probe symlink ancestor', requestedPath, { originalError: formatUnknownErrorMessage(lstatErr) }, lstatErr instanceof Error ? lstatErr : undefined);
556
+ }
557
+ }
558
+ const parent = dirname(current);
559
+ if (parent === current) {
560
+ throw new FsError(ErrorCode.UNKNOWN, 'Cannot resolve path', requestedPath, { originalError: formatUnknownErrorMessage(error) }, error instanceof Error ? error : undefined);
561
+ }
562
+ missingSegments.push(basename(current));
563
+ current = parent;
564
+ }
565
+ }
566
+ }
567
+ async validatePathForWrite(requestedPath) {
568
+ const { normalizedRequested, allowedDirs, accessDeniedHint } = this.validateAccessAndSensitivity(requestedPath);
569
+ const { realAncestor, resolvedTarget } = await this.resolveNearestExistingAncestor(requestedPath, normalizedRequested);
570
+ if (!isPathWithinDirectories(realAncestor, allowedDirs) ||
571
+ !isPathWithinDirectories(resolvedTarget, allowedDirs)) {
572
+ this.throwAccessDenied(requestedPath, accessDeniedHint);
573
+ }
574
+ // Re-check the resolved target: a symlink inside an allowed root may point
575
+ // at a sensitive file. Writing through such a link must be blocked too.
576
+ this.assertNotSensitiveFile(resolvedTarget, requestedPath);
577
+ return resolvedTarget;
578
+ }
579
+ async validatePathForDelete(requestedPath) {
580
+ const { normalizedRequested, allowedDirs, accessDeniedHint } = this.validateAccessAndSensitivity(requestedPath);
581
+ // A workspace root is refused here, before the parent-containment check
582
+ // below reaches for a parent that is out-of-root by construction and
583
+ // reports "Outside allowed directories" for the one directory the caller
584
+ // can see IS allowed. delete-file.ts keeps its own root check for the
585
+ // nested-root case, where the parent is itself an allowed root and this
586
+ // path is never taken.
587
+ if (this.isAllowedRoot(normalizedRequested)) {
588
+ throw new FsError(ErrorCode.ACCESS_DENIED, 'Deleting a workspace root directory is not allowed', requestedPath);
589
+ }
590
+ const parent = dirname(normalizedRequested);
591
+ let realParent;
592
+ try {
593
+ realParent = await realpath(parent);
594
+ }
595
+ catch (error) {
596
+ throw new FsError(ErrorCode.NOT_FOUND, 'Parent directory not found', requestedPath, { originalError: formatUnknownErrorMessage(error) }, error instanceof Error ? error : undefined);
597
+ }
598
+ const normalizedRealParent = normalizePath(realParent);
599
+ if (!isPathWithinDirectories(normalizedRealParent, allowedDirs)) {
600
+ this.throwAccessDenied(requestedPath, accessDeniedHint);
601
+ }
602
+ // Resolve the final component when it exists. For deletion, we ONLY
603
+ // block if the real target is outside the sandbox IF the target is
604
+ // NOT a symlink. Deleting a symlink is safe even if it points outside.
605
+ try {
606
+ const stats = await lstat(normalizedRequested);
607
+ if (stats.isSymbolicLink()) {
608
+ // Symlink: check link sensitivity but don't resolve target.
609
+ // The parent check above ensures the link itself is in an allowed root.
610
+ this.assertNotSensitiveFile(normalizedRequested, requestedPath);
611
+ return normalizedRequested;
612
+ }
613
+ // Not a symlink: resolve to catch path escapes (e.g. /allowed/dir/../../etc)
614
+ // and block sensitive files.
615
+ const realTarget = normalizePath(await realpath(normalizedRequested));
616
+ if (!isPathWithinDirectories(realTarget, allowedDirs)) {
617
+ this.throwAccessDenied(requestedPath, accessDeniedHint);
618
+ }
619
+ this.assertNotSensitiveFile(realTarget, requestedPath);
620
+ return realTarget;
621
+ }
622
+ catch (error) {
623
+ // A denial raised inside this block (out-of-root real target, or a
624
+ // sensitive file reached through a symlinked parent) must propagate.
625
+ // Only a probe failure — ENOENT and friends — falls through to the
626
+ // parent check, which is sufficient for a path that does not exist.
627
+ if (isFsError(error)) {
628
+ throw error;
629
+ }
630
+ }
631
+ return normalizedRequested;
632
+ }
633
+ async recomputeAllowedDirectories() {
634
+ const cliAllowedDirs = normalizeAllowedDirectories(this.options?.cliAllowedDirs ?? []);
635
+ // Parse allowed directories from environment variable
636
+ const allowMissing = cli.allowMissingRoots ??
637
+ parseTrueEnvFlag(process.env['FS_ALLOW_MISSING_ROOTS'], 'FS_ALLOW_MISSING_ROOTS');
638
+ const envAllowedDirs = await resolveConfiguredDirs('FS_ALLOWED_DIRS', { allowMissing });
639
+ // Parse FS_ROOT_BOUNDARY (the --root-boundary flag beats the env var)
640
+ const boundaries = await resolveConfiguredDirs('FS_ROOT_BOUNDARY', {
641
+ resolveReal: true,
642
+ ...(cli.rootBoundary !== undefined ? { rawValue: cli.rootBoundary } : {}),
643
+ });
644
+ const allowCwd = Boolean(this.options?.allowCwd);
645
+ const allowCwdDirs = [];
646
+ if (allowCwd) {
647
+ let cwd = normalizePath(process.cwd());
648
+ const walkCwd = cli.allowCwdWalk ?? parseTrueEnvFlag(process.env['FS_ALLOW_CWD_WALK'], 'FS_ALLOW_CWD_WALK');
649
+ if (walkCwd) {
650
+ cwd = await findProjectRoot(cwd, [...boundaries, homedir()]);
651
+ }
652
+ if (isUnsafeCwdPath(cwd)) {
653
+ Logger.emit('warning', `Skipped adding unsafe current working directory to allowed list: ${cwd}`);
654
+ }
655
+ else {
656
+ allowCwdDirs.push(cwd);
657
+ }
658
+ }
659
+ const baseline = [...cliAllowedDirs, ...envAllowedDirs, ...allowCwdDirs];
660
+ const signal = timedSignal(undefined, ROOTS_TIMEOUT_MS);
661
+ // FS_ROOT_BOUNDARY is the only filter grants answer to (see
662
+ // `grantedDirectories`); without one they pass through as accepted.
663
+ const grantsToInclude = boundaries.length > 0
664
+ ? await filterRootsWithin(this.grantedDirectories, boundaries, 'grantBoundary', signal)
665
+ : this.grantedDirectories;
666
+ const combined = [...baseline, ...grantsToInclude];
667
+ const nextState = await resolveAllowedDirectoriesState(combined, signal);
668
+ // Commit both fields together, after every await has resolved, so a
669
+ // rejecting recompute leaves the guard's previous, consistent view intact.
670
+ this.rootBoundaries = boundaries;
671
+ this.initialize(nextState);
672
+ }
673
+ }
674
+ //# sourceMappingURL=path.js.map