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 +128 -0
- aigp/attributes.py +129 -0
- aigp/baggage.py +126 -0
- aigp/cloudevents.py +264 -0
- aigp/decorators.py +747 -0
- aigp/events.py +535 -0
- aigp/instrumentor.py +1312 -0
- aigp/openlineage.py +249 -0
- aigp/tracestate.py +149 -0
- aigp_python-1.0.0.dist-info/METADATA +280 -0
- aigp_python-1.0.0.dist-info/RECORD +12 -0
- aigp_python-1.0.0.dist-info/WHEEL +4 -0
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
|