auditail 0.2.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.
- auditail-0.2.0/LICENSE +21 -0
- auditail-0.2.0/PKG-INFO +100 -0
- auditail-0.2.0/README.md +76 -0
- auditail-0.2.0/auditail/__init__.py +14 -0
- auditail-0.2.0/auditail/client.py +339 -0
- auditail-0.2.0/auditail/rules.py +79 -0
- auditail-0.2.0/auditail/types.py +54 -0
- auditail-0.2.0/auditail.egg-info/PKG-INFO +100 -0
- auditail-0.2.0/auditail.egg-info/SOURCES.txt +11 -0
- auditail-0.2.0/auditail.egg-info/dependency_links.txt +1 -0
- auditail-0.2.0/auditail.egg-info/top_level.txt +1 -0
- auditail-0.2.0/pyproject.toml +35 -0
- auditail-0.2.0/setup.cfg +4 -0
auditail-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Auditail
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
auditail-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: auditail
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Guardrails that block risky actions, plus a tamper-evident audit trail for autonomous AI agents.
|
|
5
|
+
Author: Auditail
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://auditail.io
|
|
8
|
+
Project-URL: Documentation, https://auditail.io/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/auditailhq/auditail
|
|
10
|
+
Project-URL: Request an API key, https://auditail.io/waitlist
|
|
11
|
+
Keywords: ai,agents,security,guardrails,audit,compliance,soc2,langchain
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# auditail (Python SDK)
|
|
26
|
+
|
|
27
|
+
> The security layer for autonomous AI agents. Guardrails that block risky actions, and a tamper-evident audit trail of everything your agents do.
|
|
28
|
+
|
|
29
|
+
**[๐ Full Documentation](https://auditail.io/docs)** ยท **[๐ Join the Private Beta](https://auditail.io/waitlist)**
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install auditail
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from auditail import Auditail, rules
|
|
43
|
+
|
|
44
|
+
aegis = Auditail(api_key="your_key")
|
|
45
|
+
|
|
46
|
+
aegis.add_rules([
|
|
47
|
+
rules.no_external_email(internal_domains=["acme.com"]),
|
|
48
|
+
rules.no_data_deletion(),
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
result = aegis.guard(
|
|
52
|
+
action="send_email",
|
|
53
|
+
payload={"to": "client@example.com", "body": "Report attached."},
|
|
54
|
+
agent_id="financial-bot-01",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if result.approved:
|
|
58
|
+
send_email(...)
|
|
59
|
+
else:
|
|
60
|
+
print(f"Blocked: {result.reason}")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## LangChain Integration
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from langchain.agents import tool
|
|
67
|
+
from auditail import Auditail, rules
|
|
68
|
+
|
|
69
|
+
aegis = Auditail(api_key="your_key")
|
|
70
|
+
aegis.add_rule(rules.no_external_email(internal_domains=["acme.com"]))
|
|
71
|
+
|
|
72
|
+
@tool
|
|
73
|
+
def send_email(to: str, body: str) -> str:
|
|
74
|
+
"""Send an email to a recipient."""
|
|
75
|
+
result = aegis.guard(
|
|
76
|
+
action="send_email",
|
|
77
|
+
payload={"to": to, "body": body},
|
|
78
|
+
agent_id="langchain-agent",
|
|
79
|
+
)
|
|
80
|
+
if not result.approved:
|
|
81
|
+
return f"Action blocked by Auditail: {result.reason}"
|
|
82
|
+
# ... actual send logic
|
|
83
|
+
return "Email sent."
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Private beta
|
|
87
|
+
|
|
88
|
+
The SDK, API and dashboard are live. Guardrail rules run locally in your process; every decision is delivered in the background to a **hash-chained audit log** you can browse and verify at [app.auditail.io](https://app.auditail.io).
|
|
89
|
+
|
|
90
|
+
- Batched, retried, idempotent delivery. `guard()` never waits on the network, and a delivery failure never reaches your agent.
|
|
91
|
+
- Secrets are redacted before an event leaves your process (`password`, `token`, `apiKey`, ... plus your own `redact_keys`).
|
|
92
|
+
- Call `aegis.flush()` before a serverless function returns or the process exits.
|
|
93
|
+
|
|
94
|
+
**[Request an API key](https://auditail.io/waitlist)**: keys are issued to design partners personally.
|
|
95
|
+
|
|
96
|
+
**Roadmap** (not available yet): Slack/PagerDuty alerts, human-in-the-loop approval (`require_approval` currently behaves like `block`), compliance export, private-cloud deployment.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
auditail-0.2.0/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# auditail (Python SDK)
|
|
2
|
+
|
|
3
|
+
> The security layer for autonomous AI agents. Guardrails that block risky actions, and a tamper-evident audit trail of everything your agents do.
|
|
4
|
+
|
|
5
|
+
**[๐ Full Documentation](https://auditail.io/docs)** ยท **[๐ Join the Private Beta](https://auditail.io/waitlist)**
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install auditail
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from auditail import Auditail, rules
|
|
19
|
+
|
|
20
|
+
aegis = Auditail(api_key="your_key")
|
|
21
|
+
|
|
22
|
+
aegis.add_rules([
|
|
23
|
+
rules.no_external_email(internal_domains=["acme.com"]),
|
|
24
|
+
rules.no_data_deletion(),
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
result = aegis.guard(
|
|
28
|
+
action="send_email",
|
|
29
|
+
payload={"to": "client@example.com", "body": "Report attached."},
|
|
30
|
+
agent_id="financial-bot-01",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if result.approved:
|
|
34
|
+
send_email(...)
|
|
35
|
+
else:
|
|
36
|
+
print(f"Blocked: {result.reason}")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## LangChain Integration
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from langchain.agents import tool
|
|
43
|
+
from auditail import Auditail, rules
|
|
44
|
+
|
|
45
|
+
aegis = Auditail(api_key="your_key")
|
|
46
|
+
aegis.add_rule(rules.no_external_email(internal_domains=["acme.com"]))
|
|
47
|
+
|
|
48
|
+
@tool
|
|
49
|
+
def send_email(to: str, body: str) -> str:
|
|
50
|
+
"""Send an email to a recipient."""
|
|
51
|
+
result = aegis.guard(
|
|
52
|
+
action="send_email",
|
|
53
|
+
payload={"to": to, "body": body},
|
|
54
|
+
agent_id="langchain-agent",
|
|
55
|
+
)
|
|
56
|
+
if not result.approved:
|
|
57
|
+
return f"Action blocked by Auditail: {result.reason}"
|
|
58
|
+
# ... actual send logic
|
|
59
|
+
return "Email sent."
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Private beta
|
|
63
|
+
|
|
64
|
+
The SDK, API and dashboard are live. Guardrail rules run locally in your process; every decision is delivered in the background to a **hash-chained audit log** you can browse and verify at [app.auditail.io](https://app.auditail.io).
|
|
65
|
+
|
|
66
|
+
- Batched, retried, idempotent delivery. `guard()` never waits on the network, and a delivery failure never reaches your agent.
|
|
67
|
+
- Secrets are redacted before an event leaves your process (`password`, `token`, `apiKey`, ... plus your own `redact_keys`).
|
|
68
|
+
- Call `aegis.flush()` before a serverless function returns or the process exits.
|
|
69
|
+
|
|
70
|
+
**[Request an API key](https://auditail.io/waitlist)**: keys are issued to design partners personally.
|
|
71
|
+
|
|
72
|
+
**Roadmap** (not available yet): Slack/PagerDuty alerts, human-in-the-loop approval (`require_approval` currently behaves like `block`), compliance export, private-cloud deployment.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auditail Python SDK
|
|
3
|
+
The security layer for autonomous AI agents.
|
|
4
|
+
Guardrails that block risky actions, plus a tamper-evident audit trail.
|
|
5
|
+
|
|
6
|
+
https://auditail.io/docs
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .client import Auditail
|
|
10
|
+
from .types import GuardOptions, GuardResult, GuardrailRule, AuditEvent
|
|
11
|
+
from . import rules
|
|
12
|
+
|
|
13
|
+
__version__ = "0.2.0"
|
|
14
|
+
__all__ = ["Auditail", "GuardOptions", "GuardResult", "GuardrailRule", "AuditEvent", "rules"]
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auditail Python SDK โ Client
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import atexit
|
|
7
|
+
import concurrent.futures
|
|
8
|
+
import inspect
|
|
9
|
+
import json
|
|
10
|
+
import random
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.request
|
|
15
|
+
import uuid
|
|
16
|
+
from collections import deque
|
|
17
|
+
from dataclasses import asdict
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from typing import Any, Callable, Deque, Iterable, List, Optional
|
|
20
|
+
|
|
21
|
+
from .types import AuditEvent, GuardOptions, GuardResult, GuardrailRule
|
|
22
|
+
|
|
23
|
+
SDK_VERSION = "0.2.0"
|
|
24
|
+
MAX_QUEUE = 10_000
|
|
25
|
+
MAX_BATCH = 50
|
|
26
|
+
MAX_DELIVERY_ATTEMPTS = 5
|
|
27
|
+
DEFAULT_REDACT = {
|
|
28
|
+
"password", "passwd", "secret", "token", "apikey", "api_key", "authorization", "auth",
|
|
29
|
+
"ssn", "creditcard", "credit_card", "cardnumber", "card_number", "cvv", "privatekey", "private_key",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _redact(value: Any, keys: set, depth: int = 0) -> Any:
|
|
34
|
+
if depth > 10:
|
|
35
|
+
return "[TRUNCATED]"
|
|
36
|
+
if isinstance(value, dict):
|
|
37
|
+
return {k: ("[REDACTED]" if str(k).lower() in keys else _redact(v, keys, depth + 1)) for k, v in value.items()}
|
|
38
|
+
if isinstance(value, (list, tuple)):
|
|
39
|
+
return [_redact(v, keys, depth + 1) for v in value]
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _to_wire(e: AuditEvent) -> dict:
|
|
44
|
+
"""AuditEvent -> the camelCase JSON the API expects."""
|
|
45
|
+
return {
|
|
46
|
+
"auditId": e.audit_id, "agentId": e.agent_id, "userId": e.user_id, "action": e.action,
|
|
47
|
+
"payload": e.payload, "approved": e.approved, "reason": e.reason, "matchedRule": e.matched_rule,
|
|
48
|
+
"tags": e.tags, "timestamp": e.timestamp, "environment": e.environment, "sdkVersion": e.sdk_version,
|
|
49
|
+
}
|
|
50
|
+
WAITLIST_URL = "https://auditail.io/waitlist"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Auditail:
|
|
54
|
+
"""
|
|
55
|
+
Auditail client. Instantiate once at app startup and reuse.
|
|
56
|
+
|
|
57
|
+
Example::
|
|
58
|
+
|
|
59
|
+
from auditail import Auditail, rules
|
|
60
|
+
|
|
61
|
+
aegis = Auditail(api_key="your_key")
|
|
62
|
+
aegis.add_rule(rules.no_external_email())
|
|
63
|
+
aegis.add_rule(rules.no_data_deletion())
|
|
64
|
+
|
|
65
|
+
result = aegis.guard(
|
|
66
|
+
action="send_email",
|
|
67
|
+
payload={"to": "client@example.com", "body": "Report attached."},
|
|
68
|
+
agent_id="financial-bot-01",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if result.approved:
|
|
72
|
+
send_email(...)
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
api_key: str,
|
|
78
|
+
environment: str = "production",
|
|
79
|
+
log_level: str = "info",
|
|
80
|
+
base_url: str = "https://api.auditail.io",
|
|
81
|
+
redact_keys: Optional[Iterable[str]] = None,
|
|
82
|
+
ship: bool = True,
|
|
83
|
+
flush_interval: float = 1.0,
|
|
84
|
+
on_error: Optional[Callable[[Exception], None]] = None,
|
|
85
|
+
):
|
|
86
|
+
if not api_key:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"[Auditail] No API key provided.\nGet your free API key at: {WAITLIST_URL}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
self.api_key = api_key
|
|
92
|
+
self.environment = environment
|
|
93
|
+
self.log_level = log_level
|
|
94
|
+
self.base_url = base_url.rstrip("/")
|
|
95
|
+
self.ship = ship
|
|
96
|
+
self.flush_interval = flush_interval
|
|
97
|
+
self._redact = DEFAULT_REDACT | {k.lower() for k in (redact_keys or [])}
|
|
98
|
+
self._on_error = on_error or (lambda e: self._log("error", str(e)))
|
|
99
|
+
|
|
100
|
+
self._rules: List[GuardrailRule] = []
|
|
101
|
+
self._audit_log: List[AuditEvent] = []
|
|
102
|
+
|
|
103
|
+
self._queue: Deque[AuditEvent] = deque()
|
|
104
|
+
self._qlock = threading.Lock()
|
|
105
|
+
self._flush_lock = threading.Lock()
|
|
106
|
+
self._wake = threading.Event()
|
|
107
|
+
self._stop = threading.Event()
|
|
108
|
+
self._worker: Optional[threading.Thread] = None
|
|
109
|
+
if ship:
|
|
110
|
+
atexit.register(self.flush)
|
|
111
|
+
|
|
112
|
+
def add_rule(self, rule: GuardrailRule) -> "Auditail":
|
|
113
|
+
"""Register a guardrail rule. Rules are evaluated in registration order."""
|
|
114
|
+
self._rules.append(rule)
|
|
115
|
+
self._log("debug", f'Guardrail rule added: "{rule.name}"')
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def add_rules(self, rules: List[GuardrailRule]) -> "Auditail":
|
|
119
|
+
"""Register multiple guardrail rules at once."""
|
|
120
|
+
for rule in rules:
|
|
121
|
+
self.add_rule(rule)
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
def guard(
|
|
125
|
+
self,
|
|
126
|
+
action: str,
|
|
127
|
+
payload: dict,
|
|
128
|
+
agent_id: str,
|
|
129
|
+
user_id: Optional[str] = None,
|
|
130
|
+
tags: Optional[dict] = None,
|
|
131
|
+
) -> GuardResult:
|
|
132
|
+
"""
|
|
133
|
+
Synchronous guard evaluation.
|
|
134
|
+
Evaluates the action against all registered rules and logs an audit event.
|
|
135
|
+
"""
|
|
136
|
+
opts = GuardOptions(
|
|
137
|
+
action=action,
|
|
138
|
+
payload=payload,
|
|
139
|
+
agent_id=agent_id,
|
|
140
|
+
user_id=user_id,
|
|
141
|
+
tags=tags,
|
|
142
|
+
)
|
|
143
|
+
try:
|
|
144
|
+
asyncio.get_running_loop()
|
|
145
|
+
except RuntimeError:
|
|
146
|
+
return asyncio.run(self._evaluate(opts))
|
|
147
|
+
# Already inside a running event loop (FastAPI, async LangChain):
|
|
148
|
+
# run the evaluation on a worker thread so we don't crash or block the loop's state.
|
|
149
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
150
|
+
return pool.submit(asyncio.run, self._evaluate(opts)).result()
|
|
151
|
+
|
|
152
|
+
async def guard_async(
|
|
153
|
+
self,
|
|
154
|
+
action: str,
|
|
155
|
+
payload: dict,
|
|
156
|
+
agent_id: str,
|
|
157
|
+
user_id: Optional[str] = None,
|
|
158
|
+
tags: Optional[dict] = None,
|
|
159
|
+
) -> GuardResult:
|
|
160
|
+
"""
|
|
161
|
+
Async guard evaluation for use in async codebases (FastAPI, etc.).
|
|
162
|
+
"""
|
|
163
|
+
opts = GuardOptions(
|
|
164
|
+
action=action,
|
|
165
|
+
payload=payload,
|
|
166
|
+
agent_id=agent_id,
|
|
167
|
+
user_id=user_id,
|
|
168
|
+
tags=tags,
|
|
169
|
+
)
|
|
170
|
+
return await self._evaluate(opts)
|
|
171
|
+
|
|
172
|
+
def get_audit_log(self) -> List[AuditEvent]:
|
|
173
|
+
"""
|
|
174
|
+
Returns all audit events from this session.
|
|
175
|
+
In the full SDK, use the dashboard at https://app.auditail.io for persistent logs.
|
|
176
|
+
"""
|
|
177
|
+
return list(self._audit_log)
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Private
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
async def _evaluate(self, opts: GuardOptions) -> GuardResult:
|
|
184
|
+
start = time.monotonic()
|
|
185
|
+
audit_id = self._generate_id()
|
|
186
|
+
|
|
187
|
+
self._log("debug", f'Evaluating action "{opts.action}" for agent "{opts.agent_id}"')
|
|
188
|
+
|
|
189
|
+
approved = True
|
|
190
|
+
reason: Optional[str] = None
|
|
191
|
+
matched_rule: Optional[str] = None
|
|
192
|
+
|
|
193
|
+
for rule in self._rules:
|
|
194
|
+
try:
|
|
195
|
+
result = rule.condition(opts)
|
|
196
|
+
triggered = await result if inspect.isawaitable(result) else result
|
|
197
|
+
except Exception as exc: # fail closed: a broken rule must not approve the action
|
|
198
|
+
self._log("error", f'Rule "{rule.name}" raised {exc!r}. Failing closed (blocking action).')
|
|
199
|
+
triggered = True
|
|
200
|
+
|
|
201
|
+
if triggered:
|
|
202
|
+
matched_rule = rule.name
|
|
203
|
+
if rule.action in ("block", "require_approval"):
|
|
204
|
+
approved = False
|
|
205
|
+
reason = rule.description
|
|
206
|
+
self._log("warn", f'Action BLOCKED by rule "{rule.name}": {rule.description}')
|
|
207
|
+
break
|
|
208
|
+
elif rule.action == "alert":
|
|
209
|
+
self._log("warn", f'ALERT fired by rule "{rule.name}": {rule.description}')
|
|
210
|
+
|
|
211
|
+
latency_ms = int((time.monotonic() - start) * 1000)
|
|
212
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
213
|
+
|
|
214
|
+
event = AuditEvent(
|
|
215
|
+
audit_id=audit_id,
|
|
216
|
+
agent_id=opts.agent_id,
|
|
217
|
+
user_id=opts.user_id,
|
|
218
|
+
action=opts.action,
|
|
219
|
+
payload=_redact(opts.payload, self._redact),
|
|
220
|
+
approved=approved,
|
|
221
|
+
reason=reason,
|
|
222
|
+
matched_rule=matched_rule,
|
|
223
|
+
tags=opts.tags,
|
|
224
|
+
timestamp=timestamp,
|
|
225
|
+
environment=self.environment,
|
|
226
|
+
sdk_version=SDK_VERSION,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
self._audit_log.append(event)
|
|
230
|
+
status = "APPROVED" if approved else "BLOCKED"
|
|
231
|
+
self._log("info", f"[{status}] {opts.action} | auditId: {audit_id} | {latency_ms}ms")
|
|
232
|
+
|
|
233
|
+
self._ship_to_api(event)
|
|
234
|
+
|
|
235
|
+
return GuardResult(
|
|
236
|
+
approved=approved,
|
|
237
|
+
audit_id=audit_id,
|
|
238
|
+
reason=reason,
|
|
239
|
+
matched_rule=matched_rule,
|
|
240
|
+
timestamp=timestamp,
|
|
241
|
+
latency_ms=latency_ms,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def _generate_id(self) -> str:
|
|
245
|
+
return f"aeg_{uuid.uuid4().hex[:16]}"
|
|
246
|
+
|
|
247
|
+
def _log(self, level: str, message: str) -> None:
|
|
248
|
+
levels = ["debug", "info", "warn", "error", "silent"]
|
|
249
|
+
if level not in levels:
|
|
250
|
+
return
|
|
251
|
+
if levels.index(level) < levels.index(self.log_level):
|
|
252
|
+
return
|
|
253
|
+
prefix = "[Auditail]"
|
|
254
|
+
if level == "warn":
|
|
255
|
+
print(f"{prefix} โ ๏ธ {message}")
|
|
256
|
+
elif level == "error":
|
|
257
|
+
print(f"{prefix} โ {message}")
|
|
258
|
+
else:
|
|
259
|
+
print(f"{prefix} {message}")
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# Delivery: non-blocking queue + background worker. Never raises into the caller.
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
def _ship_to_api(self, event: AuditEvent) -> None:
|
|
266
|
+
if not self.ship:
|
|
267
|
+
return
|
|
268
|
+
with self._qlock:
|
|
269
|
+
if len(self._queue) >= MAX_QUEUE:
|
|
270
|
+
self._queue.popleft()
|
|
271
|
+
self._on_error(RuntimeError(f"Auditail queue full ({MAX_QUEUE}); dropped oldest event"))
|
|
272
|
+
self._queue.append(event)
|
|
273
|
+
self._ensure_worker()
|
|
274
|
+
self._wake.set()
|
|
275
|
+
|
|
276
|
+
def _ensure_worker(self) -> None:
|
|
277
|
+
if self._worker and self._worker.is_alive():
|
|
278
|
+
return
|
|
279
|
+
self._stop.clear()
|
|
280
|
+
self._worker = threading.Thread(target=self._run, name="auditail-shipper", daemon=True)
|
|
281
|
+
self._worker.start()
|
|
282
|
+
|
|
283
|
+
def _run(self) -> None:
|
|
284
|
+
while not self._stop.is_set():
|
|
285
|
+
self._wake.wait(timeout=self.flush_interval)
|
|
286
|
+
self._wake.clear()
|
|
287
|
+
self._drain()
|
|
288
|
+
|
|
289
|
+
def flush(self) -> None:
|
|
290
|
+
"""Deliver all queued events now. Never raises. Call before a serverless handler returns."""
|
|
291
|
+
self._drain()
|
|
292
|
+
|
|
293
|
+
def shutdown(self) -> None:
|
|
294
|
+
"""Flush and stop the background worker."""
|
|
295
|
+
self._stop.set()
|
|
296
|
+
self._wake.set()
|
|
297
|
+
self.flush()
|
|
298
|
+
|
|
299
|
+
def _drain(self) -> None:
|
|
300
|
+
with self._flush_lock:
|
|
301
|
+
while True:
|
|
302
|
+
with self._qlock:
|
|
303
|
+
batch = list(self._queue)[:MAX_BATCH]
|
|
304
|
+
if not batch:
|
|
305
|
+
return
|
|
306
|
+
if not self._deliver(batch):
|
|
307
|
+
return # keep events queued; the next flush retries
|
|
308
|
+
done = {id(e) for e in batch}
|
|
309
|
+
with self._qlock:
|
|
310
|
+
self._queue = deque(e for e in self._queue if id(e) not in done)
|
|
311
|
+
|
|
312
|
+
def _deliver(self, batch: List[AuditEvent]) -> bool:
|
|
313
|
+
body = json.dumps({"events": [_to_wire(e) for e in batch]}).encode()
|
|
314
|
+
req = urllib.request.Request(
|
|
315
|
+
f"{self.base_url}/v1/events",
|
|
316
|
+
data=body,
|
|
317
|
+
method="POST",
|
|
318
|
+
headers={
|
|
319
|
+
"content-type": "application/json",
|
|
320
|
+
"authorization": f"Bearer {self.api_key}",
|
|
321
|
+
# Explicit UA: Cloudflare blocks the default "Python-urllib" signature (error 1010).
|
|
322
|
+
"user-agent": f"auditail-python/{SDK_VERSION}",
|
|
323
|
+
},
|
|
324
|
+
)
|
|
325
|
+
for attempt in range(1, MAX_DELIVERY_ATTEMPTS + 1):
|
|
326
|
+
try:
|
|
327
|
+
with urllib.request.urlopen(req, timeout=10):
|
|
328
|
+
return True
|
|
329
|
+
except urllib.error.HTTPError as exc:
|
|
330
|
+
if 400 <= exc.code < 500 and exc.code != 429:
|
|
331
|
+
detail = exc.read(200).decode(errors="replace")
|
|
332
|
+
self._on_error(RuntimeError(f"Auditail rejected {len(batch)} event(s): HTTP {exc.code} {detail}"))
|
|
333
|
+
return True # permanent: drop, don't retry forever
|
|
334
|
+
except Exception:
|
|
335
|
+
pass # network error: retry
|
|
336
|
+
if attempt < MAX_DELIVERY_ATTEMPTS:
|
|
337
|
+
time.sleep(random.random() * min(0.25 * 2**attempt, 4.0))
|
|
338
|
+
self._on_error(RuntimeError(f"Auditail could not deliver {len(batch)} event(s); will retry on next flush"))
|
|
339
|
+
return False
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pre-built guardrail rules for common AI agent security scenarios.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .types import GuardrailRule, GuardOptions
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def no_external_email(internal_domains: list[str] | None = None) -> GuardrailRule:
|
|
9
|
+
"""
|
|
10
|
+
Block any email action targeting an external domain.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
internal_domains: Trusted domains (e.g. ['acme.com']). Emails to any other
|
|
14
|
+
domain are blocked. If empty, the rule is inactive.
|
|
15
|
+
"""
|
|
16
|
+
domains = internal_domains or []
|
|
17
|
+
|
|
18
|
+
def condition(opts: GuardOptions) -> bool:
|
|
19
|
+
if not domains:
|
|
20
|
+
return False # not configured, skip rule
|
|
21
|
+
to = opts.payload.get("to", "")
|
|
22
|
+
if not to:
|
|
23
|
+
return False
|
|
24
|
+
return not any(to.endswith(f"@{d}") for d in domains)
|
|
25
|
+
|
|
26
|
+
return GuardrailRule(
|
|
27
|
+
name="no-external-email",
|
|
28
|
+
description="Blocked: sending email to an external domain requires human approval.",
|
|
29
|
+
condition=condition,
|
|
30
|
+
action="block",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def no_data_deletion() -> GuardrailRule:
|
|
35
|
+
"""Block destructive SQL operations (DELETE, DROP, TRUNCATE)."""
|
|
36
|
+
|
|
37
|
+
def condition(opts: GuardOptions) -> bool:
|
|
38
|
+
query = str(opts.payload.get("query", "")).upper()
|
|
39
|
+
return any(kw in query for kw in ["DELETE", "DROP TABLE", "TRUNCATE"])
|
|
40
|
+
|
|
41
|
+
return GuardrailRule(
|
|
42
|
+
name="no-data-deletion",
|
|
43
|
+
description="Blocked: destructive data operations are not permitted.",
|
|
44
|
+
condition=condition,
|
|
45
|
+
action="block",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def no_external_api_calls() -> GuardrailRule:
|
|
50
|
+
"""Alert when an agent makes an outbound HTTP request."""
|
|
51
|
+
|
|
52
|
+
def condition(opts: GuardOptions) -> bool:
|
|
53
|
+
return opts.action == "http_request"
|
|
54
|
+
|
|
55
|
+
return GuardrailRule(
|
|
56
|
+
name="no-external-api-calls",
|
|
57
|
+
description="Alert: agent is making a call to an external API.",
|
|
58
|
+
condition=condition,
|
|
59
|
+
action="alert",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def no_pii_in_logs() -> GuardrailRule:
|
|
64
|
+
"""Block actions that appear to include PII (email addresses, phone numbers) in payloads."""
|
|
65
|
+
import re
|
|
66
|
+
|
|
67
|
+
EMAIL_RE = re.compile(r"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}")
|
|
68
|
+
PHONE_RE = re.compile(r"\b(\+?\d[\d\s\-().]{7,}\d)\b")
|
|
69
|
+
|
|
70
|
+
def condition(opts: GuardOptions) -> bool:
|
|
71
|
+
payload_str = str(opts.payload)
|
|
72
|
+
return bool(EMAIL_RE.search(payload_str) or PHONE_RE.search(payload_str))
|
|
73
|
+
|
|
74
|
+
return GuardrailRule(
|
|
75
|
+
name="no-pii-in-logs",
|
|
76
|
+
description="Alert: payload appears to contain PII (email or phone number).",
|
|
77
|
+
condition=condition,
|
|
78
|
+
action="alert",
|
|
79
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any, Callable, Awaitable, Dict, Optional, Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class GuardOptions:
|
|
7
|
+
"""Options for a single guard evaluation."""
|
|
8
|
+
action: str
|
|
9
|
+
payload: Dict[str, Any]
|
|
10
|
+
agent_id: str
|
|
11
|
+
user_id: Optional[str] = None
|
|
12
|
+
tags: Optional[Dict[str, str]] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class GuardResult:
|
|
17
|
+
"""Result of a guard evaluation."""
|
|
18
|
+
approved: bool
|
|
19
|
+
audit_id: str
|
|
20
|
+
timestamp: str
|
|
21
|
+
latency_ms: int
|
|
22
|
+
reason: Optional[str] = None
|
|
23
|
+
matched_rule: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class GuardrailRule:
|
|
28
|
+
"""
|
|
29
|
+
A single guardrail rule.
|
|
30
|
+
|
|
31
|
+
condition: callable that receives GuardOptions and returns True to BLOCK.
|
|
32
|
+
action: 'block' | 'require_approval' | 'alert'
|
|
33
|
+
"""
|
|
34
|
+
name: str
|
|
35
|
+
description: str
|
|
36
|
+
condition: Callable[[GuardOptions], Union[bool, Awaitable[bool]]]
|
|
37
|
+
action: str = "block" # 'block' | 'require_approval' | 'alert'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class AuditEvent:
|
|
42
|
+
"""A record of a single agent action evaluation."""
|
|
43
|
+
audit_id: str
|
|
44
|
+
agent_id: str
|
|
45
|
+
action: str
|
|
46
|
+
payload: Dict[str, Any]
|
|
47
|
+
approved: bool
|
|
48
|
+
timestamp: str
|
|
49
|
+
environment: str
|
|
50
|
+
sdk_version: str
|
|
51
|
+
user_id: Optional[str] = None
|
|
52
|
+
reason: Optional[str] = None
|
|
53
|
+
matched_rule: Optional[str] = None
|
|
54
|
+
tags: Optional[Dict[str, str]] = None
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: auditail
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Guardrails that block risky actions, plus a tamper-evident audit trail for autonomous AI agents.
|
|
5
|
+
Author: Auditail
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://auditail.io
|
|
8
|
+
Project-URL: Documentation, https://auditail.io/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/auditailhq/auditail
|
|
10
|
+
Project-URL: Request an API key, https://auditail.io/waitlist
|
|
11
|
+
Keywords: ai,agents,security,guardrails,audit,compliance,soc2,langchain
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# auditail (Python SDK)
|
|
26
|
+
|
|
27
|
+
> The security layer for autonomous AI agents. Guardrails that block risky actions, and a tamper-evident audit trail of everything your agents do.
|
|
28
|
+
|
|
29
|
+
**[๐ Full Documentation](https://auditail.io/docs)** ยท **[๐ Join the Private Beta](https://auditail.io/waitlist)**
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install auditail
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from auditail import Auditail, rules
|
|
43
|
+
|
|
44
|
+
aegis = Auditail(api_key="your_key")
|
|
45
|
+
|
|
46
|
+
aegis.add_rules([
|
|
47
|
+
rules.no_external_email(internal_domains=["acme.com"]),
|
|
48
|
+
rules.no_data_deletion(),
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
result = aegis.guard(
|
|
52
|
+
action="send_email",
|
|
53
|
+
payload={"to": "client@example.com", "body": "Report attached."},
|
|
54
|
+
agent_id="financial-bot-01",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if result.approved:
|
|
58
|
+
send_email(...)
|
|
59
|
+
else:
|
|
60
|
+
print(f"Blocked: {result.reason}")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## LangChain Integration
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from langchain.agents import tool
|
|
67
|
+
from auditail import Auditail, rules
|
|
68
|
+
|
|
69
|
+
aegis = Auditail(api_key="your_key")
|
|
70
|
+
aegis.add_rule(rules.no_external_email(internal_domains=["acme.com"]))
|
|
71
|
+
|
|
72
|
+
@tool
|
|
73
|
+
def send_email(to: str, body: str) -> str:
|
|
74
|
+
"""Send an email to a recipient."""
|
|
75
|
+
result = aegis.guard(
|
|
76
|
+
action="send_email",
|
|
77
|
+
payload={"to": to, "body": body},
|
|
78
|
+
agent_id="langchain-agent",
|
|
79
|
+
)
|
|
80
|
+
if not result.approved:
|
|
81
|
+
return f"Action blocked by Auditail: {result.reason}"
|
|
82
|
+
# ... actual send logic
|
|
83
|
+
return "Email sent."
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Private beta
|
|
87
|
+
|
|
88
|
+
The SDK, API and dashboard are live. Guardrail rules run locally in your process; every decision is delivered in the background to a **hash-chained audit log** you can browse and verify at [app.auditail.io](https://app.auditail.io).
|
|
89
|
+
|
|
90
|
+
- Batched, retried, idempotent delivery. `guard()` never waits on the network, and a delivery failure never reaches your agent.
|
|
91
|
+
- Secrets are redacted before an event leaves your process (`password`, `token`, `apiKey`, ... plus your own `redact_keys`).
|
|
92
|
+
- Call `aegis.flush()` before a serverless function returns or the process exits.
|
|
93
|
+
|
|
94
|
+
**[Request an API key](https://auditail.io/waitlist)**: keys are issued to design partners personally.
|
|
95
|
+
|
|
96
|
+
**Roadmap** (not available yet): Slack/PagerDuty alerts, human-in-the-loop approval (`require_approval` currently behaves like `block`), compliance export, private-cloud deployment.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
auditail
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "auditail"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Guardrails that block risky actions, plus a tamper-evident audit trail for autonomous AI agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Auditail" }]
|
|
13
|
+
keywords = ["ai", "agents", "security", "guardrails", "audit", "compliance", "soc2", "langchain"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
"Topic :: Security",
|
|
23
|
+
]
|
|
24
|
+
requires-python = ">=3.10"
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://auditail.io"
|
|
29
|
+
Documentation = "https://auditail.io/docs"
|
|
30
|
+
Repository = "https://github.com/auditailhq/auditail"
|
|
31
|
+
"Request an API key" = "https://auditail.io/waitlist"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["."]
|
|
35
|
+
include = ["auditail*"]
|
auditail-0.2.0/setup.cfg
ADDED