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
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .._logging import logger
|
|
6
|
+
from ..auth import AuthConfig
|
|
7
|
+
from ..base_projection import BaseProjection
|
|
8
|
+
from ..client import MacpClient
|
|
9
|
+
from ..constants import (
|
|
10
|
+
MODE_DECISION,
|
|
11
|
+
MODE_HANDOFF,
|
|
12
|
+
MODE_PROPOSAL,
|
|
13
|
+
MODE_QUORUM,
|
|
14
|
+
MODE_TASK,
|
|
15
|
+
)
|
|
16
|
+
from ..envelope import build_commitment_payload, build_envelope, serialize_message
|
|
17
|
+
from ..handoff import HandoffProjection
|
|
18
|
+
from ..projections import DecisionProjection
|
|
19
|
+
from ..proposal import ProposalProjection
|
|
20
|
+
from ..quorum import QuorumProjection
|
|
21
|
+
from ..task import TaskProjection
|
|
22
|
+
from .dispatcher import Dispatcher
|
|
23
|
+
from .transports import GrpcTransportAdapter, TransportAdapter, _envelope_to_message
|
|
24
|
+
from .types import (
|
|
25
|
+
HandlerContext,
|
|
26
|
+
IncomingMessage,
|
|
27
|
+
MessageHandler,
|
|
28
|
+
PhaseChangeHandler,
|
|
29
|
+
SessionInfo,
|
|
30
|
+
TerminalHandler,
|
|
31
|
+
TerminalResult,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_MODE_PROJECTIONS: dict[str, type[BaseProjection]] = {
|
|
35
|
+
MODE_DECISION: DecisionProjection,
|
|
36
|
+
MODE_PROPOSAL: ProposalProjection,
|
|
37
|
+
MODE_QUORUM: QuorumProjection,
|
|
38
|
+
MODE_TASK: TaskProjection,
|
|
39
|
+
MODE_HANDOFF: HandoffProjection,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ParticipantActions:
|
|
44
|
+
"""Thin wrapper providing action methods bound to a participant's session."""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
client: MacpClient,
|
|
49
|
+
session_id: str,
|
|
50
|
+
auth: AuthConfig | None,
|
|
51
|
+
*,
|
|
52
|
+
mode: str = "",
|
|
53
|
+
participant_id: str = "",
|
|
54
|
+
) -> None:
|
|
55
|
+
self._client = client
|
|
56
|
+
self._session_id = session_id
|
|
57
|
+
self._auth = auth
|
|
58
|
+
self._mode = mode
|
|
59
|
+
self._participant_id = participant_id
|
|
60
|
+
|
|
61
|
+
def send_envelope(self, envelope: Any) -> Any:
|
|
62
|
+
"""Send a pre-built envelope via the MACP client."""
|
|
63
|
+
return self._client.send(envelope, auth=self._auth)
|
|
64
|
+
|
|
65
|
+
def get_session(self) -> Any:
|
|
66
|
+
"""Query session metadata from the runtime."""
|
|
67
|
+
return self._client.get_session(self._session_id, auth=self._auth)
|
|
68
|
+
|
|
69
|
+
def cancel_session(self, reason: str = "") -> Any:
|
|
70
|
+
"""Cancel the session."""
|
|
71
|
+
return self._client.cancel_session(self._session_id, reason=reason, auth=self._auth)
|
|
72
|
+
|
|
73
|
+
def evaluate(
|
|
74
|
+
self,
|
|
75
|
+
proposal_id: str,
|
|
76
|
+
recommendation: str,
|
|
77
|
+
*,
|
|
78
|
+
confidence: float,
|
|
79
|
+
reason: str = "",
|
|
80
|
+
) -> Any:
|
|
81
|
+
"""Send an Evaluation envelope for a decision-mode session."""
|
|
82
|
+
from macp.modes.decision.v1 import decision_pb2
|
|
83
|
+
|
|
84
|
+
payload = decision_pb2.EvaluationPayload(
|
|
85
|
+
proposal_id=proposal_id,
|
|
86
|
+
recommendation=recommendation.upper(),
|
|
87
|
+
confidence=confidence,
|
|
88
|
+
reason=reason,
|
|
89
|
+
)
|
|
90
|
+
envelope = build_envelope(
|
|
91
|
+
mode=self._mode,
|
|
92
|
+
message_type="Evaluation",
|
|
93
|
+
session_id=self._session_id,
|
|
94
|
+
sender=self._participant_id,
|
|
95
|
+
payload=serialize_message(payload),
|
|
96
|
+
)
|
|
97
|
+
return self.send_envelope(envelope)
|
|
98
|
+
|
|
99
|
+
def vote(
|
|
100
|
+
self,
|
|
101
|
+
proposal_id: str,
|
|
102
|
+
vote: str,
|
|
103
|
+
*,
|
|
104
|
+
reason: str = "",
|
|
105
|
+
) -> Any:
|
|
106
|
+
"""Send a Vote envelope for a decision-mode session."""
|
|
107
|
+
from macp.modes.decision.v1 import decision_pb2
|
|
108
|
+
|
|
109
|
+
payload = decision_pb2.VotePayload(
|
|
110
|
+
proposal_id=proposal_id,
|
|
111
|
+
vote=vote.upper(),
|
|
112
|
+
reason=reason,
|
|
113
|
+
)
|
|
114
|
+
envelope = build_envelope(
|
|
115
|
+
mode=self._mode,
|
|
116
|
+
message_type="Vote",
|
|
117
|
+
session_id=self._session_id,
|
|
118
|
+
sender=self._participant_id,
|
|
119
|
+
payload=serialize_message(payload),
|
|
120
|
+
)
|
|
121
|
+
return self.send_envelope(envelope)
|
|
122
|
+
|
|
123
|
+
def raise_objection(
|
|
124
|
+
self,
|
|
125
|
+
proposal_id: str,
|
|
126
|
+
*,
|
|
127
|
+
reason: str,
|
|
128
|
+
severity: str = "medium",
|
|
129
|
+
) -> Any:
|
|
130
|
+
"""Send an Objection envelope for a decision-mode session."""
|
|
131
|
+
from macp.modes.decision.v1 import decision_pb2
|
|
132
|
+
|
|
133
|
+
payload = decision_pb2.ObjectionPayload(
|
|
134
|
+
proposal_id=proposal_id,
|
|
135
|
+
reason=reason,
|
|
136
|
+
severity=severity.lower(),
|
|
137
|
+
)
|
|
138
|
+
envelope = build_envelope(
|
|
139
|
+
mode=self._mode,
|
|
140
|
+
message_type="Objection",
|
|
141
|
+
session_id=self._session_id,
|
|
142
|
+
sender=self._participant_id,
|
|
143
|
+
payload=serialize_message(payload),
|
|
144
|
+
)
|
|
145
|
+
return self.send_envelope(envelope)
|
|
146
|
+
|
|
147
|
+
def propose(
|
|
148
|
+
self,
|
|
149
|
+
proposal_id: str,
|
|
150
|
+
option: str,
|
|
151
|
+
*,
|
|
152
|
+
rationale: str = "",
|
|
153
|
+
supporting_data: bytes = b"",
|
|
154
|
+
) -> Any:
|
|
155
|
+
"""Send a Proposal envelope for a decision-mode session."""
|
|
156
|
+
from macp.modes.decision.v1 import decision_pb2
|
|
157
|
+
|
|
158
|
+
payload = decision_pb2.ProposalPayload(
|
|
159
|
+
proposal_id=proposal_id,
|
|
160
|
+
option=option,
|
|
161
|
+
rationale=rationale,
|
|
162
|
+
supporting_data=supporting_data,
|
|
163
|
+
)
|
|
164
|
+
envelope = build_envelope(
|
|
165
|
+
mode=self._mode,
|
|
166
|
+
message_type="Proposal",
|
|
167
|
+
session_id=self._session_id,
|
|
168
|
+
sender=self._participant_id,
|
|
169
|
+
payload=serialize_message(payload),
|
|
170
|
+
)
|
|
171
|
+
return self.send_envelope(envelope)
|
|
172
|
+
|
|
173
|
+
def commit(
|
|
174
|
+
self,
|
|
175
|
+
action: str,
|
|
176
|
+
authority_scope: str,
|
|
177
|
+
*,
|
|
178
|
+
reason: str = "",
|
|
179
|
+
commitment_id: str | None = None,
|
|
180
|
+
outcome_positive: bool = True,
|
|
181
|
+
) -> Any:
|
|
182
|
+
"""Send a Commitment envelope for the session."""
|
|
183
|
+
commitment_payload = build_commitment_payload(
|
|
184
|
+
action=action,
|
|
185
|
+
authority_scope=authority_scope,
|
|
186
|
+
reason=reason,
|
|
187
|
+
commitment_id=commitment_id,
|
|
188
|
+
outcome_positive=outcome_positive,
|
|
189
|
+
)
|
|
190
|
+
envelope = build_envelope(
|
|
191
|
+
mode=self._mode,
|
|
192
|
+
message_type="Commitment",
|
|
193
|
+
session_id=self._session_id,
|
|
194
|
+
sender=self._participant_id,
|
|
195
|
+
payload=serialize_message(commitment_payload),
|
|
196
|
+
)
|
|
197
|
+
return self.send_envelope(envelope)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Participant:
|
|
201
|
+
"""High-level agent abstraction for participating in MACP sessions.
|
|
202
|
+
|
|
203
|
+
Wraps a :class:`MacpClient`, a :class:`Dispatcher`, and a mode-specific
|
|
204
|
+
projection. Handlers are registered via ``on()``, ``on_phase_change()``,
|
|
205
|
+
and ``on_terminal()`` with a fluent API.
|
|
206
|
+
|
|
207
|
+
The ``run()`` method enters a blocking event loop that polls the
|
|
208
|
+
control-plane for session events and dispatches them to handlers.
|
|
209
|
+
Call ``stop()`` to signal the loop to exit.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
def __init__(
|
|
213
|
+
self,
|
|
214
|
+
*,
|
|
215
|
+
participant_id: str,
|
|
216
|
+
session_id: str,
|
|
217
|
+
mode: str,
|
|
218
|
+
client: MacpClient,
|
|
219
|
+
auth: AuthConfig | None = None,
|
|
220
|
+
participants: list[str] | None = None,
|
|
221
|
+
mode_version: str | None = None,
|
|
222
|
+
configuration_version: str | None = None,
|
|
223
|
+
policy_version: str | None = None,
|
|
224
|
+
transport: TransportAdapter | None = None,
|
|
225
|
+
) -> None:
|
|
226
|
+
self._participant_id = participant_id
|
|
227
|
+
self._session_id = session_id
|
|
228
|
+
self._mode = mode
|
|
229
|
+
self._client = client
|
|
230
|
+
self._auth = auth
|
|
231
|
+
self._stopped = False
|
|
232
|
+
|
|
233
|
+
self._dispatcher = Dispatcher()
|
|
234
|
+
self._session = SessionInfo(
|
|
235
|
+
session_id=session_id,
|
|
236
|
+
mode=mode,
|
|
237
|
+
participants=list(participants or []),
|
|
238
|
+
mode_version=mode_version,
|
|
239
|
+
configuration_version=configuration_version,
|
|
240
|
+
policy_version=policy_version,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
projection_cls = _MODE_PROJECTIONS.get(mode)
|
|
244
|
+
if projection_cls is not None:
|
|
245
|
+
self._projection: BaseProjection | None = projection_cls()
|
|
246
|
+
else:
|
|
247
|
+
self._projection = None
|
|
248
|
+
|
|
249
|
+
self._actions = ParticipantActions(
|
|
250
|
+
client,
|
|
251
|
+
session_id,
|
|
252
|
+
auth,
|
|
253
|
+
mode=mode,
|
|
254
|
+
participant_id=participant_id,
|
|
255
|
+
)
|
|
256
|
+
self._last_phase: str | None = None
|
|
257
|
+
self._transport = transport
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def participant_id(self) -> str:
|
|
261
|
+
return self._participant_id
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def session_id(self) -> str:
|
|
265
|
+
return self._session_id
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def mode(self) -> str:
|
|
269
|
+
return self._mode
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def projection(self) -> BaseProjection | None:
|
|
273
|
+
return self._projection
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def actions(self) -> ParticipantActions:
|
|
277
|
+
return self._actions
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def session(self) -> SessionInfo:
|
|
281
|
+
return self._session
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def is_stopped(self) -> bool:
|
|
285
|
+
return self._stopped
|
|
286
|
+
|
|
287
|
+
def on(self, message_type: str, handler: MessageHandler) -> Participant:
|
|
288
|
+
"""Register a handler for a message type (fluent API)."""
|
|
289
|
+
self._dispatcher.on(message_type, handler)
|
|
290
|
+
return self
|
|
291
|
+
|
|
292
|
+
def on_phase_change(self, phase: str, handler: PhaseChangeHandler) -> Participant:
|
|
293
|
+
"""Register a handler for a phase change (fluent API)."""
|
|
294
|
+
self._dispatcher.on_phase_change(phase, handler)
|
|
295
|
+
return self
|
|
296
|
+
|
|
297
|
+
def on_terminal(self, handler: TerminalHandler) -> Participant:
|
|
298
|
+
"""Register the terminal handler (fluent API)."""
|
|
299
|
+
self._dispatcher.on_terminal(handler)
|
|
300
|
+
return self
|
|
301
|
+
|
|
302
|
+
def _build_context(self) -> HandlerContext:
|
|
303
|
+
return HandlerContext(
|
|
304
|
+
participant=self._participant_id,
|
|
305
|
+
projection=self._projection,
|
|
306
|
+
actions=self._actions,
|
|
307
|
+
session=self._session,
|
|
308
|
+
log_fn=logger.info,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def _process_envelope(self, envelope: Any) -> None:
|
|
312
|
+
"""Process a single envelope: update projection, dispatch handlers."""
|
|
313
|
+
# Update the projection if available
|
|
314
|
+
if self._projection is not None:
|
|
315
|
+
self._projection.apply_envelope(envelope)
|
|
316
|
+
|
|
317
|
+
# Check for terminal messages
|
|
318
|
+
if envelope.message_type == "Commitment":
|
|
319
|
+
result = TerminalResult(
|
|
320
|
+
state="Committed",
|
|
321
|
+
commitment=envelope,
|
|
322
|
+
)
|
|
323
|
+
self._dispatcher.dispatch_terminal(result)
|
|
324
|
+
self._stopped = True
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
if envelope.message_type == "SessionCancel":
|
|
328
|
+
result = TerminalResult(state="Cancelled")
|
|
329
|
+
self._dispatcher.dispatch_terminal(result)
|
|
330
|
+
self._stopped = True
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
# Build the message and context
|
|
334
|
+
message = _envelope_to_message(envelope)
|
|
335
|
+
ctx = self._build_context()
|
|
336
|
+
|
|
337
|
+
# Dispatch message handler
|
|
338
|
+
self._dispatcher.dispatch(message, ctx)
|
|
339
|
+
|
|
340
|
+
# Check for phase changes
|
|
341
|
+
if self._projection is not None:
|
|
342
|
+
current_phase = self._projection.phase
|
|
343
|
+
if current_phase and current_phase != self._last_phase:
|
|
344
|
+
self._dispatcher.dispatch_phase_change(current_phase, ctx)
|
|
345
|
+
self._last_phase = current_phase
|
|
346
|
+
|
|
347
|
+
def _process_message(self, message: IncomingMessage) -> None:
|
|
348
|
+
"""Process a pre-built IncomingMessage (from HTTP transport)."""
|
|
349
|
+
ctx = self._build_context()
|
|
350
|
+
|
|
351
|
+
if message.message_type == "Commitment":
|
|
352
|
+
result = TerminalResult(state="Committed")
|
|
353
|
+
self._dispatcher.dispatch_terminal(result)
|
|
354
|
+
self._stopped = True
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
if message.message_type == "SessionCancel":
|
|
358
|
+
result = TerminalResult(state="Cancelled")
|
|
359
|
+
self._dispatcher.dispatch_terminal(result)
|
|
360
|
+
self._stopped = True
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
self._dispatcher.dispatch(message, ctx)
|
|
364
|
+
|
|
365
|
+
def run(self) -> None:
|
|
366
|
+
"""Enter the blocking event loop.
|
|
367
|
+
|
|
368
|
+
If a :class:`TransportAdapter` was provided, it is used to receive
|
|
369
|
+
messages. Otherwise a gRPC ``StreamSession`` is opened.
|
|
370
|
+
|
|
371
|
+
Dispatches received events to registered handlers until the session
|
|
372
|
+
reaches a terminal state or ``stop()`` is called.
|
|
373
|
+
"""
|
|
374
|
+
logger.info(
|
|
375
|
+
"participant %s joining session %s (mode=%s)",
|
|
376
|
+
self._participant_id,
|
|
377
|
+
self._session_id,
|
|
378
|
+
self._mode,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
transport = self._transport or GrpcTransportAdapter(
|
|
382
|
+
self._client,
|
|
383
|
+
self._session_id,
|
|
384
|
+
auth=self._auth,
|
|
385
|
+
)
|
|
386
|
+
try:
|
|
387
|
+
for message in transport.start():
|
|
388
|
+
if self._stopped:
|
|
389
|
+
break
|
|
390
|
+
# If the transport yields raw envelopes (gRPC), process as envelope
|
|
391
|
+
if message.raw is not None:
|
|
392
|
+
self._process_envelope(message.raw)
|
|
393
|
+
else:
|
|
394
|
+
self._process_message(message)
|
|
395
|
+
finally:
|
|
396
|
+
transport.stop()
|
|
397
|
+
|
|
398
|
+
def process_event(self, envelope: Any) -> None:
|
|
399
|
+
"""Manually process a single envelope (for testing or polling transports)."""
|
|
400
|
+
self._process_envelope(envelope)
|
|
401
|
+
|
|
402
|
+
def stop(self) -> None:
|
|
403
|
+
"""Signal the event loop to stop."""
|
|
404
|
+
self._stopped = True
|
macp_sdk/agent/runner.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from ..auth import AuthConfig
|
|
7
|
+
from ..client import MacpClient
|
|
8
|
+
from ..constants import DEFAULT_POLICY_VERSION
|
|
9
|
+
from .participant import Participant
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def from_bootstrap(bootstrap_path: str | None = None) -> Participant:
|
|
13
|
+
"""Create a Participant from a bootstrap context file.
|
|
14
|
+
|
|
15
|
+
The bootstrap file is a JSON document produced by the MACP hosting
|
|
16
|
+
infrastructure containing the information needed to connect and
|
|
17
|
+
participate in a session.
|
|
18
|
+
|
|
19
|
+
If ``bootstrap_path`` is not provided, the ``MACP_BOOTSTRAP_FILE``
|
|
20
|
+
environment variable is used.
|
|
21
|
+
|
|
22
|
+
Expected bootstrap JSON structure::
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
"participant_id": "...",
|
|
26
|
+
"session_id": "...",
|
|
27
|
+
"mode": "macp.mode.decision.v1",
|
|
28
|
+
"runtime_url": "localhost:50051",
|
|
29
|
+
"auth": {
|
|
30
|
+
"bearer_token": "...", // or "agent_id": "..."
|
|
31
|
+
},
|
|
32
|
+
"participants": ["agent-a", "agent-b"],
|
|
33
|
+
"policy_version": "policy.default",
|
|
34
|
+
"secure": true, // default; set false only for local dev
|
|
35
|
+
"allow_insecure": false // required if secure=false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Transport security follows RFC-MACP-0006 §3: ``secure`` defaults to
|
|
39
|
+
``true``. Setting ``secure: false`` additionally requires
|
|
40
|
+
``allow_insecure: true`` in the bootstrap (opt-in for local dev only).
|
|
41
|
+
"""
|
|
42
|
+
path = bootstrap_path or os.environ.get("MACP_BOOTSTRAP_FILE")
|
|
43
|
+
if not path:
|
|
44
|
+
raise ValueError("No bootstrap path provided and MACP_BOOTSTRAP_FILE not set")
|
|
45
|
+
|
|
46
|
+
with open(path) as f:
|
|
47
|
+
ctx: dict[str, object] = json.load(f)
|
|
48
|
+
|
|
49
|
+
participant_id = str(ctx["participant_id"])
|
|
50
|
+
session_id = str(ctx["session_id"])
|
|
51
|
+
mode = str(ctx["mode"])
|
|
52
|
+
runtime_url = str(ctx.get("runtime_url", "localhost:50051"))
|
|
53
|
+
secure = bool(ctx.get("secure", True))
|
|
54
|
+
allow_insecure = bool(ctx.get("allow_insecure", False))
|
|
55
|
+
|
|
56
|
+
# Build auth config
|
|
57
|
+
auth_data = ctx.get("auth")
|
|
58
|
+
auth: AuthConfig | None = None
|
|
59
|
+
if isinstance(auth_data, dict):
|
|
60
|
+
bearer = auth_data.get("bearer_token")
|
|
61
|
+
agent_id = auth_data.get("agent_id")
|
|
62
|
+
expected_sender = auth_data.get("expected_sender") or participant_id
|
|
63
|
+
if bearer:
|
|
64
|
+
auth = AuthConfig.for_bearer(
|
|
65
|
+
str(bearer),
|
|
66
|
+
sender_hint=participant_id,
|
|
67
|
+
expected_sender=str(expected_sender),
|
|
68
|
+
)
|
|
69
|
+
elif agent_id:
|
|
70
|
+
auth = AuthConfig.for_dev_agent(str(agent_id), expected_sender=str(expected_sender))
|
|
71
|
+
|
|
72
|
+
# Build client
|
|
73
|
+
client = MacpClient(
|
|
74
|
+
target=runtime_url,
|
|
75
|
+
secure=secure,
|
|
76
|
+
allow_insecure=allow_insecure,
|
|
77
|
+
auth=auth,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Extract optional fields
|
|
81
|
+
raw_participants = ctx.get("participants")
|
|
82
|
+
participants: list[str] = []
|
|
83
|
+
if isinstance(raw_participants, list):
|
|
84
|
+
participants = [str(p) for p in raw_participants]
|
|
85
|
+
|
|
86
|
+
mode_version = ctx.get("mode_version")
|
|
87
|
+
configuration_version = ctx.get("configuration_version")
|
|
88
|
+
policy_version = ctx.get("policy_version")
|
|
89
|
+
|
|
90
|
+
return Participant(
|
|
91
|
+
participant_id=participant_id,
|
|
92
|
+
session_id=session_id,
|
|
93
|
+
mode=mode,
|
|
94
|
+
client=client,
|
|
95
|
+
auth=auth,
|
|
96
|
+
participants=participants,
|
|
97
|
+
mode_version=str(mode_version) if mode_version else None,
|
|
98
|
+
configuration_version=str(configuration_version) if configuration_version else None,
|
|
99
|
+
policy_version=str(policy_version) if policy_version else DEFAULT_POLICY_VERSION,
|
|
100
|
+
)
|