(args: z.infer<typeof ArgsSchema>, ctx: ToolContext)
| 18 | argsSchema = ArgsSchema; |
| 19 | |
| 20 | async execute(args: z.infer<typeof ArgsSchema>, ctx: ToolContext): Promise<ToolResult> { |
| 21 | const abs = path.isAbsolute(args.path) ? args.path : path.resolve(ctx.cwd, args.path); |
| 22 | |
| 23 | // Safety: refuse to write outside cwd unless explicitly absolute and user-approved |
| 24 | const rel = path.relative(ctx.cwd, abs); |
| 25 | if (rel.startsWith('..') && !path.isAbsolute(args.path)) { |
| 26 | return { content: `[ERROR] Refusing to write outside cwd: ${args.path}`, isError: true }; |
| 27 | } |
| 28 | |
| 29 | // Refuse to overwrite binary files (would corrupt utf-8 conversion) |
| 30 | let before: string | null = null; |
| 31 | try { |
| 32 | const stat = await fs.stat(abs); |
| 33 | // Read as buffer first to check binary-ness |
| 34 | const buf = await fs.readFile(abs); |
| 35 | if (isBinaryBuffer(buf)) { |
| 36 | return { |
| 37 | content: `[BINARY_FILE_REFUSED] ${rel} is an existing binary file (${(stat.size / 1024).toFixed(1)}KB). write_file refuses to overwrite binary files because utf-8 round-tripping would corrupt them. If you really mean to replace this file, delete it first with: shell({ command: "rm '${rel}'", ... })`, |
| 38 | isError: true, |
| 39 | }; |
| 40 | } |
| 41 | before = buf.toString('utf-8'); |
| 42 | } catch (e: any) { |
| 43 | if (e.code !== 'ENOENT') { |
| 44 | // not a missing-file error |
| 45 | return { content: `[ERROR] Cannot inspect ${args.path}: ${e.message}`, isError: true }; |
| 46 | } |
| 47 | // File doesn't exist — that's fine, we're creating it |
| 48 | } |
| 49 | |
| 50 | // Permission check |
| 51 | const permReq = { tool: 'write_file', operation: rel, description: before ? `Overwrite ${rel}` : `Create ${rel}` }; |
| 52 | const decision = ctx.permissions.evaluate(permReq); |
| 53 | if (decision === 'deny') { |
| 54 | return { content: `[PERMISSION_DENIED] Cannot write to ${args.path} (blocked by policy)`, isError: true }; |
| 55 | } |
| 56 | let contentToWrite = args.content; |
| 57 | if (decision === 'ask') { |
| 58 | const sizeNote = (args.content.length > 64 * 1024) |
| 59 | ? ` [large file: ${(args.content.length / 1024).toFixed(1)}KB — diff display truncated]` |
| 60 | : ''; |
| 61 | const dec = await confirmEdit(ctx, { |
| 62 | rel, before, after: args.content, absPath: abs, permReq, |
| 63 | label: (before ? `Overwrite ${rel}?` : `Create ${rel}?`) + sizeNote, |
| 64 | }); |
| 65 | if (dec.kind === 'reject') { |
| 66 | return { content: `[USER_REJECTED] User declined to write ${args.path}`, isError: true }; |
| 67 | } |
| 68 | if (dec.kind === 'revise') return reviseResult(rel); |
| 69 | contentToWrite = dec.content; // may be the user-edited version from [E] Edit |
| 70 | } |
| 71 | |
| 72 | // Execute through transaction. `base` is the snapshot this overwrite was |
| 73 | // computed against (null = create). Re-checked after the approval pause. |
| 74 | try { |
| 75 | await ctx.transaction.write(abs, contentToWrite, { base: before, label: rel }); |
| 76 | } catch (e: any) { |
| 77 | if (typeof e?.message === 'string' && e.message.startsWith('[FILE_CHANGED]')) { |
nothing calls this directly
no test coverage detected