Expand description
Pre-canonicalisation argument validation for toolset tool dispatch.
The public entry point is validate_toolset_tool_args. It performs an
iterative, depth-bounded, node-count-bounded walk over a serde_json::Value to:
-
Reject any argument object that contains a JS-runtime-dangerous key (the denylist; see
ARGS_KEY_DENYLIST) at ANY depth, including keys inside objects nested within arrays. -
Reject payloads that nest deeper than
TOOLSET_ARGS_MAX_DEPTH. -
Reject payloads whose total node count exceeds
TOOLSET_ARGS_MAX_NODES. This closes the O(payload-width) unbounded case that the depth bound alone does not prevent (a flat object with millions of keys passes depth-1 but would queue millions of stack entries before the denylist check runs).
§Why this guard exists
When a JS extension attaches a toJSON method to an argument object, the
value that PASSES validation differs from the value DISPATCHED after
JSON.stringify (the toJSON hook runs during serialisation). In a
serde_json::Value world there is no live toJSON method — a Value is
inert data — so the Rust realisation of that invariant is: the exact
in-memory Value validated here is the one moved into dispatch with no
re-parse. The guard is still necessary because:
- Our canonical JSON output is consumed DOWNSTREAM by a JS agent runtime.
- A
toJSONkey in the serialised JSON output enables the serialisation-hook bypass in the downstream runtime. - Dangerous keys (
then,__proto__, etc.) enable thenable-hijack and prototype-pollution attacks downstream.
Rejecting these keys at the Rust validation layer prevents our serialised output from carrying them.
§Walk strategy
The walk is ITERATIVE — it uses an explicit heap-allocated work-stack
(Vec<(&Value, usize)>) rather than native C-stack recursion. This prevents
stack overflow on adversarially-deep payloads.
Note: serde_json’s own parse recursion limit (~128 on most builds) already
rejects pathologically-deep JSON before our layer, but a Value constructed
in-memory (e.g. in a unit test or by a future refactor) can exceed that limit.
The iterative walk is therefore bounded independently of the parse path.
§Mutation-before-guard invariant
The caller MUST pass the FINAL post-injection Value to this function. ALL
mutation (chain_id injection, envelope_xdr insertion, any future merge)
happens BEFORE this call; no insert/merge/serde round-trip occurs between this
function and from_value::<TypedArgs>. This is the “freeze before dispatch”
invariant: the validated Value is the dispatched Value.
Constants§
- ARGS_
KEY_ DENYLIST - The denylist of JS-runtime-dangerous object-key names.
- TOOLSET_
ARGS_ MAX_ DEPTH - Maximum nesting depth for toolset tool argument payloads.
- TOOLSET_
ARGS_ MAX_ NODES - Maximum total node count for toolset tool argument payloads.
Functions§
- validate_
toolset_ tool_ args - Validate a toolset tool argument payload before dispatch.