imbi-api 2.11.6__tar.gz → 2.12.2__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 (333) hide show
  1. {imbi_api-2.11.6 → imbi_api-2.12.2}/PKG-INFO +2 -9
  2. imbi_api-2.12.2/docs/adr/0016-delegated-execution-for-scheduled-jobs.md +261 -0
  3. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr.md +31 -0
  4. {imbi_api-2.11.6 → imbi_api-2.12.2}/pyproject.toml +3 -11
  5. imbi_api-2.12.2/src/imbi_api/auth/login_providers.py +205 -0
  6. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/models.py +2 -2
  7. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/oauth.py +8 -16
  8. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/permissions.py +3 -5
  9. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/seed.py +88 -17
  10. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/backfill_embeddings.py +1 -1
  11. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/commit_sync/service.py +57 -125
  12. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/domain/models.py +178 -319
  13. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/__init__.py +4 -0
  14. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/_helpers.py +25 -22
  15. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/_json_fields.py +1 -1
  16. imbi_api-2.12.2/src/imbi_api/endpoints/admin_plugins.py +156 -0
  17. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/auth.py +50 -452
  18. imbi_api-2.12.2/src/imbi_api/endpoints/auth_providers.py +335 -0
  19. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/blueprints.py +1 -1
  20. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/comments.py +1 -1
  21. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/events.py +5 -5
  22. imbi_api-2.12.2/src/imbi_api/endpoints/integrations.py +884 -0
  23. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/organizations.py +3 -23
  24. imbi_api-2.12.2/src/imbi_api/endpoints/plugins.py +45 -0
  25. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_analysis.py +47 -55
  26. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_commit_sync.py +14 -12
  27. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_configuration.py +62 -89
  28. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_deployments.py +31 -34
  29. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_incidents.py +15 -17
  30. imbi_api-2.12.2/src/imbi_api/endpoints/project_integrations.py +271 -0
  31. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_logs.py +48 -45
  32. imbi_api-2.12.2/src/imbi_api/endpoints/project_plugins.py +142 -0
  33. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_pr_sync.py +12 -9
  34. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/projects.py +41 -37
  35. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/pull_requests.py +10 -6
  36. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/search.py +1 -1
  37. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/user_activity.py +2 -2
  38. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/users.py +6 -5
  39. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/webhooks.py +44 -44
  40. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/entrypoint.py +6 -49
  41. imbi_api-2.12.2/src/imbi_api/identity/attribution.py +112 -0
  42. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/endpoints.py +54 -39
  43. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/errors.py +9 -7
  44. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/flows.py +152 -267
  45. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/host_integration.py +66 -42
  46. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/models.py +11 -9
  47. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/repository.py +126 -48
  48. imbi_api-2.12.2/src/imbi_api/identity/resolution.py +237 -0
  49. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/state.py +6 -6
  50. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/sweeper.py +20 -19
  51. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/models.py +13 -4
  52. imbi_api-2.12.2/src/imbi_api/plugins/assignment_writer.py +125 -0
  53. imbi_api-2.12.2/src/imbi_api/plugins/assignments.py +255 -0
  54. imbi_api-2.12.2/src/imbi_api/plugins/credentials.py +184 -0
  55. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/plugins/lifecycle.py +3 -3
  56. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/plugins/lifecycle_dispatch.py +47 -44
  57. imbi_api-2.12.2/src/imbi_api/plugins/resolution.py +225 -0
  58. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/pr_sync/service.py +60 -125
  59. imbi_api-2.12.2/tests/auth/test_login_providers.py +291 -0
  60. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_oauth.py +8 -5
  61. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_seed.py +41 -0
  62. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_admin_plugins.py +31 -31
  63. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_auth.py +91 -701
  64. imbi_api-2.12.2/tests/endpoints/test_auth_providers.py +304 -0
  65. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_events.py +6 -8
  66. imbi_api-2.12.2/tests/endpoints/test_integrations.py +709 -0
  67. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_plugin_label_validation.py +31 -27
  68. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_plugins.py +41 -38
  69. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_analysis.py +57 -48
  70. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_commit_sync.py +5 -5
  71. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_configuration.py +69 -55
  72. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_deployments.py +102 -134
  73. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_incidents.py +67 -49
  74. imbi_api-2.12.2/tests/endpoints/test_project_integrations.py +179 -0
  75. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_lifecycle.py +7 -5
  76. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_logs.py +71 -60
  77. imbi_api-2.12.2/tests/endpoints/test_project_plugins.py +143 -0
  78. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_response_surfacing.py +2 -2
  79. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_projects.py +38 -27
  80. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_releases.py +3 -2
  81. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_service_writeback.py +13 -9
  82. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_users.py +8 -8
  83. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_webhooks.py +42 -40
  84. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_endpoints.py +40 -6
  85. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_errors.py +4 -4
  86. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_flows.py +211 -149
  87. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_host_integration.py +89 -9
  88. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_repository.py +13 -12
  89. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_resolution.py +173 -97
  90. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_state.py +14 -14
  91. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_sweeper.py +13 -13
  92. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/support.py +31 -0
  93. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_assignment_writer.py +30 -27
  94. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_commit_sync_service.py +53 -77
  95. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_entrypoint.py +17 -44
  96. imbi_api-2.12.2/tests/test_identity_attribution.py +128 -0
  97. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_lifecycle_dispatch.py +119 -161
  98. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_models.py +12 -190
  99. imbi_api-2.12.2/tests/test_plugins.py +942 -0
  100. imbi_api-2.12.2/tests/test_plugins_assignments.py +186 -0
  101. imbi_api-2.12.2/tests/test_plugins_credentials.py +183 -0
  102. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_pr_sync_service.py +53 -77
  103. {imbi_api-2.11.6 → imbi_api-2.12.2}/uv.lock +14 -120
  104. imbi_api-2.11.6/CODE_REVIEW_PUNCHLIST.md +0 -172
  105. imbi_api-2.11.6/src/imbi_api/auth/login_providers.py +0 -268
  106. imbi_api-2.11.6/src/imbi_api/endpoints/admin_plugins.py +0 -502
  107. imbi_api-2.11.6/src/imbi_api/endpoints/auth_providers.py +0 -673
  108. imbi_api-2.11.6/src/imbi_api/endpoints/identity_plugins.py +0 -79
  109. imbi_api-2.11.6/src/imbi_api/endpoints/plugins.py +0 -65
  110. imbi_api-2.11.6/src/imbi_api/endpoints/project_plugins.py +0 -180
  111. imbi_api-2.11.6/src/imbi_api/endpoints/project_type_plugins.py +0 -140
  112. imbi_api-2.11.6/src/imbi_api/endpoints/service_plugins.py +0 -922
  113. imbi_api-2.11.6/src/imbi_api/endpoints/third_party_services.py +0 -1691
  114. imbi_api-2.11.6/src/imbi_api/identity/attribution.py +0 -130
  115. imbi_api-2.11.6/src/imbi_api/identity/resolution.py +0 -203
  116. imbi_api-2.11.6/src/imbi_api/plugins/assignment_writer.py +0 -140
  117. imbi_api-2.11.6/src/imbi_api/plugins/assignments.py +0 -168
  118. imbi_api-2.11.6/src/imbi_api/plugins/credentials.py +0 -363
  119. imbi_api-2.11.6/src/imbi_api/plugins/resolution.py +0 -706
  120. imbi_api-2.11.6/tests/auth/test_login_providers.py +0 -282
  121. imbi_api-2.11.6/tests/endpoints/test_auth_providers.py +0 -621
  122. imbi_api-2.11.6/tests/endpoints/test_project_plugins.py +0 -193
  123. imbi_api-2.11.6/tests/endpoints/test_project_type_plugins.py +0 -151
  124. imbi_api-2.11.6/tests/endpoints/test_service_plugins.py +0 -1191
  125. imbi_api-2.11.6/tests/endpoints/test_third_party_services.py +0 -2153
  126. imbi_api-2.11.6/tests/test_identity_attribution.py +0 -158
  127. imbi_api-2.11.6/tests/test_plugins.py +0 -1982
  128. imbi_api-2.11.6/tests/test_resolution_identity.py +0 -49
  129. {imbi_api-2.11.6 → imbi_api-2.12.2}/.github/workflows/deploy.yaml +0 -0
  130. {imbi_api-2.11.6 → imbi_api-2.12.2}/.github/workflows/docs.yaml +0 -0
  131. {imbi_api-2.11.6 → imbi_api-2.12.2}/.github/workflows/testing.yaml +0 -0
  132. {imbi_api-2.11.6 → imbi_api-2.12.2}/.gitignore +0 -0
  133. {imbi_api-2.11.6 → imbi_api-2.12.2}/.pre-commit-config.yaml +0 -0
  134. {imbi_api-2.11.6 → imbi_api-2.12.2}/.python-version +0 -0
  135. {imbi_api-2.11.6 → imbi_api-2.12.2}/CLAUDE.md +0 -0
  136. {imbi_api-2.11.6 → imbi_api-2.12.2}/LICENSE +0 -0
  137. {imbi_api-2.11.6 → imbi_api-2.12.2}/README.md +0 -0
  138. {imbi_api-2.11.6 → imbi_api-2.12.2}/coderabbit.yaml +0 -0
  139. {imbi_api-2.11.6 → imbi_api-2.12.2}/compose.yaml +0 -0
  140. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0001-record-architecture-decisions.md +0 -0
  141. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0002-authentication-and-authorization-architecture.md +0 -0
  142. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0003-email-sending-architecture.md +0 -0
  143. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0004-phase-5-authentication-enhancements.md +0 -0
  144. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0005-file-upload-storage-architecture.md +0 -0
  145. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0006-project-identity-and-multi-type.md +0 -0
  146. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0007-relationship-blueprints.md +0 -0
  147. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0008-plugin-system-architecture.md +0 -0
  148. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0009-database-driven-oauth-providers.md +0 -0
  149. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0010-identity-plugin-architecture.md +0 -0
  150. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0011-graph-based-project-scoring.md +0 -0
  151. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0012-plugin-manifest-service-template.md +0 -0
  152. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0013-deployment-plugin-type.md +0 -0
  153. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0014-generic-plugin-entity-abstraction.md +0 -0
  154. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0015-cyclonedx-1.7-sbom-standard.md +0 -0
  155. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/configuration.md +0 -0
  156. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/index.md +0 -0
  157. {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/restore-backup.md +0 -0
  158. {imbi_api-2.11.6 → imbi_api-2.12.2}/justfile +0 -0
  159. {imbi_api-2.11.6 → imbi_api-2.12.2}/mkdocs.yml +0 -0
  160. {imbi_api-2.11.6 → imbi_api-2.12.2}/mypy.ini +0 -0
  161. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/__init__.py +0 -0
  162. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/app.py +0 -0
  163. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/__init__.py +0 -0
  164. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/authorization_codes.py +0 -0
  165. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/local_auth.py +0 -0
  166. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/membership.py +0 -0
  167. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/oauth_clients.py +0 -0
  168. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/password.py +0 -0
  169. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/sessions.py +0 -0
  170. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/tokens.py +0 -0
  171. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/totp.py +0 -0
  172. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/blueprint_attributes.py +0 -0
  173. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/blueprint_compliance.py +0 -0
  174. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/commit_sync/__init__.py +0 -0
  175. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/commit_sync/queue.py +0 -0
  176. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/domain/__init__.py +0 -0
  177. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/domain/scoring.py +0 -0
  178. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/__init__.py +0 -0
  179. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/client.py +0 -0
  180. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/dependencies.py +0 -0
  181. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/models.py +0 -0
  182. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/templates/base.html +0 -0
  183. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/templates/base.txt +0 -0
  184. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/templates/password_reset.html +0 -0
  185. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/templates/password_reset.txt +0 -0
  186. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/templates/welcome.html +0 -0
  187. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/templates/welcome.txt +0 -0
  188. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/templates.py +0 -0
  189. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/_credentials.py +0 -0
  190. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/_pagination.py +0 -0
  191. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/_request_urls.py +0 -0
  192. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/admin.py +0 -0
  193. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/api_keys.py +0 -0
  194. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/client_credentials.py +0 -0
  195. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/dashboard.py +0 -0
  196. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/document_templates.py +0 -0
  197. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/documents.py +0 -0
  198. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/environments.py +0 -0
  199. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/graph_query.py +0 -0
  200. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/link_definitions.py +0 -0
  201. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/local_auth.py +0 -0
  202. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/mcp_servers.py +0 -0
  203. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/mfa.py +0 -0
  204. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/oauth_metadata.py +0 -0
  205. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/operations_log.py +0 -0
  206. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/plugin_edges.py +0 -0
  207. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/plugin_entities.py +0 -0
  208. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_lifecycle.py +0 -0
  209. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_types.py +0 -0
  210. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/releases.py +0 -0
  211. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/roles.py +0 -0
  212. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/sa_api_keys.py +0 -0
  213. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/scoring.py +0 -0
  214. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/scoring_policies.py +0 -0
  215. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/service_accounts.py +0 -0
  216. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/status.py +0 -0
  217. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/tags.py +0 -0
  218. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/teams.py +0 -0
  219. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/uploads.py +0 -0
  220. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/graph_sql.py +0 -0
  221. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/__init__.py +0 -0
  222. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/lifespans.py +0 -0
  223. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/llm/__init__.py +0 -0
  224. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/llm/dependencies.py +0 -0
  225. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/mcp_test.py +0 -0
  226. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/middleware/__init__.py +0 -0
  227. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/middleware/rate_limit.py +0 -0
  228. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/openapi.py +0 -0
  229. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/patch.py +0 -0
  230. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/plugins/__init__.py +0 -0
  231. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/plugins/installer.py +0 -0
  232. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/plugins/reload.py +0 -0
  233. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/plugins/schemas.py +0 -0
  234. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/pr_sync/__init__.py +0 -0
  235. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/pr_sync/queue.py +0 -0
  236. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/prompts/__init__.py +0 -0
  237. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/prompts/release_notes_system.md +0 -0
  238. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/py.typed +0 -0
  239. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/relationships.py +0 -0
  240. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/sbom.py +0 -0
  241. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/scoring/__init__.py +0 -0
  242. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/scoring/queue.py +0 -0
  243. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/settings.py +0 -0
  244. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/storage/__init__.py +0 -0
  245. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/storage/client.py +0 -0
  246. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/storage/dependencies.py +0 -0
  247. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/storage/thumbnails.py +0 -0
  248. {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/storage/validation.py +0 -0
  249. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/__init__.py +0 -0
  250. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/__init__.py +0 -0
  251. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_api_key_auth.py +0 -0
  252. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_authentication.py +0 -0
  253. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_authorization.py +0 -0
  254. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_encryption.py +0 -0
  255. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_membership.py +0 -0
  256. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_oauth_server.py +0 -0
  257. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_permissions.py +0 -0
  258. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_sessions.py +0 -0
  259. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_totp.py +0 -0
  260. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/clickhouse/__init__.py +0 -0
  261. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/clickhouse/test_client.py +0 -0
  262. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/clickhouse/test_init.py +0 -0
  263. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/clickhouse/test_privacy.py +0 -0
  264. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/email/__init__.py +0 -0
  265. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/email/test_client.py +0 -0
  266. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/email/test_init.py +0 -0
  267. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/email/test_integration.py +0 -0
  268. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/email/test_models.py +0 -0
  269. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/email/test_templates.py +0 -0
  270. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/__init__.py +0 -0
  271. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_admin.py +0 -0
  272. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_api_keys.py +0 -0
  273. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_blueprints.py +0 -0
  274. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_client_credentials.py +0 -0
  275. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_comments.py +0 -0
  276. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_credentials.py +0 -0
  277. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_dashboard.py +0 -0
  278. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_document_templates.py +0 -0
  279. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_documents.py +0 -0
  280. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_environments.py +0 -0
  281. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_graph_query.py +0 -0
  282. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_init.py +0 -0
  283. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_json_fields.py +0 -0
  284. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_link_definitions.py +0 -0
  285. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_local_auth.py +0 -0
  286. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_mcp_servers.py +0 -0
  287. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_mfa.py +0 -0
  288. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_operations_log.py +0 -0
  289. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_organizations.py +0 -0
  290. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_pagination.py +0 -0
  291. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_types.py +0 -0
  292. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_projects_helpers.py +0 -0
  293. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_pull_requests.py +0 -0
  294. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_request_urls.py +0 -0
  295. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_roles.py +0 -0
  296. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_sa_api_keys.py +0 -0
  297. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_scoring.py +0 -0
  298. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_scoring_policies.py +0 -0
  299. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_search.py +0 -0
  300. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_service_accounts.py +0 -0
  301. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_status.py +0 -0
  302. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_tags.py +0 -0
  303. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_teams.py +0 -0
  304. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_uploads.py +0 -0
  305. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_user_activity.py +0 -0
  306. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/fixtures/sbom/npm-realistic.json +0 -0
  307. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/fixtures/sbom/pypi-realistic.json +0 -0
  308. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/fixtures/sbom/tiny.json +0 -0
  309. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/__init__.py +0 -0
  310. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/middleware/test_rate_limit.py +0 -0
  311. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/storage/__init__.py +0 -0
  312. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/storage/test_client.py +0 -0
  313. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/storage/test_init.py +0 -0
  314. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/storage/test_thumbnails.py +0 -0
  315. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/storage/test_validation.py +0 -0
  316. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_app.py +0 -0
  317. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_backfill_embeddings.py +0 -0
  318. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_blueprint_attributes.py +0 -0
  319. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_blueprint_compliance.py +0 -0
  320. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_blueprints.py +0 -0
  321. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_commit_sync_queue.py +0 -0
  322. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_graph_sql.py +0 -0
  323. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_init.py +0 -0
  324. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_installer.py +0 -0
  325. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_lifespans.py +0 -0
  326. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_openapi.py +0 -0
  327. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_patch.py +0 -0
  328. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_plugin_schemas.py +0 -0
  329. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_pr_sync_queue.py +0 -0
  330. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_sbom.py +0 -0
  331. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_scoring_queue.py +0 -0
  332. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_scoring_triggers.py +0 -0
  333. {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imbi-api
3
- Version: 2.11.6
3
+ Version: 2.12.2
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: BSD-3-Clause
@@ -20,7 +20,7 @@ Requires-Dist: cyclonedx-python-lib<12,>=11.7
20
20
  Requires-Dist: fastapi
21
21
  Requires-Dist: filetype
22
22
  Requires-Dist: httpx<1,>=0.28.1
23
- Requires-Dist: imbi-common[databases,llm,sentry,server]==2.11.6
23
+ Requires-Dist: imbi-common[databases,llm,sentry,server]==2.12.2
24
24
  Requires-Dist: jinja2
25
25
  Requires-Dist: jsonpatch>=1.33
26
26
  Requires-Dist: mcp>=1.26.0
@@ -42,13 +42,6 @@ Requires-Dist: opentelemetry-instrumentation-asyncio; extra == 'otel'
42
42
  Requires-Dist: opentelemetry-instrumentation-fastapi; extra == 'otel'
43
43
  Requires-Dist: opentelemetry-instrumentation-httpx; extra == 'otel'
44
44
  Requires-Dist: opentelemetry-instrumentation-jinja2; extra == 'otel'
45
- Provides-Extra: plugins
46
- Requires-Dist: imbi-plugin-aws>=2.1.0; extra == 'plugins'
47
- Requires-Dist: imbi-plugin-github>=2.1.0; extra == 'plugins'
48
- Requires-Dist: imbi-plugin-logzio>=2.1.0; extra == 'plugins'
49
- Requires-Dist: imbi-plugin-oidc>=2.1.0; extra == 'plugins'
50
- Requires-Dist: imbi-plugin-pagerduty>=2.1.0; extra == 'plugins'
51
- Requires-Dist: imbi-plugin-sonarqube>=2.1.0; extra == 'plugins'
52
45
  Provides-Extra: sentry
53
46
  Requires-Dist: sentry-sdk; extra == 'sentry'
54
47
  Description-Content-Type: text/markdown
@@ -0,0 +1,261 @@
1
+ # ADR 0016: Delegated Execution for Scheduled Jobs
2
+
3
+ Date: 2026-06-29
4
+
5
+ ## Status
6
+
7
+ Accepted
8
+
9
+ Implementation is phased; the full design lives in
10
+ [`imbi/docs/imbi-scheduler-functional-spec.md`](https://github.com/AWeber-Imbi/imbi/blob/main/docs/imbi-scheduler-functional-spec.md).
11
+ This ADR records the `imbi-api`-side decision only — the new delegation
12
+ primitive and the migration of the in-process scheduled loops off the API
13
+ process. The standalone `imbi-scheduler` service itself is a separate repo and
14
+ not in scope for this ADR log.
15
+
16
+ ## Context
17
+
18
+ Imbi has grown to need general-purpose scheduled execution, and the platform
19
+ has accumulated scheduling-like behavior in an ad-hoc way:
20
+
21
+ 1. **No general scheduler exists.** There is no APScheduler, cron daemon, job
22
+ registry, or recurring-job domain model anywhere in the stack. The graph
23
+ vlabel set (`imbi_common/graph/schemata.toml`) has no `Schedule`/`Job`/
24
+ `Task`/`Trigger` label. `pg_cron` is preloaded and `CREATE EXTENSION`'d but
25
+ unused.
26
+
27
+ 2. **The only existing "scheduled" job is hand-rolled.** `imbi_api/scoring/
28
+ queue.py:451-510` (`run_daily_tick`) is a polling loop on `asyncio.sleep`
29
+ that fires once per UTC day at `DAILY_TICK_HOUR_UTC=6`, using a Valkey
30
+ `SETNX` 25h-TTL lock keyed `imbi:score-recompute:daily:<date>` for
31
+ cross-worker single-firing, then enqueues every project for
32
+ `scheduled_recompute`. It is hardcoded to one task and not generalizable.
33
+ Three more in-process consumer loops (commit-sync, PR-sync,
34
+ identity-sweeper) are wired through `lifespans.py` and run inside the API
35
+ process.
36
+
37
+ 3. **`imbi-automations` is a one-shot CLI, not a scheduler.** It runs workflow
38
+ TOMLs across projects when invoked manually (`just run <workflow>`); it has
39
+ no recurring-execution capability. The missing piece is a trigger layer.
40
+
41
+ 4. **Planned features need scheduling.** Dynamic blueprint-field
42
+ recalculation (e.g. `min(date) where foo = bar` over an applied blueprint
43
+ field) requires periodic re-evaluation. There is no mechanism to schedule
44
+ that today.
45
+
46
+ The platform has decided to introduce a **standalone `imbi-scheduler` service**
47
+ (APScheduler 4.x, owning its own job store and run history) that triggers
48
+ `imbi-api` REST calls on schedules. See the functional spec for that service's
49
+ design; this ADR covers what `imbi-api` must provide for it to work.
50
+
51
+ The hard requirement that drives this ADR: scheduled jobs must be able to
52
+ **execute as the scheduling user** — i.e. with that user's identity and
53
+ permissions, including resource-level `CAN_ACCESS` ACLs — and also as the
54
+ scheduler's own service account for system jobs.
55
+
56
+ The blocker: **`imbi-api` has no impersonation or delegation primitive.**
57
+ `AuthContext` (`auth/permissions.py:43-86`) is always the principal that
58
+ presented the token. Service accounts cannot use resource-level ACLs
59
+ (`permissions.py:907-913` — the `CAN_ACCESS` fallback is `auth.user`-only),
60
+ and admin bypass is `User`-only (`permissions.py:745`). There is no "act on
61
+ behalf of" path, no `act`/`azp` claim, and no consent model. So "run as a user"
62
+ cannot be satisfied by any existing mechanism.
63
+
64
+ ## Decision
65
+
66
+ Add a **delegated-execution primitive** to `imbi-api` based on OAuth2 token
67
+ exchange (RFC 8693), plus a **consent model**, so the scheduler can obtain
68
+ short-lived JWTs scoped to a target user. The scheduler holds no user
69
+ secrets.
70
+
71
+ ### 1. Token-exchange grant
72
+
73
+ Add a branch to `POST /auth/token` (`endpoints/auth.py:325-499`) for:
74
+
75
+ ```
76
+ grant_type = urn:ietf:params:oauth:grant-type:token-exchange
77
+ subject_token = <scheduler SA client-credential JWT> (or client_id+secret)
78
+ requested_subject = <target user email>
79
+ scope = <scope covered by a Consent>
80
+ ```
81
+
82
+ `imbi-api` validates that (a) the scheduler SA is authenticated and holds the
83
+ new `auth:delegate` permission, (b) a non-expired, non-revoked `Consent`
84
+ exists for `(user, 'imbi-scheduler', scope ⊇ requested)`, and (c) the target
85
+ user is active. It then mints a short-lived access JWT via
86
+ `imbi_common.auth.core.create_access_token` carrying:
87
+
88
+ ```
89
+ sub = <target user email>
90
+ auth_method = 'token_exchange'
91
+ act = { client_id: <scheduler SA> } # RFC 8693 `act` claim
92
+ azp = <scheduler SA slug>
93
+ scope = <granted scope>
94
+ jti = new; TokenMetadata node ISSUED_TO the user, linked to the SA
95
+ ```
96
+
97
+ ### 2. Consent model
98
+
99
+ Add a **`Consent`** graph node recording that a user authorized the scheduler
100
+ to act on their behalf for a scope. `Consent` is recorded under the **user's
101
+ own** authenticated session (the scheduler passes the user's JWT through when
102
+ creating a schedule), so `imbi-api` attributes consent to the real user — the
103
+ scheduler never records consent on a user's behalf:
104
+
105
+ ```
106
+ Consent:
107
+ user_email, actor_sa ('imbi-scheduler'), scope,
108
+ created_at, expires_at?, revoked_at?
109
+ ```
110
+
111
+ Endpoints: `POST /consents` (records under caller identity), `GET /consents`,
112
+ `DELETE /consents/{id}` (revoke). v1 uses one broad scope (`imbi-api:*`) per
113
+ user; finer per-resource-type scopes are a deferred refinement.
114
+
115
+ ### 3. `AuthContext.actor`
116
+
117
+ `AuthContext` gains an `actor: ServiceAccount | None` field populated when
118
+ `auth_method == 'token_exchange'`. Permission resolution uses the **user's**
119
+ permissions (including `CAN_ACCESS` resource ACLs — the whole point of
120
+ delegation). `principal_name` remains the user; a new `actor_name` property
121
+ returns the scheduler SA. Audit/`requested_by`-style attribution records
122
+ `"{user} on behalf of {actor}"`. The `act`/`azp` claim is verified on every
123
+ request, not only at exchange.
124
+
125
+ ### 4. New permission & service account
126
+
127
+ Seed a new `auth:delegate` permission. Provision an `imbi-scheduler` service
128
+ account (`POST /service-accounts`) with a client credential
129
+ (`POST /service-accounts/{slug}/client-credentials`) whose role grants
130
+ `auth:delegate` plus the global permissions each system job needs (resource
131
+ ACLs are user-only, so system jobs require global perms — document the set
132
+ per migrated task). System jobs authenticate directly with the SA's
133
+ client-credential JWT (refreshed proactively, the
134
+ `imbi-automations/clients/imbi.py:_AuthManager` pattern); user jobs exchange
135
+ for a delegated token at fire time.
136
+
137
+ ### 5. Migrate the score-recompute daily tick
138
+
139
+ Externalize the trigger of `run_daily_tick` to the scheduler; **keep the
140
+ worker/queue logic in `imbi-api`** (the `imbi:score-recompute` Valkey-stream
141
+ consumer stays). Concretely: expose (or reuse) an HTTP trigger —
142
+ `POST /api/scoring/recompute-all` guarded by a new
143
+ `scoring:trigger-scheduled` (or `scoring:recompute`) permission — that
144
+ enqueues every project with reason `scheduled_recompute`. Remove
145
+ `run_daily_tick` and its task from `score_worker_hook`
146
+ (`lifespans.py:202-237`); keep `consume_recompute`. The scheduler seeds a
147
+ system schedule `0 6 * * *` → `POST /api/scoring/recompute-all`. The Valkey
148
+ per-date `SETNX` dedupe lock is no longer needed (APScheduler's shared data
149
+ store guarantees single firing across replicas); the existing per-project
150
+ debounce + idempotency in `enqueue_recompute` remains as a backstop.
151
+
152
+ The remaining in-process loops (identity-token sweeper, commit/PR-sync pause
153
+ sweeps) are assessed per-task as follow-ups; only score recompute is in the
154
+ first cut.
155
+
156
+ ## Alternatives Considered
157
+
158
+ ### A. Per-user API keys stored by the scheduler
159
+
160
+ Users mint `ik_…` API keys and register them with the scheduler, which stores
161
+ them encrypted and replays them at fire time. **Rejected:** the scheduler
162
+ becomes a custodian of long-lived user secrets; keys don't expire unless
163
+ rotated; revocation is manual and scattered; and it conflates "a credential
164
+ the user happened to create" with "an authorization to act." Token exchange
165
+ keeps secrets server-side and makes consent an explicit, revocable record.
166
+
167
+ ### B. Run everything as a service account with `requested_by` attribution only
168
+
169
+ Every job runs as the scheduler SA; the originating user is recorded as
170
+ `requested_by`/`actor` for audit only, not as the security principal.
171
+ **Rejected for user jobs:** this does not satisfy "execute as them." The job
172
+ would run with the SA's global permissions and could not pass resource-level
173
+ `CAN_ACCESS` checks, so a user-scheduled job targeting a resource they can
174
+ access but the SA cannot would 403. This remains the model for **system**
175
+ jobs, where it is correct.
176
+
177
+ ### C. `pg_cron` driving DB-side jobs
178
+
179
+ The Postgres image already loads `pg_cron`. **Rejected as the primary
180
+ engine:** it can only run SQL, not call arbitrary `imbi-api` endpoints or
181
+ speak the auth/delegation flow, and it couples scheduling to the database
182
+ process. It may still be useful for pure-SQL maintenance later, but it is not
183
+ the general scheduler. The standalone APScheduler service is the engine.
184
+
185
+ ### D. Build the scheduler inside `imbi-api` (in-process)
186
+
187
+ Add APScheduler to `imbi-api` and run schedules as a lifespan loop, reusing
188
+ the existing auth context directly. **Rejected:** the platform has
189
+ explicitly chosen a standalone scheduler repo (separable deploy/scaling,
190
+ independent failure domain, and a clean home for run-history that does not
191
+ belong in the graph). The delegation primitive is precisely what lets a
192
+ separate service run as a user safely.
193
+
194
+ ## Consequences
195
+
196
+ ### Positive
197
+
198
+ - **True run-as-user.** Delegated JWTs run with the user's full permissions,
199
+ including resource ACLs that service accounts cannot use — the capability
200
+ that did not exist before.
201
+ - **No stored user secrets.** The scheduler stores a `consent_id`, not user
202
+ tokens or passwords. The decisive security advantage over per-user keys.
203
+ - **Explicit, revocable authorization.** Consent is a first-class, auditable
204
+ graph node; revoking a schedule or consent stops future exchanges.
205
+ - **Short-lived, revocable delegated tokens** with `jti`-tracked
206
+ `TokenMetadata`, reusing the existing revocation machinery.
207
+ - **General scheduler replaces a hand-rolled loop.** The score daily tick
208
+ becomes one cron entry; future scheduled work (dynamic field recalculation,
209
+ `imbi-automations` triggers) has a home.
210
+ - **Attribution is honest.** `actor_name` + `principal_name` make "who caused
211
+ this and on whose behalf" explicit in logs and audit.
212
+
213
+ ### Negative
214
+
215
+ - **New grant type + `AuthContext.actor` + `Consent` model is real surface
216
+ area** in `imbi-api`: auth is the most security-sensitive part of the
217
+ system, and this touches `authenticate_jwt`, token issuance, and the
218
+ permissions module. Requires thorough tests (consent required, scope
219
+ checked, short-lived, revocable, `act`/`azp` verified on every request).
220
+ - **Two systems must agree on consent lifecycle.** The scheduler stores
221
+ `consent_id`; `imbi-api` owns the `Consent` node. A revoked/consent or
222
+ deleted schedule must suppress firings (the scheduler re-checks consent
223
+ before each exchange and treats a failed exchange as a `skipped` run).
224
+ - **System jobs need global permissions.** Because resource ACLs are
225
+ user-only, every system job's SA role must hold the global permission for
226
+ the endpoint it calls. This widens the scheduler SA's blast radius; it is
227
+ mitigated by scoping the role to exactly the trigger endpoints and by
228
+ preferring delegated (user) jobs where the action is user-scoped.
229
+ - **Migration moves a working path.** The score daily tick works today; moving
230
+ it introduces a transitional risk. Mitigated by keeping per-day idempotency
231
+ so a double-fire is harmless even before APScheduler's single-firing
232
+ guarantee applies.
233
+
234
+ ### Risks Accepted
235
+
236
+ - **Broad v1 consent scope (`imbi-api:*`).** A consenting user authorizes the
237
+ scheduler to call any `imbi-api` endpoint as them. Acceptable for an
238
+ internal, admin-curated platform for v1; finer scopes are an explicit
239
+ follow-up (functional spec §15).
240
+ - **Delegated token lifetime (5–15 min).** Chosen short; a job making many
241
+ calls reuses the token until expiry then re-exchanges. Not a refresh-token
242
+ family for v1.
243
+ - **Target-path allow-listing deferred.** v1 allows arbitrary `imbi-api`
244
+ method+path as a job target. If abuse becomes a concern, restrict to an
245
+ allow-list per scope (functional spec §12).
246
+
247
+ ## References
248
+
249
+ - [`imbi/docs/imbi-scheduler-functional-spec.md`](https://github.com/AWeber-Imbi/imbi/blob/main/docs/imbi-scheduler-functional-spec.md)
250
+ — Full scheduler service design, data model, REST API, and phased plan.
251
+ - ADR 0002: Authentication and Authorization Architecture (the auth system
252
+ this extends — JWTs, service accounts, API keys, permission model).
253
+ - [RFC 8693: OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)
254
+ — `act`/`azp` claims and the token-exchange grant type.
255
+ - `imbi_api/auth/permissions.py:43-86,306-417,907-913` — `AuthContext`,
256
+ `authenticate_jwt`, and the user-only `CAN_ACCESS` fallback that motivates
257
+ delegation.
258
+ - `imbi_api/scoring/queue.py:451-510` and `imbi_api/lifespans.py:202-237` —
259
+ the `run_daily_tick` loop being externalized.
260
+ - `imbi-automations/src/imbi_automations/clients/imbi.py:87-180` — the
261
+ `_AuthManager` reference pattern for SA client-credential refresh.
@@ -285,6 +285,37 @@ Replaces hand-coded per-plugin entity routers (e.g., `aws_accounts.py`) with a g
285
285
 
286
286
  ---
287
287
 
288
+ ### Operations & Integrations
289
+
290
+ #### [ADR 0015: CycloneDX 1.7 as the SBoM Standard](adr/0015-cyclonedx-1.7-sbom-standard.md)
291
+
292
+ *Date: 2026-05-27 | Status: Accepted*
293
+
294
+ Standardizes the SBoM spec version accepted on `PUT .../releases/{id}/sbom` and the recommended producer for build CI.
295
+
296
+ **Key Decisions:**
297
+
298
+ - Accept CycloneDX 1.7 on the wire (1.5/1.6 silently drop fields Imbi reads)
299
+ - Single recommended producer that covers the polyglot service population and emits the per-component metadata needed for `ReleaseComponentEdge` `scope`/`groups`
300
+
301
+ ---
302
+
303
+ #### [ADR 0016: Delegated Execution for Scheduled Jobs](adr/0016-delegated-execution-for-scheduled-jobs.md)
304
+
305
+ *Date: 2026-06-29 | Status: Accepted*
306
+
307
+ Adds an OAuth2 token-exchange (RFC 8693) delegation primitive plus a `Consent` model so the standalone `imbi-scheduler` service can run scheduled jobs as a user, and externalizes the score-recompute daily tick off the API process.
308
+
309
+ **Key Decisions:**
310
+
311
+ - New `urn:ietf:params:oauth:grant-type:token-exchange` grant minting short-lived JWTs with `sub=<user>` and an `act`/`azp` claim naming the scheduler SA
312
+ - `Consent` graph node recorded under the user's own session; scheduler stores no user secrets
313
+ - `AuthContext.actor` field + `actor_name`; permission resolution uses the user's permissions (including user-only `CAN_ACCESS` ACLs)
314
+ - New `auth:delegate` permission and `imbi-scheduler` service account; system jobs use the SA directly
315
+ - Score-recompute daily tick (`run_daily_tick`) moves to a scheduler-owned cron; the Valkey-stream consumer stays in `imbi-api`
316
+
317
+ ---
318
+
288
319
  ## ADR Format
289
320
 
290
321
  Our ADRs follow this structure:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "imbi-api"
3
- version = "2.11.6"
3
+ version = "2.12.2"
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"
@@ -28,7 +28,7 @@ dependencies = [
28
28
  "fastapi",
29
29
  "filetype",
30
30
  "httpx>=0.28.1,<1",
31
- "imbi-common[databases,llm,sentry,server]==2.11.6",
31
+ "imbi-common[databases,llm,sentry,server]==2.12.2",
32
32
  "jinja2",
33
33
  "jsonpatch>=1.33",
34
34
  "mcp>=1.26.0",
@@ -57,14 +57,6 @@ otel = [
57
57
  "opentelemetry-instrumentation-httpx",
58
58
  "opentelemetry-instrumentation-jinja2",
59
59
  ]
60
- plugins = [
61
- "imbi-plugin-aws>=2.1.0",
62
- "imbi-plugin-github>=2.1.0",
63
- "imbi-plugin-logzio>=2.1.0",
64
- "imbi-plugin-oidc>=2.1.0",
65
- "imbi-plugin-pagerduty>=2.1.0",
66
- "imbi-plugin-sonarqube>=2.1.0"
67
- ]
68
60
  sentry = ["sentry-sdk"]
69
61
 
70
62
  [dependency-groups]
@@ -231,7 +223,7 @@ max-complexity = 15
231
223
 
232
224
  [tool.uv]
233
225
  exclude-newer = "7 days"
234
- exclude-newer-package = { "clickhouse-connect" = false, "imbi-common" = false, "imbi-plugin-aws" = false, "imbi-plugin-github" = false, "imbi-plugin-logzio" = false, "imbi-plugin-oidc" = false, "imbi-plugin-pagerduty" = false, "imbi-plugin-sentry" = false, "imbi-plugin-sonarqube" = false }
226
+ exclude-newer-package = { "clickhouse-connect" = false, "imbi-common" = false }
235
227
 
236
228
  [[tool.uv.index]]
237
229
  url = "https://pypi.org/simple"
@@ -0,0 +1,205 @@
1
+ """Login-provider repository.
2
+
3
+ Plugin Architecture v3: a login provider is an ``Integration`` whose
4
+ plugin declares an ``identity`` capability with the ``login_capable``
5
+ hint, and whose node carries a direct ``used_as_login=true`` property
6
+ (set by the admin UI when promoting the Integration to a login
7
+ provider). There is exactly one source now -- the legacy
8
+ ``ServiceApplication`` / hardcoded ``google``/``github``/``oidc`` rows
9
+ and the separate ``:Plugin`` node lookup are both gone.
10
+
11
+ A 30s in-memory TTL cache keeps the OAuth hot path off the graph.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ import time
18
+ import typing
19
+
20
+ import pydantic
21
+ from imbi_common import graph
22
+ from imbi_common.plugins import (
23
+ PluginNotFoundError,
24
+ decrypt_integration_credentials,
25
+ get_plugin,
26
+ )
27
+
28
+ from imbi_api import settings
29
+ from imbi_api.plugins.assignments import (
30
+ capability_enabled,
31
+ hydrate_integration,
32
+ )
33
+
34
+ LOGGER = logging.getLogger(__name__)
35
+
36
+ _CACHE_TTL_SECONDS = 30.0
37
+
38
+ _provider_cache: dict[str, tuple[LoginApp, float]] = {}
39
+ _list_cache: dict[bool, tuple[list[LoginApp], float]] = {}
40
+
41
+
42
+ class LoginApp(pydantic.BaseModel):
43
+ """Flat view of a login-eligible Integration.
44
+
45
+ Combines the Integration's decrypted OAuth credentials with its
46
+ ``options`` (issuer URL, OAuth endpoints, etc.) so callers don't
47
+ need to re-query the graph or decrypt anything themselves.
48
+ """
49
+
50
+ model_config = pydantic.ConfigDict(extra='ignore')
51
+
52
+ slug: str
53
+ name: str
54
+ integration_id: str
55
+ #: The plugin's declared ``auth_type`` (``oauth2``, ``oidc``,
56
+ #: ``aws-iam-ic``, ...), or ``integration.options['oauth_app_type']``
57
+ #: when the plugin overrides it.
58
+ oauth_app_type: str
59
+ client_id: str | None = None
60
+ client_secret: str | None = None
61
+ issuer_url: str | None = None
62
+ allowed_domains: list[str] = pydantic.Field(default_factory=list)
63
+ scopes: list[str] = pydantic.Field(default_factory=list)
64
+ status: str = 'active'
65
+ authorization_endpoint: str | None = None
66
+ token_endpoint: str | None = None
67
+ revoke_endpoint: str | None = None
68
+ callback_url: str = ''
69
+
70
+
71
+ def invalidate_cache(slug: str | None = None) -> None:
72
+ """Drop cached entries.
73
+
74
+ ``slug=None`` clears everything; otherwise only the slug + lists.
75
+ """
76
+ if slug is None:
77
+ _provider_cache.clear()
78
+ else:
79
+ _provider_cache.pop(slug, None)
80
+ _list_cache.clear()
81
+
82
+
83
+ _LOGIN_INTEGRATIONS_QUERY: typing.LiteralString = """
84
+ MATCH (i:Integration)
85
+ WHERE i.used_as_login = true
86
+ RETURN i
87
+ ORDER BY i.slug
88
+ """
89
+
90
+ _LOGIN_INTEGRATION_QUERY: typing.LiteralString = """
91
+ MATCH (i:Integration {{slug: {slug}}})
92
+ WHERE i.used_as_login = true
93
+ RETURN i
94
+ LIMIT 1
95
+ """
96
+
97
+
98
+ def _integration_to_login_app(
99
+ integration: dict[str, typing.Any],
100
+ ) -> LoginApp | None:
101
+ """Materialize a ``LoginApp`` from a hydrated Integration, or
102
+ ``None`` when its plugin doesn't declare a login-capable identity
103
+ capability or the capability is disabled."""
104
+ plugin_slug = str(integration.get('plugin') or '')
105
+ try:
106
+ entry = get_plugin(plugin_slug)
107
+ except PluginNotFoundError:
108
+ return None
109
+ capability = entry.manifest.get_capability('identity')
110
+ if capability is None or not capability.hints.get('login_capable'):
111
+ return None
112
+ if not capability_enabled(integration, 'identity'):
113
+ return None
114
+
115
+ # An Integration with no stable ``id`` cannot back a login flow
116
+ # (``load_integration`` matches on ``id``); skip it rather than let a
117
+ # single malformed node 500 the whole login page.
118
+ integration_id = integration.get('id')
119
+ if not integration_id:
120
+ LOGGER.warning(
121
+ 'Integration %r is marked used_as_login but has no id; skipping',
122
+ integration.get('slug'),
123
+ )
124
+ return None
125
+
126
+ options = typing.cast(
127
+ 'dict[str, typing.Any]', integration.get('options') or {}
128
+ )
129
+ credentials = decrypt_integration_credentials(
130
+ typing.cast(
131
+ 'dict[str, str]', integration.get('encrypted_credentials') or {}
132
+ )
133
+ )
134
+ slug = str(integration['slug'])
135
+ return LoginApp(
136
+ slug=slug,
137
+ name=str(integration.get('name') or slug),
138
+ integration_id=str(integration_id),
139
+ oauth_app_type=str(
140
+ options.get('oauth_app_type') or entry.manifest.auth_type
141
+ ),
142
+ client_id=credentials.get('client_id'),
143
+ client_secret=credentials.get('client_secret'),
144
+ issuer_url=options.get('issuer_url'),
145
+ allowed_domains=list(options.get('allowed_domains') or []),
146
+ scopes=list(options.get('scopes') or []),
147
+ status=str(integration.get('status') or 'active'),
148
+ authorization_endpoint=options.get('authorization_endpoint'),
149
+ token_endpoint=options.get('token_endpoint'),
150
+ revoke_endpoint=options.get('revoke_endpoint'),
151
+ callback_url=settings.oauth_callback_url(slug),
152
+ )
153
+
154
+
155
+ async def list_login_apps(
156
+ db: graph.Graph,
157
+ *,
158
+ enabled_only: bool = False,
159
+ ) -> list[LoginApp]:
160
+ """Return every login-eligible Integration.
161
+
162
+ Cached per ``enabled_only`` for ``_CACHE_TTL_SECONDS``.
163
+ """
164
+ cached = _list_cache.get(enabled_only)
165
+ now = time.time()
166
+ if cached is not None and (now - cached[1]) < _CACHE_TTL_SECONDS:
167
+ return list(cached[0])
168
+
169
+ records = await db.execute(_LOGIN_INTEGRATIONS_QUERY, {}, ['i'])
170
+ apps: list[LoginApp] = []
171
+ for record in records:
172
+ props: typing.Any = graph.parse_agtype(record['i'])
173
+ if not isinstance(props, dict):
174
+ continue
175
+ typed_props = typing.cast('dict[str, typing.Any]', props)
176
+ login_app = _integration_to_login_app(hydrate_integration(typed_props))
177
+ if login_app is None:
178
+ continue
179
+ if enabled_only and login_app.status != 'active':
180
+ continue
181
+ apps.append(login_app)
182
+
183
+ _list_cache[enabled_only] = (list(apps), now)
184
+ return apps
185
+
186
+
187
+ async def get_login_app(db: graph.Graph, slug: str) -> LoginApp | None:
188
+ """Look up a single login app by Integration slug."""
189
+ cached = _provider_cache.get(slug)
190
+ now = time.time()
191
+ if cached is not None and (now - cached[1]) < _CACHE_TTL_SECONDS:
192
+ return cached[0]
193
+
194
+ records = await db.execute(_LOGIN_INTEGRATION_QUERY, {'slug': slug}, ['i'])
195
+ if not records:
196
+ return None
197
+ props: typing.Any = graph.parse_agtype(records[0]['i'])
198
+ if not isinstance(props, dict):
199
+ return None
200
+ typed_props = typing.cast('dict[str, typing.Any]', props)
201
+ login_app = _integration_to_login_app(hydrate_integration(typed_props))
202
+ if login_app is None:
203
+ return None
204
+ _provider_cache[slug] = (login_app, now)
205
+ return login_app
@@ -53,7 +53,7 @@ class OAuthStateData(pydantic.BaseModel):
53
53
  * ``intent`` discriminates ``'login'`` from ``'identity'``. Login
54
54
  flows still create the local user; identity flows persist an
55
55
  :class:`imbi_common.models.IdentityConnection` for the actor.
56
- * ``plugin_id`` names the target identity plugin (or ``None`` for
56
+ * ``integration_id`` names the target Integration (or ``None`` for
57
57
  the legacy hardcoded login providers).
58
58
  * ``code_verifier`` carries the PKCE verifier through the redirect
59
59
  so we don't need server-side state for in-flight flows.
@@ -72,7 +72,7 @@ class OAuthStateData(pydantic.BaseModel):
72
72
  redirect_uri: str # Where to redirect after auth
73
73
  timestamp: int # Unix timestamp for expiry
74
74
  intent: typing.Literal['login', 'identity'] = 'login'
75
- plugin_id: str | None = None
75
+ integration_id: str | None = None
76
76
  code_verifier: str | None = None
77
77
  return_to: str | None = None
78
78
  actor_user_id: str | None = None
@@ -13,7 +13,6 @@ from urllib import parse as urlparse
13
13
  import httpx
14
14
  import jwt
15
15
  from imbi_common import graph
16
- from imbi_common.auth import encryption
17
16
  from valkey import asyncio as valkey_module
18
17
 
19
18
  from imbi_api import settings
@@ -279,7 +278,7 @@ async def exchange_oauth_code(
279
278
  """Exchange OAuth authorization code for tokens.
280
279
 
281
280
  Args:
282
- slug: ServiceApplication slug for the login provider
281
+ slug: Login-Integration slug
283
282
  code: Authorization code from provider
284
283
  redirect_uri: Redirect URI used in authorization request
285
284
  db: Graph database used to look up provider configuration
@@ -327,7 +326,7 @@ async def fetch_oauth_profile(
327
326
  """Fetch user profile from OAuth provider.
328
327
 
329
328
  Args:
330
- slug: ServiceApplication slug for the login provider
329
+ slug: Login-Integration slug
331
330
  access_token: Access token from provider
332
331
  db: Graph database used to look up provider configuration
333
332
 
@@ -446,17 +445,6 @@ async def _load_active_login_app(
446
445
  return app
447
446
 
448
447
 
449
- def _decrypt_secret(app: login_providers.LoginApp) -> str:
450
- """Decrypt the row's ``client_secret_encrypted`` value."""
451
- if not app.client_secret_encrypted:
452
- return ''
453
- encryptor = encryption.TokenEncryption.get_instance()
454
- secret = encryptor.decrypt(app.client_secret_encrypted)
455
- if secret is None:
456
- raise ValueError('Failed to decrypt OAuth client secret')
457
- return secret
458
-
459
-
460
448
  async def _get_provider_config(
461
449
  slug: str, db: graph.Graph
462
450
  ) -> tuple[str, str, str]:
@@ -467,7 +455,11 @@ async def _get_provider_config(
467
455
  """
468
456
  app = await _load_active_login_app(slug, db)
469
457
  client_id = app.client_id or ''
470
- client_secret = _decrypt_secret(app)
458
+ # LoginApp.client_secret is already plaintext -- Integration
459
+ # credentials are decrypted once when the login app is materialized
460
+ # (imbi_api.auth.login_providers), unlike the old ServiceApplication
461
+ # row which stored an encrypted value this module used to decrypt.
462
+ client_secret = app.client_secret or ''
471
463
  if app.oauth_app_type == 'google':
472
464
  return (
473
465
  'https://oauth2.googleapis.com/token',
@@ -480,7 +472,7 @@ async def _get_provider_config(
480
472
  client_id,
481
473
  client_secret,
482
474
  )
483
- # oidc: prefer parent ThirdPartyService.token_endpoint, else discover
475
+ # oidc: prefer the Integration's configured token_endpoint, else discover
484
476
  if app.token_endpoint:
485
477
  return (app.token_endpoint, client_id, client_secret)
486
478
  if not app.issuer_url: