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.
Files changed (121) hide show
  1. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/MANIFEST.in +0 -1
  2. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/PKG-INFO +63 -1
  3. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/README.md +62 -0
  4. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/__init__.py +4 -1
  5. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/cli.py +92 -16
  6. agentauthlayer-0.1.11/agent_auth/client.py +253 -0
  7. agentauthlayer-0.1.11/agent_auth/context.py +126 -0
  8. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/policy.py +18 -6
  9. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/principals.py +3 -0
  10. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/server_runtime.py +10 -2
  11. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/session.py +6 -0
  12. agentauthlayer-0.1.11/agent_auth/web_dist/assets/index-Cs3lTJhd.js +320 -0
  13. agentauthlayer-0.1.11/agent_auth/web_dist/assets/index-DaB2dsnD.css +1 -0
  14. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/web_dist/index.html +2 -2
  15. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agentauthlayer.egg-info/PKG-INFO +63 -1
  16. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agentauthlayer.egg-info/SOURCES.txt +3 -3
  17. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/agents.py +9 -1
  18. agentauthlayer-0.1.11/auth_app/api/routes/tokens.py +120 -0
  19. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/main.py +16 -0
  20. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/contracts.py +4 -0
  21. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_token_repo.py +24 -0
  22. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/token_repo.py +6 -0
  23. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/agent.py +7 -0
  24. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/token.py +29 -0
  25. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/agent_service.py +15 -1
  26. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/token_service.py +22 -0
  27. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/pyproject.toml +1 -1
  28. agentauthlayer-0.1.11/tests/test_runtime_binding.py +109 -0
  29. agentauthlayer-0.1.9/agent_auth/client.py +0 -138
  30. agentauthlayer-0.1.9/agent_auth/context.py +0 -21
  31. agentauthlayer-0.1.9/agent_auth/web_dist/assets/index-BBJ7rinV.css +0 -1
  32. agentauthlayer-0.1.9/agent_auth/web_dist/assets/index-DXUoW2DG.js +0 -429
  33. agentauthlayer-0.1.9/agent_auth_definitive_guide.pdf +0 -0
  34. agentauthlayer-0.1.9/auth_app/api/routes/tokens.py +0 -50
  35. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/__main__.py +0 -0
  36. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/agents.py +0 -0
  37. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/audit.py +0 -0
  38. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/auth.py +0 -0
  39. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/core.py +0 -0
  40. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/credentials.py +0 -0
  41. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/delegation.py +0 -0
  42. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/exceptions.py +0 -0
  43. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/models.py +0 -0
  44. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/policy_service.py +0 -0
  45. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/registry.py +0 -0
  46. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/runtime.py +0 -0
  47. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/storage.py +0 -0
  48. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/tokens.py +0 -0
  49. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/users.py +0 -0
  50. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/web_dist/favicon.ico +0 -0
  51. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/web_dist/grid.svg +0 -0
  52. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/web_dist/placeholder.svg +0 -0
  53. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agent_auth/web_dist/robots.txt +0 -0
  54. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agentauthlayer.egg-info/dependency_links.txt +0 -0
  55. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agentauthlayer.egg-info/entry_points.txt +0 -0
  56. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agentauthlayer.egg-info/requires.txt +0 -0
  57. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/agentauthlayer.egg-info/top_level.txt +0 -0
  58. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/__init__.py +0 -0
  59. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/__init__.py +0 -0
  60. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/__init__.py +0 -0
  61. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/audit.py +0 -0
  62. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/auth.py +0 -0
  63. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/bootstrap.py +0 -0
  64. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/health.py +0 -0
  65. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/policy.py +0 -0
  66. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/projects.py +0 -0
  67. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/api/routes/users.py +0 -0
  68. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/core/__init__.py +0 -0
  69. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/core/config.py +0 -0
  70. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/core/db.py +0 -0
  71. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/core/errors.py +0 -0
  72. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/core/logging.py +0 -0
  73. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/dependencies/__init__.py +0 -0
  74. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/dependencies/auth.py +0 -0
  75. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/dependencies/security.py +0 -0
  76. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/dependencies/user.py +0 -0
  77. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/domain/__init__.py +0 -0
  78. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/domain/enums.py +0 -0
  79. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/domain/models.py +0 -0
  80. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/middleware/__init__.py +0 -0
  81. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/middleware/correlation.py +0 -0
  82. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/__init__.py +0 -0
  83. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/agent_repo.py +0 -0
  84. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/audit_repo.py +0 -0
  85. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/constraint_repo.py +0 -0
  86. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/delegation_repo.py +0 -0
  87. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/project_repo.py +0 -0
  88. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/role_repo.py +0 -0
  89. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_agent_repo.py +0 -0
  90. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_audit_repo.py +0 -0
  91. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_constraint_repo.py +0 -0
  92. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_delegation_repo.py +0 -0
  93. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_permission_repo.py +0 -0
  94. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_project_repo.py +0 -0
  95. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_role_repo.py +0 -0
  96. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/sqlite_user_repo.py +0 -0
  97. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/repositories/user_repo.py +0 -0
  98. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/__init__.py +0 -0
  99. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/audit.py +0 -0
  100. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/auth.py +0 -0
  101. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/bootstrap.py +0 -0
  102. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/device_agents.py +0 -0
  103. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/policy.py +0 -0
  104. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/project.py +0 -0
  105. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/schemas/user.py +0 -0
  106. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/__init__.py +0 -0
  107. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/audit_service.py +0 -0
  108. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/device_agent_service.py +0 -0
  109. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/policy_service.py +0 -0
  110. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/project_service.py +0 -0
  111. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/auth_app/services/user_service.py +0 -0
  112. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/setup.cfg +0 -0
  113. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_agent_auth_library.py +0 -0
  114. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_auth_flow.py +0 -0
  115. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_core_first_boundary.py +0 -0
  116. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_health.py +0 -0
  117. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_iam_policy.py +0 -0
  118. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_project_flow.py +0 -0
  119. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_sqlite_repos.py +0 -0
  120. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_storage.py +0 -0
  121. {agentauthlayer-0.1.9 → agentauthlayer-0.1.11}/tests/test_tool_registry.py +0 -0
@@ -1,2 +1 @@
1
1
  recursive-include agent_auth/web_dist *
2
- include agent_auth_definitive_guide.pdf
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentauthlayer
3
- Version: 0.1.9
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
- if tools:
141
- client.sync_tools([
142
- {'action': tool.action, 'description': tool.description}
143
- for tool in tools
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
- created_agents = []
146
+ agent_results = []
147
147
  for agent in agents:
148
- created_agents.append(client.create_agent(
149
- agent_id=agent.agent_id,
150
- name=agent.name,
151
- owner=agent.owner,
152
- role=agent.role,
153
- scopes=agent.scopes,
154
- project_id=args.project or agent.project_id,
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
- 'synced_tools': [tool.action for tool in tools],
161
- 'synced_agents': [agent['agent_id'] for agent in created_agents],
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
+ )