bindable 0.1.3__tar.gz → 0.2.0__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 (102) hide show
  1. bindable-0.2.0/CHANGELOG.md +59 -0
  2. bindable-0.2.0/PKG-INFO +144 -0
  3. bindable-0.2.0/README.md +126 -0
  4. bindable-0.2.0/bindable/__init__.py +63 -0
  5. bindable-0.2.0/bindable/__main__.py +7 -0
  6. bindable-0.2.0/bindable/_canonical_trackpoint.py +53 -0
  7. bindable-0.2.0/bindable/_forms.py +455 -0
  8. bindable-0.2.0/bindable/_version.py +1 -0
  9. bindable-0.2.0/bindable/cli.py +165 -0
  10. {bindable-0.1.3 → bindable-0.2.0}/bindable/client.py +43 -5
  11. bindable-0.2.0/bindable/verify.py +685 -0
  12. {bindable-0.1.3 → bindable-0.2.0}/pyproject.toml +4 -1
  13. bindable-0.2.0/tests/conftest.py +32 -0
  14. bindable-0.2.0/tests/forms_pin/SOURCE.json +205 -0
  15. bindable-0.2.0/tests/forms_pin/__init__.py +0 -0
  16. bindable-0.2.0/tests/forms_pin/refresh.py +144 -0
  17. bindable-0.2.0/tests/forms_pin/specs.json +906 -0
  18. bindable-0.2.0/tests/forms_pin/test-key.public.json +6 -0
  19. bindable-0.2.0/tests/forms_pin/vectors/v4/canonical/gate-authority-signed.json +35 -0
  20. bindable-0.2.0/tests/forms_pin/vectors/v4/canonical/integers-and-booleans.json +35 -0
  21. bindable-0.2.0/tests/forms_pin/vectors/v4/canonical/key-order.json +25 -0
  22. bindable-0.2.0/tests/forms_pin/vectors/v4/canonical/nulls.json +17 -0
  23. bindable-0.2.0/tests/forms_pin/vectors/v4/canonical/principal-association-signed.json +33 -0
  24. bindable-0.2.0/tests/forms_pin/vectors/v4/canonical/receipt-signed.json +133 -0
  25. bindable-0.2.0/tests/forms_pin/vectors/v4/canonical/sorted-lists.json +61 -0
  26. bindable-0.2.0/tests/forms_pin/vectors/v4/canonical/string-escaping.json +28 -0
  27. bindable-0.2.0/tests/forms_pin/vectors/v4/canonical/trackpoint-signed.json +51 -0
  28. bindable-0.2.0/tests/forms_pin/vectors/v4/contract_version.json +29 -0
  29. bindable-0.2.0/tests/forms_pin/vectors/v4/evidence.json +24 -0
  30. bindable-0.2.0/tests/forms_pin/vectors/v4/gate_authority.json +34 -0
  31. bindable-0.2.0/tests/forms_pin/vectors/v4/governing_set.json +59 -0
  32. bindable-0.2.0/tests/forms_pin/vectors/v4/principal_association.json +32 -0
  33. bindable-0.2.0/tests/forms_pin/vectors/v4/proposal.json +21 -0
  34. bindable-0.2.0/tests/forms_pin/vectors/v4/receipt.json +142 -0
  35. bindable-0.2.0/tests/forms_pin/vectors/v4/scope_definition.json +21 -0
  36. bindable-0.2.0/tests/forms_pin/vectors/v4/sponsor_context.json +12 -0
  37. bindable-0.2.0/tests/forms_pin/vectors/v4/trackpoint.json +50 -0
  38. bindable-0.2.0/tests/forms_pin/vectors/v5/agent_record.json +14 -0
  39. bindable-0.2.0/tests/forms_pin/vectors/v5/authority_event.json +15 -0
  40. bindable-0.2.0/tests/forms_pin/vectors/v5/binding.json +14 -0
  41. bindable-0.2.0/tests/forms_pin/vectors/v5/contract_version.json +29 -0
  42. bindable-0.2.0/tests/forms_pin/vectors/v5/evidence.json +24 -0
  43. bindable-0.2.0/tests/forms_pin/vectors/v5/gate_authority.json +34 -0
  44. bindable-0.2.0/tests/forms_pin/vectors/v5/governing_set.json +59 -0
  45. bindable-0.2.0/tests/forms_pin/vectors/v5/principal_association.json +32 -0
  46. bindable-0.2.0/tests/forms_pin/vectors/v5/proposal.json +21 -0
  47. bindable-0.2.0/tests/forms_pin/vectors/v5/receipt.json +142 -0
  48. bindable-0.2.0/tests/forms_pin/vectors/v5/scope_definition.json +21 -0
  49. bindable-0.2.0/tests/forms_pin/vectors/v5/sponsor_context.json +12 -0
  50. bindable-0.2.0/tests/forms_pin/vectors/v5/trackpoint.json +50 -0
  51. bindable-0.2.0/tests/forms_pin/vectors/v6/agent_record.json +14 -0
  52. bindable-0.2.0/tests/forms_pin/vectors/v6/authority_event.json +15 -0
  53. bindable-0.2.0/tests/forms_pin/vectors/v6/binding.json +14 -0
  54. bindable-0.2.0/tests/forms_pin/vectors/v6/contract_version.json +29 -0
  55. bindable-0.2.0/tests/forms_pin/vectors/v6/evidence.json +24 -0
  56. bindable-0.2.0/tests/forms_pin/vectors/v6/gate_authority.json +34 -0
  57. bindable-0.2.0/tests/forms_pin/vectors/v6/governing_set.json +59 -0
  58. bindable-0.2.0/tests/forms_pin/vectors/v6/principal_association.json +46 -0
  59. bindable-0.2.0/tests/forms_pin/vectors/v6/principal_authorization.json +20 -0
  60. bindable-0.2.0/tests/forms_pin/vectors/v6/principal_naming.json +12 -0
  61. bindable-0.2.0/tests/forms_pin/vectors/v6/proposal.json +21 -0
  62. bindable-0.2.0/tests/forms_pin/vectors/v6/receipt.json +142 -0
  63. bindable-0.2.0/tests/forms_pin/vectors/v6/scope_definition.json +21 -0
  64. bindable-0.2.0/tests/forms_pin/vectors/v6/sponsor_context.json +12 -0
  65. bindable-0.2.0/tests/forms_pin/vectors/v6/trackpoint.json +50 -0
  66. bindable-0.2.0/tests/test_forms_equality.py +226 -0
  67. bindable-0.2.0/tests/test_offline_bundle_and_cli.py +304 -0
  68. bindable-0.2.0/tests/test_stage.py +152 -0
  69. bindable-0.2.0/tests/test_trackpoint_vectors.py +90 -0
  70. bindable-0.2.0/tests/test_verify_current_forms.py +318 -0
  71. bindable-0.2.0/tests/test_version.py +15 -0
  72. bindable-0.2.0/tests/vectors/trackpoint_vectors.json +189 -0
  73. bindable-0.1.3/PKG-INFO +0 -38
  74. bindable-0.1.3/README.md +0 -20
  75. bindable-0.1.3/bindable/__init__.py +0 -28
  76. bindable-0.1.3/bindable/_version.py +0 -1
  77. bindable-0.1.3/bindable/verify.py +0 -251
  78. bindable-0.1.3/tests/test_version.py +0 -5
  79. {bindable-0.1.3 → bindable-0.2.0}/.gitignore +0 -0
  80. {bindable-0.1.3 → bindable-0.2.0}/LICENSE +0 -0
  81. {bindable-0.1.3 → bindable-0.2.0}/NOTICE +0 -0
  82. {bindable-0.1.3 → bindable-0.2.0}/bindable/_canonical_record.py +0 -0
  83. {bindable-0.1.3 → bindable-0.2.0}/bindable/crypto/__init__.py +0 -0
  84. {bindable-0.1.3 → bindable-0.2.0}/bindable/crypto/canonical.py +0 -0
  85. {bindable-0.1.3 → bindable-0.2.0}/bindable/crypto/hash.py +0 -0
  86. {bindable-0.1.3 → bindable-0.2.0}/bindable/crypto/keys.py +0 -0
  87. {bindable-0.1.3 → bindable-0.2.0}/bindable/crypto/sign.py +0 -0
  88. {bindable-0.1.3 → bindable-0.2.0}/bindable/decision.py +0 -0
  89. {bindable-0.1.3 → bindable-0.2.0}/bindable/errors.py +0 -0
  90. {bindable-0.1.3 → bindable-0.2.0}/bindable/py.typed +0 -0
  91. {bindable-0.1.3 → bindable-0.2.0}/tests/__init__.py +0 -0
  92. {bindable-0.1.3 → bindable-0.2.0}/tests/crypto/sign_vectors.json +0 -0
  93. {bindable-0.1.3 → bindable-0.2.0}/tests/crypto/vectors.json +0 -0
  94. {bindable-0.1.3 → bindable-0.2.0}/tests/fixtures/KEY_PROVENANCE.md +0 -0
  95. {bindable-0.1.3 → bindable-0.2.0}/tests/fixtures/bindable-test-rc4-2026-08.pem +0 -0
  96. {bindable-0.1.3 → bindable-0.2.0}/tests/fixtures/receipt_public_blocked_rc4.json +0 -0
  97. {bindable-0.1.3 → bindable-0.2.0}/tests/fixtures/receipt_public_released_rc4.json +0 -0
  98. {bindable-0.1.3 → bindable-0.2.0}/tests/test_canonical_vectors.py +0 -0
  99. {bindable-0.1.3 → bindable-0.2.0}/tests/test_client.py +0 -0
  100. {bindable-0.1.3 → bindable-0.2.0}/tests/test_decision_from.py +0 -0
  101. {bindable-0.1.3 → bindable-0.2.0}/tests/test_signature_verify.py +0 -0
  102. {bindable-0.1.3 → bindable-0.2.0}/tests/test_verify_receipt.py +0 -0
@@ -0,0 +1,59 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 (unreleased)
4
+
5
+ ### Added
6
+
7
+ - `verify_receipt` and `verify_trackpoint` verify the Receipts and Trackpoints
8
+ Bindable issues today, form `v5`, and the previous form `v4`, offline. A
9
+ record names its form in its `form` field. The digest is recomputed from
10
+ the covered fields and compared with the one the record declares, and the
11
+ signature is checked over that digest with Bindable's public key.
12
+ - `ReceiptVerification` and `TrackpointVerification` gain `form`, the form
13
+ the record was checked as, and `hash_valid`, the same fact as
14
+ `chain_hash_valid` under its current name. Existing fields are unchanged.
15
+ - A record that is not the form it claims to be (an unknown field, a missing
16
+ field, a null where none is allowed, a float, an unknown form version)
17
+ returns `valid=False` with a reason. It never raises.
18
+ - The test suite proves the canonical bytes, digests and signature checks
19
+ equal to Bindable's committed test vectors for every form, byte for byte.
20
+ The vectors are a pinned copy (`tests/forms_pin`), and a test fails if the
21
+ copy drifts.
22
+ - Form `v6` (Bindable's forms v0.4.0, not yet issued): its Receipt and
23
+ Trackpoint are v5's with only the form value changed, and are verified the
24
+ same way; the pinned
25
+ vectors are now those of forms v0.4.0, with v4 and v5 unchanged.
26
+ - `verify_receipt(receipt, public_key)` and
27
+ `verify_trackpoint(trackpoint, public_key)`: the key may be given as the
28
+ second argument as well as by name.
29
+ - The file the Portal's "Download Receipt" saves (the Receipt, the public key
30
+ it names and its Trackpoint) is accepted as it is: `verify_receipt(file)`
31
+ checks the Receipt under the file's own key, and the new
32
+ `verify_bundle(file, public_key=None)` returns a `BundleVerification` for
33
+ the Receipt, the Trackpoint, and whether the Trackpoint names the Receipt
34
+ (`link_valid`). A file whose key is not the one the Receipt names fails.
35
+ - `bindable verify <file>` (and `python -m bindable verify <file>`): the same
36
+ checks from the command line, for a downloaded file or for a bare Receipt or
37
+ Trackpoint with `--key`; `--json` prints the result; exit 0, 1 or 2.
38
+ - README: "Verify a Receipt offline", with the downloaded file first.
39
+
40
+ ### Changed
41
+
42
+ - `import bindable` no longer imports the HTTP client: `ActionGate` and
43
+ `STAGES` are loaded on first use, so the verify path imports no network
44
+ module. `from bindable import ActionGate` works as before.
45
+ - The test suite runs with sockets refused, and proves in a fresh interpreter
46
+ that verifying a file imports no network module and opens no socket.
47
+
48
+ ### Fixed
49
+
50
+ - `bindable.__version__` now matches the package version. 0.1.5 reported
51
+ `0.1.4`.
52
+
53
+ ### Unchanged
54
+
55
+ - `verify_receipt(receipt, public_key=key)` keeps working.
56
+ - Receipts and Trackpoints without a `form` field (the 0.1.x layouts) are
57
+ verified exactly as before.
58
+ - No new dependency. Verification uses the Python standard library and
59
+ `cryptography` only, and makes no network call.
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.5
2
+ Name: bindable
3
+ Version: 0.2.0
4
+ Summary: Python client for the hosted Bindable Action Gate, with offline Receipt verification
5
+ Author: Bindable AI LLC
6
+ License-Expression: Apache-2.0
7
+ License-File: LICENSE
8
+ License-File: NOTICE
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: cryptography>=42
11
+ Requires-Dist: httpx>=0.27
12
+ Provides-Extra: dev
13
+ Requires-Dist: build>=1.2; extra == 'dev'
14
+ Requires-Dist: mypy>=1.10; extra == 'dev'
15
+ Requires-Dist: pytest>=8; extra == 'dev'
16
+ Requires-Dist: ruff>=0.5; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # bindable
20
+
21
+ Bindable Python client — propose, decide, and verify accountable agent actions against the hosted Bindable Action Gate. Offline receipt verification included.
22
+
23
+ ## Install
24
+
25
+ ```
26
+ pip install bindable
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```python
32
+ import bindable
33
+
34
+ # Submit a Proposal to the Bindable Action Gate, receive a Decision,
35
+ # record the Outcome, then verify the returned Receipt offline.
36
+ ```
37
+
38
+ ## Verify a Receipt offline
39
+
40
+ Every action Bindable reviews leaves a Receipt: a signed record of what was
41
+ proposed, what was decided and why. Anyone who holds a Receipt can check it on
42
+ their own machine. No network connection is needed, and Bindable is never
43
+ contacted.
44
+
45
+ ### From a downloaded Receipt file
46
+
47
+ On a Receipt's page in the Portal, **Download Receipt** saves one file,
48
+ `bindable-receipt-<receipt_id>.json`, holding the Receipt, the public key it
49
+ was signed under and, once your Gate has reported, its Trackpoint. Check it
50
+ with no connection at all:
51
+
52
+ ```
53
+ pip install bindable
54
+ bindable verify bindable-receipt-<receipt_id>.json
55
+ ```
56
+
57
+ It prints whether the Receipt and its Trackpoint verify, and that the
58
+ Trackpoint names this Receipt, and exits `0` when everything does, `1` when
59
+ something does not, and `2` when the file cannot be read. Add `--json` for a
60
+ result a program can read, or `--key <pem file>` to check against a key you
61
+ hold rather than the one in the file. The key in the file shows the file
62
+ agrees with itself; to show the Receipt is Bindable's, compare that key with
63
+ the one Bindable publishes at `GET /v1/keys/{signing_key_id}`, or pass it with
64
+ `--key`.
65
+
66
+ In Python, the same file is accepted as it is, by `verify_receipt(document)`
67
+ for the Receipt alone, or by `verify_bundle(document)` for the Receipt, its
68
+ Trackpoint and the link between them:
69
+
70
+ ```python
71
+ import json
72
+ from bindable import verify_bundle
73
+
74
+ result = verify_bundle(json.load(open("bindable-receipt-<receipt_id>.json")))
75
+ print("Verified." if result.valid else f"Not verified: {result.reason}")
76
+ ```
77
+
78
+ ### From a Receipt and a key
79
+
80
+ You need two things:
81
+
82
+ 1. **The Receipt**, as JSON. Your Gate returns it with each decision. You can
83
+ also fetch a Receipt's public form by its ID from
84
+ `GET /v1/receipts/{receipt_id}/public`, which needs no credential.
85
+ 2. **Bindable's public key** for the key named in the Receipt's
86
+ `signing_key_id`. Fetch it once from `GET /v1/keys/{signing_key_id}` (no
87
+ credential needed) and keep it. Retired keys stay published, so an old
88
+ Receipt can always be checked.
89
+
90
+ Then:
91
+
92
+ ```python
93
+ import json
94
+ from bindable import verify_receipt
95
+
96
+ receipt = json.load(open("receipt.json"))
97
+ public_key = open("bindable-public-key.pem").read()
98
+
99
+ result = verify_receipt(receipt, public_key)
100
+
101
+ if result.valid:
102
+ print("Genuine and unchanged.")
103
+ else:
104
+ print("Do not rely on this Receipt:", result.reason)
105
+ ```
106
+
107
+ What the answer tells you:
108
+
109
+ | Field | Meaning |
110
+ |---|---|
111
+ | `valid` | `True` only when both checks below pass. Rely on the Receipt only then. |
112
+ | `hash_valid` | The Receipt's contents are exactly what was recorded. If even one character of a covered field changed, this is `False`. |
113
+ | `signature_valid` | Bindable's key signed this Receipt. `False` for a different key or a broken signature. `None` if you gave no key. |
114
+ | `form` | Which version of the Receipt layout was checked, for example `"v5"`. |
115
+ | `reason` | A short explanation when `valid` is `False`. Meant for people to read, not for programs to parse. |
116
+
117
+ `verify_receipt` never raises an exception for a bad or incomplete Receipt.
118
+ It returns `valid=False` with a reason.
119
+
120
+ Some fields travel beside a Receipt but are not covered by its signature:
121
+ `reason_detail`, `proposal_payload`, `principal_disclosed`, `principal_id`
122
+ and `release_token`. Changing them does not change the result, so do not treat
123
+ them as signed evidence.
124
+
125
+ A Trackpoint, the record of what your Gate did after the decision, is
126
+ checked the same way with `verify_trackpoint`. If you give a key and the
127
+ Trackpoint carries no signature, the answer is `valid=False`, never a pass on
128
+ the contents alone.
129
+
130
+ Without a key, both functions check the contents only. Anyone can compute that
131
+ check, so it shows a record is internally consistent, not that Bindable made
132
+ it. Give the key whenever you need to know where a record came from.
133
+
134
+ **Which Receipts this checks.** Forms `v4` and `v5`, the layouts Bindable
135
+ has issued, and `v6`, the next, defined and ready to check. Receipts from the
136
+ 0.1.x releases, which have no `form` field, are still checked the way 0.1.x
137
+ checked them.
138
+
139
+ The checks use only the Python standard library and the `cryptography`
140
+ package, and import nothing that can reach a network: the HTTP client this
141
+ library also ships is loaded only when you use `ActionGate`. This library
142
+ contains no signing code and no private key.
143
+
144
+ Status: pre-release
@@ -0,0 +1,126 @@
1
+ # bindable
2
+
3
+ Bindable Python client — propose, decide, and verify accountable agent actions against the hosted Bindable Action Gate. Offline receipt verification included.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ pip install bindable
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ import bindable
15
+
16
+ # Submit a Proposal to the Bindable Action Gate, receive a Decision,
17
+ # record the Outcome, then verify the returned Receipt offline.
18
+ ```
19
+
20
+ ## Verify a Receipt offline
21
+
22
+ Every action Bindable reviews leaves a Receipt: a signed record of what was
23
+ proposed, what was decided and why. Anyone who holds a Receipt can check it on
24
+ their own machine. No network connection is needed, and Bindable is never
25
+ contacted.
26
+
27
+ ### From a downloaded Receipt file
28
+
29
+ On a Receipt's page in the Portal, **Download Receipt** saves one file,
30
+ `bindable-receipt-<receipt_id>.json`, holding the Receipt, the public key it
31
+ was signed under and, once your Gate has reported, its Trackpoint. Check it
32
+ with no connection at all:
33
+
34
+ ```
35
+ pip install bindable
36
+ bindable verify bindable-receipt-<receipt_id>.json
37
+ ```
38
+
39
+ It prints whether the Receipt and its Trackpoint verify, and that the
40
+ Trackpoint names this Receipt, and exits `0` when everything does, `1` when
41
+ something does not, and `2` when the file cannot be read. Add `--json` for a
42
+ result a program can read, or `--key <pem file>` to check against a key you
43
+ hold rather than the one in the file. The key in the file shows the file
44
+ agrees with itself; to show the Receipt is Bindable's, compare that key with
45
+ the one Bindable publishes at `GET /v1/keys/{signing_key_id}`, or pass it with
46
+ `--key`.
47
+
48
+ In Python, the same file is accepted as it is, by `verify_receipt(document)`
49
+ for the Receipt alone, or by `verify_bundle(document)` for the Receipt, its
50
+ Trackpoint and the link between them:
51
+
52
+ ```python
53
+ import json
54
+ from bindable import verify_bundle
55
+
56
+ result = verify_bundle(json.load(open("bindable-receipt-<receipt_id>.json")))
57
+ print("Verified." if result.valid else f"Not verified: {result.reason}")
58
+ ```
59
+
60
+ ### From a Receipt and a key
61
+
62
+ You need two things:
63
+
64
+ 1. **The Receipt**, as JSON. Your Gate returns it with each decision. You can
65
+ also fetch a Receipt's public form by its ID from
66
+ `GET /v1/receipts/{receipt_id}/public`, which needs no credential.
67
+ 2. **Bindable's public key** for the key named in the Receipt's
68
+ `signing_key_id`. Fetch it once from `GET /v1/keys/{signing_key_id}` (no
69
+ credential needed) and keep it. Retired keys stay published, so an old
70
+ Receipt can always be checked.
71
+
72
+ Then:
73
+
74
+ ```python
75
+ import json
76
+ from bindable import verify_receipt
77
+
78
+ receipt = json.load(open("receipt.json"))
79
+ public_key = open("bindable-public-key.pem").read()
80
+
81
+ result = verify_receipt(receipt, public_key)
82
+
83
+ if result.valid:
84
+ print("Genuine and unchanged.")
85
+ else:
86
+ print("Do not rely on this Receipt:", result.reason)
87
+ ```
88
+
89
+ What the answer tells you:
90
+
91
+ | Field | Meaning |
92
+ |---|---|
93
+ | `valid` | `True` only when both checks below pass. Rely on the Receipt only then. |
94
+ | `hash_valid` | The Receipt's contents are exactly what was recorded. If even one character of a covered field changed, this is `False`. |
95
+ | `signature_valid` | Bindable's key signed this Receipt. `False` for a different key or a broken signature. `None` if you gave no key. |
96
+ | `form` | Which version of the Receipt layout was checked, for example `"v5"`. |
97
+ | `reason` | A short explanation when `valid` is `False`. Meant for people to read, not for programs to parse. |
98
+
99
+ `verify_receipt` never raises an exception for a bad or incomplete Receipt.
100
+ It returns `valid=False` with a reason.
101
+
102
+ Some fields travel beside a Receipt but are not covered by its signature:
103
+ `reason_detail`, `proposal_payload`, `principal_disclosed`, `principal_id`
104
+ and `release_token`. Changing them does not change the result, so do not treat
105
+ them as signed evidence.
106
+
107
+ A Trackpoint, the record of what your Gate did after the decision, is
108
+ checked the same way with `verify_trackpoint`. If you give a key and the
109
+ Trackpoint carries no signature, the answer is `valid=False`, never a pass on
110
+ the contents alone.
111
+
112
+ Without a key, both functions check the contents only. Anyone can compute that
113
+ check, so it shows a record is internally consistent, not that Bindable made
114
+ it. Give the key whenever you need to know where a record came from.
115
+
116
+ **Which Receipts this checks.** Forms `v4` and `v5`, the layouts Bindable
117
+ has issued, and `v6`, the next, defined and ready to check. Receipts from the
118
+ 0.1.x releases, which have no `form` field, are still checked the way 0.1.x
119
+ checked them.
120
+
121
+ The checks use only the Python standard library and the `cryptography`
122
+ package, and import nothing that can reach a network: the HTTP client this
123
+ library also ships is loaded only when you use `ActionGate`. This library
124
+ contains no signing code and no private key.
125
+
126
+ Status: pre-release
@@ -0,0 +1,63 @@
1
+ """Bindable — Python client for the hosted Bindable Action Gate.
2
+
3
+ Submit a Proposal to the Bindable Action Gate via ``ActionGate.decide()``,
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.
9
+
10
+ Review resolves the governing Contract by stage. ``decide()`` takes an optional
11
+ ``stage`` — one of ``STAGES`` — and omits the field entirely when it is not
12
+ given, leaving the platform's ``development`` default in force.
13
+
14
+ Offline verification imports nothing that can reach a network. The transport
15
+ client, which does, is imported only when ``ActionGate`` or ``STAGES`` is first
16
+ used, so ``from bindable import verify_receipt`` and ``bindable verify`` load
17
+ no HTTP library at all.
18
+ """
19
+
20
+ from typing import TYPE_CHECKING, Any
21
+
22
+ from ._version import __version__
23
+ from .crypto import load_public_key
24
+ from .decision import BLOCKED, RELEASED, decision_from
25
+ from .errors import BindableAPIError
26
+ from .verify import (
27
+ BundleVerification,
28
+ ReceiptVerification,
29
+ TrackpointVerification,
30
+ verify_bundle,
31
+ verify_receipt,
32
+ verify_trackpoint,
33
+ )
34
+
35
+ if TYPE_CHECKING:
36
+ from .client import STAGES, ActionGate
37
+
38
+
39
+ def __getattr__(name: str) -> Any:
40
+ """The transport client, imported on first use."""
41
+ if name in ("ActionGate", "STAGES"):
42
+ from . import client
43
+
44
+ return getattr(client, name)
45
+ raise AttributeError(f"module 'bindable' has no attribute {name!r}")
46
+
47
+
48
+ __all__ = [
49
+ "BLOCKED",
50
+ "RELEASED",
51
+ "STAGES",
52
+ "ActionGate",
53
+ "BindableAPIError",
54
+ "BundleVerification",
55
+ "ReceiptVerification",
56
+ "TrackpointVerification",
57
+ "__version__",
58
+ "decision_from",
59
+ "load_public_key",
60
+ "verify_bundle",
61
+ "verify_receipt",
62
+ "verify_trackpoint",
63
+ ]
@@ -0,0 +1,7 @@
1
+ """``python -m bindable verify <file>``, the same as ``bindable verify <file>``."""
2
+
3
+ import sys
4
+
5
+ from bindable.cli import main
6
+
7
+ sys.exit(main())
@@ -0,0 +1,53 @@
1
+ """Canonical hash-input serialization for trackpoints (form v3).
2
+
3
+ Single source of truth for the canonical input over which a trackpoint's
4
+ chain_hash is computed, and — on production — what its signature covers.
5
+ ``verify_trackpoint`` reconstructs this input to verify offline; the platform
6
+ constructs it when appending. Both must be byte-identical.
7
+
8
+ FIFTEEN FIELDS, form v3 (2026-09-03). Form v2 hashed thirteen: ``gate_runner``
9
+ and ``stage`` were excluded only because rows predated the columns, and
10
+ including them then would have split the chain at an undocumented instant. Both
11
+ chains restarted at genesis on 2026-09-03 with zero rows, so that reason is gone
12
+ and both fields are now covered.
13
+
14
+ ``chain_hash`` is NOT in this input — it is the OUTPUT. A signature covers this
15
+ canonical record, which is equivalent to signing the chain_hash by ECDSA's
16
+ standard construction.
17
+
18
+ Pure and deterministic: no I/O, no mutation. Keys are sorted by RFC 8785 at
19
+ serialization, so the order written here is for readers, not for the digest.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Any, Mapping
25
+
26
+ #: The fifteen hashed field names, in the order a reader should think about them.
27
+ CANONICAL_FIELDS: tuple[str, ...] = (
28
+ "trackpoint_id",
29
+ "receipt_id",
30
+ "persistent_agent_id",
31
+ "principal_id",
32
+ "outcome",
33
+ "action_result",
34
+ "proposed_at",
35
+ "decided_at",
36
+ "enforced_at",
37
+ "gate_version",
38
+ "gate_kind",
39
+ "gate_runner",
40
+ "stage",
41
+ "environment",
42
+ "previous_hash",
43
+ )
44
+
45
+
46
+ def _canonical_trackpoint_input(trackpoint: Mapping[str, Any]) -> dict[str, Any]:
47
+ """Return the canonical hash input for one trackpoint.
48
+
49
+ Timestamps are hashed as the wire strings the platform wrote
50
+ (ISO 8601 UTC, millisecond precision, ``Z`` suffix). They are taken
51
+ verbatim: a verifier must not reformat a value it is checking.
52
+ """
53
+ return {name: trackpoint.get(name) for name in CANONICAL_FIELDS}