-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathverify-plugin.ts
More file actions
109 lines (101 loc) · 3.63 KB
/
Copy pathverify-plugin.ts
File metadata and controls
109 lines (101 loc) · 3.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import { readFile } from "node:fs/promises";
import type { ToolPlugin } from "@intx/tools-posix";
import type { ToolCall, ToolResult } from "@intx/types/runtime";
import { withFileMutationLock } from "./file-mutation-lock.js";
import { applyLineRangeEdit, parseEditFileMode } from "./edit-file-line-range.js";
function mutationPath(call: ToolCall): string | undefined {
if (call.name !== "edit_file" && call.name !== "write_file") return undefined;
const path = call.arguments.path;
return typeof path === "string" && path.length > 0 ? path : undefined;
}
export function verifyPlugin(): ToolPlugin {
return {
middleware: (next) => async (call, signal) => {
const lockedPath = mutationPath(call);
const run = async (): Promise<ToolResult> => {
let before: string | undefined;
if (call.name === "edit_file") {
const path = String(call.arguments.path ?? "");
try {
before = await readFile(path, "utf8");
} catch {
// File may not exist yet; edit will likely fail downstream
}
}
const result = await next(call, signal);
if (call.name === "write_file" && !result.isError) {
const path = String(call.arguments.path ?? "");
const expected = String(call.arguments.content ?? "");
try {
const actual = await readFile(path, "utf8");
if (actual !== expected) {
return {
callId: call.id,
content: `Write verification failed: content mismatch`,
isError: true,
};
}
} catch (err) {
return {
callId: call.id,
content: `Write verification failed: could not re-read file: ${err instanceof Error ? err.message : String(err)}`,
isError: true,
};
}
}
if (call.name === "edit_file" && !result.isError && before !== undefined) {
const path = String(call.arguments.path ?? "");
const mode = parseEditFileMode(call.arguments);
try {
const actual = await readFile(path, "utf8");
let expected: string;
if (mode.kind === "line_range") {
expected = applyLineRangeEdit(
before,
mode.start_line,
mode.end_line,
mode.new_string,
);
} else if (mode.kind === "substring") {
expected = applyEdit(before, mode.old_string, mode.new_string, mode.replace_all);
} else {
return result;
}
if (actual !== expected) {
return {
callId: call.id,
content: `Edit verification failed: content mismatch after replacement`,
isError: true,
};
}
} catch (err) {
return {
callId: call.id,
content: `Edit verification failed: could not re-read file: ${err instanceof Error ? err.message : String(err)}`,
isError: true,
};
}
}
return result;
};
if (lockedPath === undefined) {
return run();
}
return withFileMutationLock(lockedPath, run);
},
};
}
function applyEdit(
content: string,
oldStr: string,
newStr: string,
replaceAll: boolean,
): string {
if (oldStr.length === 0) return content;
if (replaceAll) {
return content.split(oldStr).join(newStr);
}
const idx = content.indexOf(oldStr);
if (idx === -1) return content;
return content.slice(0, idx) + newStr + content.slice(idx + oldStr.length);
}