OpenClaw's xAI/Venice Tool-Call Fix Is a Byte-Level Reminder That Agent Safety Starts Before the Tool Runs

OpenClaw's xAI/Venice Tool-Call Fix Is a Byte-Level Reminder That Agent Safety Starts Before the Tool Runs

Tool-call security usually gets discussed at the dramatic boundary: the moment an agent wants to run a command, edit a file, send a message, or touch production state. That is the visible checkpoint, so it gets the approval dialog, the audit event, and the argument about whether humans should stay in the loop. PR #90260 is a useful correction to that mental model because it shows the boundary starts earlier and lower: at the exact bytes the provider emits before the runtime ever asks whether the tool should run.

The OpenClaw patch targets xAI and Venice tool-call arguments that contain literal HTML, XML, or Markdown entities. These providers can emit escaped content, so an intended literal & may arrive as &, where exactly one decode is the compatibility transform the runtime wants. The bug was that OpenClaw could decode the same shared arguments object more than once before execution and transcript persistence. A value intended to remain & could collapse to &. That is not formatting trivia. That is a different input to the tool.

The root cause is the kind of orchestration detail that only looks small until you debug it in production. OpenClaw's wrapStreamMessageObjects path decoded event.partial, event.message, and the object returned by an overridden result(). On a done event, a provider can set partial === message, and result() can return that same object again. The decoder then mutates one object through several aliases. Because decodeHtmlEntities is intentionally non-idempotent, repeated calls keep changing the content: & becomes &, then &.

Provider compatibility code is a shared-runtime concern

The xAI path had a second stacked-decoder problem. applyXaiModelCompat already set toolCallArgumentsEncoding: "html-entities", which installed the core compatibility decoder. But wrapXaiProviderStream also carried its own provider-local decoder. Two wrappers doing the same job is exactly how runtime compatibility turns into runtime corruption. The patch removes the duplicate provider-local xAI decoder and makes the shared decoder mark each arguments object as decoded once, using object identity rather than trying to infer safety from string content.

The file diff tells the editorial story better than a feature announcement would. extensions/xai/stream.ts drops 144 lines and gains one. The core decoder gains a small amount of state and tests. The xAI stream tests add focused coverage. ClawSweeper summarized the PR as source -134, tests +77, total -57 across four files. That is the kind of negative-code security patch worth liking: fewer provider-specific branches, more central invariants.

The review discussion also focused on the right thing. A reviewer noted that the new WeakSet is module-scoped and keys by object identity. That is not incidental implementation taste. It is the contract. If the same arguments object reaches the decoder through multiple event references, decode it once. If a new object arrives, decode that object once too. This is the shape of a real runtime invariant, not another string-normalization guess.

Why should practitioners care if they do not use xAI or Venice? Because this class of bug is provider-shaped today and platform-shaped tomorrow. Agent runtimes increasingly normalize model outputs across OpenAI-compatible APIs, Anthropic-style tool use, local inference servers, proxy providers, and custom adapters. Every compatibility transform has to preserve an auditable chain: what the model emitted, what the runtime transformed, what the approval UI displayed, what the tool received, and what the transcript stored. If any of those disagree silently, your forensic story is already compromised.

Consider the ordinary workloads affected by a double decode. A documentation agent updating HTML examples may change literal entity text. A config-editing agent may mutate XML fragments. A Markdown-writing agent may alter links or escaped characters. A code agent may patch a fixture where the difference between & and & is the test. None of those require an attacker. The model can be correct, the approval can be granted, and the runtime can still execute the wrong bytes.

The security angle is not theoretical either. Approval systems are only meaningful when the reviewed arguments match the executed arguments. If the approval prompt displays one representation and the tool receives another after hidden normalization, then human-in-the-loop becomes theater. The same is true for replay. If the corrupted value is persisted, later debugging inherits false evidence. Engineers will blame the model, the provider, or the user prompt when the real failure was an orchestration layer that mutated shared state too many times.

The action item for teams building or operating agent platforms is straightforward: test tool-call argument integrity at the byte level. Do not only assert that a tool was called or that the output looks semantically plausible. Add regression cases for literal ampersands, nested HTML entities, XML fragments, Markdown links, JSON strings containing escaped text, Unicode normalization, shell-escaped values, and replay persistence. Then verify the intended compatibility transform exactly once. "Equivalent enough" is not good enough when the tool is editing files or calling APIs.

OpenClaw's fix is narrow, but the lesson is broad. Provider adapters should not each reinvent decoding, schema cleanup, argument repair, or compatibility shims if the runtime can own those transforms centrally. Duplication creates drift; drift creates mutation; mutation creates logs that cannot be trusted. The right abstraction is boring and strict: model-specific quirks are declared, the core applies a single bounded transform, and every downstream surface sees the same canonical object.

The headline is not that OpenClaw fixed an HTML entity bug. The headline is that agent safety starts before the tool runs. If the control plane cannot prove that model-emitted arguments become tool-received arguments through one deliberate transform, then it does not have a tool boundary. It has a suggestion box with side effects.

Sources: OpenClaw PR #90260, PR file diff