macp-sdk-python 0.8.0__tar.gz → 0.9.1__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.8.0/src/macp_sdk_python.egg-info → macp_sdk_python-0.9.1}/PKG-INFO +1 -1
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/pyproject.toml +1 -1
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/__init__.py +2 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/base_projection.py +42 -4
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/client.py +12 -3
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/policy.py +17 -2
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/validation.py +36 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1/src/macp_sdk_python.egg-info}/PKG-INFO +1 -1
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/LICENSE +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/README.md +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/setup.cfg +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/_logging.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/agent/__init__.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/agent/cancel_callback.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/agent/dispatcher.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/agent/participant.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/agent/runner.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/agent/strategies.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/agent/transports.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/agent/types.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/auth.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/base_session.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/commitment_hash.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/constants.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/decision.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/envelope.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/errors.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/handoff.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/projections.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/proposal.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/proto_registry.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/py.typed +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/quorum.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/retry.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/task.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk/watchers.py +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk_python.egg-info/SOURCES.txt +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk_python.egg-info/dependency_links.txt +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk_python.egg-info/requires.txt +0 -0
- {macp_sdk_python-0.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk_python.egg-info/top_level.txt +0 -0
|
@@ -95,6 +95,7 @@ from .validation import (
|
|
|
95
95
|
validate_confidence,
|
|
96
96
|
validate_participant_count,
|
|
97
97
|
validate_participants,
|
|
98
|
+
validate_progress_scope,
|
|
98
99
|
validate_recommendation,
|
|
99
100
|
validate_required_field,
|
|
100
101
|
validate_session_id,
|
|
@@ -218,6 +219,7 @@ __all__ = [
|
|
|
218
219
|
"validate_confidence",
|
|
219
220
|
"validate_participant_count",
|
|
220
221
|
"validate_participants",
|
|
222
|
+
"validate_progress_scope",
|
|
221
223
|
"validate_recommendation",
|
|
222
224
|
"validate_required_field",
|
|
223
225
|
"validate_session_id",
|
|
@@ -207,7 +207,7 @@ class BaseProjection(ABC):
|
|
|
207
207
|
#
|
|
208
208
|
# What this does NOT cover: self.phase (assigned directly on
|
|
209
209
|
# BaseProjection by subclasses — see projections.py:84,
|
|
210
|
-
# task.py:
|
|
210
|
+
# task.py:102, handoff.py:70) and any subclass-owned collection
|
|
211
211
|
# (evaluations, objections, accepts, rejections, updates,
|
|
212
212
|
# completions, failures) are never rolled back, because this
|
|
213
213
|
# method has no way to know what a subclass mutated. That is safe
|
|
@@ -219,7 +219,7 @@ class BaseProjection(ABC):
|
|
|
219
219
|
# mutates subclass state and then raises. This is a
|
|
220
220
|
# raise-before-mutate invariant that _apply_mode_message
|
|
221
221
|
# implementations must preserve. BaseProjection is exported for
|
|
222
|
-
# third-party subclassing (see __init__.py:
|
|
222
|
+
# third-party subclassing (see __init__.py:151), and upcoming
|
|
223
223
|
# first-wins/anomaly-tracking logic must keep fallible work ahead
|
|
224
224
|
# of mutations to keep this guarantee honest.
|
|
225
225
|
#
|
|
@@ -242,7 +242,7 @@ class BaseProjection(ABC):
|
|
|
242
242
|
# above is the only mutation of transcript between there and
|
|
243
243
|
# here, so transcript[-1] is the entry this call just
|
|
244
244
|
# appended. But BaseProjection is a public ABC (exported at
|
|
245
|
-
# __init__.py:
|
|
245
|
+
# __init__.py:151) that third parties may subclass, so guard
|
|
246
246
|
# with an identity check rather than popping unconditionally.
|
|
247
247
|
# The two ways the guard could see something else at [-1] are:
|
|
248
248
|
# - a subclass's _apply_mode_message appending to
|
|
@@ -267,7 +267,45 @@ class BaseProjection(ABC):
|
|
|
267
267
|
|
|
268
268
|
@abstractmethod
|
|
269
269
|
def _apply_mode_message(self, envelope: envelope_pb2.Envelope) -> None:
|
|
270
|
-
"""Handle a mode-specific (non-Commitment) envelope.
|
|
270
|
+
"""Handle a mode-specific (non-Commitment) envelope.
|
|
271
|
+
|
|
272
|
+
**Implementations MUST do all fallible work before any mutation**
|
|
273
|
+
("raise-before-mutate"). Parse the payload (``ParseFromString``, the
|
|
274
|
+
one operation that realistically raises here), validate it, and
|
|
275
|
+
decide what to do — *then* assign ``self.phase`` or touch any
|
|
276
|
+
subclass collection. Never mutate and then run something that can
|
|
277
|
+
raise.
|
|
278
|
+
|
|
279
|
+
This is a real contract, not style advice, and it is what makes
|
|
280
|
+
``apply_envelope``'s rollback sufficient. That rollback restores
|
|
281
|
+
``transcript`` and the ``message_id`` dedup set only — it cannot
|
|
282
|
+
restore subclass state, because the base class has no way to know
|
|
283
|
+
what a subclass mutated (a slotted subclass has no ``__dict__`` to
|
|
284
|
+
snapshot, and deep-copying every projection per envelope would be a
|
|
285
|
+
real per-message cost). An implementation that mutates and then
|
|
286
|
+
raises therefore leaves *its own* state half-applied while
|
|
287
|
+
``apply_envelope`` reports a clean rollback and invites the caller
|
|
288
|
+
to retry — a partial apply the base class cannot detect or undo.
|
|
289
|
+
|
|
290
|
+
Every implementation in this SDK honors this today (see the
|
|
291
|
+
per-branch parse-then-mutate ordering in ``projections.py``,
|
|
292
|
+
``quorum.py``, ``task.py``, ``handoff.py``, and ``proposal.py``).
|
|
293
|
+
Two branches come close to the line: ``handoff.py``'s
|
|
294
|
+
``HandoffAccept`` writes the record before reading
|
|
295
|
+
``getattr(p, "implicit", False)``, and ``proposal.py``'s ``Reject``
|
|
296
|
+
appends before reading ``p.terminal`` and ``self.proposals.get(...)``.
|
|
297
|
+
Both are safe only because a defaulted ``getattr``, a protobuf field
|
|
298
|
+
read, and ``dict.get`` cannot raise — not because the ordering there
|
|
299
|
+
is exemplary.
|
|
300
|
+
|
|
301
|
+
``tests/unit/test_projection_rollback_invariant.py`` pins the
|
|
302
|
+
ordering by feeding each branch a payload that cannot parse and
|
|
303
|
+
asserting no derived state moved. Note what that does *not* reach: a
|
|
304
|
+
branch that mutates and then does something else fallible (a raising
|
|
305
|
+
property, an ``int()``, a strict lookup) is not caught, because a
|
|
306
|
+
malformed payload never gets that far. The contract is broader than
|
|
307
|
+
the test — hence stating it here.
|
|
308
|
+
"""
|
|
271
309
|
|
|
272
310
|
def _record_anomaly(
|
|
273
311
|
self,
|
|
@@ -24,6 +24,7 @@ from .errors import (
|
|
|
24
24
|
MacpSessionError,
|
|
25
25
|
MacpTransportError,
|
|
26
26
|
)
|
|
27
|
+
from .validation import validate_progress_scope
|
|
27
28
|
|
|
28
29
|
# Public typing alias for inline stream-error callbacks. Parity with
|
|
29
30
|
# typescript-sdk's ``InlineErrorCallback``. Receives the protobuf
|
|
@@ -962,10 +963,18 @@ class MacpClient:
|
|
|
962
963
|
) -> envelope_pb2.Ack:
|
|
963
964
|
"""Send a progress update.
|
|
964
965
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
966
|
+
Per RFC-MACP-0001 §6, ``Progress`` is legal in exactly two shapes:
|
|
967
|
+
|
|
968
|
+
* *ambient* -- ``session_id`` and ``mode`` both empty, routed through
|
|
969
|
+
the signal broadcast path; or
|
|
970
|
+
* *session-scoped* -- both non-empty.
|
|
971
|
+
|
|
972
|
+
Supplying exactly one of the two raises ``MacpSessionError`` naming
|
|
973
|
+
the mismatched field. The runtime rejects that mixed envelope with
|
|
974
|
+
``INVALID_ENVELOPE``, so this only surfaces the failure earlier and
|
|
975
|
+
more clearly.
|
|
968
976
|
"""
|
|
977
|
+
validate_progress_scope(session_id, mode)
|
|
969
978
|
auth_cfg = self._require_auth(auth)
|
|
970
979
|
payload = build_progress_payload(
|
|
971
980
|
progress_token=progress_token,
|
|
@@ -155,7 +155,8 @@ class QuorumThreshold:
|
|
|
155
155
|
``quorum-rules.schema.json``: an approval count for ``n_of_m`` /
|
|
156
156
|
``weighted``, and an integer percentage 0-100 for ``percentage``. A
|
|
157
157
|
fractional value (e.g. ``0.75``) is rejected by the runtime's schema
|
|
158
|
-
validation, so this is typed ``int`` and
|
|
158
|
+
validation, so this is typed ``int`` and ``build_quorum_policy`` rejects
|
|
159
|
+
a non-integer or out-of-range value at build time.
|
|
159
160
|
"""
|
|
160
161
|
|
|
161
162
|
type: str = "n_of_m"
|
|
@@ -185,7 +186,21 @@ def build_quorum_policy(
|
|
|
185
186
|
|
|
186
187
|
# Match the canonical schema's constraints before the runtime does, so a
|
|
187
188
|
# bad descriptor fails immediately client-side instead of round-tripping
|
|
188
|
-
# to an INVALID_POLICY_DEFINITION from RegisterPolicy.
|
|
189
|
+
# to an INVALID_POLICY_DEFINITION from RegisterPolicy. Order matters:
|
|
190
|
+
# type first (a float or bool reaching the range checks below would
|
|
191
|
+
# compare fine numerically but produce a confusing message), then >= 0,
|
|
192
|
+
# then the percentage-specific <= 100 cap.
|
|
193
|
+
#
|
|
194
|
+
# bool is checked explicitly because isinstance(True, int) is True in
|
|
195
|
+
# Python -- QuorumThreshold(value=True) must not silently mean 1.
|
|
196
|
+
if isinstance(t.value, bool) or not isinstance(t.value, int):
|
|
197
|
+
raise MacpSessionError(
|
|
198
|
+
"quorum threshold value must be an integer (e.g. 75 for 75%), got "
|
|
199
|
+
f"{t.value!r}. The canonical quorum-rules schema declares "
|
|
200
|
+
"'value' as an integer for every threshold type; a fractional "
|
|
201
|
+
"value like 0.75 would produce a schema-invalid descriptor that "
|
|
202
|
+
"the runtime rejects at RegisterPolicy with worse diagnostics."
|
|
203
|
+
)
|
|
189
204
|
if t.value < 0:
|
|
190
205
|
raise MacpSessionError(f"quorum threshold value must be >= 0, got {t.value}")
|
|
191
206
|
if t.type == "percentage" and t.value > 100:
|
|
@@ -122,6 +122,42 @@ def validate_signal_type(signal_type: str, data: bytes | None = None) -> None:
|
|
|
122
122
|
raise MacpSessionError("signal_type must be non-empty when data is present")
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
def validate_progress_scope(session_id: str, mode: str) -> None:
|
|
126
|
+
"""Validate the ``Progress`` scope pairing (RFC-MACP-0001 §6).
|
|
127
|
+
|
|
128
|
+
``Progress`` is legal in exactly two shapes -- *ambient* (``session_id``
|
|
129
|
+
and ``mode`` both empty) or *session-scoped* (both non-empty). An envelope
|
|
130
|
+
with exactly one of the two empty is a mixed shape that the runtime
|
|
131
|
+
rejects with ``INVALID_ENVELOPE``; raising here names the mismatched field
|
|
132
|
+
instead.
|
|
133
|
+
|
|
134
|
+
Unlike Signals, ``Progress`` is *not* required to be ambient -- this is a
|
|
135
|
+
tri-state rule, so neither field may be inferred from the other.
|
|
136
|
+
|
|
137
|
+
The emptiness test deliberately mirrors the runtime's
|
|
138
|
+
``validate_envelope_shape`` **exactly**, including its asymmetry:
|
|
139
|
+
``session_id`` is compared raw while ``mode`` is stripped first. Mirroring
|
|
140
|
+
rather than normalising is the only choice under which the SDK neither
|
|
141
|
+
accepts a shape the runtime rejects nor rejects one it accepts.
|
|
142
|
+
Concretely, a whitespace-only ``mode`` alongside an empty ``session_id``
|
|
143
|
+
is ambient to the runtime, so it stays ambient here.
|
|
144
|
+
"""
|
|
145
|
+
session_id_empty = session_id == ""
|
|
146
|
+
mode_empty = mode.strip() == ""
|
|
147
|
+
if session_id_empty == mode_empty:
|
|
148
|
+
return
|
|
149
|
+
detail = (
|
|
150
|
+
f"mode is {mode!r} but session_id is empty"
|
|
151
|
+
if session_id_empty
|
|
152
|
+
else f"session_id is {session_id!r} but mode is empty"
|
|
153
|
+
)
|
|
154
|
+
raise MacpSessionError(
|
|
155
|
+
"Progress must be either ambient (session_id and mode both empty) or "
|
|
156
|
+
f"session-scoped (both non-empty), but {detail} (RFC-MACP-0001 §6). "
|
|
157
|
+
"Pass both fields for a session-scoped Progress, or neither for an ambient one."
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
125
161
|
def validate_ttl_ms(ttl_ms: int) -> None:
|
|
126
162
|
"""Validate that *ttl_ms* is in [1, 86_400_000]."""
|
|
127
163
|
if ttl_ms < 1 or ttl_ms > _MAX_TTL_MS:
|
|
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.8.0 → macp_sdk_python-0.9.1}/src/macp_sdk_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|