imbi-api 2.45.0__tar.gz → 2.46.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (426) hide show
  1. {imbi_api-2.45.0 → imbi_api-2.46.0}/PKG-INFO +2 -2
  2. {imbi_api-2.45.0 → imbi_api-2.46.0}/pyproject.toml +2 -2
  3. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/agent_tasks/__init__.py +1 -2
  4. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/agent_tasks/log.py +63 -30
  5. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/agent_tasks/schemata.toml +8 -5
  6. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/agent_tasks/store.py +172 -48
  7. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/agent_tools.py +23 -48
  8. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/agent_api_routes.py +4 -0
  9. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/seed.py +15 -0
  10. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/blueprint_compliance.py +88 -3
  11. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/agent_tasks.py +277 -55
  12. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/agents.py +10 -5
  13. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/ai_models.py +223 -18
  14. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/harness.py +41 -31
  15. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/project_analysis.py +49 -18
  16. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/projects.py +23 -5
  17. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/roles.py +5 -21
  18. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/secret_broker.py +120 -13
  19. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/workflow_runs.py +25 -3
  20. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/workflows.py +32 -1
  21. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/harness/platform.py +80 -8
  22. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/harness/store.py +7 -3
  23. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/secret_broker/broker.py +161 -42
  24. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/secret_broker/schemata.toml +5 -0
  25. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/secret_broker/store.py +188 -20
  26. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_agent_platform.py +28 -2
  27. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_agent_task_log.py +68 -6
  28. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_agent_task_relations.py +48 -13
  29. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_agent_tasks.py +183 -10
  30. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_agent_usage.py +1 -1
  31. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_agents.py +14 -13
  32. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_ai_models.py +203 -3
  33. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_harness_secrets.py +1 -3
  34. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_project_analysis.py +57 -0
  35. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_projects.py +31 -5
  36. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_roles.py +37 -19
  37. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_secret_broker.py +112 -1
  38. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_workflow_runs.py +41 -4
  39. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_workflow_triggers.py +17 -0
  40. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_workflows.py +26 -1
  41. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/secret_support.py +22 -9
  42. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_agent_platform.py +149 -5
  43. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_agent_task_events_e2e.py +1 -1
  44. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_agent_tools.py +12 -33
  45. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_blueprint_compliance.py +73 -7
  46. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_secret_broker.py +350 -13
  47. {imbi_api-2.45.0 → imbi_api-2.46.0}/.gitignore +0 -0
  48. {imbi_api-2.45.0 → imbi_api-2.46.0}/README.md +0 -0
  49. {imbi_api-2.45.0 → imbi_api-2.46.0}/moon.yml +0 -0
  50. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/README.md +0 -0
  51. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/__init__.py +0 -0
  52. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/agent_tasks/sweeper.py +0 -0
  53. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/app.py +0 -0
  54. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/__init__.py +0 -0
  55. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/agent_api.py +0 -0
  56. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/authorization_codes.py +0 -0
  57. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/autonomous.py +0 -0
  58. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/internal_services.py +0 -0
  59. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/local_auth.py +0 -0
  60. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/login_providers.py +0 -0
  61. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/membership.py +0 -0
  62. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/models.py +0 -0
  63. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/oauth_clients.py +0 -0
  64. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/organizations.py +0 -0
  65. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/password.py +0 -0
  66. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/permissions.py +0 -0
  67. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/principals.py +0 -0
  68. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/sessions.py +0 -0
  69. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/tokens.py +0 -0
  70. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/auth/totp.py +0 -0
  71. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/backfill_embeddings.py +0 -0
  72. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/blueprint_attributes.py +0 -0
  73. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/commit_pushed_at_repair.py +0 -0
  74. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/commit_sync/__init__.py +0 -0
  75. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/commit_sync/queue.py +0 -0
  76. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/commit_sync/service.py +0 -0
  77. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/component_facts.py +0 -0
  78. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/deployment_status_repair.py +0 -0
  79. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/deployment_sweeper.py +0 -0
  80. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/deployment_sync/__init__.py +0 -0
  81. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/deployment_sync/queue.py +0 -0
  82. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/deployment_sync/service.py +0 -0
  83. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/documents/__init__.py +0 -0
  84. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/documents/read_sweeper.py +0 -0
  85. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/domain/__init__.py +0 -0
  86. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/domain/models.py +0 -0
  87. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/domain/scoring.py +0 -0
  88. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/drift.py +0 -0
  89. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/email/__init__.py +0 -0
  90. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/email/client.py +0 -0
  91. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/email/dependencies.py +0 -0
  92. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/email/models.py +0 -0
  93. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/email/templates/base.html +0 -0
  94. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/email/templates/base.txt +0 -0
  95. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/email/templates/password_reset.html +0 -0
  96. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/email/templates/password_reset.txt +0 -0
  97. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/email/templates/welcome.html +0 -0
  98. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/email/templates/welcome.txt +0 -0
  99. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/email/templates.py +0 -0
  100. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/__init__.py +0 -0
  101. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/_credentials.py +0 -0
  102. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/_document_events.py +0 -0
  103. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/_document_history.py +0 -0
  104. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/_document_reads.py +0 -0
  105. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/_document_scope.py +0 -0
  106. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/_helpers.py +0 -0
  107. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/_json_fields.py +0 -0
  108. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/_pagination.py +0 -0
  109. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/_request_urls.py +0 -0
  110. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/_search_index.py +0 -0
  111. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/admin.py +0 -0
  112. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/admin_plugins.py +0 -0
  113. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/agent_platform.py +0 -0
  114. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/agent_task_harness.py +0 -0
  115. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/agent_usage.py +0 -0
  116. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/ai_providers.py +0 -0
  117. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/api_keys.py +0 -0
  118. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/auth.py +0 -0
  119. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/auth_providers.py +0 -0
  120. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/blueprints.py +0 -0
  121. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/client_credentials.py +0 -0
  122. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/comments.py +0 -0
  123. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/components.py +0 -0
  124. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/dashboard.py +0 -0
  125. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/document_analytics.py +0 -0
  126. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/document_likes.py +0 -0
  127. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/document_presence.py +0 -0
  128. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/document_templates.py +0 -0
  129. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/document_versions.py +0 -0
  130. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/documents.py +0 -0
  131. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/environments.py +0 -0
  132. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/events.py +0 -0
  133. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/graph_query.py +0 -0
  134. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/iggy_connectors.py +0 -0
  135. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/integrations.py +0 -0
  136. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/link_definitions.py +0 -0
  137. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/local_auth.py +0 -0
  138. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/maintenance.py +0 -0
  139. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/mcp_servers.py +0 -0
  140. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/mfa.py +0 -0
  141. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/oauth_metadata.py +0 -0
  142. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/operations_log.py +0 -0
  143. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/organizations.py +0 -0
  144. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/plugin_edges.py +0 -0
  145. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/plugin_entities.py +0 -0
  146. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/plugins.py +0 -0
  147. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/project_commit_sync.py +0 -0
  148. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/project_configuration.py +0 -0
  149. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/project_deployments.py +0 -0
  150. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/project_incidents.py +0 -0
  151. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/project_integrations.py +0 -0
  152. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/project_lifecycle.py +0 -0
  153. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/project_logs.py +0 -0
  154. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/project_plugins.py +0 -0
  155. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/project_pr_sync.py +0 -0
  156. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/project_types.py +0 -0
  157. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/prompts.py +0 -0
  158. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/pull_requests.py +0 -0
  159. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/releases.py +0 -0
  160. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/sa_api_keys.py +0 -0
  161. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/scoring.py +0 -0
  162. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/scoring_policies.py +0 -0
  163. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/search.py +0 -0
  164. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/service_accounts.py +0 -0
  165. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/status.py +0 -0
  166. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/tags.py +0 -0
  167. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/teams.py +0 -0
  168. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/uploads.py +0 -0
  169. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/user_activity.py +0 -0
  170. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/users.py +0 -0
  171. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/endpoints/webhooks.py +0 -0
  172. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/entrypoint.py +0 -0
  173. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/graph_ids.py +0 -0
  174. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/graph_sql.py +0 -0
  175. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/harness/__init__.py +0 -0
  176. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/harness/auth.py +0 -0
  177. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/harness/egress.py +0 -0
  178. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/harness/marketplaces.py +0 -0
  179. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/harness/metrics.py +0 -0
  180. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/harness/schemata.toml +0 -0
  181. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/harness/sweeper.py +0 -0
  182. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/harness/tokens.py +0 -0
  183. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/identity/__init__.py +0 -0
  184. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/identity/attribution.py +0 -0
  185. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/identity/endpoints.py +0 -0
  186. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/identity/errors.py +0 -0
  187. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/identity/flows.py +0 -0
  188. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/identity/host_integration.py +0 -0
  189. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/identity/models.py +0 -0
  190. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/identity/repository.py +0 -0
  191. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/identity/resolution.py +0 -0
  192. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/identity/state.py +0 -0
  193. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/identity/sweeper.py +0 -0
  194. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/iggy_metrics.py +0 -0
  195. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/lifespans.py +0 -0
  196. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/llm/__init__.py +0 -0
  197. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/llm/dependencies.py +0 -0
  198. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/llm/tokens.py +0 -0
  199. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/maintenance/__init__.py +0 -0
  200. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/maintenance/log.py +0 -0
  201. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/maintenance/operations.py +0 -0
  202. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/maintenance/registry.py +0 -0
  203. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/maintenance/state.py +0 -0
  204. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/maintenance/worker.py +0 -0
  205. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/mcp_test.py +0 -0
  206. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/middleware/__init__.py +0 -0
  207. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/middleware/rate_limit.py +0 -0
  208. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/models.py +0 -0
  209. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/openapi.py +0 -0
  210. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/orphan_releases.py +0 -0
  211. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/plugins/__init__.py +0 -0
  212. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/plugins/assignment_writer.py +0 -0
  213. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/plugins/assignments.py +0 -0
  214. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/plugins/credentials.py +0 -0
  215. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/plugins/installer.py +0 -0
  216. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/plugins/lifecycle.py +0 -0
  217. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/plugins/lifecycle_dispatch.py +0 -0
  218. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/plugins/reload.py +0 -0
  219. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/plugins/resolution.py +0 -0
  220. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/plugins/schemas.py +0 -0
  221. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/pr_sync/__init__.py +0 -0
  222. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/pr_sync/queue.py +0 -0
  223. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/pr_sync/service.py +0 -0
  224. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/prompts/__init__.py +0 -0
  225. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/prompts/release_notes_system.md +0 -0
  226. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/prompts/seed.py +0 -0
  227. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/py.typed +0 -0
  228. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/relationships.py +0 -0
  229. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/release_promote/__init__.py +0 -0
  230. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/release_promote/queue.py +0 -0
  231. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/release_promote/service.py +0 -0
  232. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/sbom.py +0 -0
  233. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/scoring/__init__.py +0 -0
  234. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/scoring/queue.py +0 -0
  235. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/secret_broker/__init__.py +0 -0
  236. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/secret_broker/audit.py +0 -0
  237. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/secret_broker/identity.py +0 -0
  238. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/secret_broker/providers.py +0 -0
  239. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/settings.py +0 -0
  240. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/storage/__init__.py +0 -0
  241. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/storage/client.py +0 -0
  242. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/storage/dependencies.py +0 -0
  243. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/storage/thumbnails.py +0 -0
  244. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/storage/validation.py +0 -0
  245. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/user_document_orgs.py +0 -0
  246. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/workflows/__init__.py +0 -0
  247. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/workflows/runs.py +0 -0
  248. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/workflows/runtime_schemata.toml +0 -0
  249. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/workflows/schemata.toml +0 -0
  250. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/workflows/store.py +0 -0
  251. {imbi_api-2.45.0 → imbi_api-2.46.0}/src/imbi/api/workflows/sweeper.py +0 -0
  252. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/__init__.py +0 -0
  253. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/auth/__init__.py +0 -0
  254. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/auth/test_authentication.py +0 -0
  255. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/auth/test_authorization.py +0 -0
  256. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/auth/test_encryption.py +0 -0
  257. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/auth/test_internal_services.py +0 -0
  258. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/auth/test_login_providers.py +0 -0
  259. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/auth/test_membership.py +0 -0
  260. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/auth/test_oauth_server.py +0 -0
  261. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/auth/test_organizations.py +0 -0
  262. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/auth/test_permissions.py +0 -0
  263. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/auth/test_seed.py +0 -0
  264. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/auth/test_sessions.py +0 -0
  265. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/auth/test_totp.py +0 -0
  266. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/clickhouse/__init__.py +0 -0
  267. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/clickhouse/test_client.py +0 -0
  268. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/clickhouse/test_init.py +0 -0
  269. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/clickhouse/test_privacy.py +0 -0
  270. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/email/__init__.py +0 -0
  271. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/email/test_audit_clickhouse.py +0 -0
  272. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/email/test_client.py +0 -0
  273. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/email/test_init.py +0 -0
  274. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/email/test_integration.py +0 -0
  275. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/email/test_models.py +0 -0
  276. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/email/test_templates.py +0 -0
  277. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/__init__.py +0 -0
  278. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_admin.py +0 -0
  279. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_admin_plugins.py +0 -0
  280. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_agent_api.py +0 -0
  281. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_agent_task_actions.py +0 -0
  282. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_agent_task_harness.py +0 -0
  283. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_ai_providers.py +0 -0
  284. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_api_keys.py +0 -0
  285. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_auth.py +0 -0
  286. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_auth_providers.py +0 -0
  287. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_blueprints.py +0 -0
  288. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_client_credentials.py +0 -0
  289. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_comments.py +0 -0
  290. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_components.py +0 -0
  291. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_credentials.py +0 -0
  292. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_cross_org_reads.py +0 -0
  293. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_dashboard.py +0 -0
  294. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_document_analytics.py +0 -0
  295. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_document_likes.py +0 -0
  296. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_document_presence.py +0 -0
  297. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_document_reads.py +0 -0
  298. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_document_reads_sql.py +0 -0
  299. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_document_templates.py +0 -0
  300. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_document_versions.py +0 -0
  301. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_documents.py +0 -0
  302. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_environments.py +0 -0
  303. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_events.py +0 -0
  304. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_github_app_repository.py +0 -0
  305. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_graph_query.py +0 -0
  306. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_harness.py +0 -0
  307. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_harness_egress.py +0 -0
  308. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_iggy_connectors.py +0 -0
  309. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_init.py +0 -0
  310. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_integrations.py +0 -0
  311. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_json_fields.py +0 -0
  312. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_link_definitions.py +0 -0
  313. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_local_auth.py +0 -0
  314. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_maintenance.py +0 -0
  315. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_mcp_servers.py +0 -0
  316. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_mfa.py +0 -0
  317. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_operations_log.py +0 -0
  318. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_organizations.py +0 -0
  319. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_pagination.py +0 -0
  320. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_plugin_label_validation.py +0 -0
  321. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_plugins.py +0 -0
  322. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_project_commit_sync.py +0 -0
  323. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_project_configuration.py +0 -0
  324. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_project_deployments.py +0 -0
  325. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_project_deployments_autonomous.py +0 -0
  326. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_project_incidents.py +0 -0
  327. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_project_integrations.py +0 -0
  328. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_project_lifecycle.py +0 -0
  329. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_project_logs.py +0 -0
  330. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_project_plugins.py +0 -0
  331. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_project_response_surfacing.py +0 -0
  332. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_project_types.py +0 -0
  333. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_projects_helpers.py +0 -0
  334. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_prompt_decisions.py +0 -0
  335. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_prompts.py +0 -0
  336. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_pull_requests.py +0 -0
  337. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_releases.py +0 -0
  338. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_request_urls.py +0 -0
  339. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_sa_api_keys.py +0 -0
  340. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_scoring.py +0 -0
  341. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_scoring_policies.py +0 -0
  342. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_search.py +0 -0
  343. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_search_index.py +0 -0
  344. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_service_accounts.py +0 -0
  345. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_service_writeback.py +0 -0
  346. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_status.py +0 -0
  347. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_tags.py +0 -0
  348. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_teams.py +0 -0
  349. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_uploads.py +0 -0
  350. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_user_activity.py +0 -0
  351. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_users.py +0 -0
  352. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_webhooks.py +0 -0
  353. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/endpoints/test_workflow_run_history.py +0 -0
  354. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/fixtures/marketplaces/claude-code.json +0 -0
  355. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/fixtures/sbom/npm-realistic.json +0 -0
  356. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/fixtures/sbom/pypi-realistic.json +0 -0
  357. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/fixtures/sbom/tiny.json +0 -0
  358. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/identity/__init__.py +0 -0
  359. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/identity/test_endpoints.py +0 -0
  360. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/identity/test_errors.py +0 -0
  361. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/identity/test_flows.py +0 -0
  362. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/identity/test_host_integration.py +0 -0
  363. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/identity/test_repository.py +0 -0
  364. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/identity/test_resolution.py +0 -0
  365. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/identity/test_state.py +0 -0
  366. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/identity/test_sweeper.py +0 -0
  367. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/middleware/test_rate_limit.py +0 -0
  368. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/storage/__init__.py +0 -0
  369. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/storage/test_client.py +0 -0
  370. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/storage/test_init.py +0 -0
  371. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/storage/test_thumbnails.py +0 -0
  372. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/storage/test_validation.py +0 -0
  373. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/support.py +0 -0
  374. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_ai_provider_tokens.py +0 -0
  375. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_app.py +0 -0
  376. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_assignment_writer.py +0 -0
  377. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_auth_settings.py +0 -0
  378. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_backfill_embeddings.py +0 -0
  379. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_blueprint_attributes.py +0 -0
  380. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_blueprints.py +0 -0
  381. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_clickhouse_project_ids.py +0 -0
  382. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_commit_pushed_at_repair.py +0 -0
  383. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_commit_sync_queue.py +0 -0
  384. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_commit_sync_service.py +0 -0
  385. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_component_facts.py +0 -0
  386. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_deployment_status_repair.py +0 -0
  387. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_deployment_sweeper.py +0 -0
  388. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_deployment_sync_queue.py +0 -0
  389. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_deployment_sync_service.py +0 -0
  390. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_drift.py +0 -0
  391. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_entrypoint.py +0 -0
  392. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_graph_ids.py +0 -0
  393. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_graph_sql.py +0 -0
  394. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_identity_attribution.py +0 -0
  395. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_iggy_metrics.py +0 -0
  396. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_init.py +0 -0
  397. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_installer.py +0 -0
  398. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_lifecycle_dispatch.py +0 -0
  399. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_lifespans.py +0 -0
  400. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_maintenance_log.py +0 -0
  401. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_maintenance_operations.py +0 -0
  402. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_maintenance_state.py +0 -0
  403. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_maintenance_worker.py +0 -0
  404. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_models.py +0 -0
  405. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_openapi.py +0 -0
  406. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_orphan_releases.py +0 -0
  407. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_plugin_schemas.py +0 -0
  408. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_plugins.py +0 -0
  409. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_plugins_assignments.py +0 -0
  410. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_plugins_credentials.py +0 -0
  411. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_pr_sync_queue.py +0 -0
  412. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_pr_sync_service.py +0 -0
  413. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_projects_drift_ranges.py +0 -0
  414. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_prompt_seed.py +0 -0
  415. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_pull_request_resync_clickhouse.py +0 -0
  416. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_pull_request_reviews_clickhouse.py +0 -0
  417. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_pull_requests_pending_deploy.py +0 -0
  418. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_release_promote_service.py +0 -0
  419. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_sbom.py +0 -0
  420. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_scoring_queue.py +0 -0
  421. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_scoring_triggers.py +0 -0
  422. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_secret_github_app.py +0 -0
  423. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_secret_identity.py +0 -0
  424. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_secret_providers.py +0 -0
  425. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_settings.py +0 -0
  426. {imbi_api-2.45.0 → imbi_api-2.46.0}/tests/test_workflow_reconcile.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: imbi-api
