macp-sdk-python 0.2.1__py3-none-any.whl
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/__init__.py +207 -0
- macp_sdk/_logging.py +16 -0
- macp_sdk/agent/__init__.py +64 -0
- macp_sdk/agent/dispatcher.py +105 -0
- macp_sdk/agent/participant.py +404 -0
- macp_sdk/agent/runner.py +100 -0
- macp_sdk/agent/strategies.py +312 -0
- macp_sdk/agent/transports.py +153 -0
- macp_sdk/agent/types.py +64 -0
- macp_sdk/auth.py +74 -0
- macp_sdk/base_projection.py +52 -0
- macp_sdk/base_session.py +169 -0
- macp_sdk/client.py +625 -0
- macp_sdk/constants.py +20 -0
- macp_sdk/decision.py +154 -0
- macp_sdk/envelope.py +180 -0
- macp_sdk/errors.py +101 -0
- macp_sdk/handoff.py +241 -0
- macp_sdk/policy.py +292 -0
- macp_sdk/projections.py +183 -0
- macp_sdk/proposal.py +305 -0
- macp_sdk/proto_registry.py +150 -0
- macp_sdk/py.typed +0 -0
- macp_sdk/quorum.py +258 -0
- macp_sdk/retry.py +64 -0
- macp_sdk/task.py +368 -0
- macp_sdk/validation.py +117 -0
- macp_sdk/watchers.py +122 -0
- macp_sdk_python-0.2.1.dist-info/METADATA +165 -0
- macp_sdk_python-0.2.1.dist-info/RECORD +33 -0
- macp_sdk_python-0.2.1.dist-info/WHEEL +5 -0
- macp_sdk_python-0.2.1.dist-info/licenses/LICENSE +201 -0
- macp_sdk_python-0.2.1.dist-info/top_level.txt +1 -0
macp_sdk/client.py
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import queue
|
|
4
|
+
import threading
|
|
5
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import grpc
|
|
9
|
+
from macp.v1 import core_pb2, core_pb2_grpc, envelope_pb2, policy_pb2
|
|
10
|
+
|
|
11
|
+
from ._logging import logger
|
|
12
|
+
from .auth import AuthConfig
|
|
13
|
+
from .envelope import (
|
|
14
|
+
build_envelope,
|
|
15
|
+
build_progress_payload,
|
|
16
|
+
build_signal_payload,
|
|
17
|
+
serialize_message,
|
|
18
|
+
)
|
|
19
|
+
from .errors import (
|
|
20
|
+
AckFailure,
|
|
21
|
+
MacpAckError,
|
|
22
|
+
MacpIdentityMismatchError,
|
|
23
|
+
MacpSdkError,
|
|
24
|
+
MacpTransportError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_ack_reasons(ack: object) -> list[str]:
|
|
29
|
+
"""Extract structured denial reasons from an ACK error's details."""
|
|
30
|
+
import json as _json
|
|
31
|
+
|
|
32
|
+
error = getattr(ack, "error", None)
|
|
33
|
+
if not error:
|
|
34
|
+
return []
|
|
35
|
+
details_bytes = getattr(error, "details", None) or b""
|
|
36
|
+
if not details_bytes:
|
|
37
|
+
return []
|
|
38
|
+
try:
|
|
39
|
+
parsed = _json.loads(details_bytes)
|
|
40
|
+
reasons = parsed.get("reasons", [])
|
|
41
|
+
return list(reasons) if isinstance(reasons, list) else []
|
|
42
|
+
except Exception:
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_grpc_metadata_reasons(rpc_error: grpc.RpcError) -> list[str]:
|
|
47
|
+
"""Extract structured reasons from gRPC trailing metadata."""
|
|
48
|
+
import json as _json
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
metadata = rpc_error.trailing_metadata()
|
|
52
|
+
if not metadata:
|
|
53
|
+
return []
|
|
54
|
+
for item in metadata:
|
|
55
|
+
key, value = item.key, item.value
|
|
56
|
+
if key == "macp-error-details-bin":
|
|
57
|
+
data = value if isinstance(value, bytes) else value.encode("utf-8")
|
|
58
|
+
parsed = _json.loads(data)
|
|
59
|
+
reasons = parsed.get("reasons", [])
|
|
60
|
+
return list(reasons) if isinstance(reasons, list) else []
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _default_capabilities() -> core_pb2.Capabilities:
|
|
67
|
+
return core_pb2.Capabilities(
|
|
68
|
+
sessions=core_pb2.SessionsCapability(stream=True),
|
|
69
|
+
cancellation=core_pb2.CancellationCapability(cancel_session=True),
|
|
70
|
+
progress=core_pb2.ProgressCapability(progress=True),
|
|
71
|
+
manifest=core_pb2.ManifestCapability(get_manifest=True),
|
|
72
|
+
mode_registry=core_pb2.ModeRegistryCapability(list_modes=True, list_changed=True),
|
|
73
|
+
roots=core_pb2.RootsCapability(list_roots=True, list_changed=True),
|
|
74
|
+
policy_registry=policy_pb2.PolicyRegistryCapability(
|
|
75
|
+
register_policy=True, list_policies=True, list_changed=True
|
|
76
|
+
),
|
|
77
|
+
experimental=core_pb2.ExperimentalCapabilities(features={}),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MacpStream:
|
|
82
|
+
_END = object()
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
stub: core_pb2_grpc.MACPRuntimeServiceStub,
|
|
87
|
+
*,
|
|
88
|
+
metadata: Sequence[tuple[str, str]],
|
|
89
|
+
timeout: float | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
self._requests: queue.Queue[object] = queue.Queue()
|
|
92
|
+
self._responses: queue.Queue[object] = queue.Queue()
|
|
93
|
+
self._closed = False
|
|
94
|
+
self._inline_error_callbacks: list[Callable[[Any], None]] = []
|
|
95
|
+
self._call = stub.StreamSession(self._request_iter(), metadata=metadata, timeout=timeout)
|
|
96
|
+
self._thread = threading.Thread(target=self._pump_responses, daemon=True)
|
|
97
|
+
self._thread.start()
|
|
98
|
+
|
|
99
|
+
def _request_iter(self) -> Iterator[core_pb2.StreamSessionRequest]:
|
|
100
|
+
while True:
|
|
101
|
+
item = self._requests.get()
|
|
102
|
+
if item is self._END:
|
|
103
|
+
return
|
|
104
|
+
assert isinstance(item, envelope_pb2.Envelope)
|
|
105
|
+
yield core_pb2.StreamSessionRequest(envelope=item)
|
|
106
|
+
|
|
107
|
+
def _pump_responses(self) -> None:
|
|
108
|
+
try:
|
|
109
|
+
for response in self._call:
|
|
110
|
+
# Support both formats:
|
|
111
|
+
# New: StreamSessionResponse { response: { envelope | error } }
|
|
112
|
+
# Old: StreamSessionResponse { envelope }
|
|
113
|
+
inner = getattr(response, "response", None)
|
|
114
|
+
if inner is not None and hasattr(inner, "ByteSize") and inner.ByteSize() > 0:
|
|
115
|
+
envelope = getattr(inner, "envelope", None)
|
|
116
|
+
error = getattr(inner, "error", None)
|
|
117
|
+
if envelope is not None and envelope.ByteSize() > 0:
|
|
118
|
+
self._responses.put(envelope)
|
|
119
|
+
elif error is not None:
|
|
120
|
+
# Inline application-level error — notify callbacks, keep stream open
|
|
121
|
+
for cb in self._inline_error_callbacks:
|
|
122
|
+
cb(error)
|
|
123
|
+
logger.warning("inline stream error: %s", error)
|
|
124
|
+
continue
|
|
125
|
+
else:
|
|
126
|
+
# Flat format: response.envelope
|
|
127
|
+
self._responses.put(response.envelope)
|
|
128
|
+
except grpc.RpcError as exc:
|
|
129
|
+
self._responses.put(exc)
|
|
130
|
+
finally:
|
|
131
|
+
self._responses.put(self._END)
|
|
132
|
+
|
|
133
|
+
def on_inline_error(self, callback: Callable[[Any], None]) -> None:
|
|
134
|
+
"""Register a callback for inline application-level stream errors."""
|
|
135
|
+
self._inline_error_callbacks.append(callback)
|
|
136
|
+
|
|
137
|
+
def send(self, envelope: envelope_pb2.Envelope) -> None:
|
|
138
|
+
if self._closed:
|
|
139
|
+
raise MacpSdkError("stream is already closed")
|
|
140
|
+
self._requests.put(envelope)
|
|
141
|
+
|
|
142
|
+
def read(self, timeout: float | None = None) -> envelope_pb2.Envelope | None:
|
|
143
|
+
item = self._responses.get(timeout=timeout)
|
|
144
|
+
if item is self._END:
|
|
145
|
+
return None
|
|
146
|
+
if isinstance(item, grpc.RpcError):
|
|
147
|
+
raise MacpTransportError(item.details() or str(item))
|
|
148
|
+
assert isinstance(item, envelope_pb2.Envelope)
|
|
149
|
+
return item
|
|
150
|
+
|
|
151
|
+
def responses(self, timeout: float | None = None) -> Iterator[envelope_pb2.Envelope]:
|
|
152
|
+
while True:
|
|
153
|
+
envelope = self.read(timeout=timeout)
|
|
154
|
+
if envelope is None:
|
|
155
|
+
return
|
|
156
|
+
yield envelope
|
|
157
|
+
|
|
158
|
+
def close(self) -> None:
|
|
159
|
+
if self._closed:
|
|
160
|
+
return
|
|
161
|
+
self._closed = True
|
|
162
|
+
self._requests.put(self._END)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class MacpClient:
|
|
166
|
+
"""gRPC client for the MACP runtime.
|
|
167
|
+
|
|
168
|
+
Transport security follows RFC-MACP-0006 §3: TLS 1.2+ is REQUIRED in
|
|
169
|
+
production, so ``secure`` defaults to ``True``. Plaintext gRPC is only
|
|
170
|
+
available via the explicit ``allow_insecure=True`` opt-in, which is
|
|
171
|
+
intended for local development against a runtime started with
|
|
172
|
+
``MACP_ALLOW_INSECURE=1``.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def __init__(
|
|
176
|
+
self,
|
|
177
|
+
*,
|
|
178
|
+
target: str,
|
|
179
|
+
secure: bool | None = None,
|
|
180
|
+
allow_insecure: bool = False,
|
|
181
|
+
auth: AuthConfig | None = None,
|
|
182
|
+
root_certificates: bytes | None = None,
|
|
183
|
+
default_timeout: float | None = None,
|
|
184
|
+
client_name: str = "macp-sdk-python",
|
|
185
|
+
client_version: str = "0.2.1",
|
|
186
|
+
) -> None:
|
|
187
|
+
if secure is None:
|
|
188
|
+
secure = not allow_insecure
|
|
189
|
+
if not secure and not allow_insecure:
|
|
190
|
+
raise MacpSdkError(
|
|
191
|
+
"secure=False requires allow_insecure=True; "
|
|
192
|
+
"TLS is required by RFC-MACP-0006 §3 in production. "
|
|
193
|
+
"For local dev only, pass allow_insecure=True."
|
|
194
|
+
)
|
|
195
|
+
self.target = target
|
|
196
|
+
self.secure = secure
|
|
197
|
+
self.auth = auth
|
|
198
|
+
self.default_timeout = default_timeout
|
|
199
|
+
self.client_name = client_name
|
|
200
|
+
self.client_version = client_version
|
|
201
|
+
if secure:
|
|
202
|
+
creds = grpc.ssl_channel_credentials(root_certificates=root_certificates)
|
|
203
|
+
self.channel = grpc.secure_channel(target, creds)
|
|
204
|
+
else:
|
|
205
|
+
logger.warning("MacpClient insecure channel to %s — allowed only for local dev", target)
|
|
206
|
+
self.channel = grpc.insecure_channel(target)
|
|
207
|
+
self.stub = core_pb2_grpc.MACPRuntimeServiceStub(self.channel)
|
|
208
|
+
|
|
209
|
+
def close(self) -> None:
|
|
210
|
+
self.channel.close()
|
|
211
|
+
|
|
212
|
+
def __enter__(self) -> MacpClient:
|
|
213
|
+
return self
|
|
214
|
+
|
|
215
|
+
def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
|
|
216
|
+
self.close()
|
|
217
|
+
|
|
218
|
+
def _metadata(self, auth: AuthConfig | None = None) -> Sequence[tuple[str, str]]:
|
|
219
|
+
selected = auth or self.auth
|
|
220
|
+
return selected.metadata() if selected else []
|
|
221
|
+
|
|
222
|
+
def _require_auth(self, auth: AuthConfig | None = None) -> AuthConfig:
|
|
223
|
+
selected = auth or self.auth
|
|
224
|
+
if selected is None:
|
|
225
|
+
raise MacpSdkError("this operation requires auth; pass auth= or configure client.auth")
|
|
226
|
+
return selected
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def _resolve_sender(auth_cfg: AuthConfig, sender: str) -> str:
|
|
230
|
+
"""Resolve and validate the envelope sender against auth.expected_sender.
|
|
231
|
+
|
|
232
|
+
Raises :class:`MacpIdentityMismatchError` when an explicit ``sender``
|
|
233
|
+
contradicts ``auth_cfg.expected_sender``. Returns the effective sender
|
|
234
|
+
string to place on the envelope (possibly the fallback from ``auth_cfg``).
|
|
235
|
+
"""
|
|
236
|
+
expected = auth_cfg.expected_sender
|
|
237
|
+
if sender:
|
|
238
|
+
if expected is not None and sender != expected:
|
|
239
|
+
raise MacpIdentityMismatchError(expected=expected, actual=sender)
|
|
240
|
+
return sender
|
|
241
|
+
return auth_cfg.sender or ""
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def _failure_from_ack(ack: envelope_pb2.Ack) -> AckFailure:
|
|
245
|
+
"""Build an :class:`AckFailure` from a NACK envelope, including reasons.
|
|
246
|
+
|
|
247
|
+
Used by every RPC that returns an ``Ack`` (``send``, ``cancel_session``)
|
|
248
|
+
so structured denial reasons (``POLICY_DENIED`` rule IDs, etc.) surface
|
|
249
|
+
uniformly in ``MacpAckError.reasons`` no matter which call produced
|
|
250
|
+
them.
|
|
251
|
+
"""
|
|
252
|
+
error = ack.error
|
|
253
|
+
return AckFailure(
|
|
254
|
+
code=(error.code if error else "UNKNOWN"),
|
|
255
|
+
message=(error.message if error else "runtime returned nack"),
|
|
256
|
+
session_id=ack.session_id,
|
|
257
|
+
message_id=ack.message_id,
|
|
258
|
+
reasons=_parse_ack_reasons(ack),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def initialize(self, *, timeout: float | None = None) -> core_pb2.InitializeResponse:
|
|
262
|
+
request = core_pb2.InitializeRequest(
|
|
263
|
+
supported_protocol_versions=["1.0"],
|
|
264
|
+
client_info=core_pb2.ClientInfo(
|
|
265
|
+
name=self.client_name,
|
|
266
|
+
title=self.client_name,
|
|
267
|
+
version=self.client_version,
|
|
268
|
+
description="Python SDK for the MACP runtime",
|
|
269
|
+
website_url="",
|
|
270
|
+
),
|
|
271
|
+
capabilities=_default_capabilities(),
|
|
272
|
+
)
|
|
273
|
+
return self.stub.Initialize(request, timeout=timeout or self.default_timeout)
|
|
274
|
+
|
|
275
|
+
def send(
|
|
276
|
+
self,
|
|
277
|
+
envelope: envelope_pb2.Envelope,
|
|
278
|
+
*,
|
|
279
|
+
auth: AuthConfig | None = None,
|
|
280
|
+
timeout: float | None = None,
|
|
281
|
+
raise_on_nack: bool = True,
|
|
282
|
+
) -> envelope_pb2.Ack:
|
|
283
|
+
auth_cfg = self._require_auth(auth)
|
|
284
|
+
try:
|
|
285
|
+
response = self.stub.Send(
|
|
286
|
+
core_pb2.SendRequest(envelope=envelope),
|
|
287
|
+
metadata=self._metadata(auth_cfg),
|
|
288
|
+
timeout=timeout or self.default_timeout,
|
|
289
|
+
)
|
|
290
|
+
except grpc.RpcError as rpc_err:
|
|
291
|
+
code = rpc_err.code()
|
|
292
|
+
if code == grpc.StatusCode.ALREADY_EXISTS:
|
|
293
|
+
failure = AckFailure(
|
|
294
|
+
code="SESSION_ALREADY_EXISTS",
|
|
295
|
+
message=rpc_err.details() or "session already exists",
|
|
296
|
+
)
|
|
297
|
+
raise MacpAckError(failure) from rpc_err
|
|
298
|
+
if code == grpc.StatusCode.FAILED_PRECONDITION:
|
|
299
|
+
reasons = _parse_grpc_metadata_reasons(rpc_err)
|
|
300
|
+
failure = AckFailure(
|
|
301
|
+
code="POLICY_DENIED",
|
|
302
|
+
message=rpc_err.details() or "policy denied",
|
|
303
|
+
reasons=reasons,
|
|
304
|
+
)
|
|
305
|
+
raise MacpAckError(failure) from rpc_err
|
|
306
|
+
if code == grpc.StatusCode.INVALID_ARGUMENT:
|
|
307
|
+
raise MacpTransportError(rpc_err.details() or "invalid argument") from rpc_err
|
|
308
|
+
raise MacpTransportError(rpc_err.details() or str(rpc_err)) from rpc_err
|
|
309
|
+
ack = response.ack
|
|
310
|
+
# Duplicate acks are idempotent success — the message was already accepted.
|
|
311
|
+
# This matches TypeScript SDK behaviour and is correct for retry scenarios.
|
|
312
|
+
if ack.duplicate:
|
|
313
|
+
return ack
|
|
314
|
+
if raise_on_nack and not ack.ok:
|
|
315
|
+
raise MacpAckError(self._failure_from_ack(ack))
|
|
316
|
+
return ack
|
|
317
|
+
|
|
318
|
+
def get_session(
|
|
319
|
+
self,
|
|
320
|
+
session_id: str,
|
|
321
|
+
*,
|
|
322
|
+
auth: AuthConfig | None = None,
|
|
323
|
+
timeout: float | None = None,
|
|
324
|
+
) -> core_pb2.GetSessionResponse:
|
|
325
|
+
auth_cfg = self._require_auth(auth)
|
|
326
|
+
return self.stub.GetSession(
|
|
327
|
+
core_pb2.GetSessionRequest(session_id=session_id),
|
|
328
|
+
metadata=self._metadata(auth_cfg),
|
|
329
|
+
timeout=timeout or self.default_timeout,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def cancel_session(
|
|
333
|
+
self,
|
|
334
|
+
session_id: str,
|
|
335
|
+
*,
|
|
336
|
+
reason: str,
|
|
337
|
+
cancelled_by: str = "",
|
|
338
|
+
auth: AuthConfig | None = None,
|
|
339
|
+
timeout: float | None = None,
|
|
340
|
+
raise_on_nack: bool = True,
|
|
341
|
+
) -> envelope_pb2.Ack:
|
|
342
|
+
auth_cfg = self._require_auth(auth)
|
|
343
|
+
request_kwargs: dict[str, object] = {
|
|
344
|
+
"session_id": session_id,
|
|
345
|
+
"reason": reason,
|
|
346
|
+
}
|
|
347
|
+
# Forward-compatible: add cancelled_by if proto supports it
|
|
348
|
+
if cancelled_by:
|
|
349
|
+
has_field = any(
|
|
350
|
+
f.name == "cancelled_by" for f in core_pb2.CancelSessionRequest.DESCRIPTOR.fields
|
|
351
|
+
)
|
|
352
|
+
if has_field:
|
|
353
|
+
request_kwargs["cancelled_by"] = cancelled_by
|
|
354
|
+
try:
|
|
355
|
+
response = self.stub.CancelSession(
|
|
356
|
+
core_pb2.CancelSessionRequest(**request_kwargs),
|
|
357
|
+
metadata=self._metadata(auth_cfg),
|
|
358
|
+
timeout=timeout or self.default_timeout,
|
|
359
|
+
)
|
|
360
|
+
except grpc.RpcError as rpc_err:
|
|
361
|
+
raise MacpTransportError(rpc_err.details() or str(rpc_err)) from rpc_err
|
|
362
|
+
ack = response.ack
|
|
363
|
+
if raise_on_nack and not ack.ok:
|
|
364
|
+
raise MacpAckError(self._failure_from_ack(ack))
|
|
365
|
+
return ack
|
|
366
|
+
|
|
367
|
+
def get_manifest(
|
|
368
|
+
self, agent_id: str = "", *, timeout: float | None = None
|
|
369
|
+
) -> core_pb2.GetManifestResponse:
|
|
370
|
+
return self.stub.GetManifest(
|
|
371
|
+
core_pb2.GetManifestRequest(agent_id=agent_id),
|
|
372
|
+
timeout=timeout or self.default_timeout,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def list_modes(self, *, timeout: float | None = None) -> core_pb2.ListModesResponse:
|
|
376
|
+
return self.stub.ListModes(
|
|
377
|
+
core_pb2.ListModesRequest(),
|
|
378
|
+
timeout=timeout or self.default_timeout,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def list_ext_modes(self, *, timeout: float | None = None) -> core_pb2.ListExtModesResponse:
|
|
382
|
+
return self.stub.ListExtModes(
|
|
383
|
+
core_pb2.ListExtModesRequest(),
|
|
384
|
+
timeout=timeout or self.default_timeout,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def list_roots(self, *, timeout: float | None = None) -> core_pb2.ListRootsResponse:
|
|
388
|
+
return self.stub.ListRoots(
|
|
389
|
+
core_pb2.ListRootsRequest(),
|
|
390
|
+
timeout=timeout or self.default_timeout,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
def register_ext_mode(
|
|
394
|
+
self,
|
|
395
|
+
descriptor: core_pb2.ModeDescriptor,
|
|
396
|
+
*,
|
|
397
|
+
auth: AuthConfig | None = None,
|
|
398
|
+
timeout: float | None = None,
|
|
399
|
+
) -> core_pb2.RegisterExtModeResponse:
|
|
400
|
+
auth_cfg = self._require_auth(auth)
|
|
401
|
+
return self.stub.RegisterExtMode(
|
|
402
|
+
core_pb2.RegisterExtModeRequest(mode_descriptor=descriptor),
|
|
403
|
+
metadata=self._metadata(auth_cfg),
|
|
404
|
+
timeout=timeout or self.default_timeout,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def unregister_ext_mode(
|
|
408
|
+
self,
|
|
409
|
+
mode: str,
|
|
410
|
+
*,
|
|
411
|
+
auth: AuthConfig | None = None,
|
|
412
|
+
timeout: float | None = None,
|
|
413
|
+
) -> core_pb2.UnregisterExtModeResponse:
|
|
414
|
+
auth_cfg = self._require_auth(auth)
|
|
415
|
+
return self.stub.UnregisterExtMode(
|
|
416
|
+
core_pb2.UnregisterExtModeRequest(mode=mode),
|
|
417
|
+
metadata=self._metadata(auth_cfg),
|
|
418
|
+
timeout=timeout or self.default_timeout,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def promote_mode(
|
|
422
|
+
self,
|
|
423
|
+
mode: str,
|
|
424
|
+
promoted_mode_name: str = "",
|
|
425
|
+
*,
|
|
426
|
+
auth: AuthConfig | None = None,
|
|
427
|
+
timeout: float | None = None,
|
|
428
|
+
) -> core_pb2.PromoteModeResponse:
|
|
429
|
+
auth_cfg = self._require_auth(auth)
|
|
430
|
+
return self.stub.PromoteMode(
|
|
431
|
+
core_pb2.PromoteModeRequest(mode=mode, promoted_mode_name=promoted_mode_name),
|
|
432
|
+
metadata=self._metadata(auth_cfg),
|
|
433
|
+
timeout=timeout or self.default_timeout,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# ── Governance policy lifecycle ───────────────────────────────────
|
|
437
|
+
|
|
438
|
+
def register_policy(
|
|
439
|
+
self,
|
|
440
|
+
descriptor: policy_pb2.PolicyDescriptor,
|
|
441
|
+
*,
|
|
442
|
+
auth: AuthConfig | None = None,
|
|
443
|
+
timeout: float | None = None,
|
|
444
|
+
) -> policy_pb2.RegisterPolicyResponse:
|
|
445
|
+
"""Register a governance policy with the runtime."""
|
|
446
|
+
auth_cfg = self._require_auth(auth)
|
|
447
|
+
return self.stub.RegisterPolicy(
|
|
448
|
+
policy_pb2.RegisterPolicyRequest(policy_descriptor=descriptor),
|
|
449
|
+
metadata=self._metadata(auth_cfg),
|
|
450
|
+
timeout=timeout or self.default_timeout,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
def unregister_policy(
|
|
454
|
+
self,
|
|
455
|
+
policy_id: str,
|
|
456
|
+
*,
|
|
457
|
+
auth: AuthConfig | None = None,
|
|
458
|
+
timeout: float | None = None,
|
|
459
|
+
) -> policy_pb2.UnregisterPolicyResponse:
|
|
460
|
+
"""Unregister a governance policy from the runtime."""
|
|
461
|
+
auth_cfg = self._require_auth(auth)
|
|
462
|
+
return self.stub.UnregisterPolicy(
|
|
463
|
+
policy_pb2.UnregisterPolicyRequest(policy_id=policy_id),
|
|
464
|
+
metadata=self._metadata(auth_cfg),
|
|
465
|
+
timeout=timeout or self.default_timeout,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def get_policy(
|
|
469
|
+
self,
|
|
470
|
+
policy_id: str,
|
|
471
|
+
*,
|
|
472
|
+
auth: AuthConfig | None = None,
|
|
473
|
+
timeout: float | None = None,
|
|
474
|
+
) -> policy_pb2.GetPolicyResponse:
|
|
475
|
+
"""Retrieve a single governance policy by ID."""
|
|
476
|
+
auth_cfg = self._require_auth(auth)
|
|
477
|
+
return self.stub.GetPolicy(
|
|
478
|
+
policy_pb2.GetPolicyRequest(policy_id=policy_id),
|
|
479
|
+
metadata=self._metadata(auth_cfg),
|
|
480
|
+
timeout=timeout or self.default_timeout,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
def list_policies(
|
|
484
|
+
self,
|
|
485
|
+
mode: str | None = None,
|
|
486
|
+
*,
|
|
487
|
+
auth: AuthConfig | None = None,
|
|
488
|
+
timeout: float | None = None,
|
|
489
|
+
) -> policy_pb2.ListPoliciesResponse:
|
|
490
|
+
"""List registered governance policies, optionally filtered by mode."""
|
|
491
|
+
auth_cfg = self._require_auth(auth)
|
|
492
|
+
return self.stub.ListPolicies(
|
|
493
|
+
policy_pb2.ListPoliciesRequest(mode=mode or ""),
|
|
494
|
+
metadata=self._metadata(auth_cfg),
|
|
495
|
+
timeout=timeout or self.default_timeout,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
def watch_policies(
|
|
499
|
+
self, *, timeout: float | None = None
|
|
500
|
+
) -> Iterator[policy_pb2.WatchPoliciesResponse]:
|
|
501
|
+
"""Server-streaming RPC: yields governance policy change events."""
|
|
502
|
+
logger.debug("watch_policies starting")
|
|
503
|
+
call = self.stub.WatchPolicies(
|
|
504
|
+
policy_pb2.WatchPoliciesRequest(),
|
|
505
|
+
timeout=timeout or self.default_timeout,
|
|
506
|
+
)
|
|
507
|
+
try:
|
|
508
|
+
yield from call
|
|
509
|
+
except grpc.RpcError as exc:
|
|
510
|
+
raise MacpTransportError(str(exc)) from exc
|
|
511
|
+
|
|
512
|
+
def open_stream(
|
|
513
|
+
self, *, auth: AuthConfig | None = None, timeout: float | None = None
|
|
514
|
+
) -> MacpStream:
|
|
515
|
+
auth_cfg = self._require_auth(auth)
|
|
516
|
+
return MacpStream(
|
|
517
|
+
self.stub,
|
|
518
|
+
metadata=self._metadata(auth_cfg),
|
|
519
|
+
timeout=timeout or self.default_timeout,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
def watch_mode_registry(
|
|
523
|
+
self, *, timeout: float | None = None
|
|
524
|
+
) -> Iterator[core_pb2.WatchModeRegistryResponse]:
|
|
525
|
+
"""Server-streaming RPC: yields mode registry change events."""
|
|
526
|
+
logger.debug("watch_mode_registry starting")
|
|
527
|
+
call = self.stub.WatchModeRegistry(
|
|
528
|
+
core_pb2.WatchModeRegistryRequest(),
|
|
529
|
+
timeout=timeout or self.default_timeout,
|
|
530
|
+
)
|
|
531
|
+
try:
|
|
532
|
+
yield from call
|
|
533
|
+
except grpc.RpcError as exc:
|
|
534
|
+
raise MacpTransportError(str(exc)) from exc
|
|
535
|
+
|
|
536
|
+
def watch_roots(self, *, timeout: float | None = None) -> Iterator[core_pb2.WatchRootsResponse]:
|
|
537
|
+
"""Server-streaming RPC: yields root change events."""
|
|
538
|
+
logger.debug("watch_roots starting")
|
|
539
|
+
call = self.stub.WatchRoots(
|
|
540
|
+
core_pb2.WatchRootsRequest(),
|
|
541
|
+
timeout=timeout or self.default_timeout,
|
|
542
|
+
)
|
|
543
|
+
try:
|
|
544
|
+
yield from call
|
|
545
|
+
except grpc.RpcError as exc:
|
|
546
|
+
raise MacpTransportError(str(exc)) from exc
|
|
547
|
+
|
|
548
|
+
def watch_signals(
|
|
549
|
+
self, *, timeout: float | None = None
|
|
550
|
+
) -> Iterator[core_pb2.WatchSignalsResponse]:
|
|
551
|
+
"""Server-streaming RPC: yields ambient signal envelopes."""
|
|
552
|
+
logger.debug("watch_signals starting")
|
|
553
|
+
call = self.stub.WatchSignals(
|
|
554
|
+
core_pb2.WatchSignalsRequest(),
|
|
555
|
+
timeout=timeout or self.default_timeout,
|
|
556
|
+
)
|
|
557
|
+
try:
|
|
558
|
+
yield from call
|
|
559
|
+
except grpc.RpcError as exc:
|
|
560
|
+
raise MacpTransportError(str(exc)) from exc
|
|
561
|
+
|
|
562
|
+
def send_signal(
|
|
563
|
+
self,
|
|
564
|
+
*,
|
|
565
|
+
signal_type: str,
|
|
566
|
+
data: bytes = b"",
|
|
567
|
+
confidence: float = 0.0,
|
|
568
|
+
correlation_session_id: str = "",
|
|
569
|
+
sender: str = "",
|
|
570
|
+
auth: AuthConfig | None = None,
|
|
571
|
+
timeout: float | None = None,
|
|
572
|
+
) -> envelope_pb2.Ack:
|
|
573
|
+
"""Send an ambient (non-session) signal to the runtime."""
|
|
574
|
+
auth_cfg = self._require_auth(auth)
|
|
575
|
+
payload = build_signal_payload(
|
|
576
|
+
signal_type=signal_type,
|
|
577
|
+
data=data,
|
|
578
|
+
confidence=confidence,
|
|
579
|
+
correlation_session_id=correlation_session_id,
|
|
580
|
+
)
|
|
581
|
+
envelope = build_envelope(
|
|
582
|
+
mode="",
|
|
583
|
+
message_type="Signal",
|
|
584
|
+
session_id="",
|
|
585
|
+
payload=serialize_message(payload),
|
|
586
|
+
sender=self._resolve_sender(auth_cfg, sender),
|
|
587
|
+
)
|
|
588
|
+
return self.send(envelope, auth=auth_cfg, timeout=timeout)
|
|
589
|
+
|
|
590
|
+
def send_progress(
|
|
591
|
+
self,
|
|
592
|
+
*,
|
|
593
|
+
session_id: str = "",
|
|
594
|
+
mode: str = "",
|
|
595
|
+
progress_token: str,
|
|
596
|
+
progress: float,
|
|
597
|
+
total: float,
|
|
598
|
+
message: str = "",
|
|
599
|
+
target_message_id: str = "",
|
|
600
|
+
sender: str = "",
|
|
601
|
+
auth: AuthConfig | None = None,
|
|
602
|
+
timeout: float | None = None,
|
|
603
|
+
) -> envelope_pb2.Ack:
|
|
604
|
+
"""Send a progress update.
|
|
605
|
+
|
|
606
|
+
When ``session_id`` and ``mode`` are empty, the progress is treated
|
|
607
|
+
as an *ambient* progress message routed through the signal broadcast
|
|
608
|
+
path.
|
|
609
|
+
"""
|
|
610
|
+
auth_cfg = self._require_auth(auth)
|
|
611
|
+
payload = build_progress_payload(
|
|
612
|
+
progress_token=progress_token,
|
|
613
|
+
progress=progress,
|
|
614
|
+
total=total,
|
|
615
|
+
message=message,
|
|
616
|
+
target_message_id=target_message_id,
|
|
617
|
+
)
|
|
618
|
+
envelope = build_envelope(
|
|
619
|
+
mode=mode,
|
|
620
|
+
message_type="Progress",
|
|
621
|
+
session_id=session_id,
|
|
622
|
+
payload=serialize_message(payload),
|
|
623
|
+
sender=self._resolve_sender(auth_cfg, sender),
|
|
624
|
+
)
|
|
625
|
+
return self.send(envelope, auth=auth_cfg, timeout=timeout)
|
macp_sdk/constants.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MACP_VERSION = "1.0"
|
|
2
|
+
|
|
3
|
+
DEFAULT_MODE_VERSION = "1.0.0"
|
|
4
|
+
DEFAULT_CONFIGURATION_VERSION = "config.default"
|
|
5
|
+
DEFAULT_POLICY_VERSION = "policy.default"
|
|
6
|
+
|
|
7
|
+
MODE_DECISION = "macp.mode.decision.v1"
|
|
8
|
+
MODE_PROPOSAL = "macp.mode.proposal.v1"
|
|
9
|
+
MODE_TASK = "macp.mode.task.v1"
|
|
10
|
+
MODE_HANDOFF = "macp.mode.handoff.v1"
|
|
11
|
+
MODE_QUORUM = "macp.mode.quorum.v1"
|
|
12
|
+
MODE_MULTI_ROUND = "ext.multi_round.v1"
|
|
13
|
+
|
|
14
|
+
STANDARD_MODES = (
|
|
15
|
+
MODE_DECISION,
|
|
16
|
+
MODE_PROPOSAL,
|
|
17
|
+
MODE_TASK,
|
|
18
|
+
MODE_HANDOFF,
|
|
19
|
+
MODE_QUORUM,
|
|
20
|
+
)
|