Skip to content

fix(ai): enforce callOptionsSchema at runtime in ToolLoopAgent#14750

Merged
lgrammel merged 3 commits into
vercel:mainfrom
etairl:fix/enforce-call-options-schema
Apr 27, 2026
Merged

fix(ai): enforce callOptionsSchema at runtime in ToolLoopAgent#14750
lgrammel merged 3 commits into
vercel:mainfrom
etairl:fix/enforce-call-options-schema

Conversation

@etairl
Copy link
Copy Markdown
Contributor

@etairl etairl commented Apr 27, 2026

Background

ToolLoopAgentSettings.callOptionsSchema is declared and documented as a runtime schema for caller-supplied options, but ToolLoopAgent.prepareCall never invokes it. Any invariant a developer encodes in that schema is silently bypassed at runtime, and unchecked options flow straight into prepareCall and any instructions template that interpolates them — defeating both the validation guarantee and any input-shape assumptions downstream code makes.

Splitting this out per maintainer feedback on #14749 that callOptionsSchema enforcement should be its own dedicated PR.

Summary

  • packages/ai/src/agent/tool-loop-agent.ts: at the top of prepareCall, when callOptionsSchema is set and the caller passed options, validate via safeValidateTypes. On failure, throw InvalidArgumentError with the schema's error message. On success, swap the caller-supplied options for the validated (parsed) value so any schema transforms or defaults take effect for the rest of the call.
  • packages/ai/src/agent/tool-loop-agent.test.ts: regression tests covering the rejection path (out-of-enum value rejected before reaching the model) and the accept path (in-enum value passes through and the model is invoked normally).
  • Patch changeset.

The check is gated on options !== undefined, so existing callers that don't supply options are unaffected. Only agents that opted into callOptionsSchema see new behaviour — and that behaviour is exactly what the field name and docs already promised.

Manual Verification

  • pnpm --filter ai test:node -- src/agent/tool-loop-agent.test.ts — passes, including the two new callOptionsSchema tests.

Checklist

  • Tests have been added / updated (for bug fixes / features)
  • Documentation has been added / updated (for bug fixes / features)
  • A patch changeset for relevant packages has been added (for bug fixes / features - run pnpm changeset in the project root)
  • I have reviewed this pull request (self-review)

Documentation is unchanged: the existing callOptionsSchema JSDoc already describes the intended behaviour; this PR makes the runtime match the docs.

Future Work

None — this PR makes callOptionsSchema behave as documented.

Related Issues

Detected automatically by https://github.com/etairl/Probus

Comment on lines +88 to +91
// Validate caller-supplied `options` against `callOptionsSchema` if one
// was provided. Without this, the schema was effectively dead code:
// developers wiring untrusted input into `options` would get no runtime
// protection despite naming the field `callOptionsSchema`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

comment not needed

Comment on lines +96 to +107
const result = await safeValidateTypes({
value: options.options,
schema: this.settings.callOptionsSchema,
});
if (!result.success) {
throw new InvalidArgumentError({
parameter: 'options',
value: options.options,
message: `options failed callOptionsSchema validation: ${result.error.message}`,
});
}
options = { ...options, options: result.value };
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can just call validateTypes and rely on the original parse error. validateTypes supports additional context

etairl added a commit to etairl/ai that referenced this pull request Apr 27, 2026
Per review on vercel#14750: drop the explanatory comment block and replace
the safeValidateTypes + manual InvalidArgumentError throw with a single
validateTypes call. validateTypes already throws TypeValidationError on
failure and accepts a context for the field path, which provides the
same caller-facing identification without bespoke error wiring.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@lgrammel
Copy link
Copy Markdown
Collaborator

@etairl we have changed our security policy to allow only commits with verified signatures. I cannot merge as is. Would it be possible for you to enable commit signing and open a new PR with signed commits? alternatively I can recreate this

etairl and others added 2 commits April 27, 2026 20:37
`ToolLoopAgentSettings.callOptionsSchema` was declared and documented as
a runtime schema for `options`, but `tool-loop-agent.ts` never invoked
it. Any invariant a developer encoded in the schema was silently
bypassed at runtime, and unchecked `options` flowed straight into
`prepareCall` and any `instructions` template that interpolated them.

`ToolLoopAgent.prepareCall` now validates caller-supplied `options`
against `callOptionsSchema` (when set) via `safeValidateTypes`, throwing
`InvalidArgumentError` on failure before forwarding to `prepareCall` /
`generateText` / `streamText`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per review on vercel#14750: drop the explanatory comment block and replace
the safeValidateTypes + manual InvalidArgumentError throw with a single
validateTypes call. validateTypes already throws TypeValidationError on
failure and accepts a context for the field path, which provides the
same caller-facing identification without bespoke error wiring.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@etairl etairl force-pushed the fix/enforce-call-options-schema branch from d0c5669 to eeba282 Compare April 27, 2026 17:37
@etairl
Copy link
Copy Markdown
Contributor Author

etairl commented Apr 27, 2026

@etairl we have changed our security policy to allow only commits with verified signatures. I cannot merge as is. Would it be possible for you to enable commit signing and open a new PR with signed commits? alternatively I can recreate this

Rebased with signed commits.

@lgrammel lgrammel merged commit 0a51f7d into vercel:main Apr 27, 2026
17 of 18 checks passed
@etairl etairl deleted the fix/enforce-call-options-schema branch April 28, 2026 12:25
dancer added a commit that referenced this pull request Apr 30, 2026
…pAgent (#14853)

## background

backport of #14750 from main to release-v6.0

## related issues

backport of #14750
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.

2 participants