lelu-agent-auth-sdk 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.
- lelu_agent_auth_sdk-0.1.0/.gitignore +41 -0
- lelu_agent_auth_sdk-0.1.0/PKG-INFO +67 -0
- lelu_agent_auth_sdk-0.1.0/README.md +44 -0
- lelu_agent_auth_sdk-0.1.0/auth_pe/__init__.py +66 -0
- lelu_agent_auth_sdk-0.1.0/auth_pe/autogpt_plugin.py +63 -0
- lelu_agent_auth_sdk-0.1.0/auth_pe/client.py +208 -0
- lelu_agent_auth_sdk-0.1.0/auth_pe/crewai.py +189 -0
- lelu_agent_auth_sdk-0.1.0/auth_pe/fastapi.py +101 -0
- lelu_agent_auth_sdk-0.1.0/auth_pe/langgraph.py +191 -0
- lelu_agent_auth_sdk-0.1.0/auth_pe/middleware.py +53 -0
- lelu_agent_auth_sdk-0.1.0/auth_pe/models.py +126 -0
- lelu_agent_auth_sdk-0.1.0/autogpt_plugin/plugin_config.json +18 -0
- lelu_agent_auth_sdk-0.1.0/lelu/__init__.py +7 -0
- lelu_agent_auth_sdk-0.1.0/lelu/autogpt_plugin.py +1 -0
- lelu_agent_auth_sdk-0.1.0/lelu/client.py +1 -0
- lelu_agent_auth_sdk-0.1.0/lelu/crewai.py +1 -0
- lelu_agent_auth_sdk-0.1.0/lelu/fastapi.py +1 -0
- lelu_agent_auth_sdk-0.1.0/lelu/langgraph.py +1 -0
- lelu_agent_auth_sdk-0.1.0/lelu/middleware.py +1 -0
- lelu_agent_auth_sdk-0.1.0/lelu/models.py +1 -0
- lelu_agent_auth_sdk-0.1.0/prism/__init__.py +7 -0
- lelu_agent_auth_sdk-0.1.0/prism/autogpt_plugin.py +1 -0
- lelu_agent_auth_sdk-0.1.0/prism/client.py +1 -0
- lelu_agent_auth_sdk-0.1.0/prism/crewai.py +1 -0
- lelu_agent_auth_sdk-0.1.0/prism/fastapi.py +1 -0
- lelu_agent_auth_sdk-0.1.0/prism/langgraph.py +1 -0
- lelu_agent_auth_sdk-0.1.0/prism/middleware.py +1 -0
- lelu_agent_auth_sdk-0.1.0/prism/models.py +1 -0
- lelu_agent_auth_sdk-0.1.0/pyproject.toml +51 -0
- lelu_agent_auth_sdk-0.1.0/tests/test_client.py +265 -0
- lelu_agent_auth_sdk-0.1.0/tests/test_langgraph.py +105 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Binaries
|
|
2
|
+
bin/
|
|
3
|
+
*.exe
|
|
4
|
+
|
|
5
|
+
# Go
|
|
6
|
+
engine/gen/
|
|
7
|
+
coverage.out
|
|
8
|
+
*.test
|
|
9
|
+
|
|
10
|
+
# Node
|
|
11
|
+
node_modules/
|
|
12
|
+
**/node_modules/
|
|
13
|
+
**/.next/
|
|
14
|
+
*.tsbuildinfo
|
|
15
|
+
sdk/typescript/node_modules/
|
|
16
|
+
sdk/typescript/dist/
|
|
17
|
+
platform/ui/node_modules/
|
|
18
|
+
platform/ui/.next/
|
|
19
|
+
|
|
20
|
+
# Python
|
|
21
|
+
sdk/python/__pycache__/
|
|
22
|
+
sdk/python/*.egg-info/
|
|
23
|
+
sdk/python/.pytest_cache/
|
|
24
|
+
sdk/python/.mypy_cache/
|
|
25
|
+
__pycache__/
|
|
26
|
+
*.pyc
|
|
27
|
+
|
|
28
|
+
# Docker
|
|
29
|
+
.docker/
|
|
30
|
+
|
|
31
|
+
# Local caches
|
|
32
|
+
engine/.gomodcache/
|
|
33
|
+
|
|
34
|
+
# Local env
|
|
35
|
+
.env
|
|
36
|
+
.env.*
|
|
37
|
+
!.env.example
|
|
38
|
+
|
|
39
|
+
# OS
|
|
40
|
+
.DS_Store
|
|
41
|
+
Thumbs.db
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lelu-agent-auth-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Lelu — the confidence-aware authorization engine for autonomous AI agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/lelu-auth/lelu
|
|
6
|
+
Project-URL: Repository, https://github.com/lelu-auth/lelu
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: ai-agents,auth,authorization,langchain,langgraph,permissions
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: httpx>=0.27.0
|
|
11
|
+
Requires-Dist: pydantic>=2.7.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest-httpx>=0.30.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest>=8.2.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
18
|
+
Provides-Extra: fastapi
|
|
19
|
+
Requires-Dist: fastapi>=0.111.0; extra == 'fastapi'
|
|
20
|
+
Provides-Extra: langgraph
|
|
21
|
+
Requires-Dist: langgraph>=0.1.0; extra == 'langgraph'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Lelu · Python SDK
|
|
25
|
+
|
|
26
|
+
Python client for [Lelu](https://github.com/lelu-auth/lelu) — the confidence-aware authorization engine for autonomous AI agents.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install lelu-agent-auth-sdk
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import asyncio
|
|
38
|
+
from lelu import LeluClient, AgentAuthRequest, AgentContext
|
|
39
|
+
|
|
40
|
+
async def main():
|
|
41
|
+
async with LeluClient(base_url="http://localhost:8082") as client:
|
|
42
|
+
result = await client.agent_authorize(AgentAuthRequest(
|
|
43
|
+
actor="invoice_bot",
|
|
44
|
+
action="invoice:create",
|
|
45
|
+
context=AgentContext(
|
|
46
|
+
confidence=0.92,
|
|
47
|
+
acting_for="user_123",
|
|
48
|
+
),
|
|
49
|
+
))
|
|
50
|
+
print(result.allowed, result.reason)
|
|
51
|
+
|
|
52
|
+
asyncio.run(main())
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## API
|
|
56
|
+
|
|
57
|
+
| Method | Description |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `agent_authorize(req)` | Confidence-aware agent authorization |
|
|
60
|
+
| `authorize(req)` | Human RBAC authorization |
|
|
61
|
+
| `mint_token(req)` | Mint a JIT-scoped JWT |
|
|
62
|
+
| `revoke_token(token_id)` | Revoke a token immediately |
|
|
63
|
+
| `is_healthy()` | Health-check the engine |
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Lelu · Python SDK
|
|
2
|
+
|
|
3
|
+
Python client for [Lelu](https://github.com/lelu-auth/lelu) — the confidence-aware authorization engine for autonomous AI agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install lelu-agent-auth-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import asyncio
|
|
15
|
+
from lelu import LeluClient, AgentAuthRequest, AgentContext
|
|
16
|
+
|
|
17
|
+
async def main():
|
|
18
|
+
async with LeluClient(base_url="http://localhost:8082") as client:
|
|
19
|
+
result = await client.agent_authorize(AgentAuthRequest(
|
|
20
|
+
actor="invoice_bot",
|
|
21
|
+
action="invoice:create",
|
|
22
|
+
context=AgentContext(
|
|
23
|
+
confidence=0.92,
|
|
24
|
+
acting_for="user_123",
|
|
25
|
+
),
|
|
26
|
+
))
|
|
27
|
+
print(result.allowed, result.reason)
|
|
28
|
+
|
|
29
|
+
asyncio.run(main())
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## API
|
|
33
|
+
|
|
34
|
+
| Method | Description |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `agent_authorize(req)` | Confidence-aware agent authorization |
|
|
37
|
+
| `authorize(req)` | Human RBAC authorization |
|
|
38
|
+
| `mint_token(req)` | Mint a JIT-scoped JWT |
|
|
39
|
+
| `revoke_token(token_id)` | Revoke a token immediately |
|
|
40
|
+
| `is_healthy()` | Health-check the engine |
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lelu Python SDK.
|
|
3
|
+
|
|
4
|
+
Quick start::
|
|
5
|
+
|
|
6
|
+
from lelu import LeluClient, AgentAuthRequest, AgentContext
|
|
7
|
+
|
|
8
|
+
async with LeluClient(base_url="http://localhost:8080") as lelu:
|
|
9
|
+
decision = await lelu.agent_authorize(
|
|
10
|
+
AgentAuthRequest(
|
|
11
|
+
actor="invoice_bot",
|
|
12
|
+
action="approve_refunds",
|
|
13
|
+
context=AgentContext(confidence=0.92, acting_for="user_123"),
|
|
14
|
+
)
|
|
15
|
+
)
|
|
16
|
+
if not decision.allowed:
|
|
17
|
+
print(decision.reason)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .client import LeluClient
|
|
21
|
+
from .autogpt_plugin import LeluAutoGPTPlugin
|
|
22
|
+
from .middleware import AgentMiddleware
|
|
23
|
+
from .models import (
|
|
24
|
+
AgentAuthDecision,
|
|
25
|
+
AgentAuthRequest,
|
|
26
|
+
AgentContext,
|
|
27
|
+
AuthDecision,
|
|
28
|
+
AuthEngineError,
|
|
29
|
+
AuthRequest,
|
|
30
|
+
DelegateScopeRequest,
|
|
31
|
+
DelegateScopeResult,
|
|
32
|
+
MintTokenRequest,
|
|
33
|
+
MintTokenResult,
|
|
34
|
+
RevokeTokenResult,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# CrewAI integration — requires `pip install crewai`
|
|
38
|
+
try:
|
|
39
|
+
from .crewai import LeluTool, PermissionDeniedError as CrewAIPermissionDeniedError # noqa: F401
|
|
40
|
+
except ImportError:
|
|
41
|
+
pass # crewai not installed; LeluTool not available
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"LeluClient",
|
|
45
|
+
"LeluAutoGPTPlugin",
|
|
46
|
+
"AgentMiddleware",
|
|
47
|
+
# CrewAI
|
|
48
|
+
"LeluTool",
|
|
49
|
+
"CrewAIPermissionDeniedError",
|
|
50
|
+
# Requests
|
|
51
|
+
"AuthRequest",
|
|
52
|
+
"AgentAuthRequest",
|
|
53
|
+
"AgentContext",
|
|
54
|
+
"MintTokenRequest",
|
|
55
|
+
"DelegateScopeRequest",
|
|
56
|
+
# Decisions
|
|
57
|
+
"AuthDecision",
|
|
58
|
+
"AgentAuthDecision",
|
|
59
|
+
"MintTokenResult",
|
|
60
|
+
"DelegateScopeResult",
|
|
61
|
+
"RevokeTokenResult",
|
|
62
|
+
# Errors
|
|
63
|
+
"AuthEngineError",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""AutoGPT plugin scaffold for Lelu Auth Permission Engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .client import LeluClient
|
|
9
|
+
from .models import AgentAuthRequest, AgentContext
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class LeluAutoGPTPlugin:
|
|
14
|
+
"""
|
|
15
|
+
Minimal plugin helper for AutoGPT-style tool execution guards.
|
|
16
|
+
|
|
17
|
+
This is framework-agnostic so it can be plugged into different AutoGPT
|
|
18
|
+
runtimes without requiring a hard dependency.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
client: LeluClient
|
|
22
|
+
actor: str
|
|
23
|
+
|
|
24
|
+
async def can_execute(
|
|
25
|
+
self,
|
|
26
|
+
action: str,
|
|
27
|
+
*,
|
|
28
|
+
confidence: float,
|
|
29
|
+
acting_for: str = "",
|
|
30
|
+
scope: str = "",
|
|
31
|
+
resource: dict[str, Any] | None = None,
|
|
32
|
+
) -> tuple[bool, str]:
|
|
33
|
+
req = AgentAuthRequest(
|
|
34
|
+
actor=self.actor,
|
|
35
|
+
action=action,
|
|
36
|
+
resource=resource or {},
|
|
37
|
+
context=AgentContext(
|
|
38
|
+
confidence=confidence,
|
|
39
|
+
acting_for=acting_for,
|
|
40
|
+
scope=scope,
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
decision = await self.client.agent_authorize(req)
|
|
44
|
+
return decision.allowed, decision.reason
|
|
45
|
+
|
|
46
|
+
async def enforce(
|
|
47
|
+
self,
|
|
48
|
+
action: str,
|
|
49
|
+
*,
|
|
50
|
+
confidence: float,
|
|
51
|
+
acting_for: str = "",
|
|
52
|
+
scope: str = "",
|
|
53
|
+
resource: dict[str, Any] | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
allowed, reason = await self.can_execute(
|
|
56
|
+
action,
|
|
57
|
+
confidence=confidence,
|
|
58
|
+
acting_for=acting_for,
|
|
59
|
+
scope=scope,
|
|
60
|
+
resource=resource,
|
|
61
|
+
)
|
|
62
|
+
if not allowed:
|
|
63
|
+
raise PermissionError(reason)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Auth Permission Engine — async Python client (httpx-backed)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .models import (
|
|
11
|
+
AgentAuthDecision,
|
|
12
|
+
AgentAuthRequest,
|
|
13
|
+
AuthDecision,
|
|
14
|
+
AuthEngineError,
|
|
15
|
+
AuthRequest,
|
|
16
|
+
DelegateScopeRequest,
|
|
17
|
+
DelegateScopeResult,
|
|
18
|
+
MintTokenRequest,
|
|
19
|
+
MintTokenResult,
|
|
20
|
+
RevokeTokenResult,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LeluClient:
|
|
25
|
+
"""
|
|
26
|
+
Async client for the Auth Permission Engine HTTP API.
|
|
27
|
+
|
|
28
|
+
Usage::
|
|
29
|
+
|
|
30
|
+
async with LeluClient(base_url="http://localhost:8080") as lelu:
|
|
31
|
+
decision = await lelu.agent_authorize(
|
|
32
|
+
AgentAuthRequest(
|
|
33
|
+
actor="invoice_bot",
|
|
34
|
+
action="approve_refunds",
|
|
35
|
+
context=AgentContext(confidence=0.92, acting_for="user_123"),
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
if not decision.allowed:
|
|
39
|
+
print(decision.reason)
|
|
40
|
+
|
|
41
|
+
Or without a context manager::
|
|
42
|
+
|
|
43
|
+
lelu = LeluClient()
|
|
44
|
+
decision = await lelu.authorize(AuthRequest(user_id="u1", action="view_invoices"))
|
|
45
|
+
await lelu.aclose()
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
base_url: str = "http://localhost:8080",
|
|
51
|
+
timeout: float = 5.0,
|
|
52
|
+
api_key: str | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
headers: dict[str, str] = {"Content-Type": "application/json"}
|
|
55
|
+
if api_key:
|
|
56
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
57
|
+
|
|
58
|
+
self._client = httpx.AsyncClient(
|
|
59
|
+
base_url=base_url.rstrip("/"),
|
|
60
|
+
headers=headers,
|
|
61
|
+
timeout=timeout,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# ── Context manager ───────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
async def __aenter__(self) -> "LeluClient":
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
70
|
+
await self.aclose()
|
|
71
|
+
|
|
72
|
+
async def aclose(self) -> None:
|
|
73
|
+
await self._client.aclose()
|
|
74
|
+
|
|
75
|
+
# ── Human authorization ───────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
async def authorize(self, req: AuthRequest) -> AuthDecision:
|
|
78
|
+
"""Check whether a human user is permitted to perform an action."""
|
|
79
|
+
payload = {
|
|
80
|
+
"user_id": req.user_id,
|
|
81
|
+
"action": req.action,
|
|
82
|
+
"resource": req.resource,
|
|
83
|
+
}
|
|
84
|
+
data = await self._post("/v1/authorize", payload)
|
|
85
|
+
return AuthDecision(
|
|
86
|
+
allowed=data["allowed"],
|
|
87
|
+
reason=data["reason"],
|
|
88
|
+
trace_id=data["trace_id"],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# ── Agent authorization ───────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
async def agent_authorize(self, req: AgentAuthRequest) -> AgentAuthDecision:
|
|
94
|
+
"""
|
|
95
|
+
Check whether an AI agent is permitted to perform an action.
|
|
96
|
+
|
|
97
|
+
The confidence score in ``req.context`` is passed through the
|
|
98
|
+
Confidence-Aware Auth gate ★ before policy evaluation.
|
|
99
|
+
"""
|
|
100
|
+
payload = {
|
|
101
|
+
"actor": req.actor,
|
|
102
|
+
"action": req.action,
|
|
103
|
+
"resource": req.resource,
|
|
104
|
+
"confidence": req.context.confidence,
|
|
105
|
+
"acting_for": req.context.acting_for,
|
|
106
|
+
"scope": req.context.scope,
|
|
107
|
+
}
|
|
108
|
+
data = await self._post("/v1/agent/authorize", payload)
|
|
109
|
+
return AgentAuthDecision(
|
|
110
|
+
allowed=data["allowed"],
|
|
111
|
+
reason=data["reason"],
|
|
112
|
+
trace_id=data["trace_id"],
|
|
113
|
+
downgraded_scope=data.get("downgraded_scope"),
|
|
114
|
+
requires_human_review=data.get("requires_human_review", False),
|
|
115
|
+
confidence_used=data.get("confidence_used", 0.0),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# ── JIT token minting ─────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
async def mint_token(self, req: MintTokenRequest) -> MintTokenResult:
|
|
121
|
+
"""Mint a scoped JIT token for an agent. Default TTL is 60 seconds."""
|
|
122
|
+
payload = {
|
|
123
|
+
"scope": req.scope,
|
|
124
|
+
"acting_for": req.acting_for,
|
|
125
|
+
"ttl_seconds": req.ttl_seconds or 60,
|
|
126
|
+
}
|
|
127
|
+
data = await self._post("/v1/tokens/mint", payload)
|
|
128
|
+
return MintTokenResult(
|
|
129
|
+
token=data["token"],
|
|
130
|
+
token_id=data["token_id"],
|
|
131
|
+
expires_at=datetime.fromtimestamp(data["expires_at"], tz=timezone.utc),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# ── Token revocation ──────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
async def revoke_token(self, token_id: str) -> RevokeTokenResult:
|
|
137
|
+
"""Immediately revoke a JIT token by its ID."""
|
|
138
|
+
resp = await self._client.delete(f"/v1/tokens/{token_id}")
|
|
139
|
+
await self._raise_for_status(resp)
|
|
140
|
+
return RevokeTokenResult(**resp.json())
|
|
141
|
+
|
|
142
|
+
# ── Multi-agent delegation ────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
async def delegate_scope(self, req: DelegateScopeRequest) -> DelegateScopeResult:
|
|
145
|
+
"""
|
|
146
|
+
Delegate a constrained sub-scope from one agent to another.
|
|
147
|
+
|
|
148
|
+
Validates the delegation rule in the policy, caps the TTL, and mints a
|
|
149
|
+
child JIT token scoped to the granted actions.
|
|
150
|
+
|
|
151
|
+
Confidence-Aware: the delegator's confidence score is checked against
|
|
152
|
+
``require_confidence_above`` in the policy before delegation is granted.
|
|
153
|
+
"""
|
|
154
|
+
payload = {
|
|
155
|
+
"delegator": req.delegator,
|
|
156
|
+
"delegatee": req.delegatee,
|
|
157
|
+
"scoped_to": req.scoped_to,
|
|
158
|
+
"ttl_seconds": req.ttl_seconds or 60,
|
|
159
|
+
"confidence": req.confidence,
|
|
160
|
+
"acting_for": req.acting_for or "",
|
|
161
|
+
"tenant_id": req.tenant_id or "",
|
|
162
|
+
}
|
|
163
|
+
data = await self._post("/v1/agent/delegate", payload)
|
|
164
|
+
from datetime import datetime, timezone
|
|
165
|
+
return DelegateScopeResult(
|
|
166
|
+
token=data["token"],
|
|
167
|
+
token_id=data["token_id"],
|
|
168
|
+
expires_at=datetime.fromtimestamp(data["expires_at"], tz=timezone.utc),
|
|
169
|
+
delegator=data["delegator"],
|
|
170
|
+
delegatee=data["delegatee"],
|
|
171
|
+
granted_scopes=data["granted_scopes"],
|
|
172
|
+
trace_id=data["trace_id"],
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# ── Health check ──────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
async def is_healthy(self) -> bool:
|
|
178
|
+
"""Return True if the engine sidecar is reachable and healthy."""
|
|
179
|
+
try:
|
|
180
|
+
resp = await self._client.get("/healthz")
|
|
181
|
+
return resp.status_code == 200 and resp.json().get("status") == "ok"
|
|
182
|
+
except httpx.HTTPError:
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
# ── HTTP helpers ──────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
async def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
188
|
+
resp = await self._client.post(path, json=payload)
|
|
189
|
+
await self._raise_for_status(resp)
|
|
190
|
+
return resp.json() # type: ignore[no-any-return]
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
async def _raise_for_status(resp: httpx.Response) -> None:
|
|
194
|
+
if not resp.is_error:
|
|
195
|
+
return
|
|
196
|
+
try:
|
|
197
|
+
detail = resp.json().get("error", resp.text)
|
|
198
|
+
except Exception:
|
|
199
|
+
detail = resp.text
|
|
200
|
+
raise AuthEngineError(
|
|
201
|
+
message=str(detail),
|
|
202
|
+
status=resp.status_code,
|
|
203
|
+
details=resp.text,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# Backward compatibility alias.
|
|
208
|
+
LeluClient = LeluClient
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lelu integration for CrewAI agents.
|
|
3
|
+
|
|
4
|
+
Provides ``LeluTool`` — a CrewAI-compatible tool base class that gates every
|
|
5
|
+
``_run()`` call through Lelu's Confidence-Aware Auth before execution.
|
|
6
|
+
|
|
7
|
+
Usage
|
|
8
|
+
-----
|
|
9
|
+
.. code-block:: python
|
|
10
|
+
|
|
11
|
+
from crewai.tools import BaseTool
|
|
12
|
+
from lelu.crewai import LeluTool
|
|
13
|
+
from lelu import LeluClient
|
|
14
|
+
|
|
15
|
+
client = LeluClient(base_url="http://localhost:8080")
|
|
16
|
+
|
|
17
|
+
class RefundTool(LeluTool):
|
|
18
|
+
name: str = "process_refund"
|
|
19
|
+
description: str = "Process a customer refund by invoice ID."
|
|
20
|
+
actor: str = "invoice_bot"
|
|
21
|
+
action: str = "invoice:refund"
|
|
22
|
+
confidence: float = 0.92 # set dynamically per-call if needed
|
|
23
|
+
|
|
24
|
+
def _execute(self, invoice_id: str) -> str:
|
|
25
|
+
# Your real tool logic here
|
|
26
|
+
return f"Refund processed for invoice {invoice_id}"
|
|
27
|
+
|
|
28
|
+
tool = RefundTool(lelu_client=client)
|
|
29
|
+
|
|
30
|
+
# Use in a CrewAI agent
|
|
31
|
+
from crewai import Agent, Task, Crew
|
|
32
|
+
agent = Agent(role="Finance Bot", tools=[tool], ...)
|
|
33
|
+
|
|
34
|
+
Notes
|
|
35
|
+
-----
|
|
36
|
+
- ``_execute()`` is the method you override (not ``_run()``).
|
|
37
|
+
- If Lelu denies the action, ``_run()`` returns a structured refusal string
|
|
38
|
+
that the LLM can use to self-correct.
|
|
39
|
+
- If the action requires human review, it is queued automatically and a
|
|
40
|
+
pending message is returned.
|
|
41
|
+
- Set ``throw_on_deny=True`` to raise ``PermissionDeniedError`` instead.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import asyncio
|
|
47
|
+
import logging
|
|
48
|
+
from typing import Any
|
|
49
|
+
|
|
50
|
+
from pydantic import Field
|
|
51
|
+
|
|
52
|
+
from .client import LeluClient
|
|
53
|
+
from .models import AgentAuthRequest, AgentContext
|
|
54
|
+
|
|
55
|
+
logger = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
# ─── Attempt to import CrewAI ────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
from crewai.tools import BaseTool as CrewAIBaseTool
|
|
61
|
+
|
|
62
|
+
_CREWAI_AVAILABLE = True
|
|
63
|
+
except ImportError:
|
|
64
|
+
_CREWAI_AVAILABLE = False
|
|
65
|
+
CrewAIBaseTool = object
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ─── Exceptions ───────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class PermissionDeniedError(Exception):
|
|
72
|
+
"""Raised by LeluTool when ``throw_on_deny=True`` and Lelu denies."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, message: str, reason: str) -> None:
|
|
75
|
+
super().__init__(message)
|
|
76
|
+
self.reason = reason
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ─── LeluTool ────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class LeluTool(CrewAIBaseTool): # type: ignore[misc]
|
|
83
|
+
"""CrewAI ``BaseTool`` with Lelu Confidence-Aware Auth.
|
|
84
|
+
|
|
85
|
+
Subclass this and implement ``_execute()`` instead of ``_run()``.
|
|
86
|
+
Every call to ``_run()`` is intercepted and gated through Lelu first.
|
|
87
|
+
|
|
88
|
+
Attributes
|
|
89
|
+
----------
|
|
90
|
+
actor:
|
|
91
|
+
The Lelu agent scope / actor name registered in your ``auth.yaml``.
|
|
92
|
+
action:
|
|
93
|
+
The permission string being checked (e.g. ``"invoice:refund"``).
|
|
94
|
+
confidence:
|
|
95
|
+
LLM confidence score for this invocation (0.0–1.0). Can be set
|
|
96
|
+
dynamically before calling the tool.
|
|
97
|
+
throw_on_deny:
|
|
98
|
+
If ``True``, raise :class:`PermissionDeniedError` on denial.
|
|
99
|
+
If ``False`` (default), return a structured refusal string for LLM
|
|
100
|
+
self-correction.
|
|
101
|
+
acting_for:
|
|
102
|
+
Optional user ID the agent is acting on behalf of.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
# Lelu-specific fields (Pydantic v2 model fields)
|
|
106
|
+
actor: str = Field(..., description="Lelu agent actor / scope name")
|
|
107
|
+
action: str = Field(..., description="Permission string to authorize")
|
|
108
|
+
confidence: float = Field(default=1.0, ge=0.0, le=1.0, description="LLM confidence score")
|
|
109
|
+
throw_on_deny: bool = Field(default=False, description="Raise on deny instead of returning refusal")
|
|
110
|
+
acting_for: str | None = Field(default=None, description="User the agent acts on behalf of")
|
|
111
|
+
|
|
112
|
+
# LeluClient is injected at construction time.
|
|
113
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
114
|
+
_lelu: LeluClient
|
|
115
|
+
|
|
116
|
+
def __init__(self, lelu_client: LeluClient, **data: Any) -> None:
|
|
117
|
+
if not _CREWAI_AVAILABLE:
|
|
118
|
+
raise ImportError(
|
|
119
|
+
"crewai is not installed. Install it with: pip install crewai"
|
|
120
|
+
)
|
|
121
|
+
super().__init__(**data)
|
|
122
|
+
self._lelu = lelu_client
|
|
123
|
+
|
|
124
|
+
# ── Intercept CrewAI's _run ───────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
def _run(self, *args: Any, **kwargs: Any) -> str:
|
|
127
|
+
"""
|
|
128
|
+
Gate the tool call through Lelu before executing ``_execute()``.
|
|
129
|
+
Called by CrewAI's agent loop automatically.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
decision = asyncio.run(
|
|
133
|
+
self._lelu.agent_authorize(
|
|
134
|
+
AgentAuthRequest(
|
|
135
|
+
actor=self.actor,
|
|
136
|
+
action=self.action,
|
|
137
|
+
context=AgentContext(
|
|
138
|
+
confidence=self.confidence,
|
|
139
|
+
acting_for=self.acting_for,
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
except Exception as exc: # noqa: BLE001
|
|
145
|
+
msg = f"[Lelu] Authorization check failed for '{self.name}': {exc}"
|
|
146
|
+
logger.error("lelu: %s", msg)
|
|
147
|
+
if self.throw_on_deny:
|
|
148
|
+
raise PermissionDeniedError(msg, str(exc)) from exc
|
|
149
|
+
return msg
|
|
150
|
+
|
|
151
|
+
# ── Human review required ──────────────────────────────────────────
|
|
152
|
+
if decision.requires_human_review:
|
|
153
|
+
logger.info(
|
|
154
|
+
"lelu: tool=%s queued for human review reason=%r",
|
|
155
|
+
self.name,
|
|
156
|
+
decision.reason,
|
|
157
|
+
)
|
|
158
|
+
return (
|
|
159
|
+
f"[Lelu] Action '{self.name}' is queued for human review. "
|
|
160
|
+
f"Reason: {decision.reason}. Please wait for approval before retrying."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# ── Hard deny ─────────────────────────────────────────────────────
|
|
164
|
+
if not decision.allowed:
|
|
165
|
+
msg = (
|
|
166
|
+
f"[Lelu] Action '{self.name}' was denied for agent '{self.actor}'. "
|
|
167
|
+
f"Reason: {decision.reason}."
|
|
168
|
+
)
|
|
169
|
+
if decision.downgraded_scope:
|
|
170
|
+
msg += f" Downgraded to: {decision.downgraded_scope}."
|
|
171
|
+
logger.warning("lelu: %s", msg)
|
|
172
|
+
if self.throw_on_deny:
|
|
173
|
+
raise PermissionDeniedError(msg, decision.reason)
|
|
174
|
+
return msg
|
|
175
|
+
|
|
176
|
+
# ── Authorized — run the real tool ────────────────────────────────
|
|
177
|
+
logger.debug(
|
|
178
|
+
"lelu: tool=%s authorized confidence=%.2f trace_id=%s",
|
|
179
|
+
self.name,
|
|
180
|
+
decision.confidence_used,
|
|
181
|
+
decision.trace_id,
|
|
182
|
+
)
|
|
183
|
+
return self._execute(*args, **kwargs)
|
|
184
|
+
|
|
185
|
+
def _execute(self, *args: Any, **kwargs: Any) -> str:
|
|
186
|
+
"""Override this method with your actual tool logic."""
|
|
187
|
+
raise NotImplementedError(
|
|
188
|
+
f"LeluTool subclass '{type(self).__name__}' must implement _execute()"
|
|
189
|
+
)
|