Parcourir la source

Remove tenant model and add password auth

docker il y a 1 mois
Parent
commit
5185353b64
100 fichiers modifiés avec 1309 ajouts et 2959 suppressions
  1. 62 65
      README.md
  2. 19 19
      deployments/docker/.env.example
  3. 8 7
      deployments/docker/docker-compose.yml
  4. 9 1436
      docs/agent-platform-database-design.md
  5. 1 1
      docs/agent-platform-multi-service-roadmap.md
  6. 165 0
      docs/auth-service-design.md
  7. 2 2
      libs/core-db/src/core_db/__init__.py
  8. 2 4
      libs/core-db/src/core_db/mixins.py
  9. 3 6
      libs/core-db/src/core_db/session.py
  10. 8 15
      libs/core-domain/src/core_domain/__init__.py
  11. 1 6
      libs/core-domain/src/core_domain/agent_contracts.py
  12. 1 4
      libs/core-domain/src/core_domain/agent_tool_invocation_contracts.py
  13. 1 7
      libs/core-domain/src/core_domain/auth_contracts.py
  14. 1 2
      libs/core-domain/src/core_domain/code_contracts.py
  15. 1 3
      libs/core-domain/src/core_domain/execution_contracts.py
  16. 1 5
      libs/core-domain/src/core_domain/human_contracts.py
  17. 1 7
      libs/core-domain/src/core_domain/knowledge_contracts.py
  18. 2 7
      libs/core-domain/src/core_domain/memory_contracts.py
  19. 1 2
      libs/core-domain/src/core_domain/model_contracts.py
  20. 1 4
      libs/core-domain/src/core_domain/runtime_contracts.py
  21. 1 4
      libs/core-domain/src/core_domain/scheduler_contracts.py
  22. 1 7
      libs/core-domain/src/core_domain/skill_contracts.py
  23. 1 6
      libs/core-domain/src/core_domain/team_contracts.py
  24. 1 6
      libs/core-domain/src/core_domain/tool_contracts.py
  25. 1 3
      libs/core-domain/src/core_domain/workflow_contracts.py
  26. 2 4
      libs/core-dsl/src/core_dsl/workflow.py
  27. 1 6
      libs/core-events/src/core_events/__init__.py
  28. 1 2
      libs/core-events/src/core_events/client.py
  29. 1 5
      libs/core-events/src/core_events/envelope.py
  30. 1 2
      libs/core-shared/src/core_shared/config.py
  31. 10 16
      libs/core-shared/src/core_shared/observability.py
  32. 9 16
      libs/core-shared/src/core_shared/rate_limit.py
  33. 4 8
      libs/core-shared/src/core_shared/redis_primitives.py
  34. 8 8
      libs/core-shared/src/core_shared/secrets.py
  35. 8 15
      libs/core-shared/src/core_shared/security.py
  36. 10 10
      libs/core-shared/src/core_shared/task_queue.py
  37. 2 2
      libs/core-shared/src/core_shared/types.py
  38. 10 21
      scripts/migrate_all.py
  39. 20 49
      scripts/smoke_runtime_no_key.py
  40. 2 4
      services/agent-service/alembic/env.py
  41. 4 17
      services/agent-service/alembic/versions/20260424_0001_init_agent_models.py
  42. 2 4
      services/agent-service/alembic/versions/20260425_0002_add_agent_run_worker_fields.py
  43. 9 25
      services/agent-service/alembic/versions/20260426_0003_add_agent_tool_invocations.py
  44. 23 48
      services/agent-service/app/api/routes.py
  45. 117 259
      services/agent-service/app/application/services.py
  46. 2 3
      services/agent-service/app/bootstrap/app.py
  47. 3 4
      services/agent-service/app/db/models/agent_definition.py
  48. 3 4
      services/agent-service/app/db/models/agent_run.py
  49. 3 4
      services/agent-service/app/db/models/agent_tool_invocation.py
  50. 3 4
      services/agent-service/app/db/models/agent_version.py
  51. 3 6
      services/agent-service/app/db/session.py
  52. 23 56
      services/agent-service/app/domain/repositories.py
  53. 3 7
      services/agent-service/app/infrastructure/memory_client.py
  54. 2 5
      services/agent-service/app/infrastructure/model_gateway_client.py
  55. 6 15
      services/agent-service/app/infrastructure/skill_client.py
  56. 7 18
      services/agent-service/app/infrastructure/tool_client.py
  57. 2 11
      services/agent-service/app/schemas/agent.py
  58. 8 16
      services/agent-service/app/worker.py
  59. 2 4
      services/api-gateway/alembic/env.py
  60. 2 7
      services/api-gateway/alembic/versions/20260423_0001_add_gateway_request_audit.py
  61. 2 7
      services/api-gateway/alembic/versions/20260423_0002_add_api_keys.py
  62. 93 190
      services/api-gateway/app/api/routes.py
  63. 3 4
      services/api-gateway/app/bootstrap/app.py
  64. 1 1
      services/api-gateway/app/bootstrap/settings.py
  65. 2 3
      services/api-gateway/app/db/models/api_key.py
  66. 2 3
      services/api-gateway/app/db/models/gateway_request_audit.py
  67. 3 6
      services/api-gateway/app/db/session.py
  68. 14 32
      services/api-gateway/app/domain/repositories.py
  69. 0 1
      services/api-gateway/app/infrastructure/api_keys.py
  70. 3 7
      services/api-gateway/app/infrastructure/audit.py
  71. 10 23
      services/api-gateway/app/infrastructure/proxy.py
  72. 13 21
      services/api-gateway/app/infrastructure/rate_limit.py
  73. 90 73
      services/api-gateway/app/infrastructure/request_context.py
  74. 2 9
      services/api-gateway/app/schemas/gateway.py
  75. 4 4
      services/auth-service/alembic/env.py
  76. 5 14
      services/auth-service/alembic/versions/20260425_0001_init_auth_models.py
  77. 27 0
      services/auth-service/alembic/versions/20260427_0002_add_user_password_hash.py
  78. 28 0
      services/auth-service/alembic/versions/20260427_0003_remove_auth_partition_columns.py
  79. 48 33
      services/auth-service/app/api/routes.py
  80. 67 44
      services/auth-service/app/application/services.py
  81. 2 3
      services/auth-service/app/bootstrap/app.py
  82. 3 1
      services/auth-service/app/bootstrap/settings.py
  83. 6 5
      services/auth-service/app/db/models/role.py
  84. 4 3
      services/auth-service/app/db/models/role_assignment.py
  85. 7 6
      services/auth-service/app/db/models/user.py
  86. 2 4
      services/auth-service/app/db/session.py
  87. 38 49
      services/auth-service/app/domain/repositories.py
  88. 46 0
      services/auth-service/app/infrastructure/passwords.py
  89. 88 0
      services/auth-service/app/infrastructure/tokens.py
  90. 41 8
      services/auth-service/app/schemas/auth.py
  91. 5 9
      services/code-runner-service/app/api/routes.py
  92. 2 4
      services/code-runner-service/app/application/services.py
  93. 2 3
      services/code-runner-service/app/bootstrap/app.py
  94. 4 8
      services/code-runner-service/app/infrastructure/runner.py
  95. 2 4
      services/event-service/alembic/env.py
  96. 2 7
      services/event-service/alembic/versions/20260425_0001_init_event_models.py
  97. 12 27
      services/event-service/app/api/routes.py
  98. 10 23
      services/event-service/app/application/services.py
  99. 2 3
      services/event-service/app/bootstrap/app.py
  100. 3 4
      services/event-service/app/db/models/event_record.py

+ 62 - 65
README.md

@@ -111,35 +111,35 @@ alembic upgrade head
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8002/workflows/apps `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","code":"sales_assistant","name":"Sales Assistant"}'
+  -Body '{"code":"sales_assistant","name":"Sales Assistant"}'
 ```
 
 ```powershell
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8001/sessions `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","app_id":"app-1","user_id":"user-1","channel_type":"web"}'
+  -Body '{"app_id":"app-1","user_id":"user-1","channel_type":"web"}'
 ```
 
 ```powershell
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8002/workflows/versions `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","workflow_id":"wf-1","dsl_json":{"nodes":[],"edges":[]}}'
+  -Body '{"workflow_id":"wf-1","dsl_json":{"nodes":[],"edges":[]}}'
 ```
 
 ```powershell
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8001/sessions/run-requests `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","session_id":"sess-1","app_version_id":"appv-1","workflow_version_id":"wfv-1"}'
+  -Body '{"session_id":"sess-1","app_version_id":"appv-1","workflow_version_id":"wfv-1"}'
 ```
 
 ```powershell
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8003/runtime/runs `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","app_id":"app-1","app_version_id":"appv-1","workflow_id":"wf-1","workflow_version_id":"wfv-1","session_id":"sess-1","initial_node":{"node_id":"start","node_type":"llm"}}'
+  -Body '{"app_id":"app-1","app_version_id":"appv-1","workflow_id":"wf-1","workflow_version_id":"wfv-1","session_id":"sess-1","initial_node":{"node_id":"start","node_type":"llm"}}'
 ```
 
 如果不传 `initial_node`,`runtime-service` 会调用 `workflow-service` 读取对应的 `workflow version`,并从 DSL 中自动推导首节点:
@@ -148,7 +148,7 @@ Invoke-RestMethod -Method Post `
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8003/runtime/runs `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","app_id":"app-1","app_version_id":"appv-1","workflow_id":"wf-1","workflow_version_id":"wfv-1","session_id":"sess-1"}'
+  -Body '{"app_id":"app-1","app_version_id":"appv-1","workflow_id":"wf-1","workflow_version_id":"wfv-1","session_id":"sess-1"}'
 ```
 
 一条链直接派发到 runtime:
@@ -157,7 +157,7 @@ Invoke-RestMethod -Method Post `
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8001/sessions/run-requests/dispatch `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","session_id":"sess-1","app_id":"app-1","app_version_id":"appv-1","workflow_id":"wf-1","workflow_version_id":"wfv-1","initial_node":{"node_id":"start","node_type":"llm"}}'
+  -Body '{"session_id":"sess-1","app_id":"app-1","app_version_id":"appv-1","workflow_id":"wf-1","workflow_version_id":"wfv-1","initial_node":{"node_id":"start","node_type":"llm"}}'
 ```
 
 工具定义示例:
@@ -166,14 +166,14 @@ Invoke-RestMethod -Method Post `
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8004/tools `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","code":"search_products","name":"Search Products","tool_type":"http"}'
+  -Body '{"code":"search_products","name":"Search Products","tool_type":"http"}'
 ```
 
 ```powershell
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8004/tools/versions `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","tool_id":"tool-1","input_schema_json":{"query":{"type":"string"}},"invoke_config_json":{"method":"GET","path":"/products/search"}}'
+  -Body '{"tool_id":"tool-1","input_schema_json":{"query":{"type":"string"}},"invoke_config_json":{"method":"GET","path":"/products/search"}}'
 ```
 
 运行状态推进示例:
@@ -246,7 +246,7 @@ Create an agent:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8007/agents `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","code":"sales_agent","name":"Sales Agent","agent_type":"assistant"}'
+  -Body '{"code":"sales_agent","name":"Sales Agent","agent_type":"assistant"}'
 ```
 
 Create a published agent version:
@@ -255,7 +255,7 @@ Create a published agent version:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8007/agents/versions `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","agent_id":"agent-id","status":"published","role":"sales_assistant","goal":"Help qualify leads","system_prompt":"You are a careful sales assistant."}'
+  -Body '{"agent_id":"agent-id","status":"published","role":"sales_assistant","goal":"Help qualify leads","system_prompt":"You are a careful sales assistant."}'
 ```
 
 Enable multi-step ReAct planning for an agent version:
@@ -280,14 +280,14 @@ Create an agent run. If `agent_version_id` is omitted, the latest published vers
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8007/agents/runs `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","agent_id":"agent-id","session_id":"session-id","input_text":"Summarize this lead."}'
+  -Body '{"agent_id":"agent-id","session_id":"session-id","input_text":"Summarize this lead."}'
 ```
 
 List tool invocation records for an agent run:
 
 ```powershell
 Invoke-RestMethod `
