Skip to content

🤖 fix(stream): reject excessively nested tool inputs before SDK parsing - #4336

Open
ThomasK33 wants to merge 2 commits into
mainfrom
ThomasK33/plan-review-depth-live
Open

ThomasK33 wants to merge 2 commits into
mainfrom
ThomasK33/plan-review-depth-live

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Sep 22, 2026

Copy link
Copy Markdown
Member

Summary

Reject tool calls whose JSON input exceeds the chosen nesting limit before the AI SDK parses it. The model receives the normal invalid-input tool error instead of a stack overflow, and the rejected call does not execute—even with a permissive tool schema.

This is the live-input prerequisite for #4317. Recovery of already-persisted deep payloads is a separate layer above this one.

Review focus

  1. jsonTextExceedsDepth scans the raw text without recursion and ignores quoted/escaped brackets.
  2. The replacement is deliberately invalid JSON. A valid replacement object could pass a permissive schema and execute a tool with fabricated arguments.
  3. ProviderModelFactory installs the guard before SDK parsing. Every object-model version receives the guard through the SDK’s existing adapters, including Copilot Responses v2. DevTools retains its prior core-v4 scope outside the guard, and model cleanup ownership is transferred.

Validation

The native-Node regression exercises depths 1200, 2100, 4000, and 6000 through the real streaming service. It checks zero tool executions, normal turn completion, and an error result in the next provider request. Depths 2 and 256 and quoted-bracket inputs remain controls.

On 89ed0a7553f1be668f2f967b093edafbab8234d5: make static-check passed, 188 focused Bun tests passed, and all 13 native-Node integration cases passed. A real Copilot Responses factory-route regression first demonstrated deep-input execution on the previous head; the guarded route now rejects it while its shallow control executes. Streaming v2/v3 adapter coverage uses test models, not live external providers. The current gateway SDK is v4; no real v3 factory route is claimed. Bun 1.3.5, Node 22.19.0, and main's frozen AI SDK 7.0.107 dependencies were used. Combined remote UAT and the stack's code/security reviews and CI remain merge gates.

Compatibility

256 is an explicit safety policy, not a claim about every external tool. Calls above that nesting limit now fail explicitly. This layer does not repair existing history or add persistence, UI, or provider settings.


Generated with xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $760.34

…re the SDK parses them

Deeply nested tool-call input (observed at depth ~1200/2100 in the app) aborts
the turn in AI SDK 7: parseToolCall's outer catch re-parses the raw text even
for an INVALID call and cloneModelMessages then recursively clones the deep
object at step finish (RangeError, surfaced as a bogus context_exceeded). With
a permissive schema (z.record / passthrough) the deep payload was even executed.

- toolInputDepthGuardMiddleware (v4; innermost wrap in ProviderModelFactory,
  DevTools outside so it records what the SDK saw): a non-recursive,
  string/escape-aware scanner bounds the raw `tool-call.input` text and
  replaces over-deep input with an intentionally INVALID JSON diagnostic, so
  the SDK stamps the call `invalid` (never executed regardless of schema) and
  the model receives the normal invalid-input tool error. `tool-input-delta`
  text passes through untouched; nothing parses it.
- MAX_TOOL_PAYLOAD_JSON_DEPTH = 256: a policy safety bound below the observed
  V8/SDK failures (app ~1200; in-process Node 22 harness ~4000).
- Node/Jest regression (tests/ipc/streaming/toolPayloadDepth.test.ts, live
  half): 1200/2100/4000/6000 rejected and never executed with a permissive
  schema, turn settles; depth 2 and 256 execute unchanged; quoted/escaped
  brackets are not nesting. Bun unit test for the scanner and the
  invalid-JSON invariant of the diagnostic.
- providerModelFactory test no longer asserts the concrete model class (the
  `<name>.chat` provider id already proves adapter selection).

Persisted rows that already hold a deep payload are handled by the follow-up
history layer.

_Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh`_

<!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh -->

Signed-off-by: Thomas Kosiewski <tk@coder.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-22T15:54:30.487000Z 89ed0a7 New commits
🔒 Security Review Completed 2026-09-22T15:56:05.598770Z 89ed0a7 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0760d7b032

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/providerModelFactory.ts
…, not only v4

Review finding (PR #4336): createModelEffect returned early for any non-v4
model, so the CopilotResponses model (v2) and any v3 provider never received
the depth guard and a permissive schema executed deep tool input unguarded.

- Wrap every object model with the guard middleware. wrapLanguageModel runs
  the SDK's own asLanguageModelV4 adapters (v2→v3→v4) — the same adaptation
  streamText/generateText perform before consuming the model — so no new
  adapter is introduced. Cleanup ownership moves to the wrapper as before.
- DevTools keeps its existing constraint, now gated on the core model's
  version because the guard wrapper always reports v4. turnRequestBuilder's
  run-metadata queue keys entries per request and the request clears them,
  so a queued entry nothing consumes cannot leak into another run.
- Tests: actual factory route for github-copilot Codex (v2 CopilotResponses)
  via generateText with a permissive tool — depth-2100 arguments are turned
  into an invalid call and never executed (red on the C1 base: executed),
  shallow arguments execute unchanged; Jest live suite adds v2/v3 mock models
  through the same wrap (deep rejected, shallow executes). The two Copilot
  assertions on the concrete class now check the adapted v4 wrapper via its
  provider id.

_Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh`_

<!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh -->

Signed-off-by: Thomas Kosiewski <tk@coder.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant