agentauthlayer 0.1.10__tar.gz → 0.1.12__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 (135) hide show
  1. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/PKG-INFO +34 -2
  2. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/README.md +32 -1
  3. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/__init__.py +2 -1
  4. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/cli.py +9 -1
  5. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/client.py +91 -7
  6. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/context.py +41 -0
  7. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/principals.py +3 -0
  8. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/session.py +6 -0
  9. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/users.py +3 -0
  10. agentauthlayer-0.1.12/agent_auth/web_dist/assets/index-BOER6keK.js +21 -0
  11. agentauthlayer-0.1.12/agent_auth/web_dist/assets/index-t7ZqV4V6.css +1 -0
  12. agentauthlayer-0.1.12/agent_auth/web_dist/assets/ui-C1LFd_jm.js +242 -0
  13. agentauthlayer-0.1.12/agent_auth/web_dist/assets/vendor-BNEucebo.js +59 -0
  14. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/web_dist/index.html +4 -2
  15. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agentauthlayer.egg-info/PKG-INFO +34 -2
  16. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agentauthlayer.egg-info/SOURCES.txt +15 -8
  17. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agentauthlayer.egg-info/requires.txt +1 -0
  18. agentauthlayer-0.1.12/auth_app/api/routes/projects.py +145 -0
  19. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/api/routes/tokens.py +6 -0
  20. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/api/routes/users.py +6 -2
  21. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/core/config.py +1 -1
  22. agentauthlayer-0.1.12/auth_app/core/pg_shim.py +67 -0
  23. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/dependencies/auth.py +27 -2
  24. agentauthlayer-0.1.12/auth_app/dependencies/user.py +16 -0
  25. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/domain/models.py +10 -0
  26. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/contracts.py +13 -1
  27. agentauthlayer-0.1.12/auth_app/repositories/postgres_agent_repo.py +114 -0
  28. agentauthlayer-0.1.12/auth_app/repositories/postgres_audit_repo.py +76 -0
  29. agentauthlayer-0.1.12/auth_app/repositories/postgres_constraint_repo.py +47 -0
  30. agentauthlayer-0.1.12/auth_app/repositories/postgres_delegation_repo.py +85 -0
  31. agentauthlayer-0.1.12/auth_app/repositories/postgres_permission_repo.py +41 -0
  32. agentauthlayer-0.1.12/auth_app/repositories/postgres_project_repo.py +175 -0
  33. agentauthlayer-0.1.12/auth_app/repositories/postgres_role_repo.py +68 -0
  34. agentauthlayer-0.1.12/auth_app/repositories/postgres_token_repo.py +117 -0
  35. agentauthlayer-0.1.12/auth_app/repositories/postgres_user_repo.py +233 -0
  36. agentauthlayer-0.1.12/auth_app/repositories/project_repo.py +64 -0
  37. agentauthlayer-0.1.12/auth_app/repositories/sqlite_project_repo.py +160 -0
  38. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/schemas/project.py +17 -6
  39. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/schemas/token.py +4 -0
  40. agentauthlayer-0.1.12/auth_app/services/project_service.py +52 -0
  41. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/services/token_service.py +8 -2
  42. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/services/user_service.py +3 -0
  43. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/pyproject.toml +2 -1
  44. agentauthlayer-0.1.12/tests/test_runtime_binding.py +109 -0
  45. agentauthlayer-0.1.10/agent_auth/web_dist/assets/index-CcMhzoW_.css +0 -1
  46. agentauthlayer-0.1.10/agent_auth/web_dist/assets/index-Coc7Vjc1.js +0 -306
  47. agentauthlayer-0.1.10/auth_app/api/routes/projects.py +0 -84
  48. agentauthlayer-0.1.10/auth_app/dependencies/user.py +0 -38
  49. agentauthlayer-0.1.10/auth_app/repositories/project_repo.py +0 -37
  50. agentauthlayer-0.1.10/auth_app/repositories/sqlite_project_repo.py +0 -104
  51. agentauthlayer-0.1.10/auth_app/services/project_service.py +0 -38
  52. agentauthlayer-0.1.10/tests/test_agent_auth_library.py +0 -267
  53. agentauthlayer-0.1.10/tests/test_core_first_boundary.py +0 -111
  54. agentauthlayer-0.1.10/tests/test_runtime_binding.py +0 -56
  55. agentauthlayer-0.1.10/tests/test_sqlite_repos.py +0 -167
  56. agentauthlayer-0.1.10/tests/test_storage.py +0 -237
  57. agentauthlayer-0.1.10/tests/test_tool_registry.py +0 -197
  58. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/MANIFEST.in +0 -0
  59. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/__main__.py +0 -0
  60. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/agents.py +0 -0
  61. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/audit.py +0 -0
  62. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/auth.py +0 -0
  63. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/core.py +0 -0
  64. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/credentials.py +0 -0
  65. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/delegation.py +0 -0
  66. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/exceptions.py +0 -0
  67. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/models.py +0 -0
  68. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/policy.py +0 -0
  69. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/policy_service.py +0 -0
  70. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/registry.py +0 -0
  71. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/runtime.py +0 -0
  72. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/server_runtime.py +0 -0
  73. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/storage.py +0 -0
  74. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/tokens.py +0 -0
  75. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/web_dist/favicon.ico +0 -0
  76. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/web_dist/grid.svg +0 -0
  77. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/web_dist/placeholder.svg +0 -0
  78. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agent_auth/web_dist/robots.txt +0 -0
  79. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agentauthlayer.egg-info/dependency_links.txt +0 -0
  80. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agentauthlayer.egg-info/entry_points.txt +0 -0
  81. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/agentauthlayer.egg-info/top_level.txt +0 -0
  82. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/__init__.py +0 -0
  83. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/api/__init__.py +0 -0
  84. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/api/routes/__init__.py +0 -0
  85. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/api/routes/agents.py +0 -0
  86. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/api/routes/audit.py +0 -0
  87. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/api/routes/auth.py +0 -0
  88. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/api/routes/bootstrap.py +0 -0
  89. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/api/routes/health.py +0 -0
  90. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/api/routes/policy.py +0 -0
  91. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/core/__init__.py +0 -0
  92. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/core/db.py +0 -0
  93. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/core/errors.py +0 -0
  94. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/core/logging.py +0 -0
  95. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/dependencies/__init__.py +0 -0
  96. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/dependencies/security.py +0 -0
  97. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/domain/__init__.py +0 -0
  98. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/domain/enums.py +0 -0
  99. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/main.py +0 -0
  100. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/middleware/__init__.py +0 -0
  101. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/middleware/correlation.py +0 -0
  102. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/__init__.py +0 -0
  103. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/agent_repo.py +0 -0
  104. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/audit_repo.py +0 -0
  105. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/constraint_repo.py +0 -0
  106. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/delegation_repo.py +0 -0
  107. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/role_repo.py +0 -0
  108. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/sqlite_agent_repo.py +0 -0
  109. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/sqlite_audit_repo.py +0 -0
  110. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/sqlite_constraint_repo.py +0 -0
  111. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/sqlite_delegation_repo.py +0 -0
  112. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/sqlite_permission_repo.py +0 -0
  113. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/sqlite_role_repo.py +0 -0
  114. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/sqlite_token_repo.py +0 -0
  115. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/sqlite_user_repo.py +0 -0
  116. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/token_repo.py +0 -0
  117. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/repositories/user_repo.py +0 -0
  118. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/schemas/__init__.py +0 -0
  119. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/schemas/agent.py +0 -0
  120. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/schemas/audit.py +0 -0
  121. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/schemas/auth.py +0 -0
  122. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/schemas/bootstrap.py +0 -0
  123. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/schemas/device_agents.py +0 -0
  124. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/schemas/policy.py +0 -0
  125. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/schemas/user.py +0 -0
  126. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/services/__init__.py +0 -0
  127. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/services/agent_service.py +0 -0
  128. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/services/audit_service.py +0 -0
  129. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/services/device_agent_service.py +0 -0
  130. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/auth_app/services/policy_service.py +0 -0
  131. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/setup.cfg +0 -0
  132. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/tests/test_auth_flow.py +0 -0
  133. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/tests/test_health.py +0 -0
  134. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/tests/test_iam_policy.py +0 -0
  135. {agentauthlayer-0.1.10 → agentauthlayer-0.1.12}/tests/test_project_flow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentauthlayer
