authe 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.
- authe-0.1.0/.gitignore +12 -0
- authe-0.1.0/LICENSE +21 -0
- authe-0.1.0/PKG-INFO +142 -0
- authe-0.1.0/README.md +110 -0
- authe-0.1.0/authe/__init__.py +75 -0
- authe-0.1.0/authe/client.py +279 -0
- authe-0.1.0/authe/config.py +47 -0
- authe-0.1.0/authe/instrumentor.py +310 -0
- authe-0.1.0/examples/example_manual.py +42 -0
- authe-0.1.0/examples/example_openai.py +38 -0
- authe-0.1.0/pyproject.toml +52 -0
- authe-0.1.0/tests/__init__.py +0 -0
- authe-0.1.0/tests/test_config.py +63 -0
authe-0.1.0/.gitignore
ADDED
authe-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 authe.me
|
|
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.
|
authe-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: authe
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The trust layer for AI agents. Observability, identity, and reputation in one line of code.
|
|
5
|
+
Project-URL: Homepage, https://authe.me
|
|
6
|
+
Project-URL: Documentation, https://docs.authe.me
|
|
7
|
+
Project-URL: Repository, https://github.com/autheme/authe-python
|
|
8
|
+
Project-URL: Issues, https://github.com/autheme/authe-python/issues
|
|
9
|
+
Author-email: "authe.me" <hello@authe.me>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,ai,identity,observability,security,trust
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: httpx>=0.25.0
|
|
26
|
+
Requires-Dist: wrapt>=1.15.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# authe
|
|
34
|
+
|
|
35
|
+
The trust layer for AI agents. Observability, identity, and reputation in one line of code.
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/authe/)
|
|
38
|
+
[](https://opensource.org/licenses/MIT)
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install authe
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import authe
|
|
50
|
+
authe.init()
|
|
51
|
+
|
|
52
|
+
# That's it. Your agent is now observable.
|
|
53
|
+
# View actions at dashboard.authe.me
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## What it does
|
|
57
|
+
|
|
58
|
+
`authe.init()` automatically instruments your agent and captures:
|
|
59
|
+
|
|
60
|
+
- **Every tool call** — function name, inputs, outputs, duration
|
|
61
|
+
- **LLM requests** — model, message count, tool usage
|
|
62
|
+
- **File operations** — writes tracked with path and mode
|
|
63
|
+
- **System commands** — subprocess calls captured
|
|
64
|
+
- **Scope violations** — alerts when agent exceeds declared capabilities
|
|
65
|
+
|
|
66
|
+
All actions are sent to your dashboard at [dashboard.authe.me](https://dashboard.authe.me) with a tamper-proof audit trail.
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import authe
|
|
72
|
+
|
|
73
|
+
authe.init(
|
|
74
|
+
api_key="ak_xxxxx", # or set AUTHE_API_KEY env var
|
|
75
|
+
agent_name="my-agent", # auto-detected if not set
|
|
76
|
+
capabilities=["read:email", "write:file"], # declared permissions
|
|
77
|
+
redact_pii=True, # redact sensitive fields
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Environment Variables
|
|
82
|
+
|
|
83
|
+
| Variable | Description |
|
|
84
|
+
|----------|-------------|
|
|
85
|
+
| `AUTHE_API_KEY` | Your API key (required) |
|
|
86
|
+
| `AUTHE_AGENT_NAME` | Agent name (optional, auto-detected) |
|
|
87
|
+
|
|
88
|
+
## Manual Tracking
|
|
89
|
+
|
|
90
|
+
For custom tools that aren't auto-detected:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from authe.instrumentor import track
|
|
94
|
+
|
|
95
|
+
@track("send_email")
|
|
96
|
+
def send_email(to, subject, body):
|
|
97
|
+
# your code here
|
|
98
|
+
pass
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Or track directly:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from authe import get_client
|
|
105
|
+
|
|
106
|
+
client = get_client()
|
|
107
|
+
client.track_action(
|
|
108
|
+
tool="custom_tool",
|
|
109
|
+
input_data={"key": "value"},
|
|
110
|
+
output_data={"result": "done"},
|
|
111
|
+
status="success",
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Supported Frameworks
|
|
116
|
+
|
|
117
|
+
| Framework | Auto-instrumented |
|
|
118
|
+
|-----------|:-:|
|
|
119
|
+
| OpenAI | ✅ |
|
|
120
|
+
| LangChain | ✅ |
|
|
121
|
+
| CrewAI | 🔜 |
|
|
122
|
+
| AutoGPT | 🔜 |
|
|
123
|
+
| Custom agents | via `@track` decorator |
|
|
124
|
+
|
|
125
|
+
## How it works
|
|
126
|
+
|
|
127
|
+
1. `authe.init()` registers your agent and gets a short-lived token
|
|
128
|
+
2. Actions are captured via monkey-patching (zero code changes needed)
|
|
129
|
+
3. Actions are batched and sent to the API every 5 seconds
|
|
130
|
+
4. Your dashboard shows a real-time timeline of everything your agent did
|
|
131
|
+
5. Scope violations trigger alerts automatically
|
|
132
|
+
|
|
133
|
+
## Links
|
|
134
|
+
|
|
135
|
+
- **Website**: [authe.me](https://authe.me)
|
|
136
|
+
- **Dashboard**: [dashboard.authe.me](https://dashboard.authe.me)
|
|
137
|
+
- **API Docs**: [docs.authe.me](https://docs.authe.me)
|
|
138
|
+
- **GitHub**: [github.com/autheme](https://github.com/autheme)
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
authe-0.1.0/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# authe
|
|
2
|
+
|
|
3
|
+
The trust layer for AI agents. Observability, identity, and reputation in one line of code.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/authe/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install authe
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import authe
|
|
18
|
+
authe.init()
|
|
19
|
+
|
|
20
|
+
# That's it. Your agent is now observable.
|
|
21
|
+
# View actions at dashboard.authe.me
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What it does
|
|
25
|
+
|
|
26
|
+
`authe.init()` automatically instruments your agent and captures:
|
|
27
|
+
|
|
28
|
+
- **Every tool call** — function name, inputs, outputs, duration
|
|
29
|
+
- **LLM requests** — model, message count, tool usage
|
|
30
|
+
- **File operations** — writes tracked with path and mode
|
|
31
|
+
- **System commands** — subprocess calls captured
|
|
32
|
+
- **Scope violations** — alerts when agent exceeds declared capabilities
|
|
33
|
+
|
|
34
|
+
All actions are sent to your dashboard at [dashboard.authe.me](https://dashboard.authe.me) with a tamper-proof audit trail.
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import authe
|
|
40
|
+
|
|
41
|
+
authe.init(
|
|
42
|
+
api_key="ak_xxxxx", # or set AUTHE_API_KEY env var
|
|
43
|
+
agent_name="my-agent", # auto-detected if not set
|
|
44
|
+
capabilities=["read:email", "write:file"], # declared permissions
|
|
45
|
+
redact_pii=True, # redact sensitive fields
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Environment Variables
|
|
50
|
+
|
|
51
|
+
| Variable | Description |
|
|
52
|
+
|----------|-------------|
|
|
53
|
+
| `AUTHE_API_KEY` | Your API key (required) |
|
|
54
|
+
| `AUTHE_AGENT_NAME` | Agent name (optional, auto-detected) |
|
|
55
|
+
|
|
56
|
+
## Manual Tracking
|
|
57
|
+
|
|
58
|
+
For custom tools that aren't auto-detected:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from authe.instrumentor import track
|
|
62
|
+
|
|
63
|
+
@track("send_email")
|
|
64
|
+
def send_email(to, subject, body):
|
|
65
|
+
# your code here
|
|
66
|
+
pass
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or track directly:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from authe import get_client
|
|
73
|
+
|
|
74
|
+
client = get_client()
|
|
75
|
+
client.track_action(
|
|
76
|
+
tool="custom_tool",
|
|
77
|
+
input_data={"key": "value"},
|
|
78
|
+
output_data={"result": "done"},
|
|
79
|
+
status="success",
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Supported Frameworks
|
|
84
|
+
|
|
85
|
+
| Framework | Auto-instrumented |
|
|
86
|
+
|-----------|:-:|
|
|
87
|
+
| OpenAI | ✅ |
|
|
88
|
+
| LangChain | ✅ |
|
|
89
|
+
| CrewAI | 🔜 |
|
|
90
|
+
| AutoGPT | 🔜 |
|
|
91
|
+
| Custom agents | via `@track` decorator |
|
|
92
|
+
|
|
93
|
+
## How it works
|
|
94
|
+
|
|
95
|
+
1. `authe.init()` registers your agent and gets a short-lived token
|
|
96
|
+
2. Actions are captured via monkey-patching (zero code changes needed)
|
|
97
|
+
3. Actions are batched and sent to the API every 5 seconds
|
|
98
|
+
4. Your dashboard shows a real-time timeline of everything your agent did
|
|
99
|
+
5. Scope violations trigger alerts automatically
|
|
100
|
+
|
|
101
|
+
## Links
|
|
102
|
+
|
|
103
|
+
- **Website**: [authe.me](https://authe.me)
|
|
104
|
+
- **Dashboard**: [dashboard.authe.me](https://dashboard.authe.me)
|
|
105
|
+
- **API Docs**: [docs.authe.me](https://docs.authe.me)
|
|
106
|
+
- **GitHub**: [github.com/autheme](https://github.com/autheme)
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
authe — The trust layer for AI agents.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
import authe
|
|
6
|
+
authe.init()
|
|
7
|
+
|
|
8
|
+
That's it. Your agent is now observable.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from authe.client import AutheClient
|
|
12
|
+
from authe.config import AutheConfig
|
|
13
|
+
from authe.instrumentor import Instrumentor
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
__all__ = ["init", "get_client"]
|
|
17
|
+
|
|
18
|
+
# Global client instance
|
|
19
|
+
_client: AutheClient | None = None
|
|
20
|
+
_instrumentor: Instrumentor | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def init(
|
|
24
|
+
api_key: str | None = None,
|
|
25
|
+
agent_name: str | None = None,
|
|
26
|
+
capabilities: list[str] | None = None,
|
|
27
|
+
base_url: str = "https://api.authe.me",
|
|
28
|
+
auto_instrument: bool = True,
|
|
29
|
+
redact_pii: bool = False,
|
|
30
|
+
debug: bool = False,
|
|
31
|
+
) -> AutheClient:
|
|
32
|
+
"""
|
|
33
|
+
Initialize authe.me agent observability.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
api_key: Your authe.me API key. Falls back to AUTHE_API_KEY env var.
|
|
37
|
+
agent_name: Name for this agent. Falls back to AUTHE_AGENT_NAME or auto-detected.
|
|
38
|
+
capabilities: Declared capabilities (e.g. ["read:email", "write:file"]).
|
|
39
|
+
base_url: API endpoint. Default: https://api.authe.me
|
|
40
|
+
auto_instrument: Auto-instrument detected frameworks. Default: True.
|
|
41
|
+
redact_pii: Redact potentially sensitive data from logs. Default: False.
|
|
42
|
+
debug: Enable debug logging. Default: False.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
AutheClient instance.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
import authe
|
|
49
|
+
authe.init()
|
|
50
|
+
"""
|
|
51
|
+
global _client, _instrumentor
|
|
52
|
+
|
|
53
|
+
config = AutheConfig(
|
|
54
|
+
api_key=api_key,
|
|
55
|
+
agent_name=agent_name,
|
|
56
|
+
capabilities=capabilities or [],
|
|
57
|
+
base_url=base_url,
|
|
58
|
+
auto_instrument=auto_instrument,
|
|
59
|
+
redact_pii=redact_pii,
|
|
60
|
+
debug=debug,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
_client = AutheClient(config)
|
|
64
|
+
_client.register_or_authenticate()
|
|
65
|
+
|
|
66
|
+
if auto_instrument:
|
|
67
|
+
_instrumentor = Instrumentor(_client)
|
|
68
|
+
_instrumentor.auto_instrument()
|
|
69
|
+
|
|
70
|
+
return _client
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_client() -> AutheClient | None:
|
|
74
|
+
"""Get the global AutheClient instance."""
|
|
75
|
+
return _client
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""HTTP client for communicating with the authe.me API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from authe.config import AutheConfig
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("authe")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AutheClient:
|
|
22
|
+
"""Client that manages agent registration, token refresh, and action batching."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config: AutheConfig):
|
|
25
|
+
self.config = config
|
|
26
|
+
self._http = httpx.Client(
|
|
27
|
+
base_url=config.base_url,
|
|
28
|
+
timeout=30.0,
|
|
29
|
+
headers={"Content-Type": "application/json"},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Session
|
|
33
|
+
self.config.session_id = f"ses_{uuid.uuid4().hex[:16]}"
|
|
34
|
+
|
|
35
|
+
# Action buffer — batches actions before sending
|
|
36
|
+
self._buffer: list[dict] = []
|
|
37
|
+
self._buffer_lock = threading.Lock()
|
|
38
|
+
self._flush_interval = 5.0 # seconds
|
|
39
|
+
self._max_buffer_size = 100
|
|
40
|
+
|
|
41
|
+
# Start background flush thread
|
|
42
|
+
self._running = True
|
|
43
|
+
self._flush_thread = threading.Thread(target=self._flush_loop, daemon=True)
|
|
44
|
+
self._flush_thread.start()
|
|
45
|
+
|
|
46
|
+
# Flush on exit
|
|
47
|
+
atexit.register(self.flush)
|
|
48
|
+
|
|
49
|
+
# Token refresh
|
|
50
|
+
self._token_lock = threading.Lock()
|
|
51
|
+
self._token_expires_at: float = 0
|
|
52
|
+
|
|
53
|
+
if config.debug:
|
|
54
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
55
|
+
logger.setLevel(logging.DEBUG)
|
|
56
|
+
|
|
57
|
+
def register_or_authenticate(self):
|
|
58
|
+
"""Register the agent or authenticate if it already exists."""
|
|
59
|
+
try:
|
|
60
|
+
# First, try to register a new agent
|
|
61
|
+
resp = self._http.post(
|
|
62
|
+
"/v1/agents",
|
|
63
|
+
json={
|
|
64
|
+
"name": self.config.agent_name,
|
|
65
|
+
"description": f"Auto-registered by authe SDK v{self._get_version()}",
|
|
66
|
+
"framework": self._detect_framework(),
|
|
67
|
+
"capabilities": self.config.capabilities,
|
|
68
|
+
},
|
|
69
|
+
headers={"Authorization": f"Bearer {self.config.api_key}"},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if resp.status_code == 201:
|
|
73
|
+
data = resp.json()
|
|
74
|
+
self.config.agent_id = data["agent"]["id"]
|
|
75
|
+
logger.info(f"authe.me: registered agent '{self.config.agent_name}' ({self.config.agent_id})")
|
|
76
|
+
self._refresh_token()
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
if resp.status_code == 409:
|
|
80
|
+
# Agent already exists — fetch it and get a token
|
|
81
|
+
logger.debug("authe.me: agent already registered, fetching token")
|
|
82
|
+
self._fetch_existing_agent()
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
logger.warning(f"authe.me: registration returned {resp.status_code}: {resp.text}")
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.warning(f"authe.me: failed to register agent: {e}")
|
|
89
|
+
logger.warning("authe.me: running in offline mode — actions will be buffered locally")
|
|
90
|
+
|
|
91
|
+
def _fetch_existing_agent(self):
|
|
92
|
+
"""Fetch existing agents and find ours by name."""
|
|
93
|
+
try:
|
|
94
|
+
resp = self._http.get(
|
|
95
|
+
"/v1/agents",
|
|
96
|
+
headers={"Authorization": f"Bearer {self.config.api_key}"},
|
|
97
|
+
)
|
|
98
|
+
if resp.status_code == 200:
|
|
99
|
+
agents = resp.json().get("agents", [])
|
|
100
|
+
for agent in agents:
|
|
101
|
+
if agent["name"] == self.config.agent_name:
|
|
102
|
+
self.config.agent_id = agent["id"]
|
|
103
|
+
logger.info(f"authe.me: connected to agent '{self.config.agent_name}' ({self.config.agent_id})")
|
|
104
|
+
self._refresh_token()
|
|
105
|
+
return
|
|
106
|
+
logger.warning("authe.me: could not find existing agent")
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.warning(f"authe.me: failed to fetch agents: {e}")
|
|
109
|
+
|
|
110
|
+
def _refresh_token(self):
|
|
111
|
+
"""Get a short-lived JWT token for the agent."""
|
|
112
|
+
if not self.config.agent_id:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
with self._token_lock:
|
|
116
|
+
try:
|
|
117
|
+
resp = self._http.get(
|
|
118
|
+
f"/v1/agents/{self.config.agent_id}/token",
|
|
119
|
+
headers={"Authorization": f"Bearer {self.config.api_key}"},
|
|
120
|
+
)
|
|
121
|
+
if resp.status_code == 200:
|
|
122
|
+
data = resp.json()
|
|
123
|
+
self.config.agent_token = data["token"]
|
|
124
|
+
self._token_expires_at = time.time() + data.get("expires_in", 900) - 60 # refresh 60s early
|
|
125
|
+
logger.debug("authe.me: agent token refreshed")
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.warning(f"authe.me: failed to refresh token: {e}")
|
|
128
|
+
|
|
129
|
+
def _ensure_token(self):
|
|
130
|
+
"""Ensure we have a valid agent token."""
|
|
131
|
+
if time.time() >= self._token_expires_at:
|
|
132
|
+
self._refresh_token()
|
|
133
|
+
|
|
134
|
+
# ─── Action Tracking ───
|
|
135
|
+
|
|
136
|
+
def track_action(
|
|
137
|
+
self,
|
|
138
|
+
tool: str,
|
|
139
|
+
action_type: str = "tool_call",
|
|
140
|
+
input_data: dict[str, Any] | None = None,
|
|
141
|
+
output_data: dict[str, Any] | None = None,
|
|
142
|
+
status: str = "success",
|
|
143
|
+
duration_ms: int = 0,
|
|
144
|
+
signature: str = "",
|
|
145
|
+
):
|
|
146
|
+
"""
|
|
147
|
+
Record an agent action.
|
|
148
|
+
|
|
149
|
+
This is called automatically by instrumentors, but can also be called manually:
|
|
150
|
+
|
|
151
|
+
from authe import get_client
|
|
152
|
+
client = get_client()
|
|
153
|
+
client.track_action("send_email", input_data={"to": "bob@example.com"})
|
|
154
|
+
"""
|
|
155
|
+
action = {
|
|
156
|
+
"session_id": self.config.session_id,
|
|
157
|
+
"type": action_type,
|
|
158
|
+
"tool": tool,
|
|
159
|
+
"input": self._maybe_redact(input_data or {}),
|
|
160
|
+
"output": self._maybe_redact(output_data or {}),
|
|
161
|
+
"status": status,
|
|
162
|
+
"duration_ms": duration_ms,
|
|
163
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
164
|
+
"signature": signature,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
with self._buffer_lock:
|
|
168
|
+
self._buffer.append(action)
|
|
169
|
+
if len(self._buffer) >= self._max_buffer_size:
|
|
170
|
+
self._send_batch()
|
|
171
|
+
|
|
172
|
+
def flush(self):
|
|
173
|
+
"""Flush buffered actions to the API."""
|
|
174
|
+
with self._buffer_lock:
|
|
175
|
+
self._send_batch()
|
|
176
|
+
|
|
177
|
+
def _send_batch(self):
|
|
178
|
+
"""Send buffered actions to the API. Must be called with _buffer_lock held."""
|
|
179
|
+
if not self._buffer or not self.config.agent_id:
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
batch = self._buffer.copy()
|
|
183
|
+
self._buffer.clear()
|
|
184
|
+
|
|
185
|
+
self._ensure_token()
|
|
186
|
+
|
|
187
|
+
if not self.config.agent_token:
|
|
188
|
+
logger.warning(f"authe.me: no token, dropping {len(batch)} actions")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
resp = self._http.post(
|
|
193
|
+
"/v1/ingest",
|
|
194
|
+
json={
|
|
195
|
+
"agent_id": self.config.agent_id,
|
|
196
|
+
"actions": batch,
|
|
197
|
+
},
|
|
198
|
+
headers={"Authorization": f"Bearer {self.config.agent_token}"},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if resp.status_code == 200:
|
|
202
|
+
data = resp.json()
|
|
203
|
+
logger.debug(
|
|
204
|
+
f"authe.me: sent {data.get('inserted', 0)} actions "
|
|
205
|
+
f"({data.get('alerts', 0)} alerts)"
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
logger.warning(f"authe.me: ingest returned {resp.status_code}: {resp.text}")
|
|
209
|
+
# Put actions back in buffer for retry
|
|
210
|
+
self._buffer = batch + self._buffer
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.warning(f"authe.me: failed to send batch: {e}")
|
|
214
|
+
# Put actions back in buffer for retry
|
|
215
|
+
self._buffer = batch + self._buffer
|
|
216
|
+
|
|
217
|
+
def _flush_loop(self):
|
|
218
|
+
"""Background thread that flushes the buffer periodically."""
|
|
219
|
+
while self._running:
|
|
220
|
+
time.sleep(self._flush_interval)
|
|
221
|
+
try:
|
|
222
|
+
self.flush()
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
# ─── Helpers ───
|
|
227
|
+
|
|
228
|
+
def _maybe_redact(self, data: dict) -> dict:
|
|
229
|
+
"""Optionally redact sensitive-looking fields."""
|
|
230
|
+
if not self.config.redact_pii:
|
|
231
|
+
return data
|
|
232
|
+
|
|
233
|
+
redacted = {}
|
|
234
|
+
sensitive_keys = {"password", "token", "secret", "key", "authorization", "cookie", "ssn", "credit_card"}
|
|
235
|
+
|
|
236
|
+
for k, v in data.items():
|
|
237
|
+
if any(s in k.lower() for s in sensitive_keys):
|
|
238
|
+
redacted[k] = "[REDACTED]"
|
|
239
|
+
elif isinstance(v, dict):
|
|
240
|
+
redacted[k] = self._maybe_redact(v)
|
|
241
|
+
else:
|
|
242
|
+
redacted[k] = v
|
|
243
|
+
|
|
244
|
+
return redacted
|
|
245
|
+
|
|
246
|
+
def _detect_framework(self) -> str:
|
|
247
|
+
"""Detect which agent framework is being used."""
|
|
248
|
+
try:
|
|
249
|
+
import openai # noqa
|
|
250
|
+
return "openai"
|
|
251
|
+
except ImportError:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
import langchain # noqa
|
|
256
|
+
return "langchain"
|
|
257
|
+
except ImportError:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
import crewai # noqa
|
|
262
|
+
return "crewai"
|
|
263
|
+
except ImportError:
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
return "custom"
|
|
267
|
+
|
|
268
|
+
def _get_version(self) -> str:
|
|
269
|
+
try:
|
|
270
|
+
from authe import __version__
|
|
271
|
+
return __version__
|
|
272
|
+
except Exception:
|
|
273
|
+
return "0.1.0"
|
|
274
|
+
|
|
275
|
+
def close(self):
|
|
276
|
+
"""Shutdown the client."""
|
|
277
|
+
self._running = False
|
|
278
|
+
self.flush()
|
|
279
|
+
self._http.close()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Configuration for the authe SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import socket
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class AutheConfig:
|
|
13
|
+
"""SDK configuration, resolved from args + environment variables."""
|
|
14
|
+
|
|
15
|
+
api_key: str | None = None
|
|
16
|
+
agent_name: str | None = None
|
|
17
|
+
capabilities: list[str] = field(default_factory=list)
|
|
18
|
+
base_url: str = "https://api.authe.me"
|
|
19
|
+
auto_instrument: bool = True
|
|
20
|
+
redact_pii: bool = False
|
|
21
|
+
debug: bool = False
|
|
22
|
+
|
|
23
|
+
# Internal — set after resolution
|
|
24
|
+
agent_id: str | None = None
|
|
25
|
+
agent_token: str | None = None
|
|
26
|
+
session_id: str | None = None
|
|
27
|
+
|
|
28
|
+
def __post_init__(self):
|
|
29
|
+
# Resolve from environment
|
|
30
|
+
self.api_key = self.api_key or os.environ.get("AUTHE_API_KEY")
|
|
31
|
+
self.agent_name = self.agent_name or os.environ.get("AUTHE_AGENT_NAME") or self._detect_agent_name()
|
|
32
|
+
self.base_url = self.base_url.rstrip("/")
|
|
33
|
+
|
|
34
|
+
if not self.api_key:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
"authe.me API key required. Pass api_key= to authe.init() "
|
|
37
|
+
"or set the AUTHE_API_KEY environment variable.\n"
|
|
38
|
+
"Get your key at https://authe.me"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def _detect_agent_name(self) -> str:
|
|
42
|
+
"""Auto-generate an agent name from the environment."""
|
|
43
|
+
# Try script filename
|
|
44
|
+
script = os.path.basename(sys.argv[0]) if sys.argv[0] else "agent"
|
|
45
|
+
script = script.replace(".py", "").replace("_", "-")
|
|
46
|
+
hostname = socket.gethostname()[:12]
|
|
47
|
+
return f"{script}-{hostname}"
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-instrumentation for AI agent frameworks.
|
|
3
|
+
|
|
4
|
+
Hooks into tool calls, API requests, and file operations
|
|
5
|
+
to capture actions without any code changes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import functools
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Callable
|
|
14
|
+
|
|
15
|
+
from authe.client import AutheClient
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("authe")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Instrumentor:
|
|
21
|
+
"""Automatically instruments detected agent frameworks."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, client: AutheClient):
|
|
24
|
+
self.client = client
|
|
25
|
+
self._patched: list[str] = []
|
|
26
|
+
|
|
27
|
+
def auto_instrument(self):
|
|
28
|
+
"""Detect and instrument all available frameworks."""
|
|
29
|
+
self._instrument_openai()
|
|
30
|
+
self._instrument_langchain()
|
|
31
|
+
self._instrument_subprocess()
|
|
32
|
+
self._instrument_file_ops()
|
|
33
|
+
|
|
34
|
+
if self._patched:
|
|
35
|
+
logger.info(f"authe.me: instrumented {', '.join(self._patched)}")
|
|
36
|
+
else:
|
|
37
|
+
logger.debug("authe.me: no frameworks detected, use client.track_action() manually")
|
|
38
|
+
|
|
39
|
+
# ─── OpenAI ───
|
|
40
|
+
|
|
41
|
+
def _instrument_openai(self):
|
|
42
|
+
"""Patch OpenAI client to capture completions and tool calls."""
|
|
43
|
+
try:
|
|
44
|
+
import openai
|
|
45
|
+
except ImportError:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
from openai.resources.chat import completions as chat_mod
|
|
50
|
+
|
|
51
|
+
original_create = chat_mod.Completions.create
|
|
52
|
+
|
|
53
|
+
@functools.wraps(original_create)
|
|
54
|
+
def patched_create(self_inner, *args, **kwargs):
|
|
55
|
+
start = time.time()
|
|
56
|
+
status = "success"
|
|
57
|
+
result = None
|
|
58
|
+
error_msg = None
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
result = original_create(self_inner, *args, **kwargs)
|
|
62
|
+
return result
|
|
63
|
+
except Exception as e:
|
|
64
|
+
status = "error"
|
|
65
|
+
error_msg = str(e)
|
|
66
|
+
raise
|
|
67
|
+
finally:
|
|
68
|
+
duration_ms = int((time.time() - start) * 1000)
|
|
69
|
+
|
|
70
|
+
# Extract tool calls from response
|
|
71
|
+
tool_calls = []
|
|
72
|
+
output = {}
|
|
73
|
+
|
|
74
|
+
if result and hasattr(result, "choices"):
|
|
75
|
+
for choice in result.choices:
|
|
76
|
+
msg = choice.message
|
|
77
|
+
if hasattr(msg, "tool_calls") and msg.tool_calls:
|
|
78
|
+
for tc in msg.tool_calls:
|
|
79
|
+
tool_calls.append({
|
|
80
|
+
"id": tc.id,
|
|
81
|
+
"function": tc.function.name,
|
|
82
|
+
"arguments": tc.function.arguments,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if hasattr(msg, "content") and msg.content:
|
|
86
|
+
output["content"] = msg.content[:500] # truncate
|
|
87
|
+
|
|
88
|
+
if tool_calls:
|
|
89
|
+
output["tool_calls"] = tool_calls
|
|
90
|
+
|
|
91
|
+
# Track the LLM call
|
|
92
|
+
self.client.track_action(
|
|
93
|
+
tool="openai.chat.completions.create",
|
|
94
|
+
action_type="llm_call",
|
|
95
|
+
input_data={
|
|
96
|
+
"model": kwargs.get("model", "unknown"),
|
|
97
|
+
"messages_count": len(kwargs.get("messages", [])),
|
|
98
|
+
"tools_count": len(kwargs.get("tools", [])),
|
|
99
|
+
"has_tools": bool(kwargs.get("tools")),
|
|
100
|
+
},
|
|
101
|
+
output_data=output if not error_msg else {"error": error_msg},
|
|
102
|
+
status=status,
|
|
103
|
+
duration_ms=duration_ms,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Track individual tool calls
|
|
107
|
+
for tc in tool_calls:
|
|
108
|
+
self.client.track_action(
|
|
109
|
+
tool=tc["function"],
|
|
110
|
+
action_type="tool_call",
|
|
111
|
+
input_data={"arguments": tc["arguments"]},
|
|
112
|
+
status="success",
|
|
113
|
+
duration_ms=0,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
chat_mod.Completions.create = patched_create
|
|
117
|
+
self._patched.append("openai")
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.debug(f"authe.me: failed to instrument openai: {e}")
|
|
121
|
+
|
|
122
|
+
# ─── LangChain ───
|
|
123
|
+
|
|
124
|
+
def _instrument_langchain(self):
|
|
125
|
+
"""Patch LangChain to capture tool invocations."""
|
|
126
|
+
try:
|
|
127
|
+
from langchain_core.tools import BaseTool
|
|
128
|
+
except ImportError:
|
|
129
|
+
try:
|
|
130
|
+
from langchain.tools import BaseTool
|
|
131
|
+
except ImportError:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
original_run = BaseTool.run
|
|
136
|
+
|
|
137
|
+
@functools.wraps(original_run)
|
|
138
|
+
def patched_run(self_inner, *args, **kwargs):
|
|
139
|
+
start = time.time()
|
|
140
|
+
status = "success"
|
|
141
|
+
result = None
|
|
142
|
+
error_msg = None
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
result = original_run(self_inner, *args, **kwargs)
|
|
146
|
+
return result
|
|
147
|
+
except Exception as e:
|
|
148
|
+
status = "error"
|
|
149
|
+
error_msg = str(e)
|
|
150
|
+
raise
|
|
151
|
+
finally:
|
|
152
|
+
duration_ms = int((time.time() - start) * 1000)
|
|
153
|
+
|
|
154
|
+
self.client.track_action(
|
|
155
|
+
tool=getattr(self_inner, "name", "langchain_tool"),
|
|
156
|
+
action_type="tool_call",
|
|
157
|
+
input_data={"args": str(args)[:500], "kwargs": _safe_serialize(kwargs)},
|
|
158
|
+
output_data={"result": str(result)[:500]} if not error_msg else {"error": error_msg},
|
|
159
|
+
status=status,
|
|
160
|
+
duration_ms=duration_ms,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
BaseTool.run = patched_run
|
|
164
|
+
self._patched.append("langchain")
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.debug(f"authe.me: failed to instrument langchain: {e}")
|
|
168
|
+
|
|
169
|
+
# ─── Subprocess (system commands) ───
|
|
170
|
+
|
|
171
|
+
def _instrument_subprocess(self):
|
|
172
|
+
"""Patch subprocess to capture system command execution."""
|
|
173
|
+
try:
|
|
174
|
+
import subprocess
|
|
175
|
+
|
|
176
|
+
original_run = subprocess.run
|
|
177
|
+
|
|
178
|
+
@functools.wraps(original_run)
|
|
179
|
+
def patched_run(*args, **kwargs):
|
|
180
|
+
start = time.time()
|
|
181
|
+
status = "success"
|
|
182
|
+
result = None
|
|
183
|
+
error_msg = None
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
result = original_run(*args, **kwargs)
|
|
187
|
+
if result.returncode != 0:
|
|
188
|
+
status = "error"
|
|
189
|
+
return result
|
|
190
|
+
except Exception as e:
|
|
191
|
+
status = "error"
|
|
192
|
+
error_msg = str(e)
|
|
193
|
+
raise
|
|
194
|
+
finally:
|
|
195
|
+
duration_ms = int((time.time() - start) * 1000)
|
|
196
|
+
|
|
197
|
+
cmd = args[0] if args else kwargs.get("args", "unknown")
|
|
198
|
+
if isinstance(cmd, list):
|
|
199
|
+
cmd = " ".join(str(c) for c in cmd)
|
|
200
|
+
|
|
201
|
+
self.client.track_action(
|
|
202
|
+
tool="subprocess.run",
|
|
203
|
+
action_type="system_command",
|
|
204
|
+
input_data={"command": str(cmd)[:500]},
|
|
205
|
+
output_data={
|
|
206
|
+
"returncode": result.returncode if result else None,
|
|
207
|
+
"stdout": str(result.stdout)[:200] if result and result.stdout else None,
|
|
208
|
+
} if not error_msg else {"error": error_msg},
|
|
209
|
+
status=status,
|
|
210
|
+
duration_ms=duration_ms,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
subprocess.run = patched_run
|
|
214
|
+
self._patched.append("subprocess")
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.debug(f"authe.me: failed to instrument subprocess: {e}")
|
|
218
|
+
|
|
219
|
+
# ─── File operations ───
|
|
220
|
+
|
|
221
|
+
def _instrument_file_ops(self):
|
|
222
|
+
"""Patch builtins.open to capture file read/write operations."""
|
|
223
|
+
import builtins
|
|
224
|
+
|
|
225
|
+
original_open = builtins.open
|
|
226
|
+
|
|
227
|
+
client = self.client
|
|
228
|
+
|
|
229
|
+
@functools.wraps(original_open)
|
|
230
|
+
def patched_open(file, mode="r", *args, **kwargs):
|
|
231
|
+
result = original_open(file, mode, *args, **kwargs)
|
|
232
|
+
|
|
233
|
+
# Only track write operations — reads are too noisy
|
|
234
|
+
if any(m in str(mode) for m in ("w", "a", "x")):
|
|
235
|
+
client.track_action(
|
|
236
|
+
tool="file.write",
|
|
237
|
+
action_type="file_operation",
|
|
238
|
+
input_data={"path": str(file), "mode": str(mode)},
|
|
239
|
+
status="success",
|
|
240
|
+
duration_ms=0,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
builtins.open = patched_open
|
|
246
|
+
self._patched.append("file_ops")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ─── Decorator for manual instrumentation ───
|
|
250
|
+
|
|
251
|
+
def track(tool_name: str | None = None):
|
|
252
|
+
"""
|
|
253
|
+
Decorator to manually track a function as an agent action.
|
|
254
|
+
|
|
255
|
+
Usage:
|
|
256
|
+
from authe.instrumentor import track
|
|
257
|
+
|
|
258
|
+
@track("send_email")
|
|
259
|
+
def send_email(to, subject, body):
|
|
260
|
+
...
|
|
261
|
+
"""
|
|
262
|
+
def decorator(func: Callable) -> Callable:
|
|
263
|
+
@functools.wraps(func)
|
|
264
|
+
def wrapper(*args, **kwargs):
|
|
265
|
+
from authe import get_client
|
|
266
|
+
client = get_client()
|
|
267
|
+
|
|
268
|
+
name = tool_name or func.__name__
|
|
269
|
+
start = time.time()
|
|
270
|
+
status = "success"
|
|
271
|
+
result = None
|
|
272
|
+
error_msg = None
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
result = func(*args, **kwargs)
|
|
276
|
+
return result
|
|
277
|
+
except Exception as e:
|
|
278
|
+
status = "error"
|
|
279
|
+
error_msg = str(e)
|
|
280
|
+
raise
|
|
281
|
+
finally:
|
|
282
|
+
duration_ms = int((time.time() - start) * 1000)
|
|
283
|
+
if client:
|
|
284
|
+
client.track_action(
|
|
285
|
+
tool=name,
|
|
286
|
+
input_data=_safe_serialize(kwargs) if kwargs else {"args": str(args)[:500]},
|
|
287
|
+
output_data={"result": str(result)[:500]} if not error_msg else {"error": error_msg},
|
|
288
|
+
status=status,
|
|
289
|
+
duration_ms=duration_ms,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return wrapper
|
|
293
|
+
return decorator
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _safe_serialize(data: Any, max_depth: int = 3) -> dict:
|
|
297
|
+
"""Safely serialize data for logging, handling non-JSON types."""
|
|
298
|
+
if max_depth <= 0:
|
|
299
|
+
return {"_truncated": True}
|
|
300
|
+
|
|
301
|
+
if isinstance(data, dict):
|
|
302
|
+
return {str(k): _safe_serialize(v, max_depth - 1) for k, v in data.items()}
|
|
303
|
+
elif isinstance(data, (list, tuple)):
|
|
304
|
+
return {"_list": [_safe_serialize(v, max_depth - 1) for v in data[:20]]}
|
|
305
|
+
elif isinstance(data, (str, int, float, bool, type(None))):
|
|
306
|
+
if isinstance(data, str) and len(data) > 500:
|
|
307
|
+
return data[:500] + "..."
|
|
308
|
+
return data
|
|
309
|
+
else:
|
|
310
|
+
return str(data)[:500]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example: Manual action tracking with the @track decorator.
|
|
3
|
+
|
|
4
|
+
Use this when you have custom tools that aren't auto-detected.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import authe
|
|
8
|
+
from authe.instrumentor import track
|
|
9
|
+
|
|
10
|
+
authe.init(
|
|
11
|
+
agent_name="custom-agent",
|
|
12
|
+
capabilities=["read:email", "send:email", "read:calendar"],
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@track("read_inbox")
|
|
17
|
+
def read_inbox(account: str, limit: int = 10):
|
|
18
|
+
"""Custom tool: read emails from inbox."""
|
|
19
|
+
# Your actual email reading logic here
|
|
20
|
+
return [
|
|
21
|
+
{"from": "alice@example.com", "subject": "Meeting tomorrow"},
|
|
22
|
+
{"from": "bob@example.com", "subject": "Project update"},
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@track("send_email")
|
|
27
|
+
def send_email(to: str, subject: str, body: str):
|
|
28
|
+
"""Custom tool: send an email."""
|
|
29
|
+
# Your actual email sending logic here
|
|
30
|
+
print(f"Sending email to {to}: {subject}")
|
|
31
|
+
return {"status": "sent", "message_id": "msg_123"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Use your tools normally — authe tracks everything
|
|
35
|
+
emails = read_inbox("me@company.com", limit=5)
|
|
36
|
+
send_email("alice@example.com", "Re: Meeting tomorrow", "Sounds good, see you there!")
|
|
37
|
+
|
|
38
|
+
# Flush to ensure all actions are sent
|
|
39
|
+
client = authe.get_client()
|
|
40
|
+
client.flush()
|
|
41
|
+
|
|
42
|
+
print("Done! Check dashboard.authe.me for the action timeline.")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example: Using authe.me with an OpenAI agent.
|
|
3
|
+
|
|
4
|
+
1. Get your API key at https://authe.me
|
|
5
|
+
2. Set it: export AUTHE_API_KEY=ak_xxxxx
|
|
6
|
+
3. Run this script: python example_openai.py
|
|
7
|
+
4. View your agent's actions at dashboard.authe.me
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import authe
|
|
11
|
+
|
|
12
|
+
# One line. That's it.
|
|
13
|
+
authe.init(
|
|
14
|
+
agent_name="inbox-summarizer",
|
|
15
|
+
capabilities=["read:email", "send:slack"],
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Your agent code runs normally
|
|
19
|
+
from openai import OpenAI
|
|
20
|
+
|
|
21
|
+
client = OpenAI()
|
|
22
|
+
|
|
23
|
+
response = client.chat.completions.create(
|
|
24
|
+
model="gpt-4",
|
|
25
|
+
messages=[
|
|
26
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
|
27
|
+
{"role": "user", "content": "Summarize the latest news about AI agents."},
|
|
28
|
+
],
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
print(response.choices[0].message.content)
|
|
32
|
+
|
|
33
|
+
# authe.me automatically captured:
|
|
34
|
+
# → the LLM call (model, token count, duration)
|
|
35
|
+
# → any tool calls the agent made
|
|
36
|
+
# → scope violations if the agent tried something unauthorized
|
|
37
|
+
#
|
|
38
|
+
# View it all at dashboard.authe.me
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "authe"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "The trust layer for AI agents. Observability, identity, and reputation in one line of code."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "authe.me", email = "hello@authe.me" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ai", "agents", "observability", "trust", "identity", "security"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Security",
|
|
27
|
+
"Topic :: Software Development :: Libraries",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"httpx>=0.25.0",
|
|
31
|
+
"wrapt>=1.15.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=7.0",
|
|
37
|
+
"pytest-asyncio>=0.21",
|
|
38
|
+
"ruff>=0.1.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://authe.me"
|
|
43
|
+
Documentation = "https://docs.authe.me"
|
|
44
|
+
Repository = "https://github.com/autheme/authe-python"
|
|
45
|
+
Issues = "https://github.com/autheme/authe-python/issues"
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 100
|
|
49
|
+
target-version = "py39"
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Basic tests for the authe SDK."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_config_requires_api_key():
|
|
8
|
+
"""Config should raise if no API key is provided."""
|
|
9
|
+
# Clear env var if set
|
|
10
|
+
os.environ.pop("AUTHE_API_KEY", None)
|
|
11
|
+
|
|
12
|
+
from authe.config import AutheConfig
|
|
13
|
+
|
|
14
|
+
with pytest.raises(ValueError, match="API key required"):
|
|
15
|
+
AutheConfig()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_config_from_env():
|
|
19
|
+
"""Config should read from environment variables."""
|
|
20
|
+
os.environ["AUTHE_API_KEY"] = "ak_test123"
|
|
21
|
+
os.environ["AUTHE_AGENT_NAME"] = "test-agent"
|
|
22
|
+
|
|
23
|
+
from authe.config import AutheConfig
|
|
24
|
+
|
|
25
|
+
config = AutheConfig()
|
|
26
|
+
assert config.api_key == "ak_test123"
|
|
27
|
+
assert config.agent_name == "test-agent"
|
|
28
|
+
assert config.base_url == "https://api.authe.me"
|
|
29
|
+
|
|
30
|
+
# Cleanup
|
|
31
|
+
del os.environ["AUTHE_API_KEY"]
|
|
32
|
+
del os.environ["AUTHE_AGENT_NAME"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_config_args_override_env():
|
|
36
|
+
"""Explicit args should override env vars."""
|
|
37
|
+
os.environ["AUTHE_API_KEY"] = "ak_from_env"
|
|
38
|
+
|
|
39
|
+
from authe.config import AutheConfig
|
|
40
|
+
|
|
41
|
+
config = AutheConfig(api_key="ak_from_args", agent_name="explicit-agent")
|
|
42
|
+
assert config.api_key == "ak_from_args"
|
|
43
|
+
assert config.agent_name == "explicit-agent"
|
|
44
|
+
|
|
45
|
+
del os.environ["AUTHE_API_KEY"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_safe_serialize():
|
|
49
|
+
"""_safe_serialize should handle nested data safely."""
|
|
50
|
+
from authe.instrumentor import _safe_serialize
|
|
51
|
+
|
|
52
|
+
result = _safe_serialize({"key": "value", "nested": {"a": 1}})
|
|
53
|
+
assert result["key"] == "value"
|
|
54
|
+
assert result["nested"]["a"] == 1
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_safe_serialize_truncates_strings():
|
|
58
|
+
"""Long strings should be truncated."""
|
|
59
|
+
from authe.instrumentor import _safe_serialize
|
|
60
|
+
|
|
61
|
+
long_string = "a" * 1000
|
|
62
|
+
result = _safe_serialize({"text": long_string})
|
|
63
|
+
assert len(result["text"]) < 600
|