diff --git a/conformance/negative/README.md b/conformance/negative/README.md new file mode 100644 index 0000000..f548052 --- /dev/null +++ b/conformance/negative/README.md @@ -0,0 +1,95 @@ +# Negative conformance vectors for the enforcement layer + +Implements the proposal of issue #29 and the conformance-suite gap of issue #19. + +The security value of an enforcement point is defined by its behaviour on inputs that are +*almost* right. This directory holds test vectors for those inputs: actions that are correctly +signed but unauthorized, replayed, stale, bound to the wrong principal, carried by malformed +evidence, or stripped of their observability references. + +## Design rules + +**1. Two rejection verdicts, never one.** A rejection because evidence was measured and found +wrong (`REJECT`) is a different verdict from a rejection because evidence could not be measured +at all (`UNMEASURABLE`). Both must refuse the action. Collapsing them either lets unmeasurable +inputs fail open, or sends operators hunting for frauds that never happened. Every vector +declares which of the two it expects. + +**2. Every category carries a positive control.** Each category includes at least one +almost-identical input that MUST pass. A suite made only of must-reject inputs cannot +distinguish an enforcement layer that works from one that rejects everything. The runner fails +the whole suite if any category lacks its positive control. + +**3. Vectors are framework-agnostic.** A vector describes an action, its authorization +evidence, and the state the enforcement point can consult. It never references a specific +implementation. Each implementation provides one adapter (see `runner.py`). + +**4. Declared gaps are part of the suite.** Category 3 (expired or revoked mandates) is +covered here for expiry only. Revocation vectors are absent because the contributing +implementation has no revocation mechanism, by documented decision, and vectors for a path +never exercised in production would be design fiction. The gap is stated rather than filled. +Contributions from implementations that exercise revocation are the way to close it. + +## Failure codes (proposed enumeration) + +| Code | Verdict | Meaning | +|---|---|---| +| `CONTEXT_NOT_SUPPORTED` | REJECT | Signature valid, but the consulted state does not carry this action (wrong target, closed auction, exceeded bound) | +| `REPLAY_CONSUMED` | REJECT | The authorization was already used once; one authorization covers one act | +| `STALE_DECISION` | REJECT | The evidence is older than the declared freshness bound | +| `PRINCIPAL_MISMATCH` | REJECT | Approval bound to a different agent, tool, or signing device than the one acting | +| `REFERENCE_MISSING` | REJECT | A required observability reference is absent or zeroed | +| `REFERENCE_MISMATCH` | REJECT | The named reference diverges from the one already bound to this action's context | +| `MALFORMED_EVIDENCE` | UNMEASURABLE | Evidence present but not decodable to the declared shape; consumers must fail closed | +| `ORACLE_UNAVAILABLE` | UNMEASURABLE | The state needed to judge could not be read; absence of measurement is never a pass | + +## Vector schema + +All eighteen vectors live as a single array in `vectors/negative_vectors.json`; the schema +below describes one entry. + +```json +{ + "id": "neg-cat1-001", + "category": 1, + "description": "what makes this input almost right, and why it must fail", + "input": { + "action": { }, + "authorization": { }, + "state": { } + }, + "expected": { + "verdict": "REJECT | UNMEASURABLE | PASS", + "code": "one of the enumeration above, absent for PASS", + "reason_must_mention": ["substring the refusal message must contain"] + }, + "positive_control": false +} +``` + +`reason_must_mention` exists because a correct verdict with a wrong reason is a latent bug: +an enforcement point can reject for the wrong cause and pass the suite while hunting the +wrong class of attack. + +## Running + +``` +python conformance/negative/runner.py --adapter your_adapter.py +``` + +The adapter exposes `evaluate(vector) -> {"verdict": ..., "code": ..., "reason": ..., +"entry_point": ...}`, where `entry_point` names the production entry point the adapter +dispatched through for that vector. The runner exits non-zero on: any verdict mismatch, +any code mismatch (positive controls included), any missing `reason_must_mention` +substring, any category lacking a positive control (a control counts only if it expects +`PASS`), any answer naming no entry point, or any category whose positive controls +resolved through a different entry point than its negative vectors. The last two rules +exist because a suite whose positive controls certify a test double certifies nothing: +an adapter that reads the answer key passes every content check and fails only there. + +## Provenance + +Vectors are generalized from an enforcement layer running in production since June 2026 +(hash-chained decision registries, out-of-band human confirmation, on-chain context checks). +Each rejection code in the enumeration was produced by a real refusal before it was named +here. The field notes behind them are public: https://github.com/avp9-nexus/nexus-art diff --git a/conformance/negative/reference_adapter.py b/conformance/negative/reference_adapter.py new file mode 100644 index 0000000..29aca38 --- /dev/null +++ b/conformance/negative/reference_adapter.py @@ -0,0 +1,67 @@ +"""Reference adapter: a minimal enforcement layer that satisfies the negative suite. + +Ships WITH the suite for one reason: a runner nobody can execute end-to-end is a promise. +This adapter is the suite's own positive control. It is deliberately small and readable; +it is not a product. +""" + + +def evaluate(vector): + inp = vector["input"] + action, auth, state = inp.get("action", {}), inp.get("authorization", {}), inp.get("state", {}) + + def reject(code, reason): + return {"verdict": "REJECT", "code": code, "entry_point": "reference_adapter.decide", "reason": reason} + + def unmeasurable(code, reason): + return {"verdict": "UNMEASURABLE", "code": code, "entry_point": "reference_adapter.decide", "reason": reason} + + # Evidence shape first: fail closed on what cannot be decoded, with its own code. + oracle = state.get("oracle_response", "ABSENT_KEY") + if oracle is None: + return unmeasurable("ORACLE_UNAVAILABLE", "state could not be measured; absence is never a pass") + if isinstance(oracle, dict): + if oracle.get("shape") == "address" and not oracle.get("bytes_hex", "").startswith("0" * 24): + return unmeasurable("MALFORMED_EVIDENCE", "leading bytes do not encode the declared address type") + if oracle.get("shape", "").endswith("_6_fields") and oracle.get("fields_served") != 6: + return unmeasurable("MALFORMED_EVIDENCE", f"expected 6 fields, served {oracle.get('fields_served')}") + + # Observability references: the omission itself is refused, before anything else. + if "registry_ref" in action: + ref = action["registry_ref"] + if set(ref.replace("0x", "")) == {"0"}: + return reject("REFERENCE_MISSING", "required registry reference is zeroed") + target = state.get(action.get("target"), {}) + bound = target.get("bound_registry_ref") + if bound and bound != ref: + return reject("REFERENCE_MISMATCH", f"reference diverges from the one bound to this context ({bound})") + + # Principal binding. + if auth.get("decision_for") and auth["decision_for"] != action.get("actor"): + return reject("PRINCIPAL_MISMATCH", f"decision names agent {auth['decision_for']}, actor differs") + if "required_signer" in action and auth.get("connected_device_derives") not in (None, action["required_signer"]): + return reject("PRINCIPAL_MISMATCH", "connected device derives a different key; wrong device") + + # Replay. + if auth.get("consumed"): + return reject("REPLAY_CONSUMED", "this one-time authorization was already used") + if auth.get("session_secret") and auth.get("current_session"): + if auth["session_secret"] != f"SECRET_OF_SESSION_{auth['current_session']}": + return reject("REPLAY_CONSUMED", "secret belongs to a previous session") + + # Freshness. + if "decision_written_at" in auth: + from datetime import datetime + age = (datetime.fromisoformat(state["now"].replace("Z", "+00:00")) + - datetime.fromisoformat(auth["decision_written_at"].replace("Z", "+00:00"))).total_seconds() + if age > auth["freshness_bound_seconds"]: + return reject("STALE_DECISION", f"decision is {int(age)}s old, freshness bound exceeded") + + # Context: signature validity alone never suffices; the state must carry the action. + target = state.get(action.get("target"), {}) + if target.get("settled"): + return reject("CONTEXT_NOT_SUPPORTED", "target auction is settled; this action replays a closed context") + if "ceiling" in target and int(action.get("amount", 0)) > int(target["ceiling"]): + return reject("CONTEXT_NOT_SUPPORTED", f"amount exceeds the ceiling the state carries ({target['ceiling']})") + + return {"verdict": "PASS", "code": None, "entry_point": "reference_adapter.decide", "reason": "context supports the action"} diff --git a/conformance/negative/runner.py b/conformance/negative/runner.py new file mode 100644 index 0000000..60a5f81 --- /dev/null +++ b/conformance/negative/runner.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Negative conformance runner (issue #29, gap #19). + +Loads the vector suite, calls one implementation adapter per vector, and fails on: + - any verdict mismatch, + - any failure-code mismatch (positive controls included), + - any missing reason substring (a right verdict for a wrong reason is a latent bug), + - any category lacking its POSITIVE CONTROL (a control counts only if it expects PASS), + - any adapter answer that names no entry_point, and any category whose positive + controls dispatched through a different entry point than its negative vectors + (a suite that certifies a test double certifies nothing - review 4996153628). + +The last rule is structural, not cosmetic: a suite made only of must-reject inputs cannot +distinguish an enforcement layer that works from one that rejects everything. + +Adapter contract: a Python file exposing + evaluate(vector: dict) -> {"verdict": "PASS|REJECT|UNMEASURABLE", "code": str|None, + "reason": str, "entry_point": str} +where entry_point names the production entry point the adapter dispatched through. + +Usage: + python runner.py --adapter path/to/adapter.py [--vectors vectors/negative_vectors.json] +""" +import argparse +import importlib.util +import json +import sys +from collections import defaultdict +from pathlib import Path + +HERE = Path(__file__).parent + + +def load_adapter(path: Path): + # An adapter spanning several files must be able to import its siblings (review 4996153628). + sys.path.insert(0, str(path.resolve().parent)) + spec = importlib.util.spec_from_file_location("acs_adapter", path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + if not hasattr(mod, "evaluate"): + sys.exit(f"FATAL: adapter {path} exposes no evaluate(vector)") + return mod + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--adapter", required=True, type=Path) + ap.add_argument("--vectors", type=Path, default=HERE / "vectors" / "negative_vectors.json") + args = ap.parse_args() + + suite = json.loads(args.vectors.read_text(encoding="utf-8")) + vectors = suite["vectors"] + adapter = load_adapter(args.adapter) + + # Structural gate first: every category present must carry a positive control. + cats = defaultdict(lambda: {"neg": 0, "pos": 0}) + for v in vectors: + # A declared flag is not a positive control; only an expected PASS is (review 4996153628: + # a must-reject vector flagged positive_control satisfied the gate with zero must-pass inputs). + is_pos = v.get("positive_control") and v["expected"]["verdict"] == "PASS" + cats[v["category"]]["pos" if is_pos else "neg"] += 1 + missing = [c for c, k in sorted(cats.items()) if k["pos"] == 0] + if missing: + print(f"SUITE INVALID: categories without a positive control: {missing}") + print("A suite of pure rejections cannot tell a working layer from one that rejects everything.") + return 2 + + failures = [] + entry_points = defaultdict(lambda: {"pos": set(), "neg": set()}) + for v in vectors: + try: + out = adapter.evaluate(v) or {} + except Exception as e: # an adapter crash is a failure, never a skip + failures.append((v["id"], f"adapter raised {type(e).__name__}: {e}")) + continue + exp = v["expected"] + # The suite certifies an enforcement layer, not a test double (review 4996153628): + # every adapter answer must name the production entry point it dispatched through. + ep = out.get("entry_point") + if not ep: + failures.append((v["id"], "adapter reported no entry_point - cannot tell the enforcement layer from a test double")) + continue + entry_points[v["category"]]["pos" if v.get("positive_control") else "neg"].add(ep) + if out.get("verdict") != exp["verdict"]: + failures.append((v["id"], f"verdict {out.get('verdict')!r} != expected {exp['verdict']!r}")) + continue + # code and reason are compared on PASS vectors too (review 4996153628: a positive + # control's code field previously went uncompared). + if True: + if out.get("code") != exp.get("code"): + failures.append((v["id"], f"code {out.get('code')!r} != expected {exp.get('code')!r}")) + continue + reason = (out.get("reason") or "").lower() + for needle in exp.get("reason_must_mention", []): + if needle.lower() not in reason: + failures.append((v["id"], f"reason does not mention {needle!r}: {reason[:120]!r}")) + break + + for c, k in sorted(entry_points.items()): + if k["pos"] and k["neg"] and k["pos"] != k["neg"]: + failures.append((f"cat{c}", f"positive controls resolved through {sorted(k['pos'])} but negatives through {sorted(k['neg'])} - the gate certified a different code path than the one negatively tested")) + + print(f"{len(vectors) - len(failures)}/{len(vectors)} vectors conform " + f"({sum(k['pos'] for k in cats.values())} positive controls across {len(cats)} categories)") + for vid, why in failures: + print(f" FAIL {vid}: {why}") + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/conformance/negative/vector.schema.json b/conformance/negative/vector.schema.json new file mode 100644 index 0000000..963dbae --- /dev/null +++ b/conformance/negative/vector.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "ACS negative conformance vector suite", + "type": "object", + "required": ["suite", "vectors"], + "properties": { + "suite": { "type": "string" }, + "vectors": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["id", "category", "description", "input", "expected", "positive_control"], + "properties": { + "id": { "type": "string", "pattern": "^(neg|pos)-cat[0-9]+-[0-9]{3}$" }, + "category": { "type": "integer", "minimum": 1, "maximum": 6 }, + "description": { "type": "string", "minLength": 20 }, + "input": { + "type": "object", + "required": ["action", "authorization", "state"], + "properties": { + "action": { "type": "object" }, + "authorization": { "type": "object" }, + "state": { "type": "object" } + } + }, + "expected": { + "type": "object", + "required": ["verdict"], + "properties": { + "verdict": { "enum": ["PASS", "REJECT", "UNMEASURABLE"] }, + "code": { "enum": ["CONTEXT_NOT_SUPPORTED", "REPLAY_CONSUMED", "STALE_DECISION", "PRINCIPAL_MISMATCH", "REFERENCE_MISSING", "REFERENCE_MISMATCH", "MALFORMED_EVIDENCE", "ORACLE_UNAVAILABLE"] }, + "reason_must_mention": { "type": "array", "items": { "type": "string" } } + } + }, + "positive_control": { "type": "boolean" } + } + } + } + } +} diff --git a/conformance/negative/vectors/negative_vectors.json b/conformance/negative/vectors/negative_vectors.json new file mode 100644 index 0000000..1b7d3dd --- /dev/null +++ b/conformance/negative/vectors/negative_vectors.json @@ -0,0 +1,513 @@ +{ + "$schema": "../vector.schema.json", + "suite": "enforcement-negative-v0", + "vectors": [ + { + "id": "neg-cat1-001", + "category": 1, + "description": "Correctly-signed action targeting an item whose state no longer carries it: the auction is already settled. Signature verification alone passes; only consulting state catches it.", + "input": { + "action": { + "type": "commit_funds", + "target": "auction-41", + "amount": "1000" + }, + "authorization": { + "signature": "VALID", + "signer": "agent-A", + "scope": "auction-41" + }, + "state": { + "auction-41": { + "open": false, + "settled": true + } + } + }, + "expected": { + "verdict": "REJECT", + "code": "CONTEXT_NOT_SUPPORTED", + "reason_must_mention": [ + "settled" + ] + }, + "positive_control": false + }, + { + "id": "neg-cat1-002", + "category": 1, + "description": "Correctly-signed action exceeding a declared bound: amount above the ceiling the state carries. Almost right: same signer, same target, open state.", + "input": { + "action": { + "type": "commit_funds", + "target": "auction-42", + "amount": "9000" + }, + "authorization": { + "signature": "VALID", + "signer": "agent-A", + "scope": "auction-42" + }, + "state": { + "auction-42": { + "open": true, + "settled": false, + "ceiling": "5000" + } + } + }, + "expected": { + "verdict": "REJECT", + "code": "CONTEXT_NOT_SUPPORTED", + "reason_must_mention": [ + "ceiling" + ] + }, + "positive_control": false + }, + { + "id": "pos-cat1-001", + "category": 1, + "description": "POSITIVE CONTROL. Identical to neg-cat1-002 except the amount sits under the ceiling. MUST pass: an enforcement layer that rejects this is indistinguishable from one that rejects everything.", + "input": { + "action": { + "type": "commit_funds", + "target": "auction-42", + "amount": "1000" + }, + "authorization": { + "signature": "VALID", + "signer": "agent-A", + "scope": "auction-42" + }, + "state": { + "auction-42": { + "open": true, + "settled": false, + "ceiling": "5000" + } + } + }, + "expected": { + "verdict": "PASS" + }, + "positive_control": true + }, + { + "id": "neg-cat2-001", + "category": 2, + "description": "Replayed authorization: the same one-time approval presented for a second act. One authorization covers one act; the second presentation must fail even though every byte is valid.", + "input": { + "action": { + "type": "commit_funds", + "target": "auction-43", + "amount": "1000" + }, + "authorization": { + "signature": "VALID", + "signer": "agent-A", + "one_time_id": "auth-777", + "consumed": true + }, + "state": { + "auction-43": { + "open": true, + "settled": false + } + } + }, + "expected": { + "verdict": "REJECT", + "code": "REPLAY_CONSUMED", + "reason_must_mention": [ + "already" + ] + }, + "positive_control": false + }, + { + "id": "neg-cat2-002", + "category": 2, + "description": "Replay across sessions: an approval secret captured from a previous armed session, structurally valid, presented by a fresh confirmer. The session binding, not the secret's shape, is what must be checked.", + "input": { + "action": { + "type": "confirm_transaction", + "target": "tx-pending-9" + }, + "authorization": { + "signature": "VALID", + "session_secret": "SECRET_OF_SESSION_N_MINUS_1", + "current_session": "N" + }, + "state": { + "tx-pending-9": { + "awaiting_confirmation": true, + "session": "N" + } + } + }, + "expected": { + "verdict": "REJECT", + "code": "REPLAY_CONSUMED", + "reason_must_mention": [ + "session" + ] + }, + "positive_control": false + }, + { + "id": "neg-cat3-001", + "category": 3, + "description": "Stale decision: evidence older than the declared freshness bound. The decision was genuine when written; only its age disqualifies it. Expiry only — revocation vectors are a declared gap, see README.", + "input": { + "action": { + "type": "launch_bidder", + "target": "auction-44" + }, + "authorization": { + "signature": "VALID", + "decision_written_at": "2026-08-16T14:46:02Z", + "freshness_bound_seconds": 900 + }, + "state": { + "now": "2026-08-16T15:15:08Z", + "auction-44": { + "open": true + } + } + }, + "expected": { + "verdict": "REJECT", + "code": "STALE_DECISION", + "reason_must_mention": [ + "fresh" + ] + }, + "positive_control": false + }, + { + "id": "pos-cat3-001", + "category": 3, + "description": "POSITIVE CONTROL. Identical decision, re-issued inside the freshness bound. MUST pass.", + "input": { + "action": { + "type": "launch_bidder", + "target": "auction-44" + }, + "authorization": { + "signature": "VALID", + "decision_written_at": "2026-08-16T15:16:00Z", + "freshness_bound_seconds": 900 + }, + "state": { + "now": "2026-08-16T15:16:25Z", + "auction-44": { + "open": true + } + } + }, + "expected": { + "verdict": "PASS" + }, + "positive_control": true + }, + { + "id": "neg-cat4-001", + "category": 4, + "description": "Approval bound to the wrong principal: the decision names agent-B, the actor is agent-A. Everything else matches.", + "input": { + "action": { + "type": "launch_bidder", + "target": "auction-45", + "actor": "agent-A" + }, + "authorization": { + "signature": "VALID", + "decision_for": "agent-B" + }, + "state": { + "auction-45": { + "open": true + } + } + }, + "expected": { + "verdict": "REJECT", + "code": "PRINCIPAL_MISMATCH", + "reason_must_mention": [ + "agent" + ] + }, + "positive_control": false + }, + { + "id": "neg-cat4-002", + "category": 4, + "description": "Wrong signing device: the operation requires the device holding key K1, the connected device derives K2. The check must happen by reading the device, never by asking the operator which one is plugged in.", + "input": { + "action": { + "type": "sign_transaction", + "required_signer": "0xAAAA" + }, + "authorization": { + "connected_device_derives": "0xBBBB" + }, + "state": {} + }, + "expected": { + "verdict": "REJECT", + "code": "PRINCIPAL_MISMATCH", + "reason_must_mention": [ + "device" + ] + }, + "positive_control": false + }, + { + "id": "neg-cat5-001", + "category": 5, + "description": "Malformed evidence, regulation length: the payload has exactly the right byte length but does not encode the declared type (leading bytes non-zero where an address encoding requires zeros). A length check alone fails open here.", + "input": { + "action": { + "type": "read_context", + "target": "auction-46" + }, + "authorization": { + "signature": "VALID" + }, + "state": { + "oracle_response": { + "shape": "address", + "bytes_hex": "ffffffffffffffffffffffff1111111111111111111111111111111111111111" + } + } + }, + "expected": { + "verdict": "UNMEASURABLE", + "code": "MALFORMED_EVIDENCE", + "reason_must_mention": [ + "encod" + ] + }, + "positive_control": false + }, + { + "id": "neg-cat5-002", + "category": 5, + "description": "Truncated evidence: two fields served where six are declared. The consumer must refuse to conclude, never zero-fill the missing fields.", + "input": { + "action": { + "type": "read_context", + "target": "auction-47" + }, + "authorization": { + "signature": "VALID" + }, + "state": { + "oracle_response": { + "shape": "auction_struct_6_fields", + "fields_served": 2 + } + } + }, + "expected": { + "verdict": "UNMEASURABLE", + "code": "MALFORMED_EVIDENCE", + "reason_must_mention": [ + "6" + ] + }, + "positive_control": false + }, + { + "id": "neg-cat5-003", + "category": 5, + "description": "Oracle unreachable: the state needed to judge cannot be read at all. Absence of measurement is never a pass, and the code must differ from a measured rejection.", + "input": { + "action": { + "type": "commit_funds", + "target": "auction-48", + "amount": "1000" + }, + "authorization": { + "signature": "VALID", + "signer": "agent-A", + "scope": "auction-48" + }, + "state": { + "oracle_response": null + } + }, + "expected": { + "verdict": "UNMEASURABLE", + "code": "ORACLE_UNAVAILABLE", + "reason_must_mention": [ + "measur" + ] + }, + "positive_control": false + }, + { + "id": "neg-cat6-001", + "category": 6, + "description": "Stripped observability reference: the action's required registry reference is zeroed. The enforcement point must refuse the omission itself, before judging anything else.", + "input": { + "action": { + "type": "commit_funds", + "target": "auction-49", + "amount": "1000", + "registry_ref": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "authorization": { + "signature": "VALID", + "signer": "agent-A", + "scope": "auction-49" + }, + "state": { + "auction-49": { + "open": true + } + } + }, + "expected": { + "verdict": "REJECT", + "code": "REFERENCE_MISSING", + "reason_must_mention": [ + "reference" + ] + }, + "positive_control": false + }, + { + "id": "neg-cat6-002", + "category": 6, + "description": "Mismatched observability reference: a non-zero reference that diverges from the one already bound to this context by a prior act. Naming something is required; naming a DIFFERENT something must fail.", + "input": { + "action": { + "type": "commit_funds", + "target": "auction-50", + "amount": "2000", + "registry_ref": "0xbbbb..." + }, + "authorization": { + "signature": "VALID", + "signer": "agent-B", + "scope": "auction-50" + }, + "state": { + "auction-50": { + "open": true, + "bound_registry_ref": "0xaaaa..." + } + } + }, + "expected": { + "verdict": "REJECT", + "code": "REFERENCE_MISMATCH", + "reason_must_mention": [ + "bound" + ] + }, + "positive_control": false + }, + { + "id": "pos-cat6-001", + "category": 6, + "description": "POSITIVE CONTROL. Same action naming the SAME reference as the one bound. MUST pass: the check is on divergence, not on novelty.", + "input": { + "action": { + "type": "commit_funds", + "target": "auction-50", + "amount": "2000", + "registry_ref": "0xaaaa..." + }, + "authorization": { + "signature": "VALID", + "signer": "agent-B", + "scope": "auction-50" + }, + "state": { + "auction-50": { + "open": true, + "bound_registry_ref": "0xaaaa..." + } + } + }, + "expected": { + "verdict": "PASS" + }, + "positive_control": true + }, + { + "id": "pos-cat2-001", + "category": 2, + "description": "POSITIVE CONTROL. Same shape as neg-cat2-001 but the one-time authorization is unconsumed. MUST pass.", + "input": { + "action": { + "type": "commit_funds", + "target": "auction-43", + "amount": "1000" + }, + "authorization": { + "signature": "VALID", + "signer": "agent-A", + "one_time_id": "auth-778", + "consumed": false + }, + "state": { + "auction-43": { + "open": true, + "settled": false + } + } + }, + "expected": { + "verdict": "PASS" + }, + "positive_control": true + }, + { + "id": "pos-cat4-001", + "category": 4, + "description": "POSITIVE CONTROL. The connected device derives exactly the required key. MUST pass.", + "input": { + "action": { + "type": "sign_transaction", + "required_signer": "0xAAAA" + }, + "authorization": { + "connected_device_derives": "0xAAAA" + }, + "state": {} + }, + "expected": { + "verdict": "PASS" + }, + "positive_control": true + }, + { + "id": "pos-cat5-001", + "category": 5, + "description": "POSITIVE CONTROL. Well-formed evidence of the declared shape (address encoding with zeroed leading bytes). MUST pass: strictness is about shape, not about refusing evidence per se.", + "input": { + "action": { + "type": "read_context", + "target": "auction-46" + }, + "authorization": { + "signature": "VALID" + }, + "state": { + "oracle_response": { + "shape": "address", + "bytes_hex": "0000000000000000000000001111111111111111111111111111111111111111" + } + } + }, + "expected": { + "verdict": "PASS" + }, + "positive_control": true + } + ] +} \ No newline at end of file