3
- Version: 0.1.10
3
+ Version: 0.1.12
4
4
  Summary: Library-first authentication and authorization SDK for AI agents
5
5
  Author: Vaibhav Ahluwalia
6
6
  License: MIT
@@ -26,6 +26,7 @@ Requires-Dist: bcrypt==4.0.1
26
26
  Requires-Dist: python-multipart
27
27
  Requires-Dist: httpx
28
28
  Requires-Dist: sqlalchemy
29
+ Requires-Dist: psycopg2-binary
29
30
  Requires-Dist: email-validator
30
31
 
31
32
  # agentauthlayer
@@ -39,12 +40,19 @@ You can now create project-scoped tokens from:
39
40
  - the Tokens page in the UI
40
41
  - the CLI with `agentauth token create`
41
42
 
43
+ These tokens support one visible token model with richer internal semantics:
44
+ - `purpose=sync` for registration and sync flows
45
+ - `purpose=runtime` for runtime execution flows
46
+ - `purpose=automation` for service or CI style automation
47
+ - optional `agent_id` binding for runtime-oriented tokens
48
+
42
49
  ## Runtime binding
43
50
 
44
51
  Runtime authorization now enforces stronger execution binding checks for project-scoped flows:
45
52
  - project scope mismatches are denied before policy evaluation
46
53
  - agent/principal mismatches are denied before policy evaluation
47
- - execution context automatically carries project, agent, and principal context into authorization checks
54
+ - execution context automatically carries project, agent, principal, and token-purpose context into authorization checks
55
+ - runtime-bound tokens now carry first-class `project_id`, `agent_id`, and `purpose` fields, with scope-based fallback for older tokens
48
56
 
49
57
  Use the resulting token in code directly:
50
58
 
@@ -63,6 +71,30 @@ Or store it locally for CLI + SDK reuse:
63
71
  agentauth login --base-url http://127.0.0.1:8002 --token YOUR_PROJECT_TOKEN --email admin@agentauth.dev
64
72
  ```
65
73
 
74
+ ### Create a bound runtime token from the CLI
75
+
76
+ ```bash
77
+ agentauth token create \
78
+ --project ai-platform \
79
+ --name research-runtime-token \
80
+ --purpose runtime \
81
+ --agent-id research-agent-01 \
82
+ --scope docs.read
83
+ ```
84
+
85
+ Then use it directly in code:
86
+
87
+ ```python
88
+ from agent_auth import AuthAPIClient
89
+
90
+ client = AuthAPIClient(token="YOUR_RUNTIME_TOKEN")
91
+ ctx = client.execution_context()
92
+
93
+ print(ctx.project_id)
94
+ print(ctx.agent_id)
95
+ print(ctx.context.get("token_purpose"))
96
+ ```
97
+
66
98
  `agentauthlayer` helps you:
67
99
  - start a local Agent Auth backend and UI with one command
68
100
  - authenticate once and reuse local credentials
@@ -9,12 +9,19 @@ You can now create project-scoped tokens from:
9
9
  - the Tokens page in the UI
10
10
  - the CLI with `agentauth token create`
11
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
+
12
18
  ## Runtime binding
13
19
 
14
20
  Runtime authorization now enforces stronger execution binding checks for project-scoped flows:
15
21
  - project scope mismatches are denied before policy evaluation
16
22
  - agent/principal mismatches are denied before policy evaluation
17
- - execution context automatically carries project, agent, and principal context into authorization checks
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
18
25
 
19
26
  Use the resulting token in code directly:
20
27
 
@@ -33,6 +40,30 @@ Or store it locally for CLI + SDK reuse:
33
40
  agentauth login --base-url http://127.0.0.1:8002 --token YOUR_PROJECT_TOKEN --email admin@agentauth.dev
34
41
  ```
35
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
+
36
67
  `agentauthlayer` helps you:
37
68
  - start a local Agent Auth backend and UI with one command
38
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, ExecutionContext, execution_context_from_args
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,
@@ -52,6 +52,7 @@ __all__ = [
52
52
  "list_registered_agents",
53
53
  "list_registered_tools",
54
54
  "execution_context_from_args",
55
+ "execution_context_from_token_payload",
55
56
  "principal_fields",
56
57
  "principal_from_agent",
57
58
  "principal_from_user",
@@ -216,7 +216,13 @@ def project_create_command(args) -> int:
216
216
 
217
217
  def token_create_command(args) -> int:
218
218
  client = AuthAPIClient()
219
- token = client.create_project_token(args.project, args.name, args.scopes or [])
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
+ )
220
226
  print(json.dumps(token, indent=2))
221
227
  return 0
222
228
 
@@ -324,6 +330,8 @@ def main():
324
330
  token_create_parser.add_argument('--project', required=True)
325
331
  token_create_parser.add_argument('--name', required=True)
326
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.')
327
335
  token_create_parser.set_defaults(func=token_create_command)
328
336
 
329
337
  token_list_parser = token_subparsers.add_parser('list', help='List tokens')
@@ -1,11 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
4
+ import json
3
5
  import os
4
6
  from typing import Any
5
7
 
6
8
  import requests
7
9
 
8
- from agent_auth.context import AuthContext
10
+ from agent_auth.context import AuthContext, ExecutionContext, execution_context_from_token_payload
9
11
  from agent_auth.credentials import load_credentials
10
12
  from agent_auth.exceptions import (
11
13
  AgentNotFoundError,
@@ -40,6 +42,21 @@ class AuthAPIClient:
40
42
  headers["Authorization"] = f"Bearer {self.token}"
41
43
  return headers
42
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
+
43
60
  def _request(self, method: str, path: str, json: dict[str, Any] | None = None) -> Any:
44
61
  response = requests.request(
45
62
  method,
@@ -114,12 +131,33 @@ class AuthAPIClient:
114
131
  json={"project_id": project_id, "name": name, "description": description},
115
132
  )
116
133
 
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
- )
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")
123
161
 
124
162
  def list_tokens(self, subject_type: str | None = None) -> list[dict[str, Any]]:
125
163
  path = "/tokens"
@@ -145,6 +183,52 @@ class AuthAPIClient:
145
183
  def list_permissions(self) -> list[dict[str, Any]]:
146
184
  return self._request("GET", "/policy/permissions")
147
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
+
148
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]:
149
233
  return self._request(
150
234
  "POST",
@@ -83,3 +83,44 @@ def execution_context_from_args(
83
83
  context=context or {},
84
84
  email=email,
85
85
  )
86
+
87
+
88
+ def execution_context_from_token_payload(payload: dict[str, Any], *, agent_id: str | None = None, role: str | None = None, context: dict[str, Any] | None = None) -> ExecutionContext:
89
+ scopes = list(payload.get('scopes') or [])
90
+
91
+ project_id = payload.get('project_id') or payload.get('bound_project_id')
92
+ if project_id is None:
93
+ for scope in scopes:
94
+ if isinstance(scope, str) and scope.startswith('project:'):
95
+ project_id = scope.split(':', 1)[1]
96
+ break
97
+
98
+ principal_id = payload.get('sub') or payload.get('principal_id')
99
+ principal_type = payload.get('principal_type') or payload.get('subject_type') or payload.get('sub_type')
100
+
101
+ derived_agent_id = agent_id
102
+ if derived_agent_id is None and principal_type == 'agent':
103
+ derived_agent_id = principal_id
104
+ if derived_agent_id is None:
105
+ derived_agent_id = payload.get('agent_id') or payload.get('bound_agent_id')
106
+ if derived_agent_id is None:
107
+ for scope in scopes:
108
+ if isinstance(scope, str) and scope.startswith('agent:'):
109
+ derived_agent_id = scope.split(':', 1)[1]
110
+ break
111
+
112
+ base_context = dict(context or {})
113
+ token_purpose = payload.get('purpose') or payload.get('token_purpose')
114
+ if token_purpose and 'token_purpose' not in base_context:
115
+ base_context['token_purpose'] = token_purpose
116
+
117
+ return ExecutionContext(
118
+ principal_id=principal_id,
119
+ principal_type=principal_type,
120
+ agent_id=derived_agent_id,
121
+ project_id=project_id,
122
+ role=role or payload.get('role'),
123
+ scopes=scopes,
124
+ context=base_context,
125
+ email=payload.get('email'),
126
+ )
@@ -15,6 +15,9 @@ class Principal:
15
15
  email: str | None = None
16
16
  owner: str | None = None
17
17
  status: str | None = None
18
+ token_purpose: str | None = None
19
+ bound_project_id: str | None = None
20
+ bound_agent_id: str | None = None
18
21
 
19
22
 
20
23
  @dataclass(slots=True)
@@ -121,6 +121,12 @@ class PrincipalTokenService:
121
121
  "email": principal.email,
122
122
  "exp": expires_at,
123
123
  }
124
+ if principal.token_purpose:
125
+ payload["purpose"] = principal.token_purpose
126
+ if principal.bound_project_id:
127
+ payload["project_id"] = principal.bound_project_id
128
+ if principal.bound_agent_id:
129
+ payload["agent_id"] = principal.bound_agent_id
124
130
  if token_type:
125
131
  payload["type"] = token_type
126
132
  token = jwt.encode(payload, self.settings.secret_key, algorithm=self.settings.algorithm)
@@ -129,6 +129,9 @@ class CoreUserService:
129
129
  def list_invites(self) -> list[InviteT]:
130
130
  return self.store.list_invites()
131
131
 
132
+ def get_user_by_email(self, email: str) -> UserT | None:
133
+ return self.store.get_user_by_email(email)
134
+
132
135
  def get_user_by_id(self, user_id: str) -> UserT | None:
133
136
  return self.store.get_user_by_id(user_id)
134
137