agentauthlayer 0.1.9__tar.gz → 0.1.11__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.
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/MANIFEST.in +0 -1
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/PKG-INFO +63 -1
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/README.md +62 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/__init__.py +4 -1
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/cli.py +92 -16
- agentauthlayer-0.1.11/agent_auth/client.py +253 -0
- agentauthlayer-0.1.11/agent_auth/context.py +126 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/policy.py +18 -6
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/principals.py +3 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/server_runtime.py +10 -2
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/session.py +6 -0
- agentauthlayer-0.1.11/agent_auth/web_dist/assets/index-Cs3lTJhd.js +320 -0
- agentauthlayer-0.1.11/agent_auth/web_dist/assets/index-DaB2dsnD.css +1 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/web_dist/index.html +2 -2
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agentauthlayer.egg-info/PKG-INFO +63 -1
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agentauthlayer.egg-info/SOURCES.txt +3 -3
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/agents.py +9 -1
- agentauthlayer-0.1.11/auth_app/api/routes/tokens.py +120 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/main.py +16 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/contracts.py +4 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_token_repo.py +24 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/token_repo.py +6 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/agent.py +7 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/token.py +29 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/agent_service.py +15 -1
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/token_service.py +22 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/pyproject.toml +1 -1
- agentauthlayer-0.1.11/tests/test_runtime_binding.py +109 -0
- agentauthlayer-0.1.9/agent_auth/client.py +0 -138
- agentauthlayer-0.1.9/agent_auth/context.py +0 -21
- agentauthlayer-0.1.9/agent_auth/web_dist/assets/index-BBJ7rinV.css +0 -1
- agentauthlayer-0.1.9/agent_auth/web_dist/assets/index-DXUoW2DG.js +0 -429
- agentauthlayer-0.1.9/agent_auth_definitive_guide.pdf +0 -0
- agentauthlayer-0.1.9/auth_app/api/routes/tokens.py +0 -50
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/__main__.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/agents.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/audit.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/auth.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/core.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/credentials.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/delegation.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/exceptions.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/models.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/policy_service.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/registry.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/runtime.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/storage.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/tokens.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/users.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/web_dist/favicon.ico +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/web_dist/grid.svg +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/web_dist/placeholder.svg +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/web_dist/robots.txt +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agentauthlayer.egg-info/dependency_links.txt +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agentauthlayer.egg-info/entry_points.txt +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agentauthlayer.egg-info/requires.txt +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agentauthlayer.egg-info/top_level.txt +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/__init__.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/__init__.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/__init__.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/audit.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/auth.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/bootstrap.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/health.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/policy.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/projects.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/users.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/core/__init__.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/core/config.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/core/db.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/core/errors.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/core/logging.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/dependencies/__init__.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/dependencies/auth.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/dependencies/security.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/dependencies/user.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/domain/__init__.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/domain/enums.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/domain/models.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/middleware/__init__.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/middleware/correlation.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/__init__.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/agent_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/audit_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/constraint_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/delegation_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/project_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/role_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_agent_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_audit_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_constraint_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_delegation_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_permission_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_project_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_role_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_user_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/user_repo.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/__init__.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/audit.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/auth.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/bootstrap.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/device_agents.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/policy.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/project.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/user.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/__init__.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/audit_service.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/device_agent_service.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/policy_service.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/project_service.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/user_service.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/setup.cfg +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_agent_auth_library.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_auth_flow.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_core_first_boundary.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_health.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_iam_policy.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_project_flow.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_sqlite_repos.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_storage.py +0 -0
- {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_tool_registry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentauthlayer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: Library-first authentication and authorization SDK for AI agents
|
|
5
5
|
Author: Vaibhav Ahluwalia
|
|
6
6
|
License: MIT
|
|
@@ -32,6 +32,68 @@ Requires-Dist: email-validator
|
|
|
32
32
|
|
|
33
33
|
Unified SDK and local control plane package for Agent Auth.
|
|
34
34
|
|
|
35
|
+
## Token-first developer flow
|
|
36
|
+
|
|
37
|
+
You can now create project-scoped tokens from:
|
|
38
|
+
- the Project Settings page in the UI
|
|
39
|
+
- the Tokens page in the UI
|
|
40
|
+
- the CLI with `agentauth token create`
|
|
41
|
+
|
|
42
|
+
These tokens support one visible token model with richer internal semantics:
|
|
43
|
+
- `purpose=sync` for registration and sync flows
|
|
44
|
+
- `purpose=runtime` for runtime execution flows
|
|
45
|
+
- `purpose=automation` for service or CI style automation
|
|
46
|
+
- optional `agent_id` binding for runtime-oriented tokens
|
|
47
|
+
|
|
48
|
+
## Runtime binding
|
|
49
|
+
|
|
50
|
+
Runtime authorization now enforces stronger execution binding checks for project-scoped flows:
|
|
51
|
+
- project scope mismatches are denied before policy evaluation
|
|
52
|
+
- agent/principal mismatches are denied before policy evaluation
|
|
53
|
+
- execution context automatically carries project, agent, principal, and token-purpose context into authorization checks
|
|
54
|
+
- runtime-bound tokens now carry first-class `project_id`, `agent_id`, and `purpose` fields, with scope-based fallback for older tokens
|
|
55
|
+
|
|
56
|
+
Use the resulting token in code directly:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from agent_auth.client import AuthAPIClient
|
|
60
|
+
|
|
61
|
+
client = AuthAPIClient(
|
|
62
|
+
base_url="http://127.0.0.1:8002",
|
|
63
|
+
token="YOUR_PROJECT_TOKEN",
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or store it locally for CLI + SDK reuse:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
agentauth login --base-url http://127.0.0.1:8002 --token YOUR_PROJECT_TOKEN --email admin@agentauth.dev
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Create a bound runtime token from the CLI
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
agentauth token create \
|
|
77
|
+
--project ai-platform \
|
|
78
|
+
--name research-runtime-token \
|
|
79
|
+
--purpose runtime \
|
|
80
|
+
--agent-id research-agent-01 \
|
|
81
|
+
--scope docs.read
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Then use it directly in code:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from agent_auth import AuthAPIClient
|
|
88
|
+
|
|
89
|
+
client = AuthAPIClient(token="YOUR_RUNTIME_TOKEN")
|
|
90
|
+
ctx = client.execution_context()
|
|
91
|
+
|
|
92
|
+
print(ctx.project_id)
|
|
93
|
+
print(ctx.agent_id)
|
|
94
|
+
print(ctx.context.get("token_purpose"))
|
|
95
|
+
```
|
|
96
|
+
|
|
35
97
|
`agentauthlayer` helps you:
|
|
36
98
|
- start a local Agent Auth backend and UI with one command
|
|
37
99
|
- authenticate once and reuse local credentials
|
|
@@ -2,6 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
Unified SDK and local control plane package for Agent Auth.
|
|
4
4
|
|
|
5
|
+
## Token-first developer flow
|
|
6
|
+
|
|
7
|
+
You can now create project-scoped tokens from:
|
|
8
|
+
- the Project Settings page in the UI
|
|
9
|
+
- the Tokens page in the UI
|
|
10
|
+
- the CLI with `agentauth token create`
|
|
11
|
+
|
|
12
|
+
These tokens support one visible token model with richer internal semantics:
|
|
13
|
+
- `purpose=sync` for registration and sync flows
|
|
14
|
+
- `purpose=runtime` for runtime execution flows
|
|
15
|
+
- `purpose=automation` for service or CI style automation
|
|
16
|
+
- optional `agent_id` binding for runtime-oriented tokens
|
|
17
|
+
|
|
18
|
+
## Runtime binding
|
|
19
|
+
|
|
20
|
+
Runtime authorization now enforces stronger execution binding checks for project-scoped flows:
|
|
21
|
+
- project scope mismatches are denied before policy evaluation
|
|
22
|
+
- agent/principal mismatches are denied before policy evaluation
|
|
23
|
+
- execution context automatically carries project, agent, principal, and token-purpose context into authorization checks
|
|
24
|
+
- runtime-bound tokens now carry first-class `project_id`, `agent_id`, and `purpose` fields, with scope-based fallback for older tokens
|
|
25
|
+
|
|
26
|
+
Use the resulting token in code directly:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from agent_auth.client import AuthAPIClient
|
|
30
|
+
|
|
31
|
+
client = AuthAPIClient(
|
|
32
|
+
base_url="http://127.0.0.1:8002",
|
|
33
|
+
token="YOUR_PROJECT_TOKEN",
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or store it locally for CLI + SDK reuse:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
agentauth login --base-url http://127.0.0.1:8002 --token YOUR_PROJECT_TOKEN --email admin@agentauth.dev
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Create a bound runtime token from the CLI
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
agentauth token create \
|
|
47
|
+
--project ai-platform \
|
|
48
|
+
--name research-runtime-token \
|
|
49
|
+
--purpose runtime \
|
|
50
|
+
--agent-id research-agent-01 \
|
|
51
|
+
--scope docs.read
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Then use it directly in code:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from agent_auth import AuthAPIClient
|
|
58
|
+
|
|
59
|
+
client = AuthAPIClient(token="YOUR_RUNTIME_TOKEN")
|
|
60
|
+
ctx = client.execution_context()
|
|
61
|
+
|
|
62
|
+
print(ctx.project_id)
|
|
63
|
+
print(ctx.agent_id)
|
|
64
|
+
print(ctx.context.get("token_purpose"))
|
|
65
|
+
```
|
|
66
|
+
|
|
5
67
|
`agentauthlayer` helps you:
|
|
6
68
|
- start a local Agent Auth backend and UI with one command
|
|
7
69
|
- authenticate once and reuse local credentials
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from agent_auth.auth import principal_fields, principal_from_agent, principal_from_user
|
|
4
4
|
from agent_auth.client import AuthAPIClient
|
|
5
|
-
from agent_auth.context import AuthContext
|
|
5
|
+
from agent_auth.context import AuthContext, ExecutionContext, execution_context_from_args, execution_context_from_token_payload
|
|
6
6
|
from agent_auth.delegation import DelegationGrant
|
|
7
7
|
from agent_auth.exceptions import (
|
|
8
8
|
AgentAuthError,
|
|
@@ -30,6 +30,7 @@ __all__ = [
|
|
|
30
30
|
"AgentPrincipal",
|
|
31
31
|
"AuthAPIClient",
|
|
32
32
|
"AuthContext",
|
|
33
|
+
"ExecutionContext",
|
|
33
34
|
"AuthServiceError",
|
|
34
35
|
"DelegationGrant",
|
|
35
36
|
"InvalidTokenError",
|
|
@@ -50,6 +51,8 @@ __all__ = [
|
|
|
50
51
|
"clear_registries",
|
|
51
52
|
"list_registered_agents",
|
|
52
53
|
"list_registered_tools",
|
|
54
|
+
"execution_context_from_args",
|
|
55
|
+
"execution_context_from_token_payload",
|
|
53
56
|
"principal_fields",
|
|
54
57
|
"principal_from_agent",
|
|
55
58
|
"principal_from_user",
|
|
@@ -137,28 +137,58 @@ def sync_command(args) -> int:
|
|
|
137
137
|
tools = list_registered_tools()
|
|
138
138
|
agents = list_registered_agents()
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
existing_permissions = {permission['action']: permission for permission in client.list_permissions()}
|
|
141
|
+
tool_payload = [{'action': tool.action, 'description': tool.description} for tool in tools]
|
|
142
|
+
tools_to_sync = [tool for tool in tool_payload if existing_permissions.get(tool['action'], {}).get('description') != tool['description']]
|
|
143
|
+
if tools_to_sync:
|
|
144
|
+
client.sync_tools(tools_to_sync)
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
agent_results = []
|
|
147
147
|
for agent in agents:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
target_project = args.project or agent.project_id
|
|
149
|
+
try:
|
|
150
|
+
existing = client.get_agent(agent.agent_id)
|
|
151
|
+
except AgentNotFoundError:
|
|
152
|
+
existing = None
|
|
153
|
+
|
|
154
|
+
desired = {
|
|
155
|
+
'name': agent.name,
|
|
156
|
+
'role': agent.role,
|
|
157
|
+
'scopes': list(agent.scopes),
|
|
158
|
+
'project_id': target_project,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if existing is None:
|
|
162
|
+
created = client.create_agent(
|
|
163
|
+
agent_id=agent.agent_id,
|
|
164
|
+
name=agent.name,
|
|
165
|
+
owner=agent.owner,
|
|
166
|
+
role=agent.role,
|
|
167
|
+
scopes=agent.scopes,
|
|
168
|
+
project_id=target_project,
|
|
169
|
+
)
|
|
170
|
+
agent_results.append({'agent_id': created['agent_id'], 'status': 'created'})
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
changed_fields = {}
|
|
174
|
+
for field in ['name', 'role', 'project_id', 'scopes']:
|
|
175
|
+
if existing.get(field) != desired[field]:
|
|
176
|
+
changed_fields[field] = desired[field]
|
|
177
|
+
|
|
178
|
+
if changed_fields:
|
|
179
|
+
updated = client.update_agent(agent.agent_id, **changed_fields)
|
|
180
|
+
agent_results.append({'agent_id': updated['agent_id'], 'status': 'updated', 'changed_fields': sorted(changed_fields.keys())})
|
|
181
|
+
else:
|
|
182
|
+
agent_results.append({'agent_id': agent.agent_id, 'status': 'unchanged'})
|
|
156
183
|
|
|
157
184
|
print(json.dumps({
|
|
158
185
|
'module': args.module,
|
|
159
186
|
'project': args.project,
|
|
160
|
-
'
|
|
161
|
-
|
|
187
|
+
'tools': {
|
|
188
|
+
'added_or_updated': [tool['action'] for tool in tools_to_sync],
|
|
189
|
+
'unchanged': [tool.action for tool in tools if tool.action not in {item['action'] for item in tools_to_sync}],
|
|
190
|
+
},
|
|
191
|
+
'agents': agent_results,
|
|
162
192
|
}, indent=2))
|
|
163
193
|
return 0
|
|
164
194
|
|
|
@@ -184,6 +214,33 @@ def project_create_command(args) -> int:
|
|
|
184
214
|
return 0
|
|
185
215
|
|
|
186
216
|
|
|
217
|
+
def token_create_command(args) -> int:
|
|
218
|
+
client = AuthAPIClient()
|
|
219
|
+
token = client.create_project_token(
|
|
220
|
+
args.project,
|
|
221
|
+
args.name,
|
|
222
|
+
args.scopes or [],
|
|
223
|
+
purpose=args.purpose,
|
|
224
|
+
agent_id=args.agent_id,
|
|
225
|
+
)
|
|
226
|
+
print(json.dumps(token, indent=2))
|
|
227
|
+
return 0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def token_list_command(args) -> int:
|
|
231
|
+
client = AuthAPIClient()
|
|
232
|
+
tokens = client.list_tokens(args.subject_type)
|
|
233
|
+
print(json.dumps(tokens, indent=2))
|
|
234
|
+
return 0
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def token_revoke_command(args) -> int:
|
|
238
|
+
client = AuthAPIClient()
|
|
239
|
+
result = client.revoke_token_by_jti(args.jti)
|
|
240
|
+
print(json.dumps(result, indent=2))
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
|
|
187
244
|
def ui_command(args) -> int:
|
|
188
245
|
creds = load_credentials() or {}
|
|
189
246
|
base_url = (args.base_url or creds.get('base_url') or DEFAULT_BASE_URL).rstrip('/')
|
|
@@ -266,6 +323,25 @@ def main():
|
|
|
266
323
|
project_create_parser.add_argument('--description')
|
|
267
324
|
project_create_parser.set_defaults(func=project_create_command)
|
|
268
325
|
|
|
326
|
+
token_parser = subparsers.add_parser('token', help='Manage project tokens from the CLI')
|
|
327
|
+
token_subparsers = token_parser.add_subparsers(dest='token_command')
|
|
328
|
+
|
|
329
|
+
token_create_parser = token_subparsers.add_parser('create', help='Create a project token')
|
|
330
|
+
token_create_parser.add_argument('--project', required=True)
|
|
331
|
+
token_create_parser.add_argument('--name', required=True)
|
|
332
|
+
token_create_parser.add_argument('--scope', dest='scopes', action='append', help='Scope to include. Repeat for multiple scopes.')
|
|
333
|
+
token_create_parser.add_argument('--purpose', default='sync', choices=['sync', 'runtime', 'automation'], help='Intended token purpose.')
|
|
334
|
+
token_create_parser.add_argument('--agent-id', help='Optional runtime agent binding for runtime-oriented tokens.')
|
|
335
|
+
token_create_parser.set_defaults(func=token_create_command)
|
|
336
|
+
|
|
337
|
+
token_list_parser = token_subparsers.add_parser('list', help='List tokens')
|
|
338
|
+
token_list_parser.add_argument('--subject-type', choices=['user', 'agent', 'system'])
|
|
339
|
+
token_list_parser.set_defaults(func=token_list_command)
|
|
340
|
+
|
|
341
|
+
token_revoke_parser = token_subparsers.add_parser('revoke', help='Revoke a token by JTI')
|
|
342
|
+
token_revoke_parser.add_argument('--jti', required=True)
|
|
343
|
+
token_revoke_parser.set_defaults(func=token_revoke_command)
|
|
344
|
+
|
|
269
345
|
delete_agent_parser = subparsers.add_parser('delete-agent', help='Delete an agent by ID')
|
|
270
346
|
delete_agent_parser.add_argument('agent_id')
|
|
271
347
|
delete_agent_parser.set_defaults(func=delete_agent_command)
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from agent_auth.context import AuthContext, ExecutionContext, execution_context_from_token_payload
|
|
11
|
+
from agent_auth.credentials import load_credentials
|
|
12
|
+
from agent_auth.exceptions import (
|
|
13
|
+
AgentNotFoundError,
|
|
14
|
+
AuthServiceError,
|
|
15
|
+
InvalidTokenError,
|
|
16
|
+
PermissionDeniedError,
|
|
17
|
+
RevokedTokenError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthAPIClient:
|
|
22
|
+
"""Thin SDK client for talking to the Agent Auth control plane."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, base_url: str | None = None, token: str | None = None, timeout: int = 30) -> None:
|
|
25
|
+
stored = load_credentials() or {}
|
|
26
|
+
self.base_url = (base_url or os.getenv("AGENT_AUTH_URL") or stored.get("base_url") or "http://127.0.0.1:8002").rstrip("/")
|
|
27
|
+
self.token = token or os.getenv("AGENT_AUTH_TOKEN") or stored.get("token")
|
|
28
|
+
self.timeout = timeout
|
|
29
|
+
|
|
30
|
+
if token:
|
|
31
|
+
self.auth_source = "explicit_token"
|
|
32
|
+
elif os.getenv("AGENT_AUTH_TOKEN"):
|
|
33
|
+
self.auth_source = "env_token"
|
|
34
|
+
elif stored.get("token"):
|
|
35
|
+
self.auth_source = "stored_credentials"
|
|
36
|
+
else:
|
|
37
|
+
self.auth_source = "none"
|
|
38
|
+
|
|
39
|
+
def _headers(self) -> dict[str, str]:
|
|
40
|
+
headers = {"Content-Type": "application/json"}
|
|
41
|
+
if self.token:
|
|
42
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
43
|
+
return headers
|
|
44
|
+
|
|
45
|
+
def decode_token(self, token: str | None = None) -> dict[str, Any] | None:
|
|
46
|
+
raw = token or self.token
|
|
47
|
+
if not raw:
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
parts = raw.split('.')
|
|
51
|
+
if len(parts) != 3:
|
|
52
|
+
return None
|
|
53
|
+
payload = parts[1]
|
|
54
|
+
padding = '=' * (-len(payload) % 4)
|
|
55
|
+
decoded = base64.urlsafe_b64decode(payload + padding)
|
|
56
|
+
return json.loads(decoded.decode('utf-8'))
|
|
57
|
+
except Exception:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def _request(self, method: str, path: str, json: dict[str, Any] | None = None) -> Any:
|
|
61
|
+
response = requests.request(
|
|
62
|
+
method,
|
|
63
|
+
f"{self.base_url}{path}",
|
|
64
|
+
json=json,
|
|
65
|
+
headers=self._headers(),
|
|
66
|
+
timeout=self.timeout,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if response.ok:
|
|
70
|
+
if not response.content:
|
|
71
|
+
return None
|
|
72
|
+
return response.json()
|
|
73
|
+
|
|
74
|
+
detail = None
|
|
75
|
+
try:
|
|
76
|
+
detail = response.json().get("detail")
|
|
77
|
+
except Exception:
|
|
78
|
+
detail = response.text or "Request failed"
|
|
79
|
+
|
|
80
|
+
if response.status_code == 401:
|
|
81
|
+
if detail and ("revoked" in detail.lower() or "inactive" in detail.lower()):
|
|
82
|
+
raise RevokedTokenError(detail)
|
|
83
|
+
raise InvalidTokenError(detail or "Invalid token")
|
|
84
|
+
if response.status_code == 403:
|
|
85
|
+
raise PermissionDeniedError(detail or "Permission denied")
|
|
86
|
+
if response.status_code == 404 and path.startswith("/agents/"):
|
|
87
|
+
raise AgentNotFoundError(detail or "Agent not found")
|
|
88
|
+
if response.status_code == 404 and path == "/agents" and detail and "Project not found" in detail:
|
|
89
|
+
raise AuthServiceError(
|
|
90
|
+
"Project not found. Create the project in the Agent Auth UI first, or update your agent's project_id to match an existing project."
|
|
91
|
+
)
|
|
92
|
+
raise AuthServiceError(detail or f"Request failed with status {response.status_code}")
|
|
93
|
+
|
|
94
|
+
def health(self) -> dict[str, Any]:
|
|
95
|
+
return self._request("GET", "/health")
|
|
96
|
+
|
|
97
|
+
def me(self) -> dict[str, Any]:
|
|
98
|
+
return self._request("GET", "/users/me")
|
|
99
|
+
|
|
100
|
+
def create_agent(self, agent_id: str, name: str, owner: str, role: str, scopes: list[str], project_id: str | None = None) -> dict[str, Any]:
|
|
101
|
+
payload = {
|
|
102
|
+
"agent_id": agent_id,
|
|
103
|
+
"name": name,
|
|
104
|
+
"owner": owner,
|
|
105
|
+
"role": role,
|
|
106
|
+
"scopes": scopes,
|
|
107
|
+
}
|
|
108
|
+
if project_id:
|
|
109
|
+
payload["project_id"] = project_id
|
|
110
|
+
return self._request("POST", "/agents", json=payload)
|
|
111
|
+
|
|
112
|
+
def update_agent(self, agent_id: str, *, name: str | None = None, role: str | None = None, scopes: list[str] | None = None, project_id: str | None = None) -> dict[str, Any]:
|
|
113
|
+
payload: dict[str, Any] = {}
|
|
114
|
+
if name is not None:
|
|
115
|
+
payload["name"] = name
|
|
116
|
+
if role is not None:
|
|
117
|
+
payload["role"] = role
|
|
118
|
+
if scopes is not None:
|
|
119
|
+
payload["scopes"] = scopes
|
|
120
|
+
if project_id is not None:
|
|
121
|
+
payload["project_id"] = project_id
|
|
122
|
+
return self._request("PATCH", f"/agents/{agent_id}", json=payload)
|
|
123
|
+
|
|
124
|
+
def list_projects(self) -> list[dict[str, Any]]:
|
|
125
|
+
return self._request("GET", "/projects")
|
|
126
|
+
|
|
127
|
+
def create_project(self, project_id: str, name: str, description: str = "") -> dict[str, Any]:
|
|
128
|
+
return self._request(
|
|
129
|
+
"POST",
|
|
130
|
+
"/projects",
|
|
131
|
+
json={"project_id": project_id, "name": name, "description": description},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def create_project_token(
|
|
135
|
+
self,
|
|
136
|
+
project_id: str,
|
|
137
|
+
name: str,
|
|
138
|
+
scopes: list[str] | None = None,
|
|
139
|
+
*,
|
|
140
|
+
purpose: str = "sync",
|
|
141
|
+
agent_id: str | None = None,
|
|
142
|
+
) -> dict[str, Any]:
|
|
143
|
+
payload: dict[str, Any] = {
|
|
144
|
+
"project_id": project_id,
|
|
145
|
+
"name": name,
|
|
146
|
+
"scopes": scopes or [],
|
|
147
|
+
"purpose": purpose,
|
|
148
|
+
}
|
|
149
|
+
if agent_id:
|
|
150
|
+
payload["agent_id"] = agent_id
|
|
151
|
+
return self._request("POST", "/tokens/project", json=payload)
|
|
152
|
+
|
|
153
|
+
def create_sync_token(self, project_id: str, name: str = "project-sync-token", scopes: list[str] | None = None) -> dict[str, Any]:
|
|
154
|
+
return self.create_project_token(project_id, name, scopes or ["admin_agents", "admin_tokens"], purpose="sync")
|
|
155
|
+
|
|
156
|
+
def create_runtime_token(self, project_id: str, agent_id: str, name: str = "project-runtime-token", scopes: list[str] | None = None) -> dict[str, Any]:
|
|
157
|
+
return self.create_project_token(project_id, name, scopes or ["docs.read"], purpose="runtime", agent_id=agent_id)
|
|
158
|
+
|
|
159
|
+
def create_automation_token(self, project_id: str, name: str = "project-automation-token", scopes: list[str] | None = None) -> dict[str, Any]:
|
|
160
|
+
return self.create_project_token(project_id, name, scopes or ["admin_agents"], purpose="automation")
|
|
161
|
+
|
|
162
|
+
def list_tokens(self, subject_type: str | None = None) -> list[dict[str, Any]]:
|
|
163
|
+
path = "/tokens"
|
|
164
|
+
if subject_type:
|
|
165
|
+
path = f"/tokens?subject_type={subject_type}"
|
|
166
|
+
return self._request("GET", path)
|
|
167
|
+
|
|
168
|
+
def revoke_token_by_jti(self, jti: str) -> dict[str, Any]:
|
|
169
|
+
return self._request("POST", "/tokens/revoke", json={"jti": jti})
|
|
170
|
+
|
|
171
|
+
def delete_agent(self, agent_id: str) -> dict[str, Any]:
|
|
172
|
+
return self._request("DELETE", f"/agents/{agent_id}")
|
|
173
|
+
|
|
174
|
+
def get_agent(self, agent_id: str) -> dict[str, Any]:
|
|
175
|
+
return self._request("GET", f"/agents/{agent_id}")
|
|
176
|
+
|
|
177
|
+
def issue_token(self, agent_id: str) -> dict[str, Any]:
|
|
178
|
+
return self._request("POST", "/auth/token", json={"agent_id": agent_id})
|
|
179
|
+
|
|
180
|
+
def sync_tools(self, permissions: list[dict[str, str]]) -> dict[str, Any]:
|
|
181
|
+
return self._request("POST", "/policy/permissions/sync", json={"permissions": permissions})
|
|
182
|
+
|
|
183
|
+
def list_permissions(self) -> list[dict[str, Any]]:
|
|
184
|
+
return self._request("GET", "/policy/permissions")
|
|
185
|
+
|
|
186
|
+
def explain_token(self, token: str | None = None) -> dict[str, Any]:
|
|
187
|
+
payload = self.decode_token(token)
|
|
188
|
+
if not payload:
|
|
189
|
+
raise InvalidTokenError("Unable to decode token")
|
|
190
|
+
scopes = list(payload.get("scopes") or [])
|
|
191
|
+
project_id = payload.get("project_id")
|
|
192
|
+
agent_id = payload.get("agent_id")
|
|
193
|
+
purpose = payload.get("purpose")
|
|
194
|
+
if not project_id:
|
|
195
|
+
project_id = next((scope.split(":", 1)[1] for scope in scopes if isinstance(scope, str) and scope.startswith("project:")), None)
|
|
196
|
+
if not agent_id:
|
|
197
|
+
agent_id = next((scope.split(":", 1)[1] for scope in scopes if isinstance(scope, str) and scope.startswith("agent:")), None)
|
|
198
|
+
if not purpose:
|
|
199
|
+
purpose = next((scope.split(":", 1)[1] for scope in scopes if isinstance(scope, str) and scope.startswith("purpose:")), None)
|
|
200
|
+
return {
|
|
201
|
+
"principal_id": payload.get("sub") or payload.get("principal_id"),
|
|
202
|
+
"principal_type": payload.get("principal_type") or payload.get("subject_type") or payload.get("sub_type"),
|
|
203
|
+
"role": payload.get("role"),
|
|
204
|
+
"email": payload.get("email"),
|
|
205
|
+
"purpose": purpose,
|
|
206
|
+
"project_id": project_id,
|
|
207
|
+
"agent_id": agent_id,
|
|
208
|
+
"scopes": scopes,
|
|
209
|
+
"auth_source": self.auth_source,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
def execution_context(self, *, agent_id: str | None = None, role: str | None = None, context: dict[str, Any] | None = None, require_runtime_binding: bool = False) -> ExecutionContext:
|
|
213
|
+
if not self.token:
|
|
214
|
+
raise InvalidTokenError("No token available to derive execution context")
|
|
215
|
+
payload = self.decode_token(self.token)
|
|
216
|
+
if not payload:
|
|
217
|
+
raise InvalidTokenError("Unable to decode token for execution context")
|
|
218
|
+
ctx = execution_context_from_token_payload(payload, agent_id=agent_id, role=role, context=context)
|
|
219
|
+
if require_runtime_binding:
|
|
220
|
+
token_info = self.explain_token()
|
|
221
|
+
if token_info.get("purpose") != "runtime":
|
|
222
|
+
raise InvalidTokenError("Token is not bound for runtime usage")
|
|
223
|
+
if not ctx.project_id:
|
|
224
|
+
raise InvalidTokenError("Runtime token is missing project binding")
|
|
225
|
+
if not ctx.agent_id:
|
|
226
|
+
raise InvalidTokenError("Runtime token is missing agent binding")
|
|
227
|
+
return ctx
|
|
228
|
+
|
|
229
|
+
def runtime_context(self, *, context: dict[str, Any] | None = None, role: str | None = None) -> ExecutionContext:
|
|
230
|
+
return self.execution_context(context=context, role=role, require_runtime_binding=True)
|
|
231
|
+
|
|
232
|
+
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]:
|
|
233
|
+
return self._request(
|
|
234
|
+
"POST",
|
|
235
|
+
"/policy/evaluate",
|
|
236
|
+
json={
|
|
237
|
+
"principal_id": principal_id,
|
|
238
|
+
"action": action,
|
|
239
|
+
"resource": resource,
|
|
240
|
+
"granted_scopes": granted_scopes or [],
|
|
241
|
+
"role": role,
|
|
242
|
+
"context": context or {},
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def auth_context(self, principal_id: str, scopes: list[str], role: str | None = None, principal_type: str = "agent", email: str | None = None) -> AuthContext:
|
|
247
|
+
return AuthContext(
|
|
248
|
+
principal_id=principal_id,
|
|
249
|
+
principal_type=principal_type,
|
|
250
|
+
scopes=scopes,
|
|
251
|
+
role=role,
|
|
252
|
+
email=email,
|
|
253
|
+
)
|