agent-did-microsoft-agent-framework 1.0.0rc1__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.
@@ -0,0 +1,63 @@
1
+ """Public package surface for the Agent-DID Microsoft Agent Framework integration."""
2
+
3
+ from .config import AgentDidExposureConfig, AgentDidMicrosoftAgentFrameworkConfig
4
+ from .context import build_agent_did_instructions, compose_instructions
5
+ from .handoff import (
6
+ HANDOFF_TTL_DEFAULTS,
7
+ ActionClass,
8
+ SignedHandoffMessage,
9
+ VerificationBlockedCallback,
10
+ VerificationBlockedError,
11
+ build_handoff_verifier_executor,
12
+ build_handoff_verifier_function,
13
+ )
14
+ from .integration import (
15
+ AgentDidMicrosoftAgentFrameworkIntegration,
16
+ AgentDidWorkflowBuilder,
17
+ create_agent_did_microsoft_agent_framework_integration,
18
+ )
19
+ from .observability import (
20
+ AgentDidEventHandler,
21
+ AgentDidMicrosoftAgentFrameworkObservabilityEvent,
22
+ compose_event_handlers,
23
+ create_json_logger_event_handler,
24
+ create_opentelemetry_event_handler,
25
+ create_opentelemetry_tracer,
26
+ serialize_observability_event,
27
+ )
28
+ from .sanitization import sanitize_observability_attributes
29
+ from .snapshot import AgentDidIdentitySnapshot, build_agent_did_identity_snapshot
30
+ from .tools import create_agent_framework_tools
31
+
32
+ PACKAGE_STATUS = "functional"
33
+ createAgentDidMicrosoftAgentFrameworkIntegration = create_agent_did_microsoft_agent_framework_integration
34
+
35
+ __all__ = [
36
+ "PACKAGE_STATUS",
37
+ "ActionClass",
38
+ "AgentDidEventHandler",
39
+ "AgentDidExposureConfig",
40
+ "AgentDidIdentitySnapshot",
41
+ "AgentDidMicrosoftAgentFrameworkConfig",
42
+ "AgentDidMicrosoftAgentFrameworkIntegration",
43
+ "AgentDidMicrosoftAgentFrameworkObservabilityEvent",
44
+ "AgentDidWorkflowBuilder",
45
+ "HANDOFF_TTL_DEFAULTS",
46
+ "SignedHandoffMessage",
47
+ "VerificationBlockedCallback",
48
+ "VerificationBlockedError",
49
+ "build_agent_did_identity_snapshot",
50
+ "build_agent_did_instructions",
51
+ "build_handoff_verifier_executor",
52
+ "build_handoff_verifier_function",
53
+ "compose_event_handlers",
54
+ "compose_instructions",
55
+ "createAgentDidMicrosoftAgentFrameworkIntegration",
56
+ "create_agent_did_microsoft_agent_framework_integration",
57
+ "create_agent_framework_tools",
58
+ "create_json_logger_event_handler",
59
+ "create_opentelemetry_event_handler",
60
+ "create_opentelemetry_tracer",
61
+ "sanitize_observability_attributes",
62
+ "serialize_observability_event",
63
+ ]
@@ -0,0 +1,29 @@
1
+ """Public configuration models for the Agent-DID Microsoft Agent Framework integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+
10
+ class AgentDidExposureConfig(BaseModel):
11
+ model_config = ConfigDict(extra="forbid")
12
+
13
+ current_identity: bool = True
14
+ resolve_did: bool = True
15
+ verify_signatures: bool = True
16
+ sign_http: bool = False
17
+ sign_payload: bool = False
18
+ rotate_keys: bool = False
19
+ document_history: bool = False
20
+
21
+
22
+ class AgentDidMicrosoftAgentFrameworkConfig(BaseModel):
23
+ model_config = ConfigDict(extra="forbid")
24
+
25
+ expose: AgentDidExposureConfig = Field(default_factory=AgentDidExposureConfig)
26
+ tool_prefix: str = "agent_did"
27
+ additional_instructions: str | None = None
28
+ allow_private_network_targets: bool = False
29
+ tool_approval_mode: Literal["always_require", "never_require"] = "never_require"
@@ -0,0 +1,42 @@
1
+ """Instruction composition utilities for Agent-DID identity context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .snapshot import AgentDidIdentitySnapshot
6
+
7
+
8
+ def build_agent_did_instructions(
9
+ identity_snapshot: AgentDidIdentitySnapshot,
10
+ additional_instructions: str | None = None,
11
+ ) -> str:
12
+ capabilities = ", ".join(identity_snapshot.capabilities) if identity_snapshot.capabilities else "none"
13
+ lines = [
14
+ "Agent-DID identity context:",
15
+ f"- did: {identity_snapshot.did}",
16
+ f"- controller: {identity_snapshot.controller}",
17
+ f"- name: {identity_snapshot.name}",
18
+ f"- version: {identity_snapshot.version}",
19
+ f"- capabilities: {capabilities}",
20
+ f"- member_of: {identity_snapshot.member_of or 'none'}",
21
+ f"- authentication_key_id: {identity_snapshot.authentication_key_id or 'unknown'}",
22
+ "Rules:",
23
+ "- Treat this DID as the authoritative identity of this agent.",
24
+ "- Never invent or substitute another DID for this agent.",
25
+ "- Use the dedicated Agent-DID tools for DID resolution, verification and signing tasks.",
26
+ ]
27
+
28
+ if additional_instructions and additional_instructions.strip():
29
+ lines.append(f"Additional identity policy: {additional_instructions.strip()}")
30
+
31
+ return "\n".join(lines)
32
+
33
+
34
+ def compose_instructions(
35
+ base_instructions: str | None,
36
+ identity_snapshot: AgentDidIdentitySnapshot,
37
+ additional_instructions: str | None = None,
38
+ ) -> str:
39
+ identity_section = build_agent_did_instructions(identity_snapshot, additional_instructions)
40
+ if base_instructions and base_instructions.strip():
41
+ return f"{base_instructions.strip()}\n\n{identity_section}"
42
+ return identity_section
@@ -0,0 +1,421 @@
1
+ """Verified-handoff helper for Microsoft Agent Framework workflows.
2
+
3
+ Implements the `add_verified_handoff(...)` design locked with @haroldmalikfrimpong-ops
4
+ in https://github.com/edisonduran/agent-did/issues/26 and the originating thread
5
+ `microsoft/agent-framework#4842`.
6
+
7
+ Design contract (locked):
8
+ - Hard gate, fail-closed at the handoff boundary.
9
+ - `action_class` first-class parameter drives TTL semantics.
10
+ - Default behavior on a blocked handoff is to raise ``VerificationBlockedError``;
11
+ callers may opt in via ``on_verification_blocked`` callback to either halt the
12
+ workflow gracefully (return ``None``) or route to a custom executor result
13
+ (return any value, which becomes the verifier executor's output).
14
+ - Cross-domain hops always force a fresh verification (TTL collapses to 0).
15
+ - ``action_class="irreversible"`` always forces a fresh verification regardless of
16
+ any larger TTL the caller may try to override.
17
+ - Key-rotation race: signatures issued under a previous key remain accepted while
18
+ inside the action-class TTL window via the SDK's historical signature path.
19
+ Once the TTL elapses, the next handoff MUST re-verify against the agent's
20
+ current key set.
21
+
22
+ This module deliberately delegates ALL key-lifecycle logic to the central SDK
23
+ primitives (``AgentIdentity.verify_signature`` and
24
+ ``AgentIdentity.verify_historical_signature``). It must not re-implement the
25
+ key rotation state machine — when the spec converges with `aeoess/agent-passport-system`
26
+ the SDK is the single source of truth and this helper inherits any change for free.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import time
32
+ from collections.abc import Callable, Sequence
33
+ from dataclasses import dataclass, field
34
+ from typing import Any, Literal
35
+
36
+ from agent_did_sdk import AgentIdentity, IdentityCompositionError
37
+ from agent_framework import FunctionExecutor, WorkflowContext
38
+
39
+ from .observability import AgentDidObserver
40
+
41
+ ActionClass = Literal["irreversible", "compensable", "reversible"]
42
+
43
+ HANDOFF_TTL_DEFAULTS: dict[ActionClass, int] = {
44
+ "irreversible": 30,
45
+ "compensable": 120,
46
+ "reversible": 300,
47
+ }
48
+
49
+ VerificationBlockedCallback = Callable[["VerificationBlockedError"], Any]
50
+
51
+
52
+ @dataclass(slots=True)
53
+ class SignedHandoffMessage:
54
+ """Wrapper carrying the signature metadata required at a handoff boundary.
55
+
56
+ Upstream executors MUST emit a ``SignedHandoffMessage`` when handing off across
57
+ a verified edge. The verifier extracts ``payload`` after a successful check and
58
+ forwards it to the downstream executor unchanged.
59
+ """
60
+
61
+ payload: Any
62
+ did: str
63
+ signature: str
64
+ signed_at: float
65
+ key_id: str | None = None
66
+ trust_domain: str | None = None
67
+
68
+
69
+ class VerificationBlockedError(Exception):
70
+ """Raised by ``add_verified_handoff`` when a handoff fails verification.
71
+
72
+ Attributes match the shape locked in
73
+ https://github.com/microsoft/agent-framework/issues/4842#issuecomment-4327716940.
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ *,
79
+ result: dict[str, Any],
80
+ failing_gates: Sequence[str],
81
+ enforcement_note: str,
82
+ checked_did: str | None,
83
+ action_class: ActionClass,
84
+ ) -> None:
85
+ super().__init__(enforcement_note)
86
+ self.result = result
87
+ self.failing_gates = list(failing_gates)
88
+ self.enforcement_note = enforcement_note
89
+ self.checked_did = checked_did
90
+ self.action_class = action_class
91
+
92
+
93
+ @dataclass(slots=True)
94
+ class _VerifierContext:
95
+ from_executor_id: str
96
+ to_executor_id: str
97
+ action_class: ActionClass
98
+ ttl_seconds: int
99
+ allowed_dids: tuple[str, ...] | None
100
+ require_signature: bool
101
+ cross_domain: bool
102
+ on_verification_blocked: VerificationBlockedCallback | None
103
+ observer: AgentDidObserver
104
+ verify_signature: Callable[..., Any] = field(default=AgentIdentity.verify_signature)
105
+ verify_historical_signature: Callable[..., Any] = field(
106
+ default=AgentIdentity.verify_historical_signature
107
+ )
108
+ now: Callable[[], float] = field(default=time.time)
109
+
110
+
111
+ def _resolve_executor_id(executor: Any) -> str:
112
+ return getattr(executor, "id", None) or getattr(executor, "name", None) or type(executor).__name__
113
+
114
+
115
+ def _resolve_effective_ttl(
116
+ action_class: ActionClass,
117
+ ttl_seconds: int | None,
118
+ cross_domain: bool,
119
+ ) -> int:
120
+ if cross_domain:
121
+ return 0
122
+ if action_class == "irreversible":
123
+ # Irreversible always uses the (small) action-class default; callers cannot widen it.
124
+ return HANDOFF_TTL_DEFAULTS["irreversible"]
125
+ if ttl_seconds is not None:
126
+ return max(0, int(ttl_seconds))
127
+ return HANDOFF_TTL_DEFAULTS[action_class]
128
+
129
+
130
+ def _build_block_error(
131
+ *,
132
+ failing_gates: Sequence[str],
133
+ checked_did: str | None,
134
+ action_class: ActionClass,
135
+ enforcement_note: str,
136
+ ) -> VerificationBlockedError:
137
+ return VerificationBlockedError(
138
+ result={
139
+ "valid": False,
140
+ "did": checked_did,
141
+ "failing_gates": list(failing_gates),
142
+ "action_class": action_class,
143
+ },
144
+ failing_gates=failing_gates,
145
+ enforcement_note=enforcement_note,
146
+ checked_did=checked_did,
147
+ action_class=action_class,
148
+ )
149
+
150
+
151
+ def _emit_blocked(observer: AgentDidObserver, ctx: _VerifierContext, error: VerificationBlockedError) -> None:
152
+ observer.emit(
153
+ "agent_did.workflow.handoff_blocked",
154
+ attributes={
155
+ "from_executor": ctx.from_executor_id,
156
+ "to_executor": ctx.to_executor_id,
157
+ "from_did": error.checked_did,
158
+ "action_class": ctx.action_class,
159
+ "ttl_seconds": ctx.ttl_seconds,
160
+ "failing_gates": error.failing_gates,
161
+ "decision_reason": error.enforcement_note,
162
+ },
163
+ level="error",
164
+ )
165
+
166
+
167
+ def _emit_verified(
168
+ observer: AgentDidObserver,
169
+ ctx: _VerifierContext,
170
+ *,
171
+ from_did: str,
172
+ used_historical: bool,
173
+ staleness_seconds: float,
174
+ ) -> None:
175
+ observer.emit(
176
+ "agent_did.workflow.handoff_verified",
177
+ attributes={
178
+ "from_executor": ctx.from_executor_id,
179
+ "to_executor": ctx.to_executor_id,
180
+ "from_did": from_did,
181
+ "to_did": None,
182
+ "action_class": ctx.action_class,
183
+ "ttl_seconds": ctx.ttl_seconds,
184
+ "decision_reason": "ok",
185
+ "used_historical_signature": used_historical,
186
+ "staleness_seconds": staleness_seconds,
187
+ },
188
+ )
189
+
190
+
191
+ def _handle_block(ctx: _VerifierContext, error: VerificationBlockedError) -> Any:
192
+ """Legacy synchronous block handler.
193
+
194
+ Retained for callers that exercise the verifier directly without a workflow
195
+ context (returns the callback value or raises). The async path used inside
196
+ ``build_handoff_verifier_function`` calls ``_emit_block_outcome`` instead.
197
+ """
198
+
199
+ _emit_blocked(ctx.observer, ctx, error)
200
+ if ctx.on_verification_blocked is None:
201
+ raise error
202
+ return ctx.on_verification_blocked(error)
203
+
204
+
205
+ def build_handoff_verifier_function(ctx: _VerifierContext) -> Callable[..., Any]:
206
+ """Build the async function that backs the verifier ``FunctionExecutor``.
207
+
208
+ The returned coroutine accepts ``(message, wf_ctx)`` where ``wf_ctx`` is the
209
+ Microsoft Agent Framework ``WorkflowContext``. On success the inner payload
210
+ is forwarded via ``await wf_ctx.send_message(payload)``. On failure the
211
+ behaviour follows the locked design contract:
212
+
213
+ - If no ``on_verification_blocked`` callback is registered, raise
214
+ ``VerificationBlockedError``.
215
+ - If a callback is registered and returns ``None``, halt gracefully (no
216
+ message is sent downstream).
217
+ - If a callback is registered and returns any other value, forward that value
218
+ via ``await wf_ctx.send_message(value)``.
219
+ """
220
+
221
+ async def _verifier(message: Any, wf_ctx: WorkflowContext[Any]) -> None:
222
+ if not isinstance(message, SignedHandoffMessage):
223
+ await _emit_unsigned_outcome(ctx, wf_ctx, message)
224
+ return
225
+
226
+ if ctx.allowed_dids is not None and message.did not in ctx.allowed_dids:
227
+ error = _build_block_error(
228
+ failing_gates=["did_allowlist"],
229
+ checked_did=message.did,
230
+ action_class=ctx.action_class,
231
+ enforcement_note=f"DID {message.did!r} not in allowed_dids",
232
+ )
233
+ await _emit_block_outcome(ctx, wf_ctx, error)
234
+ return
235
+
236
+ valid, used_historical, staleness, gate = await _run_signature_verification(ctx, message)
237
+ if not valid:
238
+ error = _build_block_error(
239
+ failing_gates=[gate],
240
+ checked_did=message.did,
241
+ action_class=ctx.action_class,
242
+ enforcement_note=_block_note_for_gate(gate, ctx.ttl_seconds, message.did),
243
+ )
244
+ await _emit_block_outcome(ctx, wf_ctx, error)
245
+ return
246
+
247
+ _emit_verified(
248
+ ctx.observer, ctx,
249
+ from_did=message.did, used_historical=used_historical, staleness_seconds=staleness,
250
+ )
251
+ await wf_ctx.send_message(message.payload)
252
+
253
+ return _verifier
254
+
255
+
256
+ async def _emit_unsigned_outcome(ctx: _VerifierContext, wf_ctx: Any, message: Any) -> None:
257
+ if not ctx.require_signature:
258
+ await wf_ctx.send_message(message)
259
+ return
260
+ error = _build_block_error(
261
+ failing_gates=["signature"],
262
+ checked_did=None,
263
+ action_class=ctx.action_class,
264
+ enforcement_note="Handoff message is not a SignedHandoffMessage and require_signature=True",
265
+ )
266
+ await _emit_block_outcome(ctx, wf_ctx, error)
267
+
268
+
269
+ async def _emit_block_outcome(ctx: _VerifierContext, wf_ctx: Any, error: VerificationBlockedError) -> None:
270
+ _emit_blocked(ctx.observer, ctx, error)
271
+ if ctx.on_verification_blocked is None:
272
+ raise error
273
+ callback_result = ctx.on_verification_blocked(error)
274
+ if callback_result is None:
275
+ return # halt gracefully
276
+ await wf_ctx.send_message(callback_result)
277
+
278
+
279
+ def _block_note_for_gate(gate: str, ttl_seconds: int, did: str) -> str:
280
+ if gate == "key_lifecycle":
281
+ return (
282
+ f"Signature past action-class TTL ({ttl_seconds}s) and the previous key "
283
+ "no longer verifies against the current key set"
284
+ )
285
+ return f"Signature did not verify against the current or historical key set for {did}"
286
+
287
+
288
+ async def _run_signature_verification(
289
+ ctx: _VerifierContext, message: SignedHandoffMessage
290
+ ) -> tuple[bool, bool, float, str]:
291
+ """Return ``(valid, used_historical, staleness_seconds, failing_gate)``.
292
+
293
+ ``failing_gate`` is meaningful only when ``valid`` is ``False``.
294
+ """
295
+
296
+ payload_str = message.payload if isinstance(message.payload, str) else str(message.payload)
297
+ staleness = max(0.0, ctx.now() - message.signed_at)
298
+
299
+ if staleness > ctx.ttl_seconds:
300
+ valid = await _verify_signature_with_purpose(ctx, message, payload_str)
301
+ return bool(valid), False, staleness, "key_lifecycle"
302
+
303
+ valid = await _verify_signature_with_purpose(ctx, message, payload_str)
304
+ used_historical = False
305
+ if not valid and message.key_id:
306
+ valid = await _verify_historical_signature_with_purpose(ctx, message, payload_str)
307
+ used_historical = bool(valid)
308
+ return bool(valid), used_historical, staleness, "signature"
309
+
310
+
311
+ async def _verify_signature_with_purpose(
312
+ ctx: _VerifierContext,
313
+ message: SignedHandoffMessage,
314
+ payload_str: str,
315
+ ) -> bool:
316
+ try:
317
+ valid = await ctx.verify_signature(
318
+ message.did,
319
+ payload_str,
320
+ message.signature,
321
+ message.key_id,
322
+ required_purpose="assertionMethod",
323
+ )
324
+ return bool(valid)
325
+ except IdentityCompositionError:
326
+ return False
327
+
328
+
329
+ async def _verify_historical_signature_with_purpose(
330
+ ctx: _VerifierContext,
331
+ message: SignedHandoffMessage,
332
+ payload_str: str,
333
+ ) -> bool:
334
+ try:
335
+ valid = await ctx.verify_historical_signature(
336
+ message.did,
337
+ payload_str,
338
+ message.signature,
339
+ message.key_id,
340
+ required_purpose="assertionMethod",
341
+ )
342
+ return bool(valid)
343
+ except IdentityCompositionError:
344
+ return False
345
+
346
+
347
+ def build_handoff_verifier_executor(
348
+ *,
349
+ from_executor: Any,
350
+ to_executor: Any,
351
+ action_class: ActionClass,
352
+ ttl_seconds: int | None,
353
+ allowed_dids: Sequence[str] | None,
354
+ require_signature: bool,
355
+ cross_domain: bool | None,
356
+ on_verification_blocked: VerificationBlockedCallback | None,
357
+ observer: AgentDidObserver,
358
+ executor_id: str | None = None,
359
+ output_type: Any = object,
360
+ ) -> FunctionExecutor:
361
+ """Build the ``FunctionExecutor`` that enforces verification at a handoff edge."""
362
+
363
+ from_id = _resolve_executor_id(from_executor)
364
+ to_id = _resolve_executor_id(to_executor)
365
+ from_domain = getattr(from_executor, "trust_domain", None)
366
+ to_domain = getattr(to_executor, "trust_domain", None)
367
+ if cross_domain is None:
368
+ cross_domain = bool(from_domain and to_domain and from_domain != to_domain)
369
+
370
+ effective_ttl = _resolve_effective_ttl(action_class, ttl_seconds, cross_domain)
371
+
372
+ ctx = _VerifierContext(
373
+ from_executor_id=from_id,
374
+ to_executor_id=to_id,
375
+ action_class=action_class,
376
+ ttl_seconds=effective_ttl,
377
+ allowed_dids=tuple(allowed_dids) if allowed_dids is not None else None,
378
+ require_signature=require_signature,
379
+ cross_domain=cross_domain,
380
+ on_verification_blocked=on_verification_blocked,
381
+ observer=observer,
382
+ )
383
+
384
+ verifier_func = build_handoff_verifier_function(ctx)
385
+ resolved_executor_id = executor_id or f"agent_did_verified_handoff__{from_id}__{to_id}"
386
+ # Declare explicit IO types so the workflow validator doesn't skip edge checks
387
+ # for this implicit executor we inserted on the user's behalf. Inputs are
388
+ # SignedHandoffMessage when require_signature=True; outputs are deliberately
389
+ # ``object`` to keep the inner payload type-agnostic (the downstream executor
390
+ # supplies the concrete annotation).
391
+ executor = FunctionExecutor(
392
+ verifier_func,
393
+ id=resolved_executor_id,
394
+ input=SignedHandoffMessage if require_signature else object,
395
+ output=output_type,
396
+ )
397
+ observer.emit(
398
+ "agent_did.workflow.verifier_created",
399
+ attributes={
400
+ "executor_id": resolved_executor_id,
401
+ "from_executor": from_id,
402
+ "to_executor": to_id,
403
+ "action_class": action_class,
404
+ "ttl_seconds": effective_ttl,
405
+ "cross_domain": cross_domain,
406
+ "allowed_did_count": len(ctx.allowed_dids) if ctx.allowed_dids else 0,
407
+ "require_signature": require_signature,
408
+ },
409
+ )
410
+ return executor
411
+
412
+
413
+ __all__ = [
414
+ "ActionClass",
415
+ "HANDOFF_TTL_DEFAULTS",
416
+ "SignedHandoffMessage",
417
+ "VerificationBlockedCallback",
418
+ "VerificationBlockedError",
419
+ "build_handoff_verifier_executor",
420
+ "build_handoff_verifier_function",
421
+ ]