aegiscore-ai 0.1.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.
- aegiscore_ai-0.1.0/LICENSE +17 -0
- aegiscore_ai-0.1.0/PKG-INFO +124 -0
- aegiscore_ai-0.1.0/README.md +97 -0
- aegiscore_ai-0.1.0/aegiscore/__init__.py +26 -0
- aegiscore_ai-0.1.0/aegiscore/client.py +222 -0
- aegiscore_ai-0.1.0/aegiscore/exceptions.py +41 -0
- aegiscore_ai-0.1.0/aegiscore/integrations/__init__.py +4 -0
- aegiscore_ai-0.1.0/aegiscore/integrations/langgraph.py +42 -0
- aegiscore_ai-0.1.0/aegiscore/integrations/openai.py +61 -0
- aegiscore_ai-0.1.0/aegiscore/models.py +39 -0
- aegiscore_ai-0.1.0/aegiscore_ai.egg-info/PKG-INFO +124 -0
- aegiscore_ai-0.1.0/aegiscore_ai.egg-info/SOURCES.txt +15 -0
- aegiscore_ai-0.1.0/aegiscore_ai.egg-info/dependency_links.txt +1 -0
- aegiscore_ai-0.1.0/aegiscore_ai.egg-info/top_level.txt +1 -0
- aegiscore_ai-0.1.0/pyproject.toml +44 -0
- aegiscore_ai-0.1.0/setup.cfg +4 -0
- aegiscore_ai-0.1.0/tests/test_client.py +196 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 AegisCore Contributors
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aegiscore-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic Agent Governance Gateway - Zero-Trust Policy Enclave for AI Agents
|
|
5
|
+
Author: Sandeep Hipparagi & AegisCore Contributors
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/Sandeep-Hipparagi/aegis-core
|
|
8
|
+
Project-URL: Repository, https://github.com/Sandeep-Hipparagi/aegis-core.git
|
|
9
|
+
Project-URL: Issues, https://github.com/Sandeep-Hipparagi/aegis-core/issues
|
|
10
|
+
Keywords: ai-agent,governance,security,zero-trust,langgraph,openai,mcp,dpdp
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# aegiscore-py
|
|
29
|
+
|
|
30
|
+
Official, zero-dependency Python client for **AegisCore** — the Deterministic Zero-Trust Policy & Cryptographic WORM Audit Gateway for AI Agents.
|
|
31
|
+
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
[](https://pypi.org/project/aegiscore-ai/)
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install aegiscore-ai
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
*Note: `aegiscore-ai` is 100% zero-dependency, implemented purely using Python's standard library (`urllib.request`, `json`, `dataclasses`).*
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### 1. Direct Tool Proposal Dispatch
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from aegiscore import AegisGateway
|
|
49
|
+
|
|
50
|
+
gateway = AegisGateway("http://localhost:8080")
|
|
51
|
+
|
|
52
|
+
# Dispatch tool intent: evaluates schema, invariants, tokenizes PII, logs WORM audit
|
|
53
|
+
result = gateway.dispatch(
|
|
54
|
+
tool="transfer_funds",
|
|
55
|
+
arguments={"amount": 2500, "recipient": "ACC-PAYROLL"},
|
|
56
|
+
session_id="sess_01"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if result.is_success:
|
|
60
|
+
print("Action Approved & Executed:", result.output)
|
|
61
|
+
elif result.requires_hitl:
|
|
62
|
+
print(f"Action escalated to human approval. Ticket ID: {result.ticket_id}")
|
|
63
|
+
else:
|
|
64
|
+
print(f"Action BLOCKED: {result.reason}")
|
|
65
|
+
for violation in result.violations:
|
|
66
|
+
print(f" - {violation}")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. LangGraph & LangChain Integration
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from aegiscore import AegisGateway
|
|
73
|
+
from aegiscore.integrations import AegisLangGraphWrapper
|
|
74
|
+
|
|
75
|
+
gateway = AegisGateway("http://localhost:8080")
|
|
76
|
+
wrapper = AegisLangGraphWrapper(gateway=gateway)
|
|
77
|
+
|
|
78
|
+
# Wrap any native function or tool
|
|
79
|
+
@wrapper.wrap_tool("query_database", raw_query_func)
|
|
80
|
+
def safe_query_database(query: str):
|
|
81
|
+
return raw_query_func(query)
|
|
82
|
+
|
|
83
|
+
# Calls are intercepted and verified against policy invariants before invocation
|
|
84
|
+
safe_query_database(query="SELECT * FROM users")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 3. OpenAI Function Call Interception
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from aegiscore import AegisGateway
|
|
91
|
+
from aegiscore.integrations import AegisOpenAIInterceptor
|
|
92
|
+
|
|
93
|
+
gateway = AegisGateway("http://localhost:8080")
|
|
94
|
+
interceptor = AegisOpenAIInterceptor(gateway=gateway)
|
|
95
|
+
|
|
96
|
+
# In your chat completion loop:
|
|
97
|
+
for tool_call in response.choices[0].message.tool_calls:
|
|
98
|
+
# Evaluates policy and returns tool response dict formatted for OpenAI
|
|
99
|
+
tool_message = interceptor.intercept_tool_call(tool_call)
|
|
100
|
+
messages.append(tool_message)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 4. Human-in-the-Loop (HITL) Resolution
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
# Check pending ticket and resolve with cryptographic signature
|
|
107
|
+
gateway.resolve_hitl(
|
|
108
|
+
ticket_id="ticket-12345",
|
|
109
|
+
decision="APPROVED",
|
|
110
|
+
signature="sig-supervisor-ed25519"
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 5. Forensic Cryptographic Ledger Verification
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# Verifies the tamper-evident WORM Merkle chain
|
|
118
|
+
audit = gateway.verify_audit_integrity()
|
|
119
|
+
print(f"Ledger Valid: {audit['isValid']}")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
Apache-2.0
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# aegiscore-py
|
|
2
|
+
|
|
3
|
+
Official, zero-dependency Python client for **AegisCore** — the Deterministic Zero-Trust Policy & Cryptographic WORM Audit Gateway for AI Agents.
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://pypi.org/project/aegiscore-ai/)
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install aegiscore-ai
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
*Note: `aegiscore-ai` is 100% zero-dependency, implemented purely using Python's standard library (`urllib.request`, `json`, `dataclasses`).*
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### 1. Direct Tool Proposal Dispatch
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from aegiscore import AegisGateway
|
|
22
|
+
|
|
23
|
+
gateway = AegisGateway("http://localhost:8080")
|
|
24
|
+
|
|
25
|
+
# Dispatch tool intent: evaluates schema, invariants, tokenizes PII, logs WORM audit
|
|
26
|
+
result = gateway.dispatch(
|
|
27
|
+
tool="transfer_funds",
|
|
28
|
+
arguments={"amount": 2500, "recipient": "ACC-PAYROLL"},
|
|
29
|
+
session_id="sess_01"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if result.is_success:
|
|
33
|
+
print("Action Approved & Executed:", result.output)
|
|
34
|
+
elif result.requires_hitl:
|
|
35
|
+
print(f"Action escalated to human approval. Ticket ID: {result.ticket_id}")
|
|
36
|
+
else:
|
|
37
|
+
print(f"Action BLOCKED: {result.reason}")
|
|
38
|
+
for violation in result.violations:
|
|
39
|
+
print(f" - {violation}")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. LangGraph & LangChain Integration
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from aegiscore import AegisGateway
|
|
46
|
+
from aegiscore.integrations import AegisLangGraphWrapper
|
|
47
|
+
|
|
48
|
+
gateway = AegisGateway("http://localhost:8080")
|
|
49
|
+
wrapper = AegisLangGraphWrapper(gateway=gateway)
|
|
50
|
+
|
|
51
|
+
# Wrap any native function or tool
|
|
52
|
+
@wrapper.wrap_tool("query_database", raw_query_func)
|
|
53
|
+
def safe_query_database(query: str):
|
|
54
|
+
return raw_query_func(query)
|
|
55
|
+
|
|
56
|
+
# Calls are intercepted and verified against policy invariants before invocation
|
|
57
|
+
safe_query_database(query="SELECT * FROM users")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 3. OpenAI Function Call Interception
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from aegiscore import AegisGateway
|
|
64
|
+
from aegiscore.integrations import AegisOpenAIInterceptor
|
|
65
|
+
|
|
66
|
+
gateway = AegisGateway("http://localhost:8080")
|
|
67
|
+
interceptor = AegisOpenAIInterceptor(gateway=gateway)
|
|
68
|
+
|
|
69
|
+
# In your chat completion loop:
|
|
70
|
+
for tool_call in response.choices[0].message.tool_calls:
|
|
71
|
+
# Evaluates policy and returns tool response dict formatted for OpenAI
|
|
72
|
+
tool_message = interceptor.intercept_tool_call(tool_call)
|
|
73
|
+
messages.append(tool_message)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 4. Human-in-the-Loop (HITL) Resolution
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# Check pending ticket and resolve with cryptographic signature
|
|
80
|
+
gateway.resolve_hitl(
|
|
81
|
+
ticket_id="ticket-12345",
|
|
82
|
+
decision="APPROVED",
|
|
83
|
+
signature="sig-supervisor-ed25519"
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 5. Forensic Cryptographic Ledger Verification
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
# Verifies the tamper-evident WORM Merkle chain
|
|
91
|
+
audit = gateway.verify_audit_integrity()
|
|
92
|
+
print(f"Ledger Valid: {audit['isValid']}")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
Apache-2.0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AegisCore Python Client SDK
|
|
3
|
+
Zero-dependency client for deterministic agent governance.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .client import AegisGateway, AegisClient
|
|
7
|
+
from .models import DispatchResult
|
|
8
|
+
from .exceptions import (
|
|
9
|
+
AegisError,
|
|
10
|
+
AegisGatewayError,
|
|
11
|
+
AegisPolicyBlockedError,
|
|
12
|
+
AegisSchemaError,
|
|
13
|
+
AegisHitlRequiredError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AegisGateway",
|
|
19
|
+
"AegisClient",
|
|
20
|
+
"DispatchResult",
|
|
21
|
+
"AegisError",
|
|
22
|
+
"AegisGatewayError",
|
|
23
|
+
"AegisPolicyBlockedError",
|
|
24
|
+
"AegisSchemaError",
|
|
25
|
+
"AegisHitlRequiredError",
|
|
26
|
+
]
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AegisCore Python Client - Zero-dependency gateway wrapper
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import urllib.request
|
|
7
|
+
import urllib.error
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union
|
|
9
|
+
import uuid
|
|
10
|
+
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
AegisGatewayError,
|
|
13
|
+
AegisPolicyBlockedError,
|
|
14
|
+
AegisSchemaError,
|
|
15
|
+
AegisHitlRequiredError,
|
|
16
|
+
)
|
|
17
|
+
from .models import DispatchResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AegisGateway:
|
|
21
|
+
"""
|
|
22
|
+
Zero-dependency Python client for the AegisCore Deterministic Governance Gateway.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
base_url: str = "http://localhost:8080",
|
|
28
|
+
agent_id: str = "python-agent",
|
|
29
|
+
role: str = "operator",
|
|
30
|
+
default_headers: Optional[Dict[str, str]] = None,
|
|
31
|
+
timeout: float = 30.0,
|
|
32
|
+
):
|
|
33
|
+
self.base_url = base_url.rstrip("/")
|
|
34
|
+
self.agent_id = agent_id
|
|
35
|
+
self.role = role
|
|
36
|
+
self.default_headers = default_headers or {}
|
|
37
|
+
self.timeout = timeout
|
|
38
|
+
|
|
39
|
+
def _request(
|
|
40
|
+
self,
|
|
41
|
+
method: str,
|
|
42
|
+
path: str,
|
|
43
|
+
data: Optional[Dict[str, Any]] = None,
|
|
44
|
+
headers: Optional[Dict[str, str]] = None,
|
|
45
|
+
) -> Dict[str, Any]:
|
|
46
|
+
"""Internal helper for standard HTTP requests using urllib."""
|
|
47
|
+
url = f"{self.base_url}{path}"
|
|
48
|
+
req_headers = {
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
"User-Agent": "aegiscore-python/0.1.0",
|
|
51
|
+
**self.default_headers,
|
|
52
|
+
}
|
|
53
|
+
if headers:
|
|
54
|
+
req_headers.update(headers)
|
|
55
|
+
|
|
56
|
+
body_bytes = json.dumps(data).encode("utf-8") if data is not None else None
|
|
57
|
+
req = urllib.request.Request(url, data=body_bytes, headers=req_headers, method=method)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as response:
|
|
61
|
+
resp_text = response.read().decode("utf-8")
|
|
62
|
+
return json.loads(resp_text) if resp_text else {}
|
|
63
|
+
except urllib.error.HTTPError as err:
|
|
64
|
+
err_text = err.read().decode("utf-8") if err.fp else ""
|
|
65
|
+
try:
|
|
66
|
+
parsed = json.loads(err_text)
|
|
67
|
+
except Exception:
|
|
68
|
+
parsed = None
|
|
69
|
+
|
|
70
|
+
# 403 Forbidden is used for BLOCKED actions
|
|
71
|
+
if err.code == 403 and parsed and isinstance(parsed, dict):
|
|
72
|
+
return parsed
|
|
73
|
+
|
|
74
|
+
# 400 Bad Request is used for SCHEMA_ERROR
|
|
75
|
+
if err.code == 400 and parsed and isinstance(parsed, dict) and parsed.get("status") == "SCHEMA_ERROR":
|
|
76
|
+
return parsed
|
|
77
|
+
|
|
78
|
+
raise AegisGatewayError(
|
|
79
|
+
f"AegisCore Gateway HTTP {err.code}: {err.reason} - {err_text}",
|
|
80
|
+
status_code=err.code,
|
|
81
|
+
response_body=err_text,
|
|
82
|
+
) from err
|
|
83
|
+
except urllib.error.URLError as err:
|
|
84
|
+
raise AegisGatewayError(f"Connection to AegisCore Gateway failed ({url}): {err.reason}") from err
|
|
85
|
+
|
|
86
|
+
def dispatch(
|
|
87
|
+
self,
|
|
88
|
+
tool: str,
|
|
89
|
+
arguments: Optional[Dict[str, Any]] = None,
|
|
90
|
+
session_id: Optional[str] = None,
|
|
91
|
+
agent_id: Optional[str] = None,
|
|
92
|
+
role: Optional[str] = None,
|
|
93
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
94
|
+
raise_on_blocked: bool = False,
|
|
95
|
+
) -> DispatchResult:
|
|
96
|
+
"""
|
|
97
|
+
Dispatches a proposed tool invocation to AegisCore for invariant checking,
|
|
98
|
+
DPDP PII tokenization, policy evaluation, WORM audit logging, and sandbox execution.
|
|
99
|
+
"""
|
|
100
|
+
sid = session_id or f"sess-{uuid.uuid4().hex[:12]}"
|
|
101
|
+
aid = agent_id or self.agent_id
|
|
102
|
+
r = role or self.role
|
|
103
|
+
params = arguments if arguments is not None else {}
|
|
104
|
+
|
|
105
|
+
payload = {
|
|
106
|
+
"tool": tool,
|
|
107
|
+
"parameters": params,
|
|
108
|
+
"metadata": metadata or {},
|
|
109
|
+
"context": {
|
|
110
|
+
"sessionId": sid,
|
|
111
|
+
"agentId": aid,
|
|
112
|
+
"role": r,
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
raw = self._request("POST", "/v1/gateway/dispatch", data=payload)
|
|
117
|
+
status = raw.get("status", "UNKNOWN")
|
|
118
|
+
reason = raw.get("reason")
|
|
119
|
+
violations = raw.get("violations", [])
|
|
120
|
+
challenge = raw.get("challenge")
|
|
121
|
+
audit_entry = raw.get("auditEntry")
|
|
122
|
+
|
|
123
|
+
output = None
|
|
124
|
+
if "execution" in raw and isinstance(raw["execution"], dict):
|
|
125
|
+
output = raw["execution"].get("output")
|
|
126
|
+
|
|
127
|
+
result = DispatchResult(
|
|
128
|
+
status=status,
|
|
129
|
+
intent_id=raw.get("intentId"),
|
|
130
|
+
session_id=sid,
|
|
131
|
+
tool=tool,
|
|
132
|
+
reason=reason,
|
|
133
|
+
output=output,
|
|
134
|
+
violations=violations,
|
|
135
|
+
challenge=challenge,
|
|
136
|
+
audit_entry=audit_entry,
|
|
137
|
+
raw=raw,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if raise_on_blocked and status == "BLOCKED":
|
|
141
|
+
raise AegisPolicyBlockedError(
|
|
142
|
+
f"Tool invocation '{tool}' was BLOCKED: {reason}",
|
|
143
|
+
violations=violations,
|
|
144
|
+
raw_result=raw,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if raise_on_blocked and status == "SCHEMA_ERROR":
|
|
148
|
+
raise AegisSchemaError(
|
|
149
|
+
f"Tool invocation '{tool}' failed schema validation: {reason}",
|
|
150
|
+
raw_result=raw,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
def evaluate_intent(
|
|
156
|
+
self,
|
|
157
|
+
tool: str,
|
|
158
|
+
parameters: Optional[Dict[str, Any]] = None,
|
|
159
|
+
session_id: Optional[str] = None,
|
|
160
|
+
agent_id: Optional[str] = None,
|
|
161
|
+
role: Optional[str] = None,
|
|
162
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
163
|
+
) -> Dict[str, Any]:
|
|
164
|
+
"""Submits an intent to /v1/intent/evaluate returning canonical IntentEvaluationResponse."""
|
|
165
|
+
aid = agent_id or self.agent_id
|
|
166
|
+
payload = {
|
|
167
|
+
"tool": tool,
|
|
168
|
+
"parameters": parameters or {},
|
|
169
|
+
"metadata": metadata or {},
|
|
170
|
+
"agent": {
|
|
171
|
+
"agentId": aid,
|
|
172
|
+
"role": role or self.role,
|
|
173
|
+
"sessionId": session_id or f"sess-{uuid.uuid4().hex[:12]}",
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
return self._request("POST", "/v1/intent/evaluate", data=payload, headers={"X-Agent-Id": aid})
|
|
177
|
+
|
|
178
|
+
def execute_tool(
|
|
179
|
+
self,
|
|
180
|
+
tool_call_id: str,
|
|
181
|
+
name: str,
|
|
182
|
+
arguments: Union[str, Dict[str, Any]],
|
|
183
|
+
agent_id: Optional[str] = None,
|
|
184
|
+
role: Optional[str] = None,
|
|
185
|
+
) -> Dict[str, Any]:
|
|
186
|
+
"""Proxies an OpenAI tool call to /v1/tools/execute."""
|
|
187
|
+
aid = agent_id or self.agent_id
|
|
188
|
+
payload = {
|
|
189
|
+
"tool_call_id": tool_call_id,
|
|
190
|
+
"name": name,
|
|
191
|
+
"arguments": arguments,
|
|
192
|
+
"agent_id": aid,
|
|
193
|
+
"role": role or self.role,
|
|
194
|
+
}
|
|
195
|
+
return self._request("POST", "/v1/tools/execute", data=payload, headers={"x-agent-id": aid})
|
|
196
|
+
|
|
197
|
+
def resolve_hitl(
|
|
198
|
+
self,
|
|
199
|
+
ticket_id: str,
|
|
200
|
+
decision: str = "APPROVED",
|
|
201
|
+
signature: str = "sig-officer-signature",
|
|
202
|
+
scoped_credentials: Optional[Dict[str, str]] = None,
|
|
203
|
+
) -> Dict[str, Any]:
|
|
204
|
+
"""Resolves an asynchronous Human-in-the-Loop challenge ticket."""
|
|
205
|
+
payload = {
|
|
206
|
+
"decision": decision,
|
|
207
|
+
"approverSignature": signature,
|
|
208
|
+
"scopedCredentials": scoped_credentials or {},
|
|
209
|
+
}
|
|
210
|
+
return self._request("POST", f"/v1/hitl/challenges/{ticket_id}/resolve", data=payload)
|
|
211
|
+
|
|
212
|
+
def verify_audit_integrity(self) -> Dict[str, Any]:
|
|
213
|
+
"""Queries the cryptographic WORM ledger to verify Merkle chain integrity."""
|
|
214
|
+
return self._request("GET", "/v1/audit/integrity")
|
|
215
|
+
|
|
216
|
+
def health(self) -> Dict[str, Any]:
|
|
217
|
+
"""Checks gateway daemon health."""
|
|
218
|
+
return self._request("GET", "/health")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# Alias for developer ergonomics
|
|
222
|
+
AegisClient = AegisGateway
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AegisCore Python Exceptions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AegisError(Exception):
|
|
9
|
+
"""Base exception for all AegisCore errors."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AegisGatewayError(AegisError):
|
|
14
|
+
"""Raised when the AegisCore gateway returns an HTTP or network error."""
|
|
15
|
+
def __init__(self, message: str, status_code: Optional[int] = None, response_body: Optional[str] = None):
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.response_body = response_body
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AegisPolicyBlockedError(AegisError):
|
|
22
|
+
"""Raised when an action is blocked by a deterministic policy invariant."""
|
|
23
|
+
def __init__(self, message: str, violations: Optional[List[Dict[str, Any]]] = None, raw_result: Optional[Dict[str, Any]] = None):
|
|
24
|
+
super().__init__(message)
|
|
25
|
+
self.violations = violations or []
|
|
26
|
+
self.raw_result = raw_result or {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AegisSchemaError(AegisError):
|
|
30
|
+
"""Raised when a tool argument payload fails fail-closed schema validation."""
|
|
31
|
+
def __init__(self, message: str, raw_result: Optional[Dict[str, Any]] = None):
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.raw_result = raw_result or {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AegisHitlRequiredError(AegisError):
|
|
37
|
+
"""Raised when an action requires human-in-the-loop escalation before execution."""
|
|
38
|
+
def __init__(self, message: str, ticket_id: str, challenge: Dict[str, Any]):
|
|
39
|
+
super().__init__(message)
|
|
40
|
+
self.ticket_id = ticket_id
|
|
41
|
+
self.challenge = challenge
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LangGraph and LangChain Integration for AegisCore
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict, Optional
|
|
6
|
+
from ..client import AegisGateway
|
|
7
|
+
from ..exceptions import AegisPolicyBlockedError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AegisLangGraphWrapper:
|
|
11
|
+
"""
|
|
12
|
+
Wraps standard LangChain/LangGraph tools with AegisCore deterministic governance.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, gateway: Optional[AegisGateway] = None, gateway_url: str = "http://localhost:8080"):
|
|
16
|
+
self.gateway = gateway or AegisGateway(base_url=gateway_url)
|
|
17
|
+
|
|
18
|
+
def wrap_tool(self, tool_name: str, tool_func: Callable[..., Any]) -> Callable[..., Any]:
|
|
19
|
+
"""
|
|
20
|
+
Wraps a Python function/tool so that all calls are evaluated by AegisCore before execution.
|
|
21
|
+
"""
|
|
22
|
+
gateway = self.gateway
|
|
23
|
+
|
|
24
|
+
def governed_tool(**kwargs: Any) -> Any:
|
|
25
|
+
# 1. Pre-flight evaluation
|
|
26
|
+
result = gateway.dispatch(tool=tool_name, arguments=kwargs, raise_on_blocked=False)
|
|
27
|
+
if not result.is_success:
|
|
28
|
+
raise AegisPolicyBlockedError(
|
|
29
|
+
f"Action '{tool_name}' was rejected by AegisCore: {result.reason}",
|
|
30
|
+
violations=result.violations,
|
|
31
|
+
raw_result=result.raw
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# 2. If the gateway executed in sandbox, return that output; otherwise run the wrapped func
|
|
35
|
+
if result.output is not None:
|
|
36
|
+
return result.output
|
|
37
|
+
|
|
38
|
+
return tool_func(**kwargs)
|
|
39
|
+
|
|
40
|
+
governed_tool.__name__ = getattr(tool_func, "__name__", tool_name)
|
|
41
|
+
governed_tool.__doc__ = getattr(tool_func, "__doc__", "")
|
|
42
|
+
return governed_tool
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAI Tool-Call Interceptor for AegisCore
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
from ..client import AegisGateway
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AegisOpenAIInterceptor:
|
|
11
|
+
"""
|
|
12
|
+
Intercepts and governs OpenAI-style function tool calls through AegisCore.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, gateway: Optional[AegisGateway] = None, gateway_url: str = "http://localhost:8080"):
|
|
16
|
+
self.gateway = gateway or AegisGateway(base_url=gateway_url)
|
|
17
|
+
|
|
18
|
+
def intercept_tool_call(self, tool_call: Any) -> Dict[str, Any]:
|
|
19
|
+
"""
|
|
20
|
+
Intercepts a single OpenAI tool_call object and executes through AegisCore.
|
|
21
|
+
Returns a dict suitable for appending to OpenAI conversation messages.
|
|
22
|
+
"""
|
|
23
|
+
# Support dict or pydantic/object structure
|
|
24
|
+
if isinstance(tool_call, dict):
|
|
25
|
+
tool_id = tool_call.get("id", "")
|
|
26
|
+
func = tool_call.get("function", {})
|
|
27
|
+
name = func.get("name", "")
|
|
28
|
+
arguments = func.get("arguments", "{}")
|
|
29
|
+
else:
|
|
30
|
+
tool_id = getattr(tool_call, "id", "")
|
|
31
|
+
func = getattr(tool_call, "function", None)
|
|
32
|
+
name = getattr(func, "name", "") if func else ""
|
|
33
|
+
arguments = getattr(func, "arguments", "{}") if func else "{}"
|
|
34
|
+
|
|
35
|
+
raw_resp = self.gateway.execute_tool(
|
|
36
|
+
tool_call_id=tool_id,
|
|
37
|
+
name=name,
|
|
38
|
+
arguments=arguments
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
allowed = raw_resp.get("allowed", False)
|
|
42
|
+
verdict = raw_resp.get("verdict", "BLOCKED")
|
|
43
|
+
reason = raw_resp.get("reason", "Policy invariant violation")
|
|
44
|
+
execution = raw_resp.get("execution")
|
|
45
|
+
|
|
46
|
+
if not allowed:
|
|
47
|
+
content = json.dumps({
|
|
48
|
+
"error": "AEGIS_GOVERNANCE_BLOCKED",
|
|
49
|
+
"verdict": verdict,
|
|
50
|
+
"message": reason
|
|
51
|
+
})
|
|
52
|
+
else:
|
|
53
|
+
output = execution.get("output") if isinstance(execution, dict) else {"status": "SUCCESS"}
|
|
54
|
+
content = json.dumps(output)
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
"tool_call_id": tool_id,
|
|
58
|
+
"role": "tool",
|
|
59
|
+
"name": name,
|
|
60
|
+
"content": content
|
|
61
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AegisCore Python Models & Dataclasses
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class DispatchResult:
|
|
11
|
+
"""Represents the response of a tool dispatch through AegisCore Gateway."""
|
|
12
|
+
status: str # 'SUCCESS', 'BLOCKED', 'REQUIRES_HITL', 'SCHEMA_ERROR', 'EXECUTION_FAILED'
|
|
13
|
+
intent_id: Optional[str] = None
|
|
14
|
+
session_id: Optional[str] = None
|
|
15
|
+
tool: Optional[str] = None
|
|
16
|
+
reason: Optional[str] = None
|
|
17
|
+
output: Optional[Any] = None
|
|
18
|
+
violations: List[Dict[str, Any]] = field(default_factory=list)
|
|
19
|
+
challenge: Optional[Dict[str, Any]] = None
|
|
20
|
+
audit_entry: Optional[Dict[str, Any]] = None
|
|
21
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def is_success(self) -> bool:
|
|
25
|
+
return self.status == "SUCCESS"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def is_blocked(self) -> bool:
|
|
29
|
+
return self.status == "BLOCKED"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def requires_hitl(self) -> bool:
|
|
33
|
+
return self.status == "REQUIRES_HITL"
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def ticket_id(self) -> Optional[str]:
|
|
37
|
+
if self.challenge and isinstance(self.challenge, dict):
|
|
38
|
+
return self.challenge.get("ticketId")
|
|
39
|
+
return None
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aegiscore-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic Agent Governance Gateway - Zero-Trust Policy Enclave for AI Agents
|
|
5
|
+
Author: Sandeep Hipparagi & AegisCore Contributors
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/Sandeep-Hipparagi/aegis-core
|
|
8
|
+
Project-URL: Repository, https://github.com/Sandeep-Hipparagi/aegis-core.git
|
|
9
|
+
Project-URL: Issues, https://github.com/Sandeep-Hipparagi/aegis-core/issues
|
|
10
|
+
Keywords: ai-agent,governance,security,zero-trust,langgraph,openai,mcp,dpdp
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# aegiscore-py
|
|
29
|
+
|
|
30
|
+
Official, zero-dependency Python client for **AegisCore** — the Deterministic Zero-Trust Policy & Cryptographic WORM Audit Gateway for AI Agents.
|
|
31
|
+
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
[](https://pypi.org/project/aegiscore-ai/)
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install aegiscore-ai
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
*Note: `aegiscore-ai` is 100% zero-dependency, implemented purely using Python's standard library (`urllib.request`, `json`, `dataclasses`).*
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### 1. Direct Tool Proposal Dispatch
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from aegiscore import AegisGateway
|
|
49
|
+
|
|
50
|
+
gateway = AegisGateway("http://localhost:8080")
|
|
51
|
+
|
|
52
|
+
# Dispatch tool intent: evaluates schema, invariants, tokenizes PII, logs WORM audit
|
|
53
|
+
result = gateway.dispatch(
|
|
54
|
+
tool="transfer_funds",
|
|
55
|
+
arguments={"amount": 2500, "recipient": "ACC-PAYROLL"},
|
|
56
|
+
session_id="sess_01"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if result.is_success:
|
|
60
|
+
print("Action Approved & Executed:", result.output)
|
|
61
|
+
elif result.requires_hitl:
|
|
62
|
+
print(f"Action escalated to human approval. Ticket ID: {result.ticket_id}")
|
|
63
|
+
else:
|
|
64
|
+
print(f"Action BLOCKED: {result.reason}")
|
|
65
|
+
for violation in result.violations:
|
|
66
|
+
print(f" - {violation}")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. LangGraph & LangChain Integration
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from aegiscore import AegisGateway
|
|
73
|
+
from aegiscore.integrations import AegisLangGraphWrapper
|
|
74
|
+
|
|
75
|
+
gateway = AegisGateway("http://localhost:8080")
|
|
76
|
+
wrapper = AegisLangGraphWrapper(gateway=gateway)
|
|
77
|
+
|
|
78
|
+
# Wrap any native function or tool
|
|
79
|
+
@wrapper.wrap_tool("query_database", raw_query_func)
|
|
80
|
+
def safe_query_database(query: str):
|
|
81
|
+
return raw_query_func(query)
|
|
82
|
+
|
|
83
|
+
# Calls are intercepted and verified against policy invariants before invocation
|
|
84
|
+
safe_query_database(query="SELECT * FROM users")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 3. OpenAI Function Call Interception
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from aegiscore import AegisGateway
|
|
91
|
+
from aegiscore.integrations import AegisOpenAIInterceptor
|
|
92
|
+
|
|
93
|
+
gateway = AegisGateway("http://localhost:8080")
|
|
94
|
+
interceptor = AegisOpenAIInterceptor(gateway=gateway)
|
|
95
|
+
|
|
96
|
+
# In your chat completion loop:
|
|
97
|
+
for tool_call in response.choices[0].message.tool_calls:
|
|
98
|
+
# Evaluates policy and returns tool response dict formatted for OpenAI
|
|
99
|
+
tool_message = interceptor.intercept_tool_call(tool_call)
|
|
100
|
+
messages.append(tool_message)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 4. Human-in-the-Loop (HITL) Resolution
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
# Check pending ticket and resolve with cryptographic signature
|
|
107
|
+
gateway.resolve_hitl(
|
|
108
|
+
ticket_id="ticket-12345",
|
|
109
|
+
decision="APPROVED",
|
|
110
|
+
signature="sig-supervisor-ed25519"
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 5. Forensic Cryptographic Ledger Verification
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# Verifies the tamper-evident WORM Merkle chain
|
|
118
|
+
audit = gateway.verify_audit_integrity()
|
|
119
|
+
print(f"Ledger Valid: {audit['isValid']}")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
Apache-2.0
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
aegiscore/__init__.py
|
|
5
|
+
aegiscore/client.py
|
|
6
|
+
aegiscore/exceptions.py
|
|
7
|
+
aegiscore/models.py
|
|
8
|
+
aegiscore/integrations/__init__.py
|
|
9
|
+
aegiscore/integrations/langgraph.py
|
|
10
|
+
aegiscore/integrations/openai.py
|
|
11
|
+
aegiscore_ai.egg-info/PKG-INFO
|
|
12
|
+
aegiscore_ai.egg-info/SOURCES.txt
|
|
13
|
+
aegiscore_ai.egg-info/dependency_links.txt
|
|
14
|
+
aegiscore_ai.egg-info/top_level.txt
|
|
15
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aegiscore
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aegiscore-ai"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Deterministic Agent Governance Gateway - Zero-Trust Policy Enclave for AI Agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Sandeep Hipparagi & AegisCore Contributors" }
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"ai-agent",
|
|
17
|
+
"governance",
|
|
18
|
+
"security",
|
|
19
|
+
"zero-trust",
|
|
20
|
+
"langgraph",
|
|
21
|
+
"openai",
|
|
22
|
+
"mcp",
|
|
23
|
+
"dpdp"
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 4 - Beta",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.8",
|
|
30
|
+
"Programming Language :: Python :: 3.9",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Programming Language :: Python :: 3.14",
|
|
36
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
37
|
+
"Topic :: Security"
|
|
38
|
+
]
|
|
39
|
+
dependencies = []
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/Sandeep-Hipparagi/aegis-core"
|
|
43
|
+
Repository = "https://github.com/Sandeep-Hipparagi/aegis-core.git"
|
|
44
|
+
Issues = "https://github.com/Sandeep-Hipparagi/aegis-core/issues"
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for AegisCore Python SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import unittest
|
|
7
|
+
from unittest.mock import patch, MagicMock
|
|
8
|
+
import io
|
|
9
|
+
import urllib.error
|
|
10
|
+
|
|
11
|
+
from aegiscore import (
|
|
12
|
+
AegisGateway,
|
|
13
|
+
AegisClient,
|
|
14
|
+
DispatchResult,
|
|
15
|
+
AegisPolicyBlockedError,
|
|
16
|
+
AegisSchemaError,
|
|
17
|
+
)
|
|
18
|
+
from aegiscore.integrations import (
|
|
19
|
+
AegisLangGraphWrapper,
|
|
20
|
+
AegisOpenAIInterceptor,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestAegisPythonSDK(unittest.TestCase):
|
|
25
|
+
|
|
26
|
+
def setUp(self):
|
|
27
|
+
self.gateway = AegisGateway(base_url="http://127.0.0.1:8080", agent_id="test-agent", role="tester")
|
|
28
|
+
|
|
29
|
+
def test_client_alias(self):
|
|
30
|
+
self.assertIs(AegisClient, AegisGateway)
|
|
31
|
+
client = AegisClient(base_url="http://localhost:9090")
|
|
32
|
+
self.assertEqual(client.base_url, "http://localhost:9090")
|
|
33
|
+
|
|
34
|
+
def test_dispatch_result_helpers(self):
|
|
35
|
+
res_success = DispatchResult(
|
|
36
|
+
status="SUCCESS",
|
|
37
|
+
tool="check_balance",
|
|
38
|
+
output={"balance": 5000}
|
|
39
|
+
)
|
|
40
|
+
self.assertTrue(res_success.is_success)
|
|
41
|
+
self.assertFalse(res_success.is_blocked)
|
|
42
|
+
self.assertFalse(res_success.requires_hitl)
|
|
43
|
+
self.assertIsNone(res_success.ticket_id)
|
|
44
|
+
|
|
45
|
+
res_hitl = DispatchResult(
|
|
46
|
+
status="REQUIRES_HITL",
|
|
47
|
+
tool="transfer_funds",
|
|
48
|
+
challenge={"ticketId": "tick-999", "status": "PENDING"}
|
|
49
|
+
)
|
|
50
|
+
self.assertFalse(res_hitl.is_success)
|
|
51
|
+
self.assertTrue(res_hitl.requires_hitl)
|
|
52
|
+
self.assertEqual(res_hitl.ticket_id, "tick-999")
|
|
53
|
+
|
|
54
|
+
res_blocked = DispatchResult(
|
|
55
|
+
status="BLOCKED",
|
|
56
|
+
tool="purge_data",
|
|
57
|
+
reason="Unauthorized role"
|
|
58
|
+
)
|
|
59
|
+
self.assertTrue(res_blocked.is_blocked)
|
|
60
|
+
|
|
61
|
+
@patch("urllib.request.urlopen")
|
|
62
|
+
def test_dispatch_success(self, mock_urlopen):
|
|
63
|
+
mock_response = MagicMock()
|
|
64
|
+
mock_response.read.return_value = json.dumps({
|
|
65
|
+
"status": "SUCCESS",
|
|
66
|
+
"intentId": "int-101",
|
|
67
|
+
"execution": {
|
|
68
|
+
"status": "SUCCESS",
|
|
69
|
+
"output": {"txId": "tx-12345", "amount": 100}
|
|
70
|
+
}
|
|
71
|
+
}).encode("utf-8")
|
|
72
|
+
mock_urlopen.return_value.__enter__.return_value = mock_response
|
|
73
|
+
|
|
74
|
+
res = self.gateway.dispatch("transfer_funds", {"amount": 100, "recipient": "bob"})
|
|
75
|
+
self.assertTrue(res.is_success)
|
|
76
|
+
self.assertEqual(res.output, {"txId": "tx-12345", "amount": 100})
|
|
77
|
+
self.assertEqual(res.intent_id, "int-101")
|
|
78
|
+
|
|
79
|
+
@patch("urllib.request.urlopen")
|
|
80
|
+
def test_dispatch_blocked_without_raise(self, mock_urlopen):
|
|
81
|
+
err_body = json.dumps({
|
|
82
|
+
"status": "BLOCKED",
|
|
83
|
+
"reason": "Exceeds daily budget",
|
|
84
|
+
"violations": [{"ruleId": "RULE-01", "message": "Limit exceeded"}]
|
|
85
|
+
}).encode("utf-8")
|
|
86
|
+
mock_fp = io.BytesIO(err_body)
|
|
87
|
+
http_err = urllib.error.HTTPError("http://127.0.0.1:8080/v1/gateway/dispatch", 403, "Forbidden", {}, mock_fp)
|
|
88
|
+
mock_urlopen.side_effect = http_err
|
|
89
|
+
|
|
90
|
+
res = self.gateway.dispatch("transfer_funds", {"amount": 1000000}, raise_on_blocked=False)
|
|
91
|
+
self.assertTrue(res.is_blocked)
|
|
92
|
+
self.assertEqual(res.reason, "Exceeds daily budget")
|
|
93
|
+
self.assertEqual(len(res.violations), 1)
|
|
94
|
+
|
|
95
|
+
@patch("urllib.request.urlopen")
|
|
96
|
+
def test_dispatch_blocked_with_raise(self, mock_urlopen):
|
|
97
|
+
err_body = json.dumps({
|
|
98
|
+
"status": "BLOCKED",
|
|
99
|
+
"reason": "Target table blacklisted",
|
|
100
|
+
"violations": [{"ruleId": "RULE-SENSITIVE"}]
|
|
101
|
+
}).encode("utf-8")
|
|
102
|
+
mock_fp = io.BytesIO(err_body)
|
|
103
|
+
http_err = urllib.error.HTTPError("http://127.0.0.1:8080/v1/gateway/dispatch", 403, "Forbidden", {}, mock_fp)
|
|
104
|
+
mock_urlopen.side_effect = http_err
|
|
105
|
+
|
|
106
|
+
with self.assertRaises(AegisPolicyBlockedError) as ctx:
|
|
107
|
+
self.gateway.dispatch("query_database", {"targetTable": "users.credentials"}, raise_on_blocked=True)
|
|
108
|
+
|
|
109
|
+
self.assertIn("Target table blacklisted", str(ctx.exception))
|
|
110
|
+
|
|
111
|
+
@patch("urllib.request.urlopen")
|
|
112
|
+
def test_evaluate_intent(self, mock_urlopen):
|
|
113
|
+
mock_response = MagicMock()
|
|
114
|
+
mock_response.read.return_value = json.dumps({
|
|
115
|
+
"decision": {"status": "APPROVED", "executionAllowed": True}
|
|
116
|
+
}).encode("utf-8")
|
|
117
|
+
mock_urlopen.return_value.__enter__.return_value = mock_response
|
|
118
|
+
|
|
119
|
+
res = self.gateway.evaluate_intent("check_balance", {"accountId": "ACC-01"})
|
|
120
|
+
self.assertEqual(res["decision"]["status"], "APPROVED")
|
|
121
|
+
|
|
122
|
+
@patch("urllib.request.urlopen")
|
|
123
|
+
def test_execute_tool(self, mock_urlopen):
|
|
124
|
+
mock_response = MagicMock()
|
|
125
|
+
mock_response.read.return_value = json.dumps({
|
|
126
|
+
"tool_call_id": "call_123",
|
|
127
|
+
"allowed": True,
|
|
128
|
+
"verdict": "APPROVED"
|
|
129
|
+
}).encode("utf-8")
|
|
130
|
+
mock_urlopen.return_value.__enter__.return_value = mock_response
|
|
131
|
+
|
|
132
|
+
res = self.gateway.execute_tool("call_123", "check_balance", {"accountId": "ACC-01"})
|
|
133
|
+
self.assertTrue(res["allowed"])
|
|
134
|
+
|
|
135
|
+
@patch("urllib.request.urlopen")
|
|
136
|
+
def test_resolve_hitl(self, mock_urlopen):
|
|
137
|
+
mock_response = MagicMock()
|
|
138
|
+
mock_response.read.return_value = json.dumps({
|
|
139
|
+
"resolution": {"status": "APPROVED"},
|
|
140
|
+
"execution": {"status": "SUCCESS"}
|
|
141
|
+
}).encode("utf-8")
|
|
142
|
+
mock_urlopen.return_value.__enter__.return_value = mock_response
|
|
143
|
+
|
|
144
|
+
res = self.gateway.resolve_hitl("ticket-01", "APPROVED", "sig-123")
|
|
145
|
+
self.assertEqual(res["resolution"]["status"], "APPROVED")
|
|
146
|
+
|
|
147
|
+
@patch("urllib.request.urlopen")
|
|
148
|
+
def test_audit_integrity(self, mock_urlopen):
|
|
149
|
+
mock_response = MagicMock()
|
|
150
|
+
mock_response.read.return_value = json.dumps({
|
|
151
|
+
"isValid": True,
|
|
152
|
+
"totalEntries": 42
|
|
153
|
+
}).encode("utf-8")
|
|
154
|
+
mock_urlopen.return_value.__enter__.return_value = mock_response
|
|
155
|
+
|
|
156
|
+
res = self.gateway.verify_audit_integrity()
|
|
157
|
+
self.assertTrue(res["isValid"])
|
|
158
|
+
self.assertEqual(res["totalEntries"], 42)
|
|
159
|
+
|
|
160
|
+
def test_langgraph_wrapper_approved(self):
|
|
161
|
+
wrapper = AegisLangGraphWrapper(gateway=self.gateway)
|
|
162
|
+
with patch.object(self.gateway, "dispatch") as mock_dispatch:
|
|
163
|
+
mock_dispatch.return_value = DispatchResult(
|
|
164
|
+
status="SUCCESS",
|
|
165
|
+
tool="calc",
|
|
166
|
+
output=42
|
|
167
|
+
)
|
|
168
|
+
raw_fn = MagicMock(return_value=42)
|
|
169
|
+
governed = wrapper.wrap_tool("calc", raw_fn)
|
|
170
|
+
output = governed(x=20, y=22)
|
|
171
|
+
self.assertEqual(output, 42)
|
|
172
|
+
mock_dispatch.assert_called_once()
|
|
173
|
+
|
|
174
|
+
def test_openai_interceptor_blocked(self):
|
|
175
|
+
interceptor = AegisOpenAIInterceptor(gateway=self.gateway)
|
|
176
|
+
with patch.object(self.gateway, "execute_tool") as mock_exec:
|
|
177
|
+
mock_exec.return_value = {
|
|
178
|
+
"tool_call_id": "tc_1",
|
|
179
|
+
"allowed": False,
|
|
180
|
+
"verdict": "BLOCKED",
|
|
181
|
+
"reason": "High risk action"
|
|
182
|
+
}
|
|
183
|
+
msg = interceptor.intercept_tool_call({
|
|
184
|
+
"id": "tc_1",
|
|
185
|
+
"function": {
|
|
186
|
+
"name": "delete_all",
|
|
187
|
+
"arguments": "{}"
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
self.assertEqual(msg["role"], "tool")
|
|
191
|
+
parsed = json.loads(msg["content"])
|
|
192
|
+
self.assertEqual(parsed["verdict"], "BLOCKED")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
unittest.main()
|