Skip to content

Truncated tool-call arguments can be silently written to disk (stale partial-parse nativeArgs reused on finalization failure) #1221

Description

@canblmz1

Summary

When a streamed tool call's arguments are cut off mid-value (e.g. the model hits max_tokens while still writing a string argument), the value that was only ever meant for live streaming-progress display can end up used as the final, executed value with no error or diagnostic indicating it was incomplete.

Where

Observed at commit abaf732bb7a96848a7c6f5dd2ce485b86bff086d.

  • src/core/assistant-message/NativeToolCallParser.ts: processStreamingChunk() uses the partial-json package (^0.1.7 in src/package.json) to extract a best-effort value from the JSON accumulated so far, purely to show progress during streaming - createPartialToolUse()'s write_to_file case builds nativeArgs.content directly from that lenient parse.
  • When the stream ends, finalizeStreamingToolCall() calls the strict parseToolCall() (JSON.parse on the full accumulated text). A genuinely truncated payload correctly throws and the function returns null.
  • src/core/task/Task.ts (search for finalizeStreamingToolCall returned null): when finalization returns null, the code reuses the existing tool-use object - the one built during streaming from the partial-json output - and just sets .partial = false. It does not clear nativeArgs.
  • src/core/assistant-message/presentAssistantMessage.ts: the guard here is !block.nativeArgs. Since nativeArgs was already populated during streaming, this guard does not trip.
  • src/core/tools/WriteToFileTool.ts: the only content check is newContent === undefined. A truncated-but-present string passes this too.

Repro / PoC

Given a write_to_file call whose content argument is cut off mid-value:

{"path":"config/database.yml","content":"production:\n  host: db.prod.internal\n  password: correct-horse-battery-sta

Running the actual partial-json package (v0.1.7, the version pinned in src/package.json) on this string:

parse(text) -> { path: "config/database.yml",
                  content: "production:\n  host: db.prod.internal\n  password: correct-horse-battery-sta" }

No error. content is present and looks like a normal, complete string - exactly what nativeArgs.content would still hold when finalization fails and the stale streaming-phase object is reused, so WriteToFileTool would write this content to disk exactly as shown, silently.

For contrast, running the same input through prefix-safe-json (an incremental JSON parser I maintain, built specifically around never exposing a value until it's unambiguously complete):

outcome: "truncated", executable: false
stableValue: { path: "config/database.yml" }   // content is absent, not guessed

Impact

Any tool whose schema includes a free-form string argument (file content, shell command, etc.) can have that argument silently truncated-but-executed when the model's response is cut off mid-argument - a data-integrity issue for any tool with a real side effect.

Context

This codebase shares this exact code path (including comments) with RooCodeInc/Roo-Code, which this appears to be derived from - I reported the same issue there as well, in case it's useful for cross-referencing a fix.

Suggested direction (not prescriptive)

When finalizeStreamingToolCall() returns null, clear/discard the reused tool-use object's nativeArgs (and ideally params) rather than only marking it non-partial - falling back to the existing !block.nativeArgs guard at that point instead of leaving stale partial-parse data in place.

I found this by cloning the repository and reading the real source (paths/lines above), then verified the specific claim about partial-json's output by running the actual package version you depend on - not speculation. Happy to answer questions or share the small repro script if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions