agentauthlayer 0.1.8__tar.gz → 0.1.10__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 (120) hide show
  1. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/MANIFEST.in +0 -1
  2. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/PKG-INFO +32 -1
  3. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/README.md +31 -0
  4. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/__init__.py +3 -1
  5. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/cli.py +118 -16
  6. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/client.py +45 -0
  7. agentauthlayer-0.1.10/agent_auth/context.py +85 -0
  8. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/policy.py +18 -6
  9. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/server_runtime.py +10 -2
  10. agentauthlayer-0.1.10/agent_auth/web_dist/assets/index-CcMhzoW_.css +1 -0
  11. agentauthlayer-0.1.10/agent_auth/web_dist/assets/index-Coc7Vjc1.js +306 -0
  12. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/web_dist/index.html +2 -2
  13. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agentauthlayer.egg-info/PKG-INFO +32 -1
  14. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agentauthlayer.egg-info/SOURCES.txt +3 -3
  15. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/api/routes/agents.py +9 -1
  16. agentauthlayer-0.1.10/auth_app/api/routes/tokens.py +114 -0
  17. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/main.py +16 -0
  18. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/contracts.py +4 -0
  19. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/sqlite_token_repo.py +24 -0
  20. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/sqlite_user_repo.py +16 -4
  21. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/token_repo.py +6 -0
  22. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/schemas/agent.py +7 -0
  23. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/schemas/token.py +25 -0
  24. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/services/agent_service.py +15 -1
  25. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/services/token_service.py +16 -0
  26. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/pyproject.toml +1 -1
  27. agentauthlayer-0.1.10/tests/test_runtime_binding.py +56 -0
  28. agentauthlayer-0.1.8/agent_auth/context.py +0 -21
  29. agentauthlayer-0.1.8/agent_auth/web_dist/assets/index-BBJ7rinV.css +0 -1
  30. agentauthlayer-0.1.8/agent_auth/web_dist/assets/index-DXUoW2DG.js +0 -429
  31. agentauthlayer-0.1.8/agent_auth_definitive_guide.pdf +0 -0
  32. agentauthlayer-0.1.8/auth_app/api/routes/tokens.py +0 -50
  33. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/__main__.py +0 -0
  34. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/agents.py +0 -0
  35. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/audit.py +0 -0
  36. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/auth.py +0 -0
  37. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/core.py +0 -0
  38. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/credentials.py +0 -0
  39. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/delegation.py +0 -0
  40. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/exceptions.py +0 -0
  41. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/models.py +0 -0
  42. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/policy_service.py +0 -0
  43. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/principals.py +0 -0
  44. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/registry.py +0 -0
  45. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/runtime.py +0 -0
  46. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/session.py +0 -0
  47. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/storage.py +0 -0
  48. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/tokens.py +0 -0
  49. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/users.py +0 -0
  50. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/web_dist/favicon.ico +0 -0
  51. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/web_dist/grid.svg +0 -0
  52. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/web_dist/placeholder.svg +0 -0
  53. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agent_auth/web_dist/robots.txt +0 -0
  54. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agentauthlayer.egg-info/dependency_links.txt +0 -0
  55. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agentauthlayer.egg-info/entry_points.txt +0 -0
  56. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agentauthlayer.egg-info/requires.txt +0 -0
  57. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/agentauthlayer.egg-info/top_level.txt +0 -0
  58. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/__init__.py +0 -0
  59. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/api/__init__.py +0 -0
  60. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/api/routes/__init__.py +0 -0
  61. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/api/routes/audit.py +0 -0
  62. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/api/routes/auth.py +0 -0
  63. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/api/routes/bootstrap.py +0 -0
  64. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/api/routes/health.py +0 -0
  65. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/api/routes/policy.py +0 -0
  66. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/api/routes/projects.py +0 -0
  67. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/api/routes/users.py +0 -0
  68. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/core/__init__.py +0 -0
  69. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/core/config.py +0 -0
  70. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/core/db.py +0 -0
  71. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/core/errors.py +0 -0
  72. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/core/logging.py +0 -0
  73. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/dependencies/__init__.py +0 -0
  74. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/dependencies/auth.py +0 -0
  75. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/dependencies/security.py +0 -0
  76. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/dependencies/user.py +0 -0
  77. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/domain/__init__.py +0 -0
  78. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/domain/enums.py +0 -0
  79. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/domain/models.py +0 -0
  80. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/middleware/__init__.py +0 -0
  81. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/middleware/correlation.py +0 -0
  82. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/__init__.py +0 -0
  83. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/agent_repo.py +0 -0
  84. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/audit_repo.py +0 -0
  85. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/constraint_repo.py +0 -0
  86. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/delegation_repo.py +0 -0
  87. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/project_repo.py +0 -0
  88. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/role_repo.py +0 -0
  89. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/sqlite_agent_repo.py +0 -0
  90. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/sqlite_audit_repo.py +0 -0
  91. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/sqlite_constraint_repo.py +0 -0
  92. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/sqlite_delegation_repo.py +0 -0
  93. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/sqlite_permission_repo.py +0 -0
  94. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/sqlite_project_repo.py +0 -0
  95. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/sqlite_role_repo.py +0 -0
  96. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/repositories/user_repo.py +0 -0
  97. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/schemas/__init__.py +0 -0
  98. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/schemas/audit.py +0 -0
  99. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/schemas/auth.py +0 -0
  100. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/schemas/bootstrap.py +0 -0
  101. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/schemas/device_agents.py +0 -0
  102. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/schemas/policy.py +0 -0
  103. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/schemas/project.py +0 -0
  104. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/schemas/user.py +0 -0
  105. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/services/__init__.py +0 -0
  106. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/services/audit_service.py +0 -0
  107. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/services/device_agent_service.py +0 -0
  108. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/services/policy_service.py +0 -0
  109. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/services/project_service.py +0 -0
  110. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/auth_app/services/user_service.py +0 -0
  111. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/setup.cfg +0 -0
  112. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/tests/test_agent_auth_library.py +0 -0
  113. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/tests/test_auth_flow.py +0 -0
  114. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/tests/test_core_first_boundary.py +0 -0
  115. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/tests/test_health.py +0 -0
  116. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/tests/test_iam_policy.py +0 -0
  117. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/tests/test_project_flow.py +0 -0
  118. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/tests/test_sqlite_repos.py +0 -0
  119. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/tests/test_storage.py +0 -0
  120. {agentauthlayer-0.1.8 → agentauthlayer-0.1.10}/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.8
3
+ Version: 0.1.10
4
4
  Summary: Library-first authentication and authorization SDK for AI agents
5
5
  Author: Vaibhav Ahluwalia
6
6
  License: MIT
@@ -32,6 +32,37 @@ 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
+ ## Runtime binding
43
+
44
+ Runtime authorization now enforces stronger execution binding checks for project-scoped flows:
45
+ - project scope mismatches are denied before policy evaluation
46
+ - agent/principal mismatches are denied before policy evaluation
47
+ - execution context automatically carries project, agent, and principal context into authorization checks
48
+
49
+ Use the resulting token in code directly:
50
+
51
+ ```python
52
+ from agent_auth.client import AuthAPIClient
53
+
54
+ client = AuthAPIClient(
55
+ base_url="http://127.0.0.1:8002",
56
+ token="YOUR_PROJECT_TOKEN",
57
+ )
58
+ ```
59
+
60
+ Or store it locally for CLI + SDK reuse:
61
+
62
+ ```bash
63
+ agentauth login --base-url http://127.0.0.1:8002 --token YOUR_PROJECT_TOKEN --email admin@agentauth.dev
64
+ ```
65
+
35
66
  `agentauthlayer` helps you:
36
67
  - start a local Agent Auth backend and UI with one command
37
68
  - authenticate once and reuse local credentials
@@ -2,6 +2,37 @@
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
+ ## Runtime binding
13
+
14
+ Runtime authorization now enforces stronger execution binding checks for project-scoped flows:
15
+ - project scope mismatches are denied before policy evaluation
16
+ - agent/principal mismatches are denied before policy evaluation
17
+ - execution context automatically carries project, agent, and principal context into authorization checks
18
+
19
+ Use the resulting token in code directly:
20
+
21
+ ```python
22
+ from agent_auth.client import AuthAPIClient
23
+
24
+ client = AuthAPIClient(
25
+ base_url="http://127.0.0.1:8002",
26
+ token="YOUR_PROJECT_TOKEN",
27
+ )
28
+ ```
29
+
30
+ Or store it locally for CLI + SDK reuse:
31
+
32
+ ```bash
33
+ agentauth login --base-url http://127.0.0.1:8002 --token YOUR_PROJECT_TOKEN --email admin@agentauth.dev
34
+ ```
35
+
5
36
  `agentauthlayer` helps you:
6
37
  - start a local Agent Auth backend and UI with one command
7
38
  - 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
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,7 @@ __all__ = [
50
51
  "clear_registries",
51
52
  "list_registered_agents",
52
53
  "list_registered_tools",
54
+ "execution_context_from_args",
53
55
  "principal_fields",
54
56
  "principal_from_agent",
55
57
  "principal_from_user",
@@ -6,6 +6,7 @@ import importlib
6
6
  import json
7
7
  import sys
8
8
  import webbrowser
9
+ from pathlib import Path
9
10
 
10
11
  import requests
11
12
 
@@ -125,33 +126,69 @@ def logout_command(args) -> int:
125
126
 
126
127
  def sync_command(args) -> int:
127
128
  clear_registries()
129
+
130
+ cwd = str(Path.cwd())
131
+ if cwd not in sys.path:
132
+ sys.path.insert(0, cwd)
133
+
128
134
  importlib.import_module(args.module)
129
135
 
130
136
  client = AuthAPIClient()
131
137
  tools = list_registered_tools()
132
138
  agents = list_registered_agents()
133
139
 
134
- if tools:
135
- client.sync_tools([
136
- {'action': tool.action, 'description': tool.description}
137
- for tool in tools
138
- ])
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)
139
145
 
140
- created_agents = []
146
+ agent_results = []
141
147
  for agent in agents:
142
- created_agents.append(client.create_agent(
143
- agent_id=agent.agent_id,
144
- name=agent.name,
145
- owner=agent.owner,
146
- role=agent.role,
147
- scopes=agent.scopes,
148
- project_id=agent.project_id,
149
- ))
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'})
150
183
 
151
184
  print(json.dumps({
152
185
  'module': args.module,
153
- 'synced_tools': [tool.action for tool in tools],
154
- 'synced_agents': [agent['agent_id'] for agent in created_agents],
186
+ 'project': args.project,
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,
155
192
  }, indent=2))
156
193
  return 0
157
194
 
@@ -163,6 +200,41 @@ def delete_agent_command(args) -> int:
163
200
  return 0
164
201
 
165
202
 
203
+ def project_list_command(args) -> int:
204
+ client = AuthAPIClient()
205
+ projects = client.list_projects()
206
+ print(json.dumps(projects, indent=2))
207
+ return 0
208
+
209
+
210
+ def project_create_command(args) -> int:
211
+ client = AuthAPIClient()
212
+ project = client.create_project(args.project_id, args.name, args.description or "")
213
+ print(json.dumps(project, indent=2))
214
+ return 0
215
+
216
+
217
+ def token_create_command(args) -> int:
218
+ client = AuthAPIClient()
219
+ token = client.create_project_token(args.project, args.name, args.scopes or [])
220
+ print(json.dumps(token, indent=2))
221
+ return 0
222
+
223
+
224
+ def token_list_command(args) -> int:
225
+ client = AuthAPIClient()
226
+ tokens = client.list_tokens(args.subject_type)
227
+ print(json.dumps(tokens, indent=2))
228
+ return 0
229
+
230
+
231
+ def token_revoke_command(args) -> int:
232
+ client = AuthAPIClient()
233
+ result = client.revoke_token_by_jti(args.jti)
234
+ print(json.dumps(result, indent=2))
235
+ return 0
236
+
237
+
166
238
  def ui_command(args) -> int:
