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.
@@ -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()