aeonic-agentguard-sdk-python 0.1.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.
- aeonic/__init__.py +5 -0
- aeonic/adapters/__init__.py +1 -0
- aeonic/adapters/django.py +275 -0
- aeonic/adapters/fastapi.py +337 -0
- aeonic/core/__init__.py +4 -0
- aeonic/core/agent_registry.py +209 -0
- aeonic/core/agent_status_cache.py +28 -0
- aeonic/core/agentguard/__init__.py +8 -0
- aeonic/core/agentguard/registration.py +93 -0
- aeonic/core/agentguard/sse_client.py +210 -0
- aeonic/core/assessment/__init__.py +5 -0
- aeonic/core/assessment/executor.py +207 -0
- aeonic/core/buffer.py +27 -0
- aeonic/core/init.py +218 -0
- aeonic/core/introspection.py +468 -0
- aeonic/core/sender.py +234 -0
- aeonic/core/types.py +51 -0
- aeonic/utils/__init__.py +1 -0
- aeonic/utils/safe_serialize.py +22 -0
- aeonic_agentguard_sdk_python-0.1.0.dist-info/METADATA +770 -0
- aeonic_agentguard_sdk_python-0.1.0.dist-info/RECORD +24 -0
- aeonic_agentguard_sdk_python-0.1.0.dist-info/WHEEL +5 -0
- aeonic_agentguard_sdk_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- aeonic_agentguard_sdk_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Assessment execution against local agent endpoints."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
import urllib.request
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("aeonic")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_endpoint() -> str:
|
|
14
|
+
"""Get the Aeonic endpoint from environment variable."""
|
|
15
|
+
return os.environ.get("AEONIC_ENDPOINT", "http://45.79.111.106:3291")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def execute_assessment(job: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
19
|
+
"""
|
|
20
|
+
Execute an assessment job and return results.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
job: Assessment job containing scenario_steps, agent_endpoint, and playbook.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
List of assessment results for each step.
|
|
27
|
+
"""
|
|
28
|
+
results: List[Dict[str, Any]] = []
|
|
29
|
+
start_time = time.time()
|
|
30
|
+
|
|
31
|
+
scenario_steps = job.get("scenario_steps", [])
|
|
32
|
+
agent_endpoint = job.get("agent_endpoint", {})
|
|
33
|
+
playbook = job.get("playbook", {})
|
|
34
|
+
timeout = playbook.get("timeout", 30)
|
|
35
|
+
|
|
36
|
+
logger.info(
|
|
37
|
+
f"[Aeonic] Executing assessment job {job.get('job_id')} with {len(scenario_steps)} steps"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
for step in scenario_steps:
|
|
41
|
+
step_start_time = time.time()
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
response = _call_agent_endpoint(
|
|
45
|
+
agent_endpoint,
|
|
46
|
+
step.get("input"),
|
|
47
|
+
timeout,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
results.append(
|
|
51
|
+
{
|
|
52
|
+
"step": step.get("step"),
|
|
53
|
+
"input": step.get("input"),
|
|
54
|
+
"output": {
|
|
55
|
+
"body": response.get("data"),
|
|
56
|
+
"status": response.get("status"),
|
|
57
|
+
"headers": response.get("headers", {}),
|
|
58
|
+
},
|
|
59
|
+
"timestamp": int(time.time() * 1000),
|
|
60
|
+
"status": "success",
|
|
61
|
+
"latency_ms": int((time.time() - step_start_time) * 1000),
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
except Exception as error:
|
|
65
|
+
logger.warning(f"[Aeonic] Assessment step {step.get('step')} failed: {error}")
|
|
66
|
+
results.append(
|
|
67
|
+
{
|
|
68
|
+
"step": step.get("step"),
|
|
69
|
+
"input": step.get("input"),
|
|
70
|
+
"error": str(error),
|
|
71
|
+
"timestamp": int(time.time() * 1000),
|
|
72
|
+
"status": "error",
|
|
73
|
+
"latency_ms": int((time.time() - step_start_time) * 1000),
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Submit results to T-LAIOR
|
|
78
|
+
execution_time = int((time.time() - start_time) * 1000)
|
|
79
|
+
_submit_assessment_results(job, results, execution_time)
|
|
80
|
+
|
|
81
|
+
return results
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _call_agent_endpoint(
|
|
85
|
+
endpoint: Dict[str, Any],
|
|
86
|
+
input_data: Any,
|
|
87
|
+
timeout: int,
|
|
88
|
+
) -> Dict[str, Any]:
|
|
89
|
+
"""
|
|
90
|
+
Call customer's agent endpoint locally.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
endpoint: Agent endpoint configuration (url, method, headers, auth).
|
|
94
|
+
input_data: Input data for the agent.
|
|
95
|
+
timeout: Request timeout in seconds.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Response dictionary with data, status, and headers.
|
|
99
|
+
"""
|
|
100
|
+
api_url = endpoint.get("url")
|
|
101
|
+
if not api_url:
|
|
102
|
+
raise ValueError("Agent endpoint URL is required")
|
|
103
|
+
|
|
104
|
+
method = endpoint.get("method", "POST")
|
|
105
|
+
headers: Dict[str, str] = {
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
**endpoint.get("headers", {}),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Add authentication
|
|
111
|
+
auth_type = endpoint.get("auth_type")
|
|
112
|
+
if auth_type == "bearer_token" and endpoint.get("token"):
|
|
113
|
+
headers["Authorization"] = f"Bearer {endpoint['token']}"
|
|
114
|
+
elif auth_type == "api_key" and endpoint.get("api_key"):
|
|
115
|
+
headers["X-API-Key"] = endpoint["api_key"]
|
|
116
|
+
|
|
117
|
+
data = json.dumps(input_data).encode("utf-8")
|
|
118
|
+
port = os.environ.get("PORT") or os.environ.get("APP_PORT") or "8000"
|
|
119
|
+
|
|
120
|
+
url = f"http://localhost:{port}{api_url}"
|
|
121
|
+
print("URL=================>", url)
|
|
122
|
+
|
|
123
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
124
|
+
|
|
125
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
126
|
+
response_body = response.read()
|
|
127
|
+
try:
|
|
128
|
+
response_data = json.loads(response_body)
|
|
129
|
+
except json.JSONDecodeError:
|
|
130
|
+
# If response is not JSON, return as string
|
|
131
|
+
response_data = response_body.decode("utf-8")
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"data": response_data,
|
|
135
|
+
"status": response.getcode(),
|
|
136
|
+
"headers": dict(response.headers),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _submit_assessment_results(
|
|
141
|
+
job: Dict[str, Any],
|
|
142
|
+
results: List[Dict[str, Any]],
|
|
143
|
+
execution_time: int,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Submit assessment results to T-LAIOR.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
job: Original assessment job.
|
|
150
|
+
results: List of assessment results.
|
|
151
|
+
execution_time: Total execution time in milliseconds.
|
|
152
|
+
"""
|
|
153
|
+
from aeonic.core.agentguard.registration import get_agent_guard_credentials
|
|
154
|
+
|
|
155
|
+
credentials = get_agent_guard_credentials()
|
|
156
|
+
if not credentials:
|
|
157
|
+
logger.error("[Aeonic] Cannot submit results: AgentGuard not registered")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
endpoint = _get_endpoint()
|
|
161
|
+
url = f"{endpoint}/assessments/results"
|
|
162
|
+
|
|
163
|
+
payload = {
|
|
164
|
+
"job_id": job["job_id"],
|
|
165
|
+
"agentguard_id": credentials["agentguard_id"],
|
|
166
|
+
"agent_id": job.get("agent_id"),
|
|
167
|
+
"scenario_id": job.get("scenario_id"),
|
|
168
|
+
"results": results,
|
|
169
|
+
"metadata": {
|
|
170
|
+
"execution_time_ms": execution_time,
|
|
171
|
+
"total_steps": len(results),
|
|
172
|
+
"successful_steps": sum(1 for r in results if r.get("status") == "success"),
|
|
173
|
+
"failed_steps": sum(1 for r in results if r.get("status") == "error"),
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
print("PAYLOAD=================>", payload)
|
|
178
|
+
print("URL=================>", url)
|
|
179
|
+
print("CREDENTIALS=================>", credentials)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
data = json.dumps(payload).encode("utf-8")
|
|
183
|
+
req = urllib.request.Request(
|
|
184
|
+
url,
|
|
185
|
+
data=data,
|
|
186
|
+
headers={
|
|
187
|
+
"Authorization": f"Bearer {credentials['auth_token']}",
|
|
188
|
+
"Content-Type": "application/json",
|
|
189
|
+
},
|
|
190
|
+
method="POST",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
194
|
+
response_data = json.loads(response.read())
|
|
195
|
+
if response_data.get("success"):
|
|
196
|
+
logger.info(
|
|
197
|
+
f"[Aeonic] Assessment results submitted successfully "
|
|
198
|
+
f"for job {job.get('job_id')}"
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
logger.warning(
|
|
202
|
+
f"[Aeonic] Assessment results submission failed: {response_data.get('message')}"
|
|
203
|
+
)
|
|
204
|
+
except Exception as error:
|
|
205
|
+
logger.error(
|
|
206
|
+
f"[Aeonic] Failed to submit assessment results for job {job.get('job_id')}: {error}"
|
|
207
|
+
)
|
aeonic/core/buffer.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from aeonic.core.types import AgentContext, RouteContext, Sample
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SampleBuffer:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
agent_key: str,
|
|
10
|
+
agent_context: AgentContext,
|
|
11
|
+
route: RouteContext,
|
|
12
|
+
max_samples: int,
|
|
13
|
+
):
|
|
14
|
+
self.agent_key = agent_key
|
|
15
|
+
self.agent_context = agent_context
|
|
16
|
+
self.route = route
|
|
17
|
+
self.max_samples = max_samples
|
|
18
|
+
self._samples: List[Sample] = []
|
|
19
|
+
|
|
20
|
+
def add(self, sample: Sample) -> bool:
|
|
21
|
+
self._samples.append(sample)
|
|
22
|
+
return len(self._samples) >= self.max_samples
|
|
23
|
+
|
|
24
|
+
def flush(self) -> List[Sample]:
|
|
25
|
+
samples = self._samples
|
|
26
|
+
self._samples = []
|
|
27
|
+
return samples
|
aeonic/core/init.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# aeonic/core/init.py
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("aeonic")
|
|
9
|
+
|
|
10
|
+
_CONFIG: Optional[Dict[str, Any]] = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def init(config: Dict[str, Any]) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Initialize Aeonic SDK.
|
|
16
|
+
|
|
17
|
+
Required:
|
|
18
|
+
- api_key
|
|
19
|
+
|
|
20
|
+
Optional:
|
|
21
|
+
- app: FastAPI or Django app instance (for route introspection)
|
|
22
|
+
- service_name: Service identifier
|
|
23
|
+
- capture_payloads.max_samples: Number of samples to buffer
|
|
24
|
+
- introspection_delay_ms: Delay before route introspection (default: 2000ms)
|
|
25
|
+
"""
|
|
26
|
+
global _CONFIG
|
|
27
|
+
|
|
28
|
+
if "api_key" not in config:
|
|
29
|
+
raise ValueError("Aeonic init requires `api_key`")
|
|
30
|
+
|
|
31
|
+
_CONFIG = {
|
|
32
|
+
"api_key": config["api_key"],
|
|
33
|
+
"service_name": config.get("service_name"),
|
|
34
|
+
"max_samples": (config.get("capture_payloads", {}) or {}).get("max_samples", 10),
|
|
35
|
+
"app": config.get("app"),
|
|
36
|
+
"introspection_delay_ms": config.get("introspection_delay_ms", 2000),
|
|
37
|
+
"agentguard_enabled": config.get("agentguard_enabled", True),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
logger.info("[Aeonic] SDK initialized.")
|
|
41
|
+
|
|
42
|
+
# Register as AgentGuard (non-blocking, optional)
|
|
43
|
+
if _CONFIG.get("agentguard_enabled", True):
|
|
44
|
+
_register_agentguard_async()
|
|
45
|
+
|
|
46
|
+
# Schedule deferred route introspection
|
|
47
|
+
# For Django, we can introspect even without app (uses get_resolver())
|
|
48
|
+
# For FastAPI, app is required
|
|
49
|
+
delay_ms = _CONFIG.get("introspection_delay_ms", 2000)
|
|
50
|
+
|
|
51
|
+
# Check if we should run introspection
|
|
52
|
+
app = _CONFIG.get("app")
|
|
53
|
+
should_introspect = app is not None
|
|
54
|
+
|
|
55
|
+
# For Django, try to detect and run introspection even without app
|
|
56
|
+
if app is None:
|
|
57
|
+
try:
|
|
58
|
+
import django # noqa: F401
|
|
59
|
+
|
|
60
|
+
# Django is installed, try to introspect
|
|
61
|
+
should_introspect = True
|
|
62
|
+
except ImportError:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
if should_introspect:
|
|
66
|
+
_schedule_introspection(delay_ms)
|
|
67
|
+
else:
|
|
68
|
+
# Fallback: emit inventory if agents already registered (legacy behavior)
|
|
69
|
+
_emit_inventory_if_ready()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_config() -> Dict[str, Any]:
|
|
73
|
+
if _CONFIG is None:
|
|
74
|
+
raise RuntimeError("Aeonic SDK not initialized. Call init() before using the SDK.")
|
|
75
|
+
return _CONFIG
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _schedule_introspection(delay_ms: int) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Schedule deferred route introspection in a background thread.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
delay_ms: Delay in milliseconds before introspection runs
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def deferred_introspection() -> None:
|
|
87
|
+
try:
|
|
88
|
+
# Wait for routes to be defined
|
|
89
|
+
time.sleep(delay_ms / 1000.0)
|
|
90
|
+
|
|
91
|
+
app = _CONFIG.get("app") if _CONFIG else None
|
|
92
|
+
|
|
93
|
+
# For Django, we can introspect even without app (uses get_resolver())
|
|
94
|
+
# For FastAPI, app is required
|
|
95
|
+
if app is None:
|
|
96
|
+
# Try Django introspection
|
|
97
|
+
try:
|
|
98
|
+
import django # noqa: F401
|
|
99
|
+
|
|
100
|
+
from aeonic.core.introspection import introspect_routes
|
|
101
|
+
|
|
102
|
+
# Pass None for Django - introspection will use get_resolver()
|
|
103
|
+
introspect_routes(None)
|
|
104
|
+
except ImportError:
|
|
105
|
+
logger.error(
|
|
106
|
+
"[Aeonic] No app provided and Django not detected - skipping route introspection"
|
|
107
|
+
)
|
|
108
|
+
return
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"[Aeonic] Django introspection failed: {e}")
|
|
111
|
+
_emit_inventory_if_ready()
|
|
112
|
+
return
|
|
113
|
+
else:
|
|
114
|
+
# Perform introspection with app instance
|
|
115
|
+
from aeonic.core.introspection import introspect_routes
|
|
116
|
+
|
|
117
|
+
introspect_routes(app)
|
|
118
|
+
|
|
119
|
+
# Emit inventory after introspection
|
|
120
|
+
_emit_inventory_if_ready()
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.warning(f"[Aeonic] Route introspection failed: {e}")
|
|
124
|
+
import traceback
|
|
125
|
+
|
|
126
|
+
logger.error(f"[Aeonic] Traceback: {traceback.format_exc()}")
|
|
127
|
+
# Fallback: emit inventory with whatever agents were registered manually
|
|
128
|
+
_emit_inventory_if_ready()
|
|
129
|
+
|
|
130
|
+
# Run in background daemon thread (non-blocking)
|
|
131
|
+
thread = threading.Thread(target=deferred_introspection, daemon=True)
|
|
132
|
+
thread.start()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _emit_inventory_if_ready() -> None:
|
|
136
|
+
"""
|
|
137
|
+
Emit agent inventory if:
|
|
138
|
+
- SDK is initialized
|
|
139
|
+
- Agents are registered
|
|
140
|
+
- Inventory has not been emitted yet
|
|
141
|
+
|
|
142
|
+
This function never throws.
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
# Avoid circular import
|
|
146
|
+
from aeonic.core.agent_registry import (
|
|
147
|
+
get_all_agents,
|
|
148
|
+
mark_inventory_emitted,
|
|
149
|
+
should_emit_inventory,
|
|
150
|
+
)
|
|
151
|
+
from aeonic.core.sender import emit_agent_inventory
|
|
152
|
+
|
|
153
|
+
if _CONFIG is None:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
if not should_emit_inventory():
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
agents = get_all_agents()
|
|
160
|
+
if not agents:
|
|
161
|
+
logger.warning("[Aeonic] No agents found during introspection")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# Detect framework from registered agents
|
|
165
|
+
framework = agents[0]["framework"] if agents else "unknown"
|
|
166
|
+
|
|
167
|
+
# Convert to inventory format
|
|
168
|
+
inventory_agents = []
|
|
169
|
+
for agent in agents:
|
|
170
|
+
inventory_agents.append(
|
|
171
|
+
{
|
|
172
|
+
"name": agent.get("name"),
|
|
173
|
+
"type": agent.get("type"),
|
|
174
|
+
"model": agent.get("model", []),
|
|
175
|
+
"route_path": agent.get("route_path"),
|
|
176
|
+
"http_method": agent.get("http_method"),
|
|
177
|
+
"framework": agent.get("framework"),
|
|
178
|
+
"declared_at": int(agent.get("declared_at", 0) * 1000), # Convert to ms
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
payload: Dict[str, Any] = {
|
|
183
|
+
"event": "agent_inventory",
|
|
184
|
+
"api_key": _CONFIG["api_key"],
|
|
185
|
+
"service_name": _CONFIG.get("service_name"),
|
|
186
|
+
"framework": framework,
|
|
187
|
+
"agents": inventory_agents,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if emit_agent_inventory(payload):
|
|
191
|
+
mark_inventory_emitted()
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
# Never throw - silent failure
|
|
195
|
+
logger.warning(f"[Aeonic] Failed to emit inventory: {e}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _register_agentguard_async() -> None:
|
|
199
|
+
"""
|
|
200
|
+
Register SDK as AgentGuard instance with T-LAIOR.
|
|
201
|
+
Runs in background thread and never blocks or throws.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def register_async() -> None:
|
|
205
|
+
try:
|
|
206
|
+
from aeonic.core.agentguard.registration import register_agent_guard
|
|
207
|
+
|
|
208
|
+
register_agent_guard()
|
|
209
|
+
except Exception as error:
|
|
210
|
+
logger.warning(
|
|
211
|
+
f"[Aeonic] AgentGuard registration failed: {error}. "
|
|
212
|
+
"SDK will continue to function for sample collection."
|
|
213
|
+
)
|
|
214
|
+
# Don't raise - SDK can still function for sample collection
|
|
215
|
+
|
|
216
|
+
# Run in background daemon thread (non-blocking)
|
|
217
|
+
thread = threading.Thread(target=register_async, daemon=True)
|
|
218
|
+
thread.start()
|