@dexto/tools-filesystem 1.6.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/directory-approval.cjs +44 -40
- package/dist/directory-approval.d.ts +8 -4
- package/dist/directory-approval.d.ts.map +1 -1
- package/dist/directory-approval.integration.test.cjs +107 -356
- package/dist/directory-approval.integration.test.d.ts +6 -6
- package/dist/directory-approval.integration.test.js +109 -360
- package/dist/directory-approval.js +45 -41
- package/dist/edit-file-tool.cjs +69 -47
- package/dist/edit-file-tool.d.ts.map +1 -1
- package/dist/edit-file-tool.js +77 -48
- package/dist/edit-file-tool.test.cjs +54 -11
- package/dist/edit-file-tool.test.js +54 -11
- package/dist/error-codes.cjs +4 -0
- package/dist/error-codes.d.ts +4 -0
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +4 -0
- package/dist/errors.cjs +48 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +48 -0
- package/dist/filesystem-service.cjs +307 -9
- package/dist/filesystem-service.d.ts +28 -1
- package/dist/filesystem-service.d.ts.map +1 -1
- package/dist/filesystem-service.js +308 -10
- package/dist/glob-files-tool.cjs +12 -1
- package/dist/glob-files-tool.d.ts.map +1 -1
- package/dist/glob-files-tool.js +13 -2
- package/dist/grep-content-tool.cjs +13 -1
- package/dist/grep-content-tool.d.ts.map +1 -1
- package/dist/grep-content-tool.js +14 -2
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +852 -16
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/path-validator.cjs +28 -2
- package/dist/path-validator.d.ts +14 -0
- package/dist/path-validator.d.ts.map +1 -1
- package/dist/path-validator.js +28 -2
- package/dist/read-file-tool.cjs +7 -1
- package/dist/read-file-tool.d.ts.map +1 -1
- package/dist/read-file-tool.js +8 -2
- package/dist/tool-factory.cjs +21 -0
- package/dist/tool-factory.d.ts.map +1 -1
- package/dist/tool-factory.js +21 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/write-file-tool.cjs +60 -38
- package/dist/write-file-tool.d.ts +1 -1
- package/dist/write-file-tool.d.ts.map +1 -1
- package/dist/write-file-tool.js +67 -39
- package/dist/write-file-tool.test.cjs +75 -13
- package/dist/write-file-tool.test.js +75 -13
- package/package.json +6 -6
- package/dist/directory-approval.d.cts +0 -22
- package/dist/directory-approval.integration.test.d.cts +0 -2
- package/dist/edit-file-tool.d.cts +0 -34
- package/dist/edit-file-tool.test.d.cts +0 -2
- package/dist/error-codes.d.cts +0 -32
- package/dist/errors.d.cts +0 -112
- package/dist/file-tool-types.d.cts +0 -18
- package/dist/filesystem-service.d.cts +0 -117
- package/dist/filesystem-service.test.d.cts +0 -2
- package/dist/glob-files-tool.d.cts +0 -31
- package/dist/grep-content-tool.d.cts +0 -40
- package/dist/path-validator.d.cts +0 -97
- package/dist/path-validator.test.d.cts +0 -2
- package/dist/read-file-tool.d.cts +0 -31
- package/dist/tool-factory-config.d.cts +0 -63
- package/dist/tool-factory.d.cts +0 -7
- package/dist/types.d.cts +0 -178
- package/dist/write-file-tool.d.cts +0 -34
- 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
|
|
4
|
+
* Tests for the directory access permission system integrated into filesystem tools.
|
|
5
5
|
*
|
|
6
6
|
* Key behaviors tested:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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("
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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(
|
|
127
|
+
expect(metadata).toBeNull();
|
|
348
128
|
});
|
|
349
|
-
it("should
|
|
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
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
194
|
+
const metadata1 = await overrideFn(
|
|
385
195
|
tool.inputSchema.parse({ file_path: "/external/project/file.ts" }),
|
|
386
196
|
toolContext
|
|
387
197
|
);
|
|
388
|
-
expect(
|
|
389
|
-
|
|
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(
|
|
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
|
-
|
|
210
|
+
const metadata1 = await overrideFn(
|
|
399
211
|
tool.inputSchema.parse({ file_path: "/external/sub/file.ts" }),
|
|
400
212
|
toolContext
|
|
401
213
|
);
|
|
402
|
-
expect(
|
|
403
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
}
|