forked from WrongStack/WrongStack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwrite.ts
More file actions
117 lines (108 loc) · 3.89 KB
/
Copy pathwrite.ts
File metadata and controls
117 lines (108 loc) · 3.89 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
110
111
112
113
114
115
116
117
import * as fs from 'node:fs/promises';
import { atomicWrite, ToolValidationError, unifiedDiff } from '@wrongstack/core';
import type { Tool } from '@wrongstack/core';
import { safeResolveReal } from './_util.js';
interface WriteInput {
path: string;
content: string;
}
interface WriteOutput {
path: string;
bytes_written: number;
created: boolean;
diff?: string | undefined;
}
export const writeTool: Tool<WriteInput, WriteOutput> = {
name: 'write',
category: 'Filesystem',
description:
'Write or completely overwrite a file on disk. ' +
'This is a high-privilege operation. For modifying existing files, you should almost always prefer the `edit` tool instead, ' +
'because `edit` is safer and works on the last-read version of the file.',
usageHint:
'RULES FOR CORRECT USAGE:\n' +
'- Use `write` primarily for **new files** or when you want to replace the entire content.\n' +
'- For any existing file, strongly prefer `edit` (it requires a prior `read` in the same session and is more precise).\n' +
'- You MUST have called `read` on the file earlier in the conversation before using `write` on an existing path (the system enforces this for safety).\n' +
'- The path is resolved relative to the project root and protected against escaping the workspace.',
permission: 'confirm',
mutating: true,
timeoutMs: 5_000,
capabilities: ['fs.write'],
icon: 'file',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Relative path from project root. Must not escape the project.',
},
content: {
type: 'string',
description: 'The complete new content of the file.',
},
},
required: ['path', 'content'],
},
async execute(input, ctx) {
if (!input?.path) {
throw new ToolValidationError({
message: 'write: path is required',
field: 'path',
});
}
if (input.content === undefined) {
throw new ToolValidationError({
message: 'write: content is required',
field: 'content',
});
}
const absPath = await safeResolveReal(input.path, ctx);
let existed = false;
let prev = '';
try {
const stat = await fs.stat(absPath);
existed = stat.isFile();
if (existed) {
if (!ctx.hasRead(absPath)) {
// User approved this write (confirm → yes/always) but ctx has no
// read record. The model may call write without a prior explicit
// read. Read the file now so we can compute the diff and honor
// the user's intent to overwrite. Tag as 'write' (NOT 'user') so
// this internal read-for-diff does not widen the permission bypass
// — the user never saw the old content (P1 #1).
prev = await fs.readFile(absPath, 'utf8');
ctx.recordRead(absPath, stat.mtimeMs, 'write');
} else {
prev = await fs.readFile(absPath, 'utf8');
}
}
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
throw err;
}
}
await atomicWrite(absPath, input.content);
const diff = existed
? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path })
: `+++ ${input.path}\n+ (new file, ${input.content.split('\n').length} lines)`;
const stat = await fs.stat(absPath);
// Tag as 'write' so the permission bypass does not auto-approve a later
// write to this path — the user approved THIS write, not future ones
// (P1 #1).
ctx.recordRead(absPath, stat.mtimeMs, 'write');
// Record for session rewind
ctx.session.recordFileChange({
path: absPath,
action: existed ? 'modified' : 'created',
before: existed ? prev : null,
after: input.content,
});
return {
path: absPath,
bytes_written: Buffer.byteLength(input.content, 'utf8'),
created: !existed,
diff,
};
},
};