Skip to content

Fix null params for parameterless tools#72673

Merged
amknight merged 2 commits into
mainfrom
codex/fix-null-tool-params-loop
Apr 27, 2026
Merged

Fix null params for parameterless tools#72673
amknight merged 2 commits into
mainfrom
codex/fix-null-tool-params-loop

Conversation

@amknight

Copy link
Copy Markdown
Member

Summary

  • Normalize null or missing tool arguments to {} for root object schemas that have no required parameters.
  • Preserve validation failures for object schemas that still require parameters.
  • Add a regression that drives the real pi-agent loop with a wiki_lint-style tool call whose arguments are null.

Root Cause

The upstream pi-agent loop validates tool arguments before invoking OpenClaw's wrapped execute handler. When a model serialized an empty argument object as null, parameterless object-schema tools failed validation before OpenClaw's loop/outcome tracking could observe execution. The existing prepareArguments seam runs before that validation step, so OpenClaw now uses it for schemas where {} is valid.

Fixes #72587

Validation

  • pnpm test src/agents/pi-tools.schema.test.ts
  • Mutation check: temporarily removed the fix and confirmed the new regression fails before restoring it.
  • pnpm exec oxfmt --check --threads=1 src/agents/pi-tools.schema.ts src/agents/pi-tools.schema.test.ts
  • pnpm check:changed

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S maintainer Maintainer-authored PR labels Apr 27, 2026
@amknight
amknight marked this pull request as ready for review April 27, 2026 07:15
@greptile-apps

greptile-apps Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where parameterless object-schema tools failed upstream validation when a model serialized an empty arguments object as null. It adds a prepareArguments wrapper in normalizeToolParameters that coerces null/undefined{} only for object schemas with no required parameters, and adds three test cases including a full runAgentLoop regression.

Confidence Score: 4/5

Safe to merge; the one edge case (required params inside allOf/oneOf) is unlikely in practice and is a P2 hardening concern.

The fix is correct for the described bug and all standard object schemas. A single P2 concern exists around composite schemas (allOf/oneOf) where required params are nested rather than top-level — these would have their null args silently coerced to {} instead of failing validation. This is an uncommon schema pattern and does not affect the described use case.

src/agents/pi-tools.schema.ts — specifically isObjectSchemaWithNoRequiredParams if composite schemas with nested required become common in this codebase.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/pi-tools.schema.ts
Line: 11-23

Comment:
**`required` inside `allOf`/`oneOf` subschemas is not detected**

`isObjectSchemaWithNoRequiredParams` only reads `record.required` at the top level of the schema object. A JSON-Schema like `{ type: "object", allOf: [{ required: ["q"] }] }` — which genuinely requires `q` — has no top-level `required` array, so the function returns `true` and a `prepareArguments` wrapper is installed. Any `null` call would be silently coerced to `{}`, bypassing the validation error that the caller would otherwise receive. The `normalizeToolParameterSchema` utility leaves top-level `allOf` schemas unchanged per the existing test suite, so the gap persists after normalization.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix tool null params for parameterless s..." | Re-trigger Greptile

Comment on lines +11 to +23
function isObjectSchemaWithNoRequiredParams(schema: unknown): boolean {
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
return false;
}
const record = schema as Record<string, unknown>;
const type = record.type;
const hasObjectType =
type === "object" || (Array.isArray(type) && type.some((entry) => entry === "object"));
if (!hasObjectType) {
return false;
}
return !Array.isArray(record.required) || record.required.length === 0;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 required inside allOf/oneOf subschemas is not detected

isObjectSchemaWithNoRequiredParams only reads record.required at the top level of the schema object. A JSON-Schema like { type: "object", allOf: [{ required: ["q"] }] } — which genuinely requires q — has no top-level required array, so the function returns true and a prepareArguments wrapper is installed. Any null call would be silently coerced to {}, bypassing the validation error that the caller would otherwise receive. The normalizeToolParameterSchema utility leaves top-level allOf schemas unchanged per the existing test suite, so the gap persists after normalization.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-tools.schema.ts
Line: 11-23

Comment:
**`required` inside `allOf`/`oneOf` subschemas is not detected**

`isObjectSchemaWithNoRequiredParams` only reads `record.required` at the top level of the schema object. A JSON-Schema like `{ type: "object", allOf: [{ required: ["q"] }] }` — which genuinely requires `q` — has no top-level `required` array, so the function returns `true` and a `prepareArguments` wrapper is installed. Any `null` call would be silently coerced to `{}`, bypassing the validation error that the caller would otherwise receive. The `normalizeToolParameterSchema` utility leaves top-level `allOf` schemas unchanged per the existing test suite, so the gap persists after normalization.

How can I resolve this? If you propose a fix, please make it concise.

@aisle-research-bot

aisle-research-bot Bot commented Apr 27, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Potential DoS via unbounded recursive traversal of untrusted JSON Schemas in schemaHasRequiredParams()
1. 🟡 Potential DoS via unbounded recursive traversal of untrusted JSON Schemas in schemaHasRequiredParams()
Property Value
Severity Medium
CWE CWE-674
Location src/agents/pi-tools.schema.ts:25-46

Description

schemaHasRequiredParams() recursively descends into allOf/anyOf/oneOf variants with no depth limit and no cycle detection.

In this codebase, tool parameters schemas can originate from external MCP servers (e.g., bundle MCP tools materialize parameters: tool.inputSchema from listTools() responses). An attacker-controlled or unusually complex schema can therefore trigger:

  • Stack overflow via extremely deep nesting of union/composite schemas
  • Event-loop blocking / CPU spike from large recursive traversals

Vulnerable code:

function schemaHasRequiredParams(schema: Record<string, unknown>): boolean {
  if (Array.isArray(schema.required) && schema.required.length > 0) {
    return true;
  }
  for (const key of ["allOf", "anyOf", "oneOf"]) {
    const variants = schema[key];
    if (!Array.isArray(variants)) {
      continue;
    }
    if (
      variants.some(
        (variant) =>
          variant !== null &&
          typeof variant === "object" &&
          !Array.isArray(variant) &&
          schemaHasRequiredParams(variant as Record<string, unknown>),
      )
    ) {
      return true;
    }
  }
  return false;
}

This function is called during normalizeToolParameters() when deciding whether to inject a prepareArguments wrapper for object schemas with no required params.

Because MCP tool schemas are not necessarily trusted, the recursion can be abused for denial-of-service.

Recommendation

Add recursion guards when walking schemas:

  • Track visited objects to avoid cycles (use WeakSet<object>)
  • Enforce a maximum depth (and/or maximum node count) to prevent pathological schemas from consuming CPU/stack
  • Prefer an explicit stack/queue (iterative traversal) over recursion

Example (iterative + guards):

function schemaHasRequiredParams(schema: Record<string, unknown>, opts?: { maxDepth?: number }) {
  const maxDepth = opts?.maxDepth ?? 50;
  const seen = new WeakSet<object>();
  const stack: Array<{ node: Record<string, unknown>; depth: number }> = [{ node: schema, depth: 0 }];

  while (stack.length) {
    const { node, depth } = stack.pop()!;
    if (depth > maxDepth) {// Treat as "has required" (safe default) or bail out explicitly.
      return true;
    }
    if (seen.has(node)) continue;
    seen.add(node);

    if (Array.isArray(node.required) && node.required.length > 0) return true;

    for (const key of ["allOf", "anyOf", "oneOf"] as const) {
      const variants = node[key];
      if (!Array.isArray(variants)) continue;
      for (const variant of variants) {
        if (variant && typeof variant === "object" && !Array.isArray(variant)) {
          stack.push({ node: variant as Record<string, unknown>, depth: depth + 1 });
        }
      }
    }
  }
  return false;
}

Also consider applying similar limits in normalizeToolParameterSchema if it also recursively walks schema graphs.


Analyzed PR: #72673 at commit 3e51b31

Last updated on: 2026-04-27T07:26:21Z

@amknight amknight changed the title [codex] Fix null params for parameterless tools Fix null params for parameterless tools Apr 27, 2026
@amknight
amknight merged commit 4e19bc8 into main Apr 27, 2026
123 of 131 checks passed
@amknight
amknight deleted the codex/fix-null-tool-params-loop branch April 27, 2026 07:46
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
* fix tool null params for parameterless schemas

* guard composite required tool schemas
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
* fix tool null params for parameterless schemas

* guard composite required tool schemas
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
* fix tool null params for parameterless schemas

* guard composite required tool schemas
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
* fix tool null params for parameterless schemas

* guard composite required tool schemas
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
* fix tool null params for parameterless schemas

* guard composite required tool schemas
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
* fix tool null params for parameterless schemas

* guard composite required tool schemas
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Tool call infinite loop on validation failure — null params bypass loopDetection

1 participant