-  -Uri "http://127.0.0.1:8007/agents/runs/agent-run-id/tool-invocations?tenant_id=t1"
+  -Uri "http://127.0.0.1:8007/agents/runs/agent-run-id/tool-invocations"
 ```
 
 Agent execution now persists tool invocation audit records with selected,
@@ -298,7 +298,7 @@ Through `api-gateway`, use `/gateway/agents/**`.
 
 ## Memory Service APIs
 
-`memory-service` stores scoped memories for tenants, users, sessions, agents, and teams. The first version uses database text search so it works without vector infrastructure; pgvector can be added later behind the same API.
+`memory-service` stores scoped memories for users, sessions, agents, and teams. The first version uses database text search so it works without vector infrastructure; pgvector can be added later behind the same API.
 
 Memory search now stores a local deterministic embedding per memory and uses hybrid rerank:
 
@@ -313,7 +313,7 @@ Create a memory:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8008/memories `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","scope_type":"session","scope_id":"session-id","memory_type":"fact","content_text":"User prefers concise answers.","importance_score":80}'
+  -Body '{"scope_type":"session","scope_id":"session-id","memory_type":"fact","content_text":"User prefers concise answers.","importance_score":80}'
 ```
 
 Search memories:
@@ -322,7 +322,7 @@ Search memories:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8008/memories/search `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","query":"concise","scope_type":"session","scope_id":"session-id","limit":5}'
+  -Body '{"query":"concise","scope_type":"session","scope_id":"session-id","limit":5}'
 ```
 
 Through `api-gateway`, use `/gateway/memories/**`.
@@ -337,7 +337,7 @@ Create a team:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8009/teams `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","code":"research_team","name":"Research Team","team_type":"collaborative"}'
+  -Body '{"code":"research_team","name":"Research Team","team_type":"collaborative"}'
 ```
 
 Create a published team version:
@@ -346,7 +346,7 @@ Create a published team version:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8009/teams/versions `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","team_id":"team-id","status":"published","coordination_mode":"supervisor","objective":"Research and summarize complex questions","member_refs":[{"member_key":"lead","agent_id":"agent-lead","role":"supervisor","responsibility":"Plan and assign work"},{"member_key":"writer","agent_id":"agent-writer","role":"executor","responsibility":"Draft final answer"}]}'
+  -Body '{"team_id":"team-id","status":"published","coordination_mode":"supervisor","objective":"Research and summarize complex questions","member_refs":[{"member_key":"lead","agent_id":"agent-lead","role":"supervisor","responsibility":"Plan and assign work"},{"member_key":"writer","agent_id":"agent-writer","role":"executor","responsibility":"Draft final answer"}]}'
 ```
 
 Create a team run. If `team_version_id` is omitted, the latest published version is used:
@@ -355,7 +355,7 @@ Create a team run. If `team_version_id` is omitted, the latest published version
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8009/teams/runs `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","team_id":"team-id","session_id":"session-id","input_text":"Analyze this customer request."}'
+  -Body '{"team_id":"team-id","session_id":"session-id","input_text":"Analyze this customer request."}'
 ```
 
 Through `api-gateway`, use `/gateway/teams/**`.
@@ -368,7 +368,7 @@ without model API keys:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8009/teams/runs/team-run-id/execute `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","worker_key":"team-worker-1","dry_run":true}'
+  -Body '{"worker_key":"team-worker-1","dry_run":true}'
 ```
 
 Execute one queued team run through the worker claim API:
@@ -402,7 +402,7 @@ Create a skill:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8010/skills `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","code":"hello_user","name":"Hello User","skill_type":"template"}'
+  -Body '{"code":"hello_user","name":"Hello User","skill_type":"template"}'
 ```
 
 Create a published skill version:
@@ -411,16 +411,16 @@ Create a published skill version:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8010/skills/versions `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","skill_id":"skill-id","status":"published","runtime_type":"template","parameter_schema_json":{"name":{"type":"string"}},"implementation_json":{"template":"Hello $name"}}'
+  -Body '{"skill_id":"skill-id","status":"published","runtime_type":"template","parameter_schema_json":{"name":{"type":"string"}},"implementation_json":{"template":"Hello $name"}}'
 ```
 
-Install the skill for a tenant, agent, team, app, or user scope:
+Install the skill for an agent, team, app, or user scope:
 
 ```powershell
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8010/skills/installations `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","skill_id":"skill-id","install_scope":"tenant","scope_id":"t1","installed_by":"user-1"}'
+  -Body '{"skill_id":"skill-id","install_scope":"workspace","scope_id":"t1","installed_by":"user-1"}'
 ```
 
 Create and execute a skill run:
@@ -429,12 +429,12 @@ Create and execute a skill run:
 $run = Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8010/skills/runs `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","skill_id":"skill-id","input_json":{"name":"Lucas"}}'
+  -Body '{"skill_id":"skill-id","input_json":{"name":"Lucas"}}'
 
 Invoke-RestMethod -Method Post `
   -Uri "http://127.0.0.1:8010/skills/runs/$($run.id)/execute" `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","worker_key":"skill-worker-1"}'
+  -Body '{"worker_key":"skill-worker-1"}'
 ```
 
 Through `api-gateway`, use `/gateway/skills/**`.
@@ -450,7 +450,7 @@ Create an approval task:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8011/human/tasks `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","task_type":"approval","title":"Approve refund","run_id":"run-id","node_run_id":"node-run-id","assigned_to":"ops-1","request_payload_json":{"amount":99}}'
+  -Body '{"task_type":"approval","title":"Approve refund","run_id":"run-id","node_run_id":"node-run-id","assigned_to":"ops-1","request_payload_json":{"amount":99}}'
 ```
 
 Claim and complete a task:
@@ -459,12 +459,12 @@ Claim and complete a task:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8011/human/tasks/human-task-id/claim `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","claimed_by":"ops-1"}'
+  -Body '{"claimed_by":"ops-1"}'
 
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8011/human/tasks/human-task-id/complete `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","status":"approved","response_payload_json":{"approved":true}}'
+  -Body '{"status":"approved","response_payload_json":{"approved":true}}'
 ```
 
 Through `api-gateway`, use `/gateway/human/**`.
@@ -483,7 +483,7 @@ After completing the human task, resume the blocked node:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8003/runtime/node-runs/node-run-id/resume-human `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","human_task_id":"human-task-id","worker_key":"runtime-worker-1"}'
+  -Body '{"human_task_id":"human-task-id","worker_key":"runtime-worker-1"}'
 ```
 
 ## Knowledge Service APIs
@@ -506,7 +506,7 @@ Create a knowledge base:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8012/knowledge/bases `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","code":"support_kb","name":"Support Knowledge Base"}'
+  -Body '{"code":"support_kb","name":"Support Knowledge Base"}'
 ```
 
 Create and index a document:
@@ -515,7 +515,7 @@ Create and index a document:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8012/knowledge/documents `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","knowledge_base_id":"kb-id","title":"Refund Policy","content_text":"Refunds are available within seven days for eligible orders.","source_type":"text"}'
+  -Body '{"knowledge_base_id":"kb-id","title":"Refund Policy","content_text":"Refunds are available within seven days for eligible orders.","source_type":"text"}'
 ```
 
 Search the knowledge base:
@@ -524,7 +524,7 @@ Search the knowledge base:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8012/knowledge/search `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","knowledge_base_id":"kb-id","query":"refund within seven days","top_k":3}'
+  -Body '{"knowledge_base_id":"kb-id","query":"refund within seven days","top_k":3}'
 ```
 
 Through `api-gateway`, use `/gateway/knowledge/**`.
@@ -541,7 +541,7 @@ Publish an event:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8013/events `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","event_type":"run.created","source_service":"runtime-service","aggregate_type":"workflow_run","aggregate_id":"run-id","payload_json":{"run_id":"run-id"}}'
+  -Body '{"event_type":"run.created","source_service":"runtime-service","aggregate_type":"workflow_run","aggregate_id":"run-id","payload_json":{"run_id":"run-id"}}'
 ```
 
 Claim pending events for a delivery worker:
@@ -550,7 +550,7 @@ Claim pending events for a delivery worker:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8013/events/claim-pending `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","limit":50}'
+  -Body '{"limit":50}'
 ```
 
 Through `api-gateway`, use `/gateway/events/**`.
@@ -558,28 +558,28 @@ Through `api-gateway`, use `/gateway/events/**`.
 ## Auth Service APIs
 
 `auth-service` stores users, roles, role assignments, and permission checks.
-This is the first RBAC layer for tenant governance.
+This is the RBAC layer for platform governance.
 
 ```powershell
 $user = Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8014/auth/users `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","username":"alice","display_name":"Alice"}'
+  -Body '{"username":"alice","display_name":"Alice"}'
 
 $role = Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8014/auth/roles `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","code":"admin","name":"Admin","permissions_json":["*"]}'
+  -Body '{"code":"admin","name":"Admin","permissions_json":["*"]}'
 
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8014/auth/assignments `
   -ContentType "application/json" `
-  -Body "{`"tenant_id`":`"t1`",`"user_id`":`"$($user.id)`",`"role_id`":`"$($role.id)`"}"
+  -Body "{`"user_id`":`"$($user.id)`",`"role_id`":`"$($role.id)`"}"
 
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8014/auth/permissions/check `
   -ContentType "application/json" `
-  -Body "{`"tenant_id`":`"t1`",`"user_id`":`"$($user.id)`",`"permission`":`"workflow:write`"}"
+  -Body "{`"user_id`":`"$($user.id)`",`"permission`":`"workflow:write`"}"
 ```
 
 Through `api-gateway`, use `/gateway/auth/**`.
@@ -596,7 +596,7 @@ Create a scheduled job:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8015/scheduler/jobs `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","job_type":"runtime","name":"Run workflow later","schedule_time":"2026-04-26T12:00:00Z","payload_json":{"workflow_run_id":"run-id"}}'
+  -Body '{"job_type":"runtime","name":"Run workflow later","schedule_time":"2026-04-26T12:00:00Z","payload_json":{"workflow_run_id":"run-id"}}'
 ```
 
 Claim due jobs for a worker:
@@ -605,7 +605,7 @@ Claim due jobs for a worker:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8015/scheduler/jobs/claim-due `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","worker_key":"scheduler-worker-1","limit":20}'
+  -Body '{"worker_key":"scheduler-worker-1","limit":20}'
 ```
 
 Mark a job completed or failed:
@@ -614,7 +614,7 @@ Mark a job completed or failed:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8015/scheduler/jobs/job-id/status `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","status":"completed"}'
+  -Body '{"status":"completed"}'
 ```
 
 Through `api-gateway`, use `/gateway/scheduler/**`.
@@ -635,7 +635,7 @@ Execute an agent run without calling an external model:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8007/agents/runs/agent-run-id/execute `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","worker_key":"agent-worker-1","dry_run":true}'
+  -Body '{"worker_key":"agent-worker-1","dry_run":true}'
 ```
 
 Execute with `model-gateway-service`:
@@ -644,13 +644,13 @@ Execute with `model-gateway-service`:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8007/agents/runs/agent-run-id/execute `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","worker_key":"agent-worker-1"}'
+  -Body '{"worker_key":"agent-worker-1"}'
 ```
 
 Agent memory policy is stored on `agent_version.memory_policy_json`:
 
 - `enabled`: read memories before execution
-- `memory_scope`: one of `tenant`, `user`, `session`, `agent`, or `team`
+- `memory_scope`: one of `user`, `session`, `agent`, or `team`
 - `read_top_k`: maximum memories to inject into the prompt
 - `write_enabled`: write a conversation memory after successful model execution
 - `config_json.write_importance_score`: optional importance score for written memories
@@ -669,7 +669,7 @@ Example version with session memory:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8007/agents/versions `
   -ContentType "application/json" `
-  -Body '{"tenant_id":"t1","agent_id":"agent-id","status":"published","role":"assistant","system_prompt":"Use relevant memory when helpful.","memory_policy":{"enabled":true,"memory_scope":"session","read_top_k":5,"write_enabled":true,"config_json":{"write_importance_score":60}}}'
+  -Body '{"agent_id":"agent-id","status":"published","role":"assistant","system_prompt":"Use relevant memory when helpful.","memory_policy":{"enabled":true,"memory_scope":"session","read_top_k":5,"write_enabled":true,"config_json":{"write_importance_score":60}}}'
 ```
 
 Execute one queued agent run through the worker claim API:
@@ -719,7 +719,7 @@ Execute the next queued node in a run:
 
 ```powershell
 Invoke-RestMethod -Method Post `
-  -Uri "http://127.0.0.1:8003/runtime/runs/run-id/execute-next?tenant_id=t1" `
+  -Uri "http://127.0.0.1:8003/runtime/runs/run-id/execute-next" `
   -ContentType "application/json" `
   -Body '{"worker_key":"runtime-worker-1"}'
 ```
@@ -728,7 +728,7 @@ Execute queued nodes in sequence until the run is finished, blocked, or reaches
 
 ```powershell
 Invoke-RestMethod -Method Post `
-  -Uri "http://127.0.0.1:8003/runtime/runs/run-id/execute?tenant_id=t1" `
+  -Uri "http://127.0.0.1:8003/runtime/runs/run-id/execute" `
   -ContentType "application/json" `
   -Body '{"worker_key":"runtime-worker-1","max_steps":16}'
 ```
@@ -770,7 +770,7 @@ Query artifacts:
 
 ```powershell
 Invoke-RestMethod `
-  -Uri "http://127.0.0.1:8003/runtime/node-artifacts?tenant_id=t1&run_id=run-id"
+  -Uri "http://127.0.0.1:8003/runtime/node-artifacts?run_id=run-id"
 ```
 
 Trace spans are persisted on `trace_span` for timeline and latency analysis:
@@ -789,7 +789,7 @@ Query trace spans:
 
 ```powershell
 Invoke-RestMethod `
-  -Uri "http://127.0.0.1:8003/runtime/trace-spans?tenant_id=t1&run_id=run-id"
+  -Uri "http://127.0.0.1:8003/runtime/trace-spans?run_id=run-id"
 ```
 
 Current behavior:
@@ -1025,24 +1025,24 @@ Invoke-RestMethod -Uri "http://127.0.0.1:8000/gateway/services/health"
 Gateway request context:
 
 - Incoming `x-request-id` is reused; otherwise gateway generates one.
-- Incoming `x-tenant-id` is reused; otherwise gateway falls back to `tenant_id` query parameter, then `public`.
-- Gateway forwards both `x-request-id` and `x-tenant-id` to downstream services.
+- Incoming `x-workspace-id` is reused; otherwise gateway falls back to `workspace_id` query parameter, then `public`.
+- Gateway forwards both `x-request-id` and `x-workspace-id` to downstream services.
 - Gateway writes request audit records to `gateway_request_audit`.
 
 Query gateway audits:
 
 ```powershell
 Invoke-RestMethod `
-  -Uri "http://127.0.0.1:8000/gateway/audits?tenant_id=t1&limit=20" `
-  -Headers @{"x-tenant-id"="t1"}
+  -Uri "http://127.0.0.1:8000/gateway/audits?limit=20" `
+  -Headers @{}
 ```
 
 Query gateway audit stats:
 
 ```powershell
 Invoke-RestMethod `
-  -Uri "http://127.0.0.1:8000/gateway/audits/stats?tenant_id=t1" `
-  -Headers @{"x-tenant-id"="t1"}
+  -Uri "http://127.0.0.1:8000/gateway/audits/stats" `
+  -Headers @{}
 ```
 
 Gateway API Key auth:
@@ -1059,8 +1059,7 @@ Create an API key:
 
 ```powershell
 $body = @{
-  tenant_id = "t1"
-  name = "local-dev"
+    name = "local-dev"
   scopes = "gateway:agents:* gateway:runtime:read"
 } | ConvertTo-Json
 
@@ -1077,23 +1076,22 @@ Use an API key:
 
 ```powershell
 Invoke-RestMethod `
-  -Uri "http://127.0.0.1:8000/gateway/audits?tenant_id=t1" `
-  -Headers @{"x-tenant-id"="t1"; "x-api-key"=$created.api_key}
+  -Uri "http://127.0.0.1:8000/gateway/audits" `
+  -Headers @{"x-api-key"=$created.api_key}
 ```
 
 Disable or revoke an API key:
 
 ```powershell
 $body = @{
-  tenant_id = "t1"
-  status = "revoked"
+    status = "revoked"
 } | ConvertTo-Json
 
 Invoke-RestMethod `
   -Method Patch `
   -Uri "http://127.0.0.1:8000/gateway/api-keys/$($created.id)/status" `
   -ContentType "application/json" `
-  -Headers @{"x-tenant-id"="t1"; "x-api-key"=$created.api_key} `
+  -Headers @{"x-api-key"=$created.api_key} `
   -Body $body
 ```
 
@@ -1102,7 +1100,6 @@ Run smoke test through an authenticated gateway:
 ```powershell
 $env:AGENT_PLATFORM_SMOKE_WORKFLOW_URL="http://127.0.0.1:8000/gateway/workflows"
 $env:AGENT_PLATFORM_SMOKE_RUNTIME_URL="http://127.0.0.1:8000/gateway/runtime"
-$env:AGENT_PLATFORM_SMOKE_TENANT_ID="t1"
 $env:AGENT_PLATFORM_SMOKE_API_KEY=$created.api_key
 .\.venv\Scripts\python scripts\smoke_runtime_no_key.py
 ```

+ 19 - 19
deployments/docker/.env.example

@@ -1,23 +1,23 @@
 AGENT_PLATFORM_PROVIDER_BASE_URL=https://api.openai.com/v1
 AGENT_PLATFORM_PROVIDER_API_KEY=replace-me
 AGENT_PLATFORM_DEFAULT_MODEL=gpt-4o-mini
-AGENT_PLATFORM_POSTGRES_USER=agent_platform
-AGENT_PLATFORM_POSTGRES_PASSWORD=agent_platform
-AGENT_PLATFORM_POSTGRES_DB=agent_platform
-AGENT_PLATFORM_WORKFLOW_DATABASE_URL=postgresql+psycopg://agent_platform:agent_platform@postgres:5432/workflow_service
-AGENT_PLATFORM_SESSION_DATABASE_URL=postgresql+psycopg://agent_platform:agent_platform@postgres:5432/session_service
-AGENT_PLATFORM_RUNTIME_DATABASE_URL=postgresql+psycopg://agent_platform:agent_platform@postgres:5432/runtime_service
-AGENT_PLATFORM_TOOL_DATABASE_URL=postgresql+psycopg://agent_platform:agent_platform@postgres:5432/tool_service
-AGENT_PLATFORM_AGENT_DATABASE_URL=postgresql+psycopg://agent_platform:agent_platform@postgres:5432/agent_service
-AGENT_PLATFORM_MEMORY_DATABASE_URL=postgresql+psycopg://agent_platform:agent_platform@postgres:5432/memory_service
-AGENT_PLATFORM_TEAM_DATABASE_URL=postgresql+psycopg://agent_platform:agent_platform@postgres:5432/team_service
-AGENT_PLATFORM_SKILL_DATABASE_URL=postgresql+psycopg://agent_platform:agent_platform@postgres:5432/skill_service
-AGENT_PLATFORM_HUMAN_DATABASE_URL=postgresql+psycopg://agent_platform:agent_platform@postgres:5432/human_service
-AGENT_PLATFORM_KNOWLEDGE_DATABASE_URL=postgresql+psycopg://agent_platform:agent_platform@postgres:5432/knowledge_service
-AGENT_PLATFORM_EVENT_DATABASE_URL=postgresql+psycopg://agent_platform:agent_platform@postgres:5432/event_service
-AGENT_PLATFORM_AUTH_DATABASE_URL=postgresql+psycopg://agent_platform:agent_platform@postgres:5432/auth_service
-AGENT_PLATFORM_SCHEDULER_DATABASE_URL=postgresql+psycopg://agent_platform:agent_platform@postgres:5432/scheduler_service
-AGENT_PLATFORM_API_GATEWAY_DATABASE_URL=postgresql+psycopg://agent_platform:agent_platform@postgres:5432/api_gateway
+AGENT_PLATFORM_POSTGRES_USER=admin
+AGENT_PLATFORM_POSTGRES_PASSWORD=hFOvG5UBeK5KIGhz5cQH
+AGENT_PLATFORM_POSTGRES_DB=vectordb
+AGENT_PLATFORM_WORKFLOW_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/workflow_service
+AGENT_PLATFORM_SESSION_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/session_service
+AGENT_PLATFORM_RUNTIME_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/runtime_service
+AGENT_PLATFORM_TOOL_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/tool_service
+AGENT_PLATFORM_AGENT_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/agent_service
+AGENT_PLATFORM_MEMORY_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/memory_service
+AGENT_PLATFORM_TEAM_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/team_service
+AGENT_PLATFORM_SKILL_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/skill_service
+AGENT_PLATFORM_HUMAN_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/human_service
+AGENT_PLATFORM_KNOWLEDGE_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/knowledge_service
+AGENT_PLATFORM_EVENT_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/event_service
+AGENT_PLATFORM_AUTH_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+AGENT_PLATFORM_SCHEDULER_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/scheduler_service
+AGENT_PLATFORM_API_GATEWAY_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/api_gateway
 AGENT_PLATFORM_REDIS_URL=redis://redis:6379/0
 AGENT_PLATFORM_EMBEDDING_PROVIDER=local
 AGENT_PLATFORM_EMBEDDING_BASE_URL=
@@ -28,13 +28,13 @@ AGENT_PLATFORM_RETRIEVAL_VECTOR_WEIGHT=0.30
 AGENT_PLATFORM_RETRIEVAL_RERANK_WEIGHT=0.15
 AGENT_PLATFORM_RETRIEVAL_RERANK_ENABLED=true
 AGENT_PLATFORM_MAX_TIMEOUT_SECONDS=30
-AGENT_PLATFORM_AUTH_REQUIRED=false
+AGENT_PLATFORM_AUTH_REQUIRED=true
 AGENT_PLATFORM_AUTHZ_REQUIRED=false
 AGENT_PLATFORM_INTERNAL_SERVICE_AUTH_REQUIRED=false
 AGENT_PLATFORM_INTERNAL_SERVICE_TOKEN=replace-with-shared-internal-token
 AGENT_PLATFORM_CREDENTIAL_ENCRYPTION_KEY=replace-with-strong-credential-encryption-key
 AGENT_PLATFORM_RATE_LIMIT_ENABLED=false
-AGENT_PLATFORM_TENANT_RATE_LIMIT_PER_MINUTE=600
+AGENT_PLATFORM_GLOBAL_RATE_LIMIT_PER_MINUTE=600
 AGENT_PLATFORM_API_KEY_RATE_LIMIT_PER_MINUTE=1200
 AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS=1
 AGENT_PLATFORM_WORKER_LEASE_SECONDS=300

+ 8 - 7
deployments/docker/docker-compose.yml

@@ -6,11 +6,12 @@ x-agent-platform-common-env: &agent-platform-common-env
 services:
   postgres:
     image: pgvector/pgvector:pg16
-    container_name: agent-platform-postgres
+    container_name: pgsql-vector
+    restart: always
     environment:
-      POSTGRES_USER: ${AGENT_PLATFORM_POSTGRES_USER:-agent_platform}
-      POSTGRES_PASSWORD: ${AGENT_PLATFORM_POSTGRES_PASSWORD:-agent_platform}
-      POSTGRES_DB: ${AGENT_PLATFORM_POSTGRES_DB:-agent_platform}
+      POSTGRES_USER: ${AGENT_PLATFORM_POSTGRES_USER:-admin}
+      POSTGRES_PASSWORD: ${AGENT_PLATFORM_POSTGRES_PASSWORD:-hFOvG5UBeK5KIGhz5cQH}
+      POSTGRES_DB: ${AGENT_PLATFORM_POSTGRES_DB:-vectordb}
     ports:
       - "5432:5432"
     volumes:
@@ -439,7 +440,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8014"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_AUTH_DATABASE_URL:-sqlite:////data/auth_service.db}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_AUTH_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
       AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
     ports:
       - "8014:8014"
@@ -620,10 +621,10 @@ services:
       AGENT_PLATFORM_EVENT_SERVICE_URL: http://event-service:8013
       AGENT_PLATFORM_AUTH_SERVICE_URL: http://auth-service:8014
       AGENT_PLATFORM_SCHEDULER_SERVICE_URL: http://scheduler-service:8015
-      AGENT_PLATFORM_AUTH_REQUIRED: ${AGENT_PLATFORM_AUTH_REQUIRED:-false}
+      AGENT_PLATFORM_AUTH_REQUIRED: ${AGENT_PLATFORM_AUTH_REQUIRED:-true}
       AGENT_PLATFORM_AUTHZ_REQUIRED: ${AGENT_PLATFORM_AUTHZ_REQUIRED:-false}
       AGENT_PLATFORM_RATE_LIMIT_ENABLED: ${AGENT_PLATFORM_RATE_LIMIT_ENABLED:-false}
-      AGENT_PLATFORM_TENANT_RATE_LIMIT_PER_MINUTE: ${AGENT_PLATFORM_TENANT_RATE_LIMIT_PER_MINUTE:-600}
+      AGENT_PLATFORM_GLOBAL_RATE_LIMIT_PER_MINUTE: ${AGENT_PLATFORM_GLOBAL_RATE_LIMIT_PER_MINUTE:-600}
       AGENT_PLATFORM_API_KEY_RATE_LIMIT_PER_MINUTE: ${AGENT_PLATFORM_API_KEY_RATE_LIMIT_PER_MINUTE:-1200}
     ports:
       - "8000:8000"

+ 9 - 1436
docs/agent-platform-database-design.md

@@ -1,1441 +1,14 @@
-# Python 智能体开发平台数据库设计(多服务 / 可横向扩展版)
+# Agent Platform Database Design
 
-## 1. 设计目标
+This document now describes a single-workspace deployment. Business tables are keyed by their own primary IDs and foreign keys only; no workspace partition column is part of the schema.
 
-本设计面向一套基于 Python 的智能体开发平台,要求支持:
+## Core Rule
 
-- 多应用、多租户、多环境
-- 多智能体、团队协作、复杂工作流
-- 会话、运行态、上下文、记忆、技能、工具、插件
-- 多服务部署与水平扩展
-- 高并发写入、长链路执行、可观测与审计
+Do not add partition fields for workspace isolation to API payloads, domain contracts, SQLAlchemy models, indexes, or migrations.
 
-数据库设计的目标不是只满足 `V0.1`,而是保证从早期共库部署到后期多服务独立演进都能平滑过渡。
+## Primary Domains
 
-## 2. 总体原则
-
-### 2.1 核心原则
-
-- 配置数据和运行数据分离
-- 热路径数据和冷路径数据分离
-- 强事务数据优先放 PostgreSQL
-- 短期状态与幂等控制优先放 Redis
-- 语义记忆与知识检索使用 `pgvector`
-- 文件和产物不落数据库,统一进入对象存储
-
-### 2.2 分库分域原则
-
-初期可以由一个 PostgreSQL 集群承载多个 schema,后期再按热点服务拆分成独立数据库。建议逻辑分域如下:
-
-- `auth_db`
-- `app_db`
-- `session_db`
-- `runtime_db`
-- `memory_db`
-- `tool_db`
-- `retrieval_db`
-- `trace_db`
-
-### 2.3 横向扩展原则
-
-- 所有运行态表必须支持按 `tenant_id`、`app_id`、`run_id` 定位
-- 长链路表优先使用追加写,减少热点更新
-- 高并发表尽量避免大事务和跨域 join
-- 跨服务关联尽量靠业务 ID,不依赖数据库外键
-- 表设计默认支持归档和分区
-
-## 3. ID 与通用字段规范
-
-### 3.1 主键策略
-
-建议统一使用 `UUID` 或 `ULID` 作为主键。
-
-推荐:
-
-- 配置类对象:`UUID`
-- 高写入运行表:`ULID` 或时序友好的 UUID v7
-
-好处:
-
-- 支持多服务独立生成 ID
-- 避免数据库序列成为全局瓶颈
-- 便于未来跨库迁移
-
-### 3.2 通用字段
-
-核心业务表建议统一带上以下字段:
-
-- `id`
-- `tenant_id`
-- `org_id`
-- `created_by`
-- `updated_by`
-- `created_time`
-- `updated_time`
-- `deleted_time`
-- `version`
-
-说明:
-
-- `tenant_id`:多租户隔离关键字段
-- `org_id`:组织维度隔离,可选
-- `deleted_time`:软删除标记
-- `version`:乐观锁或对象版本控制
-
-时间字段规范:
-
-- 所有时间字段统一使用 `_time` 后缀
-- 所有时间字段统一使用 `datetime` 类型
-- 禁止混用 `_at`、`date`、`timestamp` 等命名风格
-
-### 3.3 枚举字段建议
-
-不要使用数据库原生 enum 绑定太死,建议使用 `varchar + check` 或应用层常量控制,方便演进。
-
-## 4. 分域数据库设计
-
-## 4.1 Auth / Tenant 域
-
-职责:
-
-- 用户、组织、租户、角色、权限
-
-### 表:`tenant`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| code | varchar(64) | 租户编码,唯一 |
-| name | varchar(128) | 租户名称 |
-| status | varchar(32) | `active` / `suspended` |
-| plan_code | varchar(64) | 套餐 |
-| settings_json | jsonb | 租户级配置 |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-索引:
-
-- `uk_tenant_code(code)`
-- `idx_tenant_status(status)`
-
-### 表:`organization`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| parent_id | uuid | 上级组织 |
-| name | varchar(128) | 组织名 |
-| status | varchar(32) | 状态 |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-索引:
-
-- `idx_org_tenant(tenant_id)`
-- `idx_org_parent(parent_id)`
-
-### 表:`user_account`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| org_id | uuid | 组织 ID |
-| username | varchar(64) | 用户名 |
-| email | varchar(128) | 邮箱 |
-| phone | varchar(32) | 手机号 |
-| display_name | varchar(128) | 显示名 |
-| status | varchar(32) | 状态 |
-| profile_json | jsonb | 扩展资料 |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-索引:
-
-- `uk_user_tenant_username(tenant_id, username)`
-- `uk_user_tenant_email(tenant_id, email)`
-- `idx_user_org(org_id)`
-
-### 表:`role`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| code | varchar(64) | 角色编码 |
-| name | varchar(128) | 角色名 |
-| role_type | varchar(32) | 系统角色 / 自定义角色 |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-### 表:`permission`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| code | varchar(128) | 权限编码 |
-| name | varchar(128) | 权限名称 |
-| resource_type | varchar(64) | 资源类型 |
-| action | varchar(32) | 动作 |
-
-### 表:`user_role`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| user_id | uuid | 用户 ID |
-| role_id | uuid | 角色 ID |
-| created_time | datetime | 创建时间 |
-
-唯一索引:
-
-- `uk_user_role(tenant_id, user_id, role_id)`
-
-### 表:`role_permission`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| role_id | uuid | 角色 ID |
-| permission_id | uuid | 权限 ID |
-| created_time | datetime | 创建时间 |
-
-唯一索引:
-
-- `uk_role_permission(role_id, permission_id)`
-
-## 4.2 App / Workflow 配置域
-
-职责:
-
-- 应用定义
-- 工作流定义
-- 版本与环境配置
-- Agent、Team、Skill 绑定
-
-### 表:`app_definition`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| code | varchar(64) | 应用编码 |
-| name | varchar(128) | 应用名称 |
-| description | text | 描述 |
-| app_type | varchar(32) | chat / workflow / hybrid |
-| status | varchar(32) | draft / active / archived |
-| owner_user_id | uuid | 负责人 |
-| settings_json | jsonb | UI、默认模型等配置 |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-索引:
-
-- `uk_app_tenant_code(tenant_id, code)`
-- `idx_app_owner(owner_user_id)`
-- `idx_app_status(tenant_id, status)`
-
-### 表:`app_environment`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| app_id | uuid | 应用 ID |
-| env_name | varchar(32) | dev / test / prod |
-| model_policy_json | jsonb | 环境模型策略 |
-| secret_ref_json | jsonb | 环境密钥引用 |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-唯一索引:
-
-- `uk_app_env(app_id, env_name)`
-
-### 表:`app_version`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| app_id | uuid | 应用 ID |
-| version_no | integer | 版本号 |
-| status | varchar(32) | draft / published / rollback |
-| workflow_version_id | uuid | 当前工作流版本 |
-| published_time | datetime | 发布时间 |
-| published_by | uuid | 发布人 |
-| changelog | text | 发布说明 |
-| created_time | datetime | 创建时间 |
-
-唯一索引:
-
-- `uk_app_version(app_id, version_no)`
-
-### 表:`workflow_definition`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| app_id | uuid | 应用 ID |
-| code | varchar(64) | 流程编码 |
-| name | varchar(128) | 流程名称 |
-| workflow_type | varchar(32) | main / subflow / template |
-| latest_version_no | integer | 最新版本号 |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-唯一索引:
-
-- `uk_workflow_code(app_id, code)`
-
-### 表:`workflow_version`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| workflow_id | uuid | 流程定义 ID |
-| version_no | integer | 版本号 |
-| dsl_json | jsonb | 内部 DSL |
-| compiled_plan_json | jsonb | 编译后的执行计划 |
-| schema_version | varchar(32) | DSL 版本 |
-| checksum | varchar(128) | 内容摘要 |
-| status | varchar(32) | draft / validated / published |
-| created_by | uuid | 创建人 |
-| created_time | datetime | 创建时间 |
-
-索引:
-
-- `uk_workflow_version(workflow_id, version_no)`
-- `idx_workflow_version_status(workflow_id, status)`
-
-### 表:`workflow_import_record`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| app_id | uuid | 应用 ID |
-| source_type | varchar(32) | dify / custom_yaml / json |
-| source_uri | varchar(512) | 原始来源 |
-| raw_content_ref | varchar(512) | 原始文件对象存储地址 |
-| converted_workflow_version_id | uuid | 转换后的流程版本 |
-| import_status | varchar(32) | success / failed |
-| error_message | text | 错误原因 |
-| created_by | uuid | 导入人 |
-| created_time | datetime | 创建时间 |
-
-### 表:`agent_definition`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| app_id | uuid | 应用 ID |
-| code | varchar(64) | Agent 编码 |
-| name | varchar(128) | Agent 名称 |
-| role_type | varchar(32) | planner / worker / judge / custom |
-| description | text | 描述 |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-唯一索引:
-
-- `uk_agent_code(app_id, code)`
-
-### 表:`agent_version`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| agent_id | uuid | Agent 定义 ID |
-| version_no | integer | 版本号 |
-| system_prompt | text | 系统提示词 |
-| model_policy_json | jsonb | 模型策略 |
-| tool_policy_json | jsonb | 工具策略 |
-| output_schema_json | jsonb | 输出结构约束 |
-| created_by | uuid | 创建人 |
-| created_time | datetime | 创建时间 |
-
-唯一索引:
-
-- `uk_agent_version(agent_id, version_no)`
-
-### 表:`team_definition`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| app_id | uuid | 应用 ID |
-| code | varchar(64) | Team 编码 |
-| name | varchar(128) | Team 名称 |
-| team_mode | varchar(32) | leader_worker / debate / pipeline |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-### 表:`team_version`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| team_id | uuid | Team 定义 ID |
-| version_no | integer | 版本号 |
-| config_json | jsonb | 团队策略 |
-| created_by | uuid | 创建人 |
-| created_time | datetime | 创建时间 |
-
-### 表:`team_member_binding`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| team_version_id | uuid | Team 版本 ID |
-| agent_version_id | uuid | Agent 版本 ID |
-| member_role | varchar(32) | planner / reviewer / researcher |
-| sort_order | integer | 顺序 |
-| config_json | jsonb | 角色级配置 |
-
-索引:
-
-- `idx_team_member_team(team_version_id)`
-
-### 表:`skill_definition`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| code | varchar(64) | 技能编码 |
-| name | varchar(128) | 技能名称 |
-| skill_type | varchar(32) | prompt / workflow / decision / retrieval |
-| owner_scope | varchar(32) | system / tenant / app |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-### 表:`skill_version`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| skill_id | uuid | 技能定义 ID |
-| version_no | integer | 版本号 |
-| content_json | jsonb | 技能定义内容 |
-| created_by | uuid | 创建人 |
-| created_time | datetime | 创建时间 |
-
-### 表:`app_binding`
-
-统一保存应用对 Agent / Team / Skill / Tool / Plugin 的绑定关系。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| app_id | uuid | 应用 ID |
-| bind_type | varchar(32) | agent / team / skill / tool / plugin |
-| target_id | uuid | 绑定对象 ID |
-| target_version_id | uuid | 绑定版本 ID |
-| binding_scope | varchar(32) | app / env / workflow / node |
-| config_json | jsonb | 绑定配置 |
-| created_time | datetime | 创建时间 |
-
-索引:
-
-- `idx_app_binding_app(app_id, bind_type)`
-
-## 4.3 Session 域
-
-职责:
-
-- 用户会话
-- 对话消息
-- 请求入口
-- 应答记录
-
-### 表:`session`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| app_id | uuid | 应用 ID |
-| user_id | uuid | 用户 ID |
-| channel_type | varchar(32) | web / api / webhook / sdk |
-| session_status | varchar(32) | active / closed |
-| title | varchar(256) | 会话标题 |
-| started_time | datetime | 开始时间 |
-| last_active_time | datetime | 最后活跃时间 |
-| closed_time | datetime | 结束时间 |
-
-索引:
-
-- `idx_session_user(user_id, last_active_time desc)`
-- `idx_session_app(app_id, last_active_time desc)`
-
-### 表:`conversation_turn`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| session_id | uuid | 会话 ID |
-| turn_no | integer | 轮次 |
-| input_message_id | uuid | 用户消息 ID |
-| output_message_id | uuid | 最终回答消息 ID |
-| run_id | uuid | 对应运行 ID |
-| created_time | datetime | 创建时间 |
-
-唯一索引:
-
-- `uk_turn_session_no(session_id, turn_no)`
-
-### 表:`message`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| session_id | uuid | 会话 ID |
-| turn_id | uuid | 对话轮次 ID |
-| role | varchar(32) | user / assistant / system / tool |
-| content_type | varchar(32) | text / json / file / stream |
-| content_text | text | 文本内容 |
-| content_json | jsonb | 结构化消息 |
-| token_count | integer | token 数 |
-| created_time | datetime | 创建时间 |
-
-索引:
-
-- `idx_message_session(session_id, created_time)`
-- `idx_message_turn(turn_id)`
-
-### 表:`run_request`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| session_id | uuid | 会话 ID |
-| app_version_id | uuid | 应用版本 ID |
-| workflow_version_id | uuid | 流程版本 ID |
-| trigger_type | varchar(32) | chat / api / schedule / webhook |
-| request_payload_json | jsonb | 原始请求 |
-| request_status | varchar(32) | accepted / rejected / started |
-| created_time | datetime | 创建时间 |
-
-索引:
-
-- `idx_run_request_session(session_id, created_time desc)`
-
-## 4.4 Runtime 域
-
-职责:
-
-- 流程运行
-- 节点执行
-- 状态迁移
-- 快照与恢复
-- 异步事件
-
-这是平台最高频的核心域之一。
-
-### 表:`workflow_run`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| app_id | uuid | 应用 ID |
-| app_version_id | uuid | 应用版本 ID |
-| workflow_id | uuid | 流程定义 ID |
-| workflow_version_id | uuid | 流程版本 ID |
-| session_id | uuid | 会话 ID,可空 |
-| parent_run_id | uuid | 父运行 ID,子流程时使用 |
-| root_run_id | uuid | 根运行 ID |
-| run_type | varchar(32) | main / subflow / scheduled / batch |
-| status | varchar(32) | pending / running / completed / failed / cancelled / paused |
-| trigger_type | varchar(32) | user / api / event / timer |
-| priority | integer | 优先级 |
-| current_node_count | integer | 已执行节点数 |
-| started_time | datetime | 开始时间 |
-| finished_time | datetime | 结束时间 |
-| error_code | varchar(64) | 错误码 |
-| error_message | text | 错误信息 |
-| created_time | datetime | 创建时间 |
-
-索引:
-
-- `idx_run_root(root_run_id)`
-- `idx_run_session(session_id, created_time desc)`
-- `idx_run_status(status, created_time desc)`
-- `idx_run_app(app_id, created_time desc)`
-
-### 表:`node_run`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| run_id | uuid | 运行 ID |
-| parent_node_run_id | uuid | 父节点运行 ID |
-| node_id | varchar(128) | DSL 节点 ID |
-| node_type | varchar(32) | llm / tool / code / team 等 |
-| attempt_no | integer | 第几次尝试 |
-| status | varchar(32) | pending / queued / running / completed / failed / skipped |
-| worker_key | varchar(128) | 执行 worker 标识 |
-| lease_expire_time | datetime | 执行 lease 到期时间 |
-| queued_time | datetime | 入队时间 |
-| started_time | datetime | 开始时间 |
-| finished_time | datetime | 结束时间 |
-| error_code | varchar(64) | 错误码 |
-| error_message | text | 错误信息 |
-| created_time | datetime | 创建时间 |
-
-唯一索引建议:
-
-- `uk_node_run_attempt(run_id, node_id, attempt_no)`
-
-普通索引:
-
-- `idx_node_run_status(status, queued_time)`
-- `idx_node_run_run(run_id, started_time)`
-- `idx_node_run_worker(worker_key, status)`
-
-### 表:`node_io_snapshot`
-
-用于存储节点输入输出快照,便于回放和问题定位。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| node_run_id | uuid | 节点运行 ID |
-| snapshot_type | varchar(32) | input / output / intermediate |
-| payload_json | jsonb | 快照内容 |
-| payload_ref | varchar(512) | 大对象外置引用 |
-| created_time | datetime | 创建时间 |
-
-索引:
-
-- `idx_node_snapshot_node(node_run_id, snapshot_type)`
-
-### 表:`run_state_snapshot`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| run_id | uuid | 运行 ID |
-| snapshot_no | integer | 快照号 |
-| state_json | jsonb | 当前运行态 |
-| context_hash | varchar(128) | 状态摘要 |
-| created_time | datetime | 创建时间 |
-
-唯一索引:
-
-- `uk_run_snapshot(run_id, snapshot_no)`
-
-### 表:`run_event`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | bigint | 主键 |
-| tenant_id | uuid | 租户 ID |
-| run_id | uuid | 运行 ID |
-| node_run_id | uuid | 节点运行 ID,可空 |
-| event_type | varchar(64) | 事件类型 |
-| event_time | datetime | 事件时间 |
-| payload_json | jsonb | 事件内容 |
-
-索引:
-
-- `idx_run_event_run(run_id, id)`
-- `idx_run_event_type(event_type, event_time desc)`
-
-建议:
-
-- `run_event` 可按月分区
-
-### 表:`run_queue_task`
-
-适合在没有独立 MQ 可视化追踪时保留调度状态。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| run_id | uuid | 运行 ID |
-| node_run_id | uuid | 节点运行 ID |
-| task_type | varchar(32) | schedule / retry / timeout / resume |
-| execute_after_time | datetime | 可执行时间 |
-| task_status | varchar(32) | pending / taken / done / dead_letter |
-| retry_count | integer | 重试次数 |
-| payload_json | jsonb | 任务内容 |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-索引:
-
-- `idx_queue_ready(task_status, execute_after_time)`
-
-## 4.5 Memory / Context 域
-
-职责:
-
-- 会话上下文
-- 任务上下文
-- 长期记忆
-- 领域隔离记忆
-- 召回记录
-
-### 表:`context_namespace`
-
-表示不同作用域下的上下文命名空间。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| app_id | uuid | 应用 ID |
-| namespace_type | varchar(32) | session / run / user / domain / team |
-| namespace_key | varchar(256) | 如 `session:{id}` 或 `domain:policy` |
-| owner_ref | varchar(256) | 归属对象 |
-| created_time | datetime | 创建时间 |
-
-唯一索引:
-
-- `uk_context_namespace(tenant_id, namespace_type, namespace_key)`
-
-### 表:`context_kv`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| namespace_id | uuid | 命名空间 ID |
-| key | varchar(128) | 键 |
-| value_type | varchar(32) | string / number / bool / json |
-| value_json | jsonb | 值 |
-| revision | integer | 修订号 |
-| updated_time | datetime | 更新时间 |
-| updated_by_run_id | uuid | 最后修改运行 |
-
-唯一索引:
-
-- `uk_context_kv(namespace_id, key)`
-
-### 表:`context_change_log`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | bigint | 主键 |
-| tenant_id | uuid | 租户 ID |
-| namespace_id | uuid | 命名空间 ID |
-| key | varchar(128) | 键 |
-| before_json | jsonb | 修改前 |
-| after_json | jsonb | 修改后 |
-| change_source | varchar(32) | node / tool / system |
-| run_id | uuid | 来源运行 |
-| node_run_id | uuid | 来源节点 |
-| created_time | datetime | 创建时间 |
-
-索引:
-
-- `idx_context_change_namespace(namespace_id, id desc)`
-
-### 表:`memory_record`
-
-统一长期记忆记录。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| app_id | uuid | 应用 ID |
-| user_id | uuid | 用户 ID,可空 |
-| session_id | uuid | 会话 ID,可空 |
-| run_id | uuid | 运行 ID,可空 |
-| namespace | varchar(128) | `user` / `app` / `domain:order` |
-| memory_type | varchar(32) | fact / preference / summary / episode |
-| importance_score | numeric(5,2) | 重要性 |
-| freshness_score | numeric(5,2) | 新鲜度 |
-| content_text | text | 内容 |
-| content_json | jsonb | 结构化内容 |
-| source_type | varchar(32) | conversation / tool / import / rule |
-| status | varchar(32) | active / archived / deleted |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-索引:
-
-- `idx_memory_namespace(namespace, created_time desc)`
-- `idx_memory_user(user_id, created_time desc)`
-- `idx_memory_app(app_id, created_time desc)`
-- `idx_memory_type(memory_type, status)`
-
-### 表:`memory_embedding`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| memory_id | uuid | 记忆记录 ID |
-| embedding_model | varchar(64) | Embedding 模型 |
-| embedding | vector | 向量 |
-| token_count | integer | token 数 |
-| created_time | datetime | 创建时间 |
-
-索引:
-
-- 向量索引:`ivfflat` 或 `hnsw(embedding)`
-- `idx_memory_embedding_memory(memory_id)`
-
-### 表:`memory_recall_log`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | bigint | 主键 |
-| tenant_id | uuid | 租户 ID |
-| run_id | uuid | 运行 ID |
-| node_run_id | uuid | 节点运行 ID |
-| query_text | text | 召回查询 |
-| namespace | varchar(128) | 召回命名空间 |
-| top_k | integer | 返回条数 |
-| result_ids_json | jsonb | 命中记录 ID 列表 |
-| created_time | datetime | 创建时间 |
-
-索引:
-
-- `idx_memory_recall_run(run_id, created_time desc)`
-
-## 4.6 Tool / Plugin 域
-
-职责:
-
-- 工具、插件、凭据、调用记录
-
-### 表:`plugin_definition`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID,可空表示系统插件 |
-| code | varchar(64) | 插件编码 |
-| name | varchar(128) | 插件名称 |
-| provider | varchar(128) | 提供方 |
-| source_type | varchar(32) | marketplace / local / git |
-| manifest_json | jsonb | 插件清单 |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-### 表:`plugin_version`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| plugin_id | uuid | 插件 ID |
-| version | varchar(64) | 版本号 |
-| package_ref | varchar(512) | 包引用 |
-| checksum | varchar(128) | 校验值 |
-| status | varchar(32) | active / disabled |
-| created_time | datetime | 创建时间 |
-
-唯一索引:
-
-- `uk_plugin_version(plugin_id, version)`
-
-### 表:`tool_definition`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID,可空表示系统工具 |
-| plugin_id | uuid | 所属插件 |
-| code | varchar(64) | 工具编码 |
-| name | varchar(128) | 工具名称 |
-| tool_type | varchar(32) | function / http / mcp / db |
-| description | text | 描述 |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-唯一索引:
-
-- `uk_tool_code(tenant_id, code)`
-
-### 表:`tool_version`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tool_id | uuid | 工具定义 ID |
-| version_no | integer | 版本号 |
-| input_schema_json | jsonb | 输入 schema |
-| output_schema_json | jsonb | 输出 schema |
-| invoke_config_json | jsonb | 调用配置 |
-| timeout_ms | integer | 超时 |
-| retry_policy_json | jsonb | 重试策略 |
-| created_time | datetime | 创建时间 |
-
-唯一索引:
-
-- `uk_tool_version(tool_id, version_no)`
-
-### 表:`tool_credential`
-
-不要直接存明文密钥,建议只存密文或外部 Secret 引用。
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| credential_name | varchar(128) | 名称 |
-| secret_type | varchar(32) | api_key / oauth / service_account |
-| secret_ref | varchar(512) | Secret Manager 引用 |
-| encrypted_payload | bytea | 可选密文 |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-### 表:`tool_binding`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| app_id | uuid | 应用 ID |
-| tool_version_id | uuid | 工具版本 ID |
-| credential_id | uuid | 凭据 ID |
-| binding_scope | varchar(32) | app / env / workflow / node |
-| enabled | boolean | 是否启用 |
-| config_json | jsonb | 绑定配置 |
-| created_time | datetime | 创建时间 |
-
-索引:
-
-- `idx_tool_binding_app(app_id, binding_scope)`
-
-### 表:`tool_invocation_log`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| run_id | uuid | 运行 ID |
-| node_run_id | uuid | 节点运行 ID |
-| tool_id | uuid | 工具 ID |
-| tool_version_id | uuid | 工具版本 ID |
-| request_json | jsonb | 请求参数 |
-| response_json | jsonb | 响应结果 |
-| response_ref | varchar(512) | 大对象引用 |
-| status | varchar(32) | success / failed / timeout |
-| latency_ms | integer | 调用耗时 |
-| error_message | text | 错误信息 |
-| invoked_time | datetime | 调用时间 |
-
-索引:
-
-- `idx_tool_invocation_run(run_id, invoked_time desc)`
-- `idx_tool_invocation_tool(tool_id, invoked_time desc)`
-- `idx_tool_invocation_status(status, invoked_time desc)`
-
-## 4.7 Retrieval / Knowledge 域
-
-职责:
-
-- 知识库
-- 文档与分片
-- 向量索引
-- 检索审计
-
-### 表:`knowledge_base`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| app_id | uuid | 应用 ID |
-| code | varchar(64) | 知识库编码 |
-| name | varchar(128) | 名称 |
-| retrieval_strategy | varchar(32) | vector / hybrid / keyword |
-| status | varchar(32) | active / disabled |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-### 表:`document`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| kb_id | uuid | 知识库 ID |
-| source_type | varchar(32) | file / url / api / manual |
-| source_uri | varchar(512) | 来源 |
-| title | varchar(256) | 标题 |
-| mime_type | varchar(128) | 文件类型 |
-| object_ref | varchar(512) | 文件存储引用 |
-| parse_status | varchar(32) | pending / parsed / failed |
-| metadata_json | jsonb | 元数据 |
-| created_time | datetime | 创建时间 |
-| updated_time | datetime | 更新时间 |
-
-索引:
-
-- `idx_document_kb(kb_id, created_time desc)`
-
-### 表:`document_chunk`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| document_id | uuid | 文档 ID |
-| chunk_no | integer | 分片序号 |
-| content_text | text | 分片内容 |
-| metadata_json | jsonb | 分片元数据 |
-| created_time | datetime | 创建时间 |
-
-唯一索引:
-
-- `uk_document_chunk(document_id, chunk_no)`
-
-### 表:`chunk_embedding`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| chunk_id | uuid | 分片 ID |
-| embedding_model | varchar(64) | Embedding 模型 |
-| embedding | vector | 向量 |
-| created_time | datetime | 创建时间 |
-
-索引:
-
-- 向量索引:`ivfflat` 或 `hnsw(embedding)`
-- `idx_chunk_embedding_chunk(chunk_id)`
-
-### 表:`retrieval_log`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | bigint | 主键 |
-| tenant_id | uuid | 租户 ID |
-| run_id | uuid | 运行 ID |
-| node_run_id | uuid | 节点运行 ID |
-| kb_id | uuid | 知识库 ID |
-| query_text | text | 查询语句 |
-| strategy | varchar(32) | vector / hybrid |
-| top_k | integer | 命中数 |
-| result_json | jsonb | 命中详情 |
-| latency_ms | integer | 耗时 |
-| created_time | datetime | 创建时间 |
-
-索引:
-
-- `idx_retrieval_run(run_id, created_time desc)`
-- `idx_retrieval_kb(kb_id, created_time desc)`
-
-## 4.8 Model / LLM 域
-
-如果 `model-gateway` 需要单独统计模型调用和成本,建议独立表。
-
-### 表:`model_provider`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| code | varchar(64) | openai / azure_openai / anthropic |
-| name | varchar(128) | 名称 |
-| provider_type | varchar(32) | hosted / self_hosted |
-| config_json | jsonb | 默认配置 |
-| created_time | datetime | 创建时间 |
-
-### 表:`model_definition`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| provider_id | uuid | Provider ID |
-| model_code | varchar(128) | 模型编码 |
-| model_kind | varchar(32) | chat / embedding / rerank |
-| capability_json | jsonb | 能力描述 |
-| pricing_json | jsonb | 价格配置 |
-| status | varchar(32) | active / deprecated |
-| created_time | datetime | 创建时间 |
-
-唯一索引:
-
-- `uk_model_provider_code(provider_id, model_code)`
-
-### 表:`model_invocation_log`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| run_id | uuid | 运行 ID |
-| node_run_id | uuid | 节点运行 ID |
-| provider_id | uuid | Provider ID |
-| model_id | uuid | 模型 ID |
-| request_json | jsonb | 请求摘要 |
-| response_json | jsonb | 响应摘要 |
-| prompt_tokens | integer | 输入 token |
-| completion_tokens | integer | 输出 token |
-| total_tokens | integer | 总 token |
-| estimated_cost | numeric(18,6) | 预估成本 |
-| latency_ms | integer | 耗时 |
-| status | varchar(32) | success / failed / timeout |
-| invoked_time | datetime | 调用时间 |
-
-索引:
-
-- `idx_model_invocation_run(run_id, invoked_time desc)`
-- `idx_model_invocation_model(model_id, invoked_time desc)`
-
-## 4.9 Trace / Eval 域
-
-职责:
-
-- 链路追踪
-- 回放
-- 评测与基线
-
-### 表:`trace_span`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| trace_id | uuid | Trace ID |
-| parent_span_id | uuid | 父 Span |
-| span_type | varchar(32) | run / node / tool / model / memory |
-| ref_id | varchar(128) | 关联业务 ID |
-| name | varchar(128) | Span 名称 |
-| status | varchar(32) | ok / error |
-| started_time | datetime | 开始时间 |
-| finished_time | datetime | 结束时间 |
-| attributes_json | jsonb | 属性 |
-
-索引:
-
-- `idx_trace_trace(trace_id, started_time)`
-- `idx_trace_ref(span_type, ref_id)`
-
-### 表:`trace_event`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | bigint | 主键 |
-| tenant_id | uuid | 租户 ID |
-| trace_id | uuid | Trace ID |
-| span_id | uuid | Span ID |
-| event_name | varchar(128) | 事件名 |
-| payload_json | jsonb | 事件内容 |
-| event_time | datetime | 事件时间 |
-
-索引:
-
-- `idx_trace_event_trace(trace_id, id)`
-
-### 表:`eval_suite`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| app_id | uuid | 应用 ID |
-| name | varchar(128) | 评测集名称 |
-| description | text | 描述 |
-| created_by | uuid | 创建人 |
-| created_time | datetime | 创建时间 |
-
-### 表:`eval_case`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| suite_id | uuid | 评测集 ID |
-| input_json | jsonb | 输入 |
-| expected_json | jsonb | 预期输出 |
-| tags_json | jsonb | 标签 |
-| created_time | datetime | 创建时间 |
-
-### 表:`eval_run`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| tenant_id | uuid | 租户 ID |
-| suite_id | uuid | 评测集 ID |
-| app_version_id | uuid | 评测应用版本 |
-| status | varchar(32) | pending / running / completed |
-| started_time | datetime | 开始时间 |
-| finished_time | datetime | 结束时间 |
-| created_time | datetime | 创建时间 |
-
-### 表:`eval_result`
-
-| 字段 | 类型 | 说明 |
-| --- | --- | --- |
-| id | uuid | 主键 |
-| eval_run_id | uuid | 评测运行 ID |
-| eval_case_id | uuid | 评测用例 ID |
-| run_id | uuid | 实际运行 ID |
-| score | numeric(5,2) | 得分 |
-| verdict | varchar(32) | pass / fail |
-| detail_json | jsonb | 评测详情 |
-| created_time | datetime | 创建时间 |
-
-索引:
-
-- `idx_eval_result_run(eval_run_id)`
-
-## 5. 关键关系说明
-
-核心关系可以理解为:
-
-- 一个 `tenant` 下有多个 `app_definition`
-- 一个 `app_definition` 下有多个 `app_version`
-- 一个 `app_definition` 下有多个 `workflow_definition`
-- 一个 `workflow_definition` 下有多个 `workflow_version`
-- 一个 `app` 可以绑定多个 `agent`、`team`、`skill`、`tool`
-- 一个 `session` 会产生多轮 `conversation_turn`
-- 一次输入通常对应一个 `run_request` 和一个 `workflow_run`
-- 一个 `workflow_run` 下会有多个 `node_run`
-- 一个 `node_run` 会产生快照、工具调用、模型调用、trace 和 memory recall
-- `memory_record` 与 `context_namespace` 共同组成“长期记忆 + 运行状态”的双层体系
-
-## 6. 索引与分区建议
-
-### 6.1 必须优先建索引的表
-
-- `workflow_run`
-- `node_run`
-- `run_event`
-- `message`
-- `memory_record`
-- `tool_invocation_log`
-- `model_invocation_log`
-- `retrieval_log`
-- `trace_event`
-
-### 6.2 建议分区的表
-
-当数据量上来后,以下表建议按时间分区:
-
-- `run_event`
-- `tool_invocation_log`
-- `model_invocation_log`
-- `retrieval_log`
-- `trace_event`
-- `context_change_log`
-
-优先方案:
-
-- PostgreSQL 按月 range partition
-
-### 6.3 JSONB 使用建议
-
-适合放 `jsonb` 的字段:
-
-- DSL 内容
-- 节点快照
-- 模型能力描述
-- 工具输入输出 schema
-- 配置策略
-
-不要把核心检索条件全部塞进 `jsonb`,例如状态、版本、归属、时间等必须单独成列。
-
-## 7. Redis 设计建议
-
-Redis 不替代主数据库,主要承载以下内容:
-
-- `session:{id}:recent_messages`
-- `run:{id}:state_cache`
-- `run:{id}:node_lock:{node_id}`
-- `idempotency:{key}`
-- `queue:ready`
-- `model_rate_limit:{provider}:{model}`
-
-建议:
-
-- 所有 Redis Key 带租户前缀
-- 运行态缓存必须可从数据库重建
-- 分布式锁必须带 TTL
-
-## 8. 对象存储设计建议
-
-对象存储主要保存:
-
-- 原始导入 YAML/JSON
-- 大体积节点快照
-- 工具大响应结果
-- 文件上传
-- 回放原始材料
-- 检索文档原文
-
-建议存储路径规范:
-
-- `tenant/{tenant_id}/app/{app_id}/imports/...`
-- `tenant/{tenant_id}/run/{run_id}/snapshots/...`
-- `tenant/{tenant_id}/trace/{trace_id}/...`
-- `tenant/{tenant_id}/kb/{kb_id}/documents/...`
-
-## 9. 分阶段建表建议
-
-### V0.1 必需表
-
-- `tenant`
-- `user_account`
-- `app_definition`
-- `app_version`
-- `workflow_definition`
-- `workflow_version`
-- `session`
-- `message`
-- `run_request`
-- `workflow_run`
-- `node_run`
-- `node_io_snapshot`
-- `tool_definition`
-- `tool_version`
-- `tool_binding`
-- `tool_invocation_log`
-- `model_provider`
-- `model_definition`
-- `model_invocation_log`
-
-目标:
-
-- 跑通“应用配置 + 工作流执行 + 工具调用 + 基础日志”
-
-### V0.2 新增表
-
-- `agent_definition`
-- `agent_version`
-- `skill_definition`
-- `skill_version`
-- `app_binding`
-- `context_namespace`
-- `context_kv`
-- `memory_record`
-- `memory_embedding`
-- `memory_recall_log`
-
-目标:
-
-- 跑通“Agent + 技能 + 记忆”
-
-### V0.3 新增表
-
-- `team_definition`
-- `team_version`
-- `team_member_binding`
-- `run_state_snapshot`
-- `run_queue_task`
-- `context_change_log`
-- `knowledge_base`
-- `document`
-- `document_chunk`
-- `chunk_embedding`
-- `retrieval_log`
-
-目标:
-
-- 跑通“多智能体 + 子流程 + 检索 + 恢复”
-
-### V0.4 新增表
-
-- `organization`
-- `role`
-- `permission`
-- `user_role`
-- `role_permission`
-- `plugin_definition`
-- `plugin_version`
-- `trace_span`
-- `trace_event`
-- `eval_suite`
-- `eval_case`
-- `eval_run`
-- `eval_result`
-
-目标:
-
-- 跑通“治理、可观测、评测、插件化”
-
-## 10. 跨服务数据边界建议
-
-为避免后续服务拆分时数据库耦合过深,建议遵循以下约束:
-
-- `auth-service` 只直接管理 `auth_db`
-- `workflow-service` 只直接写 `app_db`
-- `runtime-service` 只直接写 `runtime_db`
-- `memory-service` 只直接写 `memory_db`
-- `tool-service` 只直接写 `tool_db`
-- `retrieval-service` 只直接写 `retrieval_db`
-- `trace-service` 只直接写 `trace_db`
-
-其他服务如果需要这些数据:
-
-- 通过 API 读
-- 或通过事件异步复制只读视图
-
-不要在应用层做跨库强事务。
-
-## 11. 迁移与演进建议
-
-### 11.1 迁移工具
-
-- `Alembic` 统一管理迁移
-- 每个服务单独 migration 目录
-- 共用库只提供模型定义,不直接统一迁移所有库
-
-### 11.2 演进策略
-
-- 先 schema 兼容,再代码切换
-- 大字段新增优先 nullable
-- 热表结构变更优先分阶段 rollout
-- 分区表和归档表尽量提前规划
-
-### 11.3 审计策略
-
-以下操作建议强制审计:
-
-- 应用发布
-- 工作流导入
-- 工具绑定变更
-- 记忆写入策略变更
-- 权限变更
-- 人工审批结果
-
-## 12. 推荐下一步
-
-如果进入工程实施,最顺的下一步是:
-
-1. 先把 `V0.1` 的 SQLAlchemy 模型定义出来
-2. 建 Monorepo 下的多服务目录和共享 `libs`
-3. 先跑通 `app_db + session_db + runtime_db + tool_db`
-4. 后续再补 `memory_db` 和 `trace_db`
-
-## 13. 结论
-
-这套数据库设计的核心思想是:
-
-- 用 PostgreSQL 承载配置、运行、治理主数据
-- 用 Redis 承载高频短期状态和幂等控制
-- 用 pgvector 承载语义记忆与知识检索
-- 用对象存储承载大对象和原始产物
-- 用分域设计保证未来多服务拆分和水平扩展不需要推翻重来
-
-如果你下一步继续让我执行,我建议直接进入:
-
-1. `V0.1 SQLAlchemy 模型骨架`
-2. `Alembic 初始建表脚本`
+- Auth: users, roles, role assignments, tokens, permission checks.
+- Workflow: apps, app versions, workflow definitions, workflow versions.
+- Runtime: workflow runs, node runs, logs, artifacts, traces.
+- Agent, team, tool, skill, memory, knowledge, human task, event, scheduler, and gateway domains use direct entity identifiers and service-local indexes.

+ 1 - 1
docs/agent-platform-multi-service-roadmap.md

@@ -61,7 +61,7 @@
 - Model Gateway
 
 4. 支撑层
-- Auth / Tenant Service
+- Auth Service
 - Trace / Eval Service
 - Notification Service
 - File / Artifact Service

+ 165 - 0
docs/auth-service-design.md

@@ -0,0 +1,165 @@
+# auth-service design
+
+## Scope
+
+`auth-service` owns Web Studio account/password login, users, roles, role assignments, and permission checks.
+
+The auth domain is single-workspace. Auth API payloads and auth tables do not carry workspace partition fields.
+
+## Frontend Contract
+
+Frontend requests go through `/gateway`:
+
+- `POST /gateway/auth/login`
+- `GET /gateway/auth/users`
+- `GET /gateway/auth/roles`
+- `POST /gateway/auth/permissions/check`
+
+Gateway proxies them to auth-service:
+
+- `POST /auth/login`
+- `GET /auth/users`
+- `GET /auth/roles`
+- `POST /auth/permissions/check`
+
+After login, frontend sends:
+
+- `Authorization: Bearer ...`
+- `x-user-id`
+
+## Database
+
+PostgreSQL:
+
+```text
+postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+```
+
+Runtime setting:
+
+```powershell
+$env:AGENT_PLATFORM_DATABASE_URL="postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb"
+```
+
+## Tables
+
+### auth_user
+
+- `id`
+- `username`
+- `password_hash`
+- `display_name`
+- `email`
+- `status`: `active | disabled | deleted`
+- `metadata_json`
+- `last_login_time`
+- audit fields
+- `version`
+
+### auth_role
+
+- `id`
+- `code`
+- `name`
+- `description`
+- `status`: `active | disabled`
+- `permissions_json`
+- audit fields
+- `version`
+
+### auth_role_assignment
+
+- `id`
+- `user_id`
+- `role_id`
+- `status`: `active | revoked`
+- `scope_type`
+- `scope_id`
+- `expires_time`
+- audit fields
+- `version`
+
+## Login
+
+`POST /auth/login`
+
+```json
+{
+  "username": "demo-user",
+  "password": "demo-password"
+}
+```
+
+Response:
+
+```json
+{
+  "access_token": "apt_xxx",
+  "token_type": "bearer",
+  "expires_time": "2026-04-28T07:10:00Z",
+  "user": {
+    "id": "user-id",
+    "username": "demo-user",
+    "display_name": "Demo User",
+    "email": "demo@example.com",
+    "status": "active",
+    "metadata_json": {},
+    "last_login_time": "2026-04-27T23:10:00Z",
+    "created_time": "2026-04-27T23:00:00Z"
+  }
+}
+```
+
+Passwords are stored with salted `PBKDF2-HMAC-SHA256`. Access tokens are HMAC signed with `AGENT_PLATFORM_CREDENTIAL_ENCRYPTION_KEY`.
+
+## Token Verification
+
+`POST /auth/tokens/verify`
+
+```json
+{
+  "access_token": "apt_xxx"
+}
+```
+
+Response:
+
+```json
+{
+  "active": true,
+  "user_id": "user-id",
+  "username": "demo-user",
+  "expires_time": "2026-04-28T07:10:00"
+}
+```
+
+## Permission Check
+
+`POST /auth/permissions/check`
+
+```json
+{
+  "user_id": "user-id",
+  "permission": "workflow:read",
+  "scope_type": null,
+  "scope_id": null
+}
+```
+
+Response:
+
+```json
+{
+  "allowed": true,
+  "reason": "matched",
+  "matched_role_ids": ["role-id"]
+}
+```
+
+## Migration
+
+```powershell
+cd services/auth-service
+$env:AGENT_PLATFORM_DATABASE_URL="postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb"
+alembic upgrade head
+```

+ 2 - 2
libs/core-db/src/core_db/__init__.py

@@ -1,5 +1,5 @@
 from .base import Base
-from .mixins import AuditMixin, TenantMixin, VersionMixin
+from .mixins import AuditMixin, EntityMixin, VersionMixin
 from .session import (
     DatabaseSettings,
     create_engine_from_settings,
@@ -11,7 +11,7 @@ __all__ = [
     "AuditMixin",
     "Base",
     "DatabaseSettings",
-    "TenantMixin",
+    "EntityMixin",
     "VersionMixin",
     "create_engine_from_settings",
     "create_session_factory",

+ 2 - 4
libs/core-db/src/core_db/mixins.py

@@ -5,9 +5,8 @@ from sqlalchemy import DateTime, Integer, String
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class TenantMixin:
+class EntityMixin:
     id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
-    tenant_id: Mapped[str] = mapped_column(String(36), index=True)
 
 
 class AuditMixin:
@@ -17,8 +16,7 @@ class AuditMixin:
     updated_time: Mapped[datetime] = mapped_column(
         DateTime,
         default=datetime.utcnow,
-        onupdate=datetime.utcnow,
-    )
+        onupdate=datetime.utcnow)
     deleted_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
 
 

+ 3 - 6
libs/core-db/src/core_db/session.py

@@ -21,8 +21,7 @@ def create_engine_from_settings(settings: DatabaseSettings) -> Engine:
         settings.database_url,
         echo=settings.echo_sql,
         pool_pre_ping=settings.pool_pre_ping,
-        connect_args=connect_args,
-    )
+        connect_args=connect_args)
 
 
 def create_session_factory(engine: Engine) -> sessionmaker[Session]:
@@ -31,8 +30,7 @@ def create_session_factory(engine: Engine) -> sessionmaker[Session]:
         autoflush=False,
         autocommit=False,
         expire_on_commit=False,
-        class_=Session,
-    )
+        class_=Session)
 
 
 def session_scope(session_factory: sessionmaker[Session]) -> Generator[Session, None, None]:
@@ -45,8 +43,7 @@ def session_scope(session_factory: sessionmaker[Session]) -> Generator[Session,
 
 @contextmanager
 def transaction_scope(
-    session_factory: sessionmaker[Session],
-) -> Generator[Session, None, None]:
+    session_factory: sessionmaker[Session]) -> Generator[Session, None, None]:
     session = session_factory()
     try:
         yield session

+ 8 - 15
libs/core-domain/src/core_domain/__init__.py

@@ -10,10 +10,7 @@ from .agent_contracts import (
     AgentVersionContract,
     AgentVersionStatus,
 )
-from .agent_tool_invocation_contracts import (
-    AgentToolInvocationContract,
-    AgentToolInvocationStatus,
-)
+from .agent_tool_invocation_contracts import AgentToolInvocationContract, AgentToolInvocationStatus
 from .auth_contracts import (
     PermissionCheckContract,
     PermissionCheckResultContract,
@@ -46,11 +43,6 @@ from .knowledge_contracts import (
     KnowledgeSearchRequestContract,
     KnowledgeSearchResultContract,
 )
-from .model_contracts import (
-    ChatCompletionRequestContract,
-    ChatCompletionResponseContract,
-    ChatMessageContract,
-)
 from .memory_contracts import (
     MemoryCreateContract,
     MemoryItemContract,
@@ -59,6 +51,11 @@ from .memory_contracts import (
     MemorySearchResultContract,
     MemoryStatus,
 )
+from .model_contracts import (
+    ChatCompletionRequestContract,
+    ChatCompletionResponseContract,
+    ChatMessageContract,
+)
 from .runtime_contracts import (
     InitialNodeContract,
     NodeRunContract,
@@ -66,15 +63,11 @@ from .runtime_contracts import (
     NodeRunStatusUpdateContract,
     RunBootstrapContract,
     RunCreateContract,
+    WorkflowRunContract,
     WorkflowRunStatus,
     WorkflowRunStatusUpdateContract,
-    WorkflowRunContract,
-)
-from .scheduler_contracts import (
-    ScheduledJobContract,
-    ScheduledJobStatus,
-    ScheduledJobType,
 )
+from .scheduler_contracts import ScheduledJobContract, ScheduledJobStatus, ScheduledJobType
 from .service import ServiceDescriptor, ServiceHealth
 from .skill_contracts import (
     SkillDefinitionContract,

+ 1 - 6
libs/core-domain/src/core_domain/agent_contracts.py

@@ -1,10 +1,8 @@
 from datetime import datetime
 from typing import Literal
 
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
-
+from pydantic import BaseModel, Field
 
 AgentStatus = Literal["draft", "active", "archived"]
 AgentVersionStatus = Literal["draft", "published", "deprecated"]
@@ -48,7 +46,6 @@ class AgentMemoryPolicyContract(BaseModel):
 
 class AgentDefinitionContract(BaseModel):
     id: str
-    tenant_id: str
     code: str
     name: str
     description: str | None = None
@@ -60,7 +57,6 @@ class AgentDefinitionContract(BaseModel):
 
 class AgentVersionContract(BaseModel):
     id: str
-    tenant_id: str
     agent_id: str
     version_no: int
     status: AgentVersionStatus
@@ -77,7 +73,6 @@ class AgentVersionContract(BaseModel):
 
 class AgentRunContract(BaseModel):
     id: str
-    tenant_id: str
     agent_id: str
     agent_version_id: str
     session_id: str | None = None

+ 1 - 4
libs/core-domain/src/core_domain/agent_tool_invocation_contracts.py

@@ -1,17 +1,14 @@
 from datetime import datetime
 from typing import Literal
 
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
-
+from pydantic import BaseModel, Field
 
 AgentToolInvocationStatus = Literal["selected", "skipped", "running", "completed", "failed"]
 
 
 class AgentToolInvocationContract(BaseModel):
     id: str
-    tenant_id: str
     agent_run_id: str
     agent_id: str
     agent_version_id: str

+ 1 - 7
libs/core-domain/src/core_domain/auth_contracts.py

@@ -1,10 +1,8 @@
 from datetime import datetime
 from typing import Literal
 
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
-
+from pydantic import BaseModel, Field
 
 UserStatus = Literal["active", "disabled", "deleted"]
 RoleStatus = Literal["active", "disabled"]
@@ -13,7 +11,6 @@ RoleAssignmentStatus = Literal["active", "revoked"]
 
 class UserContract(BaseModel):
     id: str
-    tenant_id: str
     username: str
     display_name: str | None = None
     email: str | None = None
@@ -25,7 +22,6 @@ class UserContract(BaseModel):
 
 class RoleContract(BaseModel):
     id: str
-    tenant_id: str
     code: str
     name: str
     description: str | None = None
@@ -36,7 +32,6 @@ class RoleContract(BaseModel):
 
 class RoleAssignmentContract(BaseModel):
     id: str
-    tenant_id: str
     user_id: str
     role_id: str
     status: RoleAssignmentStatus
@@ -47,7 +42,6 @@ class RoleAssignmentContract(BaseModel):
 
 
 class PermissionCheckContract(BaseModel):
-    tenant_id: str
     user_id: str
     permission: str
     scope_type: str | None = None

+ 1 - 2
libs/core-domain/src/core_domain/code_contracts.py

@@ -1,6 +1,5 @@
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
+from pydantic import BaseModel, Field
 
 
 class CodeExecutionRequestContract(BaseModel):

+ 1 - 3
libs/core-domain/src/core_domain/execution_contracts.py

@@ -1,6 +1,5 @@
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
+from pydantic import BaseModel, Field
 
 from .runtime_contracts import NodeRunStatus
 
@@ -15,7 +14,6 @@ class RunExecutionRequestContract(BaseModel):
 
 
 class NodeExecutionContextContract(BaseModel):
-    tenant_id: str
     run_id: str
     node_run_id: str
     node_id: str

+ 1 - 5
libs/core-domain/src/core_domain/human_contracts.py

@@ -1,10 +1,8 @@
 from datetime import datetime
 from typing import Literal
 
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
-
+from pydantic import BaseModel, Field
 
 HumanTaskType = Literal["approval", "input", "takeover", "pause", "resume"]
 HumanTaskStatus = Literal[
@@ -19,7 +17,6 @@ HumanTaskStatus = Literal[
 
 class HumanTaskContract(BaseModel):
     id: str
-    tenant_id: str
     task_type: HumanTaskType
     status: HumanTaskStatus
     title: str
@@ -40,7 +37,6 @@ class HumanTaskContract(BaseModel):
 
 
 class HumanTaskCreateContract(BaseModel):
-    tenant_id: str
     task_type: HumanTaskType
     title: str
     description: str | None = None

+ 1 - 7
libs/core-domain/src/core_domain/knowledge_contracts.py

@@ -1,10 +1,8 @@
 from datetime import datetime
 from typing import Literal
 
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
-
+from pydantic import BaseModel, Field
 
 KnowledgeBaseStatus = Literal["active", "archived"]
 KnowledgeDocumentStatus = Literal["draft", "indexed", "failed", "archived"]
@@ -12,7 +10,6 @@ KnowledgeDocumentStatus = Literal["draft", "indexed", "failed", "archived"]
 
 class KnowledgeBaseContract(BaseModel):
     id: str
-    tenant_id: str
     code: str
     name: str
     description: str | None = None
@@ -23,7 +20,6 @@ class KnowledgeBaseContract(BaseModel):
 
 class KnowledgeDocumentContract(BaseModel):
     id: str
-    tenant_id: str
     knowledge_base_id: str
     title: str
     source_type: str
@@ -37,7 +33,6 @@ class KnowledgeDocumentContract(BaseModel):
 
 class KnowledgeChunkContract(BaseModel):
     id: str
-    tenant_id: str
     knowledge_base_id: str
     document_id: str
     chunk_index: int
@@ -50,7 +45,6 @@ class KnowledgeChunkContract(BaseModel):
 
 
 class KnowledgeSearchRequestContract(BaseModel):
-    tenant_id: str
     knowledge_base_id: str
     query: str
     top_k: int = 5

+ 2 - 7
libs/core-domain/src/core_domain/memory_contracts.py

@@ -1,18 +1,15 @@
 from datetime import datetime
 from typing import Literal
 
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
-
+from pydantic import BaseModel, Field
 
 MemoryStatus = Literal["active", "archived", "deleted"]
-MemoryScopeType = Literal["tenant", "user", "session", "agent", "team"]
+MemoryScopeType = Literal["global", "user", "session", "agent", "team"]
 
 
 class MemoryItemContract(BaseModel):
     id: str
-    tenant_id: str
     scope_type: MemoryScopeType
     scope_id: str
     memory_type: str
@@ -33,7 +30,6 @@ class MemoryItemContract(BaseModel):
 
 
 class MemoryCreateContract(BaseModel):
-    tenant_id: str
     scope_type: MemoryScopeType
     scope_id: str
     memory_type: str = "fact"
@@ -49,7 +45,6 @@ class MemoryCreateContract(BaseModel):
 
 
 class MemorySearchRequestContract(BaseModel):
-    tenant_id: str
     query: str
     scope_type: MemoryScopeType | None = None
     scope_id: str | None = None

+ 1 - 2
libs/core-domain/src/core_domain/model_contracts.py

@@ -1,6 +1,5 @@
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
+from pydantic import BaseModel, Field
 
 
 class ChatMessageContract(BaseModel):

+ 1 - 4
libs/core-domain/src/core_domain/runtime_contracts.py

@@ -1,8 +1,8 @@
 from datetime import datetime
 from typing import Literal
 
-from pydantic import BaseModel
 from core_shared import JSONValue
+from pydantic import BaseModel
 
 NodeRunStatus = Literal["pending", "queued", "running", "completed", "failed", "skipped"]
 WorkflowRunStatus = Literal["pending", "running", "completed", "failed", "cancelled", "paused"]
@@ -15,7 +15,6 @@ class InitialNodeContract(BaseModel):
 
 
 class RunCreateContract(BaseModel):
-    tenant_id: str
     app_id: str
     app_version_id: str
     workflow_id: str
@@ -31,7 +30,6 @@ class RunCreateContract(BaseModel):
 
 class WorkflowRunContract(BaseModel):
     id: str
-    tenant_id: str
     app_id: str
     app_version_id: str
     workflow_id: str
@@ -50,7 +48,6 @@ class WorkflowRunContract(BaseModel):
 
 class NodeRunContract(BaseModel):
     id: str
-    tenant_id: str
     run_id: str
     node_id: str
     node_type: str

+ 1 - 4
libs/core-domain/src/core_domain/scheduler_contracts.py

@@ -1,10 +1,8 @@
 from datetime import datetime
 from typing import Literal
 
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
-
+from pydantic import BaseModel, Field
 
 ScheduledJobStatus = Literal[
     "scheduled",
@@ -18,7 +16,6 @@ ScheduledJobType = Literal["http", "event", "runtime", "agent", "team"]
 
 class ScheduledJobContract(BaseModel):
     id: str
-    tenant_id: str
     job_type: ScheduledJobType
     status: ScheduledJobStatus
     name: str

+ 1 - 7
libs/core-domain/src/core_domain/skill_contracts.py

@@ -1,10 +1,8 @@
 from datetime import datetime
 from typing import Literal
 
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
-
+from pydantic import BaseModel, Field
 
 SkillStatus = Literal["draft", "active", "archived"]
 SkillVersionStatus = Literal["draft", "published", "deprecated"]
@@ -14,7 +12,6 @@ SkillRunStatus = Literal["queued", "running", "completed", "failed", "cancelled"
 
 class SkillDefinitionContract(BaseModel):
     id: str
-    tenant_id: str
     code: str
     name: str
     skill_type: str
@@ -26,7 +23,6 @@ class SkillDefinitionContract(BaseModel):
 
 class SkillVersionContract(BaseModel):
     id: str
-    tenant_id: str
     skill_id: str
     version_no: int
     status: SkillVersionStatus
@@ -41,7 +37,6 @@ class SkillVersionContract(BaseModel):
 
 class SkillInstallationContract(BaseModel):
     id: str
-    tenant_id: str
     skill_id: str
     skill_version_id: str
     install_scope: str
@@ -55,7 +50,6 @@ class SkillInstallationContract(BaseModel):
 
 class SkillRunContract(BaseModel):
     id: str
-    tenant_id: str
     skill_id: str
     skill_version_id: str
     installation_id: str | None = None

+ 1 - 6
libs/core-domain/src/core_domain/team_contracts.py

@@ -1,10 +1,8 @@
 from datetime import datetime
 from typing import Literal
 
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
-
+from pydantic import BaseModel, Field
 
 TeamStatus = Literal["draft", "active", "archived"]
 TeamVersionStatus = Literal["draft", "published", "deprecated"]
@@ -24,7 +22,6 @@ class TeamMemberContract(BaseModel):
 
 class TeamDefinitionContract(BaseModel):
     id: str
-    tenant_id: str
     code: str
     name: str
     description: str | None = None
@@ -36,7 +33,6 @@ class TeamDefinitionContract(BaseModel):
 
 class TeamVersionContract(BaseModel):
     id: str
-    tenant_id: str
     team_id: str
     version_no: int
     status: TeamVersionStatus
@@ -50,7 +46,6 @@ class TeamVersionContract(BaseModel):
 
 class TeamRunContract(BaseModel):
     id: str
-    tenant_id: str
     team_id: str
     team_version_id: str
     session_id: str | None = None

+ 1 - 6
libs/core-domain/src/core_domain/tool_contracts.py

@@ -1,13 +1,11 @@
 from datetime import datetime
 
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
+from pydantic import BaseModel, Field
 
 
 class ToolDefinitionContract(BaseModel):
     id: str
-    tenant_id: str
     plugin_id: str | None = None
     code: str
     name: str
@@ -18,7 +16,6 @@ class ToolDefinitionContract(BaseModel):
 
 class ToolVersionContract(BaseModel):
     id: str
-    tenant_id: str
     tool_id: str
     version_no: int
     input_schema_json: dict[str, JSONValue] | None = None
@@ -31,7 +28,6 @@ class ToolVersionContract(BaseModel):
 
 class ToolBindingContract(BaseModel):
     id: str
-    tenant_id: str
     app_id: str
     tool_version_id: str
     credential_id: str | None = None
@@ -43,7 +39,6 @@ class ToolBindingContract(BaseModel):
 
 class ToolCredentialContract(BaseModel):
     id: str
-    tenant_id: str
     name: str
     credential_type: str
     secret_fingerprint: str

+ 1 - 3
libs/core-domain/src/core_domain/workflow_contracts.py

@@ -1,13 +1,11 @@
 from datetime import datetime
 
-from pydantic import BaseModel
-
 from core_shared import JSONValue
+from pydantic import BaseModel
 
 
 class WorkflowVersionContract(BaseModel):
     id: str
-    tenant_id: str
     workflow_id: str
     version_no: int
     dsl_json: dict[str, JSONValue] | None = None

+ 2 - 4
libs/core-dsl/src/core_dsl/workflow.py

@@ -1,6 +1,5 @@
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
+from pydantic import BaseModel, Field
 
 
 class NodeDefinition(BaseModel):
@@ -48,8 +47,7 @@ def get_initial_node_definition(workflow: WorkflowDefinition) -> NodeDefinition
 
 def get_successor_node_definitions(
     workflow: WorkflowDefinition,
-    current_node_id: str,
-) -> list[NodeDefinition]:
+    current_node_id: str) -> list[NodeDefinition]:
     successor_ids = [edge.target for edge in workflow.edges if edge.source == current_node_id]
     node_map = {node.id: node for node in workflow.nodes}
     return [node_map[item] for item in successor_ids if item in node_map]

+ 1 - 6
libs/core-events/src/core_events/__init__.py

@@ -1,10 +1,5 @@
 from .client import EventServiceClient, EventServiceClientError
-from .envelope import (
-    EventDeliveryStatus,
-    EventEnvelope,
-    EventPublishContract,
-    EventRecordContract,
-)
+from .envelope import EventDeliveryStatus, EventEnvelope, EventPublishContract, EventRecordContract
 
 __all__ = [
     "EventDeliveryStatus",

+ 1 - 2
libs/core-events/src/core_events/client.py

@@ -17,8 +17,7 @@ class EventServiceClient:
             with httpx.Client(timeout=self.timeout_seconds) as client:
                 response = client.post(
                     f"{self.base_url}/events",
-                    json=payload.model_dump(mode="json"),
-                )
+                    json=payload.model_dump(mode="json"))
                 response.raise_for_status()
                 return EventRecordContract.model_validate(response.json())
         except httpx.HTTPError as exc:

+ 1 - 5
libs/core-events/src/core_events/envelope.py

@@ -2,10 +2,8 @@ from datetime import datetime
 from typing import Literal
 from uuid import uuid4
 
-from pydantic import BaseModel, Field
-
 from core_shared import JSONValue
-
+from pydantic import BaseModel, Field
 
 EventDeliveryStatus = Literal["pending", "published", "failed", "cancelled"]
 
@@ -14,7 +12,6 @@ class EventEnvelope(BaseModel):
     event_id: str = Field(default_factory=lambda: str(uuid4()))
     event_type: str
     source_service: str
-    tenant_id: str | None = None
     aggregate_type: str | None = None
     aggregate_id: str | None = None
     correlation_id: str | None = None
@@ -36,7 +33,6 @@ class EventRecordContract(EventEnvelope):
 class EventPublishContract(BaseModel):
     event_type: str
     source_service: str
-    tenant_id: str | None = None
     aggregate_type: str | None = None
     aggregate_id: str | None = None
     correlation_id: str | None = None

+ 1 - 2
libs/core-shared/src/core_shared/config.py

@@ -20,5 +20,4 @@ class ServiceSettings(BaseSettings):
     model_config = SettingsConfigDict(
         env_prefix="AGENT_PLATFORM_",
         env_file=".env",
-        extra="ignore",
-    )
+        extra="ignore")

+ 10 - 16
libs/core-shared/src/core_shared/observability.py

@@ -2,12 +2,13 @@ from __future__ import annotations
 
 import json
 import logging
-from uuid import uuid4
 from collections import defaultdict
+from collections.abc import Awaitable, Callable, Iterable
 from dataclasses import dataclass
-from time import perf_counter
 from threading import Lock
-from typing import Any, Awaitable, Callable, Iterable, Protocol
+from time import perf_counter
+from typing import Any, Protocol
+from uuid import uuid4
 
 from starlette.datastructures import Headers
 from starlette.middleware.base import BaseHTTPMiddleware
@@ -15,7 +16,6 @@ from starlette.requests import Request
 from starlette.responses import PlainTextResponse, Response
 from starlette.types import ASGIApp
 
-
 _METRICS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8"
 _DURATION_BUCKETS = (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
 TRACE_ID_HEADER = "x-trace-id"
@@ -32,8 +32,7 @@ class ObservableApp(Protocol):
     def get(
         self,
         path: str,
-        **options: Any,
-    ) -> RouteDecorator: ...
+        **options: Any) -> RouteDecorator: ...
 
 
 @dataclass(frozen=True, slots=True)
@@ -48,8 +47,7 @@ class HttpMetricLabels:
             ("service", self.service),
             ("method", self.method),
             ("path", self.path),
-            ("status_code", self.status_code),
-        )
+            ("status_code", self.status_code))
 
 
 class MetricsRegistry:
@@ -122,8 +120,7 @@ class ObservabilityMiddleware(BaseHTTPMiddleware):
     async def dispatch(
         self,
         request: Request,
-        call_next: Callable[[Request], Awaitable[Response]],
-    ) -> Response:
+        call_next: Callable[[Request], Awaitable[Response]]) -> Response:
         if request.url.path == "/metrics":
             return await call_next(request)
 
@@ -150,8 +147,7 @@ class ObservabilityMiddleware(BaseHTTPMiddleware):
                 service=self._service_name,
                 method=request.method,
                 path=path,
-                status_code=str(status_code),
-            )
+                status_code=str(status_code))
             self._registry.observe_http_request(labels, duration_seconds)
             self._log_request(request, path, status_code, duration_seconds)
 
@@ -160,8 +156,7 @@ class ObservabilityMiddleware(BaseHTTPMiddleware):
         request: Request,
         path: str,
         status_code: int,
-        duration_seconds: float,
-    ) -> None:
+        duration_seconds: float) -> None:
         headers = request.headers
         payload = {
             "event": "http_request",
@@ -171,7 +166,6 @@ class ObservabilityMiddleware(BaseHTTPMiddleware):
             "status_code": status_code,
             "duration_ms": round(duration_seconds * 1000, 3),
             "request_id": _header(headers, "x-request-id"),
-            "tenant_id": _header(headers, "x-tenant-id"),
             "trace_id": getattr(request.state, "trace_id", None),
             "span_id": getattr(request.state, "span_id", None),
             "parent_span_id": getattr(request.state, "parent_span_id", None),
@@ -183,7 +177,7 @@ def add_observability(app: ObservableApp, service_name: str) -> MetricsRegistry:
     """Attach request metrics and a /metrics endpoint to a FastAPI app."""
 
     registry = MetricsRegistry(service_name=service_name)
-    setattr(app.state, "metrics_registry", registry)
+    app.state.metrics_registry = registry
     app.add_middleware(ObservabilityMiddleware, service_name=service_name, registry=registry)
 
     @app.get("/metrics", include_in_schema=False)

+ 9 - 16
libs/core-shared/src/core_shared/rate_limit.py

@@ -25,8 +25,7 @@ class RateLimiter(Protocol):
         key: str,
         limit: int,
         window_seconds: int,
-        now_epoch_seconds: int | None = None,
-    ) -> RateLimitDecision: ...
+        now_epoch_seconds: int | None = None) -> RateLimitDecision: ...
 
 
 class InMemoryFixedWindowRateLimiter:
@@ -40,8 +39,7 @@ class InMemoryFixedWindowRateLimiter:
         key: str,
         limit: int,
         window_seconds: int,
-        now_epoch_seconds: int | None = None,
-    ) -> RateLimitDecision:
+        now_epoch_seconds: int | None = None) -> RateLimitDecision:
         now = now_epoch_seconds or int(time.time())
         window = _window_start(now, window_seconds)
         bucket_key = (key, window)
@@ -52,8 +50,7 @@ class InMemoryFixedWindowRateLimiter:
         return _decision(
             current_count=current_count,
             limit=limit,
-            reset_epoch_seconds=window + window_seconds,
-        )
+            reset_epoch_seconds=window + window_seconds)
 
     def _cleanup_expired(self, *, now: int, window_seconds: int) -> None:
         cutoff = _window_start(now, window_seconds) - window_seconds
@@ -65,7 +62,7 @@ class InMemoryFixedWindowRateLimiter:
 
 
 class RedisFixedWindowRateLimiter:
-    def __init__(self, *, client: "Redis", prefix: str = "rate-limit") -> None:
+    def __init__(self, *, client: Redis, prefix: str = "rate-limit") -> None:
         self._client = client
         self._prefix = prefix
 
@@ -75,8 +72,7 @@ class RedisFixedWindowRateLimiter:
         key: str,
         limit: int,
         window_seconds: int,
-        now_epoch_seconds: int | None = None,
-    ) -> RateLimitDecision:
+        now_epoch_seconds: int | None = None) -> RateLimitDecision:
         now = now_epoch_seconds or int(time.time())
         window = _window_start(now, window_seconds)
         redis_key = f"{self._prefix}:{key}:{window}"
@@ -88,11 +84,10 @@ class RedisFixedWindowRateLimiter:
         return _decision(
             current_count=current_count,
             limit=limit,
-            reset_epoch_seconds=window + window_seconds,
-        )
+            reset_epoch_seconds=window + window_seconds)
 
 
-def build_rate_limiter(redis_client: "Redis | None") -> RateLimiter:
+def build_rate_limiter(redis_client: Redis | None) -> RateLimiter:
     if redis_client is None:
         return InMemoryFixedWindowRateLimiter()
     try:
@@ -110,13 +105,11 @@ def _decision(
     *,
     current_count: int,
     limit: int,
-    reset_epoch_seconds: int,
-) -> RateLimitDecision:
+    reset_epoch_seconds: int) -> RateLimitDecision:
     remaining = max(limit - current_count, 0)
     return RateLimitDecision(
         allowed=current_count <= limit,
         limit=limit,
         remaining=remaining,
         reset_epoch_seconds=reset_epoch_seconds,
-        current_count=current_count,
-    )
+        current_count=current_count)

+ 4 - 8
libs/core-shared/src/core_shared/redis_primitives.py

@@ -19,8 +19,7 @@ class DistributedLock:
         *,
         client: Redis,
         name: str,
-        ttl_seconds: int = 30,
-    ) -> None:
+        ttl_seconds: int = 30) -> None:
         self.client = client
         self.name = name
         self.ttl_seconds = ttl_seconds
@@ -32,8 +31,7 @@ class DistributedLock:
                 self.name,
                 self.token,
                 nx=True,
-                ex=self.ttl_seconds,
-            )
+                ex=self.ttl_seconds)
         )
 
     def release(self) -> bool:
@@ -67,13 +65,11 @@ class IdempotencyStore:
         *,
         key: str,
         result: dict[str, JSONValue],
-        ttl_seconds: int = 86400,
-    ) -> None:
+        ttl_seconds: int = 86400) -> None:
         self.client.set(
             self._key(key),
             json.dumps({"status": "completed", "result": result}, ensure_ascii=False),
-            ex=ttl_seconds,
-        )
+            ex=ttl_seconds)
 
     def get_result(self, *, key: str) -> dict[str, JSONValue] | None:
         value = self.client.get(self._key(key))

+ 8 - 8
libs/core-shared/src/core_shared/secrets.py

@@ -30,13 +30,11 @@ class SecretCipher:
             return EncryptedSecret(
                 ciphertext=fernet_ciphertext,
                 fingerprint=fingerprint,
-                algorithm="fernet-sha256",
-            )
+                algorithm="fernet-sha256")
         return EncryptedSecret(
             ciphertext=self._encrypt_with_hmac_stream(plaintext),
             fingerprint=fingerprint,
-            algorithm="hmac-stream-sha256",
-        )
+            algorithm="hmac-stream-sha256")
 
     def decrypt_json(self, encrypted: EncryptedSecret) -> dict[str, JSONValue]:
         if encrypted.algorithm == "fernet-sha256":
@@ -60,10 +58,13 @@ class SecretCipher:
 
     def _decrypt_with_fernet(self, ciphertext: str) -> bytes:
         try:
-            from cryptography.fernet import Fernet
+            from cryptography.fernet import Fernet, InvalidToken
         except Exception as exc:
             raise ValueError("cryptography is required to decrypt fernet secrets") from exc
-        return Fernet(self._fernet_key()).decrypt(ciphertext.encode("utf-8"))
+        try:
+            return Fernet(self._fernet_key()).decrypt(ciphertext.encode("utf-8"))
+        except InvalidToken as exc:
+            raise ValueError("invalid encrypted secret payload") from exc
 
     def _fernet_key(self) -> bytes:
         digest = hashlib.sha256(self._key).digest()
@@ -89,8 +90,7 @@ class SecretCipher:
         expected_signature = hmac.new(
             self._key,
             nonce + encrypted_payload,
-            hashlib.sha256,
-        ).digest()
+            hashlib.sha256).digest()
         if not hmac.compare_digest(signature, expected_signature):
             raise ValueError("secret signature mismatch")
         stream = _build_stream(key=self._key, nonce=nonce, length=len(encrypted_payload))

+ 8 - 15
libs/core-shared/src/core_shared/security.py

@@ -1,15 +1,14 @@
 from __future__ import annotations
 
 import hmac
-from collections.abc import Mapping, MutableMapping
-from typing import Any, Awaitable, Callable, Protocol
+from collections.abc import Awaitable, Callable, Mapping, MutableMapping
+from typing import Any, Protocol
 
 from starlette.middleware.base import BaseHTTPMiddleware
 from starlette.requests import Request
 from starlette.responses import JSONResponse, Response
 from starlette.types import ASGIApp
 
-
 SENSITIVE_KEYWORDS = (
     "api_key",
     "apikey",
@@ -20,8 +19,7 @@ SENSITIVE_KEYWORDS = (
     "password",
     "private_key",
     "secret",
-    "token",
-)
+    "token")
 
 
 class InternalServiceSettings(Protocol):
@@ -40,8 +38,7 @@ class InternalServiceAuthMiddleware(BaseHTTPMiddleware):
     async def dispatch(
         self,
         request: Request,
-        call_next: Callable[[Request], Awaitable[Response]],
-    ) -> Response:
+        call_next: Callable[[Request], Awaitable[Response]]) -> Response:
         if not self._settings.internal_service_auth_required:
             return await call_next(request)
         if _is_public_internal_auth_path(request.url.path):
@@ -52,13 +49,11 @@ class InternalServiceAuthMiddleware(BaseHTTPMiddleware):
         if not configured_token:
             return JSONResponse(
                 status_code=503,
-                content={"detail": "internal service auth token is not configured"},
-            )
+                content={"detail": "internal service auth token is not configured"})
         if provided_token is None or not hmac.compare_digest(provided_token, configured_token):
             return JSONResponse(
                 status_code=401,
-                content={"detail": "invalid internal service token"},
-            )
+                content={"detail": "invalid internal service token"})
         return await call_next(request)
 
 
@@ -69,8 +64,7 @@ def add_internal_service_auth(app: Any, settings: InternalServiceSettings) -> No
 def build_internal_service_headers(
     settings: InternalServiceSettings,
     *,
-    source_service: str | None = None,
-) -> dict[str, str]:
+    source_service: str | None = None) -> dict[str, str]:
     headers: dict[str, str] = {}
     if settings.internal_service_token:
         headers[settings.internal_service_token_header_name] = settings.internal_service_token
@@ -82,8 +76,7 @@ def merge_internal_service_headers(
     headers: Mapping[str, str] | None,
     settings: InternalServiceSettings,
     *,
-    source_service: str | None = None,
-) -> dict[str, str]:
+    source_service: str | None = None) -> dict[str, str]:
     merged = dict(headers or {})
     merged.update(build_internal_service_headers(settings, source_service=source_service))
     return merged

+ 10 - 10
libs/core-shared/src/core_shared/task_queue.py

@@ -19,31 +19,31 @@ class TaskQueueConsumer(Protocol):
 
 
 class TaskQueuePublisher:
-    def __init__(self, *, client: "Redis") -> None:
+    def __init__(self, *, client: Redis) -> None:
         self._client = client
 
-    def publish_agent_run(self, *, tenant_id: str, agent_run_id: str) -> bool:
+    def publish_agent_run(self, *, agent_run_id: str) -> bool:
         return self._publish(
             queue_name=AGENT_RUN_QUEUE,
-            payload={"tenant_id": tenant_id, "agent_run_id": agent_run_id},
+            payload={"agent_run_id": agent_run_id},
         )
 
-    def publish_runtime_node_run(self, *, tenant_id: str, node_run_id: str) -> bool:
+    def publish_runtime_node_run(self, *, node_run_id: str) -> bool:
         return self._publish(
             queue_name=RUNTIME_NODE_RUN_QUEUE,
-            payload={"tenant_id": tenant_id, "node_run_id": node_run_id},
+            payload={"node_run_id": node_run_id},
         )
 
-    def publish_scheduled_job(self, *, tenant_id: str, scheduled_job_id: str) -> bool:
+    def publish_scheduled_job(self, *, scheduled_job_id: str) -> bool:
         return self._publish(
             queue_name=SCHEDULED_JOB_QUEUE,
-            payload={"tenant_id": tenant_id, "scheduled_job_id": scheduled_job_id},
+            payload={"scheduled_job_id": scheduled_job_id},
         )
 
-    def publish_team_run(self, *, tenant_id: str, team_run_id: str) -> bool:
+    def publish_team_run(self, *, team_run_id: str) -> bool:
         return self._publish(
             queue_name=TEAM_RUN_QUEUE,
-            payload={"tenant_id": tenant_id, "team_run_id": team_run_id},
+            payload={"team_run_id": team_run_id},
         )
 
     def _publish(self, *, queue_name: str, payload: dict[str, JSONValue]) -> bool:
@@ -58,7 +58,7 @@ class TaskQueuePublisher:
 
 def build_task_queue_consumer(
     *,
-    client: "Redis | None",
+    client: Redis | None,
     queue_name: str,
 ) -> TaskQueueConsumer | None:
     if client is None:

+ 2 - 2
libs/core-shared/src/core_shared/types.py

@@ -1,8 +1,8 @@
 from typing import TypeAlias
+
 from typing_extensions import TypeAliasType
 
 JSONPrimitive: TypeAlias = str | int | float | bool | None
 JSONValue = TypeAliasType(
     "JSONValue",
-    JSONPrimitive | dict[str, "JSONValue"] | list["JSONValue"],
-)
+    JSONPrimitive | dict[str, "JSONValue"] | list["JSONValue"])

+ 10 - 21
scripts/migrate_all.py

@@ -7,7 +7,6 @@ import sys
 from dataclasses import dataclass
 from pathlib import Path
 
-
 DEFAULT_SERVICE_ORDER = [
     "workflow-service",
     "session-service",
@@ -39,8 +38,7 @@ def main() -> int:
     targets = discover_targets(
         repo_root=repo_root,
         only_services=args.only,
-        skip_missing=args.skip_missing,
-    )
+        skip_missing=args.skip_missing)
     if args.dry_run:
         for target in targets:
             print(f"{target.service_name}: {target.alembic_ini_path}")
@@ -68,28 +66,23 @@ def parse_args() -> argparse.Namespace:
         "--only",
         action="append",
         default=[],
-        help="Only migrate a service. Can be passed multiple times.",
-    )
+        help="Only migrate a service. Can be passed multiple times.")
     parser.add_argument(
         "--python",
         default=sys.executable,
-        help="Python executable to use for `python -m alembic`.",
-    )
+        help="Python executable to use for `python -m alembic`.")
     parser.add_argument(
         "--continue-on-error",
         action="store_true",
-        help="Continue migrating remaining services if one migration fails.",
-    )
+        help="Continue migrating remaining services if one migration fails.")
     parser.add_argument(
         "--skip-missing",
         action="store_true",
-        help="Skip services without alembic.ini instead of failing.",
-    )
+        help="Skip services without alembic.ini instead of failing.")
     parser.add_argument(
         "--dry-run",
         action="store_true",
-        help="Print migration targets without executing migrations.",
-    )
+        help="Print migration targets without executing migrations.")
     return parser.parse_args()
 
 
@@ -97,8 +90,7 @@ def discover_targets(
     *,
     repo_root: Path,
     only_services: list[str],
-    skip_missing: bool,
-) -> list[MigrationTarget]:
+    skip_missing: bool) -> list[MigrationTarget]:
     requested_services = only_services or DEFAULT_SERVICE_ORDER
     targets: list[MigrationTarget] = []
     for service_name in requested_services:
@@ -112,8 +104,7 @@ def discover_targets(
             MigrationTarget(
                 service_name=service_name,
                 service_path=service_path,
-                alembic_ini_path=alembic_ini_path,
-            )
+                alembic_ini_path=alembic_ini_path)
         )
     return targets
 
@@ -121,16 +112,14 @@ def discover_targets(
 def run_alembic_upgrade(
     *,
     target: MigrationTarget,
-    python_executable: str,
-) -> subprocess.CompletedProcess[str]:
+    python_executable: str) -> subprocess.CompletedProcess[str]:
     env = os.environ.copy()
     result = subprocess.run(
         [python_executable, "-m", "alembic", "upgrade", "head"],
         cwd=target.service_path,
         env=env,
         text=True,
-        check=False,
-    )
+        check=False)
     return result
 
 

+ 20 - 49
scripts/smoke_runtime_no_key.py

@@ -8,16 +8,12 @@ from dataclasses import dataclass
 
 import httpx
 
-
 WORKFLOW_SERVICE_URL = os.getenv(
     "AGENT_PLATFORM_SMOKE_WORKFLOW_URL",
-    "http://127.0.0.1:8002/workflows",
-)
+    "http://127.0.0.1:8002/workflows")
 RUNTIME_SERVICE_URL = os.getenv(
     "AGENT_PLATFORM_SMOKE_RUNTIME_URL",
-    "http://127.0.0.1:8003/runtime",
-)
-TENANT_ID = os.getenv("AGENT_PLATFORM_SMOKE_TENANT_ID", "t-smoke")
+    "http://127.0.0.1:8003/runtime")
 SMOKE_API_KEY = os.getenv("AGENT_PLATFORM_SMOKE_API_KEY")
 
 
@@ -32,19 +28,16 @@ SCENARIOS = (
     SmokeScenario(
         score=7,
         expected_branch_node_id="high_path",
-        expected_output_text="Alice passed with score 7",
-    ),
+        expected_output_text="Alice passed with score 7"),
     SmokeScenario(
         score=3,
         expected_branch_node_id="low_path",
-        expected_output_text="Alice did not pass; score 3",
-    ),
-)
+        expected_output_text="Alice did not pass; score 3"))
 
 
 def main() -> int:
     unique_suffix = uuid.uuid4().hex[:8]
-    headers = {"x-tenant-id": TENANT_ID}
+    headers = {}
     if SMOKE_API_KEY:
         headers["x-api-key"] = SMOKE_API_KEY
 
@@ -65,11 +58,9 @@ def create_app(client: httpx.Client, unique_suffix: str) -> str:
     response = client.post(
         f"{WORKFLOW_SERVICE_URL}/apps",
         json={
-            "tenant_id": TENANT_ID,
             "code": f"smoke-app-{unique_suffix}",
             "name": f"Smoke App {unique_suffix}",
-        },
-    )
+        })
     response.raise_for_status()
     payload = response.json()
     return str(payload["id"])
@@ -79,12 +70,10 @@ def create_workflow(client: httpx.Client, app_id: str, unique_suffix: str) -> st
     response = client.post(
         WORKFLOW_SERVICE_URL,
         json={
-            "tenant_id": TENANT_ID,
             "app_id": app_id,
             "code": f"smoke-flow-{unique_suffix}",
             "name": f"Smoke Flow {unique_suffix}",
-        },
-    )
+        })
     response.raise_for_status()
     payload = response.json()
     return str(payload["id"])
@@ -95,8 +84,7 @@ def run_scenario(
     app_id: str,
     workflow_id: str,
     unique_suffix: str,
-    scenario: SmokeScenario,
-) -> dict[str, object]:
+    scenario: SmokeScenario) -> dict[str, object]:
     workflow_version_id = create_workflow_version(client, workflow_id, unique_suffix, scenario.score)
     app_version_id = create_app_version(client, app_id, workflow_version_id)
     run_id = create_run(client, app_id, app_version_id, workflow_id, workflow_version_id)
@@ -137,8 +125,7 @@ def run_retriever_scenario(
     client: httpx.Client,
     app_id: str,
     workflow_id: str,
-    unique_suffix: str,
-) -> dict[str, object]:
+    unique_suffix: str) -> dict[str, object]:
     workflow_version_id = create_retriever_workflow_version(client, workflow_id, unique_suffix)
     app_version_id = create_app_version(client, app_id, workflow_version_id)
     run_id = create_run(client, app_id, app_version_id, workflow_id, workflow_version_id)
@@ -180,17 +167,14 @@ def create_workflow_version(
     client: httpx.Client,
     workflow_id: str,
     unique_suffix: str,
-    score: int,
-) -> str:
+    score: int) -> str:
     response = client.post(
         f"{WORKFLOW_SERVICE_URL}/versions",
         json={
-            "tenant_id": TENANT_ID,
             "workflow_id": workflow_id,
             "status": "active",
             "dsl_json": build_workflow_dsl(unique_suffix, score),
-        },
-    )
+        })
     response.raise_for_status()
     payload = response.json()
     return str(payload["id"])
@@ -199,17 +183,14 @@ def create_workflow_version(
 def create_retriever_workflow_version(
     client: httpx.Client,
     workflow_id: str,
-    unique_suffix: str,
-) -> str:
+    unique_suffix: str) -> str:
     response = client.post(
         f"{WORKFLOW_SERVICE_URL}/versions",
         json={
-            "tenant_id": TENANT_ID,
             "workflow_id": workflow_id,
             "status": "active",
             "dsl_json": build_retriever_workflow_dsl(unique_suffix),
-        },
-    )
+        })
     response.raise_for_status()
     payload = response.json()
     return str(payload["id"])
@@ -219,12 +200,10 @@ def create_app_version(client: httpx.Client, app_id: str, workflow_version_id: s
     response = client.post(
         f"{WORKFLOW_SERVICE_URL}/apps/versions",
         json={
-            "tenant_id": TENANT_ID,
             "app_id": app_id,
             "workflow_version_id": workflow_version_id,
             "status": "active",
-        },
-    )
+        })
     response.raise_for_status()
     payload = response.json()
     return str(payload["id"])
@@ -235,18 +214,15 @@ def create_run(
     app_id: str,
     app_version_id: str,
     workflow_id: str,
-    workflow_version_id: str,
-) -> str:
+    workflow_version_id: str) -> str:
     response = client.post(
         f"{RUNTIME_SERVICE_URL}/runs",
         json={
-            "tenant_id": TENANT_ID,
             "app_id": app_id,
             "app_version_id": app_version_id,
             "workflow_id": workflow_id,
             "workflow_version_id": workflow_version_id,
-        },
-    )
+        })
     response.raise_for_status()
     payload = response.json()
     return str(payload["run"]["id"])
@@ -255,17 +231,14 @@ def create_run(
 def execute_run(client: httpx.Client, run_id: str) -> None:
     response = client.post(
         f"{RUNTIME_SERVICE_URL}/runs/{run_id}/execute",
-        params={"tenant_id": TENANT_ID},
-        json={"max_steps": 8},
-    )
+        json={"max_steps": 8})
     response.raise_for_status()
 
 
 def list_node_runs(client: httpx.Client, run_id: str) -> list[dict[str, object]]:
     response = client.get(
         f"{RUNTIME_SERVICE_URL}/node-runs",
-        params={"tenant_id": TENANT_ID, "run_id": run_id},
-    )
+        params={"run_id": run_id})
     response.raise_for_status()
     payload = response.json()
     if not isinstance(payload, list):
@@ -276,8 +249,7 @@ def list_node_runs(client: httpx.Client, run_id: str) -> list[dict[str, object]]
 def list_node_artifacts(client: httpx.Client, run_id: str) -> list[dict[str, object]]:
     response = client.get(
         f"{RUNTIME_SERVICE_URL}/node-artifacts",
-        params={"tenant_id": TENANT_ID, "run_id": run_id},
-    )
+        params={"run_id": run_id})
     response.raise_for_status()
     payload = response.json()
     if not isinstance(payload, list):
@@ -288,8 +260,7 @@ def list_node_artifacts(client: httpx.Client, run_id: str) -> list[dict[str, obj
 def list_trace_spans(client: httpx.Client, run_id: str) -> list[dict[str, object]]:
     response = client.get(
         f"{RUNTIME_SERVICE_URL}/trace-spans",
-        params={"tenant_id": TENANT_ID, "run_id": run_id},
-    )
+        params={"run_id": run_id})
     response.raise_for_status()
     payload = response.json()
     if not isinstance(payload, list):

+ 2 - 4
services/agent-service/alembic/env.py

@@ -1,9 +1,8 @@
 from logging.config import fileConfig
 
 from alembic import context
-from sqlalchemy import engine_from_config, pool
-
 from app.db.models import Base
+from sqlalchemy import engine_from_config, pool
 
 config = context.config
 
@@ -25,8 +24,7 @@ def run_migrations_online() -> None:
     connectable = engine_from_config(
         config.get_section(config.config_ini_section, {}),
         prefix="sqlalchemy.",
-        poolclass=pool.NullPool,
-    )
+        poolclass=pool.NullPool)
 
     with connectable.connect() as connection:
         context.configure(connection=connection, target_metadata=target_metadata)

+ 4 - 17
services/agent-service/alembic/versions/20260424_0001_init_agent_models.py

@@ -7,9 +7,8 @@ Create Date: 2026-04-24 10:20:00
 
 from collections.abc import Sequence
 
-from alembic import op
 import sqlalchemy as sa
-
+from alembic import op
 
 revision: str = "20260424_0001"
 down_revision: str | None = None
@@ -28,18 +27,15 @@ def upgrade() -> None:
         sa.Column("owner_user_id", sa.String(length=36), nullable=True),
         sa.Column("metadata_json", sa.JSON(), nullable=True),
         sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("tenant_id", sa.String(length=36), nullable=False),
         sa.Column("created_by", sa.String(length=36), nullable=True),
         sa.Column("updated_by", sa.String(length=36), nullable=True),
         sa.Column("created_time", sa.DateTime(), nullable=False),
         sa.Column("updated_time", sa.DateTime(), nullable=False),
         sa.Column("deleted_time", sa.DateTime(), nullable=True),
         sa.Column("version", sa.Integer(), nullable=False),
-        sa.PrimaryKeyConstraint("id"),
-    )
+        sa.PrimaryKeyConstraint("id"))
     op.create_index("ix_agent_definition_code", "agent_definition", ["code"], unique=False)
     op.create_index("ix_agent_definition_status", "agent_definition", ["status"], unique=False)
-    op.create_index("ix_agent_definition_tenant_id", "agent_definition", ["tenant_id"], unique=False)
 
     op.create_table(
         "agent_version",
@@ -55,18 +51,15 @@ def upgrade() -> None:
         sa.Column("skill_refs_json", sa.JSON(), nullable=False),
         sa.Column("published_time", sa.DateTime(), nullable=True),
         sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("tenant_id", sa.String(length=36), nullable=False),
         sa.Column("created_by", sa.String(length=36), nullable=True),
         sa.Column("updated_by", sa.String(length=36), nullable=True),
         sa.Column("created_time", sa.DateTime(), nullable=False),
         sa.Column("updated_time", sa.DateTime(), nullable=False),
         sa.Column("deleted_time", sa.DateTime(), nullable=True),
         sa.Column("version", sa.Integer(), nullable=False),
-        sa.PrimaryKeyConstraint("id"),
-    )
+        sa.PrimaryKeyConstraint("id"))
     op.create_index("ix_agent_version_agent_id", "agent_version", ["agent_id"], unique=False)
     op.create_index("ix_agent_version_status", "agent_version", ["status"], unique=False)
-    op.create_index("ix_agent_version_tenant_id", "agent_version", ["tenant_id"], unique=False)
 
     op.create_table(
         "agent_run",
@@ -84,36 +77,30 @@ def upgrade() -> None:
         sa.Column("error_code", sa.String(length=64), nullable=True),
         sa.Column("error_message", sa.Text(), nullable=True),
         sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("tenant_id", sa.String(length=36), nullable=False),
         sa.Column("created_by", sa.String(length=36), nullable=True),
         sa.Column("updated_by", sa.String(length=36), nullable=True),
         sa.Column("created_time", sa.DateTime(), nullable=False),
         sa.Column("updated_time", sa.DateTime(), nullable=False),
         sa.Column("deleted_time", sa.DateTime(), nullable=True),
         sa.Column("version", sa.Integer(), nullable=False),
-        sa.PrimaryKeyConstraint("id"),
-    )
+        sa.PrimaryKeyConstraint("id"))
     op.create_index("ix_agent_run_agent_id", "agent_run", ["agent_id"], unique=False)
     op.create_index("ix_agent_run_agent_version_id", "agent_run", ["agent_version_id"], unique=False)
     op.create_index("ix_agent_run_session_id", "agent_run", ["session_id"], unique=False)
     op.create_index("ix_agent_run_status", "agent_run", ["status"], unique=False)
-    op.create_index("ix_agent_run_tenant_id", "agent_run", ["tenant_id"], unique=False)
 
 
 def downgrade() -> None:
-    op.drop_index("ix_agent_run_tenant_id", table_name="agent_run")
     op.drop_index("ix_agent_run_status", table_name="agent_run")
     op.drop_index("ix_agent_run_session_id", table_name="agent_run")
     op.drop_index("ix_agent_run_agent_version_id", table_name="agent_run")
     op.drop_index("ix_agent_run_agent_id", table_name="agent_run")
     op.drop_table("agent_run")
 
-    op.drop_index("ix_agent_version_tenant_id", table_name="agent_version")
     op.drop_index("ix_agent_version_status", table_name="agent_version")
     op.drop_index("ix_agent_version_agent_id", table_name="agent_version")
     op.drop_table("agent_version")
 
-    op.drop_index("ix_agent_definition_tenant_id", table_name="agent_definition")
     op.drop_index("ix_agent_definition_status", table_name="agent_definition")
     op.drop_index("ix_agent_definition_code", table_name="agent_definition")
     op.drop_table("agent_definition")

+ 2 - 4
services/agent-service/alembic/versions/20260425_0002_add_agent_run_worker_fields.py

@@ -7,9 +7,8 @@ Create Date: 2026-04-25 12:35:00
 
 from collections.abc import Sequence
 
-from alembic import op
 import sqlalchemy as sa
-
+from alembic import op
 
 revision: str = "20260425_0002"
 down_revision: str | None = "20260424_0001"
@@ -24,8 +23,7 @@ def upgrade() -> None:
         "ix_agent_run_worker_queue",
         "agent_run",
         ["status", "lease_expire_time", "created_time"],
-        unique=False,
-    )
+        unique=False)
 
 
 def downgrade() -> None:

+ 9 - 25
services/agent-service/alembic/versions/20260426_0003_add_agent_tool_invocations.py

@@ -34,64 +34,48 @@ def upgrade() -> None:
         sa.Column("started_time", sa.DateTime(), nullable=True),
         sa.Column("finished_time", sa.DateTime(), nullable=True),
         sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("tenant_id", sa.String(length=36), nullable=False),
         sa.Column("created_by", sa.String(length=36), nullable=True),
         sa.Column("updated_by", sa.String(length=36), nullable=True),
         sa.Column("created_time", sa.DateTime(), nullable=False),
         sa.Column("updated_time", sa.DateTime(), nullable=False),
         sa.Column("deleted_time", sa.DateTime(), nullable=True),
         sa.Column("version", sa.Integer(), nullable=False),
-        sa.PrimaryKeyConstraint("id"),
-    )
+        sa.PrimaryKeyConstraint("id"))
     op.create_index(
         "ix_agent_tool_invocation_agent_id",
         "agent_tool_invocation",
-        ["agent_id"],
-    )
+        ["agent_id"])
     op.create_index(
         "ix_agent_tool_invocation_agent_run_id",
         "agent_tool_invocation",
-        ["agent_run_id"],
-    )
+        ["agent_run_id"])
     op.create_index(
         "ix_agent_tool_invocation_agent_version_id",
         "agent_tool_invocation",
-        ["agent_version_id"],
-    )
+        ["agent_version_id"])
     op.create_index(
         "ix_agent_tool_invocation_status",
         "agent_tool_invocation",
-        ["status"],
-    )
-    op.create_index(
-        "ix_agent_tool_invocation_tenant_id",
-        "agent_tool_invocation",
-        ["tenant_id"],
-    )
+        ["status"])
     op.create_index(
         "ix_agent_tool_invocation_tool_binding_id",
         "agent_tool_invocation",
-        ["tool_binding_id"],
-    )
+        ["tool_binding_id"])
     op.create_index(
         "ix_agent_tool_invocation_tool_code",
         "agent_tool_invocation",
-        ["tool_code"],
-    )
+        ["tool_code"])
 
 
 def downgrade() -> None:
     op.drop_index("ix_agent_tool_invocation_tool_code", table_name="agent_tool_invocation")
     op.drop_index(
         "ix_agent_tool_invocation_tool_binding_id",
-        table_name="agent_tool_invocation",
-    )
-    op.drop_index("ix_agent_tool_invocation_tenant_id", table_name="agent_tool_invocation")
+        table_name="agent_tool_invocation")
     op.drop_index("ix_agent_tool_invocation_status", table_name="agent_tool_invocation")
     op.drop_index(
         "ix_agent_tool_invocation_agent_version_id",
-        table_name="agent_tool_invocation",
-    )
+        table_name="agent_tool_invocation")
     op.drop_index("ix_agent_tool_invocation_agent_run_id", table_name="agent_tool_invocation")
     op.drop_index("ix_agent_tool_invocation_agent_id", table_name="agent_tool_invocation")
     op.drop_table("agent_tool_invocation")

+ 23 - 48
services/agent-service/app/api/routes.py

@@ -1,9 +1,8 @@
+from core_domain import ServiceHealth
 from fastapi import APIRouter, Depends, HTTPException, Query
 from sqlalchemy import text
 from sqlalchemy.orm import Session
 
-from core_domain import ServiceHealth
-
 from app.application.services import AgentApplicationService, build_agent_application_service
 from app.bootstrap.settings import AgentServiceSettings
 from app.db.session import get_db
@@ -17,10 +16,10 @@ from app.schemas.agent import (
     AgentRunStatusUpdateRequest,
     AgentStatusUpdateRequest,
     AgentToolInvocationResponse,
-    AgentWorkerExecuteNextRequest,
-    AgentWorkerExecuteNextResponse,
     AgentVersionCreateRequest,
     AgentVersionResponse,
+    AgentWorkerExecuteNextRequest,
+    AgentWorkerExecuteNextResponse,
 )
 
 router = APIRouter()
@@ -32,8 +31,7 @@ def get_agent_service_settings() -> AgentServiceSettings:
 
 def get_agent_application_service(
     db: Session = Depends(get_db),
-    settings: AgentServiceSettings = Depends(get_agent_service_settings),
-) -> AgentApplicationService:
+    settings: AgentServiceSettings = Depends(get_agent_service_settings)) -> AgentApplicationService:
     return build_agent_application_service(db=db, settings=settings)
 
 
@@ -46,26 +44,22 @@ def health_check(db: Session = Depends(get_db)) -> ServiceHealth:
 @router.post("", response_model=AgentResponse)
 def create_agent(
     payload: AgentCreateRequest,
-    service: AgentApplicationService = Depends(get_agent_application_service),
-) -> AgentResponse:
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentResponse:
     entity = service.create_agent(payload)
     return AgentResponse.from_entity(entity)
 
 
 @router.get("", response_model=list[AgentResponse])
 def list_agents(
-    tenant_id: str = Query(...),
-    service: AgentApplicationService = Depends(get_agent_application_service),
-) -> list[AgentResponse]:
-    return [AgentResponse.from_entity(item) for item in service.list_agents(tenant_id=tenant_id)]
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentResponse]:
+    return [AgentResponse.from_entity(item) for item in service.list_agents()]
 
 
 @router.patch("/{agent_id}/status", response_model=AgentResponse)
 def update_agent_status(
     agent_id: str,
     payload: AgentStatusUpdateRequest,
-    service: AgentApplicationService = Depends(get_agent_application_service),
-) -> AgentResponse:
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentResponse:
     entity = service.update_agent_status(agent_id=agent_id, payload=payload)
     if entity is None:
         raise HTTPException(status_code=404, detail=f"agent not found: {agent_id}")
@@ -75,8 +69,7 @@ def update_agent_status(
 @router.post("/versions", response_model=AgentVersionResponse)
 def create_agent_version(
     payload: AgentVersionCreateRequest,
-    service: AgentApplicationService = Depends(get_agent_application_service),
-) -> AgentVersionResponse:
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentVersionResponse:
     try:
         entity = service.create_agent_version(payload)
     except ValueError as exc:
@@ -86,21 +79,18 @@ def create_agent_version(
 
 @router.get("/versions", response_model=list[AgentVersionResponse])
 def list_agent_versions(
-    tenant_id: str = Query(...),
     agent_id: str = Query(...),
-    service: AgentApplicationService = Depends(get_agent_application_service),
-) -> list[AgentVersionResponse]:
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentVersionResponse]:
     return [
         AgentVersionResponse.from_entity(item)
-        for item in service.list_agent_versions(tenant_id=tenant_id, agent_id=agent_id)
+        for item in service.list_agent_versions(agent_id=agent_id)
     ]
 
 
 @router.post("/runs", response_model=AgentRunResponse)
 def create_agent_run(
     payload: AgentRunCreateRequest,
-    service: AgentApplicationService = Depends(get_agent_application_service),
-) -> AgentRunResponse:
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentRunResponse:
     try:
         entity = service.create_agent_run(payload)
     except ValueError as exc:
@@ -110,36 +100,27 @@ def create_agent_run(
 
 @router.get("/runs", response_model=list[AgentRunResponse])
 def list_agent_runs(
-    tenant_id: str = Query(...),
     agent_id: str | None = Query(default=None),
     session_id: str | None = Query(default=None),
-    service: AgentApplicationService = Depends(get_agent_application_service),
-) -> list[AgentRunResponse]:
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentRunResponse]:
     return [
         AgentRunResponse.from_entity(item)
         for item in service.list_agent_runs(
-            tenant_id=tenant_id,
             agent_id=agent_id,
-            session_id=session_id,
-        )
+            session_id=session_id)
     ]
 
 
 @router.get(
     "/runs/{agent_run_id}/tool-invocations",
-    response_model=list[AgentToolInvocationResponse],
-)
+    response_model=list[AgentToolInvocationResponse])
 def list_agent_tool_invocations(
     agent_run_id: str,
-    tenant_id: str = Query(...),
-    service: AgentApplicationService = Depends(get_agent_application_service),
-) -> list[AgentToolInvocationResponse]:
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentToolInvocationResponse]:
     return [
         AgentToolInvocationResponse.from_entity(item)
         for item in service.list_agent_tool_invocations(
-            tenant_id=tenant_id,
-            agent_run_id=agent_run_id,
-        )
+            agent_run_id=agent_run_id)
     ]
 
 
@@ -147,8 +128,7 @@ def list_agent_tool_invocations(
 def update_agent_run_status(
     agent_run_id: str,
     payload: AgentRunStatusUpdateRequest,
-    service: AgentApplicationService = Depends(get_agent_application_service),
-) -> AgentRunResponse:
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentRunResponse:
     entity = service.update_agent_run_status(agent_run_id=agent_run_id, payload=payload)
     if entity is None:
         raise HTTPException(status_code=404, detail=f"agent_run not found: {agent_run_id}")
@@ -159,8 +139,7 @@ def update_agent_run_status(
 def execute_agent_run(
     agent_run_id: str,
     payload: AgentRunExecuteRequest,
-    service: AgentApplicationService = Depends(get_agent_application_service),
-) -> AgentRunExecuteResponse:
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentRunExecuteResponse:
     entity = service.execute_agent_run(agent_run_id=agent_run_id, payload=payload)
     if entity is None:
         raise HTTPException(status_code=404, detail=f"agent_run not found: {agent_run_id}")
@@ -171,21 +150,18 @@ def execute_agent_run(
     return AgentRunExecuteResponse(
         run=AgentRunResponse.from_entity(entity),
         model=model_value if isinstance(model_value, str) else None,
-        dry_run=dry_run_value if isinstance(dry_run_value, bool) else False,
-    )
+        dry_run=dry_run_value if isinstance(dry_run_value, bool) else False)
 
 
 @router.post("/workers/execute-next", response_model=AgentWorkerExecuteNextResponse)
 def execute_next_worker_task(
     payload: AgentWorkerExecuteNextRequest,
     settings: AgentServiceSettings = Depends(get_agent_service_settings),
-    service: AgentApplicationService = Depends(get_agent_application_service),
-) -> AgentWorkerExecuteNextResponse:
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentWorkerExecuteNextResponse:
     result = service.execute_next_claimed_agent_run(
         worker_key=payload.worker_key,
         lease_seconds=payload.lease_seconds or settings.worker_lease_seconds,
-        dry_run=payload.dry_run if payload.dry_run is not None else settings.worker_dry_run,
-    )
+        dry_run=payload.dry_run if payload.dry_run is not None else settings.worker_dry_run)
     if result is None:
         raise HTTPException(status_code=404, detail="queued agent_run not found")
 
@@ -197,5 +173,4 @@ def execute_next_worker_task(
         run=AgentRunResponse.from_entity(entity),
         model=model_value if isinstance(model_value, str) else None,
         dry_run=dry_run_value if isinstance(dry_run_value, bool) else False,
-        released_lease_count=released_lease_count,
-    )
+        released_lease_count=released_lease_count)

Fichier diff supprimé car celui-ci est trop grand
+ 117 - 259
services/agent-service/app/application/services.py


+ 2 - 3
services/agent-service/app/bootstrap/app.py

@@ -1,6 +1,6 @@
-from fastapi import FastAPI
 from core_shared.observability import add_observability
 from core_shared.security import add_internal_service_auth
+from fastapi import FastAPI
 
 from app.api.routes import router
 from app.bootstrap.settings import AgentServiceSettings
@@ -11,8 +11,7 @@ def create_app() -> FastAPI:
     settings = AgentServiceSettings()
     app = FastAPI(
         title="agent-platform agent-service",
-        version="0.1.0",
-    )
+        version="0.1.0")
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)

+ 3 - 4
services/agent-service/app/db/models/agent_definition.py

@@ -1,12 +1,11 @@
+from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_shared import JSONValue
 from sqlalchemy import String, Text
 from sqlalchemy.dialects.sqlite import JSON
 from sqlalchemy.orm import Mapped, mapped_column
 
-from core_db import AuditMixin, Base, TenantMixin, VersionMixin
-from core_shared import JSONValue
-
 
-class AgentDefinition(TenantMixin, AuditMixin, VersionMixin, Base):
+class AgentDefinition(EntityMixin, AuditMixin, VersionMixin, Base):
     __tablename__ = "agent_definition"
 
     code: Mapped[str] = mapped_column(String(64), index=True)

+ 3 - 4
services/agent-service/app/db/models/agent_run.py

@@ -1,14 +1,13 @@
 from datetime import datetime
 
+from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_shared import JSONValue
 from sqlalchemy import DateTime, String, Text
 from sqlalchemy.dialects.sqlite import JSON
 from sqlalchemy.orm import Mapped, mapped_column
 
-from core_db import AuditMixin, Base, TenantMixin, VersionMixin
-from core_shared import JSONValue
-
 
-class AgentRun(TenantMixin, AuditMixin, VersionMixin, Base):
+class AgentRun(EntityMixin, AuditMixin, VersionMixin, Base):
     __tablename__ = "agent_run"
 
     agent_id: Mapped[str] = mapped_column(String(36), index=True)

+ 3 - 4
services/agent-service/app/db/models/agent_tool_invocation.py

@@ -1,14 +1,13 @@
 from datetime import datetime
 
+from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_shared import JSONValue
 from sqlalchemy import DateTime, String, Text
 from sqlalchemy.dialects.sqlite import JSON
 from sqlalchemy.orm import Mapped, mapped_column
 
-from core_db import AuditMixin, Base, TenantMixin, VersionMixin
-from core_shared import JSONValue
-
 
-class AgentToolInvocation(TenantMixin, AuditMixin, VersionMixin, Base):
+class AgentToolInvocation(EntityMixin, AuditMixin, VersionMixin, Base):
     __tablename__ = "agent_tool_invocation"
 
     agent_run_id: Mapped[str] = mapped_column(String(36), index=True)

+ 3 - 4
services/agent-service/app/db/models/agent_version.py

@@ -1,14 +1,13 @@
 from datetime import datetime
 
+from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_shared import JSONValue
 from sqlalchemy import DateTime, Integer, String, Text
 from sqlalchemy.dialects.sqlite import JSON
 from sqlalchemy.orm import Mapped, mapped_column
 
-from core_db import AuditMixin, Base, TenantMixin, VersionMixin
-from core_shared import JSONValue
-
 
-class AgentVersion(TenantMixin, AuditMixin, VersionMixin, Base):
+class AgentVersion(EntityMixin, AuditMixin, VersionMixin, Base):
     __tablename__ = "agent_version"
 
     agent_id: Mapped[str] = mapped_column(String(36), index=True)

+ 3 - 6
services/agent-service/app/db/session.py

@@ -1,21 +1,18 @@
 from collections.abc import Generator
 
+from core_db import DatabaseSettings, create_engine_from_settings, create_session_factory
 from fastapi import Request
 from sqlalchemy.orm import Session, sessionmaker
 
-from core_db import DatabaseSettings, create_engine_from_settings, create_session_factory
-
 from app.bootstrap.settings import AgentServiceSettings
 
 
 def build_session_factory(
-    settings: AgentServiceSettings | None = None,
-) -> sessionmaker[Session]:
+    settings: AgentServiceSettings | None = None) -> sessionmaker[Session]:
     resolved_settings = settings or AgentServiceSettings()
     db_settings = DatabaseSettings(
         database_url=resolved_settings.database_url,
-        echo_sql=resolved_settings.echo_sql,
-    )
+        echo_sql=resolved_settings.echo_sql)
     engine = create_engine_from_settings(db_settings)
     return create_session_factory(engine)
 

+ 23 - 56
services/agent-service/app/domain/repositories.py

@@ -7,8 +7,7 @@ from core_domain import (
     AgentRunStatus,
     AgentStatus,
     AgentToolInvocationStatus,
-    AgentVersionStatus,
-)
+    AgentVersionStatus)
 from core_shared import JSONValue
 
 from app.db.models import AgentDefinition, AgentRun, AgentToolInvocation, AgentVersion
@@ -21,40 +20,34 @@ class AgentDefinitionRepository:
     def create(
         self,
         *,
-        tenant_id: str,
         code: str,
         name: str,
         description: str | None,
         agent_type: str,
         owner_user_id: str | None,
-        metadata_json: dict[str, JSONValue] | None,
-    ) -> AgentDefinition:
+        metadata_json: dict[str, JSONValue] | None) -> AgentDefinition:
         entity = AgentDefinition(
-            tenant_id=tenant_id,
             code=code,
             name=name,
             description=description,
             agent_type=agent_type,
             owner_user_id=owner_user_id,
-            metadata_json=metadata_json,
-        )
+            metadata_json=metadata_json)
         self.db.add(entity)
         self.db.commit()
         self.db.refresh(entity)
         return entity
 
-    def list_by_tenant(self, *, tenant_id: str) -> list[AgentDefinition]:
+    def list_all(self) -> list[AgentDefinition]:
         stmt = (
             select(AgentDefinition)
-            .where(AgentDefinition.tenant_id == tenant_id)
             .order_by(AgentDefinition.created_time.desc())
         )
         return list(self.db.scalars(stmt))
 
-    def get_by_id(self, *, tenant_id: str, agent_id: str) -> AgentDefinition | None:
+    def get_by_id(self, *, agent_id: str) -> AgentDefinition | None:
         stmt = (
             select(AgentDefinition)
-            .where(AgentDefinition.tenant_id == tenant_id)
             .where(AgentDefinition.id == agent_id)
         )
         return self.db.scalar(stmt)
@@ -62,11 +55,9 @@ class AgentDefinitionRepository:
     def update_status(
         self,
         *,
-        tenant_id: str,
         agent_id: str,
-        status: AgentStatus,
-    ) -> AgentDefinition | None:
-        entity = self.get_by_id(tenant_id=tenant_id, agent_id=agent_id)
+        status: AgentStatus) -> AgentDefinition | None:
+        entity = self.get_by_id(agent_id=agent_id)
         if entity is None:
             return None
         entity.status = status
@@ -82,7 +73,6 @@ class AgentVersionRepository:
     def create(
         self,
         *,
-        tenant_id: str,
         agent_id: str,
         status: AgentVersionStatus,
         role: str,
@@ -91,11 +81,9 @@ class AgentVersionRepository:
         model_config_json: dict[str, JSONValue],
         memory_policy_json: dict[str, JSONValue],
         tool_refs_json: list[dict[str, JSONValue]],
-        skill_refs_json: list[dict[str, JSONValue]],
-    ) -> AgentVersion:
+        skill_refs_json: list[dict[str, JSONValue]]) -> AgentVersion:
         version_no = self._next_version_no(agent_id)
         entity = AgentVersion(
-            tenant_id=tenant_id,
             agent_id=agent_id,
             version_no=version_no,
             status=status,
@@ -106,34 +94,30 @@ class AgentVersionRepository:
             memory_policy_json=memory_policy_json,
             tool_refs_json=tool_refs_json,
             skill_refs_json=skill_refs_json,
-            published_time=datetime.utcnow() if status == "published" else None,
-        )
+            published_time=datetime.utcnow() if status == "published" else None)
         self.db.add(entity)
         self.db.commit()
         self.db.refresh(entity)
         return entity
 
-    def list_by_agent(self, *, tenant_id: str, agent_id: str) -> list[AgentVersion]:
+    def list_by_agent(self, *, agent_id: str) -> list[AgentVersion]:
         stmt = (
             select(AgentVersion)
-            .where(AgentVersion.tenant_id == tenant_id)
             .where(AgentVersion.agent_id == agent_id)
             .order_by(AgentVersion.version_no.desc())
         )
         return list(self.db.scalars(stmt))
 
-    def get_by_id(self, *, tenant_id: str, agent_version_id: str) -> AgentVersion | None:
+    def get_by_id(self, *, agent_version_id: str) -> AgentVersion | None:
         stmt = (
             select(AgentVersion)
-            .where(AgentVersion.tenant_id == tenant_id)
             .where(AgentVersion.id == agent_version_id)
         )
         return self.db.scalar(stmt)
 
-    def get_latest_published(self, *, tenant_id: str, agent_id: str) -> AgentVersion | None:
+    def get_latest_published(self, *, agent_id: str) -> AgentVersion | None:
         stmt = (
             select(AgentVersion)
-            .where(AgentVersion.tenant_id == tenant_id)
             .where(AgentVersion.agent_id == agent_id)
             .where(AgentVersion.status == "published")
             .order_by(AgentVersion.version_no.desc())
@@ -154,24 +138,20 @@ class AgentRunRepository:
     def create(
         self,
         *,
-        tenant_id: str,
         agent_id: str,
         agent_version_id: str,
         session_id: str | None,
         input_text: str | None,
-        input_json: dict[str, JSONValue] | None,
-    ) -> AgentRun:
+        input_json: dict[str, JSONValue] | None) -> AgentRun:
         now = datetime.utcnow()
         entity = AgentRun(
-            tenant_id=tenant_id,
             agent_id=agent_id,
             agent_version_id=agent_version_id,
             session_id=session_id,
             input_text=input_text,
             input_json=input_json,
             status="queued",
-            queued_time=now,
-        )
+            queued_time=now)
         self.db.add(entity)
         self.db.commit()
         self.db.refresh(entity)
@@ -180,11 +160,9 @@ class AgentRunRepository:
     def list_by_scope(
         self,
         *,
-        tenant_id: str,
         agent_id: str | None = None,
-        session_id: str | None = None,
-    ) -> list[AgentRun]:
-        stmt = select(AgentRun).where(AgentRun.tenant_id == tenant_id)
+        session_id: str | None = None) -> list[AgentRun]:
+        stmt = select(AgentRun)
         if agent_id is not None:
             stmt = stmt.where(AgentRun.agent_id == agent_id)
         if session_id is not None:
@@ -192,10 +170,9 @@ class AgentRunRepository:
         stmt = stmt.order_by(AgentRun.created_time.desc())
         return list(self.db.scalars(stmt))
 
-    def get_by_id(self, *, tenant_id: str, agent_run_id: str) -> AgentRun | None:
+    def get_by_id(self, *, agent_run_id: str) -> AgentRun | None:
         stmt = (
             select(AgentRun)
-            .where(AgentRun.tenant_id == tenant_id)
             .where(AgentRun.id == agent_run_id)
         )
         return self.db.scalar(stmt)
@@ -204,8 +181,7 @@ class AgentRunRepository:
         self,
         *,
         worker_key: str,
-        lease_expire_time: datetime,
-    ) -> AgentRun | None:
+        lease_expire_time: datetime) -> AgentRun | None:
         stmt = (
             select(AgentRun)
             .where(AgentRun.status == "queued")
@@ -258,8 +234,7 @@ class AgentRunRepository:
         output_text: str | None = None,
         output_json: dict[str, JSONValue] | None = None,
         error_code: str | None = None,
-        error_message: str | None = None,
-    ) -> AgentRun | None:
+        error_message: str | None = None) -> AgentRun | None:
         entity = self.db.get(AgentRun, agent_run_id)
         if entity is None:
             return None
@@ -289,7 +264,6 @@ class AgentToolInvocationRepository:
     def create(
         self,
         *,
-        tenant_id: str,
         agent_run_id: str,
         agent_id: str,
         agent_version_id: str,
@@ -297,10 +271,8 @@ class AgentToolInvocationRepository:
         tool_binding_id: str | None,
         status: AgentToolInvocationStatus,
         reason: str | None = None,
-        input_json: dict[str, JSONValue] | None = None,
-    ) -> AgentToolInvocation:
+        input_json: dict[str, JSONValue] | None = None) -> AgentToolInvocation:
         entity = AgentToolInvocation(
-            tenant_id=tenant_id,
             agent_run_id=agent_run_id,
             agent_id=agent_id,
             agent_version_id=agent_version_id,
@@ -308,8 +280,7 @@ class AgentToolInvocationRepository:
             tool_binding_id=tool_binding_id,
             status=status,
             reason=reason,
-            input_json=input_json or {},
-        )
+            input_json=input_json or {})
         self.db.add(entity)
         self.db.commit()
         self.db.refresh(entity)
@@ -318,12 +289,9 @@ class AgentToolInvocationRepository:
     def list_by_run(
         self,
         *,
-        tenant_id: str,
-        agent_run_id: str,
-    ) -> list[AgentToolInvocation]:
+        agent_run_id: str) -> list[AgentToolInvocation]:
         stmt = (
             select(AgentToolInvocation)
-            .where(AgentToolInvocation.tenant_id == tenant_id)
             .where(AgentToolInvocation.agent_run_id == agent_run_id)
             .order_by(AgentToolInvocation.created_time.asc())
         )
@@ -337,8 +305,7 @@ class AgentToolInvocationRepository:
         reason: str | None = None,
         output_text: str | None = None,
         output_json: dict[str, JSONValue] | None = None,
-        error_message: str | None = None,
-    ) -> AgentToolInvocation | None:
+        error_message: str | None = None) -> AgentToolInvocation | None:
         entity = self.db.get(AgentToolInvocation, invocation_id)
         if entity is None:
             return None

+ 3 - 7
services/agent-service/app/infrastructure/memory_client.py

@@ -1,5 +1,4 @@
 import httpx
-
 from core_domain import (
     MemoryCreateContract,
     MemoryItemContract,
@@ -22,8 +21,7 @@ class MemoryClient:
             with httpx.Client(timeout=self.timeout_seconds) as client:
                 response = client.post(
                     f"{self.base_url}/memories",
-                    json=payload.model_dump(mode="json"),
-                )
+                    json=payload.model_dump(mode="json"))
                 response.raise_for_status()
                 return MemoryItemContract.model_validate(response.json())
         except httpx.HTTPError as exc:
@@ -31,14 +29,12 @@ class MemoryClient:
 
     def search_memories(
         self,
-        payload: MemorySearchRequestContract,
-    ) -> list[MemorySearchResultContract]:
+        payload: MemorySearchRequestContract) -> list[MemorySearchResultContract]:
         try:
             with httpx.Client(timeout=self.timeout_seconds) as client:
                 response = client.post(
                     f"{self.base_url}/memories/search",
-                    json=payload.model_dump(mode="json"),
-                )
+                    json=payload.model_dump(mode="json"))
                 response.raise_for_status()
                 return [
                     MemorySearchResultContract.model_validate(item)

+ 2 - 5
services/agent-service/app/infrastructure/model_gateway_client.py

@@ -1,5 +1,4 @@
 import httpx
-
 from core_domain import ChatCompletionRequestContract, ChatCompletionResponseContract
 
 
@@ -14,14 +13,12 @@ class ModelGatewayClient:
 
     def create_chat_completion(
         self,
-        payload: ChatCompletionRequestContract,
-    ) -> ChatCompletionResponseContract:
+        payload: ChatCompletionRequestContract) -> ChatCompletionResponseContract:
         try:
             with httpx.Client(timeout=self.timeout_seconds) as client:
                 response = client.post(
                     f"{self.base_url}/models/chat-completions",
-                    json=payload.model_dump(mode="json"),
-                )
+                    json=payload.model_dump(mode="json"))
                 response.raise_for_status()
                 return ChatCompletionResponseContract.model_validate(response.json())
         except httpx.HTTPError as exc:

+ 6 - 15
services/agent-service/app/infrastructure/skill_client.py

@@ -13,13 +13,10 @@ class SkillServiceClient:
         self.base_url = base_url.rstrip("/")
         self.timeout_seconds = timeout_seconds
 
-    def list_skills(self, *, tenant_id: str) -> list[SkillDefinitionContract]:
+    def list_skills(self) -> list[SkillDefinitionContract]:
         try:
             with httpx.Client(timeout=self.timeout_seconds) as client:
-                response = client.get(
-                    f"{self.base_url}/skills",
-                    params={"tenant_id": tenant_id},
-                )
+                response = client.get(f"{self.base_url}/skills")
                 response.raise_for_status()
                 return [
                     SkillDefinitionContract.model_validate(item)
@@ -31,14 +28,11 @@ class SkillServiceClient:
     def create_skill_run(
         self,
         *,
-        tenant_id: str,
         skill_id: str,
         skill_version_id: str | None,
         installation_id: str | None,
-        input_json: dict[str, JSONValue],
-    ) -> SkillRunContract:
+        input_json: dict[str, JSONValue]) -> SkillRunContract:
         payload: dict[str, JSONValue] = {
-            "tenant_id": tenant_id,
             "skill_id": skill_id,
             "input_json": input_json,
         }
@@ -58,11 +52,9 @@ class SkillServiceClient:
     def execute_skill_run(
         self,
         *,
-        tenant_id: str,
         skill_run_id: str,
-        worker_key: str | None,
-    ) -> SkillRunContract:
-        payload: dict[str, JSONValue] = {"tenant_id": tenant_id}
+        worker_key: str | None) -> SkillRunContract:
+        payload: dict[str, JSONValue] = {}
         if worker_key is not None:
             payload["worker_key"] = worker_key
 
@@ -70,8 +62,7 @@ class SkillServiceClient:
             with httpx.Client(timeout=self.timeout_seconds) as client:
                 response = client.post(
                     f"{self.base_url}/skills/runs/{skill_run_id}/execute",
-                    json=payload,
-                )
+                    json=payload)
                 response.raise_for_status()
                 return SkillRunContract.model_validate(response.json())
         except httpx.HTTPError as exc:

+ 7 - 18
services/agent-service/app/infrastructure/tool_client.py

@@ -1,5 +1,4 @@
 import httpx
-
 from core_domain import ToolBindingDetailContract
 from core_shared import JSONValue
 
@@ -16,15 +15,10 @@ class ToolServiceClient:
     def get_tool_binding_detail(
         self,
         *,
-        tenant_id: str,
-        binding_id: str,
-    ) -> ToolBindingDetailContract:
+        binding_id: str) -> ToolBindingDetailContract:
         try:
             with httpx.Client(timeout=self.timeout_seconds) as client:
-                response = client.get(
-                    f"{self.base_url}/tools/bindings/{binding_id}",
-                    params={"tenant_id": tenant_id},
-                )
+                response = client.get(f"{self.base_url}/tools/bindings/{binding_id}")
                 response.raise_for_status()
                 return ToolBindingDetailContract.model_validate(response.json())
         except httpx.HTTPError as exc:
@@ -35,8 +29,7 @@ class ToolServiceClient:
         *,
         detail: ToolBindingDetailContract,
         input_json: dict[str, JSONValue],
-        config_json: dict[str, JSONValue],
-    ) -> tuple[str | None, dict[str, JSONValue]]:
+        config_json: dict[str, JSONValue]) -> tuple[str | None, dict[str, JSONValue]]:
         invoke_config_json = detail.tool_version.invoke_config_json or {}
         binding_config_json = detail.binding.config_json or {}
 
@@ -55,16 +48,13 @@ class ToolServiceClient:
         headers = _merge_string_maps(
             _read_dict(invoke_config_json, "headers"),
             _read_dict(binding_config_json, "headers"),
-            _read_dict(config_json, "headers"),
-        )
+            _read_dict(config_json, "headers"))
         params = _merge_string_maps(
             _read_dict(invoke_config_json, "query"),
-            _read_dict(config_json, "query"),
-        )
+            _read_dict(config_json, "query"))
         body_json = _merge_json_dicts(
             _read_dict(invoke_config_json, "body"),
-            _read_dict(config_json, "body"),
-        )
+            _read_dict(config_json, "body"))
         if not body_json and method not in {"GET", "HEAD"}:
             body_json = input_json
 
@@ -75,8 +65,7 @@ class ToolServiceClient:
                     url=resolved_url,
                     headers=headers,
                     params=params,
-                    json=None if method in {"GET", "HEAD"} else body_json,
-                )
+                    json=None if method in {"GET", "HEAD"} else body_json)
                 response.raise_for_status()
         except httpx.HTTPError as exc:
             raise ToolServiceClientError(f"http tool invocation failed: {exc}") from exc

+ 2 - 11
services/agent-service/app/schemas/agent.py

@@ -1,8 +1,6 @@
 from datetime import datetime
 from typing import TYPE_CHECKING
 
-from pydantic import BaseModel, ConfigDict, Field
-
 from core_domain import (
     AgentDefinitionContract,
     AgentMemoryPolicyContract,
@@ -17,13 +15,13 @@ from core_domain import (
     AgentVersionStatus,
 )
 from core_shared import JSONValue
+from pydantic import BaseModel, ConfigDict, Field
 
 if TYPE_CHECKING:
     from app.db.models import AgentDefinition, AgentRun, AgentToolInvocation, AgentVersion
 
 
 class AgentCreateRequest(BaseModel):
-    tenant_id: str
     code: str
     name: str
     description: str | None = None
@@ -33,7 +31,6 @@ class AgentCreateRequest(BaseModel):
 
 
 class AgentStatusUpdateRequest(BaseModel):
-    tenant_id: str
     status: AgentStatus
 
 
@@ -45,8 +42,6 @@ class AgentResponse(AgentDefinitionContract):
 
 class AgentVersionCreateRequest(BaseModel):
     model_config = ConfigDict(populate_by_name=True)
-
-    tenant_id: str
     agent_id: str
     status: AgentVersionStatus = "draft"
     role: str = "assistant"
@@ -54,8 +49,7 @@ class AgentVersionCreateRequest(BaseModel):
     system_prompt: str
     model_config_data: AgentModelConfigContract = Field(
         default_factory=AgentModelConfigContract,
-        alias="model_config",
-    )
+        alias="model_config")
     memory_policy: AgentMemoryPolicyContract = Field(default_factory=AgentMemoryPolicyContract)
     tool_refs: list[AgentToolRefContract] = Field(default_factory=list)
     skill_refs: list[AgentSkillRefContract] = Field(default_factory=list)
@@ -68,7 +62,6 @@ class AgentVersionResponse(AgentVersionContract):
 
 
 class AgentRunCreateRequest(BaseModel):
-    tenant_id: str
     agent_id: str
     agent_version_id: str | None = None
     session_id: str | None = None
@@ -77,7 +70,6 @@ class AgentRunCreateRequest(BaseModel):
 
 
 class AgentRunStatusUpdateRequest(BaseModel):
-    tenant_id: str
     status: AgentRunStatus
     worker_key: str | None = None
     output_text: str | None = None
@@ -87,7 +79,6 @@ class AgentRunStatusUpdateRequest(BaseModel):
 
 
 class AgentRunExecuteRequest(BaseModel):
-    tenant_id: str
     worker_key: str | None = None
     dry_run: bool = False
 

+ 8 - 16
services/agent-service/app/worker.py

@@ -8,10 +8,9 @@ from dataclasses import dataclass
 from math import ceil
 from uuid import uuid4
 
-from sqlalchemy.orm import Session, sessionmaker
-
 from core_shared import try_build_redis_client
 from core_shared.task_queue import AGENT_RUN_QUEUE, build_task_queue_consumer
+from sqlalchemy.orm import Session, sessionmaker
 
 from app.application.services import build_agent_application_service
 from app.bootstrap.settings import AgentServiceSettings
@@ -32,16 +31,14 @@ class AgentWorker:
         *,
         settings: AgentServiceSettings,
         session_factory: sessionmaker[Session],
-        worker_key: str,
-    ) -> None:
+        worker_key: str) -> None:
         self.settings = settings
         self.session_factory = session_factory
         self.worker_key = worker_key
         self.redis_client = try_build_redis_client(settings.redis_url)
         self.task_queue = build_task_queue_consumer(
             client=self.redis_client,
-            queue_name=AGENT_RUN_QUEUE,
-        )
+            queue_name=AGENT_RUN_QUEUE)
 
     def run_forever(self) -> AgentWorkerStats:
         executed_count = 0
@@ -70,8 +67,7 @@ class AgentWorker:
                         worker_key=self.worker_key,
                         executed_count=executed_count,
                         idle_count=idle_count,
-                        error_count=error_count,
-                    )
+                        error_count=error_count)
 
     def run_once(self) -> bool:
         self._wait_for_queue_signal()
@@ -82,8 +78,7 @@ class AgentWorker:
                 worker_key=self.worker_key,
                 lease_seconds=self.settings.worker_lease_seconds,
                 dry_run=self.settings.worker_dry_run,
-                redis_client=self.redis_client,
-            )
+                redis_client=self.redis_client)
             return result is not None
         finally:
             db.close()
@@ -93,8 +88,7 @@ class AgentWorker:
             return
         try:
             self.task_queue.dequeue(
-                timeout_seconds=max(1, ceil(self.settings.worker_poll_interval_seconds)),
-            )
+                timeout_seconds=max(1, ceil(self.settings.worker_poll_interval_seconds)))
         except Exception:
             return
 
@@ -112,8 +106,7 @@ def main() -> None:
     worker = AgentWorker(
         settings=settings,
         session_factory=build_session_factory(settings),
-        worker_key=build_worker_key(),
-    )
+        worker_key=build_worker_key())
     stats = worker.run_forever()
     print(
         "agent-worker stopped "
@@ -121,8 +114,7 @@ def main() -> None:
         f"executed_count={stats.executed_count} "
         f"idle_count={stats.idle_count} "
         f"error_count={stats.error_count}",
-        flush=True,
-    )
+        flush=True)
 
 
 if __name__ == "__main__":

+ 2 - 4
services/api-gateway/alembic/env.py

@@ -1,9 +1,8 @@
 from logging.config import fileConfig
 
 from alembic import context
-from sqlalchemy import engine_from_config, pool
-
 from app.db.models import Base
+from sqlalchemy import engine_from_config, pool
 
 config = context.config
 
@@ -25,8 +24,7 @@ def run_migrations_online() -> None:
     connectable = engine_from_config(
         config.get_section(config.config_ini_section, {}),
         prefix="sqlalchemy.",
-        poolclass=pool.NullPool,
-    )
+        poolclass=pool.NullPool)
 
     with connectable.connect() as connection:
         context.configure(connection=connection, target_metadata=target_metadata)

+ 2 - 7
services/api-gateway/alembic/versions/20260423_0001_add_gateway_request_audit.py

@@ -7,9 +7,8 @@ Create Date: 2026-04-23 19:00:00
 
 from collections.abc import Sequence
 
-from alembic import op
 import sqlalchemy as sa
-
+from alembic import op
 
 revision: str = "20260423_0001"
 down_revision: str | None = None
@@ -32,24 +31,20 @@ def upgrade() -> None:
         sa.Column("user_agent", sa.String(length=512), nullable=True),
         sa.Column("error_message", sa.Text(), nullable=True),
         sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("tenant_id", sa.String(length=36), nullable=False),
         sa.Column("created_by", sa.String(length=36), nullable=True),
         sa.Column("updated_by", sa.String(length=36), nullable=True),
         sa.Column("created_time", sa.DateTime(), nullable=False),
         sa.Column("updated_time", sa.DateTime(), nullable=False),
         sa.Column("deleted_time", sa.DateTime(), nullable=True),
         sa.Column("version", sa.Integer(), nullable=False),
-        sa.PrimaryKeyConstraint("id"),
-    )
+        sa.PrimaryKeyConstraint("id"))
     op.create_index("ix_gateway_request_audit_request_id", "gateway_request_audit", ["request_id"], unique=False)
     op.create_index("ix_gateway_request_audit_path", "gateway_request_audit", ["path"], unique=False)
     op.create_index("ix_gateway_request_audit_target_service", "gateway_request_audit", ["target_service"], unique=False)
     op.create_index("ix_gateway_request_audit_status_code", "gateway_request_audit", ["status_code"], unique=False)
-    op.create_index("ix_gateway_request_audit_tenant_id", "gateway_request_audit", ["tenant_id"], unique=False)
 
 
 def downgrade() -> None:
-    op.drop_index("ix_gateway_request_audit_tenant_id", table_name="gateway_request_audit")
     op.drop_index("ix_gateway_request_audit_status_code", table_name="gateway_request_audit")
     op.drop_index("ix_gateway_request_audit_target_service", table_name="gateway_request_audit")
     op.drop_index("ix_gateway_request_audit_path", table_name="gateway_request_audit")

+ 2 - 7
services/api-gateway/alembic/versions/20260423_0002_add_api_keys.py

@@ -7,9 +7,8 @@ Create Date: 2026-04-23 20:00:00
 
 from collections.abc import Sequence
 
-from alembic import op
 import sqlalchemy as sa
-
+from alembic import op
 
 revision: str = "20260423_0002"
 down_revision: str | None = "20260423_0001"
@@ -28,23 +27,19 @@ def upgrade() -> None:
         sa.Column("expires_time", sa.DateTime(), nullable=True),
         sa.Column("last_used_time", sa.DateTime(), nullable=True),
         sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("tenant_id", sa.String(length=36), nullable=False),
         sa.Column("created_by", sa.String(length=36), nullable=True),
         sa.Column("updated_by", sa.String(length=36), nullable=True),
         sa.Column("created_time", sa.DateTime(), nullable=False),
         sa.Column("updated_time", sa.DateTime(), nullable=False),
         sa.Column("deleted_time", sa.DateTime(), nullable=True),
         sa.Column("version", sa.Integer(), nullable=False),
-        sa.PrimaryKeyConstraint("id"),
-    )
+        sa.PrimaryKeyConstraint("id"))
     op.create_index("ix_api_key_key_prefix", "api_key", ["key_prefix"], unique=False)
     op.create_index("ix_api_key_key_hash", "api_key", ["key_hash"], unique=True)
     op.create_index("ix_api_key_status", "api_key", ["status"], unique=False)
-    op.create_index("ix_api_key_tenant_id", "api_key", ["tenant_id"], unique=False)
 
 
 def downgrade() -> None:
-    op.drop_index("ix_api_key_tenant_id", table_name="api_key")
     op.drop_index("ix_api_key_status", table_name="api_key")
     op.drop_index("ix_api_key_key_hash", table_name="api_key")
     op.drop_index("ix_api_key_key_prefix", table_name="api_key")

+ 93 - 190
services/api-gateway/app/api/routes.py

@@ -1,10 +1,10 @@
 import asyncio
 
+from core_domain import ServiceDescriptor, ServiceHealth
 from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
 from sqlalchemy import text
 from sqlalchemy.orm import Session
 
-from core_domain import ServiceDescriptor, ServiceHealth
 from app.bootstrap.settings import ApiGatewaySettings
 from app.db.session import get_db
 from app.domain.repositories import ApiKeyRepository, GatewayRequestAuditRepository
@@ -15,9 +15,9 @@ from app.schemas.gateway import (
     ApiKeyCreateResponse,
     ApiKeyResponse,
     ApiKeyStatusUpdateRequest,
-    GatewayRequestAuditResponse,
     GatewayAuditServiceStats,
     GatewayAuditStatsResponse,
+    GatewayRequestAuditResponse,
     GatewayServicesHealthResponse,
 )
 
@@ -39,38 +39,31 @@ def readiness_check(db: Session = Depends(get_db)) -> ServiceHealth:
 @router.post("/gateway/api-keys", response_model=ApiKeyCreateResponse)
 def create_api_key(
     payload: ApiKeyCreateRequest,
-    db: Session = Depends(get_db),
-) -> ApiKeyCreateResponse:
+    db: Session = Depends(get_db)) -> ApiKeyCreateResponse:
     api_key = generate_api_key()
     entity = ApiKeyRepository(db).create(
-        tenant_id=payload.tenant_id,
         name=payload.name,
         key_prefix=get_api_key_prefix(api_key),
         key_hash=hash_api_key(api_key),
         scopes=payload.scopes,
-        expires_time=payload.expires_time,
-    )
+        expires_time=payload.expires_time)
     return ApiKeyCreateResponse(
         id=entity.id,
-        tenant_id=entity.tenant_id,
         name=entity.name,
         key_prefix=entity.key_prefix,
         api_key=api_key,
         status=entity.status,
         scopes=entity.scopes,
         expires_time=entity.expires_time,
-        created_time=entity.created_time,
-    )
+        created_time=entity.created_time)
 
 
 @router.get("/gateway/api-keys", response_model=list[ApiKeyResponse])
 def list_api_keys(
-    tenant_id: str = Query(...),
-    db: Session = Depends(get_db),
-) -> list[ApiKeyResponse]:
+    db: Session = Depends(get_db)) -> list[ApiKeyResponse]:
     return [
         ApiKeyResponse.from_entity(item)
-        for item in ApiKeyRepository(db).list_by_tenant(tenant_id=tenant_id)
+        for item in ApiKeyRepository(db).list_all()
     ]
 
 
@@ -78,13 +71,10 @@ def list_api_keys(
 def update_api_key_status(
     api_key_id: str,
     payload: ApiKeyStatusUpdateRequest,
-    db: Session = Depends(get_db),
-) -> ApiKeyResponse:
+    db: Session = Depends(get_db)) -> ApiKeyResponse:
     entity = ApiKeyRepository(db).update_status(
-        tenant_id=payload.tenant_id,
         api_key_id=api_key_id,
-        status=payload.status,
-    )
+        status=payload.status)
     if entity is None:
         raise HTTPException(status_code=404, detail=f"api key not found: {api_key_id}")
     return ApiKeyResponse.from_entity(entity)
@@ -92,42 +82,33 @@ def update_api_key_status(
 
 @router.get("/gateway/audits", response_model=list[GatewayRequestAuditResponse])
 def list_gateway_audits(
-    tenant_id: str = Query(...),
     request_id: str | None = Query(default=None),
     target_service: str | None = Query(default=None),
     limit: int = Query(default=100, ge=1, le=500),
-    db: Session = Depends(get_db),
-) -> list[GatewayRequestAuditResponse]:
+    db: Session = Depends(get_db)) -> list[GatewayRequestAuditResponse]:
     items = GatewayRequestAuditRepository(db).list_by_scope(
-        tenant_id=tenant_id,
         request_id=request_id,
         target_service=target_service,
-        limit=limit,
-    )
+        limit=limit)
     return [GatewayRequestAuditResponse.from_entity(item) for item in items]
 
 
 @router.get("/gateway/audits/stats", response_model=GatewayAuditStatsResponse)
 def gateway_audit_stats(
-    tenant_id: str = Query(...),
-    db: Session = Depends(get_db),
-) -> GatewayAuditStatsResponse:
-    rows = GatewayRequestAuditRepository(db).stats_by_service(tenant_id=tenant_id)
+    db: Session = Depends(get_db)) -> GatewayAuditStatsResponse:
+    rows = GatewayRequestAuditRepository(db).stats_by_service()
     services = [
         GatewayAuditServiceStats(
             target_service=target_service,
             request_count=request_count,
             error_count=error_count,
-            average_duration_ms=round(average_duration_ms, 2),
-        )
+            average_duration_ms=round(average_duration_ms, 2))
         for target_service, request_count, error_count, average_duration_ms in rows
     ]
     return GatewayAuditStatsResponse(
-        tenant_id=tenant_id,
         total_request_count=sum(item.request_count for item in services),
         total_error_count=sum(item.error_count for item in services),
-        services=services,
-    )
+        services=services)
 
 
 def get_gateway_settings() -> ApiGatewaySettings:
@@ -144,424 +125,346 @@ def build_proxy_targets(settings: ApiGatewaySettings) -> dict[ProxyServiceName,
             service_name="workflow-service",
             base_url=settings.workflow_service_url,
             path_prefix="/workflows",
-            health_path="/workflows/health",
-        ),
+            health_path="/workflows/health"),
         "session-service": ProxyTarget(
             service_name="session-service",
             base_url=settings.session_service_url,
             path_prefix="/sessions",
-            health_path="/sessions/health",
-        ),
+            health_path="/sessions/health"),
         "runtime-service": ProxyTarget(
             service_name="runtime-service",
             base_url=settings.runtime_service_url,
             path_prefix="/runtime",
-            health_path="/runtime/health",
-        ),
+            health_path="/runtime/health"),
         "tool-service": ProxyTarget(
             service_name="tool-service",
             base_url=settings.tool_service_url,
             path_prefix="/tools",
-            health_path="/tools/health",
-        ),
+            health_path="/tools/health"),
         "model-gateway-service": ProxyTarget(
             service_name="model-gateway-service",
             base_url=settings.model_gateway_service_url,
             path_prefix="/models",
-            health_path="/models/health",
-        ),
+            health_path="/models/health"),
         "code-runner-service": ProxyTarget(
             service_name="code-runner-service",
             base_url=settings.code_runner_service_url,
             path_prefix="/code",
-            health_path="/code/health",
-        ),
+            health_path="/code/health"),
         "agent-service": ProxyTarget(
             service_name="agent-service",
             base_url=settings.agent_service_url,
             path_prefix="/agents",
-            health_path="/agents/health",
-        ),
+            health_path="/agents/health"),
         "memory-service": ProxyTarget(
             service_name="memory-service",
             base_url=settings.memory_service_url,
             path_prefix="/memories",
-            health_path="/memories/health",
-        ),
+            health_path="/memories/health"),
         "team-service": ProxyTarget(
             service_name="team-service",
             base_url=settings.team_service_url,
             path_prefix="/teams",
-            health_path="/teams/health",
-        ),
+            health_path="/teams/health"),
         "skill-service": ProxyTarget(
             service_name="skill-service",
             base_url=settings.skill_service_url,
             path_prefix="/skills",
-            health_path="/skills/health",
-        ),
+            health_path="/skills/health"),
         "human-service": ProxyTarget(
             service_name="human-service",
             base_url=settings.human_service_url,
             path_prefix="/human",
-            health_path="/human/health",
-        ),
+            health_path="/human/health"),
         "knowledge-service": ProxyTarget(
             service_name="knowledge-service",
             base_url=settings.knowledge_service_url,
             path_prefix="/knowledge",
-            health_path="/knowledge/health",
-        ),
+            health_path="/knowledge/health"),
         "event-service": ProxyTarget(
             service_name="event-service",
             base_url=settings.event_service_url,
             path_prefix="/events",
-            health_path="/events/health",
-        ),
+            health_path="/events/health"),
         "auth-service": ProxyTarget(
             service_name="auth-service",
             base_url=settings.auth_service_url,
             path_prefix="/auth",
-            health_path="/auth/health",
-        ),
+            health_path="/auth/health"),
         "scheduler-service": ProxyTarget(
             service_name="scheduler-service",
             base_url=settings.scheduler_service_url,
             path_prefix="/scheduler",
-            health_path="/scheduler/health",
-        ),
+            health_path="/scheduler/health"),
     }
 
 
 @router.get("/gateway/services/health", response_model=GatewayServicesHealthResponse)
 async def downstream_health_check(
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-) -> GatewayServicesHealthResponse:
+    settings: ApiGatewaySettings = Depends(get_gateway_settings)) -> GatewayServicesHealthResponse:
     targets = build_proxy_targets(settings)
     health_proxy = ServiceProxy(
         settings=settings,
-        timeout_seconds=settings.downstream_health_timeout_seconds,
-    )
+        timeout_seconds=settings.downstream_health_timeout_seconds)
     downstream_services = await asyncio.gather(
         *[health_proxy.check_health(target) for target in targets.values()]
     )
     status = "ok" if all(item.status == "ok" for item in downstream_services) else "degraded"
     return GatewayServicesHealthResponse(
         status=status,
-        downstream_services=downstream_services,
-    )
+        downstream_services=downstream_services)
 
 
 @router.api_route(
     "/gateway/workflows",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/workflows/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_workflow_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["workflow-service"],
-        path=path,
-    )
+        path=path)
 
 
 @router.api_route(
     "/gateway/sessions",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/sessions/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_session_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["session-service"],
-        path=path,
-    )
+        path=path)
 
 
 @router.api_route(
     "/gateway/runtime",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/runtime/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_runtime_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["runtime-service"],
-        path=path,
-    )
+        path=path)
 
 
 @router.api_route(
     "/gateway/agents",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/agents/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_agent_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["agent-service"],
-        path=path,
-    )
+        path=path)
 
 
 @router.api_route(
     "/gateway/memories",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/memories/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_memory_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["memory-service"],
-        path=path,
-    )
+        path=path)
 
 
 @router.api_route(
     "/gateway/teams",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/teams/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_team_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["team-service"],
-        path=path,
-    )
+        path=path)
 
 
 @router.api_route(
     "/gateway/skills",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/skills/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_skill_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["skill-service"],
-        path=path,
-    )
+        path=path)
 
 
 @router.api_route(
     "/gateway/human",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/human/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_human_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["human-service"],
-        path=path,
-    )
+        path=path)
 
 
 @router.api_route(
     "/gateway/knowledge",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/knowledge/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_knowledge_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["knowledge-service"],
-        path=path,
-    )
+        path=path)
 
 
 @router.api_route(
     "/gateway/events",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/events/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_event_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["event-service"],
-        path=path,
-    )
+        path=path)
 
 
 @router.api_route(
     "/gateway/auth",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/auth/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_auth_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["auth-service"],
-        path=path,
-    )
+        path=path)
 
 
 @router.api_route(
     "/gateway/scheduler",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/scheduler/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_scheduler_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["scheduler-service"],
-        path=path,
-    )
+        path=path)
 
 
 @router.api_route(
     "/gateway/tools",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/tools/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_tool_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["tool-service"],
-        path=path,
-    )
+        path=path)
 
 
 @router.api_route(
     "/gateway/models",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/models/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_model_gateway_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["model-gateway-service"],
-        path=path,
-    )
+        path=path)
 
 
 @router.api_route(
     "/gateway/code",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 @router.api_route(
     "/gateway/code/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
-)
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_code_runner_service(
     request: Request,
     path: str = "",
     settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy),
-) -> Response:
+    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["code-runner-service"],
-        path=path,
-    )
+        path=path)

+ 3 - 4
services/api-gateway/app/bootstrap/app.py

@@ -1,7 +1,7 @@
-from fastapi import FastAPI
-from core_shared.observability import add_observability
 from core_shared import try_build_redis_client
+from core_shared.observability import add_observability
 from core_shared.rate_limit import build_rate_limiter
+from fastapi import FastAPI
 
 from app.api.routes import router
 from app.bootstrap.settings import ApiGatewaySettings
@@ -13,8 +13,7 @@ def create_app() -> FastAPI:
     settings = ApiGatewaySettings()
     app = FastAPI(
         title="agent-platform api-gateway",
-        version="0.1.0",
-    )
+        version="0.1.0")
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     app.state.rate_limiter = build_rate_limiter(try_build_redis_client(settings.redis_url))

+ 1 - 1
services/api-gateway/app/bootstrap/settings.py

@@ -28,5 +28,5 @@ class ApiGatewaySettings(ServiceSettings):
     user_id_header_name: str = "x-user-id"
     authz_timeout_seconds: float = 2.0
     rate_limit_enabled: bool = False
-    tenant_rate_limit_per_minute: int = 600
+    global_rate_limit_per_minute: int = 600
     api_key_rate_limit_per_minute: int = 1200

+ 2 - 3
services/api-gateway/app/db/models/api_key.py

@@ -1,12 +1,11 @@
 from datetime import datetime
 
+from core_db import AuditMixin, Base, EntityMixin, VersionMixin
 from sqlalchemy import DateTime, String, Text
 from sqlalchemy.orm import Mapped, mapped_column
 
-from core_db import AuditMixin, Base, TenantMixin, VersionMixin
 
-
-class ApiKey(TenantMixin, AuditMixin, VersionMixin, Base):
+class ApiKey(EntityMixin, AuditMixin, VersionMixin, Base):
     __tablename__ = "api_key"
 
     name: Mapped[str] = mapped_column(String(128))

+ 2 - 3
services/api-gateway/app/db/models/gateway_request_audit.py

@@ -1,10 +1,9 @@
+from core_db import AuditMixin, Base, EntityMixin, VersionMixin
 from sqlalchemy import Integer, String, Text
 from sqlalchemy.orm import Mapped, mapped_column
 
-from core_db import AuditMixin, Base, TenantMixin, VersionMixin
 
-
-class GatewayRequestAudit(TenantMixin, AuditMixin, VersionMixin, Base):
+class GatewayRequestAudit(EntityMixin, AuditMixin, VersionMixin, Base):
     __tablename__ = "gateway_request_audit"
 
     request_id: Mapped[str] = mapped_column(String(64), index=True)

+ 3 - 6
services/api-gateway/app/db/session.py

@@ -1,21 +1,18 @@
 from collections.abc import Generator
 
+from core_db import DatabaseSettings, create_engine_from_settings, create_session_factory
 from fastapi import Request
 from sqlalchemy.orm import Session, sessionmaker
 
-from core_db import DatabaseSettings, create_engine_from_settings, create_session_factory
-
 from app.bootstrap.settings import ApiGatewaySettings
 
 
 def build_session_factory(
-    settings: ApiGatewaySettings | None = None,
-) -> sessionmaker[Session]:
+    settings: ApiGatewaySettings | None = None) -> sessionmaker[Session]:
     resolved_settings = settings or ApiGatewaySettings()
     db_settings = DatabaseSettings(
         database_url=resolved_settings.database_url,
-        echo_sql=resolved_settings.echo_sql,
-    )
+        echo_sql=resolved_settings.echo_sql)
     engine = create_engine_from_settings(db_settings)
     return create_session_factory(engine)
 

+ 14 - 32
services/api-gateway/app/domain/repositories.py

@@ -13,7 +13,6 @@ class GatewayRequestAuditRepository:
     def create(
         self,
         *,
-        tenant_id: str,
         request_id: str,
         method: str,
         path: str,
@@ -24,10 +23,8 @@ class GatewayRequestAuditRepository:
         duration_ms: int,
         client_host: str | None,
         user_agent: str | None,
-        error_message: str | None,
-    ) -> GatewayRequestAudit:
+        error_message: str | None) -> GatewayRequestAudit:
         entity = GatewayRequestAudit(
-            tenant_id=tenant_id,
             request_id=request_id,
             method=method,
             path=path,
@@ -38,8 +35,7 @@ class GatewayRequestAuditRepository:
             duration_ms=duration_ms,
             client_host=client_host,
             user_agent=user_agent,
-            error_message=error_message,
-        )
+            error_message=error_message)
         self.db.add(entity)
         self.db.commit()
         self.db.refresh(entity)
@@ -48,12 +44,10 @@ class GatewayRequestAuditRepository:
     def list_by_scope(
         self,
         *,
-        tenant_id: str,
         request_id: str | None = None,
         target_service: str | None = None,
-        limit: int = 100,
-    ) -> list[GatewayRequestAudit]:
-        stmt = select(GatewayRequestAudit).where(GatewayRequestAudit.tenant_id == tenant_id)
+        limit: int = 100) -> list[GatewayRequestAudit]:
+        stmt = select(GatewayRequestAudit)
         if request_id is not None:
             stmt = stmt.where(GatewayRequestAudit.request_id == request_id)
         if target_service is not None:
@@ -61,22 +55,19 @@ class GatewayRequestAuditRepository:
         stmt = stmt.order_by(GatewayRequestAudit.created_time.desc()).limit(limit)
         return list(self.db.scalars(stmt))
 
-    def stats_by_service(self, *, tenant_id: str) -> list[tuple[str, int, int, float]]:
+    def stats_by_service(self) -> list[tuple[str, int, int, float]]:
         target_service = func.coalesce(GatewayRequestAudit.target_service, "api-gateway")
         error_count = func.sum(
             case(
                 (GatewayRequestAudit.status_code >= 400, 1),
-                else_=0,
-            )
+                else_=0)
         )
         stmt = (
             select(
                 target_service.label("target_service"),
                 func.count(GatewayRequestAudit.id),
                 error_count,
-                func.avg(GatewayRequestAudit.duration_ms),
-            )
-            .where(GatewayRequestAudit.tenant_id == tenant_id)
+                func.avg(GatewayRequestAudit.duration_ms))
             .group_by(target_service)
             .order_by(target_service.asc())
         )
@@ -86,8 +77,7 @@ class GatewayRequestAuditRepository:
                 str(row[0]),
                 int(row[1] or 0),
                 int(row[2] or 0),
-                float(row[3] or 0.0),
-            )
+                float(row[3] or 0.0))
             for row in rows
         ]
 
@@ -99,31 +89,26 @@ class ApiKeyRepository:
     def create(
         self,
         *,
-        tenant_id: str,
         name: str,
         key_prefix: str,
         key_hash: str,
         scopes: str | None,
-        expires_time: datetime | None,
-    ) -> ApiKey:
+        expires_time: datetime | None) -> ApiKey:
         entity = ApiKey(
-            tenant_id=tenant_id,
             name=name,
             key_prefix=key_prefix,
             key_hash=key_hash,
             status="active",
             scopes=scopes,
-            expires_time=expires_time,
-        )
+            expires_time=expires_time)
         self.db.add(entity)
         self.db.commit()
         self.db.refresh(entity)
         return entity
 
-    def list_by_tenant(self, *, tenant_id: str) -> list[ApiKey]:
+    def list_all(self) -> list[ApiKey]:
         stmt = (
             select(ApiKey)
-            .where(ApiKey.tenant_id == tenant_id)
             .order_by(ApiKey.created_time.desc())
         )
         return list(self.db.scalars(stmt))
@@ -132,10 +117,9 @@ class ApiKeyRepository:
         stmt = select(ApiKey.id).limit(1)
         return self.db.scalar(stmt) is not None
 
-    def get_by_id(self, *, tenant_id: str, api_key_id: str) -> ApiKey | None:
+    def get_by_id(self, *, api_key_id: str) -> ApiKey | None:
         stmt = (
             select(ApiKey)
-            .where(ApiKey.tenant_id == tenant_id)
             .where(ApiKey.id == api_key_id)
             .limit(1)
         )
@@ -160,11 +144,9 @@ class ApiKeyRepository:
     def update_status(
         self,
         *,
-        tenant_id: str,
         api_key_id: str,
-        status: str,
-    ) -> ApiKey | None:
-        entity = self.get_by_id(tenant_id=tenant_id, api_key_id=api_key_id)
+        status: str) -> ApiKey | None:
+        entity = self.get_by_id(api_key_id=api_key_id)
         if entity is None:
             return None
         entity.status = status

+ 0 - 1
services/api-gateway/app/infrastructure/api_keys.py

@@ -1,7 +1,6 @@
 import hashlib
 import secrets
 
-
 API_KEY_PREFIX = "agp"
 
 

+ 3 - 7
services/api-gateway/app/infrastructure/audit.py

@@ -11,8 +11,7 @@ def mark_gateway_target(
     request: Request,
     *,
     target_service: str,
-    target_url: str,
-) -> None:
+    target_url: str) -> None:
     context = get_gateway_request_context(request)
     context.target_service = target_service
     context.target_url = target_url
@@ -23,8 +22,7 @@ def persist_gateway_audit(
     request: Request,
     session_factory: sessionmaker,
     status_code: int | None,
-    error_message: str | None = None,
-) -> None:
+    error_message: str | None = None) -> None:
     context = get_gateway_request_context(request)
     duration_ms = int((perf_counter() - context.started_perf_counter) * 1000)
     client_host = request.client.host if request.client is not None else None
@@ -34,7 +32,6 @@ def persist_gateway_audit(
     db = session_factory()
     try:
         GatewayRequestAuditRepository(db).create(
-            tenant_id=context.tenant_id,
             request_id=context.request_id,
             method=request.method,
             path=request.url.path,
@@ -45,7 +42,6 @@ def persist_gateway_audit(
             duration_ms=duration_ms,
             client_host=client_host,
             user_agent=user_agent,
-            error_message=error_message,
-        )
+            error_message=error_message)
     finally:
         db.close()

+ 10 - 23
services/api-gateway/app/infrastructure/proxy.py

@@ -2,17 +2,13 @@ from dataclasses import dataclass
 from typing import Literal
 
 import httpx
-from fastapi import Request, Response
 from core_shared.observability import PARENT_SPAN_ID_HEADER, SPAN_ID_HEADER, TRACE_ID_HEADER
 from core_shared.security import build_internal_service_headers
+from fastapi import Request, Response
 
-from app.infrastructure.audit import mark_gateway_target
 from app.bootstrap.settings import ApiGatewaySettings
-from app.infrastructure.request_context import (
-    REQUEST_ID_HEADER,
-    TENANT_ID_HEADER,
-    get_gateway_request_context,
-)
+from app.infrastructure.audit import mark_gateway_target
+from app.infrastructure.request_context import REQUEST_ID_HEADER, get_gateway_request_context
 from app.schemas.gateway import DownstreamServiceHealth
 
 ProxyServiceName = Literal[
@@ -52,18 +48,15 @@ class ServiceProxy:
         *,
         request: Request,
         target: ProxyTarget,
-        path: str,
-    ) -> Response:
+        path: str) -> Response:
         target_url = build_target_url(target=target, path=path)
         mark_gateway_target(
             request,
             target_service=target.service_name,
-            target_url=target_url,
-        )
+            target_url=target_url)
         headers = build_forward_headers(request)
         request_context = get_gateway_request_context(request)
         headers[REQUEST_ID_HEADER] = request_context.request_id
-        headers[TENANT_ID_HEADER] = request_context.tenant_id
         trace_id = getattr(request.state, "trace_id", None)
         span_id = getattr(request.state, "span_id", None)
         if isinstance(trace_id, str):
@@ -79,15 +72,13 @@ class ServiceProxy:
                 url=target_url,
                 params=request.query_params,
                 headers=headers,
-                content=body,
-            )
+                content=body)
 
         return Response(
             content=upstream_response.content,
             status_code=upstream_response.status_code,
             headers=build_response_headers(upstream_response),
-            media_type=upstream_response.headers.get("content-type"),
-        )
+            media_type=upstream_response.headers.get("content-type"))
 
     async def check_health(self, target: ProxyTarget) -> DownstreamServiceHealth:
         health_url = f"{target.base_url.rstrip('/')}{target.health_path}"
@@ -95,23 +86,20 @@ class ServiceProxy:
             async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
                 response = await client.get(
                     health_url,
-                    headers=build_internal_service_headers(self.settings),
-                )
+                    headers=build_internal_service_headers(self.settings))
         except httpx.HTTPError as exc:
             return DownstreamServiceHealth(
                 service=target.service_name,
                 status="error",
                 url=health_url,
-                error_message=str(exc),
-            )
+                error_message=str(exc))
 
         return DownstreamServiceHealth(
             service=target.service_name,
             status="ok" if response.is_success else "error",
             url=health_url,
             status_code=response.status_code,
-            error_message=None if response.is_success else response.text,
-        )
+            error_message=None if response.is_success else response.text)
 
 
 def build_target_url(*, target: ProxyTarget, path: str) -> str:
@@ -127,7 +115,6 @@ def build_forward_headers(request: Request) -> dict[str, str]:
         "content-length",
         "connection",
         REQUEST_ID_HEADER,
-        TENANT_ID_HEADER,
         "x-internal-service-token",
         "x-internal-service-name",
         TRACE_ID_HEADER,

+ 13 - 21
services/api-gateway/app/infrastructure/rate_limit.py

@@ -1,13 +1,11 @@
 from __future__ import annotations
 
+from core_shared.rate_limit import RateLimitDecision, RateLimiter
 from fastapi import Request, Response
 from starlette.responses import JSONResponse
 
-from core_shared.rate_limit import RateLimitDecision, RateLimiter
-
 from app.bootstrap.settings import ApiGatewaySettings
 
-
 RATE_LIMIT_WINDOW_SECONDS = 60
 RATE_LIMIT_LIMIT_HEADER = "x-ratelimit-limit"
 RATE_LIMIT_REMAINING_HEADER = "x-ratelimit-remaining"
@@ -18,33 +16,30 @@ def enforce_gateway_rate_limit(
     *,
     request: Request,
     settings: ApiGatewaySettings,
-    limiter: RateLimiter,
-) -> Response | None:
+    limiter: RateLimiter) -> Response | None:
     if not settings.rate_limit_enabled:
         return None
     if not request.url.path.startswith("/gateway/"):
         return None
 
-    from app.infrastructure.request_context import get_gateway_request_context
-
-    context = get_gateway_request_context(request)
     checks: list[RateLimitDecision] = []
-    tenant_limit = max(settings.tenant_rate_limit_per_minute, 1)
+    global_limit = max(settings.global_rate_limit_per_minute, 1)
     checks.append(
         limiter.check(
-            key=f"tenant:{context.tenant_id}",
-            limit=tenant_limit,
-            window_seconds=RATE_LIMIT_WINDOW_SECONDS,
-        )
+            key="global",
+            limit=global_limit,
+            window_seconds=RATE_LIMIT_WINDOW_SECONDS)
     )
+    from app.infrastructure.request_context import get_gateway_request_context
+
+    context = get_gateway_request_context(request)
     if context.api_key_id is not None:
         api_key_limit = max(settings.api_key_rate_limit_per_minute, 1)
         checks.append(
             limiter.check(
                 key=f"api-key:{context.api_key_id}",
                 limit=api_key_limit,
-                window_seconds=RATE_LIMIT_WINDOW_SECONDS,
-            )
+                window_seconds=RATE_LIMIT_WINDOW_SECONDS)
         )
 
     denied = next((item for item in checks if not item.allowed), None)
@@ -52,8 +47,7 @@ def enforce_gateway_rate_limit(
         if checks:
             request.state.gateway_rate_limit_decision = min(
                 checks,
-                key=lambda item: item.remaining,
-            )
+                key=lambda item: item.remaining)
         return None
 
     response = JSONResponse(
@@ -62,8 +56,7 @@ def enforce_gateway_rate_limit(
             "detail": "rate limit exceeded",
             "limit": denied.limit,
             "reset_epoch_seconds": denied.reset_epoch_seconds,
-        },
-    )
+        })
     apply_rate_limit_headers(response, denied)
     return response
 
@@ -73,8 +66,7 @@ def apply_gateway_rate_limit_headers(
     response: Response,
     request: Request,
     settings: ApiGatewaySettings,
-    limiter: RateLimiter,
-) -> None:
+    limiter: RateLimiter) -> None:
     if not settings.rate_limit_enabled:
         return
     if not request.url.path.startswith("/gateway/"):

+ 90 - 73
services/api-gateway/app/infrastructure/request_context.py

@@ -4,10 +4,10 @@ from time import perf_counter
 from uuid import uuid4
 
 import httpx
+from core_shared.security import build_internal_service_headers
 from fastapi import Request, Response
-from starlette.responses import JSONResponse
 from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
-from core_shared.security import build_internal_service_headers
+from starlette.responses import JSONResponse
 
 from app.bootstrap.settings import ApiGatewaySettings
 from app.domain.repositories import ApiKeyRepository
@@ -18,14 +18,11 @@ from app.infrastructure.rate_limit import (
 )
 
 REQUEST_ID_HEADER = "x-request-id"
-TENANT_ID_HEADER = "x-tenant-id"
-DEFAULT_TENANT_ID = "public"
 
 
 @dataclass
 class GatewayRequestContext:
     request_id: str
-    tenant_id: str
     started_perf_counter: float
     api_key_id: str | None = None
     user_id: str | None = None
@@ -37,15 +34,11 @@ class GatewayRequestContextMiddleware(BaseHTTPMiddleware):
     async def dispatch(
         self,
         request: Request,
-        call_next: RequestResponseEndpoint,
-    ) -> Response:
+        call_next: RequestResponseEndpoint) -> Response:
         request_id = request.headers.get(REQUEST_ID_HEADER) or str(uuid4())
-        tenant_id = resolve_tenant_id(request)
         request.state.gateway_context = GatewayRequestContext(
             request_id=request_id,
-            tenant_id=tenant_id,
-            started_perf_counter=perf_counter(),
-        )
+            started_perf_counter=perf_counter())
         auth_response = authenticate_gateway_request(request)
         if auth_response is not None:
             from app.infrastructure.audit import persist_gateway_audit
@@ -54,19 +47,16 @@ class GatewayRequestContextMiddleware(BaseHTTPMiddleware):
                 request=request,
                 session_factory=request.app.state.session_factory,
                 status_code=auth_response.status_code,
-                error_message=None,
-            )
+                error_message=None)
             context = get_gateway_request_context(request)
             auth_response.headers[REQUEST_ID_HEADER] = request_id
-            auth_response.headers[TENANT_ID_HEADER] = context.tenant_id
             return auth_response
 
         settings = request.app.state.settings
         rate_limit_response = enforce_gateway_rate_limit(
             request=request,
             settings=settings,
-            limiter=request.app.state.rate_limiter,
-        )
+            limiter=request.app.state.rate_limiter)
         if rate_limit_response is not None:
             from app.infrastructure.audit import persist_gateway_audit
 
@@ -74,11 +64,9 @@ class GatewayRequestContextMiddleware(BaseHTTPMiddleware):
                 request=request,
                 session_factory=request.app.state.session_factory,
                 status_code=rate_limit_response.status_code,
-                error_message="rate limit exceeded",
-            )
+                error_message="rate limit exceeded")
             context = get_gateway_request_context(request)
             rate_limit_response.headers[REQUEST_ID_HEADER] = request_id
-            rate_limit_response.headers[TENANT_ID_HEADER] = context.tenant_id
             return rate_limit_response
 
         try:
@@ -90,8 +78,7 @@ class GatewayRequestContextMiddleware(BaseHTTPMiddleware):
                 request=request,
                 session_factory=request.app.state.session_factory,
                 status_code=500,
-                error_message=str(exc),
-            )
+                error_message=str(exc))
             raise
 
         from app.infrastructure.audit import persist_gateway_audit
@@ -99,41 +86,24 @@ class GatewayRequestContextMiddleware(BaseHTTPMiddleware):
         persist_gateway_audit(
             request=request,
             session_factory=request.app.state.session_factory,
-            status_code=response.status_code,
-        )
+            status_code=response.status_code)
         context = get_gateway_request_context(request)
         response.headers[REQUEST_ID_HEADER] = request_id
-        response.headers[TENANT_ID_HEADER] = context.tenant_id
         apply_gateway_rate_limit_headers(
             response=response,
             request=request,
             settings=settings,
-            limiter=request.app.state.rate_limiter,
-        )
+            limiter=request.app.state.rate_limiter)
         return response
 
 
-def resolve_tenant_id(request: Request) -> str:
-    header_tenant_id = request.headers.get(TENANT_ID_HEADER)
-    if header_tenant_id:
-        return header_tenant_id
-
-    query_tenant_id = request.query_params.get("tenant_id")
-    if query_tenant_id:
-        return query_tenant_id
-
-    return DEFAULT_TENANT_ID
-
-
 def get_gateway_request_context(request: Request) -> GatewayRequestContext:
     context = getattr(request.state, "gateway_context", None)
     if isinstance(context, GatewayRequestContext):
         return context
     return GatewayRequestContext(
         request_id=str(uuid4()),
-        tenant_id=DEFAULT_TENANT_ID,
-        started_perf_counter=perf_counter(),
-    )
+        started_perf_counter=perf_counter())
 
 
 def authenticate_gateway_request(request: Request) -> Response | None:
@@ -144,16 +114,21 @@ def authenticate_gateway_request(request: Request) -> Response | None:
         return None
     if request.url.path in {"/gateway/services/health"}:
         return None
+    if is_auth_login_request(request):
+        return None
 
     if is_initial_api_key_bootstrap_request(request):
         return None
 
+    bearer_token = get_bearer_token(request)
+    if bearer_token is not None:
+        return authenticate_bearer_token(request=request, settings=settings, token=bearer_token)
+
     api_key = request.headers.get(settings.api_key_header_name)
     if not api_key:
         return JSONResponse(
             status_code=401,
-            content={"detail": "missing api key"},
-        )
+            content={"detail": "missing bearer token or api key"})
 
     db = request.app.state.session_factory()
     try:
@@ -161,45 +136,31 @@ def authenticate_gateway_request(request: Request) -> Response | None:
         if entity is None:
             return JSONResponse(
                 status_code=401,
-                content={"detail": "invalid api key"},
-            )
+                content={"detail": "invalid api key"})
         if entity.expires_time is not None and entity.expires_time <= datetime.utcnow():
             return JSONResponse(
                 status_code=401,
-                content={"detail": "api key expired"},
-            )
+                content={"detail": "api key expired"})
 
         context = get_gateway_request_context(request)
-        requested_tenant_id = resolve_tenant_id(request)
-        if requested_tenant_id not in {DEFAULT_TENANT_ID, entity.tenant_id}:
-            return JSONResponse(
-                status_code=403,
-                content={"detail": "api key tenant mismatch"},
-            )
-        context.tenant_id = entity.tenant_id
         context.api_key_id = entity.id
         context.user_id = request.headers.get(settings.user_id_header_name)
         permission = derive_gateway_permission(request)
         if permission is not None and not api_key_scope_allows(
             scopes=entity.scopes,
-            permission=permission,
-        ):
+            permission=permission):
             return JSONResponse(
                 status_code=403,
-                content={"detail": "api key scope denied", "permission": permission},
-            )
+                content={"detail": "api key scope denied", "permission": permission})
         if settings.authz_required:
             if context.user_id is None:
                 return JSONResponse(
                     status_code=401,
-                    content={"detail": "missing user id"},
-                )
+                    content={"detail": "missing user id"})
             authz_response = check_auth_service_permission(
                 settings=settings,
-                tenant_id=entity.tenant_id,
                 user_id=context.user_id,
-                permission=permission or "gateway:access",
-            )
+                permission=permission or "gateway:access")
             if authz_response is not None:
                 return authz_response
         ApiKeyRepository(db).touch_last_used_time(api_key_id=entity.id)
@@ -209,6 +170,10 @@ def authenticate_gateway_request(request: Request) -> Response | None:
     return None
 
 
+def is_auth_login_request(request: Request) -> bool:
+    return request.method.upper() == "POST" and request.url.path == "/gateway/auth/login"
+
+
 def is_initial_api_key_bootstrap_request(request: Request) -> bool:
     if request.method.upper() != "POST" or request.url.path != "/gateway/api-keys":
         return False
@@ -220,6 +185,64 @@ def is_initial_api_key_bootstrap_request(request: Request) -> bool:
         db.close()
 
 
+def get_bearer_token(request: Request) -> str | None:
+    authorization = request.headers.get("authorization")
+    if authorization is None:
+        return None
+    scheme, _, token = authorization.partition(" ")
+    if scheme.lower() != "bearer" or not token.strip():
+        return None
+    return token.strip()
+
+
+def authenticate_bearer_token(
+    *,
+    request: Request,
+    settings: ApiGatewaySettings,
+    token: str) -> Response | None:
+    try:
+        with httpx.Client(timeout=settings.authz_timeout_seconds) as client:
+            response = client.post(
+                f"{settings.auth_service_url.rstrip('/')}/auth/tokens/verify",
+                headers=build_internal_service_headers(settings),
+                json={"access_token": token})
+            response.raise_for_status()
+            payload = response.json()
+    except (httpx.HTTPError, ValueError) as exc:
+        return JSONResponse(
+            status_code=503,
+            content={"detail": "auth token verification failed", "error": str(exc)})
+
+    if payload.get("active") is not True:
+        reason = payload.get("reason")
+        return JSONResponse(
+            status_code=401,
+            content={
+                "detail": "invalid bearer token",
+                "reason": reason if isinstance(reason, str) else "inactive",
+            })
+
+    user_id = payload.get("user_id")
+    if not isinstance(user_id, str):
+        return JSONResponse(
+            status_code=401,
+            content={"detail": "invalid token identity"})
+
+    context = get_gateway_request_context(request)
+    context.user_id = user_id
+
+    if settings.authz_required:
+        permission = derive_gateway_permission(request) or "gateway:access"
+        authz_response = check_auth_service_permission(
+            settings=settings,
+            user_id=user_id,
+            permission=permission)
+        if authz_response is not None:
+            return authz_response
+
+    return None
+
+
 def derive_gateway_permission(request: Request) -> str | None:
     if not request.url.path.startswith("/gateway/"):
         return None
@@ -252,28 +275,23 @@ def parse_scope_values(scopes: str) -> set[str]:
 def check_auth_service_permission(
     *,
     settings: ApiGatewaySettings,
-    tenant_id: str,
     user_id: str,
-    permission: str,
-) -> Response | None:
+    permission: str) -> Response | None:
     try:
         with httpx.Client(timeout=settings.authz_timeout_seconds) as client:
             response = client.post(
                 f"{settings.auth_service_url.rstrip('/')}/auth/permissions/check",
                 headers=build_internal_service_headers(settings),
                 json={
-                    "tenant_id": tenant_id,
                     "user_id": user_id,
                     "permission": permission,
-                },
-            )
+                })
             response.raise_for_status()
             payload = response.json()
     except (httpx.HTTPError, ValueError) as exc:
         return JSONResponse(
             status_code=503,
-            content={"detail": "auth service permission check failed", "error": str(exc)},
-        )
+            content={"detail": "auth service permission check failed", "error": str(exc)})
 
     allowed = payload.get("allowed")
     if allowed is True:
@@ -285,5 +303,4 @@ def check_auth_service_permission(
             "detail": "permission denied",
             "permission": permission,
             "reason": reason if isinstance(reason, str) else "denied",
-        },
-    )
+        })

+ 2 - 9
services/api-gateway/app/schemas/gateway.py

@@ -1,8 +1,7 @@
-from pydantic import BaseModel
 from datetime import datetime
-from typing import Literal
+from typing import TYPE_CHECKING, Literal
 
-from typing import TYPE_CHECKING
+from pydantic import BaseModel
 
 if TYPE_CHECKING:
     from app.db.models import ApiKey, GatewayRequestAudit
@@ -24,7 +23,6 @@ class GatewayServicesHealthResponse(BaseModel):
 
 class GatewayRequestAuditResponse(BaseModel):
     id: str
-    tenant_id: str
     request_id: str
     method: str
     path: str
@@ -51,14 +49,12 @@ class GatewayAuditServiceStats(BaseModel):
 
 
 class GatewayAuditStatsResponse(BaseModel):
-    tenant_id: str
     total_request_count: int
     total_error_count: int
     services: list[GatewayAuditServiceStats]
 
 
 class ApiKeyCreateRequest(BaseModel):
-    tenant_id: str
     name: str
     scopes: str | None = None
     expires_time: datetime | None = None
@@ -66,7 +62,6 @@ class ApiKeyCreateRequest(BaseModel):
 
 class ApiKeyCreateResponse(BaseModel):
     id: str
-    tenant_id: str
     name: str
     key_prefix: str
     api_key: str
@@ -78,7 +73,6 @@ class ApiKeyCreateResponse(BaseModel):
 
 class ApiKeyResponse(BaseModel):
     id: str
-    tenant_id: str
     name: str
     key_prefix: str
     status: str
@@ -96,5 +90,4 @@ ApiKeyStatus = Literal["active", "disabled", "revoked"]
 
 
 class ApiKeyStatusUpdateRequest(BaseModel):
-    tenant_id: str
     status: ApiKeyStatus

+ 4 - 4
services/auth-service/alembic/env.py

@@ -1,11 +1,12 @@
 from logging.config import fileConfig
 
 from alembic import context
-from sqlalchemy import engine_from_config, pool
-
+from app.bootstrap.settings import AuthServiceSettings
 from app.db.models import Base
+from sqlalchemy import engine_from_config, pool
 
 config = context.config
+config.set_main_option("sqlalchemy.url", AuthServiceSettings().database_url)
 
 if config.config_file_name is not None:
     fileConfig(config.config_file_name)
@@ -24,8 +25,7 @@ def run_migrations_online() -> None:
     connectable = engine_from_config(
         config.get_section(config.config_ini_section, {}),
         prefix="sqlalchemy.",
-        poolclass=pool.NullPool,
-    )
+        poolclass=pool.NullPool)
     with connectable.connect() as connection:
         context.configure(connection=connection, target_metadata=target_metadata)
         with context.begin_transaction():

+ 5 - 14
services/auth-service/alembic/versions/20260425_0001_init_auth_models.py

@@ -7,9 +7,8 @@ Create Date: 2026-04-25 23:55:00
 
 from collections.abc import Sequence
 
-from alembic import op
 import sqlalchemy as sa
-
+from alembic import op
 
 revision: str = "20260425_0001"
 down_revision: str | None = None
@@ -27,15 +26,13 @@ def upgrade() -> None:
         sa.Column("metadata_json", sa.JSON(), nullable=False),
         sa.Column("last_login_time", sa.DateTime(), nullable=True),
         sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("tenant_id", sa.String(length=36), nullable=False),
         sa.Column("created_by", sa.String(length=36), nullable=True),
         sa.Column("updated_by", sa.String(length=36), nullable=True),
         sa.Column("created_time", sa.DateTime(), nullable=False),
         sa.Column("updated_time", sa.DateTime(), nullable=False),
         sa.Column("deleted_time", sa.DateTime(), nullable=True),
         sa.Column("version", sa.Integer(), nullable=False),
-        sa.PrimaryKeyConstraint("id"),
-    )
+        sa.PrimaryKeyConstraint("id"))
     op.create_table(
         "auth_role",
         sa.Column("code", sa.String(length=128), nullable=False),
@@ -44,15 +41,13 @@ def upgrade() -> None:
         sa.Column("status", sa.String(length=32), nullable=False),
         sa.Column("permissions_json", sa.JSON(), nullable=False),
         sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("tenant_id", sa.String(length=36), nullable=False),
         sa.Column("created_by", sa.String(length=36), nullable=True),
         sa.Column("updated_by", sa.String(length=36), nullable=True),
         sa.Column("created_time", sa.DateTime(), nullable=False),
         sa.Column("updated_time", sa.DateTime(), nullable=False),
         sa.Column("deleted_time", sa.DateTime(), nullable=True),
         sa.Column("version", sa.Integer(), nullable=False),
-        sa.PrimaryKeyConstraint("id"),
-    )
+        sa.PrimaryKeyConstraint("id"))
     op.create_table(
         "auth_role_assignment",
         sa.Column("user_id", sa.String(length=36), nullable=False),
@@ -62,17 +57,14 @@ def upgrade() -> None:
         sa.Column("scope_id", sa.String(length=64), nullable=True),
         sa.Column("expires_time", sa.DateTime(), nullable=True),
         sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("tenant_id", sa.String(length=36), nullable=False),
         sa.Column("created_by", sa.String(length=36), nullable=True),
         sa.Column("updated_by", sa.String(length=36), nullable=True),
         sa.Column("created_time", sa.DateTime(), nullable=False),
         sa.Column("updated_time", sa.DateTime(), nullable=False),
         sa.Column("deleted_time", sa.DateTime(), nullable=True),
         sa.Column("version", sa.Integer(), nullable=False),
-        sa.PrimaryKeyConstraint("id"),
-    )
+        sa.PrimaryKeyConstraint("id"))
     for table_name in ("auth_user", "auth_role", "auth_role_assignment"):
-        op.create_index(f"ix_{table_name}_tenant_id", table_name, ["tenant_id"])
         op.create_index(f"ix_{table_name}_status", table_name, ["status"])
     op.create_index("ix_auth_user_username", "auth_user", ["username"])
     op.create_index("ix_auth_user_email", "auth_user", ["email"])
@@ -84,8 +76,7 @@ def upgrade() -> None:
     op.create_index(
         "ix_auth_role_assignment_expires_time",
         "auth_role_assignment",
-        ["expires_time"],
-    )
+        ["expires_time"])
 
 
 def downgrade() -> None:

+ 27 - 0
services/auth-service/alembic/versions/20260427_0002_add_user_password_hash.py

@@ -0,0 +1,27 @@
+"""add user password hash
+
+Revision ID: 20260427_0002
+Revises: 20260425_0001
+Create Date: 2026-04-27 23:10:00
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+revision: str = "20260427_0002"
+down_revision: str | None = "20260425_0001"
+branch_labels: Sequence[str] | None = None
+depends_on: Sequence[str] | None = None
+
+
+def upgrade() -> None:
+    op.add_column(
+        "auth_user",
+        sa.Column("password_hash", sa.String(length=512), nullable=False, server_default=""))
+    op.alter_column("auth_user", "password_hash", server_default=None)
+
+
+def downgrade() -> None:
+    op.drop_column("auth_user", "password_hash")

+ 28 - 0
services/auth-service/alembic/versions/20260427_0003_remove_auth_partition_columns.py

@@ -0,0 +1,28 @@
+
+"""remove auth partition columns
+
+Revision ID: 20260427_0003
+Revises: 20260427_0002
+Create Date: 2026-04-27 23:45:00
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+revision: str = "20260427_0003"
+down_revision: str | None = "20260427_0002"
+branch_labels: Sequence[str] | None = None
+depends_on: Sequence[str] | None = None
+
+AUTH_TABLES = ("auth_user", "auth_role", "auth_role_assignment")
+
+
+def upgrade() -> None:
+    # Auth tables no longer carry a workspace partition column.
+    return None
+
+
+def downgrade() -> None:
+    return None

+ 48 - 33
services/auth-service/app/api/routes.py

@@ -1,13 +1,16 @@
-from fastapi import APIRouter, Depends, HTTPException, Query
-from sqlalchemy import text
-from sqlalchemy.orm import Session
+from typing import Annotated
 
 from core_domain import ServiceHealth
+from fastapi import APIRouter, Depends, HTTPException, Query, Request
+from sqlalchemy import text
+from sqlalchemy.orm import Session
 
 from app.application.services import AuthApplicationService
 from app.db.session import get_db
 from app.domain.repositories import RoleAssignmentRepository, RoleRepository, UserRepository
 from app.schemas.auth import (
+    LoginRequest,
+    LoginResponse,
     PermissionCheckRequest,
     PermissionCheckResponse,
     RoleAssignmentCreateRequest,
@@ -16,50 +19,71 @@ from app.schemas.auth import (
     RoleCreateRequest,
     RoleResponse,
     RoleStatusUpdateRequest,
+    TokenVerifyRequest,
+    TokenVerifyResponse,
     UserCreateRequest,
     UserResponse,
     UserStatusUpdateRequest,
 )
 
 router = APIRouter()
+DbSession = Annotated[Session, Depends(get_db)]
+UserIdQuery = Annotated[str, Query(...)]
 
 
-def get_auth_application_service(db: Session = Depends(get_db)) -> AuthApplicationService:
+def get_auth_application_service(request: Request, db: DbSession) -> AuthApplicationService:
+    settings = request.app.state.settings
     return AuthApplicationService(
         user_repository=UserRepository(db),
         role_repository=RoleRepository(db),
         assignment_repository=RoleAssignmentRepository(db),
-    )
+        token_secret=settings.credential_encryption_key)
+
+
+AuthServiceDep = Annotated[AuthApplicationService, Depends(get_auth_application_service)]
 
 
 @router.get("/health", response_model=ServiceHealth)
-def health_check(db: Session = Depends(get_db)) -> ServiceHealth:
+def health_check(db: DbSession) -> ServiceHealth:
     db.execute(text("SELECT 1"))
     return ServiceHealth(service="auth-service", status="ok", database="ok")
 
 
+@router.post("/login", response_model=LoginResponse)
+def login(
+    payload: LoginRequest,
+    service: AuthServiceDep) -> LoginResponse:
+    result = service.login(payload)
+    if result is None:
+        raise HTTPException(status_code=401, detail="invalid username or password")
+    return result
+
+
+@router.post("/tokens/verify", response_model=TokenVerifyResponse)
+def verify_token(
+    payload: TokenVerifyRequest,
+    service: AuthServiceDep) -> TokenVerifyResponse:
+    return service.verify_token(payload)
+
+
 @router.post("/users", response_model=UserResponse)
 def create_user(
     payload: UserCreateRequest,
-    service: AuthApplicationService = Depends(get_auth_application_service),
-) -> UserResponse:
+    service: AuthServiceDep) -> UserResponse:
     return UserResponse.from_entity(service.create_user(payload))
 
 
 @router.get("/users", response_model=list[UserResponse])
 def list_users(
-    tenant_id: str = Query(...),
-    service: AuthApplicationService = Depends(get_auth_application_service),
-) -> list[UserResponse]:
-    return [UserResponse.from_entity(item) for item in service.list_users(tenant_id=tenant_id)]
+    service: AuthServiceDep) -> list[UserResponse]:
+    return [UserResponse.from_entity(item) for item in service.list_users()]
 
 
 @router.patch("/users/{user_id}/status", response_model=UserResponse)
 def update_user_status(
     user_id: str,
     payload: UserStatusUpdateRequest,
-    service: AuthApplicationService = Depends(get_auth_application_service),
-) -> UserResponse:
+    service: AuthServiceDep) -> UserResponse:
     entity = service.update_user_status(user_id=user_id, payload=payload)
     if entity is None:
         raise HTTPException(status_code=404, detail=f"user not found: {user_id}")
@@ -69,25 +93,21 @@ def update_user_status(
 @router.post("/roles", response_model=RoleResponse)
 def create_role(
     payload: RoleCreateRequest,
-    service: AuthApplicationService = Depends(get_auth_application_service),
-) -> RoleResponse:
+    service: AuthServiceDep) -> RoleResponse:
     return RoleResponse.from_entity(service.create_role(payload))
 
 
 @router.get("/roles", response_model=list[RoleResponse])
 def list_roles(
-    tenant_id: str = Query(...),
-    service: AuthApplicationService = Depends(get_auth_application_service),
-) -> list[RoleResponse]:
-    return [RoleResponse.from_entity(item) for item in service.list_roles(tenant_id=tenant_id)]
+    service: AuthServiceDep) -> list[RoleResponse]:
+    return [RoleResponse.from_entity(item) for item in service.list_roles()]
 
 
 @router.patch("/roles/{role_id}/status", response_model=RoleResponse)
 def update_role_status(
     role_id: str,
     payload: RoleStatusUpdateRequest,
-    service: AuthApplicationService = Depends(get_auth_application_service),
-) -> RoleResponse:
+    service: AuthServiceDep) -> RoleResponse:
     entity = service.update_role_status(role_id=role_id, payload=payload)
     if entity is None:
         raise HTTPException(status_code=404, detail=f"role not found: {role_id}")
@@ -97,20 +117,17 @@ def update_role_status(
 @router.post("/assignments", response_model=RoleAssignmentResponse)
 def create_assignment(
     payload: RoleAssignmentCreateRequest,
-    service: AuthApplicationService = Depends(get_auth_application_service),
-) -> RoleAssignmentResponse:
+    service: AuthServiceDep) -> RoleAssignmentResponse:
     return RoleAssignmentResponse.from_entity(service.create_assignment(payload))
 
 
 @router.get("/assignments", response_model=list[RoleAssignmentResponse])
 def list_assignments(
-    tenant_id: str = Query(...),
-    user_id: str = Query(...),
-    service: AuthApplicationService = Depends(get_auth_application_service),
-) -> list[RoleAssignmentResponse]:
+    user_id: UserIdQuery,
+    service: AuthServiceDep) -> list[RoleAssignmentResponse]:
     return [
         RoleAssignmentResponse.from_entity(item)
-        for item in service.list_assignments(tenant_id=tenant_id, user_id=user_id)
+        for item in service.list_assignments(user_id=user_id)
     ]
 
 
@@ -118,8 +135,7 @@ def list_assignments(
 def update_assignment_status(
     assignment_id: str,
     payload: RoleAssignmentStatusUpdateRequest,
-    service: AuthApplicationService = Depends(get_auth_application_service),
-) -> RoleAssignmentResponse:
+    service: AuthServiceDep) -> RoleAssignmentResponse:
     entity = service.update_assignment_status(assignment_id=assignment_id, payload=payload)
     if entity is None:
         raise HTTPException(status_code=404, detail=f"assignment not found: {assignment_id}")
@@ -129,6 +145,5 @@ def update_assignment_status(
 @router.post("/permissions/check", response_model=PermissionCheckResponse)
 def check_permission(
     payload: PermissionCheckRequest,
-    service: AuthApplicationService = Depends(get_auth_application_service),
-) -> PermissionCheckResponse:
+    service: AuthServiceDep) -> PermissionCheckResponse:
     return service.check_permission(payload)

+ 67 - 44
services/auth-service/app/application/services.py

@@ -2,13 +2,20 @@ from datetime import datetime
 
 from app.db.models import Role, RoleAssignment, User
 from app.domain.repositories import RoleAssignmentRepository, RoleRepository, UserRepository
+from app.infrastructure.passwords import hash_password, verify_password
+from app.infrastructure.tokens import TokenError, issue_access_token, verify_access_token
 from app.schemas.auth import (
+    LoginRequest,
+    LoginResponse,
+    LoginUserResponse,
     PermissionCheckRequest,
     PermissionCheckResponse,
     RoleAssignmentCreateRequest,
     RoleAssignmentStatusUpdateRequest,
     RoleCreateRequest,
     RoleStatusUpdateRequest,
+    TokenVerifyRequest,
+    TokenVerifyResponse,
     UserCreateRequest,
     UserStatusUpdateRequest,
 )
@@ -21,89 +28,110 @@ class AuthApplicationService:
         user_repository: UserRepository,
         role_repository: RoleRepository,
         assignment_repository: RoleAssignmentRepository,
-    ) -> None:
+        token_secret: str) -> None:
         self.user_repository = user_repository
         self.role_repository = role_repository
         self.assignment_repository = assignment_repository
+        self.token_secret = token_secret
 
     def create_user(self, payload: UserCreateRequest) -> User:
         return self.user_repository.create(
-            tenant_id=payload.tenant_id,
             username=payload.username,
+            password_hash=hash_password(payload.password) if payload.password else "",
             display_name=payload.display_name,
             email=payload.email,
-            metadata_json=payload.metadata_json,
-        )
+            metadata_json=payload.metadata_json)
+
+    def login(self, payload: LoginRequest) -> LoginResponse | None:
+        user = self.user_repository.get_by_username(
+            username=payload.username)
+        if user is None or user.status != "active":
+            return None
+        if not verify_password(payload.password, user.password_hash):
+            return None
+
+        self.user_repository.touch_last_login_time(user_id=user.id)
+        access_token, expires_time = issue_access_token(
+            user_id=user.id,
+            secret=self.token_secret)
+        return LoginResponse(
+            access_token=access_token,
+            expires_time=expires_time,
+            user=LoginUserResponse.from_entity(user))
+
+    def verify_token(self, payload: TokenVerifyRequest) -> TokenVerifyResponse:
+        try:
+            token_payload = verify_access_token(
+                payload.access_token,
+                secret=self.token_secret)
+        except TokenError as exc:
+            return TokenVerifyResponse(active=False, reason=str(exc))
+
+        expires_time_raw = token_payload["expires_time"]
+        user = self.user_repository.get_by_id(
+            user_id=token_payload["user_id"])
+        if user is None or user.status != "active":
+            return TokenVerifyResponse(active=False, reason="user_not_active")
+
+        return TokenVerifyResponse(
+            active=True,
+            user_id=user.id,
+            username=user.username,
+            expires_time=datetime.fromisoformat(expires_time_raw.removesuffix("Z")))
 
-    def list_users(self, *, tenant_id: str) -> list[User]:
-        return self.user_repository.list_by_tenant(tenant_id=tenant_id)
+    def list_users(self) -> list[User]:
+        return self.user_repository.list_all()
 
     def update_user_status(self, *, user_id: str, payload: UserStatusUpdateRequest) -> User | None:
         return self.user_repository.update_status(
-            tenant_id=payload.tenant_id,
             user_id=user_id,
-            status=payload.status,
-        )
+            status=payload.status)
 
     def create_role(self, payload: RoleCreateRequest) -> Role:
         return self.role_repository.create(
-            tenant_id=payload.tenant_id,
             code=payload.code,
             name=payload.name,
             description=payload.description,
-            permissions_json=payload.permissions_json,
-        )
+            permissions_json=payload.permissions_json)
 
-    def list_roles(self, *, tenant_id: str) -> list[Role]:
-        return self.role_repository.list_by_tenant(tenant_id=tenant_id)
+    def list_roles(self) -> list[Role]:
+        return self.role_repository.list_all()
 
     def update_role_status(self, *, role_id: str, payload: RoleStatusUpdateRequest) -> Role | None:
         return self.role_repository.update_status(
-            tenant_id=payload.tenant_id,
             role_id=role_id,
-            status=payload.status,
-        )
+            status=payload.status)
 
     def create_assignment(
         self,
-        payload: RoleAssignmentCreateRequest,
-    ) -> RoleAssignment:
+        payload: RoleAssignmentCreateRequest) -> RoleAssignment:
         return self.assignment_repository.create(
-            tenant_id=payload.tenant_id,
             user_id=payload.user_id,
             role_id=payload.role_id,
             scope_type=payload.scope_type,
             scope_id=payload.scope_id,
-            expires_time=payload.expires_time,
-        )
+            expires_time=payload.expires_time)
 
-    def list_assignments(self, *, tenant_id: str, user_id: str) -> list[RoleAssignment]:
-        return self.assignment_repository.list_by_user(tenant_id=tenant_id, user_id=user_id)
+    def list_assignments(self, *, user_id: str) -> list[RoleAssignment]:
+        return self.assignment_repository.list_by_user(user_id=user_id)
 
     def update_assignment_status(
         self,
         *,
         assignment_id: str,
-        payload: RoleAssignmentStatusUpdateRequest,
-    ) -> RoleAssignment | None:
+        payload: RoleAssignmentStatusUpdateRequest) -> RoleAssignment | None:
         return self.assignment_repository.update_status(
-            tenant_id=payload.tenant_id,
             assignment_id=assignment_id,
-            status=payload.status,
-        )
+            status=payload.status)
 
     def check_permission(self, payload: PermissionCheckRequest) -> PermissionCheckResponse:
         user = self.user_repository.get_by_id(
-            tenant_id=payload.tenant_id,
-            user_id=payload.user_id,
-        )
+            user_id=payload.user_id)
         if user is None or user.status != "active":
             return PermissionCheckResponse(allowed=False, reason="user_not_active")
 
         assignments = self.assignment_repository.list_by_user(
-            tenant_id=payload.tenant_id,
-            user_id=payload.user_id,
-        )
+            user_id=payload.user_id)
         matched_role_ids: list[str] = []
         now = datetime.utcnow()
         for assignment in assignments:
@@ -114,13 +142,10 @@ class AuthApplicationService:
             if not self._scope_matches(
                 assignment=assignment,
                 scope_type=payload.scope_type,
-                scope_id=payload.scope_id,
-            ):
+                scope_id=payload.scope_id):
                 continue
             role = self.role_repository.get_by_id(
-                tenant_id=payload.tenant_id,
-                role_id=assignment.role_id,
-            )
+                role_id=assignment.role_id)
             if role is None or role.status != "active":
                 continue
             if self._permission_matches(role.permissions_json, payload.permission):
@@ -129,8 +154,7 @@ class AuthApplicationService:
         return PermissionCheckResponse(
             allowed=bool(matched_role_ids),
             reason="matched" if matched_role_ids else "permission_not_found",
-            matched_role_ids=matched_role_ids,
-        )
+            matched_role_ids=matched_role_ids)
 
     def _permission_matches(self, permissions: list[str], requested_permission: str) -> bool:
         if "*" in permissions or requested_permission in permissions:
@@ -146,8 +170,7 @@ class AuthApplicationService:
         *,
         assignment: RoleAssignment,
         scope_type: str | None,
-        scope_id: str | None,
-    ) -> bool:
+        scope_id: str | None) -> bool:
         if assignment.scope_type is None and assignment.scope_id is None:
             return True
         return assignment.scope_type == scope_type and assignment.scope_id == scope_id

+ 2 - 3
services/auth-service/app/bootstrap/app.py

@@ -1,6 +1,6 @@
-from fastapi import FastAPI
 from core_shared.observability import add_observability
 from core_shared.security import add_internal_service_auth
+from fastapi import FastAPI
 
 from app.api.routes import router
 from app.bootstrap.settings import AuthServiceSettings
@@ -11,8 +11,7 @@ def create_app() -> FastAPI:
     settings = AuthServiceSettings()
     app = FastAPI(
         title="agent-platform auth-service",
-        version="0.1.0",
-    )
+        version="0.1.0")
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)

+ 3 - 1
services/auth-service/app/bootstrap/settings.py

@@ -4,4 +4,6 @@ from core_shared import ServiceSettings
 class AuthServiceSettings(ServiceSettings):
     service_name: str = "auth-service"
     service_port: int = 8014
-    database_url: str = "sqlite:///./auth_service.db"
+    database_url: str = (
+        "postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb"
+    )

+ 6 - 5
services/auth-service/app/db/models/role.py

@@ -1,13 +1,14 @@
-from sqlalchemy import String, Text
-from sqlalchemy.dialects.sqlite import JSON
-from sqlalchemy.orm import Mapped, mapped_column
+from uuid import uuid4
 
-from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+from core_db import AuditMixin, Base, VersionMixin
+from sqlalchemy import JSON, String, Text
+from sqlalchemy.orm import Mapped, mapped_column
 
 
-class Role(Base, TenantMixin, AuditMixin, VersionMixin):
+class Role(Base, AuditMixin, VersionMixin):
     __tablename__ = "auth_role"
 
+    id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
     code: Mapped[str] = mapped_column(String(128), index=True)
     name: Mapped[str] = mapped_column(String(128))
     description: Mapped[str | None] = mapped_column(Text, nullable=True)

+ 4 - 3
services/auth-service/app/db/models/role_assignment.py

@@ -1,14 +1,15 @@
 from datetime import datetime
+from uuid import uuid4
 
+from core_db import AuditMixin, Base, VersionMixin
 from sqlalchemy import DateTime, String
 from sqlalchemy.orm import Mapped, mapped_column
 
-from core_db import AuditMixin, Base, TenantMixin, VersionMixin
 
-
-class RoleAssignment(Base, TenantMixin, AuditMixin, VersionMixin):
+class RoleAssignment(Base, AuditMixin, VersionMixin):
     __tablename__ = "auth_role_assignment"
 
+    id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
     user_id: Mapped[str] = mapped_column(String(36), index=True)
     role_id: Mapped[str] = mapped_column(String(36), index=True)
     status: Mapped[str] = mapped_column(String(32), default="active", index=True)

+ 7 - 6
services/auth-service/app/db/models/user.py

@@ -1,17 +1,18 @@
 from datetime import datetime
+from uuid import uuid4
 
-from sqlalchemy import DateTime, String
-from sqlalchemy.dialects.sqlite import JSON
-from sqlalchemy.orm import Mapped, mapped_column
-
-from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+from core_db import AuditMixin, Base, VersionMixin
 from core_shared import JSONValue
+from sqlalchemy import JSON, DateTime, String
+from sqlalchemy.orm import Mapped, mapped_column
 
 
-class User(TenantMixin, AuditMixin, VersionMixin, Base):
+class User(AuditMixin, VersionMixin, Base):
     __tablename__ = "auth_user"
 
+    id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
     username: Mapped[str] = mapped_column(String(128), index=True)
+    password_hash: Mapped[str] = mapped_column(String(512), default="")
     display_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
     email: Mapped[str | None] = mapped_column(String(256), nullable=True, index=True)
     status: Mapped[str] = mapped_column(String(32), default="active", index=True)

+ 2 - 4
services/auth-service/app/db/session.py

@@ -1,10 +1,9 @@
 from collections.abc import Generator
 
+from core_db import DatabaseSettings, create_engine_from_settings, create_session_factory
 from fastapi import Request
 from sqlalchemy.orm import Session, sessionmaker
 
-from core_db import DatabaseSettings, create_engine_from_settings, create_session_factory
-
 from app.bootstrap.settings import AuthServiceSettings
 
 
@@ -13,8 +12,7 @@ def build_session_factory(settings: AuthServiceSettings | None = None) -> sessio
     engine = create_engine_from_settings(
         DatabaseSettings(
             database_url=resolved_settings.database_url,
-            echo_sql=resolved_settings.echo_sql,
-        )
+            echo_sql=resolved_settings.echo_sql)
     )
     return create_session_factory(engine)
 

+ 38 - 49
services/auth-service/app/domain/repositories.py

@@ -1,10 +1,9 @@
 from datetime import datetime
 
-from sqlalchemy import select
-from sqlalchemy.orm import Session
-
 from core_domain import RoleAssignmentStatus, RoleStatus, UserStatus
 from core_shared import JSONValue
+from sqlalchemy import select
+from sqlalchemy.orm import Session
 
 from app.db.models import Role, RoleAssignment, User
 
@@ -16,34 +15,43 @@ class UserRepository:
     def create(
         self,
         *,
-        tenant_id: str,
         username: str,
+        password_hash: str,
         display_name: str | None,
         email: str | None,
-        metadata_json: dict[str, JSONValue],
-    ) -> User:
+        metadata_json: dict[str, JSONValue]) -> User:
         entity = User(
-            tenant_id=tenant_id,
             username=username,
+            password_hash=password_hash,
             display_name=display_name,
             email=email,
-            metadata_json=metadata_json,
-        )
+            metadata_json=metadata_json)
         self.db.add(entity)
         self.db.commit()
         self.db.refresh(entity)
         return entity
 
-    def list_by_tenant(self, *, tenant_id: str) -> list[User]:
-        stmt = select(User).where(User.tenant_id == tenant_id).order_by(User.created_time.desc())
+    def list_all(self) -> list[User]:
+        stmt = select(User).order_by(User.created_time.desc())
         return list(self.db.scalars(stmt))
 
-    def get_by_id(self, *, tenant_id: str, user_id: str) -> User | None:
-        stmt = select(User).where(User.tenant_id == tenant_id).where(User.id == user_id)
+    def get_by_id(self, *, user_id: str) -> User | None:
+        return self.db.get(User, user_id)
+
+    def get_by_username(self, *, username: str) -> User | None:
+        stmt = select(User).where(User.username == username)
         return self.db.scalar(stmt)
 
-    def update_status(self, *, tenant_id: str, user_id: str, status: UserStatus) -> User | None:
-        entity = self.get_by_id(tenant_id=tenant_id, user_id=user_id)
+    def touch_last_login_time(self, *, user_id: str) -> None:
+        entity = self.db.get(User, user_id)
+        if entity is None:
+            return
+        entity.last_login_time = datetime.utcnow()
+        self.db.commit()
+        self.db.refresh(entity)
+
+    def update_status(self, *, user_id: str, status: UserStatus) -> User | None:
+        entity = self.get_by_id(user_id=user_id)
         if entity is None:
             return None
         entity.status = status
@@ -59,34 +67,29 @@ class RoleRepository:
     def create(
         self,
         *,
-        tenant_id: str,
         code: str,
         name: str,
         description: str | None,
-        permissions_json: list[str],
-    ) -> Role:
+        permissions_json: list[str]) -> Role:
         entity = Role(
-            tenant_id=tenant_id,
             code=code,
             name=name,
             description=description,
-            permissions_json=permissions_json,
-        )
+            permissions_json=permissions_json)
         self.db.add(entity)
         self.db.commit()
         self.db.refresh(entity)
         return entity
 
-    def list_by_tenant(self, *, tenant_id: str) -> list[Role]:
-        stmt = select(Role).where(Role.tenant_id == tenant_id).order_by(Role.created_time.desc())
+    def list_all(self) -> list[Role]:
+        stmt = select(Role).order_by(Role.created_time.desc())
         return list(self.db.scalars(stmt))
 
-    def get_by_id(self, *, tenant_id: str, role_id: str) -> Role | None:
-        stmt = select(Role).where(Role.tenant_id == tenant_id).where(Role.id == role_id)
-        return self.db.scalar(stmt)
+    def get_by_id(self, *, role_id: str) -> Role | None:
+        return self.db.get(Role, role_id)
 
-    def update_status(self, *, tenant_id: str, role_id: str, status: RoleStatus) -> Role | None:
-        entity = self.get_by_id(tenant_id=tenant_id, role_id=role_id)
+    def update_status(self, *, role_id: str, status: RoleStatus) -> Role | None:
+        entity = self.get_by_id(role_id=role_id)
         if entity is None:
             return None
         entity.status = status
@@ -102,30 +105,25 @@ class RoleAssignmentRepository:
     def create(
         self,
         *,
-        tenant_id: str,
         user_id: str,
         role_id: str,
         scope_type: str | None,
         scope_id: str | None,
-        expires_time: datetime | None,
-    ) -> RoleAssignment:
+        expires_time: datetime | None) -> RoleAssignment:
         entity = RoleAssignment(
-            tenant_id=tenant_id,
             user_id=user_id,
             role_id=role_id,
             scope_type=scope_type,
             scope_id=scope_id,
-            expires_time=expires_time,
-        )
+            expires_time=expires_time)
         self.db.add(entity)
         self.db.commit()
         self.db.refresh(entity)
         return entity
 
-    def list_by_user(self, *, tenant_id: str, user_id: str) -> list[RoleAssignment]:
+    def list_by_user(self, *, user_id: str) -> list[RoleAssignment]:
         stmt = (
             select(RoleAssignment)
-            .where(RoleAssignment.tenant_id == tenant_id)
             .where(RoleAssignment.user_id == user_id)
             .order_by(RoleAssignment.created_time.desc())
         )
@@ -134,24 +132,15 @@ class RoleAssignmentRepository:
     def get_by_id(
         self,
         *,
-        tenant_id: str,
-        assignment_id: str,
-    ) -> RoleAssignment | None:
-        stmt = (
-            select(RoleAssignment)
-            .where(RoleAssignment.tenant_id == tenant_id)
-            .where(RoleAssignment.id == assignment_id)
-        )
-        return self.db.scalar(stmt)
+        assignment_id: str) -> RoleAssignment | None:
+        return self.db.get(RoleAssignment, assignment_id)
 
     def update_status(
         self,
         *,
-        tenant_id: str,
         assignment_id: str,
-        status: RoleAssignmentStatus,
-    ) -> RoleAssignment | None:
-        entity = self.get_by_id(tenant_id=tenant_id, assignment_id=assignment_id)
+        status: RoleAssignmentStatus) -> RoleAssignment | None:
+        entity = self.get_by_id(assignment_id=assignment_id)
         if entity is None:
             return None
         entity.status = status

+ 46 - 0
services/auth-service/app/infrastructure/passwords.py

@@ -0,0 +1,46 @@
+import base64
+import hashlib
+import hmac
+import secrets
+
+PBKDF2_ITERATIONS = 260_000
+PASSWORD_HASH_PREFIX = "pbkdf2_sha256"
+
+
+def hash_password(password: str) -> str:
+    salt = secrets.token_bytes(16)
+    digest = hashlib.pbkdf2_hmac(
+        "sha256",
+        password.encode("utf-8"),
+        salt,
+        PBKDF2_ITERATIONS)
+    return "$".join(
+        [
+            PASSWORD_HASH_PREFIX,
+            str(PBKDF2_ITERATIONS),
+            base64.b64encode(salt).decode("ascii"),
+            base64.b64encode(digest).decode("ascii"),
+        ]
+    )
+
+
+def verify_password(password: str, password_hash: str | None) -> bool:
+    if not password_hash:
+        return False
+
+    try:
+        algorithm, iterations_raw, salt_raw, digest_raw = password_hash.split("$", 3)
+        if algorithm != PASSWORD_HASH_PREFIX:
+            return False
+        iterations = int(iterations_raw)
+        salt = base64.b64decode(salt_raw.encode("ascii"))
+        expected_digest = base64.b64decode(digest_raw.encode("ascii"))
+    except (ValueError, TypeError):
+        return False
+
+    actual_digest = hashlib.pbkdf2_hmac(
+        "sha256",
+        password.encode("utf-8"),
+        salt,
+        iterations)
+    return hmac.compare_digest(actual_digest, expected_digest)

+ 88 - 0
services/auth-service/app/infrastructure/tokens.py

@@ -0,0 +1,88 @@
+import base64
+import hashlib
+import hmac
+import json
+import secrets
+from datetime import datetime, timedelta
+
+TOKEN_PREFIX = "apt"
+
+
+class TokenError(ValueError):
+    pass
+
+
+def issue_access_token(
+    *,
+    user_id: str,
+    secret: str,
+    ttl_minutes: int = 480) -> tuple[str, datetime]:
+    expires_time = datetime.utcnow() + timedelta(minutes=ttl_minutes)
+    payload = {
+        "user_id": user_id,
+        "expires_time": expires_time.isoformat(timespec="seconds") + "Z",
+        "nonce": secrets.token_urlsafe(16),
+    }
+    encoded_payload = _urlsafe_b64encode(
+        json.dumps(payload, separators=(",", ":")).encode("utf-8")
+    )
+    signature = _sign(encoded_payload, secret)
+    return f"{TOKEN_PREFIX}_{encoded_payload}.{signature}", expires_time
+
+
+def verify_access_token(token: str, *, secret: str) -> dict[str, str]:
+    encoded_payload, signature = _split_token(token)
+    expected_signature = _sign(encoded_payload, secret)
+    if not hmac.compare_digest(signature, expected_signature):
+        raise TokenError("invalid token signature")
+
+    try:
+        payload = json.loads(_urlsafe_b64decode(encoded_payload).decode("utf-8"))
+    except (json.JSONDecodeError, UnicodeDecodeError) as exc:
+        raise TokenError("invalid token payload") from exc
+
+    user_id = payload.get("user_id")
+    expires_time_raw = payload.get("expires_time")
+    if not isinstance(user_id, str) or not user_id:
+        raise TokenError("missing user_id")
+    if not isinstance(expires_time_raw, str) or not expires_time_raw:
+        raise TokenError("missing expires_time")
+
+    expires_time = datetime.fromisoformat(expires_time_raw.removesuffix("Z"))
+    if expires_time <= datetime.utcnow():
+        raise TokenError("token expired")
+
+    return {
+        "user_id": user_id,
+        "expires_time": expires_time_raw,
+    }
+
+
+def _split_token(token: str) -> tuple[str, str]:
+    if not token.startswith(f"{TOKEN_PREFIX}_"):
+        raise TokenError("invalid token prefix")
+    token_body = token.removeprefix(f"{TOKEN_PREFIX}_")
+    try:
+        encoded_payload, signature = token_body.split(".", 1)
+    except ValueError as exc:
+        raise TokenError("invalid token format") from exc
+    if not encoded_payload or not signature:
+        raise TokenError("invalid token format")
+    return encoded_payload, signature
+
+
+def _sign(encoded_payload: str, secret: str) -> str:
+    digest = hmac.new(
+        secret.encode("utf-8"),
+        encoded_payload.encode("ascii"),
+        hashlib.sha256).digest()
+    return _urlsafe_b64encode(digest)
+
+
+def _urlsafe_b64encode(value: bytes) -> str:
+    return base64.urlsafe_b64encode(value).decode("ascii").rstrip("=")
+
+
+def _urlsafe_b64decode(value: str) -> bytes:
+    padding = "=" * (-len(value) % 4)
+    return base64.urlsafe_b64decode(f"{value}{padding}".encode("ascii"))

+ 41 - 8
services/auth-service/app/schemas/auth.py

@@ -1,8 +1,6 @@
 from datetime import datetime
 from typing import TYPE_CHECKING
 
-from pydantic import BaseModel, Field
-
 from core_domain import (
     PermissionCheckContract,
     PermissionCheckResultContract,
@@ -14,21 +12,21 @@ from core_domain import (
     UserStatus,
 )
 from core_shared import JSONValue
+from pydantic import BaseModel, Field
 
 if TYPE_CHECKING:
     from app.db.models import Role, RoleAssignment, User
 
 
 class UserCreateRequest(BaseModel):
-    tenant_id: str
     username: str
+    password: str | None = Field(default=None, min_length=8)
     display_name: str | None = None
     email: str | None = None
     metadata_json: dict[str, JSONValue] = Field(default_factory=dict)
 
 
 class UserStatusUpdateRequest(BaseModel):
-    tenant_id: str
     status: UserStatus
 
 
@@ -38,8 +36,46 @@ class UserResponse(UserContract):
         return cls.model_validate(entity, from_attributes=True)
 
 
+class LoginUserResponse(BaseModel):
+    id: str
+    username: str
+    display_name: str | None = None
+    email: str | None = None
+    status: UserStatus
+    metadata_json: dict[str, JSONValue] = Field(default_factory=dict)
+    last_login_time: datetime | None = None
+    created_time: datetime
+
+    @classmethod
+    def from_entity(cls, entity: "User") -> "LoginUserResponse":
+        return cls.model_validate(entity, from_attributes=True)
+
+
+class LoginRequest(BaseModel):
+    username: str
+    password: str
+
+
+class LoginResponse(BaseModel):
+    access_token: str
+    token_type: str = "bearer"
+    expires_time: datetime
+    user: LoginUserResponse
+
+
+class TokenVerifyRequest(BaseModel):
+    access_token: str
+
+
+class TokenVerifyResponse(BaseModel):
+    active: bool
+    user_id: str | None = None
+    username: str | None = None
+    expires_time: datetime | None = None
+    reason: str | None = None
+
+
 class RoleCreateRequest(BaseModel):
-    tenant_id: str
     code: str
     name: str
     description: str | None = None
@@ -47,7 +83,6 @@ class RoleCreateRequest(BaseModel):
 
 
 class RoleStatusUpdateRequest(BaseModel):
-    tenant_id: str
     status: RoleStatus
 
 
@@ -58,7 +93,6 @@ class RoleResponse(RoleContract):
 
 
 class RoleAssignmentCreateRequest(BaseModel):
-    tenant_id: str
     user_id: str
     role_id: str
     scope_type: str | None = None
@@ -67,7 +101,6 @@ class RoleAssignmentCreateRequest(BaseModel):
 
 
 class RoleAssignmentStatusUpdateRequest(BaseModel):
-    tenant_id: str
     status: RoleAssignmentStatus
 
 

+ 5 - 9
services/code-runner-service/app/api/routes.py

@@ -1,6 +1,6 @@
+from core_domain import CodeExecutionRequestContract, CodeExecutionResponseContract, ServiceHealth
 from fastapi import APIRouter, Depends, HTTPException
 
-from core_domain import CodeExecutionRequestContract, CodeExecutionResponseContract, ServiceHealth
 from app.application.services import CodeRunnerApplicationService
 from app.bootstrap.settings import CodeRunnerServiceSettings
 from app.infrastructure.runner import CodeRunnerError, PythonCodeRunner
@@ -13,26 +13,22 @@ def get_code_runner_settings() -> CodeRunnerServiceSettings:
 
 
 def get_code_runner_application_service(
-    settings: CodeRunnerServiceSettings = Depends(get_code_runner_settings),
-) -> CodeRunnerApplicationService:
+    settings: CodeRunnerServiceSettings = Depends(get_code_runner_settings)) -> CodeRunnerApplicationService:
     return CodeRunnerApplicationService(
         runner=PythonCodeRunner(settings=settings),
-        settings=settings,
-    )
+        settings=settings)
 
 
 @router.get("/health", response_model=ServiceHealth)
 def health_check(
-    settings: CodeRunnerServiceSettings = Depends(get_code_runner_settings),
-) -> ServiceHealth:
+    settings: CodeRunnerServiceSettings = Depends(get_code_runner_settings)) -> ServiceHealth:
     return ServiceHealth(service="code-runner-service", status="ok", database=settings.python_bin)
 
 
 @router.post("/execute", response_model=CodeExecutionResponseContract)
 def execute_code(
     payload: CodeExecutionRequestContract,
-    service: CodeRunnerApplicationService = Depends(get_code_runner_application_service),
-) -> CodeExecutionResponseContract:
+    service: CodeRunnerApplicationService = Depends(get_code_runner_application_service)) -> CodeExecutionResponseContract:
     try:
         return service.execute_code(payload)
     except CodeRunnerError as exc:

+ 2 - 4
services/code-runner-service/app/application/services.py

@@ -9,8 +9,7 @@ class CodeRunnerApplicationService:
         self,
         *,
         runner: PythonCodeRunner,
-        settings: CodeRunnerServiceSettings,
-    ) -> None:
+        settings: CodeRunnerServiceSettings) -> None:
         self.runner = runner
         self.settings = settings
 
@@ -18,8 +17,7 @@ class CodeRunnerApplicationService:
         if payload.language != "python":
             return CodeExecutionResponseContract(
                 success=False,
-                error_message=f"unsupported language: {payload.language}",
-            )
+                error_message=f"unsupported language: {payload.language}")
 
         resolved_payload = payload.model_copy(
             update={

+ 2 - 3
services/code-runner-service/app/bootstrap/app.py

@@ -1,6 +1,6 @@
-from fastapi import FastAPI
 from core_shared.observability import add_observability
 from core_shared.security import add_internal_service_auth
+from fastapi import FastAPI
 
 from app.api.routes import router
 from app.bootstrap.settings import CodeRunnerServiceSettings
@@ -10,8 +10,7 @@ def create_app() -> FastAPI:
     settings = CodeRunnerServiceSettings()
     app = FastAPI(
         title="agent-platform code-runner-service",
-        version="0.1.0",
-    )
+        version="0.1.0")
     app.state.settings = settings
     add_observability(app, settings.service_name)
     add_internal_service_auth(app, settings)

+ 4 - 8
services/code-runner-service/app/infrastructure/runner.py

@@ -27,8 +27,7 @@ class PythonCodeRunner:
             script_file.write_text(script_content, encoding="utf-8")
             input_file.write_text(
                 json.dumps(payload.input_json, ensure_ascii=False),
-                encoding="utf-8",
-            )
+                encoding="utf-8")
 
             try:
                 completed = subprocess.run(
@@ -37,14 +36,12 @@ class PythonCodeRunner:
                     text=True,
                     encoding="utf-8",
                     timeout=payload.timeout_seconds,
-                    check=False,
-                )
+                    check=False)
             except subprocess.TimeoutExpired as exc:
                 return CodeExecutionResponseContract(
                     success=False,
                     stderr=exc.stderr or "",
-                    error_message=f"code execution timed out after {payload.timeout_seconds} seconds",
-                )
+                    error_message=f"code execution timed out after {payload.timeout_seconds} seconds")
             except OSError as exc:
                 raise CodeRunnerError(f"failed to start python runner: {exc}") from exc
 
@@ -58,8 +55,7 @@ class PythonCodeRunner:
             stdout=stdout,
             stderr=stderr,
             output_json=output_json,
-            error_message=error_message,
-        )
+            error_message=error_message)
 
 
 def _build_python_runner_script(user_code: str) -> str:

+ 2 - 4
services/event-service/alembic/env.py

@@ -1,9 +1,8 @@
 from logging.config import fileConfig
 
 from alembic import context
-from sqlalchemy import engine_from_config, pool
-
 from app.db.models import Base
+from sqlalchemy import engine_from_config, pool
 
 config = context.config
 
@@ -24,8 +23,7 @@ def run_migrations_online() -> None:
     connectable = engine_from_config(
         config.get_section(config.config_ini_section, {}),
         prefix="sqlalchemy.",
-        poolclass=pool.NullPool,
-    )
+        poolclass=pool.NullPool)
     with connectable.connect() as connection:
         context.configure(connection=connection, target_metadata=target_metadata)
         with context.begin_transaction():

+ 2 - 7
services/event-service/alembic/versions/20260425_0001_init_event_models.py

@@ -7,9 +7,8 @@ Create Date: 2026-04-25 23:40:00
 
 from collections.abc import Sequence
 
-from alembic import op
 import sqlalchemy as sa
-
+from alembic import op
 
 revision: str = "20260425_0001"
 down_revision: str | None = None
@@ -35,7 +34,6 @@ def upgrade() -> None:
         sa.Column("publish_attempt_count", sa.Integer(), nullable=False),
         sa.Column("last_error_message", sa.Text(), nullable=True),
         sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("tenant_id", sa.String(length=36), nullable=False),
         sa.Column("created_by", sa.String(length=36), nullable=True),
         sa.Column("updated_by", sa.String(length=36), nullable=True),
         sa.Column("created_time", sa.DateTime(), nullable=False),
@@ -43,8 +41,7 @@ def upgrade() -> None:
         sa.Column("deleted_time", sa.DateTime(), nullable=True),
         sa.Column("version", sa.Integer(), nullable=False),
         sa.PrimaryKeyConstraint("id"),
-        sa.UniqueConstraint("event_id"),
-    )
+        sa.UniqueConstraint("event_id"))
     op.create_index("ix_event_record_event_id", "event_record", ["event_id"])
     op.create_index("ix_event_record_event_type", "event_record", ["event_type"])
     op.create_index("ix_event_record_source_service", "event_record", ["source_service"])
@@ -55,11 +52,9 @@ def upgrade() -> None:
     op.create_index("ix_event_record_status", "event_record", ["status"])
     op.create_index("ix_event_record_event_time", "event_record", ["event_time"])
     op.create_index("ix_event_record_published_time", "event_record", ["published_time"])
-    op.create_index("ix_event_record_tenant_id", "event_record", ["tenant_id"])
 
 
 def downgrade() -> None:
-    op.drop_index("ix_event_record_tenant_id", table_name="event_record")
     op.drop_index("ix_event_record_published_time", table_name="event_record")
     op.drop_index("ix_event_record_event_time", table_name="event_record")
     op.drop_index("ix_event_record_status", table_name="event_record")

+ 12 - 27
services/event-service/app/api/routes.py

@@ -1,10 +1,9 @@
+from core_domain import ServiceHealth
+from core_events import EventDeliveryStatus
 from fastapi import APIRouter, Depends, HTTPException, Query
 from sqlalchemy import text
 from sqlalchemy.orm import Session
 
-from core_domain import ServiceHealth
-from core_events import EventDeliveryStatus
-
 from app.application.services import EventApplicationService
 from app.db.session import get_db
 from app.domain.repositories import EventRecordRepository
@@ -34,26 +33,22 @@ def health_check(db: Session = Depends(get_db)) -> ServiceHealth:
 @router.post("", response_model=EventRecordResponse)
 def publish_event(
     payload: EventPublishRequest,
-    service: EventApplicationService = Depends(get_event_application_service),
-) -> EventRecordResponse:
+    service: EventApplicationService = Depends(get_event_application_service)) -> EventRecordResponse:
     return EventRecordResponse.from_entity(service.publish_event(payload))
 
 
 @router.post("/batch", response_model=EventBatchPublishResponse)
 def publish_batch(
     payload: EventBatchPublishRequest,
-    service: EventApplicationService = Depends(get_event_application_service),
-) -> EventBatchPublishResponse:
+    service: EventApplicationService = Depends(get_event_application_service)) -> EventBatchPublishResponse:
     events = service.publish_batch(payload)
     return EventBatchPublishResponse(
         events=[EventRecordResponse.from_entity(item) for item in events],
-        count=len(events),
-    )
+        count=len(events))
 
 
 @router.get("", response_model=list[EventRecordResponse])
 def list_events(
-    tenant_id: str = Query(...),
     event_type: str | None = Query(default=None),
     source_service: str | None = Query(default=None),
     aggregate_type: str | None = Query(default=None),
@@ -61,28 +56,24 @@ def list_events(
     correlation_id: str | None = Query(default=None),
     status: EventDeliveryStatus | None = Query(default=None),
     limit: int = Query(default=100, ge=1, le=500),
-    service: EventApplicationService = Depends(get_event_application_service),
-) -> list[EventRecordResponse]:
+    service: EventApplicationService = Depends(get_event_application_service)) -> list[EventRecordResponse]:
     return [
         EventRecordResponse.from_entity(item)
         for item in service.list_events(
-            tenant_id=tenant_id,
             event_type=event_type,
             source_service=source_service,
             aggregate_type=aggregate_type,
             aggregate_id=aggregate_id,
             correlation_id=correlation_id,
             status=status,
-            limit=limit,
-        )
+            limit=limit)
     ]
 
 
 @router.post("/claim-pending", response_model=list[EventRecordResponse])
 def claim_pending_events(
     payload: PendingEventClaimRequest,
-    service: EventApplicationService = Depends(get_event_application_service),
-) -> list[EventRecordResponse]:
+    service: EventApplicationService = Depends(get_event_application_service)) -> list[EventRecordResponse]:
     return [
         EventRecordResponse.from_entity(item)
         for item in service.claim_pending_events(payload)
@@ -93,12 +84,10 @@ def claim_pending_events(
 def update_delivery_status(
     event_record_id: str,
     payload: EventDeliveryStatusUpdateRequest,
-    service: EventApplicationService = Depends(get_event_application_service),
-) -> EventRecordResponse:
+    service: EventApplicationService = Depends(get_event_application_service)) -> EventRecordResponse:
     entity = service.update_delivery_status(
         event_record_id=event_record_id,
-        payload=payload,
-    )
+        payload=payload)
     if entity is None:
         raise HTTPException(status_code=404, detail=f"event not found: {event_record_id}")
     return EventRecordResponse.from_entity(entity)
@@ -106,10 +95,6 @@ def update_delivery_status(
 
 @router.get("/stats", response_model=EventStatsResponse)
 def event_stats(
-    tenant_id: str = Query(...),
-    service: EventApplicationService = Depends(get_event_application_service),
-) -> EventStatsResponse:
+    service: EventApplicationService = Depends(get_event_application_service)) -> EventStatsResponse:
     return EventStatsResponse(
-        tenant_id=tenant_id,
-        counts_json=service.build_stats(tenant_id=tenant_id),
-    )
+        counts_json=service.build_stats())

+ 10 - 23
services/event-service/app/application/services.py

@@ -10,8 +10,7 @@ from app.schemas.event import (
     EventBatchPublishRequest,
     EventDeliveryStatusUpdateRequest,
     EventPublishRequest,
-    PendingEventClaimRequest,
-)
+    PendingEventClaimRequest)
 
 
 class EventApplicationService:
@@ -20,7 +19,6 @@ class EventApplicationService:
 
     def publish_event(self, payload: EventPublishRequest) -> EventRecord:
         return self.event_repository.create(
-            tenant_id=payload.tenant_id or "public",
             event_id=str(uuid4()),
             event_type=payload.event_type,
             source_service=payload.source_service,
@@ -30,8 +28,7 @@ class EventApplicationService:
             causation_id=payload.causation_id,
             payload_json=payload.payload_json,
             metadata_json=payload.metadata_json,
-            event_time=payload.event_time or datetime.utcnow(),
-        )
+            event_time=payload.event_time or datetime.utcnow())
 
     def publish_batch(self, payload: EventBatchPublishRequest) -> list[EventRecord]:
         return [self.publish_event(item) for item in payload.events]
@@ -39,52 +36,42 @@ class EventApplicationService:
     def list_events(
         self,
         *,
-        tenant_id: str,
         event_type: str | None = None,
         source_service: str | None = None,
         aggregate_type: str | None = None,
         aggregate_id: str | None = None,
         correlation_id: str | None = None,
         status: EventDeliveryStatus | None = None,
-        limit: int = 100,
-    ) -> list[EventRecord]:
+        limit: int = 100) -> list[EventRecord]:
         return self.event_repository.list_by_scope(
-            tenant_id=tenant_id,
             event_type=event_type,
             source_service=source_service,
             aggregate_type=aggregate_type,
             aggregate_id=aggregate_id,
             correlation_id=correlation_id,
             status=status,
-            limit=limit,
-        )
+            limit=limit)
 
     def claim_pending_events(self, payload: PendingEventClaimRequest) -> list[EventRecord]:
         return self.event_repository.claim_pending(
-            tenant_id=payload.tenant_id,
-            limit=payload.limit,
-        )
+            limit=payload.limit)
 
     def update_delivery_status(
         self,
         *,
         event_record_id: str,
-        payload: EventDeliveryStatusUpdateRequest,
-    ) -> EventRecord | None:
+        payload: EventDeliveryStatusUpdateRequest) -> EventRecord | None:
         entity = self.event_repository.get_by_id(
-            tenant_id=payload.tenant_id,
-            event_record_id=event_record_id,
-        )
+            event_record_id=event_record_id)
         if entity is None:
             return None
         return self.event_repository.update_delivery_status(
             event_record_id=event_record_id,
             status=payload.status,
-            last_error_message=payload.last_error_message,
-        )
+            last_error_message=payload.last_error_message)
 
-    def build_stats(self, *, tenant_id: str) -> dict[str, JSONValue]:
-        events = self.event_repository.list_by_scope(tenant_id=tenant_id, limit=500)
+    def build_stats(self) -> dict[str, JSONValue]:
+        events = self.event_repository.list_by_scope(limit=500)
         by_status: dict[str, int] = {}
         by_type: dict[str, int] = {}
         for event in events:

+ 2 - 3
services/event-service/app/bootstrap/app.py

@@ -1,6 +1,6 @@
-from fastapi import FastAPI
 from core_shared.observability import add_observability
 from core_shared.security import add_internal_service_auth
+from fastapi import FastAPI
 
 from app.api.routes import router
 from app.bootstrap.settings import EventServiceSettings
@@ -11,8 +11,7 @@ def create_app() -> FastAPI:
     settings = EventServiceSettings()
     app = FastAPI(
         title="agent-platform event-service",
-        version="0.1.0",
-    )
+        version="0.1.0")
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)

+ 3 - 4
services/event-service/app/db/models/event_record.py

@@ -1,14 +1,13 @@
 from datetime import datetime
 
+from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_shared import JSONValue
 from sqlalchemy import DateTime, Integer, String, Text
 from sqlalchemy.dialects.sqlite import JSON
 from sqlalchemy.orm import Mapped, mapped_column
 
-from core_db import AuditMixin, Base, TenantMixin, VersionMixin
-from core_shared import JSONValue
-
 
-class EventRecord(TenantMixin, AuditMixin, VersionMixin, Base):
+class EventRecord(EntityMixin, AuditMixin, VersionMixin, Base):
     __tablename__ = "event_record"
 
     event_id: Mapped[str] = mapped_column(String(36), unique=True, index=True)

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff