feat: plan 16 P2 batch 2 (SEC-12 drift, SEC-29 audit fail-closed, SEC-30 replay, SEC-15 low)

- SEC-12: drift-detect adds semantic invariants — negation-flip detection (a dropped
  "not" now FAILS despite high char-similarity) + env must-keep patterns.
- SEC-29 (X-05): governance-check audit write fails CLOSED — an unwritable audit log
  denies the action and empties the output (no unaudited output).
- SEC-30 (X-06): approval-verify records a one-time-use nonce (sha of token/sig) and
  rejects replays (enforced mode / when a nonce ledger is set); dev unchanged.
- SEC-15 (low): typosquat distance<=2 with the levenshtein length-sentinel bug fixed
  (no false positives); tool-exec fails closed with no timeout backend in enforced
  mode; validate-tool-input now validates nested objects/arrays recursively.

Verify: new SEC suites all green via gate, run-casan4 0-FAIL, adversarial 44/44,
track-c 29/0, h5-approval 12/0, no regressions.

Plan-16 P2 remaining: infra-gated only (SEC-14/22/23/24/25/26).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
thanhnv
2026-07-06 22:55:24 +09:00
co-authored by Claude Opus 4.8
parent 8c06a55aed
commit d695a598ee
14 changed files with 365 additions and 30 deletions
@@ -30,28 +30,39 @@ TYPES = {
}
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']}")
# SEC-15: validate RECURSIVELY. Previously only the top level was checked, so a
# nested object could smuggle wrong types / unexpected fields past the gate.
def validate(schema, data, path):
expected = schema.get("type")
py = TYPES.get(expected)
if py and (not isinstance(data, py)
or (expected in ("integer", "number") and isinstance(data, bool))):
errors.append(f"{path or 'root'}: expected {expected}")
return
if "enum" in schema and data not in schema["enum"]:
errors.append(f"{path or 'root'}: not in enum {schema['enum']}")
if expected == "object" and isinstance(data, dict):
props = schema.get("properties", {})
for field in schema.get("required", []):
if field not in data:
errors.append(f"{path or 'root'}: missing required field: {field}")
if schema.get("additionalProperties") is False:
for key in data:
if key not in props:
errors.append(f"{path or 'root'}: unexpected field: {key}")
for key, spec in props.items():
if key in data:
validate(spec, data[key], f"{path}.{key}" if path else key)
elif expected == "array" and isinstance(data, list):
item_spec = schema.get("items")
if isinstance(item_spec, dict):
for i, item in enumerate(data):
validate(item_spec, item, f"{path}[{i}]")
validate(schema, data, "")
if errors:
sys.stderr.write("TOOL_INPUT_INVALID " + "; ".join(errors) + "\n")