bindable 0.1.1__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {bindable-0.1.1 → bindable-0.1.3}/PKG-INFO +1 -1
  2. {bindable-0.1.1 → bindable-0.1.3}/bindable/__init__.py +9 -3
  3. {bindable-0.1.1 → bindable-0.1.3}/bindable/_canonical_record.py +4 -4
  4. bindable-0.1.3/bindable/_version.py +1 -0
  5. {bindable-0.1.1 → bindable-0.1.3}/bindable/client.py +9 -4
  6. bindable-0.1.3/bindable/decision.py +75 -0
  7. {bindable-0.1.1 → bindable-0.1.3}/pyproject.toml +1 -1
  8. bindable-0.1.3/tests/fixtures/KEY_PROVENANCE.md +7 -0
  9. bindable-0.1.3/tests/fixtures/bindable-test-rc4-2026-08.pem +4 -0
  10. bindable-0.1.3/tests/fixtures/receipt_public_blocked_rc4.json +1 -0
  11. bindable-0.1.3/tests/fixtures/receipt_public_released_rc4.json +1 -0
  12. {bindable-0.1.1 → bindable-0.1.3}/tests/test_canonical_vectors.py +2 -2
  13. {bindable-0.1.1 → bindable-0.1.3}/tests/test_client.py +4 -4
  14. bindable-0.1.3/tests/test_decision_from.py +104 -0
  15. {bindable-0.1.1 → bindable-0.1.3}/tests/test_verify_receipt.py +16 -11
  16. {bindable-0.1.1 → bindable-0.1.3}/tests/test_version.py +1 -1
  17. bindable-0.1.1/bindable/_version.py +0 -1
  18. bindable-0.1.1/tests/fixtures/KEY_PROVENANCE.md +0 -1
  19. bindable-0.1.1/tests/fixtures/bindable-public-2026-07.pem +0 -4
  20. bindable-0.1.1/tests/fixtures/receipt_public_blocked_rc3.json +0 -1
  21. bindable-0.1.1/tests/fixtures/receipt_public_released_rc3.json +0 -1
  22. {bindable-0.1.1 → bindable-0.1.3}/.gitignore +0 -0
  23. {bindable-0.1.1 → bindable-0.1.3}/LICENSE +0 -0
  24. {bindable-0.1.1 → bindable-0.1.3}/NOTICE +0 -0
  25. {bindable-0.1.1 → bindable-0.1.3}/README.md +0 -0
  26. {bindable-0.1.1 → bindable-0.1.3}/bindable/crypto/__init__.py +0 -0
  27. {bindable-0.1.1 → bindable-0.1.3}/bindable/crypto/canonical.py +0 -0
  28. {bindable-0.1.1 → bindable-0.1.3}/bindable/crypto/hash.py +0 -0
  29. {bindable-0.1.1 → bindable-0.1.3}/bindable/crypto/keys.py +0 -0
  30. {bindable-0.1.1 → bindable-0.1.3}/bindable/crypto/sign.py +0 -0
  31. {bindable-0.1.1 → bindable-0.1.3}/bindable/errors.py +0 -0
  32. {bindable-0.1.1 → bindable-0.1.3}/bindable/py.typed +0 -0
  33. {bindable-0.1.1 → bindable-0.1.3}/bindable/verify.py +0 -0
  34. {bindable-0.1.1 → bindable-0.1.3}/tests/__init__.py +0 -0
  35. {bindable-0.1.1 → bindable-0.1.3}/tests/crypto/sign_vectors.json +0 -0
  36. {bindable-0.1.1 → bindable-0.1.3}/tests/crypto/vectors.json +0 -0
  37. {bindable-0.1.1 → bindable-0.1.3}/tests/test_signature_verify.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bindable
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Python client for the hosted Bindable Action Gate, with offline Receipt verification
5
5
  Author: Bindable AI LLC
6
6
  License-Expression: Apache-2.0
