Skip to content

fix(plugins): harden tool result middleware#71241

Merged
vincentkoc merged 1 commit into
mainfrom
fix/tool-result-middleware-hardening
Apr 24, 2026
Merged

fix(plugins): harden tool result middleware#71241
vincentkoc merged 1 commit into
mainfrom
fix/tool-result-middleware-hardening

Conversation

@vincentkoc

Copy link
Copy Markdown
Member

Summary

  • Restricts registerAgentToolResultMiddleware(...) to bundled plugins and keeps captured registrations' harness metadata.
  • Fails closed on middleware exceptions, validates rewritten result shape/size, and avoids logging raw middleware error text.
  • Preserves unique Pi fallback toolCallIds, propagates Codex raw error state, and anchors Codex media trust checks to the raw tool result provenance.
  • Updates docs/changelog to mark the middleware seam as trusted bundled-only.

Review feedback covered

  • Captured registration now retains harness options.
  • Pi missing call ids use generated per-call ids instead of toolName.
  • Runner catch is now the fail-closed boundary for rethrown registry handler failures.
  • Codex middleware receives isError for failed raw tool results.
  • Pi bridge no longer uses reference equality to drop in-place middleware mutations.
  • Middleware registration is bundled-only; malformed or throwing middleware cannot pass raw output onward.
  • Codex media URL filtering uses raw tool provenance after middleware rewrites.

Validation

  • pnpm test:serial src/agents/harness/tool-result-middleware.test.ts src/agents/codex-app-server.extensions.test.ts src/agents/pi-embedded-runner.extensions.test.ts extensions/codex/src/app-server/dynamic-tools.test.ts src/plugins/captured-registration.test.ts
  • pnpm plugin-sdk:api:check
  • git diff --check
  • pnpm check:changed

Notes

  • pnpm docs:list still reports the pre-existing missing front matter on docs/AGENTS.md; the docs inventory itself completed.

@vincentkoc vincentkoc self-assigned this Apr 24, 2026
@vincentkoc
vincentkoc marked this pull request as ready for review April 24, 2026 20:12
@aisle-research-bot

aisle-research-bot Bot commented Apr 24, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

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

# Severity Title
1 🟠 High Tool result provenance bypass via in-place mutation in tool-result middleware pipeline
2 🟠 High Unbounded aggregate tool result payload allows memory/CPU DoS via middleware output (text/image blocks + unvalidated details)
1. 🟠 Tool result provenance bypass via in-place mutation in tool-result middleware pipeline
Property Value
Severity High
CWE CWE-285
Location src/agents/harness/tool-result-middleware.ts:68-97

Description

createCodexDynamicToolBridge attempts to base media trust decisions on the pre-middleware tool result by passing mediaTrustResult: rawResult into filterToolResultMediaUrls. However, the middleware runner passes the same object reference (event.result) into middleware handlers without cloning/freezing. A middleware can mutate event.result.details in-place (e.g., remove mcpServer/mcpTool) and thereby also mutate rawResult.

Impact:

  • isToolResultMediaTrusted() treats results with details.mcpServer/details.mcpTool as external/untrusted.
  • If a middleware removes/rewrites these provenance fields in-place, the subsequent trust check may incorrectly treat an external tool result as trusted and allow non-HTTP (local) media paths to pass filtering.
  • This undermines the intended provenance anchoring and can lead to local file path exposure/unsafe media handling depending on downstream consumers of toolMediaUrls.

Vulnerable flow:

  • Input: tool output object rawResult
  • Mutation opportunity: middleware handler receives {...event, result: current} where current is the same object
  • Sink: filterToolResultMediaUrls(toolName, mediaUrls, mediaTrustResult) where mediaTrustResult is expected to be immutable/raw

Recommendation

Treat tool results as immutable across middleware boundaries.

Options:

  1. Deep-clone the tool result before passing it into middleware, and separately keep an immutable snapshot for provenance decisions.
  2. Deep-freeze (or structuredClone + freeze) the provenance object used for trust checks so middleware cannot mutate it.

Example fix (clone before middleware + keep snapshot):

const rawResult = await tool.execute(...);
const rawSnapshot = structuredClone(rawResult);

const middlewareResult = await middlewareRunner.applyToolResultMiddleware({
  ...,// pass a clone so handlers can't mutate the snapshot/raw
  result: structuredClone(rawResult),
});

collectToolTelemetry({
  ...,
  result: middlewareResult,
  mediaTrustResult: rawSnapshot,
});

