@dexto/tools-filesystem 1.6.0 → 1.6.1

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 (73) hide show
  1. package/dist/directory-approval.cjs +44 -40
  2. package/dist/directory-approval.d.ts +8 -4
  3. package/dist/directory-approval.d.ts.map +1 -1
  4. package/dist/directory-approval.integration.test.cjs +107 -356
  5. package/dist/directory-approval.integration.test.d.ts +6 -6
  6. package/dist/directory-approval.integration.test.js +109 -360
  7. package/dist/directory-approval.js +45 -41
  8. package/dist/edit-file-tool.cjs +69 -47
  9. package/dist/edit-file-tool.d.ts.map +1 -1
  10. package/dist/edit-file-tool.js +77 -48
  11. package/dist/edit-file-tool.test.cjs +54 -11
  12. package/dist/edit-file-tool.test.js +54 -11
  13. package/dist/error-codes.cjs +4 -0
  14. package/dist/error-codes.d.ts +4 -0
  15. package/dist/error-codes.d.ts.map +1 -1
  16. package/dist/error-codes.js +4 -0
  17. package/dist/errors.cjs +48 -0
  18. package/dist/errors.d.ts +16 -0
  19. package/dist/errors.d.ts.map +1 -1
  20. package/dist/errors.js +48 -0
  21. package/dist/filesystem-service.cjs +307 -9
  22. package/dist/filesystem-service.d.ts +28 -1
  23. package/dist/filesystem-service.d.ts.map +1 -1
  24. package/dist/filesystem-service.js +308 -10
  25. package/dist/glob-files-tool.cjs +12 -1
  26. package/dist/glob-files-tool.d.ts.map +1 -1
  27. package/dist/glob-files-tool.js +13 -2
  28. package/dist/grep-content-tool.cjs +13 -1
  29. package/dist/grep-content-tool.d.ts.map +1 -1
  30. package/dist/grep-content-tool.js +14 -2
  31. package/dist/index.cjs +3 -0
  32. package/dist/index.d.cts +852 -16
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -0
  36. package/dist/path-validator.cjs +28 -2
  37. package/dist/path-validator.d.ts +14 -0
  38. package/dist/path-validator.d.ts.map +1 -1
  39. package/dist/path-validator.js +28 -2
  40. package/dist/read-file-tool.cjs +7 -1
  41. package/dist/read-file-tool.d.ts.map +1 -1
  42. package/dist/read-file-tool.js +8 -2
  43. package/dist/tool-factory.cjs +21 -0
  44. package/dist/tool-factory.d.ts.map +1 -1
  45. package/dist/tool-factory.js +21 -0
  46. package/dist/types.d.ts +65 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/write-file-tool.cjs +60 -38
  49. package/dist/write-file-tool.d.ts +1 -1
  50. package/dist/write-file-tool.d.ts.map +1 -1
  51. package/dist/write-file-tool.js +67 -39
  52. package/dist/write-file-tool.test.cjs +75 -13
  53. package/dist/write-file-tool.test.js +75 -13
  54. package/package.json +4 -4
  55. package/dist/directory-approval.d.cts +0 -22
  56. package/dist/directory-approval.integration.test.d.cts +0 -2
  57. package/dist/edit-file-tool.d.cts +0 -34
  58. package/dist/edit-file-tool.test.d.cts +0 -2
  59. package/dist/error-codes.d.cts +0 -32
  60. package/dist/errors.d.cts +0 -112
  61. package/dist/file-tool-types.d.cts +0 -18
  62. package/dist/filesystem-service.d.cts +0 -117
  63. package/dist/filesystem-service.test.d.cts +0 -2
  64. package/dist/glob-files-tool.d.cts +0 -31
  65. package/dist/grep-content-tool.d.cts +0 -40
  66. package/dist/path-validator.d.cts +0 -97
  67. package/dist/path-validator.test.d.cts +0 -2
  68. package/dist/read-file-tool.d.cts +0 -31
  69. package/dist/tool-factory-config.d.cts +0 -63
  70. package/dist/tool-factory.d.cts +0 -7
  71. package/dist/types.d.cts +0 -178
  72. package/dist/write-file-tool.d.cts +0 -34
  73. package/dist/write-file-tool.test.d.cts +0 -2
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Directory Approval Integration Tests
3
3
  *