@@ -1,22 +1,28 @@
1
1
  """Bindable — Python client for the hosted Bindable Action Gate.
2
2
 
3
3
  Submit a Proposal to the Bindable Action Gate via ``ActionGate.decide()``,
4
- receive a Decision (the Outcome), and verify the returned Receipt offline with
5
- ``verify_receipt``. The client is pure transport; all proof-rule evaluation
6
- happens on the hosted gate, and receipt verification is fully offline.
4
+ receive an Outcome, and verify the returned Receipt offline with
5
+ ``verify_receipt``. Derive the Decision — released or blocked — from the
6
+ signed Receipt with ``decision_from``. The client is pure transport; all
7
+ proof-rule evaluation happens on the hosted gate, and receipt verification
8
+ is fully offline.
7
9
  """
8
10
 
9
11
  from ._version import __version__
10
12
  from .client import ActionGate
11
13
  from .crypto import load_public_key
14
+ from .decision import BLOCKED, RELEASED, decision_from
12
15
  from .errors import BindableAPIError
13
16
  from .verify import ReceiptVerification, verify_receipt
14
17
 
15
18
  __all__ = [
19
+ "BLOCKED",
20
+ "RELEASED",
16
21
  "ActionGate",
17
22
  "BindableAPIError",
18
23
  "ReceiptVerification",
19
24
  "__version__",
25
+ "decision_from",
20
26
  "load_public_key",
21
27
  "verify_receipt",
22
28
  ]
@@ -14,7 +14,7 @@ verified_at handling (de-pydantic departure, behaviour-preserving):
14
14
  In the engine, ``Receipt.verified_at`` is a ``datetime`` and the
15
15
  builder calls ``_serialize_wire_datetime`` to produce the wire
16
16
  string (YYYY-MM-DDTHH:MM:SS.sssZ). In the thin client the receipt
17
- arrives from /v1/decide as JSON, so ``verified_at`` is ALREADY the
17
+ arrives from /v1/review as JSON, so ``verified_at`` is ALREADY the
18
18
  wire string. It is used verbatim — passing it back through a
19
19
  datetime serializer would be a double-serialization bug. No
20
20
  ``datetime`` object can reach this path, so ``_serialize_wire_datetime``
@@ -42,7 +42,7 @@ def _canonical_hash_input(receipt: Mapping[str, Any]) -> dict[str, Any]:
42
42
  counterparty_identity, failed_rule, nonce,
43
43
  persistent_agent_id, previous_hash, proposed_action_id,
44
44
  referenced_evidence, sponsor_identity,
45
- sponsor_identity_context, verification_outcome, verified_at.
45
+ sponsor_identity_context, verdict, verified_at.
46
46
 
47
47
  Excluded per §12.3:
48
48
  receipt_id, signature, signing_key_id, signed_at,
@@ -51,7 +51,7 @@ def _canonical_hash_input(receipt: Mapping[str, Any]) -> dict[str, Any]:
51
51
 
52
52
  Args:
53
53
  receipt: A receipt as a plain mapping (e.g. parsed JSON from
54
- /v1/decide). All 15 included fields must be present;
54
+ /v1/review). All 15 included fields must be present;
55
55
  nullable fields carry ``None``. ``verified_at`` must be
56
56
  the wire-format string already.
57
57
 
@@ -73,6 +73,6 @@ def _canonical_hash_input(receipt: Mapping[str, Any]) -> dict[str, Any]:
73
73
  "referenced_evidence": receipt["referenced_evidence"],
74
74
  "sponsor_identity": receipt["sponsor_identity"],
75
75
  "sponsor_identity_context": receipt["sponsor_identity_context"],
76
- "verification_outcome": receipt["verification_outcome"],
76
+ "verdict": receipt["verdict"],
77
77
  "verified_at": receipt["verified_at"],
78
78
  }
@@ -0,0 +1 @@
1
+ __version__ = "0.1.3"
@@ -1,6 +1,6 @@
1
1
  """Transport client for the hosted Bindable Action Gate.
2
2
 
3
- ``ActionGate.decide()`` submits a Proposal to ``POST /v1/decide`` and returns
3
+ ``ActionGate.decide()`` submits a Proposal to ``POST /v1/review`` and returns
4
4
  the hosted Decision (the Outcome dict) verbatim. The client is pure transport:
5
5
  it does NOT evaluate, canonicalize, or decide anything locally — all
6
6
  proof-rule evaluation happens on the hosted gate. To verify the returned
@@ -24,7 +24,7 @@ from bindable.errors import BindableAPIError
24
24
  class ActionGate:
25
25
  """Thin, token-authenticated httpx client for the hosted Bindable Action Gate.
26
26
 
27
- Transports a Proposal to the hosted ``/v1/decide`` endpoint and returns the
27
+ Transports a Proposal to the hosted ``/v1/review`` endpoint and returns the
28
28
  Decision verbatim as a dict. The client performs no local evaluation,
29
29
  canonicalization, or decision-making; offline receipt verification is a
30
30
  separate concern handled by ``bindable.verify_receipt``.
@@ -55,13 +55,18 @@ class ActionGate:
55
55
  proposed_action_id: str,
56
56
  nonce: str | None = None,
57
57
  ) -> dict[str, Any]:
58
- """Submit a Proposal to ``/v1/decide`` and return the hosted Outcome dict.
58
+ """Submit a Proposal to ``/v1/review`` and return the hosted Outcome dict.
59
59
 
60
60
  Sends exactly the five wire fields (persistent_agent_id, action_type,
61
61
  payload, proposed_action_id, nonce) with bearer-token auth. The Outcome
62
62
  is returned verbatim; its ``receipt`` field (when present) can be passed
63
63
  to ``verify_receipt`` for offline verification.
64
64
 
65
+ The endpoint moved from ``/v1/decide`` to ``/v1/review`` in 0.1.2. The
66
+ platform keeps BOTH paths live for a deprecation window, so an older
67
+ client pinned to ``/v1/decide`` keeps working; new callers get
68
+ ``/v1/review``.
69
+
65
70
  Args:
66
71
  persistent_agent_id: The stable agent identity.
67
72
  action_type: The action being proposed.
@@ -87,7 +92,7 @@ class ActionGate:
87
92
  "nonce": nonce,
88
93
  }
89
94
  response = self._client.post(
90
- f"{self._base_url}/v1/decide",
95
+ f"{self._base_url}/v1/review",
91
96
  json=body,
92
97
  headers={
93
98
  "Authorization": f"Bearer {self._token}",
@@ -0,0 +1,75 @@
1
+ """The published derivation of a Decision from a signed Receipt.
2
+
3
+ The Action Review does not return released/blocked. Callers derive it from
4
+ the Receipt, by this rule and only this rule:
5
+
6
+ released iff verdict == "passed"
7
+ blocked for every other verdict, and for any Receipt that cannot be
8
+ obtained or read.
9
+
10
+ This is the CANONICAL, PUBLIC derivation. It is deliberately small enough
11
+ to reimplement in any language — TypeScript, Go, Rust, Java — in a few
12
+ lines, and any such reimplementation MUST behave identically:
13
+
14
+ // TypeScript, equivalent by construction
15
+ const decisionFrom = (r: any): "released" | "blocked" =>
16
+ r?.verdict === "passed" ? "released" : "blocked";
17
+
18
+ Two properties are load-bearing and must be preserved in every port:
19
+
20
+ 1. The comparison is EXACT. ``"passed"`` releases; ``"Passed"``,
21
+ ``"PASSED"``, ``" passed"``, and ``True`` do not. Leniency here would
22
+ let two implementations disagree about the same Receipt.
23
+ 2. Blocked is the SAFE DEFAULT. A missing verdict, an unreadable Receipt,
24
+ a null, a non-mapping, or anything unexpected yields "blocked". This
25
+ function never raises and never returns "released" on ambiguity.
26
+
27
+ Reading the verdict is all this does. It performs no I/O, no
28
+ canonicalization, and no cryptography, and it does not establish that the
29
+ Receipt is authentic — pair it with ``verify_receipt`` when the Receipt's
30
+ integrity matters.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ from collections.abc import Mapping
36
+ from typing import Any
37
+
38
+ #: The Decision vocabulary. These exact strings are the wire words.
39
+ RELEASED = "released"
40
+ BLOCKED = "blocked"
41
+
42
+ #: The one verdict value that releases an action.
43
+ PASSED = "passed"
44
+
45
+
46
+ def decision_from(receipt: Mapping[str, Any] | None) -> str:
47
+ """Derive the Decision from a Receipt: ``"released"`` or ``"blocked"``.
48
+
49
+ Returns ``"released"`` if and only if the Receipt's ``verdict`` field
50
+ is exactly ``"passed"``. Everything else — any other verdict, a
51
+ missing field, an unreadable or absent Receipt — is ``"blocked"``.
52
+
53
+ Args:
54
+ receipt: A Receipt as a plain mapping, the same shape
55
+ ``verify_receipt`` takes (for example ``outcome["receipt"]``
56
+ parsed from JSON). ``None`` and malformed values are accepted
57
+ and yield ``"blocked"``.
58
+
59
+ Returns:
60
+ ``"released"`` or ``"blocked"``. Never raises.
61
+
62
+ Example:
63
+ >>> from bindable import decision_from, verify_receipt
64
+ >>> receipt = outcome["receipt"]
65
+ >>> if verify_receipt(receipt, public_key=pem).valid:
66
+ ... if decision_from(receipt) == "released":
67
+ ... run_the_action()
68
+ 'released'
69
+ """
70
+ try:
71
+ if not isinstance(receipt, Mapping):
72
+ return BLOCKED
73
+ return RELEASED if receipt.get("verdict") == PASSED else BLOCKED
74
+ except Exception: # noqa: BLE001 - fail-closed: never raise, never release
75
+ return BLOCKED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bindable"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  requires-python = ">=3.10"
5
5
  description = "Python client for the hosted Bindable Action Gate, with offline Receipt verification"
6
6
  readme = "README.md"
@@ -0,0 +1,7 @@
1
+ Key provenance: `bindable-test-rc4-2026-08.pem` is the public half of a keypair generated by the engine (0.1.0rc4) when the two receipts in this directory were minted through its own decide path, for the verdict-form cutover. The private half was never written anywhere and is not needed to verify.
2
+
3
+ These fixtures replace the retired `*_rc3.json` pair, which were real Public-landscape production receipts signed by `bindable-public-2026-07` under the pre-cutover `verification_outcome` canonical form. That form no longer exists, so those receipts could not be carried forward: hand-editing them would invalidate their `chain_hash` and signature, and re-signing them with the production key is impossible here — its private half is Key-Vault-injected and never leaves the platform. `bindable-public-2026-07.pem` (recovered previously via ECDSA public-key recovery from those receipts) was retired with them.
4
+
5
+ What the fixtures prove is unchanged: a receipt minted by the engine under the current signed canonical form verifies offline through this client's hand-maintained port, chain hash and signature both. Only the signing key's provenance changed — production-minted to engine-minted-with-a-test-key.
6
+
7
+ Worth doing once the Public landscape runs rc4: re-mint this pair from real production output and restore end-to-end production provenance. Authoritative distribution of landscape verification keys will move to a read-only platform endpoint.
@@ -0,0 +1,4 @@
1
+ -----BEGIN PUBLIC KEY-----
2
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENYh+RQNDS48qBicGl9FfgStbcPNk
3
+ 2ElW2e4VUjnFHokPHj8eXL82eCzw+jwEGwEoDZmP9OdcUTkI8xsdPeWyzQ==
4
+ -----END PUBLIC KEY-----
@@ -0,0 +1 @@
1
+ {"receipt_id": "1d8ef197c63c4a4fbb4f9523d25f4e88", "proposal_digest": "01d3430e4b046b20fb7f2e0de22d3bab5febaae1832601020109c55b81d73b64", "proposal_payload": {"matter_id": "MER-2026-014", "call_cost_usd": 126, "records_requested": 400, "purpose": "full-market data sweep"}, "contract_id": "demo-diligence-spend-public-founding", "contract_version": "1.0.0", "sponsor_identity": "did:web:bindbl.ai:sponsor:b4047dfe7c4244dda32a3c8b4a02fc8e", "sponsor_identity_context": "ctx:public", "counterparty_identity": null, "persistent_agent_id": "did:web:bindbl.ai:agent:9dd08b5846564a1fbe674b1834fb887d", "action_type": "paid_data_call", "proposed_action_id": "pf-blocked-public-founding", "verdict": "failed", "failed_rule": "r-2", "reason_detail": "126 lte 58 not satisfied", "referenced_evidence": ["demo-budget-public-founding"], "verified_at": "2026-08-21T14:52:16.607Z", "nonce": "d61480f760d63f14d1350c21573d1a8b", "chain_hash": "485d281c0590813d2fd943114004280cbadf95c56bb9f2c8caba22f26c8ac1a2", "previous_hash": null, "signature": "3045022022f79c8897615256b53d3364db8f743eb524541a766329674e25b0ca1eba3d83022100b057d504c0cdb43474b091aa03f72a0bf18f8d11e24f05fab40baa2e497d40c1", "signing_key_id": "bindable-test-rc4-2026-08", "signed_at": "2026-08-21T14:52:16.607Z", "intervention_artifacts": null}
@@ -0,0 +1 @@
1
+ {"receipt_id": "ac2c3b3e7a6c4d59b972fa297a9f4821", "proposal_digest": "f35eb78be1073174427cf7884bccecbf7a2c8b776ecb932939a2219fa5d17c16", "proposal_payload": {"matter_id": "MER-2026-014", "call_cost_usd": 12, "records_requested": 40, "purpose": "comparable-transactions pull"}, "contract_id": "demo-diligence-spend-public-founding", "contract_version": "1.0.0", "sponsor_identity": "did:web:bindbl.ai:sponsor:b4047dfe7c4244dda32a3c8b4a02fc8e", "sponsor_identity_context": "ctx:public", "counterparty_identity": null, "persistent_agent_id": "did:web:bindbl.ai:agent:9dd08b5846564a1fbe674b1834fb887d", "action_type": "paid_data_call", "proposed_action_id": "pf-released-public-founding", "verdict": "passed", "failed_rule": null, "reason_detail": null, "referenced_evidence": ["demo-budget-public-founding"], "verified_at": "2026-08-21T14:52:16.604Z", "nonce": "b38265567729c89b020e6afe8a420f70", "chain_hash": "d7449f9b6874f2774ab4902dbe18ae2093de6a4cd223ab53802d39aace2a38e4", "previous_hash": null, "signature": "3045022014974d1ae715e5f16fdc2307d82cff116414ca4ac176d5566e2f72d7385cf580022100e56c5a09184c0fe57c32e5a400c55ac59d18c35d34ffd1b95c8bf2e3f4e7c86d", "signing_key_id": "bindable-test-rc4-2026-08", "signed_at": "2026-08-21T14:52:16.607Z", "intervention_artifacts": null}
@@ -67,7 +67,7 @@ _FULL_RECEIPT: dict[str, Any] = {
67
67
  "referenced_evidence": ["audit-001", "audit-002"],
68
68
  "sponsor_identity": "sponsor-001",
69
69
  "sponsor_identity_context": "treasury",
70
- "verification_outcome": "passed",
70
+ "verdict": "passed",
71
71
  "verified_at": "2026-05-19T14:00:00.000Z",
72
72
  # excluded fields that must NOT leak into the canonical input
73
73
  "receipt_id": "receipt-zzz",
@@ -93,7 +93,7 @@ _EXPECTED_KEYS = {
93
93
  "referenced_evidence",
94
94
  "sponsor_identity",
95
95
  "sponsor_identity_context",
96
- "verification_outcome",
96
+ "verdict",
97
97
  "verified_at",
98
98
  }
99
99
 
@@ -21,7 +21,7 @@ _TOKEN = "test-token"
21
21
 
22
22
  _OUTCOME_RELEASED: dict[str, Any] = {
23
23
  "status": "released",
24
- "verification_outcome": "passed",
24
+ "verdict": "passed",
25
25
  "reason": None,
26
26
  "reason_code": None,
27
27
  "receipt": None,
@@ -29,7 +29,7 @@ _OUTCOME_RELEASED: dict[str, Any] = {
29
29
  }
30
30
 
31
31
  _FIXTURE_RECEIPT: dict[str, Any] = json.loads(
32
- (Path(__file__).parent / "fixtures" / "receipt_public_released_rc3.json").read_text(
32
+ (Path(__file__).parent / "fixtures" / "receipt_public_released_rc4.json").read_text(
33
33
  encoding="utf-8"
34
34
  )
35
35
  )
@@ -64,7 +64,7 @@ def test_decide_posts_five_field_body_with_bearer() -> None:
64
64
  )
65
65
 
66
66
  assert captured["method"] == "POST"
67
- assert captured["url"] == "https://gate.example.com/v1/decide"
67
+ assert captured["url"] == "https://gate.example.com/v1/review"
68
68
  assert captured["auth"] == "Bearer test-token"
69
69
  assert captured["content_type"] == "application/json"
70
70
  # EXACTLY the 5 frozen wire fields, nothing more.
@@ -95,7 +95,7 @@ def test_base_url_trailing_slash_stripped() -> None:
95
95
  proposed_action_id="p",
96
96
  )
97
97
 
98
- assert captured["url"] == "https://gate.example.com/v1/decide"
98
+ assert captured["url"] == "https://gate.example.com/v1/review"
99
99
 
100
100
 
101
101
  def test_nonce_auto_generated_when_omitted() -> None:
@@ -0,0 +1,104 @@
1
+ """decision_from — the published released/blocked derivation.
2
+
3
+ This rule is reimplemented by counterparties in other languages, so these
4
+ tests pin it precisely: exactly one verdict releases, everything else
5
+ blocks, and nothing raises.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import pytest
15
+
16
+ from bindable import BLOCKED, RELEASED, decision_from
17
+
18
+ _FIX = Path(__file__).parent / "fixtures"
19
+
20
+
21
+ # ─── The rule ──────────────────────────────────────────────────────────
22
+
23
+
24
+ def test_passed_verdict_releases() -> None:
25
+ assert decision_from({"verdict": "passed"}) == "released"
26
+
27
+
28
+ @pytest.mark.parametrize("verdict", ["failed", "error", "exception"])
29
+ def test_every_other_verdict_blocks(verdict: str) -> None:
30
+ assert decision_from({"verdict": verdict}) == "blocked"
31
+
32
+
33
+ def test_missing_verdict_key_blocks() -> None:
34
+ """Fail-closed: an absent verdict is never a release."""
35
+ assert decision_from({}) == "blocked"
36
+ assert decision_from({"receipt_id": "r1", "chain_hash": "ab" * 32}) == "blocked"
37
+
38
+
39
+ @pytest.mark.parametrize("receipt", [
40
+ None,
41
+ "not a receipt",
42
+ ["not", "a", "receipt"],
43
+ 42,
44
+ object(),
45
+ ])
46
+ def test_unreadable_receipt_blocks_without_raising(receipt: Any) -> None:
47
+ """'Any Receipt that cannot be obtained or read' -> blocked."""
48
+ assert decision_from(receipt) == "blocked"
49
+
50
+
51
+ @pytest.mark.parametrize("verdict", ["Passed", "PASSED", " passed", "passed ", True, 1, None])
52
+ def test_comparison_is_exact(verdict: Any) -> None:
53
+ """Near-misses must NOT release — leniency would let ports disagree."""
54
+ assert decision_from({"verdict": verdict}) == "blocked"
55
+
56
+
57
+ def test_only_ever_returns_the_two_wire_words() -> None:
58
+ for receipt in ({"verdict": "passed"}, {"verdict": "failed"}, {}, None):
59
+ assert decision_from(receipt) in {RELEASED, BLOCKED}
60
+ assert (RELEASED, BLOCKED) == ("released", "blocked")
61
+
62
+
63
+ def test_a_verdict_whose_equality_explodes_still_blocks() -> None:
64
+ """The function never raises, whatever it is handed."""
65
+
66
+ class Hostile:
67
+ def __eq__(self, other: object) -> bool:
68
+ raise RuntimeError("boom")
69
+
70
+ assert decision_from({"verdict": Hostile()}) == "blocked"
71
+
72
+
73
+ # ─── Against real minted Receipts ──────────────────────────────────────
74
+
75
+
76
+ @pytest.mark.parametrize("filename,expected", [
77
+ ("receipt_public_released_rc4.json", "released"),
78
+ ("receipt_public_blocked_rc4.json", "blocked"),
79
+ ])
80
+ def test_real_minted_receipts_derive_the_right_decision(
81
+ filename: str, expected: str
82
+ ) -> None:
83
+ receipt = json.loads((_FIX / filename).read_text(encoding="utf-8"))
84
+ assert decision_from(receipt) == expected
85
+
86
+
87
+ def test_the_derivation_tracks_the_verdict_field_specifically() -> None:
88
+ """A released Receipt whose verdict is altered derives blocked.
89
+
90
+ (Altering it also breaks the chain hash — that is verify_receipt's job,
91
+ not this function's. decision_from reads the verdict, nothing more.)
92
+ """
93
+ receipt = json.loads(
94
+ (_FIX / "receipt_public_released_rc4.json").read_text(encoding="utf-8")
95
+ )
96
+ assert decision_from(receipt) == "released"
97
+ receipt["verdict"] = "failed"
98
+ assert decision_from(receipt) == "blocked"
99
+
100
+
101
+ def test_derivation_does_not_depend_on_status_or_failed_rule() -> None:
102
+ """Only the verdict decides — no other field participates."""
103
+ assert decision_from({"verdict": "passed", "failed_rule": "r-2"}) == "released"
104
+ assert decision_from({"verdict": "failed", "status": "released"}) == "blocked"
@@ -1,13 +1,18 @@
1
1
  """verify_receipt — end-to-end gate against real engine-minted receipts.
2
2
 
3
- The fixtures (tests/fixtures/receipt_public_released_rc3.json and
4
- receipt_public_blocked_rc3.json) are REAL production receipts minted by the
5
- engine @ 0.1.0rc3 on the Public landscape, ECDSA-signed by key
6
- bindable-public-2026-07 (public half at tests/fixtures/
7
- bindable-public-2026-07.pem). The chain-hash gates below prove the
8
- de-pydantic thin-client builder reproduces the engine's chain_hash
9
- byte-for-byte on the proposal_* wire. If one fails, the builder diverged —
10
- STOP and report; never edit the fixtures.
3
+ The fixtures (tests/fixtures/receipt_public_released_rc4.json and
4
+ receipt_public_blocked_rc4.json) are REAL receipts minted and ECDSA-signed
5
+ by the engine @ 0.1.0rc4 through its own decide path, under the verdict
6
+ canonical form (public half at tests/fixtures/bindable-test-rc4-2026-08.pem).
7
+ The chain-hash gates below prove the de-pydantic thin-client builder
8
+ reproduces the engine's chain_hash byte-for-byte. If one fails, the hand
9
+ port has drifted from the engine's signed form — that failure is the desync
10
+ alarm: STOP and report; never edit the fixtures.
11
+
12
+ They are minted with a test signing key rather than the production
13
+ bindable-public-2026-07 key, whose private half lives in Key Vault. What
14
+ they prove is unchanged: the ported canonical form and the engine's agree
15
+ byte-for-byte.
11
16
  """
12
17
 
13
18
  from __future__ import annotations
@@ -25,12 +30,12 @@ from bindable.crypto import canonical_sha256
25
30
 
26
31
  _FIX = Path(__file__).parent / "fixtures"
27
32
  _RELEASED: dict[str, Any] = json.loads(
28
- (_FIX / "receipt_public_released_rc3.json").read_text(encoding="utf-8")
33
+ (_FIX / "receipt_public_released_rc4.json").read_text(encoding="utf-8")
29
34
  )
30
35
  _BLOCKED: dict[str, Any] = json.loads(
31
- (_FIX / "receipt_public_blocked_rc3.json").read_text(encoding="utf-8")
36
+ (_FIX / "receipt_public_blocked_rc4.json").read_text(encoding="utf-8")
32
37
  )
33
- _PUBLIC_KEY_PEM_BYTES = (_FIX / "bindable-public-2026-07.pem").read_bytes()
38
+ _PUBLIC_KEY_PEM_BYTES = (_FIX / "bindable-test-rc4-2026-08.pem").read_bytes()
34
39
  _PUBLIC_KEY_PEM_STR = _PUBLIC_KEY_PEM_BYTES.decode("utf-8")
35
40
 
36
41
  # A valid P-256 key that did NOT sign these receipts (from the sign vectors).
@@ -2,4 +2,4 @@ import bindable
2
2
 
3
3
 
4
4
  def test_version() -> None:
5
- assert bindable.__version__ == "0.1.1"
5
+ assert bindable.__version__ == "0.1.3"
@@ -1 +0,0 @@
1
- __version__ = "0.1.1"
@@ -1 +0,0 @@
1
- Key provenance: bindable-public-2026-07.pem was recovered via ECDSA public-key recovery from the two production receipts in this directory and cross-validated against the hosted POST /v1/verify. Authoritative distribution of landscape verification keys will move to a read-only platform endpoint.
@@ -1,4 +0,0 @@
1
- -----BEGIN PUBLIC KEY-----
2
- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgAiZnfLmwECLMvbzCsaKKn+zMZ3W
3
- sJHGfdbwH8M7fk2hkNVI+RffcCwZFu2wG3MejwmvW7+b9GC4EgOEkdlkXw==
4
- -----END PUBLIC KEY-----
@@ -1 +0,0 @@
1
- {"receipt_id": "f92558c09edc44e7a141ea5c1b5cc855", "proposal_digest": "6b317920863fdbebaeda9e824daf5aeb781d87b2307f209f1ae25188fce2fe62", "proposal_payload": {"matter_id": "MER-2026-014", "call_cost_usd": 126, "records_requested": 400, "purpose": "full-market data sweep"}, "contract_id": "demo-diligence-spend-public-founding", "contract_version": "1.0.0", "sponsor_identity": "did:web:bindbl.ai:sponsor:b4047dfe7c4244dda32a3c8b4a02fc8e", "sponsor_identity_context": "ctx:public", "counterparty_identity": null, "persistent_agent_id": "did:web:bindbl.ai:agent:9dd08b5846564a1fbe674b1834fb887d", "action_type": "paid_data_call", "proposed_action_id": "pf-blocked-public-founding", "verification_outcome": "failed", "failed_rule": "r-2", "reason_detail": "126 lte 58 not satisfied", "referenced_evidence": ["demo-budget-public-founding"], "verified_at": "2026-08-13T14:09:07.031Z", "nonce": "d61480f760d63f14d1350c21573d1a8b", "chain_hash": "63b052eef0f449d7330dfd897891385e48ef372d8c69e9ddd05eaa84247ea036", "previous_hash": null, "signature": "30460221009273d5a94dbca61570dff9f7729d7576e852c0a681a396998db5da1e27885ed4022100ada78e5200865db572d78663a07b871cfd6013f84550c34bb3242cad07c44e9e", "signing_key_id": "bindable-public-2026-07", "signed_at": "2026-08-13T14:09:07.031Z", "intervention_artifacts": null}
@@ -1 +0,0 @@
1
- {"receipt_id": "9b6e8bca643444e09dfe0ef4015244d1", "proposal_digest": "9fe07610a2f335a96454d710259145e5ed1ef546234e21c279f4d6cff79f570d", "proposal_payload": {"matter_id": "MER-2026-014", "call_cost_usd": 12, "records_requested": 40, "purpose": "comparable-transactions pull"}, "contract_id": "demo-diligence-spend-public-founding", "contract_version": "1.0.0", "sponsor_identity": "did:web:bindbl.ai:sponsor:b4047dfe7c4244dda32a3c8b4a02fc8e", "sponsor_identity_context": "ctx:public", "counterparty_identity": null, "persistent_agent_id": "did:web:bindbl.ai:agent:9dd08b5846564a1fbe674b1834fb887d", "action_type": "paid_data_call", "proposed_action_id": "pf-released-public-founding", "verification_outcome": "passed", "failed_rule": null, "reason_detail": null, "referenced_evidence": ["demo-budget-public-founding"], "verified_at": "2026-08-13T14:08:59.607Z", "nonce": "b38265567729c89b020e6afe8a420f70", "chain_hash": "61374b353d242e9ddc523b09b537506a16f84852a1352d4bbf03643e486fe99d", "previous_hash": null, "signature": "3045022100d4dc8ff4ce26b24d0725016b7243b41e96ed2bc9074f21ba40c618ca459e0b01022027065e8d3e342393d70a270d43886145b2373418f2dc6ef7fc12b7b66860a12b", "signing_key_id": "bindable-public-2026-07", "signed_at": "2026-08-13T14:08:59.621Z", "intervention_artifacts": null}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes