agentrust-telemetry 0.1.0a3__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.
- agentrust_telemetry/__init__.py +80 -0
- agentrust_telemetry/adapters/__init__.py +28 -0
- agentrust_telemetry/adapters/agt.py +207 -0
- agentrust_telemetry/adapters/agt_approval.py +235 -0
- agentrust_telemetry/adapters/agt_audit.py +177 -0
- agentrust_telemetry/adapters/agt_data.py +105 -0
- agentrust_telemetry/adapters/base.py +90 -0
- agentrust_telemetry/adapters/cedar.py +66 -0
- agentrust_telemetry/adapters/opa.py +102 -0
- agentrust_telemetry/client.py +113 -0
- agentrust_telemetry/context.py +30 -0
- agentrust_telemetry/data_flow.py +112 -0
- agentrust_telemetry/errors.py +26 -0
- agentrust_telemetry/evidence.py +204 -0
- agentrust_telemetry/otel.py +135 -0
- agentrust_telemetry/projection.py +49 -0
- agentrust_telemetry/propagation.py +102 -0
- agentrust_telemetry/py.typed +1 -0
- agentrust_telemetry/schemas/action.schema.json +39 -0
- agentrust_telemetry/schemas/approval.schema.json +27 -0
- agentrust_telemetry/schemas/common.schema.json +36 -0
- agentrust_telemetry/schemas/data-flow.schema.json +46 -0
- agentrust_telemetry/schemas/envelope.schema.json +21 -0
- agentrust_telemetry/schemas/evidence.schema.json +20 -0
- agentrust_telemetry/schemas/policy-decision.schema.json +33 -0
- agentrust_telemetry/schemas/usage.schema.json +80 -0
- agentrust_telemetry/trace_adapter.py +263 -0
- agentrust_telemetry/usage.py +190 -0
- agentrust_telemetry/validation.py +102 -0
- agentrust_telemetry-0.1.0a3.dist-info/METADATA +180 -0
- agentrust_telemetry-0.1.0a3.dist-info/RECORD +35 -0
- agentrust_telemetry-0.1.0a3.dist-info/WHEEL +5 -0
- agentrust_telemetry-0.1.0a3.dist-info/licenses/LICENSE +21 -0
- agentrust_telemetry-0.1.0a3.dist-info/licenses/NOTICE +4 -0
- agentrust_telemetry-0.1.0a3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""AgentTrust governance telemetry reference SDK."""
|
|
2
|
+
|
|
3
|
+
from .adapters import (
|
|
4
|
+
AgtGovernanceEventSink,
|
|
5
|
+
EventFactory,
|
|
6
|
+
agt_approval_request,
|
|
7
|
+
agt_approval_resolution,
|
|
8
|
+
agt_audit_action,
|
|
9
|
+
agt_audit_policy_decision,
|
|
10
|
+
agt_data_access_flow,
|
|
11
|
+
agt_data_classification,
|
|
12
|
+
agt_policy_decision,
|
|
13
|
+
agt_policy_decision_record,
|
|
14
|
+
cedar_policy_decision,
|
|
15
|
+
opa_decision_log,
|
|
16
|
+
)
|
|
17
|
+
from .client import EmitResult, TelemetryClient
|
|
18
|
+
from .data_flow import ClassificationResult, DataClassifier, DataEndpoint, classified_data_flow
|
|
19
|
+
from .context import ContextIds, active_context_ids
|
|
20
|
+
from .errors import (
|
|
21
|
+
ContextMismatchError,
|
|
22
|
+
EventValidationError,
|
|
23
|
+
EvidenceError,
|
|
24
|
+
EvidencePersistenceError,
|
|
25
|
+
ProjectionError,
|
|
26
|
+
PropagationError,
|
|
27
|
+
TraceFinalizationError,
|
|
28
|
+
)
|
|
29
|
+
from .evidence import EvidenceAccumulator, EvidenceEntry, EvidenceSnapshot
|
|
30
|
+
from .propagation import ExtractedContext, extract_context, inject_context
|
|
31
|
+
from .otel import OTelLogEmitter, OTelMetricEmitter
|
|
32
|
+
from .trace_adapter import TraceConfiguration, finalize_trace
|
|
33
|
+
from .usage import CostObservation, UsageAccumulator, usage_record
|
|
34
|
+
from .validation import SchemaValidator
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"ContextIds",
|
|
38
|
+
"ContextMismatchError",
|
|
39
|
+
"CostObservation",
|
|
40
|
+
"ClassificationResult",
|
|
41
|
+
"DataClassifier",
|
|
42
|
+
"DataEndpoint",
|
|
43
|
+
"AgtGovernanceEventSink",
|
|
44
|
+
"EmitResult",
|
|
45
|
+
"EventValidationError",
|
|
46
|
+
"EventFactory",
|
|
47
|
+
"EvidenceAccumulator",
|
|
48
|
+
"EvidenceEntry",
|
|
49
|
+
"EvidenceError",
|
|
50
|
+
"EvidencePersistenceError",
|
|
51
|
+
"EvidenceSnapshot",
|
|
52
|
+
"ExtractedContext",
|
|
53
|
+
"ProjectionError",
|
|
54
|
+
"OTelLogEmitter",
|
|
55
|
+
"OTelMetricEmitter",
|
|
56
|
+
"PropagationError",
|
|
57
|
+
"SchemaValidator",
|
|
58
|
+
"TelemetryClient",
|
|
59
|
+
"TraceConfiguration",
|
|
60
|
+
"TraceFinalizationError",
|
|
61
|
+
"UsageAccumulator",
|
|
62
|
+
"active_context_ids",
|
|
63
|
+
"agt_approval_request",
|
|
64
|
+
"agt_approval_resolution",
|
|
65
|
+
"agt_audit_action",
|
|
66
|
+
"agt_audit_policy_decision",
|
|
67
|
+
"agt_data_access_flow",
|
|
68
|
+
"agt_data_classification",
|
|
69
|
+
"agt_policy_decision",
|
|
70
|
+
"agt_policy_decision_record",
|
|
71
|
+
"cedar_policy_decision",
|
|
72
|
+
"classified_data_flow",
|
|
73
|
+
"extract_context",
|
|
74
|
+
"inject_context",
|
|
75
|
+
"opa_decision_log",
|
|
76
|
+
"usage_record",
|
|
77
|
+
"finalize_trace",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
__version__ = "0.1.0a3"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Source adapters for normalized AgentTrust telemetry events."""
|
|
2
|
+
|
|
3
|
+
from .base import EventFactory
|
|
4
|
+
from .agt import AgtGovernanceEventSink, agt_policy_decision
|
|
5
|
+
from .agt_approval import (
|
|
6
|
+
agt_approval_request,
|
|
7
|
+
agt_approval_resolution,
|
|
8
|
+
agt_policy_decision_record,
|
|
9
|
+
)
|
|
10
|
+
from .agt_audit import agt_audit_action, agt_audit_policy_decision
|
|
11
|
+
from .agt_data import agt_data_access_flow, agt_data_classification
|
|
12
|
+
from .cedar import cedar_policy_decision
|
|
13
|
+
from .opa import opa_decision_log
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AgtGovernanceEventSink",
|
|
17
|
+
"EventFactory",
|
|
18
|
+
"agt_policy_decision",
|
|
19
|
+
"agt_policy_decision_record",
|
|
20
|
+
"agt_approval_request",
|
|
21
|
+
"agt_approval_resolution",
|
|
22
|
+
"agt_audit_action",
|
|
23
|
+
"agt_audit_policy_decision",
|
|
24
|
+
"agt_data_access_flow",
|
|
25
|
+
"agt_data_classification",
|
|
26
|
+
"cedar_policy_decision",
|
|
27
|
+
"opa_decision_log",
|
|
28
|
+
]
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Optional bridge from AGT governance events to AgentTrust telemetry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
import uuid
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Callable, Iterable, Protocol, Sequence
|
|
11
|
+
|
|
12
|
+
from .base import EventFactory
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_IDENTIFIER = re.compile(r"[A-Za-z0-9_.:-]{1,128}")
|
|
16
|
+
_TIMESTAMP = re.compile(
|
|
17
|
+
r"^(?P<date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(?P<fraction>\d{1,9}))?(?:Z|\+00:00)$"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TelemetryEmitter(Protocol):
|
|
22
|
+
def emit(self, event: dict[str, Any]) -> Any: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
AgtEventMapper = Callable[[Any], Iterable[dict[str, Any]]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AgtGovernanceEventSink:
|
|
29
|
+
"""AGT-compatible batch sink without a mandatory AGT dependency.
|
|
30
|
+
|
|
31
|
+
Construct directly with the source runtime's result sentinels, or use
|
|
32
|
+
:meth:`from_agent_os` when ``agent-os`` is installed.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
client: TelemetryEmitter,
|
|
38
|
+
mapper: AgtEventMapper,
|
|
39
|
+
*,
|
|
40
|
+
success_result: Any,
|
|
41
|
+
failure_result: Any,
|
|
42
|
+
) -> None:
|
|
43
|
+
self._client = client
|
|
44
|
+
self._mapper = mapper
|
|
45
|
+
self._success = success_result
|
|
46
|
+
self._failure = failure_result
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_agent_os(
|
|
50
|
+
cls,
|
|
51
|
+
client: TelemetryEmitter,
|
|
52
|
+
mapper: AgtEventMapper,
|
|
53
|
+
) -> "AgtGovernanceEventSink":
|
|
54
|
+
try:
|
|
55
|
+
from agent_os.event_sink import SinkExportResult
|
|
56
|
+
except ImportError as exc:
|
|
57
|
+
raise ImportError(
|
|
58
|
+
"AgtGovernanceEventSink.from_agent_os requires the agent-os package"
|
|
59
|
+
) from exc
|
|
60
|
+
return cls(
|
|
61
|
+
client,
|
|
62
|
+
mapper,
|
|
63
|
+
success_result=SinkExportResult.SUCCESS,
|
|
64
|
+
failure_result=SinkExportResult.FAILURE,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def emit(self, events: Sequence[Any]) -> Any:
|
|
68
|
+
"""Normalize then emit a batch, returning the configured AGT result."""
|
|
69
|
+
try:
|
|
70
|
+
normalized = [item for source in events for item in self._mapper(source)]
|
|
71
|
+
for event in normalized:
|
|
72
|
+
result = self._client.emit(event)
|
|
73
|
+
if not getattr(result, "accepted", False):
|
|
74
|
+
return self._failure
|
|
75
|
+
if getattr(result, "projection_errors", ()):
|
|
76
|
+
return self._failure
|
|
77
|
+
return self._success
|
|
78
|
+
except Exception:
|
|
79
|
+
return self._failure
|
|
80
|
+
|
|
81
|
+
def shutdown(self, timeout_ms: int = 5000) -> bool:
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def force_flush(self, timeout_ms: int = 30000) -> bool:
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def agt_policy_decision(
|
|
89
|
+
factory: EventFactory,
|
|
90
|
+
source: Any,
|
|
91
|
+
*,
|
|
92
|
+
run_id: str,
|
|
93
|
+
policy_engine_version: str,
|
|
94
|
+
bundle_digest: dict[str, str],
|
|
95
|
+
resource_type: str | None = None,
|
|
96
|
+
enforcement_mode: str = "enforce",
|
|
97
|
+
) -> dict[str, Any]:
|
|
98
|
+
"""Normalize one AGT policy event without copying free-form source content."""
|
|
99
|
+
kind = _enum_value(_field(source, "kind"))
|
|
100
|
+
if kind not in {"policy_check", "policy_violation"}:
|
|
101
|
+
raise ValueError(f"AGT event kind is not a policy decision: {kind!r}")
|
|
102
|
+
decision = _decision(_field(source, "decision"))
|
|
103
|
+
agent_id = _required_string(_field(source, "agent_id"), "agent_id")
|
|
104
|
+
action_type = _required_string(_field(source, "action"), "action")
|
|
105
|
+
attributes = _field(source, "attributes", {})
|
|
106
|
+
if not isinstance(attributes, dict):
|
|
107
|
+
raise ValueError("AGT attributes must be an object")
|
|
108
|
+
resolved_resource_type = resource_type or attributes.get("resource_type")
|
|
109
|
+
resolved_resource_type = _required_string(resolved_resource_type, "resource_type")
|
|
110
|
+
event_id = _event_id(_field(source, "event_id"))
|
|
111
|
+
reason_codes = _reason_codes(attributes.get("reason_codes", []))
|
|
112
|
+
latency_ms = _field(source, "latency_ms", 0.0)
|
|
113
|
+
if (
|
|
114
|
+
not isinstance(latency_ms, (int, float))
|
|
115
|
+
or isinstance(latency_ms, bool)
|
|
116
|
+
or not math.isfinite(latency_ms)
|
|
117
|
+
or latency_ms < 0
|
|
118
|
+
):
|
|
119
|
+
raise ValueError("AGT latency_ms must be a finite non-negative number")
|
|
120
|
+
policy: dict[str, Any] = {
|
|
121
|
+
"engine": "agt",
|
|
122
|
+
"engine_version": _required_string(policy_engine_version, "policy_engine_version"),
|
|
123
|
+
"bundle_digest": bundle_digest,
|
|
124
|
+
}
|
|
125
|
+
policy_name = _field(source, "policy_name")
|
|
126
|
+
if policy_name is not None:
|
|
127
|
+
policy["policy_id"] = _required_string(policy_name, "policy_name")
|
|
128
|
+
return factory.build(
|
|
129
|
+
"policy.decision",
|
|
130
|
+
run_id=run_id,
|
|
131
|
+
agent_id=agent_id,
|
|
132
|
+
event_id=event_id,
|
|
133
|
+
time_unix_nano=_timestamp_ns(_field(source, "occurred_at")),
|
|
134
|
+
trace_id=_field(source, "trace_id"),
|
|
135
|
+
span_id=_field(source, "span_id"),
|
|
136
|
+
decision=decision,
|
|
137
|
+
policy=policy,
|
|
138
|
+
action_type=action_type,
|
|
139
|
+
resource_type=resolved_resource_type,
|
|
140
|
+
enforcement_mode=enforcement_mode,
|
|
141
|
+
evaluation_duration_ns=round(latency_ms * 1_000_000),
|
|
142
|
+
reason_codes=reason_codes,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _field(source: Any, name: str, default: Any = None) -> Any:
|
|
147
|
+
if isinstance(source, dict):
|
|
148
|
+
return source.get(name, default)
|
|
149
|
+
return getattr(source, name, default)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _enum_value(value: Any) -> Any:
|
|
153
|
+
return value.value if isinstance(value, Enum) else value
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _decision(value: Any) -> str:
|
|
157
|
+
normalized = _enum_value(value)
|
|
158
|
+
mapping = {
|
|
159
|
+
"allow": "allow",
|
|
160
|
+
"allowed": "allow",
|
|
161
|
+
"deny": "deny",
|
|
162
|
+
"denied": "deny",
|
|
163
|
+
"block": "deny",
|
|
164
|
+
"blocked": "deny",
|
|
165
|
+
"require_approval": "challenge",
|
|
166
|
+
"requires_approval": "challenge",
|
|
167
|
+
"review": "challenge",
|
|
168
|
+
}
|
|
169
|
+
if normalized not in mapping:
|
|
170
|
+
raise ValueError(f"unsupported AGT policy decision: {normalized!r}")
|
|
171
|
+
return mapping[normalized]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _required_string(value: Any, field: str) -> str:
|
|
175
|
+
if not isinstance(value, str) or not value:
|
|
176
|
+
raise ValueError(f"AGT {field} must be a non-empty string")
|
|
177
|
+
return value
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _reason_codes(values: Any) -> list[str]:
|
|
181
|
+
if not isinstance(values, list):
|
|
182
|
+
raise ValueError("AGT reason_codes must be an array")
|
|
183
|
+
if len(values) > 32 or any(
|
|
184
|
+
not isinstance(value, str) or _IDENTIFIER.fullmatch(value) is None
|
|
185
|
+
for value in values
|
|
186
|
+
):
|
|
187
|
+
raise ValueError("AGT reason_codes must contain at most 32 identifiers")
|
|
188
|
+
if len(values) != len(set(values)):
|
|
189
|
+
raise ValueError("AGT reason_codes must be unique")
|
|
190
|
+
return list(values)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _event_id(value: Any) -> str:
|
|
194
|
+
try:
|
|
195
|
+
return str(uuid.UUID(_required_string(value, "event_id")))
|
|
196
|
+
except (ValueError, AttributeError) as exc:
|
|
197
|
+
raise ValueError("AGT event_id must be a UUID") from exc
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _timestamp_ns(value: Any) -> int:
|
|
201
|
+
value = _required_string(value, "occurred_at")
|
|
202
|
+
match = _TIMESTAMP.fullmatch(value)
|
|
203
|
+
if match is None:
|
|
204
|
+
raise ValueError("AGT occurred_at must be an RFC 3339 UTC timestamp")
|
|
205
|
+
base = datetime.fromisoformat(match.group("date")).replace(tzinfo=timezone.utc)
|
|
206
|
+
fraction = (match.group("fraction") or "").ljust(9, "0")
|
|
207
|
+
return int(base.timestamp()) * 1_000_000_000 + int(fraction or "0")
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Adapters for AGT's action-bound approval protocol objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .base import EventFactory
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_SOURCE_NAMESPACE = uuid.UUID("ea5a1737-5417-4eaa-8bb0-4fc40e4cb837")
|
|
15
|
+
_DIGEST = re.compile(r"^(?P<algorithm>sha256):(?P<value>[0-9a-f]{64})$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def agt_policy_decision_record(
|
|
19
|
+
factory: EventFactory,
|
|
20
|
+
source: Any,
|
|
21
|
+
*,
|
|
22
|
+
run_id: str,
|
|
23
|
+
agent_id: str,
|
|
24
|
+
action_type: str,
|
|
25
|
+
resource_type: str,
|
|
26
|
+
policy_engine_version: str,
|
|
27
|
+
bundle_digest: dict[str, str],
|
|
28
|
+
evaluation_duration_ns: int = 0,
|
|
29
|
+
enforcement_mode: str = "enforce",
|
|
30
|
+
trace_id: str | None = None,
|
|
31
|
+
span_id: str | None = None,
|
|
32
|
+
) -> dict[str, Any]:
|
|
33
|
+
"""Map an AGT PolicyDecisionRecord that suspended for approval."""
|
|
34
|
+
verdict = _enum_value(_field(source, "verdict"))
|
|
35
|
+
if verdict != "require_approval":
|
|
36
|
+
raise ValueError("AGT PolicyDecisionRecord verdict must be require_approval")
|
|
37
|
+
source_id = _required_string(_field(source, "policy_decision_id"), "policy_decision_id")
|
|
38
|
+
return factory.build(
|
|
39
|
+
"policy.decision",
|
|
40
|
+
run_id=run_id,
|
|
41
|
+
agent_id=agent_id,
|
|
42
|
+
event_id=_source_event_id("policy", source_id),
|
|
43
|
+
time_unix_nano=_datetime_ns(_field(source, "decided_at"), "decided_at"),
|
|
44
|
+
trace_id=trace_id,
|
|
45
|
+
span_id=span_id,
|
|
46
|
+
decision="challenge",
|
|
47
|
+
policy={
|
|
48
|
+
"engine": "agt",
|
|
49
|
+
"engine_version": policy_engine_version,
|
|
50
|
+
"policy_id": _required_string(_field(source, "policy_rule_id"), "policy_rule_id"),
|
|
51
|
+
"bundle_digest": bundle_digest,
|
|
52
|
+
},
|
|
53
|
+
action_type=action_type,
|
|
54
|
+
resource_type=resource_type,
|
|
55
|
+
enforcement_mode=enforcement_mode,
|
|
56
|
+
evaluation_duration_ns=evaluation_duration_ns,
|
|
57
|
+
reason_codes=["agt.verdict:require_approval"],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def agt_approval_request(
|
|
62
|
+
factory: EventFactory,
|
|
63
|
+
source: Any,
|
|
64
|
+
policy_decision: Any,
|
|
65
|
+
*,
|
|
66
|
+
run_id: str,
|
|
67
|
+
trace_id: str | None = None,
|
|
68
|
+
span_id: str | None = None,
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
"""Map an AGT ApprovalRequest while preserving its policy and chain links."""
|
|
71
|
+
_verify_request_binding(source, policy_decision)
|
|
72
|
+
approval_id = _required_string(_field(source, "approval_request_id"), "approval_request_id")
|
|
73
|
+
policy_id = _required_string(_field(source, "policy_decision_id"), "policy_decision_id")
|
|
74
|
+
return factory.build(
|
|
75
|
+
"approval.requested",
|
|
76
|
+
run_id=run_id,
|
|
77
|
+
agent_id=_required_string(_field(source, "agent_id"), "agent_id"),
|
|
78
|
+
event_id=_source_event_id("approval.requested", approval_id),
|
|
79
|
+
time_unix_nano=_datetime_ns(_field(source, "requested_at"), "requested_at"),
|
|
80
|
+
trace_id=trace_id,
|
|
81
|
+
span_id=span_id,
|
|
82
|
+
approval_id=approval_id,
|
|
83
|
+
policy_event_id=_source_event_id("policy", policy_id),
|
|
84
|
+
chain_id=_required_string(_field(source, "approval_chain_id"), "approval_chain_id"),
|
|
85
|
+
chain_version=_required_string(
|
|
86
|
+
_field(source, "approval_chain_version"), "approval_chain_version"
|
|
87
|
+
),
|
|
88
|
+
action_digest=_digest(_field(source, "action_digest"), "action_digest"),
|
|
89
|
+
actor_type="policy",
|
|
90
|
+
requested_at_unix_nano=_datetime_ns(_field(source, "requested_at"), "requested_at"),
|
|
91
|
+
expires_at_unix_nano=_datetime_ns(_field(source, "expires_at"), "expires_at"),
|
|
92
|
+
reason_codes=["agt.approval:requested"],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def agt_approval_resolution(
|
|
97
|
+
factory: EventFactory,
|
|
98
|
+
resolution: Any,
|
|
99
|
+
request: Any,
|
|
100
|
+
*,
|
|
101
|
+
run_id: str,
|
|
102
|
+
trace_id: str | None = None,
|
|
103
|
+
span_id: str | None = None,
|
|
104
|
+
) -> dict[str, Any]:
|
|
105
|
+
"""Map a terminal AGT resolution after verifying its request binding."""
|
|
106
|
+
_verify_resolution_binding(resolution, request)
|
|
107
|
+
outcome = _enum_value(_field(resolution, "outcome"))
|
|
108
|
+
event_type = {
|
|
109
|
+
"allow": "approval.approved",
|
|
110
|
+
"deny": "approval.rejected",
|
|
111
|
+
"expired": "approval.expired",
|
|
112
|
+
}.get(outcome)
|
|
113
|
+
if event_type is None:
|
|
114
|
+
raise ValueError(f"unsupported AGT approval outcome: {outcome!r}")
|
|
115
|
+
approval_id = _required_string(
|
|
116
|
+
_field(resolution, "approval_request_id"), "approval_request_id"
|
|
117
|
+
)
|
|
118
|
+
resolved_at = _datetime_ns(_field(resolution, "resolved_at"), "resolved_at")
|
|
119
|
+
requested_at = _datetime_ns(_field(request, "requested_at"), "requested_at")
|
|
120
|
+
if resolved_at < requested_at:
|
|
121
|
+
raise ValueError("AGT approval resolution cannot predate its request")
|
|
122
|
+
final_digest = _field(resolution, "final_entry_digest")
|
|
123
|
+
optional_evidence = (
|
|
124
|
+
{"approval_evidence_digest": _digest(final_digest, "final_entry_digest")}
|
|
125
|
+
if final_digest is not None
|
|
126
|
+
else {}
|
|
127
|
+
)
|
|
128
|
+
policy_id = _required_string(_field(request, "policy_decision_id"), "policy_decision_id")
|
|
129
|
+
resolution_id = _required_string(
|
|
130
|
+
_field(resolution, "approval_resolution_id"), "approval_resolution_id"
|
|
131
|
+
)
|
|
132
|
+
return factory.build(
|
|
133
|
+
event_type,
|
|
134
|
+
run_id=run_id,
|
|
135
|
+
agent_id=_required_string(_field(request, "agent_id"), "agent_id"),
|
|
136
|
+
event_id=_source_event_id("approval.resolution", resolution_id),
|
|
137
|
+
time_unix_nano=resolved_at,
|
|
138
|
+
trace_id=trace_id,
|
|
139
|
+
span_id=span_id,
|
|
140
|
+
approval_id=approval_id,
|
|
141
|
+
policy_event_id=_source_event_id("policy", policy_id),
|
|
142
|
+
chain_id=_required_string(_field(request, "approval_chain_id"), "approval_chain_id"),
|
|
143
|
+
chain_version=_required_string(
|
|
144
|
+
_field(request, "approval_chain_version"), "approval_chain_version"
|
|
145
|
+
),
|
|
146
|
+
resolution_id=resolution_id,
|
|
147
|
+
action_digest=_digest(_field(resolution, "action_digest"), "action_digest"),
|
|
148
|
+
actor_type="system",
|
|
149
|
+
requested_at_unix_nano=requested_at,
|
|
150
|
+
expires_at_unix_nano=_datetime_ns(_field(request, "expires_at"), "expires_at"),
|
|
151
|
+
reason_codes=[f"agt.outcome:{outcome}"],
|
|
152
|
+
**optional_evidence,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _verify_resolution_binding(resolution: Any, request: Any) -> None:
|
|
157
|
+
pairs = (
|
|
158
|
+
("approval_request_id", "approval_request_id"),
|
|
159
|
+
("action_digest", "action_digest"),
|
|
160
|
+
("policy_version", "policy_version"),
|
|
161
|
+
("approval_chain_version", "approval_chain_version"),
|
|
162
|
+
)
|
|
163
|
+
for resolution_field, request_field in pairs:
|
|
164
|
+
left = _required_string(
|
|
165
|
+
_enum_value(_field(resolution, resolution_field)), resolution_field
|
|
166
|
+
)
|
|
167
|
+
right = _required_string(
|
|
168
|
+
_enum_value(_field(request, request_field)), request_field
|
|
169
|
+
)
|
|
170
|
+
if left != right:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"AGT resolution {resolution_field} does not match request {request_field}"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _verify_request_binding(request: Any, policy_decision: Any) -> None:
|
|
177
|
+
pairs = (
|
|
178
|
+
("policy_decision_id", "policy_decision_id"),
|
|
179
|
+
("action_digest", "action_digest"),
|
|
180
|
+
("policy_version", "policy_version"),
|
|
181
|
+
("approval_chain_id", "approval_chain_id"),
|
|
182
|
+
("approval_chain_version", "approval_chain_version"),
|
|
183
|
+
)
|
|
184
|
+
for request_field, policy_field in pairs:
|
|
185
|
+
left = _required_string(
|
|
186
|
+
_enum_value(_field(request, request_field)), request_field
|
|
187
|
+
)
|
|
188
|
+
right = _required_string(
|
|
189
|
+
_enum_value(_field(policy_decision, policy_field)), policy_field
|
|
190
|
+
)
|
|
191
|
+
if left != right:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
f"AGT request {request_field} does not match policy decision {policy_field}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _source_event_id(kind: str, source_id: str) -> str:
|
|
198
|
+
return str(uuid.uuid5(_SOURCE_NAMESPACE, f"{kind}:{source_id}"))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _field(source: Any, name: str) -> Any:
|
|
202
|
+
if isinstance(source, dict):
|
|
203
|
+
return source.get(name)
|
|
204
|
+
return getattr(source, name, None)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _enum_value(value: Any) -> Any:
|
|
208
|
+
return value.value if isinstance(value, Enum) else value
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _required_string(value: Any, field: str) -> str:
|
|
212
|
+
if not isinstance(value, str) or not value:
|
|
213
|
+
raise ValueError(f"AGT {field} must be a non-empty string")
|
|
214
|
+
return value
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _digest(value: Any, field: str) -> dict[str, str]:
|
|
218
|
+
value = _required_string(value, field)
|
|
219
|
+
match = _DIGEST.fullmatch(value)
|
|
220
|
+
if match is None:
|
|
221
|
+
raise ValueError(f"AGT {field} must use sha256:<lowercase-hex>")
|
|
222
|
+
return match.groupdict()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _datetime_ns(value: Any, field: str) -> int:
|
|
226
|
+
if not isinstance(value, datetime) or value.tzinfo is None:
|
|
227
|
+
raise ValueError(f"AGT {field} must be a timezone-aware datetime")
|
|
228
|
+
utc = value.astimezone(timezone.utc)
|
|
229
|
+
epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
230
|
+
delta = utc - epoch
|
|
231
|
+
return (
|
|
232
|
+
delta.days * 86_400_000_000_000
|
|
233
|
+
+ delta.seconds * 1_000_000_000
|
|
234
|
+
+ delta.microseconds * 1_000
|
|
235
|
+
)
|