4
- * Tests for the directory access permission system integrated into file tools.
4
+ * Tests for the directory access permission system integrated into filesystem tools.
5
5
  *
6
6
  * Key behaviors tested:
7
- * 1. Working directory: No directory prompt, normal tool flow
8
- * 2. External dir (first access): Directory prompt via getApprovalOverride
9
- * 3. External dir (after "session" approval): No directory prompt
10
- * 4. External dir (after "once" approval): Directory prompt every time
11
- * 5. Path containment: approving /ext covers /ext/sub/file.txt
7
+ * - Paths within config-allowed roots do not require directory access prompting metadata
8
+ * - Paths outside config-allowed roots return directory access prompting metadata
9
+ * - Session-approved directories do not prompt again
10
+ * - Once-approved directories still prompt again (prompting decision)
11
+ * - Session approvals cover child paths but not sibling directories
12
12
  */
13
13
  export {};
14
14
  //# sourceMappingURL=directory-approval.integration.test.d.ts.map
@@ -2,16 +2,14 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
2
  import * as path from "node:path";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as os from "node:os";
5
- import { createReadFileTool } from "./read-file-tool.js";
6
- import { createWriteFileTool } from "./write-file-tool.js";
7
- import { createEditFileTool } from "./edit-file-tool.js";
8
- import { FileSystemService } from "./filesystem-service.js";
9
5
  import {
10
6
  ApprovalManager,
11
- ApprovalStatus,
12
- ApprovalType,
13
7
  DextoRuntimeError
14
8
  } from "@dexto/core";
9
+ import { FileSystemService } from "./filesystem-service.js";
10
+ import { createReadFileTool } from "./read-file-tool.js";
11
+ import { createWriteFileTool } from "./write-file-tool.js";
12
+ import { createEditFileTool } from "./edit-file-tool.js";
15
13
  const createMockLogger = () => {
16
14
  const noopAsync = async () => void 0;
17
15
  const logger = {
@@ -22,6 +20,7 @@ const createMockLogger = () => {
22
20
  error: vi.fn(),
23
21
  trackException: vi.fn(),
24
22
  createChild: () => logger,
23
+ createFileOnlyChild: () => logger,
25
24
  setLevel: vi.fn(),
26
25
  getLevel: () => "info",
27
26
  getLogFilePath: () => null,
@@ -82,402 +81,152 @@ describe("Directory Approval Integration Tests", () => {
82
81
  } catch {
83
82
  }
84
83
  });
85
- describe("Read File Tool", () => {
86
- describe("getApprovalOverride", () => {
87
- it("should return null for paths within working directory (no prompt needed)", async () => {
88
- const tool = createReadFileTool(getFileSystemService);
89
- const testFile = path.join(tempDir, "test.txt");
90
- await fs.writeFile(testFile, "test content");
91
- const override = await tool.getApprovalOverride?.(
92
- tool.inputSchema.parse({ file_path: testFile }),
93
- toolContext
94
- );
95
- expect(override).toBeNull();
96
- });
97
- it("should return directory access approval for external paths", async () => {
98
- const tool = createReadFileTool(getFileSystemService);
99
- const externalPath = "/external/project/file.ts";
100
- const override = await tool.getApprovalOverride?.(
101
- tool.inputSchema.parse({ file_path: externalPath }),
102
- toolContext
103
- );
104
- expect(override).not.toBeNull();
105
- expect(override?.type).toBe(ApprovalType.DIRECTORY_ACCESS);
106
- const metadata = override?.metadata;
107
- expect(metadata?.path).toBe(path.resolve(externalPath));
108
- expect(metadata?.parentDir).toBe(path.dirname(path.resolve(externalPath)));
109
- expect(metadata?.operation).toBe("read");
110
- expect(metadata?.toolName).toBe("read_file");
111
- });
112
- it("should return null when external path is session-approved", async () => {
113
- approvalManager.addApprovedDirectory("/external/project", "session");
114
- const tool = createReadFileTool(getFileSystemService);
115
- const externalPath = "/external/project/file.ts";
116
- const override = await tool.getApprovalOverride?.(
117
- tool.inputSchema.parse({ file_path: externalPath }),
118
- toolContext
119
- );
120
- expect(override).toBeNull();
121
- });
122
- });
123
- describe("onApprovalGranted", () => {
124
- it("should add directory as session-approved when rememberDirectory is true", async () => {
125
- const tool = createReadFileTool(getFileSystemService);
126
- const externalPath = "/external/project/file.ts";
127
- const approvalRequest = await tool.getApprovalOverride?.(
128
- tool.inputSchema.parse({ file_path: externalPath }),
129
- toolContext
130
- );
131
- expect(approvalRequest).not.toBeNull();
132
- if (!approvalRequest) {
133
- throw new Error("Expected approval request");
134
- }
135
- tool.onApprovalGranted?.(
136
- {
137
- approvalId: "test-approval",
138
- status: ApprovalStatus.APPROVED,
139
- data: { rememberDirectory: true }
140
- },
141
- toolContext,
142
- approvalRequest
143
- );
144
- expect(
145
- approvalManager.getApprovedDirectories().get(path.dirname(path.resolve(externalPath)))
146
- ).toBe("session");
147
- });
148
- it("should add directory as once-approved when rememberDirectory is false", async () => {
149
- const tool = createReadFileTool(getFileSystemService);
150
- const externalPath = "/external/project/file.ts";
151
- const approvalRequest = await tool.getApprovalOverride?.(
152
- tool.inputSchema.parse({ file_path: externalPath }),
153
- toolContext
154
- );
155
- expect(approvalRequest).not.toBeNull();
156
- if (!approvalRequest) {
157
- throw new Error("Expected approval request");
158
- }
159
- tool.onApprovalGranted?.(
160
- {
161
- approvalId: "test-approval",
162
- status: ApprovalStatus.APPROVED,
163
- data: { rememberDirectory: false }
164
- },
165
- toolContext,
166
- approvalRequest
167
- );
168
- expect(
169
- approvalManager.getApprovedDirectories().get(path.dirname(path.resolve(externalPath)))
170
- ).toBe("once");
171
- });
172
- it("should default to once-approved when rememberDirectory is not specified", async () => {
173
- const tool = createReadFileTool(getFileSystemService);
174
- const externalPath = "/external/project/file.ts";
175
- const approvalRequest = await tool.getApprovalOverride?.(
176
- tool.inputSchema.parse({ file_path: externalPath }),
177
- toolContext
178
- );
179
- expect(approvalRequest).not.toBeNull();
180
- if (!approvalRequest) {
181
- throw new Error("Expected approval request");
182
- }
183
- tool.onApprovalGranted?.(
184
- {
185
- approvalId: "test-approval",
186
- status: ApprovalStatus.APPROVED,
187
- data: {}
188
- },
189
- toolContext,
190
- approvalRequest
191
- );
192
- expect(
193
- approvalManager.getApprovedDirectories().get(path.dirname(path.resolve(externalPath)))
194
- ).toBe("once");
195
- });
196
- });
197
- describe("execute", () => {
198
- it("should read file contents within working directory", async () => {
199
- const tool = createReadFileTool(getFileSystemService);
200
- const testFile = path.join(tempDir, "readable.txt");
201
- await fs.writeFile(testFile, "Hello, world!\nLine 2");
202
- const result = await tool.execute({ file_path: testFile }, toolContext);
203
- expect(result.content).toBe("Hello, world!\nLine 2");
204
- expect(result.lines).toBe(2);
205
- });
206
- });
207
- });
208
- describe("Write File Tool", () => {
209
- describe("getApprovalOverride", () => {
210
- it("should return null for paths within working directory", async () => {
211
- const tool = createWriteFileTool(getFileSystemService);
212
- const testFile = path.join(tempDir, "new-file.txt");
213
- const override = await tool.getApprovalOverride?.(
214
- tool.inputSchema.parse({ file_path: testFile, content: "test" }),
215
- toolContext
216
- );
217
- expect(override).toBeNull();
218
- });
219
- it("should return directory access approval for external paths", async () => {
220
- const tool = createWriteFileTool(getFileSystemService);
221
- const externalPath = "/external/project/new.ts";
222
- const override = await tool.getApprovalOverride?.(
223
- tool.inputSchema.parse({ file_path: externalPath, content: "test" }),
224
- toolContext
225
- );
226
- expect(override).not.toBeNull();
227
- expect(override?.type).toBe(ApprovalType.DIRECTORY_ACCESS);
228
- const metadata = override?.metadata;
229
- expect(metadata?.operation).toBe("write");
230
- expect(metadata?.toolName).toBe("write_file");
231
- });
232
- it("should return null when external path is session-approved", async () => {
233
- approvalManager.addApprovedDirectory("/external/project", "session");
234
- const tool = createWriteFileTool(getFileSystemService);
235
- const externalPath = "/external/project/new.ts";
236
- const override = await tool.getApprovalOverride?.(
237
- tool.inputSchema.parse({ file_path: externalPath, content: "test" }),
238
- toolContext
239
- );
240
- expect(override).toBeNull();
241
- });
84
+ describe("getApprovalOverride", () => {
85
+ it("should return null for paths within config-allowed roots", async () => {
86
+ const tool = createReadFileTool(getFileSystemService);
87
+ const overrideFn = tool.approval?.override;
88
+ expect(overrideFn).toBeDefined();
89
+ const testFile = path.join(tempDir, "test.txt");
90
+ await fs.writeFile(testFile, "test content");
91
+ const metadata = await overrideFn(
92
+ tool.inputSchema.parse({ file_path: testFile }),
93
+ toolContext
94
+ );
95
+ expect(metadata).toBeNull();
242
96
  });
243
- describe("onApprovalGranted", () => {
244
- it("should add directory as session-approved when rememberDirectory is true", async () => {
245
- const tool = createWriteFileTool(getFileSystemService);
246
- const externalPath = "/external/project/new.ts";
247
- const approvalRequest = await tool.getApprovalOverride?.(
248
- tool.inputSchema.parse({ file_path: externalPath, content: "test" }),
249
- toolContext
250
- );
251
- expect(approvalRequest).not.toBeNull();
252
- if (!approvalRequest) {
253
- throw new Error("Expected approval request");
97
+ it("should return directory access metadata for external paths", async () => {
98
+ const tool = createReadFileTool(getFileSystemService);
99
+ const overrideFn = tool.approval?.override;
100
+ expect(overrideFn).toBeDefined();
101
+ const externalPath = "/external/project/file.ts";
102
+ const metadata = await overrideFn(
103
+ tool.inputSchema.parse({ file_path: externalPath }),
104
+ toolContext
105
+ );
106
+ expect(metadata).not.toBeNull();
107
+ expect(metadata).toMatchObject({
108
+ type: "directory_access",
109
+ metadata: {
110
+ path: path.resolve(externalPath),
111
+ parentDir: path.dirname(path.resolve(externalPath)),
112
+ operation: "read",
113
+ toolName: "read_file"
254
114
  }
255
- tool.onApprovalGranted?.(
256
- {
257
- approvalId: "test-approval",
258
- status: ApprovalStatus.APPROVED,
259
- data: { rememberDirectory: true }
260
- },
261
- toolContext,
262
- approvalRequest
263
- );
264
- expect(
265
- approvalManager.getApprovedDirectories().get(path.dirname(path.resolve(externalPath)))
266
- ).toBe("session");
267
115
  });
268
116
  });
269
- });
270
- describe("Edit File Tool", () => {
271
- describe("getApprovalOverride", () => {
272
- it("should return null for paths within working directory", async () => {
273
- const tool = createEditFileTool(getFileSystemService);
274
- const testFile = path.join(tempDir, "existing.txt");
275
- const override = await tool.getApprovalOverride?.(
276
- tool.inputSchema.parse({
277
- file_path: testFile,
278
- old_string: "old",
279
- new_string: "new"
280
- }),
281
- toolContext
282
- );
283
- expect(override).toBeNull();
284
- });
285
- it("should return directory access approval for external paths", async () => {
286
- const tool = createEditFileTool(getFileSystemService);
287
- const externalPath = "/external/project/existing.ts";
288
- const override = await tool.getApprovalOverride?.(
289
- tool.inputSchema.parse({
290
- file_path: externalPath,
291
- old_string: "old",
292
- new_string: "new"
293
- }),
294
- toolContext
295
- );
296
- expect(override).not.toBeNull();
297
- expect(override?.type).toBe(ApprovalType.DIRECTORY_ACCESS);
298
- const metadata = override?.metadata;
299
- expect(metadata?.operation).toBe("edit");
300
- expect(metadata?.toolName).toBe("edit_file");
301
- });
302
- it("should return null when external path is session-approved", async () => {
303
- approvalManager.addApprovedDirectory("/external/project", "session");
304
- const tool = createEditFileTool(getFileSystemService);
305
- const externalPath = "/external/project/existing.ts";
306
- const override = await tool.getApprovalOverride?.(
307
- tool.inputSchema.parse({
308
- file_path: externalPath,
309
- old_string: "old",
310
- new_string: "new"
311
- }),
312
- toolContext
313
- );
314
- expect(override).toBeNull();
315
- });
316
- });
317
- });
318
- describe("Session vs Once Approval Scenarios", () => {
319
- it("should not prompt for subsequent requests after session approval", async () => {
117
+ it("should return null when external path is session-approved", async () => {
118
+ approvalManager.addApprovedDirectory("/external/project", "session");
320
119
  const tool = createReadFileTool(getFileSystemService);
321
- const externalPath1 = "/external/project/file1.ts";
322
- const externalPath2 = "/external/project/file2.ts";
323
- let override = await tool.getApprovalOverride?.(
324
- tool.inputSchema.parse({ file_path: externalPath1 }),
325
- toolContext
326
- );
327
- expect(override).not.toBeNull();
328
- if (!override) {
329
- throw new Error("Expected approval request");
330
- }
331
- tool.onApprovalGranted?.(
332
- {
333
- approvalId: "approval-1",
334
- status: ApprovalStatus.APPROVED,
335
- data: { rememberDirectory: true }
336
- },
337
- toolContext,
338
- override
339
- );
340
- expect(
341
- approvalManager.getApprovedDirectories().get(path.dirname(path.resolve(externalPath1)))
342
- ).toBe("session");
343
- override = await tool.getApprovalOverride?.(
344
- tool.inputSchema.parse({ file_path: externalPath2 }),
120
+ const overrideFn = tool.approval?.override;
121
+ expect(overrideFn).toBeDefined();
122
+ const externalPath = "/external/project/file.ts";
123
+ const metadata = await overrideFn(
124
+ tool.inputSchema.parse({ file_path: externalPath }),
345
125
  toolContext
346
126
  );
347
- expect(override).toBeNull();
127
+ expect(metadata).toBeNull();
348
128
  });
349
- it("should prompt for subsequent requests after once approval", async () => {
129
+ it("should still return metadata when external path is once-approved (prompt again)", async () => {
130
+ approvalManager.addApprovedDirectory("/external/project", "once");
350
131
  const tool = createReadFileTool(getFileSystemService);
351
- const externalPath1 = "/external/project/file1.ts";
352
- const externalPath2 = "/external/project/file2.ts";
353
- let override = await tool.getApprovalOverride?.(
354
- tool.inputSchema.parse({ file_path: externalPath1 }),
132
+ const overrideFn = tool.approval?.override;
133
+ expect(overrideFn).toBeDefined();
134
+ const externalPath = "/external/project/file.ts";
135
+ const metadata = await overrideFn(
136
+ tool.inputSchema.parse({ file_path: externalPath }),
355
137
  toolContext
356
138
  );
357
- expect(override).not.toBeNull();
358
- if (!override) {
359
- throw new Error("Expected approval request");
360
- }
361
- tool.onApprovalGranted?.(
362
- {
363
- approvalId: "approval-1",
364
- status: ApprovalStatus.APPROVED,
365
- data: { rememberDirectory: false }
366
- },
367
- toolContext,
368
- override
139
+ expect(metadata).not.toBeNull();
140
+ });
141
+ });
142
+ describe("Different tool operations", () => {
143
+ it("should label write operations correctly", async () => {
144
+ const tool = createWriteFileTool(getFileSystemService);
145
+ const overrideFn = tool.approval?.override;
146
+ expect(overrideFn).toBeDefined();
147
+ const externalPath = "/external/project/new.ts";
148
+ const metadata = await overrideFn(
149
+ tool.inputSchema.parse({ file_path: externalPath, content: "test" }),
150
+ toolContext
369
151
  );
370
- expect(
371
- approvalManager.getApprovedDirectories().get(path.dirname(path.resolve(externalPath1)))
372
- ).toBe("once");
373
- override = await tool.getApprovalOverride?.(
374
- tool.inputSchema.parse({ file_path: externalPath2 }),
152
+ expect(metadata).not.toBeNull();
153
+ expect(metadata).toMatchObject({
154
+ type: "directory_access",
155
+ metadata: {
156
+ path: path.resolve(externalPath),
157
+ parentDir: path.dirname(path.resolve(externalPath)),
158
+ operation: "write",
159
+ toolName: "write_file"
160
+ }
161
+ });
162
+ });
163
+ it("should label edit operations correctly", async () => {
164
+ const tool = createEditFileTool(getFileSystemService);
165
+ const overrideFn = tool.approval?.override;
166
+ expect(overrideFn).toBeDefined();
167
+ const externalPath = "/external/project/existing.ts";
168
+ const metadata = await overrideFn(
169
+ tool.inputSchema.parse({
170
+ file_path: externalPath,
171
+ old_string: "old",
172
+ new_string: "new"
173
+ }),
375
174
  toolContext
376
175
  );
377
- expect(override).not.toBeNull();
176
+ expect(metadata).not.toBeNull();
177
+ expect(metadata).toMatchObject({
178
+ type: "directory_access",
179
+ metadata: {
180
+ path: path.resolve(externalPath),
181
+ parentDir: path.dirname(path.resolve(externalPath)),
182
+ operation: "edit",
183
+ toolName: "edit_file"
184
+ }
185
+ });
378
186
  });
379
187
  });
380
- describe("Path Containment Scenarios", () => {
188
+ describe("Path containment scenarios", () => {
381
189
  it("should cover child paths when parent directory is session-approved", async () => {
382
190
  const tool = createReadFileTool(getFileSystemService);
191
+ const overrideFn = tool.approval?.override;
192
+ expect(overrideFn).toBeDefined();
383
193
  approvalManager.addApprovedDirectory("/external/project", "session");
384
- let override = await tool.getApprovalOverride?.(
194
+ const metadata1 = await overrideFn(
385
195
  tool.inputSchema.parse({ file_path: "/external/project/file.ts" }),
386
196
  toolContext
387
197
  );
388
- expect(override).toBeNull();
389
- override = await tool.getApprovalOverride?.(
198
+ expect(metadata1).toBeNull();
199
+ const metadata2 = await overrideFn(
390
200
  tool.inputSchema.parse({ file_path: "/external/project/deep/nested/file.ts" }),
391
201
  toolContext
392
202
  );
393
- expect(override).toBeNull();
203
+ expect(metadata2).toBeNull();
394
204
  });
395
205
  it("should NOT cover sibling directories", async () => {
396
206
  const tool = createReadFileTool(getFileSystemService);
207
+ const overrideFn = tool.approval?.override;
208
+ expect(overrideFn).toBeDefined();
397
209
  approvalManager.addApprovedDirectory("/external/sub", "session");
398
- let override = await tool.getApprovalOverride?.(
210
+ const metadata1 = await overrideFn(
399
211
  tool.inputSchema.parse({ file_path: "/external/sub/file.ts" }),
400
212
  toolContext
401
213
  );
402
- expect(override).toBeNull();
403
- override = await tool.getApprovalOverride?.(
214
+ expect(metadata1).toBeNull();
215
+ const metadata2 = await overrideFn(
404
216
  tool.inputSchema.parse({ file_path: "/external/other/file.ts" }),
405
217
  toolContext
406
218
  );
407
- expect(override).not.toBeNull();
408
- });
409
- });
410
- describe("Different External Directories Scenarios", () => {
411
- it("should require separate approval for different external directories", async () => {
412
- const tool = createReadFileTool(getFileSystemService);
413
- const dir1Path = "/external/project1/file.ts";
414
- const dir2Path = "/external/project2/file.ts";
415
- const override1 = await tool.getApprovalOverride?.(
416
- tool.inputSchema.parse({ file_path: dir1Path }),
417
- toolContext
418
- );
419
- expect(override1).not.toBeNull();
420
- const metadata1 = override1?.metadata;
421
- expect(metadata1?.parentDir).toBe("/external/project1");
422
- const override2 = await tool.getApprovalOverride?.(
423
- tool.inputSchema.parse({ file_path: dir2Path }),
424
- toolContext
425
- );
426
- expect(override2).not.toBeNull();
427
- const metadata2 = override2?.metadata;
428
- expect(metadata2?.parentDir).toBe("/external/project2");
429
- });
430
- });
431
- describe("Mixed Operations Scenarios", () => {
432
- it("should share directory approval across different file operations", async () => {
433
- const readTool = createReadFileTool(getFileSystemService);
434
- const writeTool = createWriteFileTool(getFileSystemService);
435
- const editTool = createEditFileTool(getFileSystemService);
436
- const externalDir = "/external/project";
437
- const approvalRequest = await readTool.getApprovalOverride?.(
438
- readTool.inputSchema.parse({ file_path: `${externalDir}/file1.ts` }),
439
- toolContext
440
- );
441
- expect(approvalRequest).not.toBeNull();
442
- if (!approvalRequest) {
443
- throw new Error("Expected approval request");
444
- }
445
- readTool.onApprovalGranted?.(
446
- {
447
- approvalId: "approval-1",
448
- status: ApprovalStatus.APPROVED,
449
- data: { rememberDirectory: true }
450
- },
451
- toolContext,
452
- approvalRequest
453
- );
454
- expect(
455
- await writeTool.getApprovalOverride?.(
456
- writeTool.inputSchema.parse({
457
- file_path: `${externalDir}/file2.ts`,
458
- content: "test"
459
- }),
460
- toolContext
461
- )
462
- ).toBeNull();
463
- expect(
464
- await editTool.getApprovalOverride?.(
465
- editTool.inputSchema.parse({
466
- file_path: `${externalDir}/file3.ts`,
467
- old_string: "a",
468
- new_string: "b"
469
- }),
470
- toolContext
471
- )
472
- ).toBeNull();
219
+ expect(metadata2).not.toBeNull();
473
220
  });
474
221
  });
475
222
  describe("Without ApprovalManager in context", () => {
476
223
  it("should throw for external paths", async () => {
477
224
  const tool = createReadFileTool(getFileSystemService);
225
+ const overrideFn = tool.approval?.override;
226
+ expect(overrideFn).toBeDefined();
478
227
  const contextWithoutApprovalManager = { logger: mockLogger };
479
228
  await expect(
480
- tool.getApprovalOverride?.(
229
+ overrideFn(
481
230
  tool.inputSchema.parse({ file_path: "/external/project/file.ts" }),
482
231
  contextWithoutApprovalManager
483
232
  )
@@ -1,55 +1,59 @@
1
1
  import * as path from "node:path";
2
- import { ApprovalType, ToolError } from "@dexto/core";
2
+ import { ApprovalStatus, ApprovalType, ToolError } from "@dexto/core";
3
3
  function resolveFilePath(workingDirectory, filePath) {
4
4
  const resolvedPath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(workingDirectory, filePath);
5
5
  return { path: resolvedPath, parentDir: path.dirname(resolvedPath) };
6
6
  }
7
7
  function createDirectoryAccessApprovalHandlers(options) {
8
8
  return {
9
- async getApprovalOverride(input, context) {
10
- const resolvedFileSystemService = await options.getFileSystemService(context);
11
- const paths = options.resolvePaths(input, resolvedFileSystemService);
12
- const isAllowed = await resolvedFileSystemService.isPathWithinConfigAllowed(paths.path);
13
- if (isAllowed) {
14
- return null;
15
- }
16
- const approvalManager = context.services?.approval;
17
- if (!approvalManager) {
18
- throw ToolError.configInvalid(
19
- `${options.toolName} requires ToolExecutionContext.services.approval`
9
+ approval: {
10
+ async override(input, context) {
11
+ const resolvedFileSystemService = await options.getFileSystemService(context);
12
+ const paths = options.resolvePaths(input, resolvedFileSystemService);
13
+ const isAllowed = await resolvedFileSystemService.isPathWithinConfigAllowed(
14
+ paths.path
20
15
  );
21
- }
22
- if (approvalManager.isDirectorySessionApproved(paths.path)) {
23
- return null;
24
- }
25
- return {
26
- type: ApprovalType.DIRECTORY_ACCESS,
27
- metadata: {
28
- path: paths.path,
29
- parentDir: paths.parentDir,
30
- operation: options.operation,
31
- toolName: options.toolName
16
+ if (isAllowed) {
17
+ return null;
32
18
  }
33
- };
34
- },
35
- onApprovalGranted(response, context, approvalRequest) {
36
- if (approvalRequest.type !== ApprovalType.DIRECTORY_ACCESS) {
37
- return;
38
- }
39
- const metadata = approvalRequest.metadata;
40
- const parentDir = typeof metadata?.parentDir === "string" ? metadata.parentDir : null;
41
- if (!parentDir) {
42
- return;
43
- }
44
- const data = response.data;
45
- const rememberDirectory = data?.rememberDirectory ?? false;
46
- const approvalManager = context.services?.approval;
47
- if (!approvalManager) {
48
- throw ToolError.configInvalid(
49
- `${options.toolName} requires ToolExecutionContext.services.approval`
19
+ const approvalManager = context.services?.approval;
20
+ if (!approvalManager) {
21
+ throw ToolError.configInvalid(
22
+ `${options.toolName} requires ToolExecutionContext.services.approval`
23
+ );
24
+ }
25
+ if (approvalManager.isDirectorySessionApproved(paths.path)) {
26
+ return null;
27
+ }
28
+ return {
29
+ type: ApprovalType.DIRECTORY_ACCESS,
30
+ metadata: {
31
+ path: paths.path,
32
+ parentDir: paths.parentDir,
33
+ operation: options.operation,
34
+ toolName: options.toolName
35
+ }
36
+ };
37
+ },
38
+ async onGranted(response, context, approvalRequest) {
39
+ const approvalManager = context.services?.approval;
40
+ if (!approvalManager) {
41
+ return;
42
+ }
43
+ if (response.status !== ApprovalStatus.APPROVED) {
44
+ return;
45
+ }
46
+ const data = response.data;
47
+ const rememberDirectory = data?.rememberDirectory ?? false;
48
+ const metadata = approvalRequest.metadata;
49
+ if (!metadata?.parentDir) {
50
+ return;
51
+ }
52
+ approvalManager.addApprovedDirectory(
53
+ metadata.parentDir,
54
+ rememberDirectory ? "session" : "once"
50
55
  );
51
56
  }
52
- approvalManager.addApprovedDirectory(parentDir, rememberDirectory ? "session" : "once");
53
57
  }
54
58
  };
55
59
  }