From 110b6cee1edadb4c124b32e7c6f6e478cab490cf Mon Sep 17 00:00:00 2001 From: Nomadcxx Date: Tue, 21 Jul 2026 12:07:29 +0000 Subject: [PATCH 1/2] fix(tools): map cursor-agent `fileText` arg to canonical `content` cursor-agent 2026.07.17 carries full-file edit/write bodies under `fileText`, replacing the earlier `streamContent` field. ARG_KEY_ALIASES had no mapping for it, so the body was dropped and the write/edit reroute lost the content. Add `filetext -> content`; key normalization also covers `file_text`. Found by reading the cursor-agent client bundle directly. --- src/provider/tool-schema-compat.ts | 1 + .../unit/provider-tool-schema-compat.test.ts | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/provider/tool-schema-compat.ts b/src/provider/tool-schema-compat.ts index 7d96e67..46f2193 100644 --- a/src/provider/tool-schema-compat.ts +++ b/src/provider/tool-schema-compat.ts @@ -38,6 +38,7 @@ const ARG_KEY_ALIASES = new Map([ ["data", "content"], ["payload", "content"], ["streamcontent", "content"], + ["filetext", "content"], ["recursive", "force"], ["oldstring", "old_string"], ["newstring", "new_string"], diff --git a/tests/unit/provider-tool-schema-compat.test.ts b/tests/unit/provider-tool-schema-compat.test.ts index 63989f6..6d5ae1d 100644 --- a/tests/unit/provider-tool-schema-compat.test.ts +++ b/tests/unit/provider-tool-schema-compat.test.ts @@ -162,6 +162,42 @@ describe("tool schema compatibility", () => { expect(result.validation.ok).toBe(true); }); + it("normalizes fileText (cursor-agent full-file content) to content", () => { + // cursor-agent 2026.07.17 carries full-file edit/write bodies under `fileText` + // (it replaced the earlier `streamContent` field). Without this alias the body + // is dropped and the write/edit reroute loses the content. + const result = applyToolSchemaCompat( + { + id: "c1", + type: "function", + function: { + name: "write", + arguments: JSON.stringify({ + filePath: "/tmp/a.txt", + fileText: "hello world", + }), + }, + }, + new Map([ + [ + "write", + { + type: "object", + properties: { + path: { type: "string" }, + content: { type: "string" }, + }, + required: ["path", "content"], + }, + ], + ]), + ); + + expect(result.normalizedArgs.content).toBe("hello world"); + expect(result.normalizedArgs.fileText).toBeUndefined(); + expect(result.validation.ok).toBe(true); + }); + it("normalizes write path to filePath when schema requires filePath", () => { const result = applyToolSchemaCompat( { From d6c00cc2ad87004195f77ae251432fb0cdef8da8 Mon Sep 17 00:00:00 2001 From: Nomadcxx Date: Tue, 21 Jul 2026 13:40:11 +0000 Subject: [PATCH 2/2] fix(tools): cover fileText payloads in suspicious overwrite guard The filetext alias re-enables edit-to-write reroutes for cursor-agent 2026.07.17+ full-file edit payloads, but hasStreamContentArg only matched the streamcontent token, so the anti-clobber guard (blocks reroutes that would shrink an existing file) never fired for those payloads. Match both tokens so the guard applies regardless of protocol field name. Also remove the dead fileText fallback when reading rerouted write args (buildWriteArguments only ever emits content), and add regression tests: fileText edit payloads reroute to write carrying the content, and suspicious fileText reroutes that would shrink an existing file are skipped. The guard test fails without the hasStreamContentArg change. --- src/provider/runtime-interception.ts | 14 ++++--- .../provider-runtime-interception.test.ts | 40 +++++++++++++++++++ .../unit/provider-tool-schema-compat.test.ts | 24 +++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/provider/runtime-interception.ts b/src/provider/runtime-interception.ts index 0fc3644..a3a2821 100644 --- a/src/provider/runtime-interception.ts +++ b/src/provider/runtime-interception.ts @@ -818,11 +818,9 @@ function detectSuspiciousStreamContentWrite( : typeof args.filePath === "string" ? args.filePath : null; - const content = typeof args.content === "string" - ? args.content - : typeof args.fileText === "string" - ? args.fileText - : null; + // Rerouted write args always carry `content` (see buildWriteArguments); a + // fileText payload has already been normalized to content by this point. + const content = typeof args.content === "string" ? args.content : null; if (!filePath || content === null || !existsSync(filePath)) { return null; } @@ -875,7 +873,11 @@ function firstToolPayload(event: StreamJsonToolCallEvent): StreamJsonToolCallPay } function hasStreamContentArg(args: Record): boolean { - return Object.keys(args).some((key) => key.toLowerCase().replace(/[^a-z0-9]/g, "") === "streamcontent"); + return Object.keys(args).some((key) => { + const token = key.toLowerCase().replace(/[^a-z0-9]/g, ""); + // cursor-agent 2026.07.17 renamed the full-file body field: streamContent -> fileText. + return token === "streamcontent" || token === "filetext"; + }); } function countLogicalLines(value: string): number { diff --git a/tests/unit/provider-runtime-interception.test.ts b/tests/unit/provider-runtime-interception.test.ts index 4d958ee..eb3607e 100644 --- a/tests/unit/provider-runtime-interception.test.ts +++ b/tests/unit/provider-runtime-interception.test.ts @@ -684,6 +684,46 @@ describe("provider runtime interception fallback", () => { expect(readFileSync(target, "utf-8").split("\n").slice(47, 52)).toEqual(["48", "49", "50", "51", "52"]); }); + it("skips suspicious fileText write reroutes that would shrink an existing file", async () => { + // cursor-agent 2026.07.17 renamed the full-file body field to fileText; + // the overwrite guard must apply to those payloads too. + const projectDir = mkdtempSync(join(tmpdir(), "runtime-file-text-shrink-")); + const target = join(projectDir, "test.txt"); + writeFileSync(target, Array.from({ length: 100 }, (_, index) => String(index + 1)).join("\n") + "\n"); + const toolResults: any[] = []; + let interceptedCount = 0; + const result = await handleToolLoopEventV1({ + ...createBaseOptions({ + event: { + type: "tool_call", + call_id: "c_edit_path_file_text_shrink", + tool_call: { + editToolCall: { + args: { + path: target, + fileText: "49\ntest\n51", + }, + }, + }, + } as any, + allowedToolNames: new Set(["edit", "write"]), + toolSchemaMap: OPENCODE_EDIT_WRITE_SCHEMA_MAP, + onToolResult: async (toolResult) => { + toolResults.push(toolResult); + }, + onInterceptedToolCall: async () => { + interceptedCount += 1; + }, + }), + boundary: createProviderBoundary("v1", "cursor-acp"), + }); + + expect(result).toEqual({ intercepted: false, skipConverter: true }); + expect(interceptedCount).toBe(0); + expect(toolResults).toHaveLength(0); + expect(readFileSync(target, "utf-8").split("\n").slice(47, 52)).toEqual(["48", "49", "50", "51", "52"]); + }); + it("records completed Cursor-owned edits when skipping suspicious streamContent reroutes", async () => { const projectDir = mkdtempSync(join(tmpdir(), "runtime-cursor-owned-edit-")); const target = join(projectDir, "test.txt"); diff --git a/tests/unit/provider-tool-schema-compat.test.ts b/tests/unit/provider-tool-schema-compat.test.ts index 6d5ae1d..36b1617 100644 --- a/tests/unit/provider-tool-schema-compat.test.ts +++ b/tests/unit/provider-tool-schema-compat.test.ts @@ -931,6 +931,30 @@ describe("tool schema compatibility", () => { expect(args.path).toBeUndefined(); }); + it("tryRerouteEditToWrite handles opencode path plus fileText edit payloads", () => { + // cursor-agent 2026.07.17 emits full-file edit bodies under `fileText`; + // the alias must carry the content through the edit-to-write reroute. + const toolSchemaMap = buildOpencodeEditWriteSchemaMap(); + const call = editToolCall({ + path: "/tmp/x", + fileText: "49\ntest\n51", + }); + const compat = applyToolSchemaCompat(call, toolSchemaMap); + const rerouted = tryRerouteEditToWrite( + call, + compat, + new Set(["edit", "write"]), + toolSchemaMap, + ); + + expect(compat.validation.missing).toEqual(["oldString"]); + expect(rerouted?.function.name).toBe("write"); + const args = JSON.parse(rerouted?.function.arguments ?? "{}"); + expect(args.filePath).toBe("/tmp/x"); + expect(args.content).toBe("49\ntest\n51"); + expect(args.path).toBeUndefined(); + }); + it("tryRerouteEditToWrite uses oc_write when fallback tools are active", () => { const toolSchemaMap = new Map([ [