167
239
  creds = load_credentials() or {}
168
240
  base_url = (args.base_url or creds.get('base_url') or DEFAULT_BASE_URL).rstrip('/')
@@ -230,8 +302,38 @@ def main():
230
302
 
231
303
  sync_parser = subparsers.add_parser('sync', help='Import a module and sync its registered tools and agents')
232
304
  sync_parser.add_argument('--module', required=True)
305
+ sync_parser.add_argument('--project', help='Override the project_id used when creating synced agents')
233
306
  sync_parser.set_defaults(func=sync_command)
234
307
 
308
+ project_parser = subparsers.add_parser('project', help='Manage projects from the CLI')
309
+ project_subparsers = project_parser.add_subparsers(dest='project_command')
310
+
311
+ project_list_parser = project_subparsers.add_parser('list', help='List available projects')
312
+ project_list_parser.set_defaults(func=project_list_command)
313
+
314
+ project_create_parser = project_subparsers.add_parser('create', help='Create a project')
315
+ project_create_parser.add_argument('--project-id', required=True)
316
+ project_create_parser.add_argument('--name', required=True)
317
+ project_create_parser.add_argument('--description')
318
+ project_create_parser.set_defaults(func=project_create_command)
319
+
320
+ token_parser = subparsers.add_parser('token', help='Manage project tokens from the CLI')
321
+ token_subparsers = token_parser.add_subparsers(dest='token_command')
322
+
323
+ token_create_parser = token_subparsers.add_parser('create', help='Create a project token')
324
+ token_create_parser.add_argument('--project', required=True)
325
+ token_create_parser.add_argument('--name', required=True)
326
+ token_create_parser.add_argument('--scope', dest='scopes', action='append', help='Scope to include. Repeat for multiple scopes.')
327
+ token_create_parser.set_defaults(func=token_create_command)
328
+
329
+ token_list_parser = token_subparsers.add_parser('list', help='List tokens')
330
+ token_list_parser.add_argument('--subject-type', choices=['user', 'agent', 'system'])
331
+ token_list_parser.set_defaults(func=token_list_command)
332
+
333
+ token_revoke_parser = token_subparsers.add_parser('revoke', help='Revoke a token by JTI')
334
+ token_revoke_parser.add_argument('--jti', required=True)
335
+ token_revoke_parser.set_defaults(func=token_revoke_command)
336
+
235
337
  delete_agent_parser = subparsers.add_parser('delete-agent', help='Delete an agent by ID')
236
338
  delete_agent_parser.add_argument('agent_id')
237
339
  delete_agent_parser.set_defaults(func=delete_agent_command)
@@ -68,6 +68,10 @@ class AuthAPIClient:
68
68
  raise PermissionDeniedError(detail or "Permission denied")
69
69
  if response.status_code == 404 and path.startswith("/agents/"):
70
70
  raise AgentNotFoundError(detail or "Agent not found")
71
+ if response.status_code == 404 and path == "/agents" and detail and "Project not found" in detail:
72
+ raise AuthServiceError(
73
+ "Project not found. Create the project in the Agent Auth UI first, or update your agent's project_id to match an existing project."
74
+ )
71
75
  raise AuthServiceError(detail or f"Request failed with status {response.status_code}")
72
76
 
73
77
  def health(self) -> dict[str, Any]:
@@ -88,6 +92,44 @@ class AuthAPIClient:
88
92
  payload["project_id"] = project_id
89
93
  return self._request("POST", "/agents", json=payload)
90
94
 
95
+ 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]:
96
+ payload: dict[str, Any] = {}
97
+ if name is not None:
98
+ payload["name"] = name
99
+ if role is not None:
100
+ payload["role"] = role
101
+ if scopes is not None:
102
+ payload["scopes"] = scopes
103
+ if project_id is not None:
104
+ payload["project_id"] = project_id
105
+ return self._request("PATCH", f"/agents/{agent_id}", json=payload)
106
+
107
+ def list_projects(self) -> list[dict[str, Any]]:
108
+ return self._request("GET", "/projects")
109
+
110
+ def create_project(self, project_id: str, name: str, description: str = "") -> dict[str, Any]:
111
+ return self._request(
112
+ "POST",
113
+ "/projects",
114
+ json={"project_id": project_id, "name": name, "description": description},
115
+ )
116
+
117
+ def create_project_token(self, project_id: str, name: str, scopes: list[str] | None = None) -> dict[str, Any]:
118
+ return self._request(
119
+ "POST",
120
+ "/tokens/project",
121
+ json={"project_id": project_id, "name": name, "scopes": scopes or []},
122
+ )
123
+
124
+ def list_tokens(self, subject_type: str | None = None) -> list[dict[str, Any]]:
125
+ path = "/tokens"
126
+ if subject_type:
127
+ path = f"/tokens?subject_type={subject_type}"
128
+ return self._request("GET", path)
129
+
130
+ def revoke_token_by_jti(self, jti: str) -> dict[str, Any]:
131
+ return self._request("POST", "/tokens/revoke", json={"jti": jti})
132
+
91
133
  def delete_agent(self, agent_id: str) -> dict[str, Any]:
92
134
  return self._request("DELETE", f"/agents/{agent_id}")
93
135
 
@@ -100,6 +142,9 @@ class AuthAPIClient:
100
142
  def sync_tools(self, permissions: list[dict[str, str]]) -> dict[str, Any]:
101
143
  return self._request("POST", "/policy/permissions/sync", json={"permissions": permissions})
102
144
 
145
+ def list_permissions(self) -> list[dict[str, Any]]:
146
+ return self._request("GET", "/policy/permissions")
147
+
103
148
  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]:
104
149
  return self._request(
105
150
  "POST",
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class AuthContext:
9
+ principal_id: str
10
+ principal_type: str
11
+ jti: str
12
+ scopes: list[str] = field(default_factory=list)
13
+ role: str | None = None
14
+ email: str | None = None
15
+
16
+ @property
17
+ def subject_id(self) -> str:
18
+ return self.principal_id
19
+
20
+ @property
21
+ def subject_type(self) -> str:
22
+ return self.principal_type
23
+
24
+
25
+ @dataclass(slots=True)
26
+ class ExecutionContext:
27
+ principal_id: str | None = None
28
+ principal_type: str | None = None
29
+ agent_id: str | None = None
30
+ project_id: str | None = None
31
+ role: str | None = None
32
+ scopes: list[str] = field(default_factory=list)
33
+ context: dict[str, Any] = field(default_factory=dict)
34
+ email: str | None = None
35
+
36
+ def merged_context(self, extra: dict[str, Any] | None = None) -> dict[str, Any]:
37
+ merged = dict(self.context)
38
+ if self.project_id and 'project_id' not in merged:
39
+ merged['project_id'] = self.project_id
40
+ if self.agent_id and 'agent_id' not in merged:
41
+ merged['agent_id'] = self.agent_id
42
+ if self.principal_id and 'principal_id' not in merged:
43
+ merged['principal_id'] = self.principal_id
44
+ if self.principal_type and 'principal_type' not in merged:
45
+ merged['principal_type'] = self.principal_type
46
+ if extra:
47
+ merged.update(extra)
48
+ return merged
49
+
50
+ def project_scope(self) -> str | None:
51
+ for scope in self.scopes:
52
+ if scope.startswith('project:'):
53
+ return scope.split(':', 1)[1]
54
+ return None
55
+
56
+ def binding_mismatch_reason(self) -> str | None:
57
+ scoped_project = self.project_scope()
58
+ if self.project_id and scoped_project and self.project_id != scoped_project:
59
+ return f"project_scope_mismatch:{self.project_id}!={scoped_project}"
60
+ if self.principal_type == 'agent' and self.agent_id and self.principal_id and self.agent_id != self.principal_id:
61
+ return f"agent_principal_mismatch:{self.agent_id}!={self.principal_id}"
62
+ return None
63
+
64
+
65
+ def execution_context_from_args(
66
+ *,
67
+ principal_id: str | None = None,
68
+ principal_type: str | None = None,
69
+ agent_id: str | None = None,
70
+ project_id: str | None = None,
71
+ role: str | None = None,
72
+ scopes: list[str] | None = None,
73
+ context: dict[str, Any] | None = None,
74
+ email: str | None = None,
75
+ ) -> ExecutionContext:
76
+ return ExecutionContext(
77
+ principal_id=principal_id,
78
+ principal_type=principal_type,
79
+ agent_id=agent_id,
80
+ project_id=project_id,
81
+ role=role,
82
+ scopes=scopes or [],
83
+ context=context or {},
84
+ email=email,
85
+ )
@@ -270,16 +270,28 @@ def require_permission(action: str, resource: str | None = None):
270
270
  def decorator(func: Callable):
271
271
  @wraps(func)
272
272
  def wrapper(*args, **kwargs):
273
- agent_id = kwargs.get('agent_id') or (args[0] if args else None)
274
- role = kwargs.get('role')
275
- context = kwargs.get('context', {}) or {}
276
- granted_scopes = kwargs.get('granted_scopes', []) or []
277
- resource_value = kwargs.get('resource') or resource or '*'
273
+ execution_context = kwargs.get('execution_context')
274
+
275
+ if execution_context is not None:
276
+ mismatch = execution_context.binding_mismatch_reason()
277
+ if mismatch:
278
+ raise PermissionError(f'Access Denied: {mismatch}')
279
+ principal_id = execution_context.principal_id or execution_context.agent_id
280
+ role = execution_context.role
281
+ context = execution_context.merged_context()
282
+ granted_scopes = execution_context.scopes or []
283
+ resource_value = kwargs.get('resource') or resource or '*'
284
+ else:
285
+ principal_id = kwargs.get('agent_id') or (args[0] if args else None)
286
+ role = kwargs.get('role')
287
+ context = kwargs.get('context', {}) or {}
288
+ granted_scopes = kwargs.get('granted_scopes', []) or []
289
+ resource_value = kwargs.get('resource') or resource or '*'
278
290
 
279
291
  evaluator = PolicyEvaluator()
280
292
  decision = evaluator.evaluate(
281
293
  PolicyRequest(
282
- principal_id=agent_id or 'unknown',
294
+ principal_id=principal_id or 'unknown',
283
295
  action=action,
284
296
  resource=resource_value,
285
297
  granted_scopes=granted_scopes,
@@ -21,8 +21,15 @@ def fallback_data_dir() -> Path:
21
21
  return Path.home() / ".agentauth"
22
22
 
23
23
 
24
+ def project_dir() -> Path:
25
+ explicit_project_dir = os.getenv("AGENTAUTH_PROJECT_DIR")
26
+ if explicit_project_dir:
27
+ return Path(explicit_project_dir).expanduser().resolve()
28
+ return Path.cwd().resolve()
29
+
30
+
24
31
  def project_state_dir() -> Path:
25
- return Path.cwd() / ".agentauth"
32
+ return project_dir() / ".agentauth"
26
33
 
27
34
 
28
35
  def local_data_dir() -> Path:
@@ -30,7 +37,7 @@ def local_data_dir() -> Path:
30
37
  if explicit_state_dir:
31
38
  return Path(explicit_state_dir).expanduser().resolve()
32
39
 
33
- cwd = Path.cwd()
40
+ cwd = project_dir()
34
41
  if cwd.exists() and cwd.is_dir():
35
42
  return project_state_dir()
36
43
 
@@ -59,6 +66,7 @@ def ensure_local_env(host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> dict
59
66
  env.setdefault("JWT_SECRET_KEY", f"agentauth_local_{secrets.token_urlsafe(24)}")
60
67
  env.setdefault("CORS_ORIGINS", "*")
61
68
  env.setdefault("AGENTAUTH_STATE_DIR", str(data_dir))
69
+ env.setdefault("AGENTAUTH_PROJECT_DIR", str(project_dir()))
62
70
  return env
63
71
 
64
72