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.
- agent_did_microsoft_agent_framework/__init__.py +63 -0
- agent_did_microsoft_agent_framework/config.py +29 -0
- agent_did_microsoft_agent_framework/context.py +42 -0
- agent_did_microsoft_agent_framework/handoff.py +421 -0
- agent_did_microsoft_agent_framework/integration.py +411 -0
- agent_did_microsoft_agent_framework/observability.py +171 -0
- agent_did_microsoft_agent_framework/sanitization.py +83 -0
- agent_did_microsoft_agent_framework/snapshot.py +50 -0
- agent_did_microsoft_agent_framework/tools.py +364 -0
- agent_did_microsoft_agent_framework-1.0.0rc1.dist-info/METADATA +298 -0
- agent_did_microsoft_agent_framework-1.0.0rc1.dist-info/RECORD +13 -0
- agent_did_microsoft_agent_framework-1.0.0rc1.dist-info/WHEEL +5 -0
- agent_did_microsoft_agent_framework-1.0.0rc1.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
]
|