@dexto/tools-filesystem 1.5.2 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/directory-approval.integration.test.cjs +36 -32
- package/dist/directory-approval.integration.test.js +36 -32
- package/dist/edit-file-tool.cjs +43 -19
- package/dist/edit-file-tool.js +43 -19
- package/dist/edit-file-tool.test.cjs +203 -0
- package/dist/edit-file-tool.test.d.cts +2 -0
- package/dist/edit-file-tool.test.d.ts +2 -0
- package/dist/edit-file-tool.test.js +180 -0
- package/dist/filesystem-service.cjs +17 -14
- package/dist/filesystem-service.d.cts +3 -3
- package/dist/filesystem-service.d.ts +3 -3
- package/dist/filesystem-service.js +17 -14
- package/dist/filesystem-service.test.cjs +233 -0
- package/dist/filesystem-service.test.d.cts +2 -0
- package/dist/filesystem-service.test.d.ts +2 -0
- package/dist/filesystem-service.test.js +210 -0
- package/dist/path-validator.cjs +29 -20
- package/dist/path-validator.d.cts +9 -2
- package/dist/path-validator.d.ts +9 -2
- package/dist/path-validator.js +29 -20
- package/dist/path-validator.test.cjs +54 -48
- package/dist/path-validator.test.js +54 -48
- package/dist/read-file-tool.cjs +2 -2
- package/dist/read-file-tool.js +2 -2
- package/dist/tool-provider.cjs +22 -7
- package/dist/tool-provider.d.cts +4 -1
- package/dist/tool-provider.d.ts +4 -1
- package/dist/tool-provider.js +22 -7
- package/dist/types.d.cts +6 -0
- package/dist/types.d.ts +6 -0
- package/dist/write-file-tool.cjs +41 -7
- package/dist/write-file-tool.js +46 -8
- package/dist/write-file-tool.test.cjs +217 -0
- package/dist/write-file-tool.test.d.cts +2 -0
- package/dist/write-file-tool.test.d.ts +2 -0
- package/dist/write-file-tool.test.js +194 -0
- package/package.json +2 -2
|
@@ -15,7 +15,7 @@ describe("PathValidator", () => {
|
|
|
15
15
|
});
|
|
16
16
|
describe("validatePath", () => {
|
|
17
17
|
describe("Empty and Invalid Paths", () => {
|
|
18
|
-
it("should reject empty path", () => {
|
|
18
|
+
it("should reject empty path", async () => {
|
|
19
19
|
const validator = new PathValidator(
|
|
20
20
|
{
|
|
21
21
|
allowedPaths: ["/home/user/project"],
|
|
@@ -28,11 +28,11 @@ describe("PathValidator", () => {
|
|
|
28
28
|
},
|
|
29
29
|
mockLogger
|
|
30
30
|
);
|
|
31
|
-
const result = validator.validatePath("");
|
|
31
|
+
const result = await validator.validatePath("");
|
|
32
32
|
expect(result.isValid).toBe(false);
|
|
33
33
|
expect(result.error).toBe("Path cannot be empty");
|
|
34
34
|
});
|
|
35
|
-
it("should reject whitespace-only path", () => {
|
|
35
|
+
it("should reject whitespace-only path", async () => {
|
|
36
36
|
const validator = new PathValidator(
|
|
37
37
|
{
|
|
38
38
|
allowedPaths: ["/home/user/project"],
|
|
@@ -45,13 +45,13 @@ describe("PathValidator", () => {
|
|
|
45
45
|
},
|
|
46
46
|
mockLogger
|
|
47
47
|
);
|
|
48
|
-
const result = validator.validatePath(" ");
|
|
48
|
+
const result = await validator.validatePath(" ");
|
|
49
49
|
expect(result.isValid).toBe(false);
|
|
50
50
|
expect(result.error).toBe("Path cannot be empty");
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
53
|
describe("Allowed Paths", () => {
|
|
54
|
-
it("should allow paths within allowed directories", () => {
|
|
54
|
+
it("should allow paths within allowed directories", async () => {
|
|
55
55
|
const validator = new PathValidator(
|
|
56
56
|
{
|
|
57
57
|
allowedPaths: ["/home/user/project"],
|
|
@@ -64,11 +64,11 @@ describe("PathValidator", () => {
|
|
|
64
64
|
},
|
|
65
65
|
mockLogger
|
|
66
66
|
);
|
|
67
|
-
const result = validator.validatePath("/home/user/project/src/file.ts");
|
|
67
|
+
const result = await validator.validatePath("/home/user/project/src/file.ts");
|
|
68
68
|
expect(result.isValid).toBe(true);
|
|
69
69
|
expect(result.normalizedPath).toBeDefined();
|
|
70
70
|
});
|
|
71
|
-
it("should allow relative paths within working directory", () => {
|
|
71
|
+
it("should allow relative paths within working directory", async () => {
|
|
72
72
|
const validator = new PathValidator(
|
|
73
73
|
{
|
|
74
74
|
allowedPaths: ["/home/user/project"],
|
|
@@ -81,10 +81,10 @@ describe("PathValidator", () => {
|
|
|
81
81
|
},
|
|
82
82
|
mockLogger
|
|
83
83
|
);
|
|
84
|
-
const result = validator.validatePath("src/file.ts");
|
|
84
|
+
const result = await validator.validatePath("src/file.ts");
|
|
85
85
|
expect(result.isValid).toBe(true);
|
|
86
86
|
});
|
|
87
|
-
it("should reject paths outside allowed directories", () => {
|
|
87
|
+
it("should reject paths outside allowed directories", async () => {
|
|
88
88
|
const validator = new PathValidator(
|
|
89
89
|
{
|
|
90
90
|
allowedPaths: ["/home/user/project"],
|
|
@@ -97,11 +97,11 @@ describe("PathValidator", () => {
|
|
|
97
97
|
},
|
|
98
98
|
mockLogger
|
|
99
99
|
);
|
|
100
|
-
const result = validator.validatePath("/external/project/file.ts");
|
|
100
|
+
const result = await validator.validatePath("/external/project/file.ts");
|
|
101
101
|
expect(result.isValid).toBe(false);
|
|
102
102
|
expect(result.error).toContain("not within allowed paths");
|
|
103
103
|
});
|
|
104
|
-
it("should allow all paths when allowedPaths is empty", () => {
|
|
104
|
+
it("should allow all paths when allowedPaths is empty", async () => {
|
|
105
105
|
const validator = new PathValidator(
|
|
106
106
|
{
|
|
107
107
|
allowedPaths: [],
|
|
@@ -114,12 +114,12 @@ describe("PathValidator", () => {
|
|
|
114
114
|
},
|
|
115
115
|
mockLogger
|
|
116
116
|
);
|
|
117
|
-
const result = validator.validatePath("/anywhere/file.ts");
|
|
117
|
+
const result = await validator.validatePath("/anywhere/file.ts");
|
|
118
118
|
expect(result.isValid).toBe(true);
|
|
119
119
|
});
|
|
120
120
|
});
|
|
121
121
|
describe("Path Traversal Detection", () => {
|
|
122
|
-
it("should reject path traversal attempts", () => {
|
|
122
|
+
it("should reject path traversal attempts", async () => {
|
|
123
123
|
const validator = new PathValidator(
|
|
124
124
|
{
|
|
125
125
|
allowedPaths: ["/home/user/project"],
|
|
@@ -132,13 +132,15 @@ describe("PathValidator", () => {
|
|
|
132
132
|
},
|
|
133
133
|
mockLogger
|
|
134
134
|
);
|
|
135
|
-
const result = validator.validatePath(
|
|
135
|
+
const result = await validator.validatePath(
|
|
136
|
+
"/home/user/project/../../../etc/passwd"
|
|
137
|
+
);
|
|
136
138
|
expect(result.isValid).toBe(false);
|
|
137
139
|
expect(result.error).toBe("Path traversal detected");
|
|
138
140
|
});
|
|
139
141
|
});
|
|
140
142
|
describe("Blocked Paths", () => {
|
|
141
|
-
it("should reject paths in blocked directories", () => {
|
|
143
|
+
it("should reject paths in blocked directories", async () => {
|
|
142
144
|
const validator = new PathValidator(
|
|
143
145
|
{
|
|
144
146
|
allowedPaths: ["/home/user/project"],
|
|
@@ -151,11 +153,11 @@ describe("PathValidator", () => {
|
|
|
151
153
|
},
|
|
152
154
|
mockLogger
|
|
153
155
|
);
|
|
154
|
-
const result = validator.validatePath("/home/user/project/.git/config");
|
|
156
|
+
const result = await validator.validatePath("/home/user/project/.git/config");
|
|
155
157
|
expect(result.isValid).toBe(false);
|
|
156
158
|
expect(result.error).toContain("blocked");
|
|
157
159
|
});
|
|
158
|
-
it("should reject paths in node_modules", () => {
|
|
160
|
+
it("should reject paths in node_modules", async () => {
|
|
159
161
|
const validator = new PathValidator(
|
|
160
162
|
{
|
|
161
163
|
allowedPaths: ["/home/user/project"],
|
|
@@ -168,7 +170,7 @@ describe("PathValidator", () => {
|
|
|
168
170
|
},
|
|
169
171
|
mockLogger
|
|
170
172
|
);
|
|
171
|
-
const result = validator.validatePath(
|
|
173
|
+
const result = await validator.validatePath(
|
|
172
174
|
"/home/user/project/node_modules/lodash/index.js"
|
|
173
175
|
);
|
|
174
176
|
expect(result.isValid).toBe(false);
|
|
@@ -176,7 +178,7 @@ describe("PathValidator", () => {
|
|
|
176
178
|
});
|
|
177
179
|
});
|
|
178
180
|
describe("Blocked Extensions", () => {
|
|
179
|
-
it("should reject files with blocked extensions", () => {
|
|
181
|
+
it("should reject files with blocked extensions", async () => {
|
|
180
182
|
const validator = new PathValidator(
|
|
181
183
|
{
|
|
182
184
|
allowedPaths: ["/home/user/project"],
|
|
@@ -189,11 +191,11 @@ describe("PathValidator", () => {
|
|
|
189
191
|
},
|
|
190
192
|
mockLogger
|
|
191
193
|
);
|
|
192
|
-
const result = validator.validatePath("/home/user/project/malware.exe");
|
|
194
|
+
const result = await validator.validatePath("/home/user/project/malware.exe");
|
|
193
195
|
expect(result.isValid).toBe(false);
|
|
194
196
|
expect(result.error).toContain(".exe is not allowed");
|
|
195
197
|
});
|
|
196
|
-
it("should handle extensions without leading dot", () => {
|
|
198
|
+
it("should handle extensions without leading dot", async () => {
|
|
197
199
|
const validator = new PathValidator(
|
|
198
200
|
{
|
|
199
201
|
allowedPaths: ["/home/user/project"],
|
|
@@ -207,10 +209,10 @@ describe("PathValidator", () => {
|
|
|
207
209
|
},
|
|
208
210
|
mockLogger
|
|
209
211
|
);
|
|
210
|
-
const result = validator.validatePath("/home/user/project/file.exe");
|
|
212
|
+
const result = await validator.validatePath("/home/user/project/file.exe");
|
|
211
213
|
expect(result.isValid).toBe(false);
|
|
212
214
|
});
|
|
213
|
-
it("should be case-insensitive for extensions", () => {
|
|
215
|
+
it("should be case-insensitive for extensions", async () => {
|
|
214
216
|
const validator = new PathValidator(
|
|
215
217
|
{
|
|
216
218
|
allowedPaths: ["/home/user/project"],
|
|
@@ -223,12 +225,12 @@ describe("PathValidator", () => {
|
|
|
223
225
|
},
|
|
224
226
|
mockLogger
|
|
225
227
|
);
|
|
226
|
-
const result = validator.validatePath("/home/user/project/file.EXE");
|
|
228
|
+
const result = await validator.validatePath("/home/user/project/file.EXE");
|
|
227
229
|
expect(result.isValid).toBe(false);
|
|
228
230
|
});
|
|
229
231
|
});
|
|
230
232
|
describe("Directory Approval Checker Integration", () => {
|
|
231
|
-
it("should consult approval checker for external paths", () => {
|
|
233
|
+
it("should consult approval checker for external paths", async () => {
|
|
232
234
|
const validator = new PathValidator(
|
|
233
235
|
{
|
|
234
236
|
allowedPaths: ["/home/user/project"],
|
|
@@ -241,16 +243,16 @@ describe("PathValidator", () => {
|
|
|
241
243
|
},
|
|
242
244
|
mockLogger
|
|
243
245
|
);
|
|
244
|
-
let result = validator.validatePath("/external/project/file.ts");
|
|
246
|
+
let result = await validator.validatePath("/external/project/file.ts");
|
|
245
247
|
expect(result.isValid).toBe(false);
|
|
246
248
|
const approvalChecker = (filePath) => {
|
|
247
249
|
return filePath.startsWith("/external/project");
|
|
248
250
|
};
|
|
249
251
|
validator.setDirectoryApprovalChecker(approvalChecker);
|
|
250
|
-
result = validator.validatePath("/external/project/file.ts");
|
|
252
|
+
result = await validator.validatePath("/external/project/file.ts");
|
|
251
253
|
expect(result.isValid).toBe(true);
|
|
252
254
|
});
|
|
253
|
-
it("should not use approval checker for config-allowed paths", () => {
|
|
255
|
+
it("should not use approval checker for config-allowed paths", async () => {
|
|
254
256
|
const approvalChecker = vi.fn().mockReturnValue(false);
|
|
255
257
|
const validator = new PathValidator(
|
|
256
258
|
{
|
|
@@ -265,14 +267,14 @@ describe("PathValidator", () => {
|
|
|
265
267
|
mockLogger
|
|
266
268
|
);
|
|
267
269
|
validator.setDirectoryApprovalChecker(approvalChecker);
|
|
268
|
-
const result = validator.validatePath("/home/user/project/src/file.ts");
|
|
270
|
+
const result = await validator.validatePath("/home/user/project/src/file.ts");
|
|
269
271
|
expect(result.isValid).toBe(true);
|
|
270
272
|
expect(approvalChecker).not.toHaveBeenCalled();
|
|
271
273
|
});
|
|
272
274
|
});
|
|
273
275
|
});
|
|
274
276
|
describe("isPathWithinAllowed", () => {
|
|
275
|
-
it("should return true for paths within config-allowed directories", () => {
|
|
277
|
+
it("should return true for paths within config-allowed directories", async () => {
|
|
276
278
|
const validator = new PathValidator(
|
|
277
279
|
{
|
|
278
280
|
allowedPaths: ["/home/user/project"],
|
|
@@ -285,12 +287,14 @@ describe("PathValidator", () => {
|
|
|
285
287
|
},
|
|
286
288
|
mockLogger
|
|
287
289
|
);
|
|
288
|
-
expect(validator.isPathWithinAllowed("/home/user/project/src/file.ts")).toBe(
|
|
289
|
-
expect(validator.isPathWithinAllowed("/home/user/project/deep/nested/file.ts")).toBe(
|
|
290
|
+
expect(await validator.isPathWithinAllowed("/home/user/project/src/file.ts")).toBe(
|
|
290
291
|
true
|
|
291
292
|
);
|
|
293
|
+
expect(
|
|
294
|
+
await validator.isPathWithinAllowed("/home/user/project/deep/nested/file.ts")
|
|
295
|
+
).toBe(true);
|
|
292
296
|
});
|
|
293
|
-
it("should return false for paths outside config-allowed directories", () => {
|
|
297
|
+
it("should return false for paths outside config-allowed directories", async () => {
|
|
294
298
|
const validator = new PathValidator(
|
|
295
299
|
{
|
|
296
300
|
allowedPaths: ["/home/user/project"],
|
|
@@ -303,10 +307,10 @@ describe("PathValidator", () => {
|
|
|
303
307
|
},
|
|
304
308
|
mockLogger
|
|
305
309
|
);
|
|
306
|
-
expect(validator.isPathWithinAllowed("/external/project/file.ts")).toBe(false);
|
|
307
|
-
expect(validator.isPathWithinAllowed("/home/user/other/file.ts")).toBe(false);
|
|
310
|
+
expect(await validator.isPathWithinAllowed("/external/project/file.ts")).toBe(false);
|
|
311
|
+
expect(await validator.isPathWithinAllowed("/home/user/other/file.ts")).toBe(false);
|
|
308
312
|
});
|
|
309
|
-
it("should NOT consult approval checker (used for prompting decisions)", () => {
|
|
313
|
+
it("should NOT consult approval checker (used for prompting decisions)", async () => {
|
|
310
314
|
const approvalChecker = vi.fn().mockReturnValue(true);
|
|
311
315
|
const validator = new PathValidator(
|
|
312
316
|
{
|
|
@@ -321,10 +325,10 @@ describe("PathValidator", () => {
|
|
|
321
325
|
mockLogger
|
|
322
326
|
);
|
|
323
327
|
validator.setDirectoryApprovalChecker(approvalChecker);
|
|
324
|
-
expect(validator.isPathWithinAllowed("/external/project/file.ts")).toBe(false);
|
|
328
|
+
expect(await validator.isPathWithinAllowed("/external/project/file.ts")).toBe(false);
|
|
325
329
|
expect(approvalChecker).not.toHaveBeenCalled();
|
|
326
330
|
});
|
|
327
|
-
it("should return false for empty path", () => {
|
|
331
|
+
it("should return false for empty path", async () => {
|
|
328
332
|
const validator = new PathValidator(
|
|
329
333
|
{
|
|
330
334
|
allowedPaths: ["/home/user/project"],
|
|
@@ -337,10 +341,10 @@ describe("PathValidator", () => {
|
|
|
337
341
|
},
|
|
338
342
|
mockLogger
|
|
339
343
|
);
|
|
340
|
-
expect(validator.isPathWithinAllowed("")).toBe(false);
|
|
341
|
-
expect(validator.isPathWithinAllowed(" ")).toBe(false);
|
|
344
|
+
expect(await validator.isPathWithinAllowed("")).toBe(false);
|
|
345
|
+
expect(await validator.isPathWithinAllowed(" ")).toBe(false);
|
|
342
346
|
});
|
|
343
|
-
it("should return true when allowedPaths is empty (all paths allowed)", () => {
|
|
347
|
+
it("should return true when allowedPaths is empty (all paths allowed)", async () => {
|
|
344
348
|
const validator = new PathValidator(
|
|
345
349
|
{
|
|
346
350
|
allowedPaths: [],
|
|
@@ -353,11 +357,11 @@ describe("PathValidator", () => {
|
|
|
353
357
|
},
|
|
354
358
|
mockLogger
|
|
355
359
|
);
|
|
356
|
-
expect(validator.isPathWithinAllowed("/anywhere/file.ts")).toBe(true);
|
|
360
|
+
expect(await validator.isPathWithinAllowed("/anywhere/file.ts")).toBe(true);
|
|
357
361
|
});
|
|
358
362
|
});
|
|
359
363
|
describe("Path Containment (Parent Directory Coverage)", () => {
|
|
360
|
-
it("should recognize that approving parent covers child paths", () => {
|
|
364
|
+
it("should recognize that approving parent covers child paths", async () => {
|
|
361
365
|
const validator = new PathValidator(
|
|
362
366
|
{
|
|
363
367
|
allowedPaths: ["/external/sub"],
|
|
@@ -370,9 +374,11 @@ describe("PathValidator", () => {
|
|
|
370
374
|
},
|
|
371
375
|
mockLogger
|
|
372
376
|
);
|
|
373
|
-
expect(validator.isPathWithinAllowed("/external/sub/deep/nested/file.ts")).toBe(
|
|
377
|
+
expect(await validator.isPathWithinAllowed("/external/sub/deep/nested/file.ts")).toBe(
|
|
378
|
+
true
|
|
379
|
+
);
|
|
374
380
|
});
|
|
375
|
-
it("should not allow sibling directories", () => {
|
|
381
|
+
it("should not allow sibling directories", async () => {
|
|
376
382
|
const validator = new PathValidator(
|
|
377
383
|
{
|
|
378
384
|
allowedPaths: ["/external/sub"],
|
|
@@ -385,9 +391,9 @@ describe("PathValidator", () => {
|
|
|
385
391
|
},
|
|
386
392
|
mockLogger
|
|
387
393
|
);
|
|
388
|
-
expect(validator.isPathWithinAllowed("/external/other/file.ts")).toBe(false);
|
|
394
|
+
expect(await validator.isPathWithinAllowed("/external/other/file.ts")).toBe(false);
|
|
389
395
|
});
|
|
390
|
-
it("should not allow parent directories when child is approved", () => {
|
|
396
|
+
it("should not allow parent directories when child is approved", async () => {
|
|
391
397
|
const validator = new PathValidator(
|
|
392
398
|
{
|
|
393
399
|
allowedPaths: ["/external/sub/deep"],
|
|
@@ -400,7 +406,7 @@ describe("PathValidator", () => {
|
|
|
400
406
|
},
|
|
401
407
|
mockLogger
|
|
402
408
|
);
|
|
403
|
-
expect(validator.isPathWithinAllowed("/external/sub/file.ts")).toBe(false);
|
|
409
|
+
expect(await validator.isPathWithinAllowed("/external/sub/file.ts")).toBe(false);
|
|
404
410
|
});
|
|
405
411
|
});
|
|
406
412
|
describe("getAllowedPaths and getBlockedPaths", () => {
|
package/dist/read-file-tool.cjs
CHANGED
|
@@ -50,10 +50,10 @@ function createReadFileTool(options) {
|
|
|
50
50
|
* Check if this read operation needs directory access approval.
|
|
51
51
|
* Returns custom approval request if the file is outside allowed paths.
|
|
52
52
|
*/
|
|
53
|
-
getApprovalOverride: (args) => {
|
|
53
|
+
getApprovalOverride: async (args) => {
|
|
54
54
|
const { file_path } = args;
|
|
55
55
|
if (!file_path) return null;
|
|
56
|
-
const isAllowed = fileSystemService.isPathWithinConfigAllowed(file_path);
|
|
56
|
+
const isAllowed = await fileSystemService.isPathWithinConfigAllowed(file_path);
|
|
57
57
|
if (isAllowed) {
|
|
58
58
|
return null;
|
|
59
59
|
}
|
package/dist/read-file-tool.js
CHANGED
|
@@ -17,10 +17,10 @@ function createReadFileTool(options) {
|
|
|
17
17
|
* Check if this read operation needs directory access approval.
|
|
18
18
|
* Returns custom approval request if the file is outside allowed paths.
|
|
19
19
|
*/
|
|
20
|
-
getApprovalOverride: (args) => {
|
|
20
|
+
getApprovalOverride: async (args) => {
|
|
21
21
|
const { file_path } = args;
|
|
22
22
|
if (!file_path) return null;
|
|
23
|
-
const isAllowed = fileSystemService.isPathWithinConfigAllowed(file_path);
|
|
23
|
+
const isAllowed = await fileSystemService.isPathWithinConfigAllowed(file_path);
|
|
24
24
|
if (isAllowed) {
|
|
25
25
|
return null;
|
|
26
26
|
}
|
package/dist/tool-provider.cjs
CHANGED
|
@@ -34,6 +34,13 @@ const DEFAULT_BLOCKED_EXTENSIONS = [".exe", ".dll", ".so"];
|
|
|
34
34
|
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
35
35
|
const DEFAULT_ENABLE_BACKUPS = false;
|
|
36
36
|
const DEFAULT_BACKUP_RETENTION_DAYS = 7;
|
|
37
|
+
const FILESYSTEM_TOOL_NAMES = [
|
|
38
|
+
"read_file",
|
|
39
|
+
"write_file",
|
|
40
|
+
"edit_file",
|
|
41
|
+
"glob_files",
|
|
42
|
+
"grep_content"
|
|
43
|
+
];
|
|
37
44
|
const FileSystemToolsConfigSchema = import_zod.z.object({
|
|
38
45
|
type: import_zod.z.literal("filesystem-tools"),
|
|
39
46
|
allowedPaths: import_zod.z.array(import_zod.z.string()).default(DEFAULT_ALLOWED_PATHS).describe("List of allowed base paths for file operations"),
|
|
@@ -47,6 +54,9 @@ const FileSystemToolsConfigSchema = import_zod.z.object({
|
|
|
47
54
|
backupPath: import_zod.z.string().optional().describe("Absolute path for storing file backups (if enableBackups is true)"),
|
|
48
55
|
backupRetentionDays: import_zod.z.number().int().positive().default(DEFAULT_BACKUP_RETENTION_DAYS).describe(
|
|
49
56
|
`Number of days to retain backup files (default: ${DEFAULT_BACKUP_RETENTION_DAYS})`
|
|
57
|
+
),
|
|
58
|
+
enabledTools: import_zod.z.array(import_zod.z.enum(FILESYSTEM_TOOL_NAMES)).optional().describe(
|
|
59
|
+
`Subset of tools to enable. If not specified, all tools are enabled. Available: ${FILESYSTEM_TOOL_NAMES.join(", ")}`
|
|
50
60
|
)
|
|
51
61
|
}).strict();
|
|
52
62
|
const fileSystemToolsProvider = {
|
|
@@ -88,13 +98,18 @@ const fileSystemToolsProvider = {
|
|
|
88
98
|
fileSystemService,
|
|
89
99
|
directoryApproval
|
|
90
100
|
};
|
|
91
|
-
|
|
92
|
-
(0, import_read_file_tool.createReadFileTool)(fileToolOptions),
|
|
93
|
-
(0, import_write_file_tool.createWriteFileTool)(fileToolOptions),
|
|
94
|
-
(0, import_edit_file_tool.createEditFileTool)(fileToolOptions),
|
|
95
|
-
(0, import_glob_files_tool.createGlobFilesTool)(fileSystemService),
|
|
96
|
-
(0, import_grep_content_tool.createGrepContentTool)(fileSystemService)
|
|
97
|
-
|
|
101
|
+
const toolCreators = {
|
|
102
|
+
read_file: () => (0, import_read_file_tool.createReadFileTool)(fileToolOptions),
|
|
103
|
+
write_file: () => (0, import_write_file_tool.createWriteFileTool)(fileToolOptions),
|
|
104
|
+
edit_file: () => (0, import_edit_file_tool.createEditFileTool)(fileToolOptions),
|
|
105
|
+
glob_files: () => (0, import_glob_files_tool.createGlobFilesTool)(fileSystemService),
|
|
106
|
+
grep_content: () => (0, import_grep_content_tool.createGrepContentTool)(fileSystemService)
|
|
107
|
+
};
|
|
108
|
+
const toolsToCreate = config.enabledTools ?? FILESYSTEM_TOOL_NAMES;
|
|
109
|
+
if (config.enabledTools) {
|
|
110
|
+
logger.debug(`Creating subset of filesystem tools: ${toolsToCreate.join(", ")}`);
|
|
111
|
+
}
|
|
112
|
+
return toolsToCreate.map((toolName) => toolCreators[toolName]());
|
|
98
113
|
},
|
|
99
114
|
metadata: {
|
|
100
115
|
displayName: "FileSystem Tools",
|
package/dist/tool-provider.d.cts
CHANGED
|
@@ -34,16 +34,18 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
|
|
|
34
34
|
enableBackups: z.ZodDefault<z.ZodBoolean>;
|
|
35
35
|
backupPath: z.ZodOptional<z.ZodString>;
|
|
36
36
|
backupRetentionDays: z.ZodDefault<z.ZodNumber>;
|
|
37
|
+
enabledTools: z.ZodOptional<z.ZodArray<z.ZodEnum<["read_file", "write_file", "edit_file", "glob_files", "grep_content"]>, "many">>;
|
|
37
38
|
}, "strict", z.ZodTypeAny, {
|
|
38
|
-
type: "filesystem-tools";
|
|
39
39
|
allowedPaths: string[];
|
|
40
40
|
blockedExtensions: string[];
|
|
41
41
|
blockedPaths: string[];
|
|
42
42
|
maxFileSize: number;
|
|
43
43
|
enableBackups: boolean;
|
|
44
44
|
backupRetentionDays: number;
|
|
45
|
+
type: "filesystem-tools";
|
|
45
46
|
backupPath?: string | undefined;
|
|
46
47
|
workingDirectory?: string | undefined;
|
|
48
|
+
enabledTools?: ("edit_file" | "glob_files" | "grep_content" | "read_file" | "write_file")[] | undefined;
|
|
47
49
|
}, {
|
|
48
50
|
type: "filesystem-tools";
|
|
49
51
|
allowedPaths?: string[] | undefined;
|
|
@@ -54,6 +56,7 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
|
|
|
54
56
|
enableBackups?: boolean | undefined;
|
|
55
57
|
backupRetentionDays?: number | undefined;
|
|
56
58
|
workingDirectory?: string | undefined;
|
|
59
|
+
enabledTools?: ("edit_file" | "glob_files" | "grep_content" | "read_file" | "write_file")[] | undefined;
|
|
57
60
|
}>;
|
|
58
61
|
type FileSystemToolsConfig = z.output<typeof FileSystemToolsConfigSchema>;
|
|
59
62
|
/**
|
package/dist/tool-provider.d.ts
CHANGED
|
@@ -34,16 +34,18 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
|
|
|
34
34
|
enableBackups: z.ZodDefault<z.ZodBoolean>;
|
|
35
35
|
backupPath: z.ZodOptional<z.ZodString>;
|
|
36
36
|
backupRetentionDays: z.ZodDefault<z.ZodNumber>;
|
|
37
|
+
enabledTools: z.ZodOptional<z.ZodArray<z.ZodEnum<["read_file", "write_file", "edit_file", "glob_files", "grep_content"]>, "many">>;
|
|
37
38
|
}, "strict", z.ZodTypeAny, {
|
|
38
|
-
type: "filesystem-tools";
|
|
39
39
|
allowedPaths: string[];
|
|
40
40
|
blockedExtensions: string[];
|
|
41
41
|
blockedPaths: string[];
|
|
42
42
|
maxFileSize: number;
|
|
43
43
|
enableBackups: boolean;
|
|
44
44
|
backupRetentionDays: number;
|
|
45
|
+
type: "filesystem-tools";
|
|
45
46
|
backupPath?: string | undefined;
|
|
46
47
|
workingDirectory?: string | undefined;
|
|
48
|
+
enabledTools?: ("edit_file" | "glob_files" | "grep_content" | "read_file" | "write_file")[] | undefined;
|
|
47
49
|
}, {
|
|
48
50
|
type: "filesystem-tools";
|
|
49
51
|
allowedPaths?: string[] | undefined;
|
|
@@ -54,6 +56,7 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
|
|
|
54
56
|
enableBackups?: boolean | undefined;
|
|
55
57
|
backupRetentionDays?: number | undefined;
|
|
56
58
|
workingDirectory?: string | undefined;
|
|
59
|
+
enabledTools?: ("edit_file" | "glob_files" | "grep_content" | "read_file" | "write_file")[] | undefined;
|
|
57
60
|
}>;
|
|
58
61
|
type FileSystemToolsConfig = z.output<typeof FileSystemToolsConfigSchema>;
|
|
59
62
|
/**
|
package/dist/tool-provider.js
CHANGED
|
@@ -11,6 +11,13 @@ const DEFAULT_BLOCKED_EXTENSIONS = [".exe", ".dll", ".so"];
|
|
|
11
11
|
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
12
12
|
const DEFAULT_ENABLE_BACKUPS = false;
|
|
13
13
|
const DEFAULT_BACKUP_RETENTION_DAYS = 7;
|
|
14
|
+
const FILESYSTEM_TOOL_NAMES = [
|
|
15
|
+
"read_file",
|
|
16
|
+
"write_file",
|
|
17
|
+
"edit_file",
|
|
18
|
+
"glob_files",
|
|
19
|
+
"grep_content"
|
|
20
|
+
];
|
|
14
21
|
const FileSystemToolsConfigSchema = z.object({
|
|
15
22
|
type: z.literal("filesystem-tools"),
|
|
16
23
|
allowedPaths: z.array(z.string()).default(DEFAULT_ALLOWED_PATHS).describe("List of allowed base paths for file operations"),
|
|
@@ -24,6 +31,9 @@ const FileSystemToolsConfigSchema = z.object({
|
|
|
24
31
|
backupPath: z.string().optional().describe("Absolute path for storing file backups (if enableBackups is true)"),
|
|
25
32
|
backupRetentionDays: z.number().int().positive().default(DEFAULT_BACKUP_RETENTION_DAYS).describe(
|
|
26
33
|
`Number of days to retain backup files (default: ${DEFAULT_BACKUP_RETENTION_DAYS})`
|
|
34
|
+
),
|
|
35
|
+
enabledTools: z.array(z.enum(FILESYSTEM_TOOL_NAMES)).optional().describe(
|
|
36
|
+
`Subset of tools to enable. If not specified, all tools are enabled. Available: ${FILESYSTEM_TOOL_NAMES.join(", ")}`
|
|
27
37
|
)
|
|
28
38
|
}).strict();
|
|
29
39
|
const fileSystemToolsProvider = {
|
|
@@ -65,13 +75,18 @@ const fileSystemToolsProvider = {
|
|
|
65
75
|
fileSystemService,
|
|
66
76
|
directoryApproval
|
|
67
77
|
};
|
|
68
|
-
|
|
69
|
-
createReadFileTool(fileToolOptions),
|
|
70
|
-
createWriteFileTool(fileToolOptions),
|
|
71
|
-
createEditFileTool(fileToolOptions),
|
|
72
|
-
createGlobFilesTool(fileSystemService),
|
|
73
|
-
createGrepContentTool(fileSystemService)
|
|
74
|
-
|
|
78
|
+
const toolCreators = {
|
|
79
|
+
read_file: () => createReadFileTool(fileToolOptions),
|
|
80
|
+
write_file: () => createWriteFileTool(fileToolOptions),
|
|
81
|
+
edit_file: () => createEditFileTool(fileToolOptions),
|
|
82
|
+
glob_files: () => createGlobFilesTool(fileSystemService),
|
|
83
|
+
grep_content: () => createGrepContentTool(fileSystemService)
|
|
84
|
+
};
|
|
85
|
+
const toolsToCreate = config.enabledTools ?? FILESYSTEM_TOOL_NAMES;
|
|
86
|
+
if (config.enabledTools) {
|
|
87
|
+
logger.debug(`Creating subset of filesystem tools: ${toolsToCreate.join(", ")}`);
|
|
88
|
+
}
|
|
89
|
+
return toolsToCreate.map((toolName) => toolCreators[toolName]());
|
|
75
90
|
},
|
|
76
91
|
metadata: {
|
|
77
92
|
displayName: "FileSystem Tools",
|
package/dist/types.d.cts
CHANGED
|
@@ -112,6 +112,8 @@ interface WriteResult {
|
|
|
112
112
|
path: string;
|
|
113
113
|
bytesWritten: number;
|
|
114
114
|
backupPath?: string | undefined;
|
|
115
|
+
/** Original content if file was overwritten (undefined for new files) */
|
|
116
|
+
originalContent?: string | undefined;
|
|
115
117
|
}
|
|
116
118
|
/**
|
|
117
119
|
* Edit operation
|
|
@@ -138,6 +140,10 @@ interface EditResult {
|
|
|
138
140
|
path: string;
|
|
139
141
|
changesCount: number;
|
|
140
142
|
backupPath?: string | undefined;
|
|
143
|
+
/** Original content before edit (for diff generation) */
|
|
144
|
+
originalContent: string;
|
|
145
|
+
/** New content after edit (for diff generation) */
|
|
146
|
+
newContent: string;
|
|
141
147
|
}
|
|
142
148
|
/**
|
|
143
149
|
* Path validation result
|
package/dist/types.d.ts
CHANGED
|
@@ -112,6 +112,8 @@ interface WriteResult {
|
|
|
112
112
|
path: string;
|
|
113
113
|
bytesWritten: number;
|
|
114
114
|
backupPath?: string | undefined;
|
|
115
|
+
/** Original content if file was overwritten (undefined for new files) */
|
|
116
|
+
originalContent?: string | undefined;
|
|
115
117
|
}
|
|
116
118
|
/**
|
|
117
119
|
* Edit operation
|
|
@@ -138,6 +140,10 @@ interface EditResult {
|
|
|
138
140
|
path: string;
|
|
139
141
|
changesCount: number;
|
|
140
142
|
backupPath?: string | undefined;
|
|
143
|
+
/** Original content before edit (for diff generation) */
|
|
144
|
+
originalContent: string;
|
|
145
|
+
/** New content after edit (for diff generation) */
|
|
146
|
+
newContent: string;
|
|
141
147
|
}
|
|
142
148
|
/**
|
|
143
149
|
* Path validation result
|
package/dist/write-file-tool.cjs
CHANGED
|
@@ -32,10 +32,16 @@ __export(write_file_tool_exports, {
|
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(write_file_tool_exports);
|
|
34
34
|
var path = __toESM(require("node:path"), 1);
|
|
35
|
+
var import_node_crypto = require("node:crypto");
|
|
35
36
|
var import_zod = require("zod");
|
|
36
37
|
var import_diff = require("diff");
|
|
37
38
|
var import_core = require("@dexto/core");
|
|
38
39
|
var import_error_codes = require("./error-codes.js");
|
|
40
|
+
const previewContentHashCache = /* @__PURE__ */ new Map();
|
|
41
|
+
const FILE_NOT_EXISTS_MARKER = null;
|
|
42
|
+
function computeContentHash(content) {
|
|
43
|
+
return (0, import_node_crypto.createHash)("sha256").update(content, "utf8").digest("hex");
|
|
44
|
+
}
|
|
39
45
|
const WriteFileInputSchema = import_zod.z.object({
|
|
40
46
|
file_path: import_zod.z.string().describe("Absolute path where the file should be written"),
|
|
41
47
|
content: import_zod.z.string().describe("Content to write to the file"),
|
|
@@ -67,10 +73,10 @@ function createWriteFileTool(options) {
|
|
|
67
73
|
* Check if this write operation needs directory access approval.
|
|
68
74
|
* Returns custom approval request if the file is outside allowed paths.
|
|
69
75
|
*/
|
|
70
|
-
getApprovalOverride: (args) => {
|
|
76
|
+
getApprovalOverride: async (args) => {
|
|
71
77
|
const { file_path } = args;
|
|
72
78
|
if (!file_path) return null;
|
|
73
|
-
const isAllowed = fileSystemService.isPathWithinConfigAllowed(file_path);
|
|
79
|
+
const isAllowed = await fileSystemService.isPathWithinConfigAllowed(file_path);
|
|
74
80
|
if (isAllowed) {
|
|
75
81
|
return null;
|
|
76
82
|
}
|
|
@@ -105,15 +111,25 @@ function createWriteFileTool(options) {
|
|
|
105
111
|
},
|
|
106
112
|
/**
|
|
107
113
|
* Generate preview for approval UI - shows diff or file creation info
|
|
114
|
+
* Stores content hash for change detection in execute phase.
|
|
108
115
|
*/
|
|
109
|
-
generatePreview: async (input,
|
|
116
|
+
generatePreview: async (input, context) => {
|
|
110
117
|
const { file_path, content } = input;
|
|
111
118
|
try {
|
|
112
119
|
const originalFile = await fileSystemService.readFile(file_path);
|
|
113
120
|
const originalContent = originalFile.content;
|
|
121
|
+
if (context?.toolCallId) {
|
|
122
|
+
previewContentHashCache.set(
|
|
123
|
+
context.toolCallId,
|
|
124
|
+
computeContentHash(originalContent)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
114
127
|
return generateDiffPreview(file_path, originalContent, content);
|
|
115
128
|
} catch (error) {
|
|
116
129
|
if (error instanceof import_core.DextoRuntimeError && error.code === import_error_codes.FileSystemErrorCode.FILE_NOT_FOUND) {
|
|
130
|
+
if (context?.toolCallId) {
|
|
131
|
+
previewContentHashCache.set(context.toolCallId, FILE_NOT_EXISTS_MARKER);
|
|
132
|
+
}
|
|
117
133
|
const lineCount = content.split("\n").length;
|
|
118
134
|
const preview = {
|
|
119
135
|
type: "file",
|
|
@@ -129,24 +145,42 @@ function createWriteFileTool(options) {
|
|
|
129
145
|
throw error;
|
|
130
146
|
}
|
|
131
147
|
},
|
|
132
|
-
execute: async (input,
|
|
148
|
+
execute: async (input, context) => {
|
|
133
149
|
const { file_path, content, create_dirs, encoding } = input;
|
|
134
150
|
let originalContent = null;
|
|
151
|
+
let fileExistsNow = false;
|
|
135
152
|
try {
|
|
136
153
|
const originalFile = await fileSystemService.readFile(file_path);
|
|
137
154
|
originalContent = originalFile.content;
|
|
155
|
+
fileExistsNow = true;
|
|
138
156
|
} catch (error) {
|
|
139
157
|
if (error instanceof import_core.DextoRuntimeError && error.code === import_error_codes.FileSystemErrorCode.FILE_NOT_FOUND) {
|
|
140
158
|
originalContent = null;
|
|
159
|
+
fileExistsNow = false;
|
|
141
160
|
} else {
|
|
142
161
|
throw error;
|
|
143
162
|
}
|
|
144
163
|
}
|
|
164
|
+
if (context?.toolCallId && previewContentHashCache.has(context.toolCallId)) {
|
|
165
|
+
const expectedHash = previewContentHashCache.get(context.toolCallId);
|
|
166
|
+
previewContentHashCache.delete(context.toolCallId);
|
|
167
|
+
if (expectedHash === FILE_NOT_EXISTS_MARKER) {
|
|
168
|
+
if (fileExistsNow) {
|
|
169
|
+
throw import_core.ToolError.fileModifiedSincePreview("write_file", file_path);
|
|
170
|
+
}
|
|
171
|
+
} else if (expectedHash !== null) {
|
|
172
|
+
if (!fileExistsNow) {
|
|
173
|
+
throw import_core.ToolError.fileModifiedSincePreview("write_file", file_path);
|
|
174
|
+
}
|
|
175
|
+
const currentHash = computeContentHash(originalContent);
|
|
176
|
+
if (expectedHash !== currentHash) {
|
|
177
|
+
throw import_core.ToolError.fileModifiedSincePreview("write_file", file_path);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
145
181
|
const result = await fileSystemService.writeFile(file_path, content, {
|
|
146
182
|
createDirs: create_dirs,
|
|
147
|
-
encoding
|
|
148
|
-
backup: true
|
|
149
|
-
// Always create backup for internal tools
|
|
183
|
+
encoding
|
|
150
184
|
});
|
|
151
185
|
let _display;
|
|
152
186
|
if (originalContent === null) {
|