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 ADDED
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.egg
8
+ .env
9
+ .venv/
10
+ venv/
11
+ .pytest_cache/
12
+ .ruff_cache/
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
+ [![PyPI](https://img.shields.io/pypi/v/authe)](https://pypi.org/project/authe/)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/authe)](https://pypi.org/project/authe/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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