aigp-python 1.0.0__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.
aigp/__init__.py ADDED
@@ -0,0 +1,128 @@
1
+ """
2
+ AIGP — AI Governance Proof
3
+ ==========================
4
+
5
+ Open standard for proving your AI agents used the approved
6
+ policies, prompts, and tools — every single time.
7
+
8
+ Quick Start::
9
+
10
+ pip install aigp-python sandarb
11
+
12
+ from aigp import configure, aigp
13
+ from sandarb import SandarbBackend
14
+
15
+ configure(
16
+ backend=SandarbBackend("https://api.sandarb.ai", token="..."),
17
+ agent_id="agent.my-bot-v1",
18
+ )
19
+
20
+ @aigp(policy="policy.trading-limits")
21
+ def process_trade(order: dict, governance=None):
22
+ if governance.denied:
23
+ return {"error": governance.denial_reason}
24
+ return execute(order)
25
+
26
+ What AIGP does:
27
+ - Proves governance was delivered (Merkle proofs, cryptographic hashes)
28
+ - Emits standardized events (INJECT_SUCCESS, TOOL_INVOKED, A2A_CALL, ...)
29
+ - Transports via OpenTelemetry (dual-emit to governance store + observability)
30
+ - Stays vendor-neutral — any GovernanceBackend can plug in
31
+
32
+ What AIGP does NOT do:
33
+ - Dictate how governance works — that's the backend's job
34
+ - Provide policies, prompts, or tools — those come from your governance server
35
+
36
+ Website: https://open-aigp.org
37
+ GitHub: https://github.com/open-aigp/aigp
38
+ """
39
+
40
+ # ── Core: what most developers need ──────────────────────────────────
41
+
42
+ from aigp.decorators import (
43
+ configure,
44
+ aigp,
45
+ aigp_action,
46
+ a2a_traced,
47
+ audit_action,
48
+ GovernanceBackend,
49
+ GovernanceResult,
50
+ GovernanceError,
51
+ GovernedActionContext,
52
+ get_backend,
53
+ get_instrumentor,
54
+ )
55
+
56
+ # ── Events & Proof ────────────────────────────────────────────────────
57
+
58
+ from aigp.instrumentor import AIGPInstrumentor
59
+ from aigp.events import (
60
+ create_aigp_event,
61
+ compute_governance_hash,
62
+ compute_leaf_hash,
63
+ compute_merkle_governance_hash,
64
+ sign_event,
65
+ verify_event_signature,
66
+ )
67
+
68
+ # ── Context propagation (OTel integration) ────────────────────────────
69
+
70
+ from aigp.attributes import AIGPAttributes
71
+ from aigp.baggage import AIGPBaggage
72
+ from aigp.tracestate import AIGPTraceState
73
+
74
+ # ── OpenLineage integration ───────────────────────────────────────────
75
+
76
+ from aigp.openlineage import (
77
+ build_governance_run_facet,
78
+ build_resource_input_facets,
79
+ build_openlineage_run_event,
80
+ )
81
+
82
+ # ── CloudEvents transport ─────────────────────────────────────────────
83
+
84
+ from aigp.cloudevents import (
85
+ wrap_as_cloudevent,
86
+ unwrap_from_cloudevent,
87
+ build_ce_headers,
88
+ ce_type_from_event_type,
89
+ event_type_from_ce_type,
90
+ )
91
+
92
+ __version__ = "1.0.0"
93
+ __all__ = [
94
+ # ── Start here ──
95
+ "configure",
96
+ "aigp",
97
+ "aigp_action",
98
+ "a2a_traced",
99
+ "audit_action",
100
+ "GovernanceBackend",
101
+ "GovernanceResult",
102
+ "GovernanceError",
103
+ "GovernedActionContext",
104
+ "get_backend",
105
+ "get_instrumentor",
106
+ # ── Events & Proof ──
107
+ "AIGPInstrumentor",
108
+ "create_aigp_event",
109
+ "compute_governance_hash",
110
+ "compute_leaf_hash",
111
+ "compute_merkle_governance_hash",
112
+ "sign_event",
113
+ "verify_event_signature",
114
+ # ── Context propagation ──
115
+ "AIGPAttributes",
116
+ "AIGPBaggage",
117
+ "AIGPTraceState",
118
+ # ── OpenLineage ──
119
+ "build_governance_run_facet",
120
+ "build_resource_input_facets",
121
+ "build_openlineage_run_event",
122
+ # ── CloudEvents ──
123
+ "wrap_as_cloudevent",
124
+ "unwrap_from_cloudevent",
125
+ "build_ce_headers",
126
+ "ce_type_from_event_type",
127
+ "event_type_from_ce_type",
128
+ ]
aigp/attributes.py ADDED
@@ -0,0 +1,129 @@
1
+ """
2
+ AIGP Semantic Attributes for OpenTelemetry
3
+ ==========================================
4
+
5
+ Defines the `aigp.*` namespace attributes as constants, organized by
6
+ their OTel signal type (Resource vs Span vs Span Event).
7
+
8
+ These follow OTel naming conventions:
9
+ - Lowercase, dot-separated namespace hierarchy
10
+ - Domain-first approach: aigp.{component}.{property}
11
+ """
12
+
13
+
14
+ class AIGPAttributes:
15
+ """Constants for AIGP semantic attributes in the aigp.* namespace."""
16
+
17
+ # -------------------------------------------------------
18
+ # Resource Attributes (constant per agent process)
19
+ # -------------------------------------------------------
20
+ AGENT_ID = "aigp.agent.id"
21
+ AGENT_NAME = "aigp.agent.name"
22
+ ORG_ID = "aigp.org.id"
23
+ ORG_NAME = "aigp.org.name"
24
+
25
+ # -------------------------------------------------------
26
+ # Span Attributes: Core Governance
27
+ # -------------------------------------------------------
28
+ EVENT_ID = "aigp.event.id"
29
+ EVENT_TYPE = "aigp.event.type"
30
+ EVENT_CATEGORY = "aigp.event.category"
31
+ GOVERNANCE_HASH = "aigp.governance.hash"
32
+ GOVERNANCE_HASH_TYPE = "aigp.governance.hash_type"
33
+ DATA_CLASSIFICATION = "aigp.data.classification"
34
+ ENFORCEMENT_RESULT = "aigp.enforcement.result"
35
+ EVENT_SIGNATURE = "aigp.event.signature"
36
+ SIGNATURE_KEY_ID = "aigp.signature.key_id"
37
+ SEQUENCE_NUMBER = "aigp.sequence.number"
38
+ CAUSALITY_REF = "aigp.causality.ref"
39
+
40
+ # -------------------------------------------------------
41
+ # Span Attributes: Policy (singular — one policy per span)
42
+ # -------------------------------------------------------
43
+ POLICY_NAME = "aigp.policy.name"
44
+ POLICY_VERSION = "aigp.policy.version"
45
+ POLICY_ID = "aigp.policy.id"
46
+
47
+ # -------------------------------------------------------
48
+ # Span Attributes: Prompt (singular — one prompt per span)
49
+ # -------------------------------------------------------
50
+ PROMPT_NAME = "aigp.prompt.name"
51
+ PROMPT_VERSION = "aigp.prompt.version"
52
+ PROMPT_ID = "aigp.prompt.id"
53
+
54
+ # -------------------------------------------------------
55
+ # Span Attributes: Multi-Policy / Multi-Prompt / Multi-Tool
56
+ # Array-valued attributes for operations involving multiple
57
+ # governed resources simultaneously.
58
+ # -------------------------------------------------------
59
+ POLICIES_NAMES = "aigp.policies.names"
60
+ POLICIES_VERSIONS = "aigp.policies.versions"
61
+ PROMPTS_NAMES = "aigp.prompts.names"
62
+ PROMPTS_VERSIONS = "aigp.prompts.versions"
63
+ TOOLS_NAMES = "aigp.tools.names"
64
+ CONTEXTS_NAMES = "aigp.contexts.names"
65
+ LINEAGES_NAMES = "aigp.lineages.names"
66
+ MEMORIES_NAMES = "aigp.memories.names"
67
+ MODELS_NAMES = "aigp.models.names"
68
+
69
+ # -------------------------------------------------------
70
+ # Span Attributes: Merkle Tree Governance
71
+ # -------------------------------------------------------
72
+ MERKLE_LEAF_COUNT = "aigp.governance.merkle.leaf_count"
73
+
74
+ # -------------------------------------------------------
75
+ # Span Attributes: Denial and Violation
76
+ # -------------------------------------------------------
77
+ SEVERITY = "aigp.severity"
78
+ VIOLATION_TYPE = "aigp.violation.type"
79
+ DENIAL_REASON = "aigp.denial.reason"
80
+
81
+ # -------------------------------------------------------
82
+ # Span Event Names
83
+ # -------------------------------------------------------
84
+ EVENT_INJECT_SUCCESS = "aigp.inject.success"
85
+ EVENT_INJECT_DENIED = "aigp.inject.denied"
86
+ EVENT_PROMPT_USED = "aigp.prompt.used"
87
+ EVENT_PROMPT_DENIED = "aigp.prompt.denied"
88
+ EVENT_POLICY_VIOLATION = "aigp.policy.violation"
89
+ EVENT_GOVERNANCE_PROOF = "aigp.governance.proof"
90
+ EVENT_A2A_CALL = "aigp.a2a.call"
91
+ EVENT_MEMORY_READ = "aigp.memory.read"
92
+ EVENT_MEMORY_WRITTEN = "aigp.memory.written"
93
+ EVENT_TOOL_INVOKED = "aigp.tool.invoked"
94
+ EVENT_TOOL_DENIED = "aigp.tool.denied"
95
+ EVENT_CONTEXT_CAPTURED = "aigp.context.captured"
96
+ EVENT_LINEAGE_SNAPSHOT = "aigp.lineage.snapshot"
97
+ EVENT_INFERENCE_STARTED = "aigp.inference.started"
98
+ EVENT_INFERENCE_COMPLETED = "aigp.inference.completed"
99
+ EVENT_INFERENCE_BLOCKED = "aigp.inference.blocked"
100
+ EVENT_HUMAN_OVERRIDE = "aigp.human.override"
101
+ EVENT_HUMAN_APPROVAL = "aigp.human.approval"
102
+ EVENT_CLASSIFICATION_CHANGED = "aigp.classification.changed"
103
+ EVENT_MODEL_LOADED = "aigp.model.loaded"
104
+ EVENT_MODEL_SWITCHED = "aigp.model.switched"
105
+ EVENT_UNVERIFIED_BOUNDARY = "aigp.boundary.unverified"
106
+
107
+ # -------------------------------------------------------
108
+ # Enforcement result values
109
+ # -------------------------------------------------------
110
+ ENFORCEMENT_ALLOWED = "allowed"
111
+ ENFORCEMENT_DENIED = "denied"
112
+
113
+ # -------------------------------------------------------
114
+ # Data classification values
115
+ # -------------------------------------------------------
116
+ CLASSIFICATION_PUBLIC = "public"
117
+ CLASSIFICATION_INTERNAL = "internal"
118
+ CLASSIFICATION_CONFIDENTIAL = "confidential"
119
+ CLASSIFICATION_RESTRICTED = "restricted"
120
+
121
+ # -------------------------------------------------------
122
+ # Classification abbreviations for tracestate
123
+ # -------------------------------------------------------
124
+ CLASSIFICATION_ABBREV = {
125
+ "public": "pub",
126
+ "internal": "int",
127
+ "confidential": "con",
128
+ "restricted": "res",
129
+ }
aigp/baggage.py ADDED
@@ -0,0 +1,126 @@
1
+ """
2
+ AIGP Baggage Propagation
3
+ ========================
4
+
5
+ Manages OTel Baggage for propagating governance context across
6
+ agent-to-agent calls.
7
+
8
+ When Agent A calls Agent B, the governance context (active policy,
9
+ data classification, org) travels via OTel Baggage in HTTP headers.
10
+
11
+ Security: Sensitive data (governance_hash, denial_reason, raw content)
12
+ MUST NOT be placed in Baggage as it leaks in HTTP headers.
13
+ """
14
+
15
+ from opentelemetry import baggage, context
16
+ from opentelemetry.context import Context
17
+ from typing import Optional
18
+
19
+ from aigp.attributes import AIGPAttributes
20
+
21
+
22
+ class AIGPBaggage:
23
+ """
24
+ Manages AIGP governance context in OTel Baggage.
25
+
26
+ Baggage items are propagated automatically across service boundaries
27
+ when OTel instrumentation is active (HTTP, gRPC, etc.).
28
+ """
29
+
30
+ # Keys that are safe to propagate in Baggage (non-sensitive)
31
+ SAFE_KEYS = {
32
+ AIGPAttributes.POLICY_NAME,
33
+ AIGPAttributes.DATA_CLASSIFICATION,
34
+ AIGPAttributes.ORG_ID,
35
+ }
36
+
37
+ # Keys that MUST NOT be propagated (sensitive)
38
+ FORBIDDEN_KEYS = {
39
+ AIGPAttributes.GOVERNANCE_HASH,
40
+ AIGPAttributes.DENIAL_REASON,
41
+ AIGPAttributes.VIOLATION_TYPE,
42
+ }
43
+
44
+ @staticmethod
45
+ def inject(
46
+ policy_name: str = "",
47
+ data_classification: str = "",
48
+ org_id: str = "",
49
+ ctx: Optional[Context] = None,
50
+ ) -> Context:
51
+ """
52
+ Inject AIGP governance context into OTel Baggage.
53
+
54
+ Call this before making an agent-to-agent request so that the
55
+ receiving agent inherits governance context.
56
+
57
+ Args:
58
+ policy_name: Active governed policy (AGRN format).
59
+ data_classification: Data sensitivity level.
60
+ org_id: Organization identifier (AGRN format).
61
+ ctx: Optional OTel context. Defaults to current context.
62
+
63
+ Returns:
64
+ Updated OTel context with Baggage items.
65
+ """
66
+ current_ctx = ctx or context.get_current()
67
+
68
+ if policy_name:
69
+ current_ctx = baggage.set_baggage(
70
+ AIGPAttributes.POLICY_NAME, policy_name, context=current_ctx
71
+ )
72
+ if data_classification:
73
+ current_ctx = baggage.set_baggage(
74
+ AIGPAttributes.DATA_CLASSIFICATION, data_classification, context=current_ctx
75
+ )
76
+ if org_id:
77
+ current_ctx = baggage.set_baggage(
78
+ AIGPAttributes.ORG_ID, org_id, context=current_ctx
79
+ )
80
+
81
+ return current_ctx
82
+
83
+ @staticmethod
84
+ def extract(ctx: Optional[Context] = None) -> dict[str, str]:
85
+ """
86
+ Extract AIGP governance context from OTel Baggage.
87
+
88
+ Call this on the receiving side of an agent-to-agent call to
89
+ recover governance context set by the calling agent.
90
+
91
+ Args:
92
+ ctx: Optional OTel context. Defaults to current context.
93
+
94
+ Returns:
95
+ Dict of AIGP Baggage items found.
96
+ """
97
+ current_ctx = ctx or context.get_current()
98
+ result = {}
99
+
100
+ for key in AIGPBaggage.SAFE_KEYS:
101
+ value = baggage.get_baggage(key, context=current_ctx)
102
+ if value:
103
+ result[key] = value
104
+
105
+ return result
106
+
107
+ @staticmethod
108
+ def clear(ctx: Optional[Context] = None) -> Context:
109
+ """
110
+ Remove all AIGP Baggage items from context.
111
+
112
+ Useful when crossing trust boundaries where governance context
113
+ should not leak further.
114
+
115
+ Args:
116
+ ctx: Optional OTel context. Defaults to current context.
117
+
118
+ Returns:
119
+ Updated context with AIGP Baggage items removed.
120
+ """
121
+ current_ctx = ctx or context.get_current()
122
+
123
+ for key in AIGPBaggage.SAFE_KEYS:
124
+ current_ctx = baggage.remove_baggage(key, context=current_ctx)
125
+
126
+ return current_ctx
aigp/cloudevents.py ADDED
@@ -0,0 +1,264 @@
1
+ """
2
+ AIGP CloudEvents Binding
3
+ ========================
4
+
5
+ Wraps AIGP events in CloudEvents envelopes (structured mode) and
6
+ unwraps CloudEvents back to raw AIGP events. Implements the binding
7
+ defined in AIGP Specification Section 13.
8
+
9
+ CloudEvents spec: https://cloudevents.io/
10
+ AIGP binding: Section 13 — Transport Bindings via CloudEvents
11
+
12
+ Usage:
13
+ from aigp.cloudevents import wrap_as_cloudevent, unwrap_from_cloudevent
14
+
15
+ ce = wrap_as_cloudevent(aigp_event)
16
+ # -> {"specversion": "1.0", "type": "org.aigp.v1.inject_success", ...}
17
+
18
+ aigp_event = unwrap_from_cloudevent(ce)
19
+ # -> {"event_id": "...", "event_type": "INJECT_SUCCESS", ...}
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Any
25
+
26
+ # CloudEvents spec version
27
+ CE_SPECVERSION = "1.0"
28
+
29
+ # AIGP type prefix (reverse-DNS, versioned)
30
+ AIGP_TYPE_PREFIX = "org.aigp.v1."
31
+
32
+ # AIGP source scheme
33
+ AIGP_SOURCE_SCHEME = "aigp://"
34
+
35
+ # AIGP JSON Schema URI
36
+ AIGP_DATA_SCHEMA = "https://open-aigp.org/schema/aigp-event.schema.json"
37
+
38
+
39
+ def wrap_as_cloudevent(
40
+ aigp_event: dict[str, Any],
41
+ *,
42
+ include_dataschema: bool = True,
43
+ ) -> dict[str, Any]:
44
+ """
45
+ Wrap an AIGP event in a CloudEvents structured-mode envelope.
46
+
47
+ Maps AIGP fields to CloudEvents context attributes and AIGP extension
48
+ attributes per Spec Section 13.
49
+
50
+ Args:
51
+ aigp_event: An AIGP event dict (from ``create_aigp_event`` or any
52
+ ``AIGPInstrumentor`` method).
53
+ include_dataschema: Whether to include the ``dataschema`` attribute
54
+ pointing to the AIGP JSON Schema. Default True.
55
+
56
+ Returns:
57
+ Dict conforming to CloudEvents JSON Format (structured mode).
58
+ The AIGP event is in the ``data`` field.
59
+
60
+ Raises:
61
+ ValueError: If required AIGP fields are missing.
62
+ """
63
+ event_id = aigp_event.get("event_id", "")
64
+ event_type = aigp_event.get("event_type", "")
65
+ agent_id = aigp_event.get("agent_id", "")
66
+
67
+ if not event_id or not event_type or not agent_id:
68
+ raise ValueError(
69
+ "AIGP event must have event_id, event_type, and agent_id "
70
+ "to wrap as CloudEvent"
71
+ )
72
+
73
+ # Build source URI: aigp://<org_id>/<agent_id>
74
+ org_id = aigp_event.get("org_id", "") or "default"
75
+ source = f"{AIGP_SOURCE_SCHEME}{org_id}/{agent_id}"
76
+
77
+ # Build type: org.aigp.v1.<lowercase_event_type>
78
+ ce_type = f"{AIGP_TYPE_PREFIX}{event_type.lower()}"
79
+
80
+ # Core context attributes
81
+ ce: dict[str, Any] = {
82
+ "specversion": CE_SPECVERSION,
83
+ "id": event_id,
84
+ "type": ce_type,
85
+ "source": source,
86
+ }
87
+
88
+ # Optional context attributes
89
+ event_time = aigp_event.get("event_time", "")
90
+ if event_time:
91
+ ce["time"] = event_time
92
+
93
+ ce["datacontenttype"] = "application/json"
94
+
95
+ if include_dataschema:
96
+ ce["dataschema"] = AIGP_DATA_SCHEMA
97
+
98
+ # Subject: primary governed resource (policy or prompt name)
99
+ policy_name = aigp_event.get("policy_name", "")
100
+ prompt_name = aigp_event.get("prompt_name", "")
101
+ subject = policy_name or prompt_name
102
+ if subject:
103
+ ce["subject"] = subject
104
+
105
+ # AIGP extension attributes (lowercase a-z0-9 only per CE spec)
106
+ ce["aigpagentid"] = agent_id
107
+
108
+ if org_id != "default":
109
+ ce["aigporgid"] = org_id
110
+
111
+ event_category = aigp_event.get("event_category", "")
112
+ if event_category:
113
+ ce["aigpcategory"] = event_category
114
+
115
+ data_classification = aigp_event.get("data_classification", "")
116
+ if data_classification:
117
+ ce["aigpclassification"] = data_classification
118
+
119
+ severity = aigp_event.get("severity", "")
120
+ if severity:
121
+ ce["aigpseverity"] = severity
122
+
123
+ hash_type = aigp_event.get("hash_type", "")
124
+ if hash_type:
125
+ ce["aigphashtype"] = hash_type
126
+
127
+ # Data payload: the full AIGP event
128
+ ce["data"] = aigp_event
129
+
130
+ return ce
131
+
132
+
133
+ def unwrap_from_cloudevent(ce: dict[str, Any]) -> dict[str, Any]:
134
+ """
135
+ Extract the AIGP event from a CloudEvents structured-mode envelope.
136
+
137
+ Validates that the CloudEvents envelope has the expected structure
138
+ and returns the ``data`` payload.
139
+
140
+ Args:
141
+ ce: A CloudEvents dict in structured JSON format.
142
+
143
+ Returns:
144
+ The AIGP event dict from the ``data`` field.
145
+
146
+ Raises:
147
+ ValueError: If the envelope is not a valid AIGP CloudEvent.
148
+ """
149
+ specversion = ce.get("specversion", "")
150
+ if specversion != CE_SPECVERSION:
151
+ raise ValueError(
152
+ f"Unsupported CloudEvents specversion: {specversion!r} "
153
+ f"(expected {CE_SPECVERSION!r})"
154
+ )
155
+
156
+ ce_type = ce.get("type", "")
157
+ if not ce_type.startswith(AIGP_TYPE_PREFIX):
158
+ raise ValueError(
159
+ f"CloudEvents type {ce_type!r} does not start with "
160
+ f"{AIGP_TYPE_PREFIX!r} — not an AIGP event"
161
+ )
162
+
163
+ data = ce.get("data")
164
+ if data is None:
165
+ raise ValueError("CloudEvents envelope has no 'data' field")
166
+
167
+ if not isinstance(data, dict):
168
+ raise ValueError(
169
+ f"CloudEvents 'data' must be a dict, got {type(data).__name__}"
170
+ )
171
+
172
+ return data
173
+
174
+
175
+ def ce_type_from_event_type(event_type: str) -> str:
176
+ """
177
+ Convert an AIGP event_type to a CloudEvents type string.
178
+
179
+ Args:
180
+ event_type: AIGP event type (e.g., "INJECT_SUCCESS").
181
+
182
+ Returns:
183
+ CloudEvents type (e.g., "org.aigp.v1.inject_success").
184
+ """
185
+ return f"{AIGP_TYPE_PREFIX}{event_type.lower()}"
186
+
187
+
188
+ def event_type_from_ce_type(ce_type: str) -> str:
189
+ """
190
+ Convert a CloudEvents type string back to an AIGP event_type.
191
+
192
+ Args:
193
+ ce_type: CloudEvents type (e.g., "org.aigp.v1.inject_success").
194
+
195
+ Returns:
196
+ AIGP event type (e.g., "INJECT_SUCCESS").
197
+
198
+ Raises:
199
+ ValueError: If the type does not have the AIGP prefix.
200
+ """
201
+ if not ce_type.startswith(AIGP_TYPE_PREFIX):
202
+ raise ValueError(
203
+ f"CloudEvents type {ce_type!r} does not start with "
204
+ f"{AIGP_TYPE_PREFIX!r}"
205
+ )
206
+ return ce_type[len(AIGP_TYPE_PREFIX):].upper()
207
+
208
+
209
+ def build_ce_headers(
210
+ aigp_event: dict[str, Any],
211
+ *,
212
+ prefix: str = "ce-",
213
+ ) -> dict[str, str]:
214
+ """
215
+ Build CloudEvents binary-mode headers from an AIGP event.
216
+
217
+ For HTTP, use prefix="ce-" (default). For Kafka, use prefix="ce_".
218
+
219
+ Args:
220
+ aigp_event: An AIGP event dict.
221
+ prefix: Header prefix. "ce-" for HTTP, "ce_" for Kafka.
222
+
223
+ Returns:
224
+ Dict of header name -> header value strings.
225
+ """
226
+ event_id = aigp_event.get("event_id", "")
227
+ event_type = aigp_event.get("event_type", "")
228
+ agent_id = aigp_event.get("agent_id", "")
229
+ org_id = aigp_event.get("org_id", "") or "default"
230
+
231
+ headers: dict[str, str] = {
232
+ f"{prefix}specversion": CE_SPECVERSION,
233
+ f"{prefix}id": event_id,
234
+ f"{prefix}type": f"{AIGP_TYPE_PREFIX}{event_type.lower()}",
235
+ f"{prefix}source": f"{AIGP_SOURCE_SCHEME}{org_id}/{agent_id}",
236
+ }
237
+
238
+ event_time = aigp_event.get("event_time", "")
239
+ if event_time:
240
+ headers[f"{prefix}time"] = event_time
241
+
242
+ # AIGP extension attributes
243
+ headers[f"{prefix}aigpagentid"] = agent_id
244
+
245
+ if org_id != "default":
246
+ headers[f"{prefix}aigporgid"] = org_id
247
+
248
+ event_category = aigp_event.get("event_category", "")
249
+ if event_category:
250
+ headers[f"{prefix}aigpcategory"] = event_category
251
+
252
+ data_classification = aigp_event.get("data_classification", "")
253
+ if data_classification:
254
+ headers[f"{prefix}aigpclassification"] = data_classification
255
+
256
+ severity = aigp_event.get("severity", "")
257
+ if severity:
258
+ headers[f"{prefix}aigpseverity"] = severity
259
+
260
+ hash_type = aigp_event.get("hash_type", "")
261
+ if hash_type:
262
+ headers[f"{prefix}aigphashtype"] = hash_type
263
+
264
+ return headers