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.
Files changed (35) hide show
  1. agentrust_telemetry/__init__.py +80 -0
  2. agentrust_telemetry/adapters/__init__.py +28 -0
  3. agentrust_telemetry/adapters/agt.py +207 -0
  4. agentrust_telemetry/adapters/agt_approval.py +235 -0
  5. agentrust_telemetry/adapters/agt_audit.py +177 -0
  6. agentrust_telemetry/adapters/agt_data.py +105 -0
  7. agentrust_telemetry/adapters/base.py +90 -0
  8. agentrust_telemetry/adapters/cedar.py +66 -0
  9. agentrust_telemetry/adapters/opa.py +102 -0
  10. agentrust_telemetry/client.py +113 -0
  11. agentrust_telemetry/context.py +30 -0
  12. agentrust_telemetry/data_flow.py +112 -0
  13. agentrust_telemetry/errors.py +26 -0
  14. agentrust_telemetry/evidence.py +204 -0
  15. agentrust_telemetry/otel.py +135 -0
  16. agentrust_telemetry/projection.py +49 -0
  17. agentrust_telemetry/propagation.py +102 -0
  18. agentrust_telemetry/py.typed +1 -0
  19. agentrust_telemetry/schemas/action.schema.json +39 -0
  20. agentrust_telemetry/schemas/approval.schema.json +27 -0
  21. agentrust_telemetry/schemas/common.schema.json +36 -0
  22. agentrust_telemetry/schemas/data-flow.schema.json +46 -0
  23. agentrust_telemetry/schemas/envelope.schema.json +21 -0
  24. agentrust_telemetry/schemas/evidence.schema.json +20 -0
  25. agentrust_telemetry/schemas/policy-decision.schema.json +33 -0
  26. agentrust_telemetry/schemas/usage.schema.json +80 -0
  27. agentrust_telemetry/trace_adapter.py +263 -0
  28. agentrust_telemetry/usage.py +190 -0
  29. agentrust_telemetry/validation.py +102 -0
  30. agentrust_telemetry-0.1.0a3.dist-info/METADATA +180 -0
  31. agentrust_telemetry-0.1.0a3.dist-info/RECORD +35 -0
  32. agentrust_telemetry-0.1.0a3.dist-info/WHEEL +5 -0
  33. agentrust_telemetry-0.1.0a3.dist-info/licenses/LICENSE +21 -0
  34. agentrust_telemetry-0.1.0a3.dist-info/licenses/NOTICE +4 -0
  35. 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
+ )