aigp-agent-core 1.0.0__tar.gz
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.
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.venv/
|
|
8
|
+
.eggs/
|
|
9
|
+
|
|
10
|
+
# Node
|
|
11
|
+
node_modules/
|
|
12
|
+
dist/
|
|
13
|
+
|
|
14
|
+
# Go
|
|
15
|
+
bin/
|
|
16
|
+
|
|
17
|
+
# Rust
|
|
18
|
+
target/
|
|
19
|
+
|
|
20
|
+
# .NET
|
|
21
|
+
bin/
|
|
22
|
+
obj/
|
|
23
|
+
|
|
24
|
+
# Java
|
|
25
|
+
*.class
|
|
26
|
+
out/
|
|
27
|
+
|
|
28
|
+
# IDE
|
|
29
|
+
.idea/
|
|
30
|
+
.vscode/
|
|
31
|
+
*.swp
|
|
32
|
+
|
|
33
|
+
# OS
|
|
34
|
+
.DS_Store
|
|
35
|
+
Thumbs.db
|
|
36
|
+
|
|
37
|
+
# Secrets (never commit)
|
|
38
|
+
.env
|
|
39
|
+
*.pem
|
|
40
|
+
*.key
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aigp-agent-core
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: AIGP Agent Governance Core — shared lifecycle for all framework adapters
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: aigp-client>=3.0.0
|
|
7
|
+
Provides-Extra: evidence
|
|
8
|
+
Requires-Dist: amigo-bedrock>=0.2.0; extra == 'evidence'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""AIGP Agent Core — shared governance lifecycle for all framework adapters.
|
|
2
|
+
|
|
3
|
+
Usage by adapter authors:
|
|
4
|
+
from aigp_agent_core import AgentGovernance
|
|
5
|
+
|
|
6
|
+
class MyFrameworkAdapter(AgentGovernance):
|
|
7
|
+
def on_my_framework_start(self, ...):
|
|
8
|
+
self.pre_invoke(agent_name, model_id, user_id)
|
|
9
|
+
def on_my_framework_end(self, ...):
|
|
10
|
+
self.post_invoke()
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__version__ = "1.0.0"
|
|
14
|
+
|
|
15
|
+
from .governance import AgentGovernance
|
|
16
|
+
from .stage_mapper import StageMapper
|
|
17
|
+
|
|
18
|
+
__all__ = ["AgentGovernance", "StageMapper"]
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Agent Governance — shared lifecycle that all framework adapters inherit."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from aigp_client import (
|
|
7
|
+
AigpClient, TraceBuilder, SessionContext, TokenAccumulator,
|
|
8
|
+
ToolGovernance, DelegationToken, retry_on_rate_limit,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GovernanceBlockedError(Exception):
|
|
15
|
+
"""Raised when AIGP CHECK returns DENY."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgentGovernance:
|
|
20
|
+
"""Base class for governed agent adapters.
|
|
21
|
+
|
|
22
|
+
Handles the full AIGP lifecycle:
|
|
23
|
+
- pre_invoke: CHECK (is this agent allowed?)
|
|
24
|
+
- on_model_call: accumulate tokens
|
|
25
|
+
- on_tool_call: optional per-tool CHECK
|
|
26
|
+
- post_invoke: RECORD + TRACE + D-DNA evidence
|
|
27
|
+
- on_error: RECORD with ERROR status
|
|
28
|
+
|
|
29
|
+
Subclasses map framework-specific hooks to these methods.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, gov_url: str, app_id: str, hmac_secret: str, *,
|
|
33
|
+
vault_url: str = "", evidence_bucket: str = "",
|
|
34
|
+
consent_tier: str = "STANDARD", tool_policies: dict | None = None):
|
|
35
|
+
self._client = AigpClient(gov_url, app_id, hmac_secret)
|
|
36
|
+
self._app_id = app_id
|
|
37
|
+
self._session: SessionContext | None = None
|
|
38
|
+
self._tokens = TokenAccumulator()
|
|
39
|
+
self._trace: TraceBuilder | None = None
|
|
40
|
+
self._tools = ToolGovernance(self._client, tool_policies) if tool_policies else None
|
|
41
|
+
self._request_id = ""
|
|
42
|
+
self._model_id = ""
|
|
43
|
+
self._use_case = ""
|
|
44
|
+
self._t0 = 0.0
|
|
45
|
+
self._consent_tier = consent_tier
|
|
46
|
+
|
|
47
|
+
# Evidence writer (optional)
|
|
48
|
+
self._evidence = None
|
|
49
|
+
if vault_url and evidence_bucket:
|
|
50
|
+
try:
|
|
51
|
+
from amigo_bedrock import EvidenceWriter
|
|
52
|
+
self._evidence = EvidenceWriter(app_id, vault_url, evidence_bucket)
|
|
53
|
+
except ImportError:
|
|
54
|
+
logger.debug("amigo_bedrock not available, evidence writing disabled")
|
|
55
|
+
|
|
56
|
+
async def start(self) -> None:
|
|
57
|
+
"""Start governance — register app and begin heartbeat."""
|
|
58
|
+
try:
|
|
59
|
+
await self._client.start_heartbeat(3600)
|
|
60
|
+
logger.info("AIGP governance started: %s", self._app_id)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.warning("AIGP start failed (non-fatal): %s", e)
|
|
63
|
+
|
|
64
|
+
def pre_invoke(self, agent_name: str, model_id: str, user_id: str = "",
|
|
65
|
+
session: SessionContext | None = None) -> str:
|
|
66
|
+
"""Call before agent execution. Sends CHECK. Returns request_id.
|
|
67
|
+
|
|
68
|
+
Raises GovernanceBlockedError if DENY.
|
|
69
|
+
"""
|
|
70
|
+
import asyncio
|
|
71
|
+
|
|
72
|
+
self._t0 = time.time()
|
|
73
|
+
self._model_id = model_id
|
|
74
|
+
self._use_case = agent_name
|
|
75
|
+
self._tokens.reset()
|
|
76
|
+
self._session = session or SessionContext(user_id=user_id, consent_tier=self._consent_tier)
|
|
77
|
+
self._trace = TraceBuilder(request_id="")
|
|
78
|
+
|
|
79
|
+
if self._session.parent_session_id:
|
|
80
|
+
self._trace.link_parent(self._session.parent_session_id)
|
|
81
|
+
|
|
82
|
+
# S1: identity_session_initiation
|
|
83
|
+
self._trace.start(1)
|
|
84
|
+
self._trace.end(1, attributes={"user_id": user_id, "agent_name": agent_name})
|
|
85
|
+
|
|
86
|
+
# S3: request_registration (CHECK)
|
|
87
|
+
self._trace.start(3)
|
|
88
|
+
try:
|
|
89
|
+
decision = asyncio.get_event_loop().run_until_complete(
|
|
90
|
+
retry_on_rate_limit(lambda: self._client.check(use_case=agent_name, model_id=model_id))
|
|
91
|
+
)
|
|
92
|
+
except RuntimeError:
|
|
93
|
+
# No event loop — try sync
|
|
94
|
+
import concurrent.futures
|
|
95
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
96
|
+
decision = pool.submit(lambda: __import__('asyncio').run(
|
|
97
|
+
retry_on_rate_limit(lambda: self._client.check(use_case=agent_name, model_id=model_id))
|
|
98
|
+
)).result()
|
|
99
|
+
|
|
100
|
+
self._trace.end(3)
|
|
101
|
+
|
|
102
|
+
d = decision.get("decision", "ALLOW") if isinstance(decision, dict) else "ALLOW"
|
|
103
|
+
if d == "DENY":
|
|
104
|
+
raise GovernanceBlockedError(decision.get("reason", "Blocked by governance"))
|
|
105
|
+
|
|
106
|
+
self._request_id = decision.get("request_id", "") if isinstance(decision, dict) else ""
|
|
107
|
+
return self._request_id
|
|
108
|
+
|
|
109
|
+
def on_model_call(self, input_tokens: int = 0, output_tokens: int = 0, usage: dict | None = None) -> None:
|
|
110
|
+
"""Call after each model invocation with token counts."""
|
|
111
|
+
if usage:
|
|
112
|
+
self._tokens.add_from_usage(usage)
|
|
113
|
+
else:
|
|
114
|
+
self._tokens.add(input_tokens, output_tokens)
|
|
115
|
+
|
|
116
|
+
async def on_tool_call(self, tool_name: str, params: dict = None, result: str = "",
|
|
117
|
+
duration_ms: int = 0) -> dict:
|
|
118
|
+
"""Call after a tool execution. Optionally CHECKs tool permission."""
|
|
119
|
+
decision = {"decision": "ALLOW"}
|
|
120
|
+
if self._tools:
|
|
121
|
+
decision = await self._tools.check_tool(tool_name, params)
|
|
122
|
+
if decision.get("decision") == "DENY":
|
|
123
|
+
raise GovernanceBlockedError(f"Tool '{tool_name}' denied: {decision.get('reason')}")
|
|
124
|
+
|
|
125
|
+
if self._trace:
|
|
126
|
+
self._trace.add_tool_span(tool_name, duration_ms, attributes={"classification": decision.get("classification", "")})
|
|
127
|
+
|
|
128
|
+
return decision
|
|
129
|
+
|
|
130
|
+
async def post_invoke(self, status: str = "SUCCESS") -> None:
|
|
131
|
+
"""Call after agent session completes. Sends RECORD + TRACE + evidence."""
|
|
132
|
+
duration_ms = int((time.time() - self._t0) * 1000)
|
|
133
|
+
|
|
134
|
+
# RECORD
|
|
135
|
+
try:
|
|
136
|
+
await self._client.record(
|
|
137
|
+
use_case=self._use_case, model_id=self._model_id,
|
|
138
|
+
status=status, request_id=self._request_id,
|
|
139
|
+
input_tokens=self._tokens.total_input,
|
|
140
|
+
output_tokens=self._tokens.total_output,
|
|
141
|
+
duration_ms=duration_ms,
|
|
142
|
+
user_id=self._session.user_id if self._session else "",
|
|
143
|
+
session_id=self._session.session_id if self._session else "",
|
|
144
|
+
)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.warning("RECORD failed: %s", e)
|
|
147
|
+
|
|
148
|
+
# TRACE
|
|
149
|
+
if self._trace:
|
|
150
|
+
self._trace.start(14)
|
|
151
|
+
self._trace.end(14)
|
|
152
|
+
spans, summary = self._trace.build()
|
|
153
|
+
try:
|
|
154
|
+
await self._client.trace(
|
|
155
|
+
trace_id=self._trace.trace_id,
|
|
156
|
+
request_id=self._request_id,
|
|
157
|
+
use_case=self._use_case,
|
|
158
|
+
model_id=self._model_id,
|
|
159
|
+
user_id=self._session.user_id if self._session else "",
|
|
160
|
+
spans=spans, summary=summary,
|
|
161
|
+
session_id=self._session.session_id if self._session else "",
|
|
162
|
+
)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.warning("TRACE failed: %s", e)
|
|
165
|
+
|
|
166
|
+
# Evidence
|
|
167
|
+
if self._evidence:
|
|
168
|
+
try:
|
|
169
|
+
from datetime import datetime, timezone
|
|
170
|
+
await self._evidence.write({
|
|
171
|
+
"app_id": self._app_id, "request_id": self._request_id,
|
|
172
|
+
"use_case": self._use_case, "model_id": self._model_id,
|
|
173
|
+
"user_id": self._session.user_id if self._session else "",
|
|
174
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
175
|
+
"duration_ms": duration_ms, "status": status,
|
|
176
|
+
"tokens": self._tokens.to_dict(),
|
|
177
|
+
})
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.warning("Evidence write failed: %s", e)
|
|
180
|
+
|
|
181
|
+
async def on_error(self, error: Exception) -> None:
|
|
182
|
+
"""Call on agent error. Sends RECORD with ERROR status."""
|
|
183
|
+
await self.post_invoke(status="ERROR")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Stage Mapper — maps framework-specific events to AIGP 14+9 stages."""
|
|
2
|
+
|
|
3
|
+
from aigp_client import STAGE_NAMES
|
|
4
|
+
|
|
5
|
+
# Common mappings across frameworks
|
|
6
|
+
COMMON_MAPPINGS = {
|
|
7
|
+
"agent_start": 1, # identity_session_initiation
|
|
8
|
+
"consent_check": 2, # consent_policy_determination
|
|
9
|
+
"pre_check": 3, # request_registration
|
|
10
|
+
"prompt_build": 5, # prompt_assembly
|
|
11
|
+
"context_retrieve": 7, # context_retrieval
|
|
12
|
+
"model_select": 8, # model_selection
|
|
13
|
+
"model_invoke": 9, # runtime_invocation
|
|
14
|
+
"tool_auth": 10, # tool_agent_authorization
|
|
15
|
+
"reasoning": 11, # intermediate_reasoning
|
|
16
|
+
"output_gen": 12, # output_generation
|
|
17
|
+
"output_release": 13, # output_release
|
|
18
|
+
"record": 14, # post_invocation_record
|
|
19
|
+
# Agentic
|
|
20
|
+
"agent_bind": 18, # agent_identity_binding
|
|
21
|
+
"delegation": 19, # delegation_scope_assignment
|
|
22
|
+
"input_attest": 20, # input_attestation
|
|
23
|
+
"tool_runtime": 21, # tool_authorization_runtime
|
|
24
|
+
"plan_govern": 22, # execution_plan_governance
|
|
25
|
+
"autonomy_eval": 23, # autonomy_boundary_evaluation
|
|
26
|
+
"inter_agent": 24, # inter_agent_communication
|
|
27
|
+
"memory_gov": 25, # memory_governance
|
|
28
|
+
"circuit_break": 26, # circuit_breaker_evaluation
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Framework-specific mappings
|
|
32
|
+
STRANDS_MAPPINGS = {
|
|
33
|
+
"Strands Agent": 1,
|
|
34
|
+
"Cycle": 11,
|
|
35
|
+
"Model invoke": 9,
|
|
36
|
+
"Tool": 21,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
LANGCHAIN_MAPPINGS = {
|
|
40
|
+
"on_chain_start": 1,
|
|
41
|
+
"on_llm_start": 9,
|
|
42
|
+
"on_llm_end": 12,
|
|
43
|
+
"on_tool_start": 21,
|
|
44
|
+
"on_tool_end": 21,
|
|
45
|
+
"on_retriever_start": 7,
|
|
46
|
+
"on_chain_end": 13,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
CREWAI_MAPPINGS = {
|
|
50
|
+
"task_start": 22, # execution_plan_governance
|
|
51
|
+
"agent_start": 18, # agent_identity_binding
|
|
52
|
+
"tool_use": 21, # tool_authorization_runtime
|
|
53
|
+
"delegation": 19, # delegation_scope_assignment
|
|
54
|
+
"task_end": 13, # output_release
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class StageMapper:
|
|
59
|
+
"""Maps framework events to AIGP stage numbers."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, framework: str = "common"):
|
|
62
|
+
if framework == "strands":
|
|
63
|
+
self._map = STRANDS_MAPPINGS
|
|
64
|
+
elif framework == "langchain":
|
|
65
|
+
self._map = LANGCHAIN_MAPPINGS
|
|
66
|
+
elif framework == "crewai":
|
|
67
|
+
self._map = CREWAI_MAPPINGS
|
|
68
|
+
else:
|
|
69
|
+
self._map = COMMON_MAPPINGS
|
|
70
|
+
|
|
71
|
+
def stage_for(self, event_name: str) -> int | None:
|
|
72
|
+
"""Get AIGP stage number for a framework event. Returns None if unmapped."""
|
|
73
|
+
return self._map.get(event_name) or COMMON_MAPPINGS.get(event_name)
|
|
74
|
+
|
|
75
|
+
def stage_name(self, stage: int) -> str:
|
|
76
|
+
return STAGE_NAMES.get(stage, f"unknown_{stage}")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aigp-agent-core"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "AIGP Agent Governance Core — shared lifecycle for all framework adapters"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = ["aigp-client>=3.0.0"]
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
evidence = ["amigo-bedrock>=0.2.0"]
|