#!/usr/bin/env bash set -euo pipefail # CASAN H2 tool-input validation. # Validates a tool-call input JSON against a JSON-Schema-style spec # (required fields + property types + additionalProperties:false). # Stdlib-only — supports type, required, properties, additionalProperties, # enum. NOT a full JSON Schema engine (no $ref, no nested object recursion # beyond one level); scoped deliberately and labeled as such. # # Usage: validate-tool-input.sh # Exit: 0 valid, 2 invalid, 64 usage error. SCHEMA="${1:-}" INPUT="${2:-}" if [[ -z "$SCHEMA" || -z "$INPUT" || ! -f "$SCHEMA" || ! -f "$INPUT" ]]; then echo "Usage: validate-tool-input.sh " >&2 exit 64 fi python3 - "$SCHEMA" "$INPUT" <<'PY' import json, sys schema = json.load(open(sys.argv[1], encoding="utf-8")) data = json.load(open(sys.argv[2], encoding="utf-8")) TYPES = { "string": str, "integer": int, "number": (int, float), "boolean": bool, "object": dict, "array": list, } errors = [] if schema.get("type") == "object" and not isinstance(data, dict): errors.append("root: expected object") else: props = schema.get("properties", {}) for field in schema.get("required", []): if field not in data: errors.append(f"missing required field: {field}") if schema.get("additionalProperties") is False: for key in data: if key not in props: errors.append(f"unexpected field: {key}") for key, spec in props.items(): if key not in data: continue expected = spec.get("type") py = TYPES.get(expected) # bool is a subclass of int — guard so a boolean isn't accepted as integer if py and (not isinstance(data[key], py) or (expected in ("integer", "number") and isinstance(data[key], bool))): errors.append(f"field {key}: expected {expected}") if "enum" in spec and data[key] not in spec["enum"]: errors.append(f"field {key}: not in enum {spec['enum']}") if errors: sys.stderr.write("TOOL_INPUT_INVALID " + "; ".join(errors) + "\n") raise SystemExit(2) print("TOOL_INPUT_VALID") PY