Files
deepseek-harness/docs/adr/0011-runtime-arg-validation.md
T
Tianyi Cui 36a30180b8 feat(tools): validate model-generated tool args at the boundary (RFC 005 pt 1)
defineTool now runs validateArgs against the SchemaSpec before execute, so a
malformed model call returns a self-correctable isError result listing the
violations instead of reaching the typed body untyped-in-practice. The
validator mirrors schemaSpecToJsonSchema semantics exactly (required from
required:true only, extra keys allowed, default not applied, object/array
without properties/items only type-checks, enum membership).

tool-bash's hand-rolled type/required checks (carrying the TODO(RFC 005)
stopgap note) are slimmed to just the value constraints the DSL can't express
(non-empty strings, positive timeout). Graduates RFC 005 pt 1 to ADR 0011.
2026-06-13 23:00:42 +08:00

2.2 KiB

ADR 0011: Runtime arg validation at the model boundary

Status: accepted (2026-06-13)

Context

defineTool (ADR 0005) gives tool authors a typed execute(args) via the InferArgs<S> mapping. But that type is a compile-time claim about a value that arrives at runtime as model-generated JSON: nothing forced the model to honor the schema, so a malformed call — missing a required key, a string where a number was declared, an enum value outside the set — reached execute typed-in-name-only. The tool body then either crashed on the bad shape (a generic stack trace the model can't act on) or, worse, silently misbehaved. Meanwhile the converter already encodes the exact structure a validator would need to walk.

Decision

validateArgs(spec, args): string[] interprets a SchemaSpec over a runtime value, returning human-readable violations (empty = valid), and is total (never throws). defineTool runs it before the typed body; on violations it throws ToolArgsError (code: 'INVALID_ARGS', message listing the violations), which the registry's existing execute-waterfall catch turns into an isError result the model reads and self-corrects from.

The validator mirrors schemaSpecToJsonSchema semantics exactly — same structure walked, same rules: top level must be a non-array object; required keys come only from required: true; extra keys are allowed (no additionalProperties: false); default is not applied; an object/array prop without properties/items only type-checks; enum is membership. Raw-registered (MCP) tools are not touched — they validate their own input.

Consequences

  • The model gets actionable feedback on its own malformed calls instead of an opaque crash, closing the gap between InferArgs's promise and runtime reality.
  • The validator and InferArgs must stay in agreement; that drift risk is closed by a property test (RFC 001) generating args that satisfy InferArgs and asserting they pass validateArgs.
  • ToolArgsError is a plain Error with a code field for now; if a harness-wide error taxonomy lands it becomes a subclass without changing callers that read .message.
  • Validation cost is negligible next to a model call.