3
- Version: 2.45.0
3
+ Version: 2.46.0
4
4
  Summary: Imbi is a DevOps Service Management Platform designed to provide an efficient way to manage a large environment that contains many services and applications.
5
5
  Author-email: Alex Campbell <alexc@aweber.com>, Dave Shawley <daves@aweber.com>, "Gavin M. Roy" <gavinr@aweber.com>
6
6
  License-Expression: BSD-3-Clause
@@ -20,7 +20,7 @@ Requires-Dist: fastapi
20
20
  Requires-Dist: filetype
21
21
  Requires-Dist: httpx<1,>=0.28.1
22
22
  Requires-Dist: idna>=3.18
23
- Requires-Dist: imbi-common[databases,llm,mcp,sentry,server]==2.45.0
23
+ Requires-Dist: imbi-common[databases,llm,mcp,sentry,server]==2.46.0
24
24
  Requires-Dist: jinja2
25
25
  Requires-Dist: jsonpatch>=1.33
26
26
  Requires-Dist: limits<6,>=5
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "imbi-api"
3
- version = "2.45.0"
3
+ version = "2.46.0"
4
4
  description = "Imbi is a DevOps Service Management Platform designed to provide an efficient way to manage a large environment that contains many services and applications."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -29,7 +29,7 @@ dependencies = [
29
29
  "filetype",
30
30
  "httpx>=0.28.1,<1",
31
31
  "idna>=3.18",
32
- "imbi-common[databases,llm,mcp,sentry,server]==2.45.0",
32
+ "imbi-common[databases,llm,mcp,sentry,server]==2.46.0",
33
33
  "jinja2",
34
34
  "jsonpatch>=1.33",
35
35
  "limits>=5,<6",
@@ -30,7 +30,6 @@ from imbi.api.agent_tasks.store import (
30
30
  NewRequest,
31
31
  NewTask,
32
32
  Pool,
33
- PrimaryProject,
34
33
  RequestKeyConflict,
35
34
  RequestNotFound,
36
35
  RequestNotOpen,
@@ -80,6 +79,7 @@ async def store_lifespan() -> abc.AsyncGenerator[TaskStore]:
80
79
  try:
81
80
  await pool.open()
82
81
  store = TaskStore(pool)
82
+ await store.move_legacy_projects()
83
83
  stop = asyncio.Event()
84
84
  sweep = asyncio.create_task(sweeper.run_sweeper(store, stop=stop))
85
85
  try:
@@ -112,7 +112,6 @@ __all__ = [
112
112
  'NewRequest',
113
113
  'NewTask',
114
114
  'Pool',
115
- 'PrimaryProject',
116
115
  'RequestKeyConflict',
117
116
  'RequestNotFound',
118
117
  'RequestNotOpen',
@@ -10,7 +10,7 @@ Both tables are ReplacingMergeTree tables keyed on the identity of the
10
10
  row, so a row that is published two times is stored one time.
11
11
 
12
12
  Request resolutions and terminal outcomes also go to the Operations Log
13
- (I9, N9), as a projection of their events: see :func:`opslog_row`.
13
+ (I9, N9), as a projection of their events: see :func:`opslog_rows`.
14
14
  """
15
15
 
16
16
  import decimal
@@ -45,6 +45,17 @@ ORDER BY seq
45
45
  LIMIT {limit:UInt32}
46
46
  """
47
47
 
48
+ _LAST_AGENT_TURN_QUERY = """
49
+ SELECT JSONExtractString(payload, 'body') AS body
50
+ FROM imbi.agent_task_events
51
+ WHERE organization_id = {organization_id:String}
52
+ AND task_id = {task_id:String}
53
+ AND type = 'turn'
54
+ AND actor_kind = 'agent'
55
+ ORDER BY seq DESC
56
+ LIMIT 1
57
+ """
58
+
48
59
  _EVENT_COUNTS_QUERY = """
49
60
  SELECT task_id, uniqExact(seq) AS events, max(seq) AS max_seq
50
61
  FROM imbi.agent_task_events
@@ -80,6 +91,11 @@ def _optional_str(value: typing.Any) -> str | None:
80
91
  return None if value is None else str(value)
81
92
 
82
93
 
94
+ def _project_ids(task: Row) -> str:
95
+ """The ids of the projects of a task, comma separated."""
96
+ return ','.join(project['project_id'] for project in task['projects'])
97
+
98
+
83
99
  def event_row(task: Row, event: Row) -> Row:
84
100
  """Return the ``agent_task_events`` row of one Postgres event."""
85
101
  return {
@@ -88,7 +104,7 @@ def event_row(task: Row, event: Row) -> Row:
88
104
  'short_id': task['short_id'],
89
105
  'agent_id': task['agent_id'],
90
106
  'agent_version': task['agent_version'],
91
- 'project_id': task['project_id'] or '',
107
+ 'project_id': _project_ids(task),
92
108
  'seq': event['seq'],
93
109
  'event_id': str(event['event_id']),
94
110
  'type': event['type'],
@@ -109,7 +125,7 @@ def usage_row(task: Row, ledger: Row) -> Row:
109
125
  'task_id': str(task['id']),
110
126
  'short_id': task['short_id'],
111
127
  'agent_id': task['agent_id'],
112
- 'project_id': task['project_id'] or '',
128
+ 'project_id': _project_ids(task),
113
129
  'session_id': _optional_str(ledger['session_id']),
114
130
  'report_id': str(ledger['id']),
115
131
  'model_id': ledger['model_id'] or '',
@@ -136,17 +152,18 @@ def usage_row(task: Row, ledger: Row) -> Row:
136
152
  return row
137
153
 
138
154
 
139
- def opslog_row(task: Row, event: Row) -> Row | None:
140
- """Return the ``operations_log`` row of an event, or ``None``.
155
+ def opslog_rows(task: Row, event: Row) -> list[Row]:
156
+ """Return the ``operations_log`` rows of an event: one row for each
157
+ project of the task.
141
158
 
142
- Only a resolution or an outcome of a task with a project has a row.
143
- The Operations Log is project scoped, and its list is not scoped to
144
- an organization, so a task with no project never goes there. The
145
- description has no task, request, or reply text. The row id is the
146
- event id, so a row that is published again collapses.
159
+ Only a resolution or an outcome has rows. The Operations Log is
160
+ project scoped, and its list is not scoped to an organization, so a
161
+ task with no project never goes there. The description has no task,
162
+ request, or reply text. The row id comes from the event id and the
163
+ project id, so a row that is published again collapses.
147
164
  """
148
- if event['type'] not in OPSLOG_EVENT_TYPES or not task['project_slug']:
149
- return None
165
+ if event['type'] not in OPSLOG_EVENT_TYPES:
166
+ return []
150
167
  payload = event['payload']
151
168
  if event['type'] == 'request.resolved':
152
169
  description = f'{task["short_id"]}: {payload["kind"]} request '
@@ -154,21 +171,24 @@ def opslog_row(task: Row, event: Row) -> Row | None:
154
171
  else:
155
172
  description = f'{task["short_id"]} closed: {payload["outcome"]}'
156
173
  actor = event['actor_id'] or 'imbi'
157
- entry = models.OperationLog(
158
- id=str(event['event_id']),
159
- occurred_at=event['at'],
160
- recorded_by=actor,
161
- performed_by=actor,
162
- completed_at=event['at'],
163
- project_id=task['project_id'],
164
- project_slug=task['project_slug'],
165
- environment_slug='',
166
- entry_type='Agent Task',
167
- description=description,
168
- )
169
- row = entry.model_dump(by_alias=True, mode='python')
170
- row['is_deleted'] = 0
171
- return row
174
+ result: list[Row] = []
175
+ for project in task['projects']:
176
+ entry = models.OperationLog(
177
+ id=str(uuid.uuid5(event['event_id'], project['project_id'])),
178
+ occurred_at=event['at'],
179
+ recorded_by=actor,
180
+ performed_by=actor,
181
+ completed_at=event['at'],
182
+ project_id=project['project_id'],
183
+ project_slug=project['project_slug'],
184
+ environment_slug='',
185
+ entry_type='Agent Task',
186
+ description=description,
187
+ )
188
+ row = entry.model_dump(by_alias=True, mode='python')
189
+ row['is_deleted'] = 0
190
+ result.append(row)
191
+ return result
172
192
 
173
193
 
174
194
  async def publish(
@@ -182,9 +202,7 @@ async def publish(
182
202
  try:
183
203
  # Before the events: when this publish fails, the sweep finds
184
204
  # the events missing and publishes both again.
185
- opslog = [
186
- row for e in events if (row := opslog_row(task, e)) is not None
187
- ]
205
+ opslog = [row for e in events for row in opslog_rows(task, e)]
188
206
  if opslog:
189
207
  await iggy.publish_rows(OPSLOG_STREAM, OPSLOG_TOPIC, opslog)
190
208
  if events:
@@ -233,6 +251,21 @@ async def archived_events(task: Row, after_seq: int, limit: int) -> list[Row]:
233
251
  ]
234
252
 
235
253
 
254
+ async def last_archived_agent_turn(task: Row) -> str | None:
255
+ """Return the body of the newest agent ``turn`` of an archived task.
256
+
257
+ ``None`` when the agent wrote no turn.
258
+ """
259
+ rows = await clickhouse.query(
260
+ _LAST_AGENT_TURN_QUERY,
261
+ {
262
+ 'organization_id': task['organization_id'],
263
+ 'task_id': str(task['id']),
264
+ },
265
+ )
266
+ return str(rows[0]['body']) if rows else None
267
+
268
+
236
269
  async def event_counts(
237
270
  organization_id: str, task_ids: list[str]
238
271
  ) -> dict[str, tuple[int, int]]:
@@ -24,10 +24,10 @@ agent_id = "TEXT NOT NULL"
24
24
  agent_version = "INTEGER NOT NULL"
25
25
  prompt_version = "INTEGER"
26
26
  service_account_id = "TEXT NOT NULL"
27
+ # Not used. A task had one primary project here before it had a list
28
+ # of projects (`task_projects`). Each start of imbi-api moves a value
29
+ # that it finds to `task_projects` and clears it.
27
30
  project_id = "TEXT"
28
- # The slug of the project at creation, for the Operations Log rows of
29
- # the task. NULL for a task with no project, and for project tasks made
30
- # before this column existed.
31
31
  project_slug = "TEXT"
32
32
  title = "TEXT NOT NULL"
33
33
  description = "TEXT NOT NULL"
@@ -64,6 +64,10 @@ chain_id = "UUID"
64
64
  # (`harness.platform_configs`) when the task was made. NULL when the
65
65
  # organization had no configuration: the defaults apply.
66
66
  platform_config_version = "INTEGER"
67
+ # When a person marked a closed task as needing no more attention, and
68
+ # the email of that person. The dashboard does not show it then.
69
+ acknowledged_at = "TIMESTAMP WITH TIME ZONE"
70
+ acknowledged_by = "TEXT"
67
71
 
68
72
  [tables.primary_key]
69
73
  columns = ["id"]
@@ -269,8 +273,7 @@ columns = ["organization_id", "dependent_task_id", "prerequisite_task_id"]
269
273
  name = "task_dependencies_prerequisite_idx"
270
274
  columns = ["organization_id", "prerequisite_task_id"]
271
275
 
272
- # One row per project that a task touches, other than its primary
273
- # project (F13). The project is a graph node; `project_slug` is its slug
276
+ # One row per project of a task (F13). The project is a graph node; `project_slug` is its slug
274
277
  # when the row was added, so the UI can name a project that was deleted.
275
278
  [[tables]]
276
279
  name = "task_projects"
@@ -110,6 +110,22 @@ _OPEN_REQUESTS = sql.SQL(
110
110
  ' AND (r.expires_at IS NULL OR r.expires_at > NOW())'
111
111
  )
112
112
 
113
+ #: The projects of task ``t`` (F13), oldest first, as a JSON list of
114
+ #: ``project_id`` and ``project_slug``. Every task row has it.
115
+ PROJECTS: typing.LiteralString = (
116
+ '(SELECT coalesce(jsonb_agg(jsonb_build_object('
117
+ "'project_id', p.project_id, 'project_slug', p.project_slug)"
118
+ " ORDER BY p.added_at, p.project_id), '[]'::jsonb)"
119
+ ' FROM agent_runtime.task_projects AS p'
120
+ ' WHERE p.organization_id = t.organization_id AND p.task_id = t.id)'
121
+ ' AS projects'
122
+ )
123
+
124
+ #: A task row with its projects. Continue it with a WHERE clause.
125
+ _SELECT_TASKS: typing.LiteralString = (
126
+ 'SELECT t.*, ' + PROJECTS + ' FROM agent_runtime.tasks AS t' # noqa: S608
127
+ )
128
+
113
129
  #: The tasks at the other end of the dependencies of a task, with the
114
130
  #: link. ``this`` is the column of the task, ``other`` the other column.
115
131
  _LINKED_TASKS = sql.SQL(
@@ -198,10 +214,6 @@ class SelfDependency(ValueError):
198
214
  """A task cannot depend on itself."""
199
215
 
200
216
 
201
- class PrimaryProject(ValueError):
202
- """The project is the primary project of the task."""
203
-
204
-
205
217
  @dataclasses.dataclass(frozen=True)
206
218
  class Actor:
207
219
  """Who caused an event, and through which channel."""
@@ -220,7 +232,6 @@ class NewTask:
220
232
  agent_version: int
221
233
  prompt_version: int | None
222
234
  service_account_id: str
223
- project_id: str | None
224
235
  title: str
225
236
  description: str
226
237
  origin_kind: str
@@ -229,7 +240,10 @@ class NewTask:
229
240
  idempotency_key: str | None
230
241
  owner: str
231
242
  budget: decimal.Decimal | None
232
- project_slug: str | None = None
243
+ #: The ``(project_id, project_slug)`` of each project of the task.
244
+ #: A child task (origin ``task``) gets the projects of its parent
245
+ #: instead.
246
+ projects: abc.Sequence[tuple[str, str]] = ()
233
247
  #: The root task of the chain of a child task. ``None`` makes the
234
248
  #: task the root of its own chain.
235
249
  chain_id: uuid.UUID | None = None
@@ -437,6 +451,30 @@ class TaskStore:
437
451
  if write.task['status'] == 'closed':
438
452
  await _run_close_hooks(write.task)
439
453
 
454
+ async def move_legacy_projects(self) -> int:
455
+ """Move the old single project of each task (``tasks.project_id``)
456
+ to ``task_projects``, and clear it. Return the number of tasks.
457
+
458
+ A task had one primary project before it had a project list. A
459
+ second run moves nothing, so each start runs it.
460
+ """
461
+ async with self._pool.connection() as conn, conn.transaction():
462
+ cursor = await conn.execute(
463
+ 'WITH moved AS (UPDATE agent_runtime.tasks AS t'
464
+ ' SET project_id = NULL, project_slug = NULL'
465
+ ' FROM agent_runtime.tasks AS o'
466
+ ' WHERE o.id = t.id AND o.project_id IS NOT NULL'
467
+ ' RETURNING t.organization_id, t.id, t.created_at,'
468
+ ' o.project_id, o.project_slug)'
469
+ ' INSERT INTO agent_runtime.task_projects (organization_id,'
470
+ ' task_id, project_id, project_slug, added_by_kind,'
471
+ ' added_by, added_at)'
472
+ ' SELECT organization_id, id, project_id,'
473
+ " coalesce(project_slug, project_id), 'system', 'migration',"
474
+ ' created_at FROM moved ON CONFLICT DO NOTHING'
475
+ )
476
+ return cursor.rowcount
477
+
440
478
  async def find_by_idempotency_key(
441
479
  self, organization_id: str, origin_kind: str, origin_id: str, key: str
442
480
  ) -> Row | None:
@@ -444,8 +482,8 @@ class TaskStore:
444
482
  async with self._pool.connection() as conn:
445
483
  return await _fetch_one(
446
484
  conn,
447
- 'SELECT * FROM agent_runtime.tasks'
448
- ' WHERE organization_id = %s AND origin_kind = %s'
485
+ _SELECT_TASKS
486
+ + ' WHERE organization_id = %s AND origin_kind = %s'
449
487
  ' AND origin_id = %s AND idempotency_key = %s',
450
488
  (organization_id, origin_kind, origin_id, key),
451
489
  )
@@ -480,11 +518,11 @@ class TaskStore:
480
518
  await conn.execute(
481
519
  'INSERT INTO agent_runtime.tasks (id, organization_id,'
482
520
  ' short_id, agent_id, agent_version, prompt_version,'
483
- ' service_account_id, project_id, project_slug, title,'
521
+ ' service_account_id, title,'
484
522
  ' description, origin_kind, origin_id, origin,'
485
523
  ' idempotency_key, owner, budget, chain_id,'
486
524
  ' platform_config_version) VALUES (%s,'
487
- ' %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,'
525
+ ' %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,'
488
526
  ' %s, %s, %s, (SELECT max(version)'
489
527
  ' FROM harness.platform_configs'
490
528
  ' WHERE organization_id = %s))',
@@ -496,8 +534,6 @@ class TaskStore:
496
534
  task.agent_version,
497
535
  task.prompt_version,
498
536
  task.service_account_id,
499
- task.project_id,
500
- task.project_slug,
501
537
  task.title,
502
538
  task.description,
503
539
  task.origin_kind,
@@ -510,6 +546,45 @@ class TaskStore:
510
546
  task.organization_id,
511
547
  ),
512
548
  )
549
+ if task.origin_kind == 'task':
550
+ # The parent row is locked, so its projects cannot
551
+ # change until this commits.
552
+ await conn.execute(
553
+ 'INSERT INTO agent_runtime.task_projects'
554
+ ' (organization_id, task_id, project_id,'
555
+ ' project_slug, added_by_kind, added_by)'
556
+ ' SELECT organization_id, %s, project_id,'
557
+ ' project_slug, %s, %s'
558
+ ' FROM agent_runtime.task_projects'
559
+ ' WHERE organization_id = %s AND task_id = %s::uuid',
560
+ (
561
+ task_id,
562
+ actor.kind,
563
+ actor.id,
564
+ task.organization_id,
565
+ task.origin_id,
566
+ ),
567
+ )
568
+ elif task.projects:
569
+ async with conn.cursor() as cursor:
570
+ await cursor.executemany(
571
+ 'INSERT INTO agent_runtime.task_projects'
572
+ ' (organization_id, task_id, project_id,'
573
+ ' project_slug, added_by_kind, added_by)'
574
+ ' VALUES (%s, %s, %s, %s, %s, %s)'
575
+ ' ON CONFLICT DO NOTHING',
576
+ [
577
+ (
578
+ task.organization_id,
579
+ task_id,
580
+ project_id,
581
+ project_slug,
582
+ actor.kind,
583
+ actor.id,
584
+ )
585
+ for project_id, project_slug in task.projects
586
+ ],
587
+ )
513
588
  event = await _append_event(
514
589
  conn, task_id, 'task.created', event_payload, actor
515
590
  )
@@ -537,8 +612,8 @@ class TaskStore:
537
612
  async with self._pool.connection() as conn:
538
613
  return await _fetch_one(
539
614
  conn,
540
- 'SELECT * FROM agent_runtime.tasks'
541
- ' WHERE organization_id = %s AND short_id = %s',
615
+ _SELECT_TASKS
616
+ + ' WHERE organization_id = %s AND short_id = %s',
542
617
  (organization_id, short_id),
543
618
  )
544
619
 
@@ -558,8 +633,8 @@ class TaskStore:
558
633
  """Return tasks in the organization, newest first.
559
634
 
560
635
  ``text`` matches the title or the short id. A task also matches
561
- when its agent is in ``text_agent_ids`` or its project is in
562
- ``text_project_ids``; the caller finds those in the graph.
636
+ when its agent is in ``text_agent_ids`` or one of its projects is
637
+ in ``text_project_ids``; the caller finds those in the graph.
563
638
  ``before`` is the ``(created_at, id)`` keyset of the last row of
564
639
  the previous page. Each row also has ``blocked_since``: for a
565
640
  task that waits on a person (see :data:`_OPEN_REQUESTS`), the
@@ -582,7 +657,10 @@ class TaskStore:
582
657
  conditions.append(
583
658
  sql.SQL(
584
659
  '(title ILIKE %s OR short_id ILIKE %s'
585
- ' OR agent_id = ANY(%s) OR project_id = ANY(%s))'
660
+ ' OR agent_id = ANY(%s) OR EXISTS (SELECT 1'
661
+ ' FROM agent_runtime.task_projects AS tp'
662
+ ' WHERE tp.organization_id = t.organization_id'
663
+ ' AND tp.task_id = t.id AND tp.project_id = ANY(%s)))'
586
664
  )
587
665
  )
588
666
  params += [
@@ -595,7 +673,8 @@ class TaskStore:
595
673
  conditions.append(sql.SQL('(created_at, id) < (%s, %s)'))
596
674
  params += list(before)
597
675
  statement = sql.SQL(
598
- "SELECT t.*, CASE WHEN t.status <> 'closed' THEN"
676
+ 'SELECT t.*, ' + PROJECTS + ',' # noqa: S608
677
+ " CASE WHEN t.status <> 'closed' THEN"
599
678
  ' (SELECT min(r.opened_at) {open}) END AS blocked_since'
600
679
  ' FROM agent_runtime.tasks AS t WHERE {where}'
601
680
  ' ORDER BY created_at DESC, id DESC LIMIT %s'
@@ -639,20 +718,33 @@ class TaskStore:
639
718
  )
640
719
  return await cursor.fetchall()
641
720
 
642
- async def last_agent_turn(self, task_id: uuid.UUID) -> str | None:
721
+ async def last_agent_turn(self, task: Row) -> str | None:
643
722
  """Return the body of the newest ``turn`` of the agent.
644
723
 
645
- ``None`` when the agent wrote no turn, or the log is archived.
724
+ It reads ClickHouse when the log of the task is archived.
725
+ ``None`` when the agent wrote no turn.
646
726
  """
727
+ if task['log_archived_at'] is not None:
728
+ return await log.last_archived_agent_turn(task)
729
+ # One statement reads the archive state and the turn from the same
730
+ # snapshot, so an archive that commits after the caller read
731
+ # ``task`` cannot hide the turn.
647
732
  async with self._pool.connection() as conn:
648
733
  cursor = await conn.execute(
649
- "SELECT payload->>'body' FROM agent_runtime.events"
650
- " WHERE task_id = %s AND type = 'turn'"
651
- " AND actor_kind = 'agent' ORDER BY seq DESC LIMIT 1",
652
- (task_id,),
734
+ 'SELECT t.log_archived_at IS NOT NULL,'
735
+ " (SELECT e.payload->>'body' FROM agent_runtime.events AS e"
736
+ " WHERE e.task_id = t.id AND e.type = 'turn'"
737
+ " AND e.actor_kind = 'agent' ORDER BY e.seq DESC LIMIT 1)"
738
+ ' FROM agent_runtime.tasks AS t WHERE t.id = %s',
739
+ (task['id'],),
653
740
  )
654
741
  row = await cursor.fetchone()
655
- return row[0] if row else None
742
+ if row is None:
743
+ return None
744
+ archived, body = row
745
+ if archived:
746
+ return await log.last_archived_agent_turn(task)
747
+ return None if body is None else str(body)
656
748
 
657
749
  async def unarchived(self) -> list[Row]:
658
750
  """Return the tasks whose log is not archived, by organization.
@@ -664,7 +756,8 @@ class TaskStore:
664
756
  conn.cursor(row_factory=rows.dict_row) as cursor,
665
757
  ):
666
758
  await cursor.execute(
667
- 'SELECT t.*, (SELECT count(*) FROM agent_runtime.budget_ledger'
759
+ 'SELECT t.*, ' + PROJECTS + ',' # noqa: S608
760
+ ' (SELECT count(*) FROM agent_runtime.budget_ledger'
668
761
  ' AS l WHERE l.task_id = t.id) AS ledger_count'
669
762
  ' FROM agent_runtime.tasks AS t'
670
763
  ' WHERE t.log_archived_at IS NULL'
@@ -745,8 +838,8 @@ class TaskStore:
745
838
  async with self._pool.connection() as conn, conn.transaction():
746
839
  task = await _fetch_one(
747
840
  conn,
748
- 'SELECT * FROM agent_runtime.tasks'
749
- ' WHERE organization_id = %s AND short_id = %s FOR UPDATE',
841
+ _SELECT_TASKS
842
+ + ' WHERE organization_id = %s AND short_id = %s FOR UPDATE',
750
843
  (organization_id, short_id),
751
844
  )
752
845
  if task is None:
@@ -875,6 +968,40 @@ class TaskStore:
875
968
  await write.update({'owner': owner})
876
969
  return write.task
877
970
 
971
+ async def acknowledge(
972
+ self, organization_id: str, short_id: str, actor_id: str | None
973
+ ) -> Row:
974
+ """Mark a closed task as seen by ``actor_id``, and return it.
975
+
976
+ The first acknowledgement stays. A closed task does not take
977
+ events, so this changes only the row.
978
+
979
+ Raises:
980
+ TaskNotFound: No such task.
981
+ TaskOpen: The task is not closed.
982
+
983
+ """
984
+ async with self._pool.connection() as conn:
985
+ task = await _fetch_one(
986
+ conn,
987
+ 'UPDATE agent_runtime.tasks AS t' # noqa: S608
988
+ ' SET acknowledged_at = COALESCE(acknowledged_at, NOW()),'
989
+ ' acknowledged_by = COALESCE(acknowledged_by, %s)'
990
+ ' WHERE organization_id = %s AND short_id = %s'
991
+ " AND status = 'closed' RETURNING t.*, " + PROJECTS,
992
+ (actor_id, organization_id, short_id),
993
+ )
994
+ if task is not None:
995
+ return task
996
+ cursor = await conn.execute(
997
+ 'SELECT 1 FROM agent_runtime.tasks'
998
+ ' WHERE organization_id = %s AND short_id = %s',
999
+ (organization_id, short_id),
1000
+ )
1001
+ if await cursor.fetchone() is None:
1002
+ raise TaskNotFound(short_id)
1003
+ raise TaskOpen(short_id)
1004
+
878
1005
  # --- Relations ----------------------------------------------------
879
1006
 
880
1007
  @contextlib.asynccontextmanager
@@ -997,7 +1124,7 @@ class TaskStore:
997
1124
  project_slug: str,
998
1125
  actor: Actor,
999
1126
  ) -> bool:
1000
- """Associate a project, other than the primary, with a task.
1127
+ """Associate a project with a task.
1001
1128
 
1002
1129
  The caller checks that the project is in the organization. Write
1003
1130
  ``project.associated``. Return ``False`` and write no event when
@@ -1006,12 +1133,9 @@ class TaskStore:
1006
1133
  Raises:
1007
1134
  TaskNotFound: No such task.
1008
1135
  TaskArchived: The task is archived.
1009
- PrimaryProject: The project is the primary project.
1010
1136
 
1011
1137
  """
1012
1138
  async with self._relate(organization_id, [short_id]) as relate:
1013
- if relate.tasks[0]['project_id'] == project_id:
1014
- raise PrimaryProject(project_id)
1015
1139
  row = await _fetch_one(
1016
1140
  relate.conn,
1017
1141
  'INSERT INTO agent_runtime.task_projects (organization_id,'
@@ -1097,14 +1221,14 @@ class TaskStore:
1097
1221
  parent: Row | None = None
1098
1222
  if task['origin_kind'] == 'task':
1099
1223
  await cursor.execute(
1100
- 'SELECT * FROM agent_runtime.tasks'
1101
- ' WHERE organization_id = %s AND id::text = %s',
1224
+ _SELECT_TASKS
1225
+ + ' WHERE organization_id = %s AND id::text = %s',
1102
1226
  (task['organization_id'], task['origin_id']),
1103
1227
  )
1104
1228
  parent = await cursor.fetchone()
1105
1229
  await cursor.execute(
1106
- 'SELECT * FROM agent_runtime.tasks'
1107
- " WHERE organization_id = %s AND origin_kind = 'task'"
1230
+ _SELECT_TASKS
1231
+ + " WHERE organization_id = %s AND origin_kind = 'task'"
1108
1232
  ' AND origin_id = %s ORDER BY created_at, id',
1109
1233
  (task['organization_id'], str(task['id'])),
1110
1234
  )
@@ -1715,8 +1839,8 @@ class TaskStore:
1715
1839
  async with self._pool.connection() as conn:
1716
1840
  task = await _fetch_one(
1717
1841
  conn,
1718
- 'SELECT * FROM agent_runtime.tasks'
1719
- ' WHERE organization_id = %s AND short_id = %s',
1842
+ _SELECT_TASKS
1843
+ + ' WHERE organization_id = %s AND short_id = %s',
1720
1844
  (organization_id, short_id),
1721
1845
  )
1722
1846
  if task is None:
@@ -1761,8 +1885,8 @@ async def requeue_chain(
1761
1885
  """
1762
1886
  async with conn.cursor(row_factory=rows.dict_row) as cursor:
1763
1887
  await cursor.execute(
1764
- 'SELECT * FROM agent_runtime.tasks'
1765
- ' WHERE (chain_id = %s OR (chain_id IS NULL AND id = %s))'
1888
+ _SELECT_TASKS
1889
+ + ' WHERE (chain_id = %s OR (chain_id IS NULL AND id = %s))'
1766
1890
  " AND status <> 'closed' ORDER BY id FOR UPDATE",
1767
1891
  (chain_id, chain_id),
1768
1892
  )
@@ -1822,7 +1946,7 @@ async def append_event(
1822
1946
  """
1823
1947
  task = await _fetch_one(
1824
1948
  conn,
1825
- 'SELECT * FROM agent_runtime.tasks WHERE id = %s'
1949
+ _SELECT_TASKS + ' WHERE id = %s'
1826
1950
  ' AND organization_id = %s AND log_archived_at IS NULL FOR UPDATE',
1827
1951
  (task_id, organization_id),
1828
1952
  )
@@ -2039,7 +2163,7 @@ async def _fetch_task(
2039
2163
  ) -> Row:
2040
2164
  row = await _fetch_one(
2041
2165
  conn,
2042
- 'SELECT * FROM agent_runtime.tasks WHERE id = %s',
2166
+ _SELECT_TASKS + ' WHERE id = %s',
2043
2167
  (task_id,),
2044
2168
  )
2045
2169
  if row is None: # pragma: no cover - read inside the write transaction
@@ -2055,8 +2179,8 @@ async def _lock_task(
2055
2179
  """Lock and return an open task, for a change in this transaction."""
2056
2180
  task = await _fetch_one(
2057
2181
  conn,
2058
- 'SELECT * FROM agent_runtime.tasks'
2059
- ' WHERE organization_id = %s AND short_id = %s FOR UPDATE',
2182
+ _SELECT_TASKS
2183
+ + ' WHERE organization_id = %s AND short_id = %s FOR UPDATE',
2060
2184
  (organization_id, short_id),
2061
2185
  )
2062
2186
  if task is None:
@@ -2072,8 +2196,8 @@ async def _lock_tasks(
2072
2196
  """Lock and return tasks in task id order, closed or not."""
2073
2197
  async with conn.cursor(row_factory=rows.dict_row) as cursor:
2074
2198
  await cursor.execute(
2075
- 'SELECT * FROM agent_runtime.tasks'
2076
- ' WHERE organization_id = %s AND short_id = ANY(%s)'
2199
+ _SELECT_TASKS
2200
+ + ' WHERE organization_id = %s AND short_id = ANY(%s)'
2077
2201
  ' ORDER BY id FOR UPDATE',
2078
2202
  (organization_id, short_ids),
2079
2203
  )
@@ -2122,8 +2246,8 @@ async def _update_task(
2122
2246
  changes: Row,
2123
2247
  ) -> Row:
2124
2248
  statement = sql.SQL(
2125
- 'UPDATE agent_runtime.tasks SET {sets}, updated_at = NOW()'
2126
- ' WHERE id = %s RETURNING *'
2249
+ 'UPDATE agent_runtime.tasks AS t SET {sets}, updated_at = NOW()' # noqa: S608
2250
+ ' WHERE id = %s RETURNING t.*, ' + PROJECTS
2127
2251
  ).format(
2128
2252
  sets=sql.SQL(', ').join(
2129
2253
  sql.SQL('{col} = %s').format(col=sql.Identifier(col))