Also consider updating the middleware runner to defensively clone event.result per handler invocation (or freeze it) to prevent accidental in-place mutation.

2. 🟠 Unbounded aggregate tool result payload allows memory/CPU DoS via middleware output (text/image blocks + unvalidated details)
Property Value
Severity High
CWE CWE-400
Location src/agents/harness/tool-result-middleware.ts:12-46

Description

createAgentToolResultMiddlewareRunner validates the shape of result.content blocks but allows extremely large aggregate payloads and does not validate or cap result.details at all.

Impact (attacker-controlled plugin / compromised middleware):

  • A middleware can return up to 200 content blocks.
  • Each text block can be 100,000 chars; each image.data can be 5,000,000 chars.
  • There is no cumulative size cap, so a single tool result can approach ~1,000,000,000 chars of image data alone (200 × 5,000,000), which can cause process OOM, excessive GC, serialization overhead, and downstream amplification.
  • details is accepted with any size/shape, enabling additional memory blowups and expensive serialization (and it is read by downstream code, e.g. error detection).

Downstream amplification example:

  • In extensions/codex/src/app-server/dynamic-tools.ts, image blocks are converted into data:${mimeType};base64,${data} strings, further increasing memory usage per block and potentially sending massive payloads over IPC/HTTP.

Vulnerable code:

const MAX_MIDDLEWARE_CONTENT_BLOCKS = 200;
const MAX_MIDDLEWARE_TEXT_CHARS = 100_000;
const MAX_MIDDLEWARE_IMAGE_DATA_CHARS = 5_000_000;
...
return value.content.every(isValidMiddlewareContentBlock);// NOTE: no validation / size cap for `value.details`

Recommendation

Add defense-in-depth validation for the entire tool result, including details, and enforce aggregate size limits.

Suggested approach:

  1. Cap total characters across all text blocks and all image.data fields (and ideally cap total bytes after base64 decode for images).
  2. Validate details to be JSON-serializable and bounded (e.g., depth/keys/total string length), or drop it entirely from middleware outputs.
  3. Optionally restrict mimeType to a safe allowlist (e.g., image/png, image/jpeg, image/webp) and verify image.data is base64.

Example (sketch):

const MAX_TOTAL_TEXT_CHARS = 200_000;
const MAX_TOTAL_IMAGE_DATA_CHARS = 5_000_000; // total, not per-block
const MAX_DETAILS_JSON_CHARS = 50_000;

function safeJsonSize(value: unknown, maxChars: number): boolean {
  try {
    return JSON.stringify(value).length <= maxChars;
  } catch {
    return false;
  }
}

function isValidMiddlewareToolResult(value: unknown): value is OpenClawAgentToolResult {
  if (!isRecord(value) || !Array.isArray(value.content)) return false;
  if (value.content.length > MAX_MIDDLEWARE_CONTENT_BLOCKS) return false;

  let totalText = 0;
  let totalImage = 0;
  for (const block of value.content) {
    if (!isValidMiddlewareContentBlock(block)) return false;
    if (block.type === "text") totalText += block.text.length;
    if (block.type === "image") totalImage += block.data.length;
    if (totalText > MAX_TOTAL_TEXT_CHARS || totalImage > MAX_TOTAL_IMAGE_DATA_CHARS) return false;
  }// either drop `details` or strictly bound it
  if ("details" in value && !safeJsonSize((value as any).details, MAX_DETAILS_JSON_CHARS)) return false;

  return true;
}

Analyzed PR: #71241 at commit 97c4877

Last updated on: 2026-04-24T20:15:06Z

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation agents Agent runtime and tooling extensions: codex size: M maintainer Maintainer-authored PR labels Apr 24, 2026
@greptile-apps

greptile-apps Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR hardens the agent tool-result middleware seam: restricts registration to bundled plugins, makes the runner fail closed on middleware exceptions (returning a safe error result instead of raw output), validates rewritten result shapes and sizes, generates unique per-call IDs for Pi events with missing toolCallIds, propagates the raw isError flag into Codex middleware, and anchors Codex media trust checks to raw tool provenance rather than the post-middleware result.

