Files
deepseek-harness/docs/rfc/implemented/2026-06-11-runtime-arg-validation.md
Tianyi Cui 7c400e9c02 docs: unify ADR/RFC trees into one lifecycle-organized RFC tree
Collapse docs/adr/ and docs/rfc/ into a single docs/rfc/ with proposed/,
implemented/, and rejected/ subfolders. Every file is renamed to
yyyy-mm-dd-topic-title.md, where the date is when the topic was first
proposed (from git history). ADRs and RFCs that covered exactly the same
topic are merged (property-based testing, session persistence); the
umbrella RFC 005 stays split across its three implemented decisions, and
RFC 006's deferred part-3 (API extractor reports) splits into its own
proposed RFC. All cross-references become machine-checkable relative
links instead of bare "ADR NNNN" / "RFC NNN" prose.

Add a verify-md-links doc-sync gate (scripts/verify-md-links.ts) that
checks every relative Markdown cross-link resolves, wired into doc-sync
alongside verify-md-wrap. This makes the reorganization self-verifying:
the same change that rewrote ~forty inter-doc links adds the check that
proves none dangle. Document the cross-link convention in a new
docs/AGENTS.md and record the gate as an implemented RFC.

doc-sync, typecheck, lint, and the full test suite (667) all pass.
2026-06-18 02:18:24 +08:00

2.5 KiB

RFC: Runtime arg validation at the model boundary

Status: implemented (accepted 2026-06-13)

Context

defineTool (the custom schema DSL) 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 to be closed by a property test (property-based testing, not yet landed) generating args that satisfy InferArgs and asserting they pass validateArgs. Until then the agreement rests on the example tests and the shared converter structure.
  • 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.