macp-sdk-python 0.6.0__tar.gz → 0.7.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.
- {macp_sdk_python-0.6.0/src/macp_sdk_python.egg-info → macp_sdk_python-0.7.0}/PKG-INFO +1 -1
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/pyproject.toml +1 -1
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/__init__.py +5 -0
- macp_sdk_python-0.7.0/src/macp_sdk/commitment_hash.py +317 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/envelope.py +14 -2
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/validation.py +17 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0/src/macp_sdk_python.egg-info}/PKG-INFO +1 -1
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk_python.egg-info/SOURCES.txt +1 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/LICENSE +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/README.md +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/setup.cfg +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/_logging.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/agent/__init__.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/agent/cancel_callback.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/agent/dispatcher.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/agent/participant.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/agent/runner.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/agent/strategies.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/agent/transports.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/agent/types.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/auth.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/base_projection.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/base_session.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/client.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/constants.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/decision.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/errors.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/handoff.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/policy.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/projections.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/proposal.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/proto_registry.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/py.typed +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/quorum.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/retry.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/task.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk/watchers.py +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk_python.egg-info/dependency_links.txt +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk_python.egg-info/requires.txt +0 -0
- {macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk_python.egg-info/top_level.txt +0 -0
|
@@ -3,6 +3,7 @@ from .auth import AuthConfig
|
|
|
3
3
|
from .base_projection import BaseProjection
|
|
4
4
|
from .base_session import BaseSession
|
|
5
5
|
from .client import InlineErrorCallback, MacpClient, MacpStream
|
|
6
|
+
from .commitment_hash import commitment_hash, is_canonical_commitment_hash
|
|
6
7
|
from .constants import (
|
|
7
8
|
DEFAULT_CONFIGURATION_VERSION,
|
|
8
9
|
DEFAULT_MODE_VERSION,
|
|
@@ -85,6 +86,7 @@ from .quorum import QuorumProjection, QuorumSession
|
|
|
85
86
|
from .retry import RetryPolicy, retry_send
|
|
86
87
|
from .task import TaskProjection, TaskSession
|
|
87
88
|
from .validation import (
|
|
89
|
+
validate_commitment_hash,
|
|
88
90
|
validate_confidence,
|
|
89
91
|
validate_participant_count,
|
|
90
92
|
validate_participants,
|
|
@@ -195,13 +197,16 @@ __all__ = [
|
|
|
195
197
|
"build_session_start_payload",
|
|
196
198
|
"build_signal_payload",
|
|
197
199
|
"build_task_policy",
|
|
200
|
+
"commitment_hash",
|
|
198
201
|
"configure_logging",
|
|
199
202
|
"infer_outcome_positive",
|
|
203
|
+
"is_canonical_commitment_hash",
|
|
200
204
|
"new_commitment_id",
|
|
201
205
|
"new_message_id",
|
|
202
206
|
"new_session_id",
|
|
203
207
|
"retry_send",
|
|
204
208
|
"serialize_message",
|
|
209
|
+
"validate_commitment_hash",
|
|
205
210
|
"validate_confidence",
|
|
206
211
|
"validate_participant_count",
|
|
207
212
|
"validate_participants",
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Canonical commitment hash (RFC-MACP-0013).
|
|
2
|
+
|
|
3
|
+
Computes ``CommitmentRef.commitment_hash`` as a domain-separated SHA-256
|
|
4
|
+
digest over an RFC 8785 (JCS) canonicalized JSON projection of the frozen
|
|
5
|
+
nine-field ``CommitmentPayload`` set (RFC-MACP-0013 §5).
|
|
6
|
+
|
|
7
|
+
This module intentionally has **no third-party imports beyond
|
|
8
|
+
``google.protobuf``/``macp.v1.core_pb2``** — the hashing algorithm is pure
|
|
9
|
+
stdlib (plus the protobuf runtime already required to hold ``payload``
|
|
10
|
+
itself) so that it can be reused (and re-verified) without pulling in the
|
|
11
|
+
rest of the SDK.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
from google.protobuf import unknown_fields # type: ignore[import-untyped]
|
|
20
|
+
from macp.v1 import core_pb2
|
|
21
|
+
|
|
22
|
+
from .errors import MacpSessionError
|
|
23
|
+
|
|
24
|
+
#: Identifies this projection/preimage construction (RFC-MACP-0013 §4, §7).
|
|
25
|
+
#: A change here is a MACP protocol MINOR change (new label, e.g. "/2").
|
|
26
|
+
LABEL = "macp-commitment-hash/1"
|
|
27
|
+
|
|
28
|
+
#: The frozen nine-field set ``CommitmentPayload`` MUST be limited to for this
|
|
29
|
+
#: hash label (RFC-MACP-0013 §5). A future ``macp-proto`` release that adds a
|
|
30
|
+
#: tenth field to the message is not hashable under this label -- see
|
|
31
|
+
#: `_check_frozen_field_set`.
|
|
32
|
+
_FROZEN_FIELD_NAMES = frozenset(
|
|
33
|
+
{
|
|
34
|
+
"commitment_id",
|
|
35
|
+
"action",
|
|
36
|
+
"authority_scope",
|
|
37
|
+
"reason",
|
|
38
|
+
"mode_version",
|
|
39
|
+
"policy_version",
|
|
40
|
+
"configuration_version",
|
|
41
|
+
"outcome_positive",
|
|
42
|
+
"supersedes",
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
#: The installed ``macp-proto``'s actual ``CommitmentPayload`` field names,
|
|
47
|
+
#: computed once at import time so `_check_frozen_field_set` never has to
|
|
48
|
+
#: walk the descriptor on every `commitment_hash()` call.
|
|
49
|
+
_ACTUAL_FIELD_NAMES: frozenset[str] = frozenset(
|
|
50
|
+
f.name for f in core_pb2.CommitmentPayload.DESCRIPTOR.fields
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
#: The frozen two-field set ``CommitmentRef`` (the type of `supersedes`) MUST
|
|
54
|
+
#: be limited to for this hash label (RFC-MACP-0013 §5): "``supersedes``,
|
|
55
|
+
#: when set, carries exactly two fields (``session_id``, ``commitment_hash``)".
|
|
56
|
+
#: A future ``macp-proto`` release that adds a third field to ``CommitmentRef``
|
|
57
|
+
#: itself is not hashable under this label -- see `_check_frozen_ref_field_set`.
|
|
58
|
+
_FROZEN_REF_FIELD_NAMES = frozenset({"session_id", "commitment_hash"})
|
|
59
|
+
|
|
60
|
+
#: The installed ``macp-proto``'s actual ``CommitmentRef`` field names,
|
|
61
|
+
#: computed once at import time, mirroring `_ACTUAL_FIELD_NAMES` above.
|
|
62
|
+
_ACTUAL_REF_FIELD_NAMES: frozenset[str] = frozenset(
|
|
63
|
+
f.name for f in core_pb2.CommitmentRef.DESCRIPTOR.fields
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
#: Matched with `re.fullmatch` (see `is_canonical_commitment_hash`), so the
|
|
67
|
+
#: anchors here are redundant but kept for readability.
|
|
68
|
+
_HASH_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
69
|
+
|
|
70
|
+
# RFC 8785 §3.2.2.2 short-form escapes.
|
|
71
|
+
_SHORT_ESCAPES: dict[str, str] = {
|
|
72
|
+
"\\": "\\\\",
|
|
73
|
+
'"': '\\"',
|
|
74
|
+
"\b": "\\b",
|
|
75
|
+
"\f": "\\f",
|
|
76
|
+
"\n": "\\n",
|
|
77
|
+
"\r": "\\r",
|
|
78
|
+
"\t": "\\t",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _escape_json_string(value: str) -> str:
|
|
83
|
+
"""Escape ``value`` per RFC 8785 §3.2.2.2 and wrap it in double quotes.
|
|
84
|
+
|
|
85
|
+
Short-form escapes are used for backslash, quote, and the C0 controls
|
|
86
|
+
that have them (\\b \\t \\n \\f \\r); other C0 controls (U+0000-U+001F)
|
|
87
|
+
use \\u00XX; everything else -- including all non-ASCII and astral-plane
|
|
88
|
+
code points -- is emitted literally as the Unicode scalar value it is
|
|
89
|
+
(Python `str` already holds scalar values, not UTF-16 code units, so no
|
|
90
|
+
surrogate-pair handling is needed here).
|
|
91
|
+
"""
|
|
92
|
+
out: list[str] = ['"']
|
|
93
|
+
for ch in value:
|
|
94
|
+
short = _SHORT_ESCAPES.get(ch)
|
|
95
|
+
if short is not None:
|
|
96
|
+
out.append(short)
|
|
97
|
+
elif ch < "\x20":
|
|
98
|
+
out.append(f"\\u{ord(ch):04x}")
|
|
99
|
+
else:
|
|
100
|
+
out.append(ch)
|
|
101
|
+
out.append('"')
|
|
102
|
+
return "".join(out)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _json_bool(value: bool) -> str:
|
|
106
|
+
return "true" if value else "false"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _supersedes_member(ref: core_pb2.CommitmentRef) -> str:
|
|
110
|
+
# Inside `supersedes`, key order is commitment_hash, session_id
|
|
111
|
+
# (plain lexicographic sort of the two ASCII member names).
|
|
112
|
+
members = [
|
|
113
|
+
f"{_escape_json_string('commitment_hash')}:{_escape_json_string(ref.commitment_hash)}",
|
|
114
|
+
f"{_escape_json_string('session_id')}:{_escape_json_string(ref.session_id)}",
|
|
115
|
+
]
|
|
116
|
+
return "{" + ",".join(members) + "}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _check_frozen_field_set(field_names: frozenset[str] | None = None) -> None:
|
|
120
|
+
"""Raise if ``field_names`` contains anything outside the RFC-MACP-0013 §5
|
|
121
|
+
frozen nine-field set for ``CommitmentPayload``.
|
|
122
|
+
|
|
123
|
+
RFC-MACP-0013 §5: "A ``CommitmentPayload`` carrying a field outside this
|
|
124
|
+
set is not hashable under this label. A verifier presented with such a
|
|
125
|
+
payload MUST return a cannot-verify result... never silently ignored."
|
|
126
|
+
§12 reinforces that skipping this check "produces a value that is not a
|
|
127
|
+
canonical commitment hash at all."
|
|
128
|
+
|
|
129
|
+
Defaults to the installed proto's actual field set (`_ACTUAL_FIELD_NAMES`,
|
|
130
|
+
computed once at import time and re-read from the module namespace here
|
|
131
|
+
-- not bound as a mutable default argument -- so that tests can
|
|
132
|
+
monkeypatch it); a caller may also pass an explicit set directly. Either
|
|
133
|
+
way this is how tests exercise the "extra field" branch without needing a
|
|
134
|
+
real 10-field proto descriptor.
|
|
135
|
+
|
|
136
|
+
This is *not* about an older proto missing a field (that's already
|
|
137
|
+
handled by the comment in `commitment_hash()`: accessing a genuinely
|
|
138
|
+
absent field raises ``AttributeError`` naturally) -- this is only about a
|
|
139
|
+
newer proto carrying a field this module does not know about.
|
|
140
|
+
"""
|
|
141
|
+
if field_names is None:
|
|
142
|
+
field_names = _ACTUAL_FIELD_NAMES
|
|
143
|
+
extra = field_names - _FROZEN_FIELD_NAMES
|
|
144
|
+
if extra:
|
|
145
|
+
raise MacpSessionError(
|
|
146
|
+
"CommitmentPayload carries field(s) outside the RFC-MACP-0013 §5 "
|
|
147
|
+
f"frozen nine-field set and is not hashable under label {LABEL!r}: "
|
|
148
|
+
f"{sorted(extra)}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _check_frozen_ref_field_set(field_names: frozenset[str] | None = None) -> None:
|
|
153
|
+
"""Raise if ``field_names`` contains anything outside the RFC-MACP-0013 §5
|
|
154
|
+
frozen two-field set for ``CommitmentRef`` (the type of ``supersedes``).
|
|
155
|
+
|
|
156
|
+
Mirrors `_check_frozen_field_set` exactly, one level down: that guard
|
|
157
|
+
catches a future ``macp-proto`` growing a 10th field on
|
|
158
|
+
``CommitmentPayload`` itself; this one catches a future ``macp-proto``
|
|
159
|
+
growing a 3rd field on the nested ``CommitmentRef`` message, which
|
|
160
|
+
`_supersedes_member` would otherwise silently under-project without
|
|
161
|
+
anything raising.
|
|
162
|
+
|
|
163
|
+
Defaults to the installed proto's actual field set
|
|
164
|
+
(`_ACTUAL_REF_FIELD_NAMES`, computed once at import time and re-read from
|
|
165
|
+
the module namespace here -- not bound as a mutable default argument --
|
|
166
|
+
so that tests can monkeypatch it); a caller may also pass an explicit set
|
|
167
|
+
directly, exactly as `_check_frozen_field_set` does.
|
|
168
|
+
"""
|
|
169
|
+
if field_names is None:
|
|
170
|
+
field_names = _ACTUAL_REF_FIELD_NAMES
|
|
171
|
+
extra = field_names - _FROZEN_REF_FIELD_NAMES
|
|
172
|
+
if extra:
|
|
173
|
+
raise MacpSessionError(
|
|
174
|
+
"CommitmentRef carries field(s) outside the RFC-MACP-0013 §5 "
|
|
175
|
+
f"frozen two-field set and is not hashable under label {LABEL!r}: "
|
|
176
|
+
f"{sorted(extra)}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _check_no_unknown_wire_fields(payload: core_pb2.CommitmentPayload) -> None:
|
|
181
|
+
"""Raise if ``payload`` carries wire data for a field number the
|
|
182
|
+
installed schema does not recognize at all (an "unknown field").
|
|
183
|
+
|
|
184
|
+
`_check_frozen_field_set` only catches schema drift: a *newer* installed
|
|
185
|
+
``macp-proto`` whose ``CommitmentPayload.DESCRIPTOR`` has grown a 10th
|
|
186
|
+
field. It cannot see this case: a peer sent bytes for a field number the
|
|
187
|
+
*local* schema has never heard of, so protobuf parses the message
|
|
188
|
+
successfully and stashes those bytes as an "unknown field" on the
|
|
189
|
+
instance, invisible to ``DESCRIPTOR.fields`` (RFC-MACP-0013 §5). Left
|
|
190
|
+
unchecked, `canonical_projection` would silently hash only the known
|
|
191
|
+
fields and drop the unknown field's contribution -- exactly the
|
|
192
|
+
"not hashable under this label ... never silently ignored" outcome the
|
|
193
|
+
RFC prohibits.
|
|
194
|
+
|
|
195
|
+
Classic ``payload.UnknownFields()`` raises ``NotImplementedError`` under
|
|
196
|
+
the ``upb`` backend (the default fast C-extension protobuf backend), so
|
|
197
|
+
this uses ``google.protobuf.unknown_fields.UnknownFieldSet``, the public
|
|
198
|
+
upb-safe replacement, instead.
|
|
199
|
+
|
|
200
|
+
This only inspects ``payload`` itself, not the nested ``supersedes``
|
|
201
|
+
submessage -- see `_check_no_unknown_ref_wire_fields` for the equivalent
|
|
202
|
+
check one level down.
|
|
203
|
+
"""
|
|
204
|
+
unknown = unknown_fields.UnknownFieldSet(payload)
|
|
205
|
+
if len(unknown) > 0:
|
|
206
|
+
field_numbers = sorted({f.field_number for f in unknown})
|
|
207
|
+
raise MacpSessionError(
|
|
208
|
+
"CommitmentPayload carries wire data for unrecognized field "
|
|
209
|
+
"number(s) outside the RFC-MACP-0013 §5 frozen nine-field set "
|
|
210
|
+
f"and is not hashable under label {LABEL!r}: {field_numbers}"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _check_no_unknown_ref_wire_fields(ref: core_pb2.CommitmentRef) -> None:
|
|
215
|
+
"""Raise if ``ref`` (a ``supersedes`` submessage) carries wire data for a
|
|
216
|
+
field number the installed schema does not recognize at all.
|
|
217
|
+
|
|
218
|
+
Mirrors `_check_no_unknown_wire_fields` one level down: a peer may send a
|
|
219
|
+
``CommitmentRef`` with wire data for a field number outside the
|
|
220
|
+
RFC-MACP-0013 §5 frozen two-field set (``session_id``=1,
|
|
221
|
+
``commitment_hash``=2). `_check_frozen_ref_field_set` only catches schema
|
|
222
|
+
drift (a newer installed ``macp-proto``'s ``CommitmentRef.DESCRIPTOR``
|
|
223
|
+
growing a 3rd field); it cannot see wire data for a field number the
|
|
224
|
+
*local* schema has never heard of at all, which protobuf parses
|
|
225
|
+
successfully and stashes as an "unknown field", invisible to
|
|
226
|
+
``DESCRIPTOR.fields``. Left unchecked, `_supersedes_member` would
|
|
227
|
+
silently hash only ``session_id``/``commitment_hash`` and drop the
|
|
228
|
+
unknown field's contribution -- the same "not hashable under this label
|
|
229
|
+
... never silently ignored" outcome the RFC prohibits, just nested one
|
|
230
|
+
level deeper.
|
|
231
|
+
"""
|
|
232
|
+
unknown = unknown_fields.UnknownFieldSet(ref)
|
|
233
|
+
if len(unknown) > 0:
|
|
234
|
+
field_numbers = sorted({f.field_number for f in unknown})
|
|
235
|
+
raise MacpSessionError(
|
|
236
|
+
"CommitmentPayload.supersedes carries wire data for unrecognized "
|
|
237
|
+
"field number(s) outside the RFC-MACP-0013 §5 frozen two-field "
|
|
238
|
+
f"set and is not hashable under label {LABEL!r}: {field_numbers}"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def canonical_projection(payload: core_pb2.CommitmentPayload) -> bytes:
|
|
243
|
+
"""Return the JCS-canonicalized UTF-8 bytes of the Section 3 projection.
|
|
244
|
+
|
|
245
|
+
This is intermediate value ``C`` of RFC-MACP-0013 §4 step 2 -- the
|
|
246
|
+
canonicalized projection, not the full domain-separated preimage.
|
|
247
|
+
|
|
248
|
+
Raises `MacpSessionError` if the installed proto's `CommitmentPayload`
|
|
249
|
+
carries a field outside the RFC-MACP-0013 §5 frozen nine-field set (see
|
|
250
|
+
`_check_frozen_field_set`), or if this particular message instance
|
|
251
|
+
carries wire data for a field number the installed schema does not
|
|
252
|
+
recognize at all (see `_check_no_unknown_wire_fields`). When `supersedes`
|
|
253
|
+
is set, the same two checks are additionally applied one level down to
|
|
254
|
+
the nested `CommitmentRef` (see `_check_frozen_ref_field_set` and
|
|
255
|
+
`_check_no_unknown_ref_wire_fields`) -- an absent `supersedes` has no
|
|
256
|
+
`CommitmentRef` to check, so those two are skipped in that case.
|
|
257
|
+
"""
|
|
258
|
+
_check_frozen_field_set()
|
|
259
|
+
_check_no_unknown_wire_fields(payload)
|
|
260
|
+
if payload.HasField("supersedes"):
|
|
261
|
+
_check_frozen_ref_field_set()
|
|
262
|
+
_check_no_unknown_ref_wire_fields(payload.supersedes)
|
|
263
|
+
members: list[tuple[str, str]] = [
|
|
264
|
+
("action", _escape_json_string(payload.action)),
|
|
265
|
+
("authority_scope", _escape_json_string(payload.authority_scope)),
|
|
266
|
+
("commitment_id", _escape_json_string(payload.commitment_id)),
|
|
267
|
+
("configuration_version", _escape_json_string(payload.configuration_version)),
|
|
268
|
+
("mode_version", _escape_json_string(payload.mode_version)),
|
|
269
|
+
("outcome_positive", _json_bool(payload.outcome_positive)),
|
|
270
|
+
("policy_version", _escape_json_string(payload.policy_version)),
|
|
271
|
+
("reason", _escape_json_string(payload.reason)),
|
|
272
|
+
]
|
|
273
|
+
# Message field: omit entirely when unset (RFC-MACP-0013 §3 rule 3).
|
|
274
|
+
# HasField, not a truthiness check -- an empty CommitmentRef ({} with
|
|
275
|
+
# both sub-fields "") is falsy-looking but MUST still be projected when
|
|
276
|
+
# explicitly set, distinct from `supersedes` being absent altogether.
|
|
277
|
+
if payload.HasField("supersedes"):
|
|
278
|
+
members.append(("supersedes", _supersedes_member(payload.supersedes)))
|
|
279
|
+
# Top-level members are already in lexicographic key order except for
|
|
280
|
+
# "supersedes", which we appended last; re-sort to be explicit and
|
|
281
|
+
# robust regardless of append order above.
|
|
282
|
+
members.sort(key=lambda kv: kv[0])
|
|
283
|
+
|
|
284
|
+
body = ",".join(f"{_escape_json_string(k)}:{v}" for k, v in members)
|
|
285
|
+
text = "{" + body + "}"
|
|
286
|
+
return text.encode("utf-8")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def commitment_hash(payload: core_pb2.CommitmentPayload) -> str:
|
|
290
|
+
"""Compute the canonical commitment hash of ``payload`` (RFC-MACP-0013 §4).
|
|
291
|
+
|
|
292
|
+
Accepts any ``CommitmentPayload``, including one that is not
|
|
293
|
+
well-formed under RFC-MACP-0001 §7.3.1 (e.g. all-empty fields, or a
|
|
294
|
+
``supersedes`` with empty sub-fields) -- hashing is a pure function of
|
|
295
|
+
the projected field values and MUST NOT be gated on validity
|
|
296
|
+
(RFC-MACP-0013 §6, "hashability"). Do not add a validation call here.
|
|
297
|
+
"""
|
|
298
|
+
# No guard against an older macp-proto lacking `outcome_positive` /
|
|
299
|
+
# `supersedes`: the repo's pin is macp-proto>=0.1.6 and both fields have
|
|
300
|
+
# existed since well before that, so the fields are always present on
|
|
301
|
+
# the installed descriptor. Accessing a genuinely absent field would
|
|
302
|
+
# raise AttributeError from the generated proto class itself rather
|
|
303
|
+
# than silently under-projecting, so no additional guard is needed here.
|
|
304
|
+
preimage = LABEL.encode("ascii") + b":" + canonical_projection(payload)
|
|
305
|
+
digest = hashlib.sha256(preimage).hexdigest()
|
|
306
|
+
return f"sha256:{digest}"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def is_canonical_commitment_hash(value: str) -> bool:
|
|
310
|
+
"""Syntax predicate: ``^sha256:[0-9a-f]{64}$`` exactly (full match), no I/O.
|
|
311
|
+
|
|
312
|
+
Uses `re.fullmatch` rather than `match` + a trailing-``$`` pattern: `$`
|
|
313
|
+
alone matches immediately before a trailing ``\\n``, which would wrongly
|
|
314
|
+
accept ``"sha256:" + "a" * 64 + "\\n"``. `fullmatch` anchors both ends
|
|
315
|
+
with no such quirk, so no separate `strip()` check is needed.
|
|
316
|
+
"""
|
|
317
|
+
return bool(_HASH_RE.fullmatch(value))
|
|
@@ -14,6 +14,7 @@ from .constants import (
|
|
|
14
14
|
MACP_VERSION,
|
|
15
15
|
)
|
|
16
16
|
from .errors import MacpSessionError
|
|
17
|
+
from .validation import validate_commitment_hash
|
|
17
18
|
|
|
18
19
|
# ── Outcome inference ────────────────────────────────────────────────
|
|
19
20
|
|
|
@@ -110,7 +111,14 @@ def _has_supersedes_field() -> bool:
|
|
|
110
111
|
|
|
111
112
|
def build_commitment_ref(*, session_id: str, commitment_hash: str) -> core_pb2.CommitmentRef:
|
|
112
113
|
"""Build a ``CommitmentRef`` (macp-proto 0.1.3) pointing at a prior
|
|
113
|
-
commitment, for use as ``build_commitment_payload(supersedes=...)``.
|
|
114
|
+
commitment, for use as ``build_commitment_payload(supersedes=...)``.
|
|
115
|
+
|
|
116
|
+
``commitment_hash`` must be a canonical RFC-MACP-0013 commitment hash
|
|
117
|
+
(``sha256:`` followed by 64 lowercase hex digits) — see
|
|
118
|
+
``commitment_hash()`` in ``macp_sdk.commitment_hash`` for computing one
|
|
119
|
+
from a ``CommitmentPayload``. Raises ``MacpSessionError`` otherwise.
|
|
120
|
+
"""
|
|
121
|
+
validate_commitment_hash(commitment_hash)
|
|
114
122
|
return core_pb2.CommitmentRef(session_id=session_id, commitment_hash=commitment_hash)
|
|
115
123
|
|
|
116
124
|
|
|
@@ -131,7 +139,10 @@ def build_commitment_payload(
|
|
|
131
139
|
``supersedes`` (macp-proto 0.1.3) optionally references a prior
|
|
132
140
|
commitment this one revises, as a ``CommitmentRef`` of
|
|
133
141
|
``(session_id, commitment_hash)``. It is absent by default and unrelated
|
|
134
|
-
to proposal-mode ``supersedes_proposal_id``.
|
|
142
|
+
to proposal-mode ``supersedes_proposal_id``. ``supersedes.commitment_hash``
|
|
143
|
+
must be a canonical RFC-MACP-0013 hash (validated here even when the
|
|
144
|
+
``CommitmentRef`` was constructed directly rather than via
|
|
145
|
+
``build_commitment_ref``); raises ``MacpSessionError`` otherwise.
|
|
135
146
|
"""
|
|
136
147
|
if outcome_positive is None:
|
|
137
148
|
outcome_positive = infer_outcome_positive(action)
|
|
@@ -149,6 +160,7 @@ def build_commitment_payload(
|
|
|
149
160
|
if supersedes is not None:
|
|
150
161
|
if not _has_supersedes_field():
|
|
151
162
|
raise MacpSessionError("CommitmentPayload.supersedes requires macp-proto >= 0.1.3")
|
|
163
|
+
validate_commitment_hash(supersedes.commitment_hash)
|
|
152
164
|
kwargs["supersedes"] = supersedes
|
|
153
165
|
return core_pb2.CommitmentPayload(**kwargs)
|
|
154
166
|
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
import re
|
|
9
9
|
from collections.abc import Sequence
|
|
10
10
|
|
|
11
|
+
from .commitment_hash import is_canonical_commitment_hash
|
|
11
12
|
from .errors import MacpSessionError
|
|
12
13
|
|
|
13
14
|
# A string with the structural shape of a UUID (36 chars, hyphens at
|
|
@@ -57,6 +58,22 @@ def validate_session_id(sid: str) -> None:
|
|
|
57
58
|
)
|
|
58
59
|
|
|
59
60
|
|
|
61
|
+
def validate_commitment_hash(value: str) -> None:
|
|
62
|
+
"""Validate that *value* has the shape of a canonical commitment hash.
|
|
63
|
+
|
|
64
|
+
Mirrors the runtime's RFC-MACP-0013 syntax check: a valid
|
|
65
|
+
``commitment_hash`` MUST match ``^sha256:[0-9a-f]{64}$`` exactly (see
|
|
66
|
+
`macp_sdk.commitment_hash.is_canonical_commitment_hash`). This is a pure
|
|
67
|
+
shape check — it does not (and cannot) verify that the digest was
|
|
68
|
+
actually produced by `macp_sdk.commitment_hash.commitment_hash` over the
|
|
69
|
+
referenced payload; that requires the payload itself.
|
|
70
|
+
"""
|
|
71
|
+
if not is_canonical_commitment_hash(value):
|
|
72
|
+
raise MacpSessionError(
|
|
73
|
+
f"commitment_hash must match ^sha256:[0-9a-f]{{64}}$ (RFC-MACP-0013), got: {value!r}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
60
77
|
def validate_vote(value: str) -> str:
|
|
61
78
|
"""Normalize *value* to uppercase and validate as APPROVE/REJECT/ABSTAIN."""
|
|
62
79
|
normalized = value.upper()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{macp_sdk_python-0.6.0 → macp_sdk_python-0.7.0}/src/macp_sdk_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|