Three P2 observations worth noting:

  • The isToolResultError allowlist in dynamic-tools.ts treats any unknown details.status string (e.g. "pending", "in_progress") as an error, which could suppress media telemetry or send an incorrect isError: true signal to middleware for valid non-failure outcomes.
  • The details field is not size-bounded in isValidMiddlewareToolResult; only content blocks are validated, leaving middleware free to return an arbitrarily large details object.
  • Middleware that mutates event.result in place and returns undefined bypasses isValidMiddlewareToolResult entirely — intentional per the PR description but undocumented.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 quality/hardening observations that do not block correctness.

The bundled-only guard, fail-closed error handling, and raw-provenance media trust fixes are all correct. The three P2 gaps are non-blocking: middleware is already restricted to trusted bundled plugins, so the attack surface for the validation gaps is minimal, and the isToolResultError concern is about telemetry accuracy rather than correctness.

extensions/codex/src/app-server/dynamic-tools.ts (isToolResultError allowlist) and src/agents/harness/tool-result-middleware.ts (details size bound, in-place mutation note).

Comments Outside Diff (1)

  1. extensions/codex/src/app-server/dynamic-tools.ts, line 327-349 (link)

    P2 Allowlist-based isToolResultError may over-classify valid statuses as errors

    The status allowlist ("ok", "success", "completed", "running", "0", "") treats any unknown string — e.g. "partial", "pending", "in_progress", "cancelled", "warning" — as an error. When a tool produces one of these non-standard but non-failure statuses, rawIsError will be true, which: (1) signals isError: true to bundled middleware like tokenjuice (possibly skipping compression), and (2) suppresses media-URL telemetry collection for successful tool calls. Given that this value was previously hardcoded to false, it may be worth expanding the allowlist or switching to an explicit error-detection blocklist ("failed", "error", "timeout", etc.) to reduce the false-positive surface.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/codex/src/app-server/dynamic-tools.ts
    Line: 327-349
    
    Comment:
    **Allowlist-based `isToolResultError` may over-classify valid statuses as errors**
    
    The status allowlist (`"ok"`, `"success"`, `"completed"`, `"running"`, `"0"`, `""`) treats any unknown string — e.g. `"partial"`, `"pending"`, `"in_progress"`, `"cancelled"`, `"warning"` — as an error. When a tool produces one of these non-standard but non-failure statuses, `rawIsError` will be `true`, which: (1) signals `isError: true` to bundled middleware like tokenjuice (possibly skipping compression), and (2) suppresses media-URL telemetry collection for successful tool calls. Given that this value was previously hardcoded to `false`, it may be worth expanding the allowlist or switching to an explicit error-detection blocklist (`"failed"`, `"error"`, `"timeout"`, etc.) to reduce the false-positive surface.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/codex/src/app-server/dynamic-tools.ts
Line: 327-349

Comment:
**Allowlist-based `isToolResultError` may over-classify valid statuses as errors**

The status allowlist (`"ok"`, `"success"`, `"completed"`, `"running"`, `"0"`, `""`) treats any unknown string — e.g. `"partial"`, `"pending"`, `"in_progress"`, `"cancelled"`, `"warning"` — as an error. When a tool produces one of these non-standard but non-failure statuses, `rawIsError` will be `true`, which: (1) signals `isError: true` to bundled middleware like tokenjuice (possibly skipping compression), and (2) suppresses media-URL telemetry collection for successful tool calls. Given that this value was previously hardcoded to `false`, it may be worth expanding the allowlist or switching to an explicit error-detection blocklist (`"failed"`, `"error"`, `"timeout"`, etc.) to reduce the false-positive surface.

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

---

This is a comment left during a code review.
Path: src/agents/harness/tool-result-middleware.ts
Line: 38-46

Comment:
**`details` field has no size bound in result validation**

`isValidMiddlewareToolResult` validates the `content` array (count + per-block size), but the `details` field is not constrained at all. A bundled middleware could return an arbitrarily large or deeply nested `details` object and it would pass validation, be forwarded to `collectToolTelemetry`, and potentially serialized downstream. Since the validation goal is to bound memory impact, adding a rough byte-budget check or a depth/key-count guard on `details` would close this gap.

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

---

This is a comment left during a code review.
Path: src/agents/harness/tool-result-middleware.ts
Line: 73-86

Comment:
**In-place mutations of `event.result` bypass shape/size validation**

The handler receives `{ ...event, result: current }`, where `result` is a reference to `current`. If a middleware mutates `event.result.content` in place and returns `undefined`, `next?.result` is falsy so `isValidMiddlewareToolResult` is never called — but `current` reflects the mutation. The Pi bridge test deliberately exercises this pattern, so this is intentional for backwards compatibility, but it means the `MAX_MIDDLEWARE_CONTENT_BLOCKS` / `MAX_MIDDLEWARE_TEXT_CHARS` limits cannot be enforced against in-place mutations. A comment documenting the design choice would prevent future confusion.

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

