Skip to content

Harden model-facing decoding against common LLM tool-call quirks (0.2.2)#6

Merged
zac merged 1 commit into
mainfrom
defensive-fixes
Jul 13, 2026
Merged

Harden model-facing decoding against common LLM tool-call quirks (0.2.2)#6
zac merged 1 commit into
mainfrom
defensive-fixes

Conversation

@zac

@zac zac commented Jul 13, 2026

Copy link
Copy Markdown
Member

Defensive fixes prompted by a field failure: an executeJavaScript call was rejected before any JS ran because the model sent timeoutMs as the string "10000" and omitted allowedCapabilityKeys/context. Root cause was strict, schema-misaligned decoding at the model-facing boundary. This hardens that boundary against the common LLM output quirks.

Changes

  1. Registry coerces declared .number/.bool arguments from strings before validation. A numeric string ("20") becomes a number; "true"/"false" (case-insensitive only — not 1/0 or "yes") becomes a bool. Applied to every declared argument, including nested dotted paths (e.g. options.timeoutMs), so both the type check and the bridge see canonical values. Safety gates like confirmed still require an explicit true/false.

  2. JavaScriptExecutionRequest gets a tolerant init(from:). timeoutMs accepts a number or a numeric string; and — matching the advertised schema where only code + allowedCapabilities are required — allowedCapabilityKeys, timeoutMs, and context may now be omitted. The synthesized decoder wrongly required all three, so a valid call that left them out was rejected outright.

  3. Unknown/misspelled capability IDs give a clear error. An unrecognized value in allowedCapabilities now names the offending value(s) and points at searchJavaScriptAPI, instead of an opaque Codable failure.

Audited the other model-facing Codable input (JavaScriptAPISearchRequest): just a code string, nothing to harden.

Tests

  • New DefensiveDecodingTests (11 cases): numeric-string / number / null / omitted timeoutMs, minimal {code, allowedCapabilities}, unknown-capability error, Codable round-trip, and registry coercion (numeric string, case-insensitive bools, nested dotted path, and non-coercion of "yes"/1/0/non-numeric).
  • Retargets registryValidationRejectsWrongArgumentType at a bool (a genuine type mismatch, not a coerced quirk).
  • Full suite: 241 pass. Deterministic evals: 49/49.

Intended as release 0.2.2.

Three defensive fixes so ordinary model output stops nuking whole calls:

1. Registry coerces declared .number/.bool arguments from strings before
   validation: a numeric string ("20") becomes a number and "true"/"false"
   (case-insensitive only — not 1/0 or "yes") becomes a bool, at every declared
   argument including nested dotted paths. Both the type check and the bridge
   then see canonical values. Safety gates like "confirmed" still require an
   explicit true/false.

2. JavaScriptExecutionRequest gets a tolerant init(from:): timeoutMs accepts a
   number or a numeric string, and — matching the advertised schema where only
   code + allowedCapabilities are required — allowedCapabilityKeys, timeoutMs,
   and context may be omitted. The synthesized decoder wrongly required all
   three, so a valid call that left them out (as the field report did) was
   rejected before any JS ran.

3. An unknown/misspelled capability ID in allowedCapabilities now yields a clear
   error naming the offending value(s) and pointing at searchJavaScriptAPI,
   instead of an opaque Codable failure.

Audited the other model-facing Codable input (JavaScriptAPISearchRequest): just
a code string, nothing to harden.

Adds DefensiveDecodingTests (11 cases) and retargets
registryValidationRejectsWrongArgumentType at a bool (a genuine type mismatch,
not a coerced quirk). 241 tests pass; deterministic evals 49/49.
@zac
zac merged commit 9e58efd into main Jul 13, 2026
4 checks passed
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