agentauthlayer 0.1.0__py3-none-any.whl

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.
agent_auth/__init__.py ADDED
@@ -0,0 +1,48 @@
1
+ """agent_auth — reusable authentication and authorization primitives for Agent Auth."""
2
+
3
+ from agent_auth.auth import principal_fields, principal_from_agent, principal_from_user
4
+ from agent_auth.client import AuthAPIClient
5
+ from agent_auth.context import AuthContext
6
+ from agent_auth.delegation import DelegationGrant
7
+ from agent_auth.exceptions import (
8
+ AgentAuthError,
9
+ AgentNotFoundError,
10
+ AuthServiceError,
11
+ InvalidTokenError,
12
+ PermissionDeniedError,
13
+ RevokedTokenError,
14
+ )
15
+ from agent_auth.models import Agent, TokenClaims, TokenRecord, User, UserClaims
16
+ from agent_auth.policy import PolicyDecision, PolicyEvaluator, PolicyRequest, PolicyStatement, RoleDefinition, require_permission
17
+ from agent_auth.principals import AgentPrincipal, Principal, SystemPrincipal, UserPrincipal, user_scopes_for_role
18
+
19
+ __all__ = [
20
+ "Agent",
21
+ "AgentAuthError",
22
+ "AgentNotFoundError",
23
+ "AgentPrincipal",
24
+ "AuthAPIClient",
25
+ "AuthContext",
26
+ "AuthServiceError",
27
+ "DelegationGrant",
28
+ "InvalidTokenError",
29
+ "PermissionDeniedError",
30
+ "PolicyDecision",
31
+ "PolicyEvaluator",
32
+ "PolicyRequest",
33
+ "PolicyStatement",
34
+ "Principal",
35
+ "RevokedTokenError",
36
+ "RoleDefinition",
37
+ "SystemPrincipal",
38
+ "TokenClaims",
39
+ "TokenRecord",
40
+ "User",
41
+ "UserClaims",
42
+ "UserPrincipal",
43
+ "principal_fields",
44
+ "principal_from_agent",
45
+ "principal_from_user",
46
+ "require_permission",
47
+ "user_scopes_for_role",
48
+ ]
agent_auth/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from agent_auth.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
agent_auth/agents.py ADDED
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol, TypeVar
4
+
5
+
6
+ AgentT = TypeVar("AgentT")
7
+
8
+
9
+ class AgentStore(Protocol[AgentT]):
10
+ def save(self, agent: AgentT) -> AgentT:
11
+ ...
12
+
13
+ def get(self, agent_id: str) -> AgentT | None:
14
+ ...
15
+
16
+ def list_all(self, project_id: str | None = None) -> list[AgentT]:
17
+ ...
18
+
19
+
20
+ class AgentIdentityService:
21
+ """Core agent lifecycle orchestration over a configured agent store."""
22
+
23
+ def __init__(self, store: AgentStore[AgentT]):
24
+ self.store = store
25
+
26
+ def save(self, agent: AgentT) -> AgentT:
27
+ return self.store.save(agent)
28
+
29
+ def get(self, agent_id: str) -> AgentT | None:
30
+ return self.store.get(agent_id)
31
+
32
+ def list_agents(self, project_id: str | None = None) -> list[AgentT]:
33
+ return self.store.list_all(project_id=project_id)
34
+
35
+ def update_scopes(self, agent_id: str, scopes: list[str]) -> AgentT | None:
36
+ agent = self.store.get(agent_id)
37
+ if not agent:
38
+ return None
39
+ agent.scopes = scopes
40
+ return self.store.save(agent)
agent_auth/audit.py ADDED
@@ -0,0 +1,51 @@
1
+ """agent_auth audit logger — pluggable, default prints to stderr."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import sys
8
+ from datetime import datetime, timezone
9
+ from typing import Any, Protocol
10
+
11
+ logger = logging.getLogger("agent_auth.audit")
12
+
13
+
14
+ class AuditBackend(Protocol):
15
+ """Interface that any custom audit backend must implement."""
16
+
17
+ def log(self, event: dict[str, Any]) -> None: ...
18
+
19
+
20
+ class StderrAuditBackend:
21
+ """Default backend — writes JSON lines to stderr."""
22
+
23
+ def log(self, event: dict[str, Any]) -> None:
24
+ line = json.dumps(event, default=str)
25
+ print(line, file=sys.stderr)
26
+
27
+
28
+ class InMemoryAuditBackend:
29
+ """Keeps events in a list — useful for testing."""
30
+
31
+ def __init__(self) -> None:
32
+ self.events: list[dict[str, Any]] = []
33
+
34
+ def log(self, event: dict[str, Any]) -> None:
35
+ self.events.append(event)
36
+
37
+
38
+ class AuditLogger:
39
+ """Thin wrapper around a backend that stamps every event."""
40
+
41
+ def __init__(self, backend: AuditBackend | None = None) -> None:
42
+ self._backend: AuditBackend = backend or StderrAuditBackend()
43
+
44
+ def record(self, event_type: str, **details: Any) -> None:
45
+ event = {
46
+ "event": event_type,
47
+ "ts": datetime.now(timezone.utc).isoformat(),
48
+ **details,
49
+ }
50
+ self._backend.log(event)
51
+ logger.debug("audit: %s", event)
agent_auth/auth.py ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from agent_auth.principals import AgentPrincipal, Principal, UserPrincipal, user_scopes_for_role
4
+
5
+
6
+ def principal_from_agent(*, agent_id: str, role: str | None = None, scopes: list[str] | None = None, owner: str | None = None, status: str | None = None) -> AgentPrincipal:
7
+ return AgentPrincipal(
8
+ principal_id=agent_id,
9
+ principal_type='agent',
10
+ role=role,
11
+ scopes=list(scopes or []),
12
+ owner=owner,
13
+ status=status,
14
+ )
15
+
16
+
17
+ def principal_from_user(*, user_id: str, email: str, role: str, scopes: list[str] | None = None, status: str | None = None) -> UserPrincipal:
18
+ effective_scopes = list(scopes) if scopes is not None else user_scopes_for_role(role)
19
+ return UserPrincipal(
20
+ principal_id=user_id,
21
+ principal_type='user',
22
+ role=role,
23
+ scopes=effective_scopes,
24
+ email=email,
25
+ status=status,
26
+ )
27
+
28
+
29
+ def principal_fields(principal: Principal) -> tuple[str, str, list[str], str | None, str | None]:
30
+ return (
31
+ principal.principal_id,
32
+ principal.principal_type,
33
+ list(principal.scopes),
34
+ principal.role,
35
+ principal.email,
36
+ )
agent_auth/cli.py ADDED
@@ -0,0 +1,28 @@
1
+ import argparse
2
+ import sys
3
+ import uvicorn
4
+
5
+ def main():
6
+ parser = argparse.ArgumentParser(
7
+ description="Agent Auth Platform CLI - Manage and run the control plane.",
8
+ prog="agent-auth"
9
+ )
10
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
11
+
12
+ # The "ui" command (like "mlflow ui")
13
+ ui_parser = subparsers.add_parser("ui", help="Launch the integrated Auth API and React Dashboard")
14
+ ui_parser.add_argument("--host", default="0.0.0.0", help="Host to bind to (default: 0.0.0.0)")
15
+ ui_parser.add_argument("--port", type=int, default=8002, help="Port to bind to (default: 8002)")
16
+ ui_parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development")
17
+
18
+ args = parser.parse_args()
19
+
20
+ if args.command == "ui":
21
+ print(f"Starting Agent Auth UI & Control Plane on http://{args.host}:{args.port}")
22
+ uvicorn.run("auth_app.main:app", host=args.host, port=args.port, reload=args.reload)
23
+ else:
24
+ parser.print_help()
25
+ sys.exit(1)
26
+
27
+ if __name__ == "__main__":
28
+ main()
agent_auth/client.py ADDED
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ import requests
7
+
8
+ from agent_auth.context import AuthContext
9
+ from agent_auth.exceptions import (
10
+ AgentNotFoundError,
11
+ AuthServiceError,
12
+ InvalidTokenError,
13
+ PermissionDeniedError,
14
+ RevokedTokenError,
15
+ )
16
+
17
+
18
+ class AuthAPIClient:
19
+ """Thin SDK client for talking to the Agent Auth control plane."""
20
+
21
+ def __init__(self, base_url: str | None = None, token: str | None = None, timeout: int = 30) -> None:
22
+ self.base_url = (base_url or os.getenv("AGENT_AUTH_URL") or "http://127.0.0.1:8002").rstrip("/")
23
+ self.token = token or os.getenv("AGENT_AUTH_TOKEN")
24
+ self.timeout = timeout
25
+
26
+ def _headers(self) -> dict[str, str]:
27
+ headers = {"Content-Type": "application/json"}
28
+ if self.token:
29
+ headers["Authorization"] = f"Bearer {self.token}"
30
+ return headers
31
+
32
+ def _request(self, method: str, path: str, json: dict[str, Any] | None = None) -> Any:
33
+ response = requests.request(
34
+ method,
35
+ f"{self.base_url}{path}",
36
+ json=json,
37
+ headers=self._headers(),
38
+ timeout=self.timeout,
39
+ )
40
+
41
+ if response.ok:
42
+ if not response.content:
43
+ return None
44
+ return response.json()
45
+
46
+ detail = None
47
+ try:
48
+ detail = response.json().get("detail")
49
+ except Exception:
50
+ detail = response.text or "Request failed"
51
+
52
+ if response.status_code == 401:
53
+ if detail and ("revoked" in detail.lower() or "inactive" in detail.lower()):
54
+ raise RevokedTokenError(detail)
55
+ raise InvalidTokenError(detail or "Invalid token")
56
+ if response.status_code == 403:
57
+ raise PermissionDeniedError(detail or "Permission denied")
58
+ if response.status_code == 404 and path.startswith("/agents/"):
59
+ raise AgentNotFoundError(detail or "Agent not found")
60
+ raise AuthServiceError(detail or f"Request failed with status {response.status_code}")
61
+
62
+ def health(self) -> dict[str, Any]:
63
+ return self._request("GET", "/health")
64
+
65
+ def create_agent(self, agent_id: str, name: str, owner: str, role: str, scopes: list[str], project_id: str | None = None) -> dict[str, Any]:
66
+ payload = {
67
+ "agent_id": agent_id,
68
+ "name": name,
69
+ "owner": owner,
70
+ "role": role,
71
+ "scopes": scopes,
72
+ }
73
+ if project_id:
74
+ payload["project_id"] = project_id
75
+ return self._request("POST", "/agents", json=payload)
76
+
77
+ def get_agent(self, agent_id: str) -> dict[str, Any]:
78
+ return self._request("GET", f"/agents/{agent_id}")
79
+
80
+ def issue_token(self, agent_id: str) -> dict[str, Any]:
81
+ return self._request("POST", "/auth/token", json={"agent_id": agent_id})
82
+
83
+ def sync_tools(self, permissions: list[dict[str, str]]) -> dict[str, Any]:
84
+ return self._request("POST", "/policy/permissions/sync", json={"permissions": permissions})
85
+
86
+ def evaluate(self, principal_id: str, action: str, resource: str | None = None, granted_scopes: list[str] | None = None, role: str | None = None, context: dict[str, str] | None = None) -> dict[str, Any]:
87
+ return self._request(
88
+ "POST",
89
+ "/policy/evaluate",
90
+ json={
91
+ "principal_id": principal_id,
92
+ "action": action,
93
+ "resource": resource,
94
+ "granted_scopes": granted_scopes or [],
95
+ "role": role,
96
+ "context": context or {},
97
+ },
98
+ )
99
+
100
+ def auth_context(self, principal_id: str, scopes: list[str], role: str | None = None, principal_type: str = "agent", email: str | None = None) -> AuthContext:
101
+ return AuthContext(
102
+ principal_id=principal_id,
103
+ principal_type=principal_type,
104
+ scopes=scopes,
105
+ role=role,
106
+ email=email,
107
+ )
agent_auth/context.py ADDED
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class AuthContext:
8
+ principal_id: str
9
+ principal_type: str
10
+ jti: str
11
+ scopes: list[str] = field(default_factory=list)
12
+ role: str | None = None
13
+ email: str | None = None
14
+
15
+ @property
16
+ def subject_id(self) -> str:
17
+ return self.principal_id
18
+
19
+ @property
20
+ def subject_type(self) -> str:
21
+ return self.principal_type