Reviews (1): Last reviewed commit: "fix(plugins): harden tool result middlew..." | Re-trigger Greptile

Comment on lines +38 to +46
function isValidMiddlewareToolResult(value: unknown): value is OpenClawAgentToolResult {
if (!isRecord(value) || !Array.isArray(value.content)) {
return false;
}
if (value.content.length > MAX_MIDDLEWARE_CONTENT_BLOCKS) {
return false;
}
return value.content.every(isValidMiddlewareContentBlock);
}

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 details field has no size bound in result validation

isValidMiddlewareToolResult validates the content array (count + per-block size), but the details field is not constrained at all. A bundled middleware could return an arbitrarily large or deeply nested details object and it would pass validation, be forwarded to collectToolTelemetry, and potentially serialized downstream. Since the validation goal is to bound memory impact, adding a rough byte-budget check or a depth/key-count guard on details would close this gap.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/harness/tool-result-middleware.ts
Line: 38-46

Comment:
**`details` field has no size bound in result validation**

`isValidMiddlewareToolResult` validates the `content` array (count + per-block size), but the `details` field is not constrained at all. A bundled middleware could return an arbitrarily large or deeply nested `details` object and it would pass validation, be forwarded to `collectToolTelemetry`, and potentially serialized downstream. Since the validation goal is to bound memory impact, adding a rough byte-budget check or a depth/key-count guard on `details` would close this gap.

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

Comment on lines 73 to 86
try {
const next = await handler({ ...event, result: current }, ctx);
if (next?.result) {
current = next.result;
if (isValidMiddlewareToolResult(next.result)) {
current = next.result;
} else {
log.warn(
`[${ctx.harness}] discarded invalid tool result middleware output for ${truncateUtf16Safe(
event.toolName,
120,
)}`,
);
}
}

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 In-place mutations of event.result bypass shape/size validation

The handler receives { ...event, result: current }, where result is a reference to current. If a middleware mutates event.result.content in place and returns undefined, next?.result is falsy so isValidMiddlewareToolResult is never called — but current reflects the mutation. The Pi bridge test deliberately exercises this pattern, so this is intentional for backwards compatibility, but it means the MAX_MIDDLEWARE_CONTENT_BLOCKS / MAX_MIDDLEWARE_TEXT_CHARS limits cannot be enforced against in-place mutations. A comment documenting the design choice would prevent future confusion.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/harness/tool-result-middleware.ts
Line: 73-86

Comment:
**In-place mutations of `event.result` bypass shape/size validation**

The handler receives `{ ...event, result: current }`, where `result` is a reference to `current`. If a middleware mutates `event.result.content` in place and returns `undefined`, `next?.result` is falsy so `isValidMiddlewareToolResult` is never called — but `current` reflects the mutation. The Pi bridge test deliberately exercises this pattern, so this is intentional for backwards compatibility, but it means the `MAX_MIDDLEWARE_CONTENT_BLOCKS` / `MAX_MIDDLEWARE_TEXT_CHARS` limits cannot be enforced against in-place mutations. A comment documenting the design choice would prevent future confusion.

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

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

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97c4877b23

ℹ️ 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".

},
],
details: {
status: "failed",

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 Badge Use canonical error status for middleware failure result

The fail-closed fallback currently sets details.status to "failed", but Pi run processing only classifies tool-result failures when status is "error" or "timeout" (see isToolResultError in src/agents/pi-embedded-subscribe.tools.ts). When middleware throws in Pi, this synthetic result is therefore treated as a successful tool completion, so downstream error-handling paths (like failure bookkeeping and guarded side-effect handling) are skipped even though the tool output was replaced due to post-processing failure.

Useful? React with 👍 / 👎.

@vincentkoc
vincentkoc merged commit 7bd7475 into main Apr 24, 2026
96 of 99 checks passed
@vincentkoc
vincentkoc deleted the fix/tool-result-middleware-hardening branch April 24, 2026 20:23
Angfr95 pushed a commit to Angfr95/openclaw that referenced this pull request Apr 25, 2026
GONZO304 pushed a commit to GONZO304/openclaw that referenced this pull request Apr 28, 2026
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling docs Improvements or additions to documentation extensions: codex maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant