ciris-agent 1.7.7__py3-none-any.whl

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 (986) hide show
  1. ciris_adapters/README.md +113 -0
  2. ciris_adapters/__init__.py +30 -0
  3. ciris_adapters/ciris_covenant_metrics/README.md +144 -0
  4. ciris_adapters/ciris_covenant_metrics/__init__.py +36 -0
  5. ciris_adapters/ciris_covenant_metrics/adapter.py +249 -0
  6. ciris_adapters/ciris_covenant_metrics/manifest.json +152 -0
  7. ciris_adapters/ciris_covenant_metrics/services.py +403 -0
  8. ciris_adapters/ciris_hosted_tools/__init__.py +24 -0
  9. ciris_adapters/ciris_hosted_tools/adapter.py +169 -0
  10. ciris_adapters/ciris_hosted_tools/manifest.json +94 -0
  11. ciris_adapters/ciris_hosted_tools/services.py +744 -0
  12. ciris_adapters/external_data_sql/README.md +559 -0
  13. ciris_adapters/external_data_sql/__init__.py +43 -0
  14. ciris_adapters/external_data_sql/adapter.py +144 -0
  15. ciris_adapters/external_data_sql/configurable.py +315 -0
  16. ciris_adapters/external_data_sql/dialects/__init__.py +37 -0
  17. ciris_adapters/external_data_sql/dialects/base.py +133 -0
  18. ciris_adapters/external_data_sql/dialects/mysql.py +63 -0
  19. ciris_adapters/external_data_sql/dialects/postgresql.py +59 -0
  20. ciris_adapters/external_data_sql/dialects/sqlite.py +62 -0
  21. ciris_adapters/external_data_sql/example_config.json +88 -0
  22. ciris_adapters/external_data_sql/example_privacy_schema.yaml +127 -0
  23. ciris_adapters/external_data_sql/manifest.json +195 -0
  24. ciris_adapters/external_data_sql/privacy_schema_loader.py +189 -0
  25. ciris_adapters/external_data_sql/protocol.py +101 -0
  26. ciris_adapters/external_data_sql/schemas.py +146 -0
  27. ciris_adapters/external_data_sql/service.py +1547 -0
  28. ciris_adapters/external_data_sql/service_old.py +492 -0
  29. ciris_adapters/home_assistant/__init__.py +63 -0
  30. ciris_adapters/home_assistant/adapter.py +201 -0
  31. ciris_adapters/home_assistant/communication_service.py +347 -0
  32. ciris_adapters/home_assistant/configurable.py +667 -0
  33. ciris_adapters/home_assistant/manifest.json +203 -0
  34. ciris_adapters/home_assistant/schemas.py +129 -0
  35. ciris_adapters/home_assistant/service.py +751 -0
  36. ciris_adapters/home_assistant/tool_service.py +441 -0
  37. ciris_adapters/mcp_client/__init__.py +82 -0
  38. ciris_adapters/mcp_client/adapter.py +847 -0
  39. ciris_adapters/mcp_client/config.py +280 -0
  40. ciris_adapters/mcp_client/configurable.py +422 -0
  41. ciris_adapters/mcp_client/manifest.json +185 -0
  42. ciris_adapters/mcp_client/mcp_communication_service.py +393 -0
  43. ciris_adapters/mcp_client/mcp_tool_service.py +463 -0
  44. ciris_adapters/mcp_client/mcp_wise_service.py +394 -0
  45. ciris_adapters/mcp_client/schemas.py +149 -0
  46. ciris_adapters/mcp_client/security.py +592 -0
  47. ciris_adapters/mcp_common/__init__.py +44 -0
  48. ciris_adapters/mcp_common/manifest.json +25 -0
  49. ciris_adapters/mcp_common/protocol.py +315 -0
  50. ciris_adapters/mcp_common/schemas.py +225 -0
  51. ciris_adapters/mcp_server/__init__.py +47 -0
  52. ciris_adapters/mcp_server/adapter.py +581 -0
  53. ciris_adapters/mcp_server/config.py +260 -0
  54. ciris_adapters/mcp_server/configurable.py +393 -0
  55. ciris_adapters/mcp_server/handlers.py +663 -0
  56. ciris_adapters/mcp_server/manifest.json +211 -0
  57. ciris_adapters/mcp_server/security.py +500 -0
  58. ciris_adapters/mock_llm/README.md +117 -0
  59. ciris_adapters/mock_llm/__init__.py +21 -0
  60. ciris_adapters/mock_llm/adapter.py +131 -0
  61. ciris_adapters/mock_llm/configurable.py +237 -0
  62. ciris_adapters/mock_llm/manifest.json +106 -0
  63. ciris_adapters/mock_llm/protocol.py +37 -0
  64. ciris_adapters/mock_llm/responses.py +520 -0
  65. ciris_adapters/mock_llm/responses_action_selection.py +1041 -0
  66. ciris_adapters/mock_llm/responses_epistemic.py +17 -0
  67. ciris_adapters/mock_llm/responses_feedback.py +27 -0
  68. ciris_adapters/mock_llm/schemas.py +35 -0
  69. ciris_adapters/mock_llm/service.py +294 -0
  70. ciris_adapters/navigation/__init__.py +21 -0
  71. ciris_adapters/navigation/adapter.py +129 -0
  72. ciris_adapters/navigation/configurable.py +239 -0
  73. ciris_adapters/navigation/manifest.json +104 -0
  74. ciris_adapters/navigation/service.py +487 -0
  75. ciris_adapters/reddit/README.md +132 -0
  76. ciris_adapters/reddit/REDDIT_ADAPTER_ANALYSIS.md +715 -0
  77. ciris_adapters/reddit/REDDIT_ADAPTER_SUMMARY.txt +278 -0
  78. ciris_adapters/reddit/REDDIT_ANALYSIS_INDEX.md +307 -0
  79. ciris_adapters/reddit/REDDIT_PRODUCTION_READINESS_PLAN.md +518 -0
  80. ciris_adapters/reddit/__init__.py +15 -0
  81. ciris_adapters/reddit/adapter.py +189 -0
  82. ciris_adapters/reddit/configurable.py +274 -0
  83. ciris_adapters/reddit/error_handler.py +307 -0
  84. ciris_adapters/reddit/manifest.json +218 -0
  85. ciris_adapters/reddit/observer.py +532 -0
  86. ciris_adapters/reddit/protocol.py +34 -0
  87. ciris_adapters/reddit/schemas.py +433 -0
  88. ciris_adapters/reddit/service.py +1471 -0
  89. ciris_adapters/sample_adapter/README.md +474 -0
  90. ciris_adapters/sample_adapter/__init__.py +45 -0
  91. ciris_adapters/sample_adapter/adapter.py +208 -0
  92. ciris_adapters/sample_adapter/configurable.py +469 -0
  93. ciris_adapters/sample_adapter/manifest.json +247 -0
  94. ciris_adapters/sample_adapter/services.py +486 -0
  95. ciris_adapters/weather/__init__.py +16 -0
  96. ciris_adapters/weather/adapter.py +130 -0
  97. ciris_adapters/weather/configurable.py +240 -0
  98. ciris_adapters/weather/manifest.json +156 -0
  99. ciris_adapters/weather/service.py +600 -0
  100. ciris_agent-1.7.7.dist-info/METADATA +284 -0
  101. ciris_agent-1.7.7.dist-info/RECORD +986 -0
  102. ciris_agent-1.7.7.dist-info/WHEEL +5 -0
  103. ciris_agent-1.7.7.dist-info/entry_points.txt +15 -0
  104. ciris_agent-1.7.7.dist-info/licenses/LICENSE +205 -0
  105. ciris_agent-1.7.7.dist-info/licenses/NOTICE +82 -0
  106. ciris_agent-1.7.7.dist-info/top_level.txt +4 -0
  107. ciris_engine/__init__.py +15 -0
  108. ciris_engine/ciris_templates/ally.yaml +632 -0
  109. ciris_engine/ciris_templates/default.yaml +411 -0
  110. ciris_engine/ciris_templates/echo-core.yaml +629 -0
  111. ciris_engine/ciris_templates/echo-speculative.yaml +764 -0
  112. ciris_engine/ciris_templates/echo.yaml +647 -0
  113. ciris_engine/ciris_templates/sage.yaml +332 -0
  114. ciris_engine/ciris_templates/scout.yaml +338 -0
  115. ciris_engine/ciris_templates/test.yaml +168 -0
  116. ciris_engine/cli.py +42 -0
  117. ciris_engine/config/CIRIS_SERVICES.json +19 -0
  118. ciris_engine/config/MODEL_CAPABILITIES.json +419 -0
  119. ciris_engine/config/PRICING_DATA.json +179 -0
  120. ciris_engine/config/__init__.py +50 -0
  121. ciris_engine/config/ciris_services.py +113 -0
  122. ciris_engine/config/model_capabilities.py +388 -0
  123. ciris_engine/config/pricing_models.py +276 -0
  124. ciris_engine/constants.py +35 -0
  125. ciris_engine/data/__init__.py +1 -0
  126. ciris_engine/data/covenant_1.0b.txt +978 -0
  127. ciris_engine/gui_static/11steps.svg +107 -0
  128. ciris_engine/gui_static/2x-schematics.png +0 -0
  129. ciris_engine/gui_static/404/index.html +1 -0
  130. ciris_engine/gui_static/404.html +1 -0
  131. ciris_engine/gui_static/_next/static/0edhkwDxd5UccTsCmtaBi/_buildManifest.js +1 -0
  132. ciris_engine/gui_static/_next/static/0edhkwDxd5UccTsCmtaBi/_ssgManifest.js +1 -0
  133. ciris_engine/gui_static/_next/static/U-3xTQao7hc2wnAi-Uekm/_buildManifest.js +1 -0
  134. ciris_engine/gui_static/_next/static/U-3xTQao7hc2wnAi-Uekm/_ssgManifest.js +1 -0
  135. ciris_engine/gui_static/_next/static/chunks/3297-60e86ba0f8a7b040.js +1 -0
  136. ciris_engine/gui_static/_next/static/chunks/3835-2aad4b7f5f8e4643.js +1 -0
  137. ciris_engine/gui_static/_next/static/chunks/4499-99a0bc47de0b8975.js +1 -0
  138. ciris_engine/gui_static/_next/static/chunks/4534-af88cd4ba6e99bff.js +1 -0
  139. ciris_engine/gui_static/_next/static/chunks/4541-84b455f9e0dc4cfe.js +1 -0
  140. ciris_engine/gui_static/_next/static/chunks/4789-61412711484754bb.js +1 -0
  141. ciris_engine/gui_static/_next/static/chunks/6539-c6398bc9d7018430.js +1 -0
  142. ciris_engine/gui_static/_next/static/chunks/704-8e827b26cc8c2d32.js +1 -0
  143. ciris_engine/gui_static/_next/static/chunks/704-fb45d630f3192c6f.js +1 -0
  144. ciris_engine/gui_static/_next/static/chunks/8072-de4952a2e6d2b33f.js +1 -0
  145. ciris_engine/gui_static/_next/static/chunks/8315-b91d03a3949db0af.js +1 -0
  146. ciris_engine/gui_static/_next/static/chunks/8386-f93a83ccbd789bd9.js +1 -0
  147. ciris_engine/gui_static/_next/static/chunks/87c73c54-781a7f35148d5433.js +1 -0
  148. ciris_engine/gui_static/_next/static/chunks/8903-fefea3339a02d41b.js +1 -0
  149. ciris_engine/gui_static/_next/static/chunks/9090-e66485adf8d9d990.js +1 -0
  150. ciris_engine/gui_static/_next/static/chunks/app/_not-found/page-a67d9808462c23b1.js +1 -0
  151. ciris_engine/gui_static/_next/static/chunks/app/account/api-keys/page-2d7ee1583bbbd02e.js +1 -0
  152. ciris_engine/gui_static/_next/static/chunks/app/account/api-keys/page-6a3c2bae6fe92b7b.js +1 -0
  153. ciris_engine/gui_static/_next/static/chunks/app/account/consent/page-2ed3a035136bc4e8.js +1 -0
  154. ciris_engine/gui_static/_next/static/chunks/app/account/consent/page-b2f5c91844a32422.js +1 -0
  155. ciris_engine/gui_static/_next/static/chunks/app/account/page-25b90f89af3ea58c.js +1 -0
  156. ciris_engine/gui_static/_next/static/chunks/app/account/page-b65d16c94ecaf69c.js +1 -0
  157. ciris_engine/gui_static/_next/static/chunks/app/account/privacy/page-675b6d05c8f9184f.js +1 -0
  158. ciris_engine/gui_static/_next/static/chunks/app/account/privacy/page-cbee2e1c8ab52145.js +1 -0
  159. ciris_engine/gui_static/_next/static/chunks/app/account/settings/page-0f44da06697cf9f0.js +1 -0
  160. ciris_engine/gui_static/_next/static/chunks/app/account/settings/page-563420253577edbf.js +1 -0
  161. ciris_engine/gui_static/_next/static/chunks/app/adapters/page-1854631018bc32be.js +1 -0
  162. ciris_engine/gui_static/_next/static/chunks/app/agents/page-8353752c176a7c70.js +1 -0
  163. ciris_engine/gui_static/_next/static/chunks/app/agents/page-f61a529f110a6040.js +1 -0
  164. ciris_engine/gui_static/_next/static/chunks/app/api-demo/page-7f19b9d20d39be28.js +1 -0
  165. ciris_engine/gui_static/_next/static/chunks/app/api-demo/page-d1063938f249b8bd.js +1 -0
  166. ciris_engine/gui_static/_next/static/chunks/app/audit/page-321b6728b8fff0bb.js +1 -0
  167. ciris_engine/gui_static/_next/static/chunks/app/audit/page-ebac35ca961a1277.js +1 -0
  168. ciris_engine/gui_static/_next/static/chunks/app/billing/page-6f3dc3bd02924f8e.js +1 -0
  169. ciris_engine/gui_static/_next/static/chunks/app/billing/page-fa4a469f814c821a.js +1 -0
  170. ciris_engine/gui_static/_next/static/chunks/app/comms/page-0d4f734269addd8f.js +1 -0
  171. ciris_engine/gui_static/_next/static/chunks/app/comms/page-79227d426050089c.js +1 -0
  172. ciris_engine/gui_static/_next/static/chunks/app/config/page-018d21d683b6e5bc.js +1 -0
  173. ciris_engine/gui_static/_next/static/chunks/app/config/page-2aa5a5363ca2a371.js +1 -0
  174. ciris_engine/gui_static/_next/static/chunks/app/consent/page-198373205fd316e2.js +1 -0
  175. ciris_engine/gui_static/_next/static/chunks/app/consent/page-f2ca39e7713b13f8.js +1 -0
  176. ciris_engine/gui_static/_next/static/chunks/app/dashboard/page-1dd5a196f643c60d.js +1 -0
  177. ciris_engine/gui_static/_next/static/chunks/app/dashboard/page-530a04d3abbb8cda.js +1 -0
  178. ciris_engine/gui_static/_next/static/chunks/app/docs/page-3193b06d094ab654.js +1 -0
  179. ciris_engine/gui_static/_next/static/chunks/app/docs/page-330e996dedb87aba.js +1 -0
  180. ciris_engine/gui_static/_next/static/chunks/app/layout-0a70f5fc460298b1.js +1 -0
  181. ciris_engine/gui_static/_next/static/chunks/app/layout-21f2f99dd5b336e9.js +1 -0
  182. ciris_engine/gui_static/_next/static/chunks/app/login/page-33240e6c6034a49d.js +1 -0
  183. ciris_engine/gui_static/_next/static/chunks/app/login/page-68ffab6d54a7fdcd.js +1 -0
  184. ciris_engine/gui_static/_next/static/chunks/app/logs/page-8a6167aecc4a475c.js +1 -0
  185. ciris_engine/gui_static/_next/static/chunks/app/memory/page-9ca8c5d0056de3ff.js +1 -0
  186. ciris_engine/gui_static/_next/static/chunks/app/memory/page-e961226941c18f81.js +1 -0
  187. ciris_engine/gui_static/_next/static/chunks/app/page-6fdb065a787a4974.js +1 -0
  188. ciris_engine/gui_static/_next/static/chunks/app/page-89f87d431be6064a.js +1 -0
  189. ciris_engine/gui_static/_next/static/chunks/app/runtime/page-2e728b9c43aa164d.js +1 -0
  190. ciris_engine/gui_static/_next/static/chunks/app/runtime/page-c7dd033dc40a72f0.js +1 -0
  191. ciris_engine/gui_static/_next/static/chunks/app/services/page-ae9f0bdf11d01a95.js +1 -0
  192. ciris_engine/gui_static/_next/static/chunks/app/services/page-b10feb79ca5d75e5.js +1 -0
  193. ciris_engine/gui_static/_next/static/chunks/app/sessions/page-13ebe7ef1c16ae11.js +1 -0
  194. ciris_engine/gui_static/_next/static/chunks/app/sessions/page-e6c82b16d617f785.js +1 -0
  195. ciris_engine/gui_static/_next/static/chunks/app/setup/page-0beb5f5b5a5c20fc.js +1 -0
  196. ciris_engine/gui_static/_next/static/chunks/app/setup/page-2595e729eae30c0e.js +1 -0
  197. ciris_engine/gui_static/_next/static/chunks/app/status-dashboard/page-1037c987aecc3653.js +1 -0
  198. ciris_engine/gui_static/_next/static/chunks/app/status-dashboard/page-2ffd147f6d3162ff.js +1 -0
  199. ciris_engine/gui_static/_next/static/chunks/app/system/page-2c5798d58cafcd91.js +1 -0
  200. ciris_engine/gui_static/_next/static/chunks/app/system/page-505b1ba4eceb01c3.js +1 -0
  201. ciris_engine/gui_static/_next/static/chunks/app/test-auth/page-b0cad31d5cb1b2fa.js +1 -0
  202. ciris_engine/gui_static/_next/static/chunks/app/test-auth/page-f3ecd7a8012df230.js +1 -0
  203. ciris_engine/gui_static/_next/static/chunks/app/test-login/page-f35117fdc4105801.js +1 -0
  204. ciris_engine/gui_static/_next/static/chunks/app/test-login/page-fb583a7924114906.js +1 -0
  205. ciris_engine/gui_static/_next/static/chunks/app/test-sdk/page-50f116fd76935563.js +1 -0
  206. ciris_engine/gui_static/_next/static/chunks/app/test-sdk/page-c37d8aa5ba623a44.js +1 -0
  207. ciris_engine/gui_static/_next/static/chunks/app/tools/page-429aec7a707777ef.js +1 -0
  208. ciris_engine/gui_static/_next/static/chunks/app/tools/page-5f705aad60e0c04e.js +1 -0
  209. ciris_engine/gui_static/_next/static/chunks/app/users/page-13476b8b0f3808cc.js +1 -0
  210. ciris_engine/gui_static/_next/static/chunks/app/users/page-7e500d154ed5bba4.js +1 -0
  211. ciris_engine/gui_static/_next/static/chunks/app/wa/page-cc4a9d8a5cb44d08.js +1 -0
  212. ciris_engine/gui_static/_next/static/chunks/app/wa/page-ec3e429efbc79230.js +1 -0
  213. ciris_engine/gui_static/_next/static/chunks/framework-9d29490f5ba089ba.js +1 -0
  214. ciris_engine/gui_static/_next/static/chunks/main-1f554952e47a82c4.js +1 -0
  215. ciris_engine/gui_static/_next/static/chunks/main-app-26fa8aed029082e5.js +1 -0
  216. ciris_engine/gui_static/_next/static/chunks/main-app-97b0486ef6bcef25.js +1 -0
  217. ciris_engine/gui_static/_next/static/chunks/pages/_app-6ce685456e616eb2.js +1 -0
  218. ciris_engine/gui_static/_next/static/chunks/pages/_error-d4bce98d93fe21e7.js +1 -0
  219. ciris_engine/gui_static/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  220. ciris_engine/gui_static/_next/static/chunks/webpack-fcebd240b7f8477d.js +1 -0
  221. ciris_engine/gui_static/_next/static/css/16b94b1fe0cc6e37.css +3 -0
  222. ciris_engine/gui_static/_next/static/css/77a24ceaae86deff.css +3 -0
  223. ciris_engine/gui_static/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
  224. ciris_engine/gui_static/_next/static/media/747892c23ea88013-s.woff2 +0 -0
  225. ciris_engine/gui_static/_next/static/media/8d697b304b401681-s.woff2 +0 -0
  226. ciris_engine/gui_static/_next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
  227. ciris_engine/gui_static/_next/static/media/9610d9e46709d722-s.woff2 +0 -0
  228. ciris_engine/gui_static/_next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
  229. ciris_engine/gui_static/_next/static/media/d8298875641ec7d4-s.p.woff2 +0 -0
  230. ciris_engine/gui_static/account/api-keys/index.html +1 -0
  231. ciris_engine/gui_static/account/api-keys/index.txt +27 -0
  232. ciris_engine/gui_static/account/consent/index.html +1 -0
  233. ciris_engine/gui_static/account/consent/index.txt +27 -0
  234. ciris_engine/gui_static/account/index.html +1 -0
  235. ciris_engine/gui_static/account/index.txt +27 -0
  236. ciris_engine/gui_static/account/privacy/index.html +1 -0
  237. ciris_engine/gui_static/account/privacy/index.txt +27 -0
  238. ciris_engine/gui_static/account/settings/index.html +1 -0
  239. ciris_engine/gui_static/account/settings/index.txt +27 -0
  240. ciris_engine/gui_static/adapters/index.html +1 -0
  241. ciris_engine/gui_static/adapters/index.txt +27 -0
  242. ciris_engine/gui_static/agents/index.html +1 -0
  243. ciris_engine/gui_static/agents/index.txt +27 -0
  244. ciris_engine/gui_static/andrew-roberts-euBRXcx57T4-unsplash.jpg +0 -0
  245. ciris_engine/gui_static/api-demo/index.html +1 -0
  246. ciris_engine/gui_static/api-demo/index.txt +27 -0
  247. ciris_engine/gui_static/audit/index.html +1 -0
  248. ciris_engine/gui_static/audit/index.txt +27 -0
  249. ciris_engine/gui_static/billing/index.html +1 -0
  250. ciris_engine/gui_static/billing/index.txt +27 -0
  251. ciris_engine/gui_static/blurryinfo.png +0 -0
  252. ciris_engine/gui_static/chip-vincent-PkQDwfl9Flc-unsplash.jpg +0 -0
  253. ciris_engine/gui_static/ciris-architecture.svg +338 -0
  254. ciris_engine/gui_static/comms/index.html +1 -0
  255. ciris_engine/gui_static/comms/index.txt +27 -0
  256. ciris_engine/gui_static/config/index.html +1 -0
  257. ciris_engine/gui_static/config/index.txt +27 -0
  258. ciris_engine/gui_static/consent/index.html +1 -0
  259. ciris_engine/gui_static/consent/index.txt +27 -0
  260. ciris_engine/gui_static/dashboard/index.html +1 -0
  261. ciris_engine/gui_static/dashboard/index.txt +27 -0
  262. ciris_engine/gui_static/docs/index.html +1 -0
  263. ciris_engine/gui_static/docs/index.txt +27 -0
  264. ciris_engine/gui_static/eric.png +0 -0
  265. ciris_engine/gui_static/file.svg +1 -0
  266. ciris_engine/gui_static/globe.svg +1 -0
  267. ciris_engine/gui_static/index.html +1 -0
  268. ciris_engine/gui_static/index.txt +27 -0
  269. ciris_engine/gui_static/infogfx-1@2x.png +0 -0
  270. ciris_engine/gui_static/infogfx-2.png +0 -0
  271. ciris_engine/gui_static/infogfx-dark-1.png +0 -0
  272. ciris_engine/gui_static/kelly-vohs-soSTXmIxTDU-unsplash.jpg +0 -0
  273. ciris_engine/gui_static/login/index.html +1 -0
  274. ciris_engine/gui_static/login/index.txt +27 -0
  275. ciris_engine/gui_static/logs/index.html +1 -0
  276. ciris_engine/gui_static/logs/index.txt +27 -0
  277. ciris_engine/gui_static/memory/index.html +1 -0
  278. ciris_engine/gui_static/memory/index.txt +27 -0
  279. ciris_engine/gui_static/nathan-farrish-ArcTfEoBgzs-unsplash.jpg +0 -0
  280. ciris_engine/gui_static/next.svg +1 -0
  281. ciris_engine/gui_static/overview.svg +512 -0
  282. ciris_engine/gui_static/overview1.svg +407 -0
  283. ciris_engine/gui_static/overview2.svg +370 -0
  284. ciris_engine/gui_static/pipeline-visualization.svg +278 -0
  285. ciris_engine/gui_static/privacy-policy.html +160 -0
  286. ciris_engine/gui_static/runtime/index.html +8 -0
  287. ciris_engine/gui_static/runtime/index.txt +27 -0
  288. ciris_engine/gui_static/services/index.html +1 -0
  289. ciris_engine/gui_static/services/index.txt +27 -0
  290. ciris_engine/gui_static/sessions/index.html +1 -0
  291. ciris_engine/gui_static/sessions/index.txt +27 -0
  292. ciris_engine/gui_static/setup/index.html +1 -0
  293. ciris_engine/gui_static/setup/index.txt +27 -0
  294. ciris_engine/gui_static/status-dashboard/index.html +1 -0
  295. ciris_engine/gui_static/status-dashboard/index.txt +27 -0
  296. ciris_engine/gui_static/system/index.html +1 -0
  297. ciris_engine/gui_static/system/index.txt +27 -0
  298. ciris_engine/gui_static/terms-of-service.html +174 -0
  299. ciris_engine/gui_static/test-auth/index.html +1 -0
  300. ciris_engine/gui_static/test-auth/index.txt +27 -0
  301. ciris_engine/gui_static/test-login/index.html +1 -0
  302. ciris_engine/gui_static/test-login/index.txt +27 -0
  303. ciris_engine/gui_static/test-sdk/index.html +1 -0
  304. ciris_engine/gui_static/test-sdk/index.txt +27 -0
  305. ciris_engine/gui_static/tools/index.html +1 -0
  306. ciris_engine/gui_static/tools/index.txt +27 -0
  307. ciris_engine/gui_static/users/index.html +1 -0
  308. ciris_engine/gui_static/users/index.txt +27 -0
  309. ciris_engine/gui_static/vercel.svg +1 -0
  310. ciris_engine/gui_static/videos/video1.mp4 +0 -0
  311. ciris_engine/gui_static/videos/video3.mp4 +0 -0
  312. ciris_engine/gui_static/wa/index.html +1 -0
  313. ciris_engine/gui_static/wa/index.txt +27 -0
  314. ciris_engine/gui_static/window.svg +1 -0
  315. ciris_engine/logic/__init__.py +8 -0
  316. ciris_engine/logic/adapters/__init__.py +74 -0
  317. ciris_engine/logic/adapters/api/__init__.py +5 -0
  318. ciris_engine/logic/adapters/api/adapter.py +1037 -0
  319. ciris_engine/logic/adapters/api/api_communication.py +370 -0
  320. ciris_engine/logic/adapters/api/api_document.py +330 -0
  321. ciris_engine/logic/adapters/api/api_observer.py +24 -0
  322. ciris_engine/logic/adapters/api/api_runtime_control.py +388 -0
  323. ciris_engine/logic/adapters/api/api_tools.py +299 -0
  324. ciris_engine/logic/adapters/api/api_vision.py +215 -0
  325. ciris_engine/logic/adapters/api/app.py +272 -0
  326. ciris_engine/logic/adapters/api/auth.py +159 -0
  327. ciris_engine/logic/adapters/api/config.py +101 -0
  328. ciris_engine/logic/adapters/api/constants.py +55 -0
  329. ciris_engine/logic/adapters/api/dependencies/__init__.py +1 -0
  330. ciris_engine/logic/adapters/api/dependencies/auth.py +260 -0
  331. ciris_engine/logic/adapters/api/endpoints/__init__.py +1 -0
  332. ciris_engine/logic/adapters/api/endpoints/emergency.py +86 -0
  333. ciris_engine/logic/adapters/api/middleware/__init__.py +1 -0
  334. ciris_engine/logic/adapters/api/middleware/rate_limiter.py +302 -0
  335. ciris_engine/logic/adapters/api/models.py +29 -0
  336. ciris_engine/logic/adapters/api/routes/__init__.py +52 -0
  337. ciris_engine/logic/adapters/api/routes/agent.py +1762 -0
  338. ciris_engine/logic/adapters/api/routes/audit.py +707 -0
  339. ciris_engine/logic/adapters/api/routes/auth.py +1745 -0
  340. ciris_engine/logic/adapters/api/routes/billing.py +895 -0
  341. ciris_engine/logic/adapters/api/routes/config.py +329 -0
  342. ciris_engine/logic/adapters/api/routes/connectors.py +534 -0
  343. ciris_engine/logic/adapters/api/routes/consent.py +637 -0
  344. ciris_engine/logic/adapters/api/routes/dsar.py +637 -0
  345. ciris_engine/logic/adapters/api/routes/dsar_multi_source.py +484 -0
  346. ciris_engine/logic/adapters/api/routes/emergency.py +302 -0
  347. ciris_engine/logic/adapters/api/routes/memory.py +733 -0
  348. ciris_engine/logic/adapters/api/routes/memory_filters.py +230 -0
  349. ciris_engine/logic/adapters/api/routes/memory_models.py +112 -0
  350. ciris_engine/logic/adapters/api/routes/memory_queries.py +236 -0
  351. ciris_engine/logic/adapters/api/routes/memory_query_helpers.py +394 -0
  352. ciris_engine/logic/adapters/api/routes/memory_visualization.py +359 -0
  353. ciris_engine/logic/adapters/api/routes/memory_visualization_helpers.py +110 -0
  354. ciris_engine/logic/adapters/api/routes/partnership.py +541 -0
  355. ciris_engine/logic/adapters/api/routes/setup.py +1374 -0
  356. ciris_engine/logic/adapters/api/routes/system.py +3049 -0
  357. ciris_engine/logic/adapters/api/routes/system_extensions.py +952 -0
  358. ciris_engine/logic/adapters/api/routes/telemetry.py +1987 -0
  359. ciris_engine/logic/adapters/api/routes/telemetry_converters.py +141 -0
  360. ciris_engine/logic/adapters/api/routes/telemetry_helpers.py +111 -0
  361. ciris_engine/logic/adapters/api/routes/telemetry_logs_reader.py +280 -0
  362. ciris_engine/logic/adapters/api/routes/telemetry_metrics.py +131 -0
  363. ciris_engine/logic/adapters/api/routes/telemetry_models.py +190 -0
  364. ciris_engine/logic/adapters/api/routes/telemetry_otlp.py +878 -0
  365. ciris_engine/logic/adapters/api/routes/telemetry_resource_helpers.py +191 -0
  366. ciris_engine/logic/adapters/api/routes/tickets.py +541 -0
  367. ciris_engine/logic/adapters/api/routes/tools.py +556 -0
  368. ciris_engine/logic/adapters/api/routes/transparency.py +281 -0
  369. ciris_engine/logic/adapters/api/routes/users.py +981 -0
  370. ciris_engine/logic/adapters/api/routes/verification.py +373 -0
  371. ciris_engine/logic/adapters/api/routes/wa.py +369 -0
  372. ciris_engine/logic/adapters/api/service_configuration.py +177 -0
  373. ciris_engine/logic/adapters/api/services/__init__.py +1 -0
  374. ciris_engine/logic/adapters/api/services/auth_service.py +1417 -0
  375. ciris_engine/logic/adapters/api/services/oauth_security.py +68 -0
  376. ciris_engine/logic/adapters/base.py +141 -0
  377. ciris_engine/logic/adapters/base_adapter.py +73 -0
  378. ciris_engine/logic/adapters/base_observer.py +1141 -0
  379. ciris_engine/logic/adapters/base_vision.py +312 -0
  380. ciris_engine/logic/adapters/cirisnode_client.py +307 -0
  381. ciris_engine/logic/adapters/cli/__init__.py +3 -0
  382. ciris_engine/logic/adapters/cli/adapter.py +207 -0
  383. ciris_engine/logic/adapters/cli/cli_adapter.py +902 -0
  384. ciris_engine/logic/adapters/cli/cli_observer.py +268 -0
  385. ciris_engine/logic/adapters/cli/cli_tools.py +427 -0
  386. ciris_engine/logic/adapters/cli/cli_wa_service.py +134 -0
  387. ciris_engine/logic/adapters/cli/config.py +73 -0
  388. ciris_engine/logic/adapters/discord/__init__.py +3 -0
  389. ciris_engine/logic/adapters/discord/adapter.py +783 -0
  390. ciris_engine/logic/adapters/discord/ciris_discord_client.py +159 -0
  391. ciris_engine/logic/adapters/discord/config.py +177 -0
  392. ciris_engine/logic/adapters/discord/constants.py +185 -0
  393. ciris_engine/logic/adapters/discord/discord-stubs.pyi +50 -0
  394. ciris_engine/logic/adapters/discord/discord_adapter.py +1584 -0
  395. ciris_engine/logic/adapters/discord/discord_audit.py +150 -0
  396. ciris_engine/logic/adapters/discord/discord_channel_manager.py +351 -0
  397. ciris_engine/logic/adapters/discord/discord_connection_manager.py +313 -0
  398. ciris_engine/logic/adapters/discord/discord_embed_formatter.py +369 -0
  399. ciris_engine/logic/adapters/discord/discord_error_classifier.py +302 -0
  400. ciris_engine/logic/adapters/discord/discord_error_handler.py +316 -0
  401. ciris_engine/logic/adapters/discord/discord_guidance_handler.py +460 -0
  402. ciris_engine/logic/adapters/discord/discord_message_handler.py +207 -0
  403. ciris_engine/logic/adapters/discord/discord_observer.py +670 -0
  404. ciris_engine/logic/adapters/discord/discord_rate_limiter.py +249 -0
  405. ciris_engine/logic/adapters/discord/discord_reaction_handler.py +278 -0
  406. ciris_engine/logic/adapters/discord/discord_tool_handler.py +465 -0
  407. ciris_engine/logic/adapters/discord/discord_tool_service.py +790 -0
  408. ciris_engine/logic/adapters/discord/discord_tools.py +90 -0
  409. ciris_engine/logic/adapters/discord/discord_vision_helper.py +148 -0
  410. ciris_engine/logic/adapters/discord/py.typed +0 -0
  411. ciris_engine/logic/adapters/document_parser.py +320 -0
  412. ciris_engine/logic/audit/__init__.py +10 -0
  413. ciris_engine/logic/audit/hash_chain.py +313 -0
  414. ciris_engine/logic/audit/signature_manager.py +352 -0
  415. ciris_engine/logic/audit/verifier.py +408 -0
  416. ciris_engine/logic/buses/__init__.py +21 -0
  417. ciris_engine/logic/buses/base_bus.py +178 -0
  418. ciris_engine/logic/buses/bus_manager.py +121 -0
  419. ciris_engine/logic/buses/communication_bus.py +387 -0
  420. ciris_engine/logic/buses/llm_bus.py +722 -0
  421. ciris_engine/logic/buses/memory_bus.py +577 -0
  422. ciris_engine/logic/buses/prohibitions.py +502 -0
  423. ciris_engine/logic/buses/runtime_control_bus.py +539 -0
  424. ciris_engine/logic/buses/tool_bus.py +482 -0
  425. ciris_engine/logic/buses/wise_bus.py +684 -0
  426. ciris_engine/logic/config/__init__.py +25 -0
  427. ciris_engine/logic/config/bootstrap.py +255 -0
  428. ciris_engine/logic/config/config_accessor.py +202 -0
  429. ciris_engine/logic/config/db_paths.py +194 -0
  430. ciris_engine/logic/config/env_utils.py +39 -0
  431. ciris_engine/logic/conscience/__init__.py +16 -0
  432. ciris_engine/logic/conscience/build_deferral_package.py +0 -0
  433. ciris_engine/logic/conscience/core.py +688 -0
  434. ciris_engine/logic/conscience/interface.py +33 -0
  435. ciris_engine/logic/conscience/registry.py +76 -0
  436. ciris_engine/logic/conscience/thought_depth_guardrail.py +231 -0
  437. ciris_engine/logic/conscience/updated_status_conscience.py +156 -0
  438. ciris_engine/logic/context/__init__.py +10 -0
  439. ciris_engine/logic/context/batch_context.py +550 -0
  440. ciris_engine/logic/context/builder.py +149 -0
  441. ciris_engine/logic/context/channel_resolution.py +136 -0
  442. ciris_engine/logic/context/secrets_snapshot.py +52 -0
  443. ciris_engine/logic/context/system_snapshot.py +116 -0
  444. ciris_engine/logic/context/system_snapshot_helpers.py +1651 -0
  445. ciris_engine/logic/covenant/__init__.py +33 -0
  446. ciris_engine/logic/covenant/executor.py +303 -0
  447. ciris_engine/logic/covenant/extractor.py +382 -0
  448. ciris_engine/logic/covenant/handler.py +241 -0
  449. ciris_engine/logic/covenant/verifier.py +383 -0
  450. ciris_engine/logic/dma/__init__.py +15 -0
  451. ciris_engine/logic/dma/action_selection/__init__.py +11 -0
  452. ciris_engine/logic/dma/action_selection/action_instruction_generator.py +444 -0
  453. ciris_engine/logic/dma/action_selection/context_builder.py +508 -0
  454. ciris_engine/logic/dma/action_selection/faculty_integration.py +193 -0
  455. ciris_engine/logic/dma/action_selection/special_cases.py +132 -0
  456. ciris_engine/logic/dma/action_selection_pdma.py +365 -0
  457. ciris_engine/logic/dma/base_dma.py +335 -0
  458. ciris_engine/logic/dma/csdma.py +239 -0
  459. ciris_engine/logic/dma/dma_executor.py +575 -0
  460. ciris_engine/logic/dma/dsdma_base.py +410 -0
  461. ciris_engine/logic/dma/exceptions.py +4 -0
  462. ciris_engine/logic/dma/factory.py +150 -0
  463. ciris_engine/logic/dma/pdma.py +120 -0
  464. ciris_engine/logic/dma/prompt_loader.py +189 -0
  465. ciris_engine/logic/dma/prompts/action_selection_pdma.yml +58 -0
  466. ciris_engine/logic/dma/prompts/csdma_common_sense.yml +28 -0
  467. ciris_engine/logic/dma/prompts/dsdma_base.yml +17 -0
  468. ciris_engine/logic/dma/prompts/pdma_ethical.yml +42 -0
  469. ciris_engine/logic/formatters/__init__.py +26 -0
  470. ciris_engine/logic/formatters/crisis_resources.py +80 -0
  471. ciris_engine/logic/formatters/escalation.py +21 -0
  472. ciris_engine/logic/formatters/identity.py +224 -0
  473. ciris_engine/logic/formatters/prompt_blocks.py +64 -0
  474. ciris_engine/logic/formatters/system_snapshot.py +193 -0
  475. ciris_engine/logic/formatters/user_profiles.py +108 -0
  476. ciris_engine/logic/handlers/__init__.py +1 -0
  477. ciris_engine/logic/handlers/control/__init__.py +1 -0
  478. ciris_engine/logic/handlers/control/defer_handler.py +195 -0
  479. ciris_engine/logic/handlers/control/ponder_handler.py +154 -0
  480. ciris_engine/logic/handlers/control/reject_handler.py +81 -0
  481. ciris_engine/logic/handlers/external/__init__.py +1 -0
  482. ciris_engine/logic/handlers/external/observe_handler.py +154 -0
  483. ciris_engine/logic/handlers/external/speak_handler.py +250 -0
  484. ciris_engine/logic/handlers/external/tool_handler.py +148 -0
  485. ciris_engine/logic/handlers/memory/__init__.py +1 -0
  486. ciris_engine/logic/handlers/memory/forget_handler.py +107 -0
  487. ciris_engine/logic/handlers/memory/memorize_handler.py +391 -0
  488. ciris_engine/logic/handlers/memory/recall_handler.py +213 -0
  489. ciris_engine/logic/handlers/terminal/__init__.py +1 -0
  490. ciris_engine/logic/handlers/terminal/task_complete_handler.py +299 -0
  491. ciris_engine/logic/infrastructure/__init__.py +1 -0
  492. ciris_engine/logic/infrastructure/handlers/__init__.py +8 -0
  493. ciris_engine/logic/infrastructure/handlers/action_dispatcher.py +382 -0
  494. ciris_engine/logic/infrastructure/handlers/base_handler.py +450 -0
  495. ciris_engine/logic/infrastructure/handlers/exceptions.py +2 -0
  496. ciris_engine/logic/infrastructure/handlers/handler_registry.py +59 -0
  497. ciris_engine/logic/infrastructure/handlers/helpers.py +55 -0
  498. ciris_engine/logic/infrastructure/step_streaming.py +149 -0
  499. ciris_engine/logic/infrastructure/sub_services/__init__.py +1 -0
  500. ciris_engine/logic/infrastructure/sub_services/identity_variance_monitor.py +1035 -0
  501. ciris_engine/logic/infrastructure/sub_services/pattern_analysis_loop.py +758 -0
  502. ciris_engine/logic/infrastructure/sub_services/wa_cli_bootstrap.py +229 -0
  503. ciris_engine/logic/infrastructure/sub_services/wa_cli_display.py +176 -0
  504. ciris_engine/logic/infrastructure/sub_services/wa_cli_oauth.py +404 -0
  505. ciris_engine/logic/infrastructure/sub_services/wa_cli_wizard.py +181 -0
  506. ciris_engine/logic/persistence/__init__.py +130 -0
  507. ciris_engine/logic/persistence/analytics.py +97 -0
  508. ciris_engine/logic/persistence/db/__init__.py +28 -0
  509. ciris_engine/logic/persistence/db/core.py +520 -0
  510. ciris_engine/logic/persistence/db/dialect.py +380 -0
  511. ciris_engine/logic/persistence/db/execution_helpers.py +216 -0
  512. ciris_engine/logic/persistence/db/migration_runner.py +191 -0
  513. ciris_engine/logic/persistence/db/operations.py +313 -0
  514. ciris_engine/logic/persistence/db/query_builder.py +232 -0
  515. ciris_engine/logic/persistence/db/retry.py +154 -0
  516. ciris_engine/logic/persistence/db/setup.py +18 -0
  517. ciris_engine/logic/persistence/migrations/postgres/001_initial_schema.sql +4 -0
  518. ciris_engine/logic/persistence/migrations/postgres/002_add_retry_status.sql +3 -0
  519. ciris_engine/logic/persistence/migrations/postgres/003_add_task_update_tracking.sql +8 -0
  520. ciris_engine/logic/persistence/migrations/postgres/004_add_occurrence_id.sql +54 -0
  521. ciris_engine/logic/persistence/migrations/postgres/005_add_consolidation_locks.sql +22 -0
  522. ciris_engine/logic/persistence/migrations/postgres/006_add_correlation_id_unique_index.sql +16 -0
  523. ciris_engine/logic/persistence/migrations/postgres/007_add_dsar_tickets.sql +39 -0
  524. ciris_engine/logic/persistence/migrations/postgres/008_rename_to_tickets_add_sop.sql +123 -0
  525. ciris_engine/logic/persistence/migrations/postgres/009_add_ticket_status_columns.sql +39 -0
  526. ciris_engine/logic/persistence/migrations/postgres/010_add_images_to_tasks.sql +5 -0
  527. ciris_engine/logic/persistence/migrations/sqlite/001_initial_schema.sql +357 -0
  528. ciris_engine/logic/persistence/migrations/sqlite/002_add_retry_status.sql +3 -0
  529. ciris_engine/logic/persistence/migrations/sqlite/003_add_task_update_tracking.sql +8 -0
  530. ciris_engine/logic/persistence/migrations/sqlite/004_add_occurrence_id.sql +45 -0
  531. ciris_engine/logic/persistence/migrations/sqlite/005_add_consolidation_locks.sql +22 -0
  532. ciris_engine/logic/persistence/migrations/sqlite/006_add_correlation_id_unique_index.sql +16 -0
  533. ciris_engine/logic/persistence/migrations/sqlite/007_add_dsar_tickets.sql +39 -0
  534. ciris_engine/logic/persistence/migrations/sqlite/008_rename_to_tickets_add_sop.sql +120 -0
  535. ciris_engine/logic/persistence/migrations/sqlite/009_add_ticket_status_columns.sql +129 -0
  536. ciris_engine/logic/persistence/migrations/sqlite/010_add_images_to_tasks.sql +17 -0
  537. ciris_engine/logic/persistence/models/__init__.py +141 -0
  538. ciris_engine/logic/persistence/models/correlations.py +881 -0
  539. ciris_engine/logic/persistence/models/deferral.py +68 -0
  540. ciris_engine/logic/persistence/models/dsar.py +286 -0
  541. ciris_engine/logic/persistence/models/graph.py +362 -0
  542. ciris_engine/logic/persistence/models/identity.py +264 -0
  543. ciris_engine/logic/persistence/models/queue_status.py +139 -0
  544. ciris_engine/logic/persistence/models/tasks.py +1043 -0
  545. ciris_engine/logic/persistence/models/thoughts.py +400 -0
  546. ciris_engine/logic/persistence/models/tickets.py +518 -0
  547. ciris_engine/logic/persistence/stores/__init__.py +13 -0
  548. ciris_engine/logic/persistence/stores/auth_helpers.py +117 -0
  549. ciris_engine/logic/persistence/stores/authentication_store.py +414 -0
  550. ciris_engine/logic/persistence/utils.py +212 -0
  551. ciris_engine/logic/processors/__init__.py +30 -0
  552. ciris_engine/logic/processors/core/__init__.py +1 -0
  553. ciris_engine/logic/processors/core/base_processor.py +280 -0
  554. ciris_engine/logic/processors/core/main_processor.py +1777 -0
  555. ciris_engine/logic/processors/core/step_decorators.py +1583 -0
  556. ciris_engine/logic/processors/core/thought_processor/__init__.py +20 -0
  557. ciris_engine/logic/processors/core/thought_processor/action_execution.py +49 -0
  558. ciris_engine/logic/processors/core/thought_processor/conscience_execution.py +382 -0
  559. ciris_engine/logic/processors/core/thought_processor/finalize_action.py +66 -0
  560. ciris_engine/logic/processors/core/thought_processor/gather_context.py +120 -0
  561. ciris_engine/logic/processors/core/thought_processor/main.py +920 -0
  562. ciris_engine/logic/processors/core/thought_processor/perform_aspdma.py +86 -0
  563. ciris_engine/logic/processors/core/thought_processor/perform_dmas.py +106 -0
  564. ciris_engine/logic/processors/core/thought_processor/recursive_processing.py +237 -0
  565. ciris_engine/logic/processors/core/thought_processor/round_complete.py +52 -0
  566. ciris_engine/logic/processors/core/thought_processor/start_round.py +64 -0
  567. ciris_engine/logic/processors/exceptions.py +59 -0
  568. ciris_engine/logic/processors/states/__init__.py +1 -0
  569. ciris_engine/logic/processors/states/dream_processor.py +1381 -0
  570. ciris_engine/logic/processors/states/play_processor.py +141 -0
  571. ciris_engine/logic/processors/states/shutdown_processor.py +623 -0
  572. ciris_engine/logic/processors/states/solitude_processor.py +305 -0
  573. ciris_engine/logic/processors/states/wakeup_processor.py +802 -0
  574. ciris_engine/logic/processors/states/work_processor.py +742 -0
  575. ciris_engine/logic/processors/support/__init__.py +1 -0
  576. ciris_engine/logic/processors/support/dma_orchestrator.py +336 -0
  577. ciris_engine/logic/processors/support/processing_queue.py +133 -0
  578. ciris_engine/logic/processors/support/shutdown_condition_evaluator.py +294 -0
  579. ciris_engine/logic/processors/support/state_manager.py +358 -0
  580. ciris_engine/logic/processors/support/task_manager.py +303 -0
  581. ciris_engine/logic/processors/support/thought_escalation.py +116 -0
  582. ciris_engine/logic/processors/support/thought_manager.py +328 -0
  583. ciris_engine/logic/processors/support/thought_manager_enhanced.py +105 -0
  584. ciris_engine/logic/registries/__init__.py +34 -0
  585. ciris_engine/logic/registries/base.py +653 -0
  586. ciris_engine/logic/registries/circuit_breaker.py +275 -0
  587. ciris_engine/logic/registries/typed_registries.py +184 -0
  588. ciris_engine/logic/runtime/__init__.py +7 -0
  589. ciris_engine/logic/runtime/adapter_loader.py +261 -0
  590. ciris_engine/logic/runtime/adapter_manager.py +1053 -0
  591. ciris_engine/logic/runtime/ciris_runtime.py +2342 -0
  592. ciris_engine/logic/runtime/ciris_runtime_helpers.py +923 -0
  593. ciris_engine/logic/runtime/component_builder.py +361 -0
  594. ciris_engine/logic/runtime/identity_manager.py +219 -0
  595. ciris_engine/logic/runtime/module_loader.py +207 -0
  596. ciris_engine/logic/runtime/prevent_sideeffects.py +30 -0
  597. ciris_engine/logic/runtime/runtime_interface.py +23 -0
  598. ciris_engine/logic/runtime/service_initializer.py +1623 -0
  599. ciris_engine/logic/secrets/__init__.py +30 -0
  600. ciris_engine/logic/secrets/encryption.py +175 -0
  601. ciris_engine/logic/secrets/filter.py +295 -0
  602. ciris_engine/logic/secrets/service.py +652 -0
  603. ciris_engine/logic/secrets/store.py +669 -0
  604. ciris_engine/logic/services/__init__.py +1 -0
  605. ciris_engine/logic/services/adaptation/__init__.py +3 -0
  606. ciris_engine/logic/services/base_graph_service.py +142 -0
  607. ciris_engine/logic/services/base_infrastructure_service.py +69 -0
  608. ciris_engine/logic/services/base_scheduled_service.py +136 -0
  609. ciris_engine/logic/services/base_service.py +247 -0
  610. ciris_engine/logic/services/governance/__init__.py +3 -0
  611. ciris_engine/logic/services/governance/adaptive_filter/__init__.py +14 -0
  612. ciris_engine/logic/services/governance/adaptive_filter/service.py +818 -0
  613. ciris_engine/logic/services/governance/consent/__init__.py +53 -0
  614. ciris_engine/logic/services/governance/consent/air.py +403 -0
  615. ciris_engine/logic/services/governance/consent/decay.py +324 -0
  616. ciris_engine/logic/services/governance/consent/dsar_automation.py +589 -0
  617. ciris_engine/logic/services/governance/consent/exceptions.py +106 -0
  618. ciris_engine/logic/services/governance/consent/metrics.py +270 -0
  619. ciris_engine/logic/services/governance/consent/partnership.py +533 -0
  620. ciris_engine/logic/services/governance/consent/service.py +1256 -0
  621. ciris_engine/logic/services/governance/dsar/__init__.py +29 -0
  622. ciris_engine/logic/services/governance/dsar/orchestrator.py +977 -0
  623. ciris_engine/logic/services/governance/dsar/schemas.py +141 -0
  624. ciris_engine/logic/services/governance/dsar/signature_service.py +283 -0
  625. ciris_engine/logic/services/governance/self_observation/__init__.py +20 -0
  626. ciris_engine/logic/services/governance/self_observation/service.py +1153 -0
  627. ciris_engine/logic/services/governance/visibility/__init__.py +17 -0
  628. ciris_engine/logic/services/governance/visibility/service.py +512 -0
  629. ciris_engine/logic/services/governance/wise_authority/__init__.py +15 -0
  630. ciris_engine/logic/services/governance/wise_authority/service.py +827 -0
  631. ciris_engine/logic/services/graph/__init__.py +5 -0
  632. ciris_engine/logic/services/graph/audit_service/__init__.py +5 -0
  633. ciris_engine/logic/services/graph/audit_service/service.py +1675 -0
  634. ciris_engine/logic/services/graph/base.py +208 -0
  635. ciris_engine/logic/services/graph/config_service/__init__.py +5 -0
  636. ciris_engine/logic/services/graph/config_service/service.py +372 -0
  637. ciris_engine/logic/services/graph/incident_service/__init__.py +5 -0
  638. ciris_engine/logic/services/graph/incident_service/service.py +803 -0
  639. ciris_engine/logic/services/graph/memory_service.py +1120 -0
  640. ciris_engine/logic/services/graph/telemetry_service/__init__.py +5 -0
  641. ciris_engine/logic/services/graph/telemetry_service/exceptions.py +104 -0
  642. ciris_engine/logic/services/graph/telemetry_service/helpers.py +1337 -0
  643. ciris_engine/logic/services/graph/telemetry_service/service.py +2429 -0
  644. ciris_engine/logic/services/graph/tsdb_consolidation/__init__.py +17 -0
  645. ciris_engine/logic/services/graph/tsdb_consolidation/aggregation_helpers.py +355 -0
  646. ciris_engine/logic/services/graph/tsdb_consolidation/cleanup_helpers.py +438 -0
  647. ciris_engine/logic/services/graph/tsdb_consolidation/compressor.py +260 -0
  648. ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/__init__.py +27 -0
  649. ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/audit.py +326 -0
  650. ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/conversation.py +291 -0
  651. ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/memory.py +197 -0
  652. ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/metrics.py +251 -0
  653. ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/task.py +257 -0
  654. ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/trace.py +363 -0
  655. ciris_engine/logic/services/graph/tsdb_consolidation/data_converter.py +545 -0
  656. ciris_engine/logic/services/graph/tsdb_consolidation/date_calculation_helpers.py +193 -0
  657. ciris_engine/logic/services/graph/tsdb_consolidation/db_query_helpers.py +296 -0
  658. ciris_engine/logic/services/graph/tsdb_consolidation/edge_helpers.py +92 -0
  659. ciris_engine/logic/services/graph/tsdb_consolidation/edge_manager.py +896 -0
  660. ciris_engine/logic/services/graph/tsdb_consolidation/extensive_helpers.py +322 -0
  661. ciris_engine/logic/services/graph/tsdb_consolidation/period_manager.py +152 -0
  662. ciris_engine/logic/services/graph/tsdb_consolidation/profound_helpers.py +277 -0
  663. ciris_engine/logic/services/graph/tsdb_consolidation/query_manager.py +812 -0
  664. ciris_engine/logic/services/graph/tsdb_consolidation/service.py +1692 -0
  665. ciris_engine/logic/services/graph/tsdb_consolidation/sql_builders.py +363 -0
  666. ciris_engine/logic/services/infrastructure/__init__.py +1 -0
  667. ciris_engine/logic/services/infrastructure/authentication/__init__.py +5 -0
  668. ciris_engine/logic/services/infrastructure/authentication/service.py +1634 -0
  669. ciris_engine/logic/services/infrastructure/database_maintenance/__init__.py +15 -0
  670. ciris_engine/logic/services/infrastructure/database_maintenance/service.py +764 -0
  671. ciris_engine/logic/services/infrastructure/resource_monitor/__init__.py +7 -0
  672. ciris_engine/logic/services/infrastructure/resource_monitor/ciris_billing_provider.py +755 -0
  673. ciris_engine/logic/services/infrastructure/resource_monitor/service.py +409 -0
  674. ciris_engine/logic/services/infrastructure/resource_monitor/simple_credit_provider.py +129 -0
  675. ciris_engine/logic/services/lifecycle/__init__.py +3 -0
  676. ciris_engine/logic/services/lifecycle/initialization/__init__.py +10 -0
  677. ciris_engine/logic/services/lifecycle/initialization/service.py +312 -0
  678. ciris_engine/logic/services/lifecycle/scheduler/__init__.py +5 -0
  679. ciris_engine/logic/services/lifecycle/scheduler/service.py +607 -0
  680. ciris_engine/logic/services/lifecycle/shutdown/__init__.py +9 -0
  681. ciris_engine/logic/services/lifecycle/shutdown/service.py +378 -0
  682. ciris_engine/logic/services/lifecycle/time/__init__.py +15 -0
  683. ciris_engine/logic/services/lifecycle/time/service.py +259 -0
  684. ciris_engine/logic/services/memory_service/__init__.py +8 -0
  685. ciris_engine/logic/services/mixins/__init__.py +13 -0
  686. ciris_engine/logic/services/mixins/example_usage.py +200 -0
  687. ciris_engine/logic/services/mixins/request_metrics.py +179 -0
  688. ciris_engine/logic/services/runtime/__init__.py +3 -0
  689. ciris_engine/logic/services/runtime/adapter_configuration/__init__.py +16 -0
  690. ciris_engine/logic/services/runtime/adapter_configuration/service.py +674 -0
  691. ciris_engine/logic/services/runtime/adapter_configuration/session.py +67 -0
  692. ciris_engine/logic/services/runtime/control_service/__init__.py +5 -0
  693. ciris_engine/logic/services/runtime/control_service/service.py +2269 -0
  694. ciris_engine/logic/services/runtime/llm_service/__init__.py +14 -0
  695. ciris_engine/logic/services/runtime/llm_service/pricing_calculator.py +279 -0
  696. ciris_engine/logic/services/runtime/llm_service/service.py +930 -0
  697. ciris_engine/logic/services/tools/__init__.py +5 -0
  698. ciris_engine/logic/services/tools/core_tool_service/__init__.py +8 -0
  699. ciris_engine/logic/services/tools/core_tool_service/service.py +852 -0
  700. ciris_engine/logic/setup/__init__.py +1 -0
  701. ciris_engine/logic/setup/first_run.py +250 -0
  702. ciris_engine/logic/setup/wizard.py +327 -0
  703. ciris_engine/logic/telemetry/__init__.py +46 -0
  704. ciris_engine/logic/telemetry/core.py +239 -0
  705. ciris_engine/logic/telemetry/hot_cold_config.py +133 -0
  706. ciris_engine/logic/telemetry/log_collector.py +190 -0
  707. ciris_engine/logic/telemetry/resource_monitor.py +7 -0
  708. ciris_engine/logic/telemetry/security.py +79 -0
  709. ciris_engine/logic/utils/__init__.py +18 -0
  710. ciris_engine/logic/utils/channel_utils.py +75 -0
  711. ciris_engine/logic/utils/consent/__init__.py +1 -0
  712. ciris_engine/logic/utils/consent/partnership_utils.py +172 -0
  713. ciris_engine/logic/utils/constants.py +92 -0
  714. ciris_engine/logic/utils/context_utils.py +145 -0
  715. ciris_engine/logic/utils/directory_setup.py +533 -0
  716. ciris_engine/logic/utils/graphql_context_provider.py +152 -0
  717. ciris_engine/logic/utils/identity_resolution.py +843 -0
  718. ciris_engine/logic/utils/incident_capture_handler.py +303 -0
  719. ciris_engine/logic/utils/initialization_manager.py +74 -0
  720. ciris_engine/logic/utils/jsondict_helpers.py +290 -0
  721. ciris_engine/logic/utils/log_sanitizer.py +97 -0
  722. ciris_engine/logic/utils/logging_config.py +151 -0
  723. ciris_engine/logic/utils/observability_decorators.py +544 -0
  724. ciris_engine/logic/utils/occurrence_utils.py +155 -0
  725. ciris_engine/logic/utils/path_resolution.py +281 -0
  726. ciris_engine/logic/utils/platform_detection.py +286 -0
  727. ciris_engine/logic/utils/privacy.py +266 -0
  728. ciris_engine/logic/utils/profile_loader.py +124 -0
  729. ciris_engine/logic/utils/profile_manager.py +16 -0
  730. ciris_engine/logic/utils/runtime_utils.py +69 -0
  731. ciris_engine/logic/utils/shutdown_manager.py +107 -0
  732. ciris_engine/logic/utils/task_formatters.py +60 -0
  733. ciris_engine/logic/utils/task_thought_factory.py +404 -0
  734. ciris_engine/logic/utils/thought_utils.py +54 -0
  735. ciris_engine/logic/utils/user_utils.py +70 -0
  736. ciris_engine/protocols/__init__.py +0 -0
  737. ciris_engine/protocols/adapters/__init__.py +35 -0
  738. ciris_engine/protocols/adapters/base.py +149 -0
  739. ciris_engine/protocols/adapters/configurable.py +265 -0
  740. ciris_engine/protocols/adapters/message.py +90 -0
  741. ciris_engine/protocols/audit/__init__.py +1 -0
  742. ciris_engine/protocols/buses/__init__.py +1 -0
  743. ciris_engine/protocols/config/__init__.py +1 -0
  744. ciris_engine/protocols/conscience/__init__.py +1 -0
  745. ciris_engine/protocols/consent.py +88 -0
  746. ciris_engine/protocols/context/__init__.py +1 -0
  747. ciris_engine/protocols/data/__init__.py +1 -0
  748. ciris_engine/protocols/dma/__init__.py +1 -0
  749. ciris_engine/protocols/dma/base.py +107 -0
  750. ciris_engine/protocols/faculties.py +34 -0
  751. ciris_engine/protocols/formatters/__init__.py +1 -0
  752. ciris_engine/protocols/handlers/__init__.py +1 -0
  753. ciris_engine/protocols/infrastructure/__init__.py +25 -0
  754. ciris_engine/protocols/infrastructure/base.py +377 -0
  755. ciris_engine/protocols/persistence/__init__.py +1 -0
  756. ciris_engine/protocols/pipeline_control.py +609 -0
  757. ciris_engine/protocols/processors/__init__.py +19 -0
  758. ciris_engine/protocols/processors/agent.py +299 -0
  759. ciris_engine/protocols/processors/base.py +130 -0
  760. ciris_engine/protocols/processors/orchestration.py +62 -0
  761. ciris_engine/protocols/registries/__init__.py +1 -0
  762. ciris_engine/protocols/runtime/__init__.py +1 -0
  763. ciris_engine/protocols/runtime/base.py +163 -0
  764. ciris_engine/protocols/secrets/__init__.py +1 -0
  765. ciris_engine/protocols/services/__init__.py +80 -0
  766. ciris_engine/protocols/services/adaptation/__init__.py +7 -0
  767. ciris_engine/protocols/services/adaptation/self_observation.py +265 -0
  768. ciris_engine/protocols/services/governance/__init__.py +20 -0
  769. ciris_engine/protocols/services/governance/communication.py +58 -0
  770. ciris_engine/protocols/services/governance/filter.py +56 -0
  771. ciris_engine/protocols/services/governance/visibility.py +32 -0
  772. ciris_engine/protocols/services/governance/wa_auth.py +192 -0
  773. ciris_engine/protocols/services/governance/wise_authority.py +75 -0
  774. ciris_engine/protocols/services/graph/__init__.py +19 -0
  775. ciris_engine/protocols/services/graph/audit.py +92 -0
  776. ciris_engine/protocols/services/graph/config.py +54 -0
  777. ciris_engine/protocols/services/graph/incident_management.py +103 -0
  778. ciris_engine/protocols/services/graph/memory.py +110 -0
  779. ciris_engine/protocols/services/graph/telemetry.py +51 -0
  780. ciris_engine/protocols/services/graph/tsdb_consolidation.py +87 -0
  781. ciris_engine/protocols/services/infrastructure/__init__.py +11 -0
  782. ciris_engine/protocols/services/infrastructure/authentication.py +159 -0
  783. ciris_engine/protocols/services/infrastructure/credit_gate.py +46 -0
  784. ciris_engine/protocols/services/infrastructure/database_maintenance.py +25 -0
  785. ciris_engine/protocols/services/infrastructure/resource_monitor.py +83 -0
  786. ciris_engine/protocols/services/lifecycle/__init__.py +13 -0
  787. ciris_engine/protocols/services/lifecycle/initialization.py +41 -0
  788. ciris_engine/protocols/services/lifecycle/scheduler.py +42 -0
  789. ciris_engine/protocols/services/lifecycle/shutdown.py +50 -0
  790. ciris_engine/protocols/services/lifecycle/time.py +31 -0
  791. ciris_engine/protocols/services/runtime/__init__.py +13 -0
  792. ciris_engine/protocols/services/runtime/llm.py +50 -0
  793. ciris_engine/protocols/services/runtime/runtime_control.py +193 -0
  794. ciris_engine/protocols/services/runtime/secrets.py +100 -0
  795. ciris_engine/protocols/services/runtime/tool.py +123 -0
  796. ciris_engine/protocols/telemetry/__init__.py +1 -0
  797. ciris_engine/protocols/utils/__init__.py +1 -0
  798. ciris_engine/schemas/__init__.py +112 -0
  799. ciris_engine/schemas/actions/__init__.py +37 -0
  800. ciris_engine/schemas/actions/parameters.py +137 -0
  801. ciris_engine/schemas/adapters/__init__.py +13 -0
  802. ciris_engine/schemas/adapters/cirisnode.py +135 -0
  803. ciris_engine/schemas/adapters/cli.py +97 -0
  804. ciris_engine/schemas/adapters/cli_tools.py +98 -0
  805. ciris_engine/schemas/adapters/discord.py +125 -0
  806. ciris_engine/schemas/adapters/graphql_core.py +144 -0
  807. ciris_engine/schemas/adapters/registration.py +47 -0
  808. ciris_engine/schemas/adapters/runtime_context.py +48 -0
  809. ciris_engine/schemas/adapters/tool_execution.py +45 -0
  810. ciris_engine/schemas/adapters/tools.py +96 -0
  811. ciris_engine/schemas/api/__init__.py +1 -0
  812. ciris_engine/schemas/api/agent.py +50 -0
  813. ciris_engine/schemas/api/audit.py +38 -0
  814. ciris_engine/schemas/api/auth.py +351 -0
  815. ciris_engine/schemas/api/config_security.py +242 -0
  816. ciris_engine/schemas/api/emergency.py +111 -0
  817. ciris_engine/schemas/api/responses.py +72 -0
  818. ciris_engine/schemas/api/runtime.py +26 -0
  819. ciris_engine/schemas/api/telemetry.py +109 -0
  820. ciris_engine/schemas/api/wa.py +90 -0
  821. ciris_engine/schemas/audit/__init__.py +13 -0
  822. ciris_engine/schemas/audit/core.py +139 -0
  823. ciris_engine/schemas/audit/hash_chain.py +58 -0
  824. ciris_engine/schemas/audit/verification.py +131 -0
  825. ciris_engine/schemas/buses/__init__.py +1 -0
  826. ciris_engine/schemas/config/__init__.py +41 -0
  827. ciris_engine/schemas/config/agent.py +279 -0
  828. ciris_engine/schemas/config/cognitive_state_behaviors.py +194 -0
  829. ciris_engine/schemas/config/default_dsar_sops.py +178 -0
  830. ciris_engine/schemas/config/essential.py +195 -0
  831. ciris_engine/schemas/config/tickets.py +86 -0
  832. ciris_engine/schemas/conscience/__init__.py +25 -0
  833. ciris_engine/schemas/conscience/context.py +34 -0
  834. ciris_engine/schemas/conscience/core.py +145 -0
  835. ciris_engine/schemas/conscience/results.py +24 -0
  836. ciris_engine/schemas/consent/__init__.py +5 -0
  837. ciris_engine/schemas/consent/core.py +404 -0
  838. ciris_engine/schemas/context/__init__.py +1 -0
  839. ciris_engine/schemas/covenant.py +382 -0
  840. ciris_engine/schemas/data/__init__.py +1 -0
  841. ciris_engine/schemas/dma/__init__.py +16 -0
  842. ciris_engine/schemas/dma/core.py +199 -0
  843. ciris_engine/schemas/dma/faculty.py +192 -0
  844. ciris_engine/schemas/dma/prompts.py +172 -0
  845. ciris_engine/schemas/dma/results.py +103 -0
  846. ciris_engine/schemas/formatters/__init__.py +1 -0
  847. ciris_engine/schemas/handlers/__init__.py +10 -0
  848. ciris_engine/schemas/handlers/context.py +119 -0
  849. ciris_engine/schemas/handlers/contexts.py +100 -0
  850. ciris_engine/schemas/handlers/core.py +167 -0
  851. ciris_engine/schemas/handlers/memory_schemas.py +67 -0
  852. ciris_engine/schemas/handlers/schemas.py +95 -0
  853. ciris_engine/schemas/identity.py +149 -0
  854. ciris_engine/schemas/infrastructure/__init__.py +1 -0
  855. ciris_engine/schemas/infrastructure/base.py +256 -0
  856. ciris_engine/schemas/infrastructure/behavioral_patterns.py +129 -0
  857. ciris_engine/schemas/infrastructure/feedback_loop.py +57 -0
  858. ciris_engine/schemas/infrastructure/identity_variance.py +141 -0
  859. ciris_engine/schemas/infrastructure/oauth.py +175 -0
  860. ciris_engine/schemas/infrastructure/wa_cli_wizard.py +54 -0
  861. ciris_engine/schemas/persistence/__init__.py +34 -0
  862. ciris_engine/schemas/persistence/core.py +140 -0
  863. ciris_engine/schemas/persistence/correlations.py +73 -0
  864. ciris_engine/schemas/persistence/postgres/__init__.py +1 -0
  865. ciris_engine/schemas/persistence/postgres/tables.py +280 -0
  866. ciris_engine/schemas/persistence/sqlite/__init__.py +1 -0
  867. ciris_engine/schemas/persistence/sqlite/tables.py +281 -0
  868. ciris_engine/schemas/platform.py +149 -0
  869. ciris_engine/schemas/processors/__init__.py +26 -0
  870. ciris_engine/schemas/processors/base.py +130 -0
  871. ciris_engine/schemas/processors/cognitive.py +77 -0
  872. ciris_engine/schemas/processors/context.py +35 -0
  873. ciris_engine/schemas/processors/core.py +152 -0
  874. ciris_engine/schemas/processors/dma.py +105 -0
  875. ciris_engine/schemas/processors/error.py +122 -0
  876. ciris_engine/schemas/processors/main.py +109 -0
  877. ciris_engine/schemas/processors/phase_results.py +21 -0
  878. ciris_engine/schemas/processors/results.py +99 -0
  879. ciris_engine/schemas/processors/solitude.py +79 -0
  880. ciris_engine/schemas/processors/state.py +202 -0
  881. ciris_engine/schemas/processors/state_example.py +177 -0
  882. ciris_engine/schemas/processors/states.py +21 -0
  883. ciris_engine/schemas/processors/status.py +34 -0
  884. ciris_engine/schemas/registries/__init__.py +1 -0
  885. ciris_engine/schemas/registries/base.py +66 -0
  886. ciris_engine/schemas/resources/__init__.py +15 -0
  887. ciris_engine/schemas/resources/crisis.py +315 -0
  888. ciris_engine/schemas/runtime/__init__.py +42 -0
  889. ciris_engine/schemas/runtime/adapter_management.py +186 -0
  890. ciris_engine/schemas/runtime/api.py +58 -0
  891. ciris_engine/schemas/runtime/audit.py +50 -0
  892. ciris_engine/schemas/runtime/bootstrap.py +33 -0
  893. ciris_engine/schemas/runtime/contexts.py +61 -0
  894. ciris_engine/schemas/runtime/core.py +161 -0
  895. ciris_engine/schemas/runtime/enums.py +167 -0
  896. ciris_engine/schemas/runtime/extended.py +232 -0
  897. ciris_engine/schemas/runtime/manifest.py +311 -0
  898. ciris_engine/schemas/runtime/memory.py +60 -0
  899. ciris_engine/schemas/runtime/messages.py +108 -0
  900. ciris_engine/schemas/runtime/models.py +156 -0
  901. ciris_engine/schemas/runtime/processing_context.py +43 -0
  902. ciris_engine/schemas/runtime/protocols_core.py +96 -0
  903. ciris_engine/schemas/runtime/resources.py +33 -0
  904. ciris_engine/schemas/runtime/system_context.py +417 -0
  905. ciris_engine/schemas/secrets/__init__.py +1 -0
  906. ciris_engine/schemas/secrets/core.py +267 -0
  907. ciris_engine/schemas/secrets/service.py +95 -0
  908. ciris_engine/schemas/services/__init__.py +33 -0
  909. ciris_engine/schemas/services/audit_summary_node.py +172 -0
  910. ciris_engine/schemas/services/authority/__init__.py +39 -0
  911. ciris_engine/schemas/services/authority/jwt.py +158 -0
  912. ciris_engine/schemas/services/authority/wa_updates.py +138 -0
  913. ciris_engine/schemas/services/authority/wise_authority.py +163 -0
  914. ciris_engine/schemas/services/authority_core.py +370 -0
  915. ciris_engine/schemas/services/capabilities.py +72 -0
  916. ciris_engine/schemas/services/community_core.py +95 -0
  917. ciris_engine/schemas/services/context.py +111 -0
  918. ciris_engine/schemas/services/conversation_summary_node.py +189 -0
  919. ciris_engine/schemas/services/core/__init__.py +153 -0
  920. ciris_engine/schemas/services/core/runtime.py +262 -0
  921. ciris_engine/schemas/services/core/runtime_config.py +117 -0
  922. ciris_engine/schemas/services/core/secrets.py +65 -0
  923. ciris_engine/schemas/services/correlation_node.py +179 -0
  924. ciris_engine/schemas/services/credit_gate.py +92 -0
  925. ciris_engine/schemas/services/discord_nodes.py +299 -0
  926. ciris_engine/schemas/services/feedback_core.py +131 -0
  927. ciris_engine/schemas/services/filters_core.py +270 -0
  928. ciris_engine/schemas/services/governance.py +26 -0
  929. ciris_engine/schemas/services/graph/__init__.py +26 -0
  930. ciris_engine/schemas/services/graph/attributes.py +254 -0
  931. ciris_engine/schemas/services/graph/audit.py +98 -0
  932. ciris_engine/schemas/services/graph/consolidation.py +338 -0
  933. ciris_engine/schemas/services/graph/edge_types.py +43 -0
  934. ciris_engine/schemas/services/graph/edges.py +88 -0
  935. ciris_engine/schemas/services/graph/incident.py +312 -0
  936. ciris_engine/schemas/services/graph/memory.py +84 -0
  937. ciris_engine/schemas/services/graph/node_data.py +174 -0
  938. ciris_engine/schemas/services/graph/query_results.py +82 -0
  939. ciris_engine/schemas/services/graph/telemetry.py +250 -0
  940. ciris_engine/schemas/services/graph/tsdb_consolidation.py +27 -0
  941. ciris_engine/schemas/services/graph/tsdb_models.py +107 -0
  942. ciris_engine/schemas/services/graph_core.py +196 -0
  943. ciris_engine/schemas/services/graph_typed_nodes.py +194 -0
  944. ciris_engine/schemas/services/infrastructure/__init__.py +1 -0
  945. ciris_engine/schemas/services/infrastructure/resource_monitor.py +20 -0
  946. ciris_engine/schemas/services/lifecycle/__init__.py +9 -0
  947. ciris_engine/schemas/services/lifecycle/initialization.py +33 -0
  948. ciris_engine/schemas/services/lifecycle/time.py +50 -0
  949. ciris_engine/schemas/services/llm.py +187 -0
  950. ciris_engine/schemas/services/metadata.py +43 -0
  951. ciris_engine/schemas/services/nodes.py +704 -0
  952. ciris_engine/schemas/services/operations.py +126 -0
  953. ciris_engine/schemas/services/requests.py +128 -0
  954. ciris_engine/schemas/services/resources_core.py +182 -0
  955. ciris_engine/schemas/services/runtime_control.py +1010 -0
  956. ciris_engine/schemas/services/shutdown.py +88 -0
  957. ciris_engine/schemas/services/special/__init__.py +0 -0
  958. ciris_engine/schemas/services/special/self_observation.py +396 -0
  959. ciris_engine/schemas/services/trace_summary_node.py +199 -0
  960. ciris_engine/schemas/services/visibility.py +98 -0
  961. ciris_engine/schemas/streaming/__init__.py +10 -0
  962. ciris_engine/schemas/streaming/reasoning_stream.py +95 -0
  963. ciris_engine/schemas/telemetry/__init__.py +0 -0
  964. ciris_engine/schemas/telemetry/collector.py +67 -0
  965. ciris_engine/schemas/telemetry/core.py +252 -0
  966. ciris_engine/schemas/telemetry/unified.py +59 -0
  967. ciris_engine/schemas/tools.py +72 -0
  968. ciris_engine/schemas/types.py +47 -0
  969. ciris_engine/schemas/utils/__init__.py +1 -0
  970. ciris_engine/schemas/utils/config_validator.py +54 -0
  971. ciris_engine/utils/__init__.py +1 -0
  972. ciris_engine/utils/serialization.py +35 -0
  973. ciris_sdk/__init__.py +124 -0
  974. ciris_sdk/auth_store.py +261 -0
  975. ciris_sdk/client.py +261 -0
  976. ciris_sdk/exceptions.py +73 -0
  977. ciris_sdk/model_types.py +258 -0
  978. ciris_sdk/models.py +354 -0
  979. ciris_sdk/pagination.py +214 -0
  980. ciris_sdk/rate_limiter.py +188 -0
  981. ciris_sdk/setup.py +17 -0
  982. ciris_sdk/telemetry_models.py +257 -0
  983. ciris_sdk/telemetry_responses.py +199 -0
  984. ciris_sdk/transport.py +177 -0
  985. ciris_sdk/websocket.py +400 -0
  986. main.py +766 -0
@@ -0,0 +1,1745 @@
1
+ """
2
+ Authentication API routes for CIRIS.
3
+
4
+ Implements session management endpoints:
5
+ - POST /v1/auth/login - Authenticate user
6
+ - POST /v1/auth/logout - End session
7
+ - GET /v1/auth/me - Current user info (includes permissions)
8
+ - POST /v1/auth/refresh - Refresh token
9
+
10
+ Note: OAuth endpoints are in api_auth_v2.py
11
+ """
12
+
13
+ import logging
14
+ import os
15
+ import secrets
16
+ from datetime import datetime, timedelta, timezone
17
+ from pathlib import Path
18
+ from typing import Any, Dict, List, Optional, Set
19
+
20
+ from fastapi import APIRouter, Depends, HTTPException, Request, status
21
+ from fastapi.responses import RedirectResponse
22
+ from pydantic import BaseModel, Field
23
+
24
+ from ciris_engine.logic.adapters.api.services.auth_service import OAuthUser
25
+ from ciris_engine.logic.adapters.api.services.oauth_security import validate_oauth_picture_url
26
+ from ciris_engine.schemas.api.auth import (
27
+ APIKeyCreateRequest,
28
+ APIKeyInfo,
29
+ APIKeyListResponse,
30
+ APIKeyResponse,
31
+ AuthContext,
32
+ LoginRequest,
33
+ LoginResponse,
34
+ TokenRefreshRequest,
35
+ UserInfo,
36
+ UserRole,
37
+ )
38
+ from ciris_engine.schemas.runtime.api import APIRole
39
+
40
+ from ..dependencies.auth import check_permissions, get_auth_context, get_auth_service, optional_auth
41
+ from ..services.auth_service import APIAuthService
42
+
43
+ # Constants
44
+ OAUTH_CONFIG_PATH = Path("/home/ciris/shared/oauth/oauth.json")
45
+ OAUTH_CONFIG_DIR = ".ciris"
46
+ OAUTH_CONFIG_FILE = "oauth.json"
47
+ PROVIDER_NAME_DESC = "Provider name"
48
+ # Get agent ID from environment, default to 'datum' if not set
49
+ AGENT_ID = os.getenv("CIRIS_AGENT_ID", "datum")
50
+ OAUTH_CALLBACK_PATH = f"/v1/auth/oauth/{AGENT_ID}/{{provider}}/callback"
51
+ DEFAULT_OAUTH_BASE_URL = "https://agents.ciris.ai"
52
+ # Error messages
53
+ FETCH_USER_INFO_ERROR = "Failed to fetch user info"
54
+
55
+ # OAuth Frontend Redirect Configuration
56
+ # These environment variables control where users are redirected after OAuth and what parameters are included
57
+ OAUTH_FRONTEND_URL = os.getenv("OAUTH_FRONTEND_URL") # e.g., https://scout.ciris.ai
58
+ OAUTH_FRONTEND_PATH = os.getenv("OAUTH_FRONTEND_PATH", "/oauth-complete.html") # Default: /oauth-complete.html
59
+ # Comma-separated list of parameters to include in redirect
60
+ # Default includes all ScoutGUI requirements
61
+ OAUTH_REDIRECT_PARAMS = os.getenv(
62
+ "OAUTH_REDIRECT_PARAMS", "access_token,token_type,role,user_id,expires_in,email,marketing_opt_in,agent,provider"
63
+ ).split(",")
64
+ # Comma-separated list of allowed redirect domains for OAuth (security: prevents open redirect attacks)
65
+ # Always includes OAUTH_FRONTEND_URL if set. Relative paths (starting with /) are always allowed.
66
+ OAUTH_ALLOWED_REDIRECT_DOMAINS = os.getenv("OAUTH_ALLOWED_REDIRECT_DOMAINS", "").split(",")
67
+ OAUTH_ALLOWED_REDIRECT_DOMAINS = [d.strip().lower() for d in OAUTH_ALLOWED_REDIRECT_DOMAINS if d.strip()]
68
+
69
+
70
+ # Helper functions
71
+ def get_oauth_callback_url(provider: str, base_url: Optional[str] = None) -> str:
72
+ """Get the OAuth callback URL for a specific provider."""
73
+ if base_url is None:
74
+ base_url = os.getenv("OAUTH_CALLBACK_BASE_URL", DEFAULT_OAUTH_BASE_URL)
75
+ return base_url + OAUTH_CALLBACK_PATH.replace("{provider}", provider)
76
+
77
+
78
+ def extract_query_params(url: str) -> Dict[str, str]:
79
+ """Extract query parameters from a URL."""
80
+ import urllib.parse
81
+
82
+ parsed = urllib.parse.urlparse(url)
83
+ return dict(urllib.parse.parse_qsl(parsed.query))
84
+
85
+
86
+ def _is_private_network_host(host: str) -> bool:
87
+ """
88
+ Check if a host is on a private/local network.
89
+
90
+ Allows HTTP for local development and Home Assistant on local networks.
91
+ """
92
+ import ipaddress
93
+
94
+ # Remove port if present
95
+ hostname = host.split(":")[0].lower()
96
+
97
+ # Check for localhost variants
98
+ if hostname in ("localhost", "127.0.0.1", "::1"):
99
+ return True
100
+
101
+ # Check for .local mDNS domains (common for Home Assistant)
102
+ if hostname.endswith(".local"):
103
+ return True
104
+
105
+ # Check for private IP ranges
106
+ try:
107
+ ip = ipaddress.ip_address(hostname)
108
+ return ip.is_private or ip.is_loopback
109
+ except ValueError:
110
+ # Not a valid IP address, check if it looks like a local hostname
111
+ pass
112
+
113
+ return False
114
+
115
+
116
+ def validate_redirect_uri(redirect_uri: Optional[str]) -> Optional[str]:
117
+ """
118
+ Validate redirect_uri to prevent open redirect attacks.
119
+
120
+ Security: Only allows:
121
+ - Relative paths (starting with /)
122
+ - URLs matching OAUTH_FRONTEND_URL domain
123
+ - URLs matching domains in OAUTH_ALLOWED_REDIRECT_DOMAINS
124
+ - HTTP allowed for private/local networks (Home Assistant, local dev)
125
+
126
+ Returns the redirect_uri if valid, None if invalid/untrusted.
127
+ """
128
+ import urllib.parse
129
+
130
+ if not redirect_uri:
131
+ return None
132
+
133
+ # Relative paths are always safe (same-origin)
134
+ if redirect_uri.startswith("/"):
135
+ # Prevent path traversal tricks like //evil.com
136
+ if redirect_uri.startswith("//"):
137
+ logger.warning(f"Rejected redirect_uri with protocol-relative path: {redirect_uri[:50]}")
138
+ return None
139
+ return redirect_uri
140
+
141
+ # Parse the URL to extract domain
142
+ try:
143
+ parsed = urllib.parse.urlparse(redirect_uri)
144
+ if not parsed.scheme or not parsed.netloc:
145
+ logger.warning(f"Rejected malformed redirect_uri: {redirect_uri[:50]}")
146
+ return None
147
+
148
+ scheme = parsed.scheme.lower()
149
+ is_private = _is_private_network_host(parsed.netloc)
150
+
151
+ # Allow HTTP only for private/local networks (Home Assistant, local dev)
152
+ # Require HTTPS for all public URLs
153
+ if scheme == "http":
154
+ if not is_private:
155
+ logger.warning(f"Rejected HTTP redirect_uri to public host: {redirect_uri[:50]}")
156
+ return None
157
+ # HTTP to private network is allowed
158
+ logger.debug(f"Allowing HTTP redirect to private network: {parsed.netloc}")
159
+ elif scheme != "https":
160
+ logger.warning(f"Rejected redirect_uri with unsupported scheme: {scheme}")
161
+ return None
162
+
163
+ redirect_domain = parsed.netloc.lower()
164
+
165
+ # Private network hosts are always allowed (Home Assistant, local dev)
166
+ # This enables OAuth callbacks to local Home Assistant instances
167
+ if is_private:
168
+ logger.debug(f"Allowing redirect to private network host: {redirect_domain}")
169
+ return redirect_uri
170
+
171
+ # Build list of allowed domains for public URLs
172
+ allowed_domains: Set[str] = set(OAUTH_ALLOWED_REDIRECT_DOMAINS)
173
+
174
+ # Always allow OAUTH_FRONTEND_URL domain if configured
175
+ if OAUTH_FRONTEND_URL:
176
+ frontend_parsed = urllib.parse.urlparse(OAUTH_FRONTEND_URL)
177
+ if frontend_parsed.netloc:
178
+ allowed_domains.add(frontend_parsed.netloc.lower())
179
+
180
+ # Check if redirect domain is allowed
181
+ if redirect_domain in allowed_domains:
182
+ return redirect_uri
183
+
184
+ # Check for subdomain matches (e.g., allow *.ciris.ai if ciris.ai is in allowed)
185
+ for allowed in allowed_domains:
186
+ if redirect_domain == allowed or redirect_domain.endswith("." + allowed):
187
+ return redirect_uri
188
+
189
+ logger.warning(
190
+ f"Rejected redirect_uri to untrusted domain: {redirect_domain}. "
191
+ f"Allowed domains: {allowed_domains or '(none configured)'}"
192
+ )
193
+ return None
194
+
195
+ except Exception as e:
196
+ logger.warning(f"Failed to parse redirect_uri: {e}")
197
+ return None
198
+
199
+
200
+ logger = logging.getLogger(__name__)
201
+
202
+ router = APIRouter(tags=["Authentication"])
203
+
204
+
205
+ @router.post("/auth/login", response_model=LoginResponse)
206
+ async def login(
207
+ request: LoginRequest, req: Request, auth_service: APIAuthService = Depends(get_auth_service)
208
+ ) -> LoginResponse:
209
+ """
210
+ Authenticate with username/password.
211
+
212
+ Currently supports system admin user only. In production, this would
213
+ integrate with a proper user database.
214
+ """
215
+ getattr(req.app.state, "config_service", None)
216
+
217
+ # Verify username and password using secure bcrypt verification
218
+ user = await auth_service.verify_user_password(request.username, request.password)
219
+
220
+ if not user:
221
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
222
+
223
+ # Generate API key based on user's role
224
+ api_key = f"ciris_{user.api_role.value.lower()}_{secrets.token_urlsafe(32)}"
225
+ expires_at = datetime.now(timezone.utc) + timedelta(hours=24)
226
+
227
+ # Map APIRole to UserRole for API key storage
228
+ user_role_map = {
229
+ APIRole.OBSERVER: UserRole.OBSERVER,
230
+ APIRole.ADMIN: UserRole.ADMIN,
231
+ APIRole.AUTHORITY: UserRole.AUTHORITY,
232
+ APIRole.SYSTEM_ADMIN: UserRole.SYSTEM_ADMIN,
233
+ }
234
+
235
+ # Store API key
236
+ auth_service.store_api_key(
237
+ key=api_key,
238
+ user_id=user.wa_id,
239
+ role=user_role_map[user.api_role],
240
+ expires_at=expires_at,
241
+ description="Login session",
242
+ )
243
+
244
+ logger.info(f"User {user.name} logged in successfully")
245
+
246
+ return LoginResponse(
247
+ access_token=api_key,
248
+ token_type="Bearer",
249
+ expires_in=86400, # 24 hours
250
+ role=user_role_map[user.api_role],
251
+ user_id=user.wa_id,
252
+ )
253
+
254
+
255
+ @router.post("/auth/logout", status_code=status.HTTP_204_NO_CONTENT)
256
+ async def logout(
257
+ auth: AuthContext = Depends(get_auth_context), auth_service: APIAuthService = Depends(get_auth_service)
258
+ ) -> None:
259
+ """
260
+ End the current session by revoking the API key.
261
+
262
+ This endpoint invalidates the current authentication token,
263
+ effectively logging out the user.
264
+ """
265
+ if auth.api_key_id:
266
+ auth_service.revoke_api_key(auth.api_key_id)
267
+ # Don't log sensitive API key ID
268
+ logger.info(f"User {auth.user_id} logged out, API key revoked")
269
+
270
+ return None
271
+
272
+
273
+ @router.get("/auth/me", response_model=UserInfo)
274
+ async def get_current_user(
275
+ auth: AuthContext = Depends(get_auth_context), auth_service: APIAuthService = Depends(get_auth_service)
276
+ ) -> UserInfo:
277
+ """
278
+ Get current authenticated user information.
279
+
280
+ Returns details about the currently authenticated user including
281
+ their role and all permissions based on that role.
282
+ """
283
+ # Use permissions from the auth context which includes custom permissions
284
+ permissions = [p.value for p in auth.permissions]
285
+
286
+ # Fetch actual username from auth service
287
+ user = auth_service.get_user(auth.user_id)
288
+ username = user.name if user else auth.user_id # Fallback to user_id if not found
289
+
290
+ return UserInfo(
291
+ user_id=auth.user_id,
292
+ username=username,
293
+ role=auth.role,
294
+ permissions=permissions,
295
+ created_at=auth.authenticated_at, # Use auth time as proxy
296
+ last_login=auth.authenticated_at,
297
+ )
298
+
299
+
300
+ @router.post("/auth/refresh", response_model=LoginResponse)
301
+ async def refresh_token(
302
+ request: TokenRefreshRequest,
303
+ auth: Optional[AuthContext] = Depends(optional_auth),
304
+ auth_service: APIAuthService = Depends(get_auth_service),
305
+ ) -> LoginResponse:
306
+ """
307
+ Refresh access token.
308
+
309
+ Creates a new access token and revokes the old one. Supports both
310
+ API key and OAuth refresh flows. The user must be authenticated
311
+ to refresh their token.
312
+ """
313
+ # For now, we require the user to be authenticated to refresh
314
+ # In a full implementation, we'd validate the refresh token separately
315
+ if not auth:
316
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required to refresh token")
317
+
318
+ # Generate new API key
319
+ new_api_key = f"ciris_{auth.role.value.lower()}_{secrets.token_urlsafe(32)}"
320
+
321
+ # Set expiration based on role
322
+ if auth.role == UserRole.SYSTEM_ADMIN:
323
+ expires_at = datetime.now(timezone.utc) + timedelta(hours=24)
324
+ expires_in = 86400 # 24 hours
325
+ else:
326
+ expires_at = datetime.now(timezone.utc) + timedelta(days=30)
327
+ expires_in = 2592000 # 30 days
328
+
329
+ # Store new API key
330
+ auth_service.store_api_key(
331
+ key=new_api_key, user_id=auth.user_id, role=auth.role, expires_at=expires_at, description="Refreshed token"
332
+ )
333
+
334
+ # Revoke old API key if it exists
335
+ if auth.api_key_id:
336
+ auth_service.revoke_api_key(auth.api_key_id)
337
+
338
+ logger.info(f"Token refreshed for user {auth.user_id}")
339
+
340
+ return LoginResponse(
341
+ access_token=new_api_key, token_type="Bearer", expires_in=expires_in, role=auth.role, user_id=auth.user_id
342
+ )
343
+
344
+
345
+ # ========== OAuth Management Endpoints ==========
346
+
347
+
348
+ class OAuthProviderInfo(BaseModel):
349
+ """OAuth provider information."""
350
+
351
+ provider: str = Field(..., description=PROVIDER_NAME_DESC)
352
+ client_id: str = Field(..., description="OAuth client ID")
353
+ created: Optional[str] = Field(None, description="Creation timestamp")
354
+ callback_url: str = Field(..., description="OAuth callback URL")
355
+ metadata: Dict[str, str] = Field(default_factory=dict, description="Additional metadata")
356
+
357
+
358
+ class OAuthProvidersResponse(BaseModel):
359
+ """OAuth providers list response."""
360
+
361
+ providers: List[OAuthProviderInfo] = Field(default_factory=list, description="List of configured providers")
362
+
363
+
364
+ @router.get("/auth/oauth/providers", response_model=OAuthProvidersResponse)
365
+ async def list_oauth_providers(
366
+ request: Request,
367
+ auth: AuthContext = Depends(get_auth_context),
368
+ _: None = Depends(check_permissions(["users.write"])), # SYSTEM_ADMIN only
369
+ ) -> OAuthProvidersResponse:
370
+ """
371
+ List configured OAuth providers.
372
+
373
+ Requires: users.write permission (SYSTEM_ADMIN only)
374
+ """
375
+ import json
376
+ from pathlib import Path
377
+
378
+ # Check shared volume first (managed mode), then fall back to local (standalone)
379
+ oauth_config_file = OAUTH_CONFIG_PATH
380
+ if not oauth_config_file.exists():
381
+ oauth_config_file = Path.home() / OAUTH_CONFIG_DIR / OAUTH_CONFIG_FILE
382
+ logger.debug(f"Using local OAuth config: {oauth_config_file}")
383
+ else:
384
+ logger.debug(f"Using shared OAuth config: {oauth_config_file}")
385
+
386
+ if not oauth_config_file.exists():
387
+ return OAuthProvidersResponse(providers=[])
388
+
389
+ try:
390
+ config = json.loads(oauth_config_file.read_text())
391
+ providers = []
392
+
393
+ for provider, settings in config.items():
394
+ providers.append(
395
+ OAuthProviderInfo(
396
+ provider=provider,
397
+ client_id=settings.get("client_id", ""),
398
+ created=settings.get("created"),
399
+ callback_url=f"{request.headers.get('x-forwarded-proto', request.url.scheme)}://{request.headers.get('host', 'localhost')}{OAUTH_CALLBACK_PATH}",
400
+ metadata=settings.get("metadata", {}),
401
+ )
402
+ )
403
+
404
+ return OAuthProvidersResponse(providers=providers)
405
+ except Exception as e:
406
+ logger.error(f"Failed to read OAuth config: {e}")
407
+ raise HTTPException(
408
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to read OAuth configuration"
409
+ )
410
+
411
+
412
+ class ConfigureOAuthProviderRequest(BaseModel):
413
+ """Request to configure an OAuth provider."""
414
+
415
+ provider: str = Field(..., description=PROVIDER_NAME_DESC)
416
+ client_id: str = Field(..., description="OAuth client ID")
417
+ client_secret: str = Field(..., description="OAuth client secret")
418
+ metadata: Optional[Dict[str, str]] = Field(None, description="Additional metadata")
419
+
420
+
421
+ class ConfigureOAuthProviderResponse(BaseModel):
422
+ """Response from OAuth provider configuration."""
423
+
424
+ provider: str = Field(..., description=PROVIDER_NAME_DESC)
425
+ callback_url: str = Field(..., description="OAuth callback URL")
426
+ message: str = Field(..., description="Status message")
427
+
428
+
429
+ @router.post("/auth/oauth/providers", response_model=ConfigureOAuthProviderResponse)
430
+ async def configure_oauth_provider(
431
+ body: ConfigureOAuthProviderRequest,
432
+ request: Request,
433
+ auth: AuthContext = Depends(get_auth_context),
434
+ _: None = Depends(check_permissions(["users.write"])), # SYSTEM_ADMIN only
435
+ ) -> ConfigureOAuthProviderResponse:
436
+ """
437
+ Configure an OAuth provider.
438
+
439
+ Requires: users.write permission (SYSTEM_ADMIN only)
440
+ """
441
+ import json
442
+ from pathlib import Path
443
+
444
+ # Check shared volume first (managed mode), then fall back to local (standalone)
445
+ oauth_config_file = OAUTH_CONFIG_PATH
446
+ if not oauth_config_file.exists():
447
+ oauth_config_file = Path.home() / OAUTH_CONFIG_DIR / OAUTH_CONFIG_FILE
448
+ logger.debug(f"Using local OAuth config: {oauth_config_file}")
449
+ else:
450
+ logger.debug(f"Using shared OAuth config: {oauth_config_file}")
451
+ oauth_config_file.parent.mkdir(exist_ok=True, mode=0o700)
452
+
453
+ # Load existing config
454
+ config = {}
455
+ if oauth_config_file.exists():
456
+ try:
457
+ config = json.loads(oauth_config_file.read_text())
458
+ except (json.JSONDecodeError, IOError, OSError) as e:
459
+ logger.warning(f"Failed to load OAuth config file: {e}")
460
+ pass
461
+
462
+ # Add/update provider
463
+ config[body.provider] = {
464
+ "client_id": body.client_id,
465
+ "client_secret": body.client_secret,
466
+ "created": datetime.now(timezone.utc).isoformat(),
467
+ "metadata": body.metadata or {},
468
+ }
469
+
470
+ # Save config
471
+ try:
472
+ oauth_config_file.write_text(json.dumps(config, indent=2))
473
+ oauth_config_file.chmod(0o600)
474
+
475
+ logger.info(f"OAuth provider '{body.provider}' configured by {auth.user_id}")
476
+
477
+ return ConfigureOAuthProviderResponse(
478
+ provider=body.provider,
479
+ callback_url=f"{request.headers.get('x-forwarded-proto', request.url.scheme)}://{request.headers.get('host', 'localhost')}{OAUTH_CALLBACK_PATH}",
480
+ message="OAuth provider configured successfully",
481
+ )
482
+ except Exception as e:
483
+ logger.error(f"Failed to save OAuth config: {e}")
484
+ raise HTTPException(
485
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to save OAuth configuration"
486
+ )
487
+
488
+
489
+ class OAuthLoginResponse(BaseModel):
490
+ """OAuth login initiation response."""
491
+
492
+ authorization_url: str = Field(..., description="URL to redirect user to for authorization")
493
+ state: str = Field(..., description="State parameter for CSRF protection")
494
+
495
+
496
+ @router.get("/auth/oauth/{provider}/login")
497
+ async def oauth_login(provider: str, request: Request, redirect_uri: Optional[str] = None) -> RedirectResponse:
498
+ """
499
+ Initiate OAuth login flow.
500
+
501
+ Redirects to the OAuth provider's authorization URL.
502
+ Accepts optional redirect_uri to specify where to send tokens after OAuth.
503
+ """
504
+ import base64
505
+ import json
506
+ import urllib.parse
507
+ from pathlib import Path
508
+
509
+ # Check shared volume first (managed mode), then fall back to local (standalone)
510
+ oauth_config_file = OAUTH_CONFIG_PATH
511
+ if not oauth_config_file.exists():
512
+ oauth_config_file = Path.home() / OAUTH_CONFIG_DIR / OAUTH_CONFIG_FILE
513
+ logger.debug(f"Using local OAuth config: {oauth_config_file}")
514
+ else:
515
+ logger.debug(f"Using shared OAuth config: {oauth_config_file}")
516
+
517
+ if not oauth_config_file.exists():
518
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"OAuth provider '{provider}' not configured")
519
+
520
+ try:
521
+ config = json.loads(oauth_config_file.read_text())
522
+ if provider not in config:
523
+ raise HTTPException(
524
+ status_code=status.HTTP_404_NOT_FOUND, detail=f"OAuth provider '{provider}' not configured"
525
+ )
526
+
527
+ provider_config = config[provider]
528
+ client_id = provider_config["client_id"]
529
+
530
+ # Generate CSRF token
531
+ csrf_token = secrets.token_urlsafe(32)
532
+
533
+ # Validate redirect_uri to prevent open redirect attacks (security)
534
+ validated_redirect_uri = validate_redirect_uri(redirect_uri)
535
+ if redirect_uri and not validated_redirect_uri:
536
+ logger.warning(
537
+ f"OAuth login rejected untrusted redirect_uri from {request.client.host if request.client else 'unknown'}"
538
+ )
539
+ raise HTTPException(
540
+ status_code=status.HTTP_400_BAD_REQUEST,
541
+ detail="Invalid redirect_uri: must be a relative path or trusted domain",
542
+ )
543
+
544
+ # Encode state with CSRF token and optional redirect_uri
545
+ state_data = {"csrf": csrf_token}
546
+ if validated_redirect_uri:
547
+ state_data["redirect_uri"] = validated_redirect_uri
548
+ logger.info(f"OAuth login initiated with validated redirect_uri: {validated_redirect_uri}")
549
+
550
+ # Base64 encode the state JSON
551
+ state = base64.urlsafe_b64encode(json.dumps(state_data).encode()).decode()
552
+
553
+ # Use OAUTH_CALLBACK_BASE_URL environment variable, or construct from request
554
+ base_url = os.getenv("OAUTH_CALLBACK_BASE_URL")
555
+ if not base_url:
556
+ # Construct from request headers
557
+ base_url = f"{request.headers.get('x-forwarded-proto', request.url.scheme)}://{request.headers.get('host', 'localhost')}"
558
+
559
+ # Always use API callback URL for OAuth providers (this is what's registered in Google Console)
560
+ callback_url = get_oauth_callback_url(provider, base_url)
561
+
562
+ # Build authorization URL based on provider
563
+ if provider == "google":
564
+ auth_url = "https://accounts.google.com/o/oauth2/v2/auth"
565
+ params = {
566
+ "client_id": client_id,
567
+ "redirect_uri": callback_url,
568
+ "response_type": "code",
569
+ "scope": "openid email profile",
570
+ "state": state,
571
+ "access_type": "offline",
572
+ "prompt": "consent",
573
+ }
574
+ elif provider == "github":
575
+ auth_url = "https://github.com/login/oauth/authorize"
576
+ params = {
577
+ "client_id": client_id,
578
+ "redirect_uri": callback_url,
579
+ "scope": "read:user user:email",
580
+ "state": state,
581
+ }
582
+ elif provider == "discord":
583
+ auth_url = "https://discord.com/api/oauth2/authorize"
584
+ params = {
585
+ "client_id": client_id,
586
+ "redirect_uri": callback_url,
587
+ "response_type": "code",
588
+ "scope": "identify email",
589
+ "state": state,
590
+ }
591
+ else:
592
+ raise HTTPException(
593
+ status_code=status.HTTP_400_BAD_REQUEST, detail=f"Unsupported OAuth provider: {provider}"
594
+ )
595
+
596
+ # Build full URL
597
+ full_url = f"{auth_url}?{urllib.parse.urlencode(params)}"
598
+
599
+ # Redirect user to OAuth provider
600
+ return RedirectResponse(url=full_url, status_code=302)
601
+
602
+ except Exception as e:
603
+ logger.error(f"OAuth login initiation failed: {e}")
604
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to initiate OAuth login")
605
+
606
+
607
+ def _load_oauth_config(provider: str) -> Dict[str, str]:
608
+ """Load OAuth configuration for the specified provider."""
609
+ import json
610
+ from pathlib import Path
611
+
612
+ # Check shared volume first (managed mode), then fall back to local (standalone)
613
+ oauth_config_file = OAUTH_CONFIG_PATH
614
+ if not oauth_config_file.exists():
615
+ oauth_config_file = Path.home() / OAUTH_CONFIG_DIR / OAUTH_CONFIG_FILE
616
+ logger.debug(f"Using local OAuth config: {oauth_config_file}")
617
+ else:
618
+ logger.debug(f"Using shared OAuth config: {oauth_config_file}")
619
+
620
+ if not oauth_config_file.exists():
621
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"OAuth provider '{provider}' not configured")
622
+
623
+ config = json.loads(oauth_config_file.read_text())
624
+ if provider not in config:
625
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"OAuth provider '{provider}' not configured")
626
+
627
+ provider_config: Dict[str, str] = config[provider]
628
+ return provider_config
629
+
630
+
631
+ async def _handle_google_oauth(code: str, client_id: str, client_secret: str) -> Dict[str, Optional[str]]:
632
+ """Handle Google OAuth token exchange and user info retrieval."""
633
+ import httpx
634
+
635
+ async with httpx.AsyncClient() as client:
636
+ # Exchange code for token
637
+ token_response = await client.post(
638
+ "https://oauth2.googleapis.com/token",
639
+ data={
640
+ "code": code,
641
+ "client_id": client_id,
642
+ "client_secret": client_secret,
643
+ "redirect_uri": get_oauth_callback_url("google"),
644
+ "grant_type": "authorization_code",
645
+ },
646
+ )
647
+
648
+ if token_response.status_code != 200:
649
+ raise HTTPException(
650
+ status_code=status.HTTP_400_BAD_REQUEST,
651
+ detail=f"Failed to exchange code for token: {token_response.text}",
652
+ )
653
+
654
+ token_data = token_response.json()
655
+ access_token = token_data["access_token"]
656
+
657
+ # Get user info
658
+ user_response = await client.get(
659
+ "https://www.googleapis.com/oauth2/v2/userinfo", headers={"Authorization": f"Bearer {access_token}"}
660
+ )
661
+
662
+ if user_response.status_code != 200:
663
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=FETCH_USER_INFO_ERROR)
664
+
665
+ user_info = user_response.json()
666
+ return {
667
+ "external_id": user_info["id"],
668
+ "email": user_info.get("email"),
669
+ "name": user_info.get("name", user_info.get("email")),
670
+ "picture": user_info.get("picture"),
671
+ }
672
+
673
+
674
+ async def _handle_github_oauth(code: str, client_id: str, client_secret: str) -> Dict[str, Optional[str]]:
675
+ """Handle GitHub OAuth token exchange and user info retrieval."""
676
+ import httpx
677
+
678
+ async with httpx.AsyncClient() as client:
679
+ # Exchange code for token
680
+ token_response = await client.post(
681
+ "https://github.com/login/oauth/access_token",
682
+ headers={"Accept": "application/json"},
683
+ data={
684
+ "code": code,
685
+ "client_id": client_id,
686
+ "client_secret": client_secret,
687
+ "redirect_uri": os.getenv("OAUTH_CALLBACK_BASE_URL", DEFAULT_OAUTH_BASE_URL)
688
+ + OAUTH_CALLBACK_PATH.replace("{provider}", "github"),
689
+ },
690
+ )
691
+
692
+ if token_response.status_code != 200:
693
+ raise HTTPException(
694
+ status_code=status.HTTP_400_BAD_REQUEST,
695
+ detail=f"Failed to exchange code for token: {token_response.text}",
696
+ )
697
+
698
+ token_data = token_response.json()
699
+ access_token = token_data["access_token"]
700
+
701
+ # Get user info
702
+ user_response = await client.get(
703
+ "https://api.github.com/user", headers={"Authorization": f"token {access_token}"}
704
+ )
705
+
706
+ if user_response.status_code != 200:
707
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=FETCH_USER_INFO_ERROR)
708
+
709
+ user_info = user_response.json()
710
+ external_id = str(user_info["id"])
711
+ email = user_info.get("email")
712
+ name = user_info.get("name", user_info.get("login"))
713
+ picture = user_info.get("avatar_url")
714
+
715
+ # If email is private, fetch from emails endpoint
716
+ if not email:
717
+ emails_response = await client.get(
718
+ "https://api.github.com/user/emails", headers={"Authorization": f"token {access_token}"}
719
+ )
720
+ if emails_response.status_code == 200:
721
+ emails = emails_response.json()
722
+ for e in emails:
723
+ if e.get("primary"):
724
+ email = e["email"]
725
+ break
726
+
727
+ return {
728
+ "external_id": external_id,
729
+ "email": email,
730
+ "name": name,
731
+ "picture": picture,
732
+ }
733
+
734
+
735
+ async def _handle_discord_oauth(code: str, client_id: str, client_secret: str) -> Dict[str, Optional[str]]:
736
+ """Handle Discord OAuth token exchange and user info retrieval."""
737
+ import httpx
738
+
739
+ async with httpx.AsyncClient() as client:
740
+ # Exchange code for token
741
+ token_response = await client.post(
742
+ "https://discord.com/api/oauth2/token",
743
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
744
+ data={
745
+ "code": code,
746
+ "client_id": client_id,
747
+ "client_secret": client_secret,
748
+ "redirect_uri": get_oauth_callback_url("discord"),
749
+ "grant_type": "authorization_code",
750
+ },
751
+ )
752
+
753
+ if token_response.status_code != 200:
754
+ raise HTTPException(
755
+ status_code=status.HTTP_400_BAD_REQUEST,
756
+ detail=f"Failed to exchange code for token: {token_response.text}",
757
+ )
758
+
759
+ token_data = token_response.json()
760
+ access_token = token_data["access_token"]
761
+
762
+ # Get user info
763
+ user_response = await client.get(
764
+ "https://discord.com/api/users/@me", headers={"Authorization": f"Bearer {access_token}"}
765
+ )
766
+
767
+ if user_response.status_code != 200:
768
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=FETCH_USER_INFO_ERROR)
769
+
770
+ user_info = user_response.json()
771
+ external_id = user_info["id"]
772
+ email = user_info.get("email")
773
+ name = user_info.get("username", email)
774
+
775
+ # Construct Discord avatar URL if avatar exists
776
+ avatar_hash = user_info.get("avatar")
777
+ picture = f"https://cdn.discordapp.com/avatars/{external_id}/{avatar_hash}.png" if avatar_hash else None
778
+
779
+ return {
780
+ "external_id": external_id,
781
+ "email": email,
782
+ "name": name,
783
+ "picture": picture,
784
+ }
785
+
786
+
787
+ # =============================================================================
788
+ # ROLE DETERMINATION HELPER FUNCTIONS (extracted for cognitive complexity reduction)
789
+ # =============================================================================
790
+
791
+
792
+ def _is_ciris_admin_email(email: Optional[str]) -> bool:
793
+ """Check if the email is a @ciris.ai domain email (gets automatic ADMIN role)."""
794
+ return email is not None and email.endswith("@ciris.ai")
795
+
796
+
797
+ def _get_oauth_users_dict(auth_service: "APIAuthService") -> Optional[Dict[str, Any]]:
798
+ """Get the _oauth_users dictionary from auth_service, or None if unavailable."""
799
+ return getattr(auth_service, "_oauth_users", None)
800
+
801
+
802
+ def _lookup_existing_user_role(oauth_users: Dict[str, Any], provider: str, external_id: str) -> Optional[UserRole]:
803
+ """Look up an existing OAuth user and return their role if found.
804
+
805
+ Returns None if user not found.
806
+ """
807
+ user_id = f"{provider}:{external_id}"
808
+ existing_user = oauth_users.get(user_id)
809
+
810
+ if not existing_user:
811
+ logger.debug(
812
+ f"[AUTH DEBUG] No existing OAuth user found for {user_id}"
813
+ ) # NOSONAR - provider:id format, not secret
814
+ logger.debug(f"[AUTH DEBUG] Existing OAuth user count: {len(oauth_users)}")
815
+ return None
816
+
817
+ logger.debug(
818
+ f"[AUTH DEBUG] Found existing OAuth user: {user_id}, role={existing_user.role}"
819
+ ) # NOSONAR - role is not sensitive
820
+ role = existing_user.role
821
+ if isinstance(role, UserRole):
822
+ return role
823
+ return UserRole(role) if role else UserRole.OBSERVER
824
+
825
+
826
+ def _is_first_oauth_user(oauth_users: Optional[Dict[str, Any]]) -> bool:
827
+ """Check if this would be the first OAuth user (empty oauth_users dict)."""
828
+ return oauth_users is not None and len(oauth_users) == 0
829
+
830
+
831
+ def _check_stored_user_role(auth_service: "APIAuthService", provider: str, external_id: str) -> Optional[UserRole]:
832
+ """Check for existing user in _users dict and return their role if found."""
833
+ user_id = f"{provider}:{external_id}"
834
+ stored_users = getattr(auth_service, "_users", {})
835
+ stored_user = stored_users.get(user_id)
836
+
837
+ if not stored_user:
838
+ return None
839
+
840
+ # User exists in database - preserve their role!
841
+ logger.debug( # NOSONAR - user_id is provider:id, roles are not sensitive
842
+ f"[AUTH DEBUG] Found existing user in _users dict: {user_id}, "
843
+ f"api_role={stored_user.api_role}, wa_role={stored_user.wa_role}"
844
+ )
845
+
846
+ # Convert APIRole to UserRole
847
+ api_role_to_user_role = {
848
+ "OBSERVER": UserRole.OBSERVER,
849
+ "ADMIN": UserRole.ADMIN,
850
+ "AUTHORITY": UserRole.ADMIN, # AUTHORITY maps to ADMIN
851
+ "SYSTEM_ADMIN": UserRole.SYSTEM_ADMIN,
852
+ "SERVICE_ACCOUNT": UserRole.SYSTEM_ADMIN, # Service accounts get full access
853
+ }
854
+ role_str = stored_user.api_role.value if hasattr(stored_user.api_role, "value") else str(stored_user.api_role)
855
+ existing_user_role = api_role_to_user_role.get(role_str.upper(), UserRole.OBSERVER)
856
+ logger.debug(f"[AUTH DEBUG] Mapped API role {role_str} to UserRole {existing_user_role}")
857
+ return existing_user_role
858
+
859
+
860
+ def _check_first_oauth_user_status(
861
+ auth_service: "APIAuthService", oauth_users: Optional[Dict[str, Any]], provider: str, external_id: Optional[str]
862
+ ) -> bool:
863
+ """Check if this is the first OAuth user (setup wizard scenario)."""
864
+ if not _is_first_oauth_user(oauth_users):
865
+ return False
866
+
867
+ # Only grant SYSTEM_ADMIN if BOTH oauth_users AND _users are empty for this OAuth identity
868
+ stored_users = getattr(auth_service, "_users", {})
869
+ user_id_check: Optional[str] = f"{provider}:{external_id}" if external_id else None
870
+ user_in_stored = user_id_check and user_id_check in stored_users
871
+
872
+ return not user_in_stored
873
+
874
+
875
+ def _determine_user_role(
876
+ email: Optional[str],
877
+ auth_service: Optional["APIAuthService"] = None,
878
+ external_id: Optional[str] = None,
879
+ provider: str = "google",
880
+ ) -> UserRole:
881
+ """Determine user role based on email domain, existing user status, and first-user status.
882
+
883
+ For Android/native OAuth flow during setup, the first OAuth user gets
884
+ SYSTEM_ADMIN role so they can see the default API channel history
885
+ where agent wakeup messages are sent.
886
+
887
+ IMPORTANT: If the user already exists with a higher role (e.g., from initial
888
+ login before setup), preserve that role instead of demoting to OBSERVER.
889
+ """
890
+ masked_email = (email[:3] + "***@" + email.split("@")[-1]) if email and "@" in email else "None"
891
+ logger.debug(
892
+ f"[AUTH DEBUG] _determine_user_role called: email={masked_email}, external_id={external_id}, provider={provider}"
893
+ ) # NOSONAR - email masked, external_id is provider ID
894
+
895
+ # @ciris.ai users always get ADMIN
896
+ if _is_ciris_admin_email(email):
897
+ logger.debug("[AUTH DEBUG] Granting ADMIN role to @ciris.ai user")
898
+ return UserRole.ADMIN
899
+
900
+ # No auth service - return default role
901
+ if auth_service is None:
902
+ logger.info("[AUTH DEBUG] No auth_service provided - returning OBSERVER role")
903
+ return UserRole.OBSERVER
904
+
905
+ try:
906
+ oauth_users = _get_oauth_users_dict(auth_service)
907
+ logger.info(f"[AUTH DEBUG] _oauth_users count: {len(oauth_users) if oauth_users else 'None'}")
908
+
909
+ # Check if this user already exists with a role - preserve their existing role
910
+ if external_id and oauth_users:
911
+ existing_role = _lookup_existing_user_role(oauth_users, provider, external_id)
912
+ if existing_role is not None:
913
+ return existing_role
914
+
915
+ # Check _users dict (for users loaded from database via OAuth link during setup)
916
+ if external_id:
917
+ stored_role = _check_stored_user_role(auth_service, provider, external_id)
918
+ if stored_role is not None:
919
+ return stored_role
920
+
921
+ # Check if this is the first OAuth user (setup wizard scenario)
922
+ if _check_first_oauth_user_status(auth_service, oauth_users, provider, external_id):
923
+ logger.info("[AUTH DEBUG] First OAuth user detected - granting SYSTEM_ADMIN role for setup wizard user")
924
+ return UserRole.SYSTEM_ADMIN
925
+
926
+ except (TypeError, AttributeError) as e:
927
+ # Mock objects or missing attributes - fall through to OBSERVER
928
+ logger.warning(f"[AUTH DEBUG] Exception accessing auth_service: {e}")
929
+
930
+ logger.info("[AUTH DEBUG] No special conditions met - returning OBSERVER role")
931
+ return UserRole.OBSERVER
932
+
933
+
934
+ def _store_oauth_profile(auth_service: APIAuthService, user_id: str, name: str, picture: Optional[str]) -> None:
935
+ """Store OAuth profile data if valid."""
936
+ if not picture:
937
+ return
938
+
939
+ if validate_oauth_picture_url(picture):
940
+ user = auth_service.get_user(user_id)
941
+ if user:
942
+ user.oauth_name = name
943
+ user.oauth_picture = picture
944
+ auth_service._users[user_id] = user
945
+ else:
946
+ logger.warning(f"Invalid OAuth picture URL rejected for user {user_id}: {picture}")
947
+
948
+
949
+ def _update_billing_provider_token(google_id_token: str) -> None:
950
+ """Update the billing provider with a fresh Google ID token.
951
+
952
+ This is called after native Google token exchange to ensure billing
953
+ is available immediately. The token is stored in the environment
954
+ so the billing provider can use it for credit checks.
955
+ """
956
+ import os
957
+
958
+ # Update environment variable so billing provider can use it
959
+ os.environ["CIRIS_BILLING_GOOGLE_ID_TOKEN"] = google_id_token
960
+ logger.info("[NativeAuth] Updated CIRIS_BILLING_GOOGLE_ID_TOKEN in environment for billing provider")
961
+
962
+ # Try to reinitialize the billing provider if resource_monitor is available
963
+ # This is done via a background task to not block the login response
964
+ try:
965
+ from ciris_engine.logic.services.infrastructure.resource_monitor import CIRISBillingProvider
966
+
967
+ # Check if we have access to the app state (will be set by FastAPI)
968
+ # The billing provider will be initialized on the next credit check if not done here
969
+ logger.info("[NativeAuth] Billing provider token updated - will be used on next credit check")
970
+ except Exception as e:
971
+ logger.warning(f"[NativeAuth] Could not update billing provider directly: {e}")
972
+
973
+
974
+ def _generate_api_key_and_store(auth_service: APIAuthService, oauth_user: OAuthUser, provider: str) -> str:
975
+ """Generate API key and store it for the OAuth user."""
976
+ # SYSTEM_ADMIN, ADMIN, and AUTHORITY all get admin prefix (elevated roles)
977
+ # OBSERVER gets observer prefix
978
+ elevated_roles = (UserRole.ADMIN, UserRole.SYSTEM_ADMIN, UserRole.AUTHORITY)
979
+ is_elevated = oauth_user.role in elevated_roles
980
+ role_prefix = "ciris_admin" if is_elevated else "ciris_observer"
981
+ logger.info(
982
+ f"[AUTH DEBUG] Generating API key for user {oauth_user.user_id} with role {oauth_user.role}, prefix: {role_prefix}"
983
+ )
984
+ api_key = f"{role_prefix}_{secrets.token_urlsafe(32)}"
985
+ expires_at = datetime.now(timezone.utc) + timedelta(days=30)
986
+
987
+ auth_service.store_api_key(
988
+ key=api_key,
989
+ user_id=oauth_user.user_id,
990
+ role=oauth_user.role,
991
+ expires_at=expires_at,
992
+ description=f"OAuth login via {provider}",
993
+ )
994
+
995
+ return api_key
996
+
997
+
998
+ def _build_redirect_response(
999
+ api_key: str,
1000
+ oauth_user: OAuthUser,
1001
+ provider: str,
1002
+ redirect_uri: Optional[str] = None,
1003
+ email: Optional[str] = None,
1004
+ marketing_opt_in: Optional[bool] = None,
1005
+ ) -> RedirectResponse:
1006
+ """
1007
+ Build the redirect response for OAuth callback.
1008
+
1009
+ Supports flexible parameter configuration via OAUTH_REDIRECT_PARAMS environment variable.
1010
+
1011
+ Args:
1012
+ api_key: Generated API key for the user
1013
+ oauth_user: OAuth user object with role and user_id
1014
+ provider: OAuth provider name (google, github, discord)
1015
+ redirect_uri: Optional redirect URI from state parameter
1016
+ email: User email from OAuth provider
1017
+ marketing_opt_in: Marketing opt-in preference from redirect_uri
1018
+
1019
+ Environment Variables:
1020
+ OAUTH_FRONTEND_URL: Frontend base URL (e.g., https://scout.ciris.ai)
1021
+ OAUTH_FRONTEND_PATH: Frontend callback path (default: /oauth-complete.html)
1022
+ OAUTH_REDIRECT_PARAMS: Comma-separated list of parameters to include in redirect
1023
+ """
1024
+ import urllib.parse
1025
+
1026
+ VALID_PROVIDERS = {"google", "github", "discord"}
1027
+ if provider not in VALID_PROVIDERS:
1028
+ # Redirect to a safe default if provider is invalid
1029
+ return RedirectResponse(url="/", status_code=302)
1030
+
1031
+ # Build all available parameters
1032
+ all_params = {
1033
+ "access_token": api_key,
1034
+ "token_type": "Bearer",
1035
+ "expires_in": "2592000", # 30 days
1036
+ "role": oauth_user.role.value,
1037
+ "user_id": oauth_user.user_id,
1038
+ "email": email or "",
1039
+ "marketing_opt_in": str(marketing_opt_in).lower() if marketing_opt_in is not None else "",
1040
+ "agent": AGENT_ID,
1041
+ "provider": provider,
1042
+ }
1043
+
1044
+ # Filter to only include configured parameters
1045
+ redirect_params = {k: v for k, v in all_params.items() if k in OAUTH_REDIRECT_PARAMS and v}
1046
+
1047
+ query_string = urllib.parse.urlencode(redirect_params)
1048
+
1049
+ # Determine redirect URL with priority:
1050
+ # 1. Explicit redirect_uri from state parameter (highest priority)
1051
+ # 2. OAUTH_FRONTEND_URL + OAUTH_FRONTEND_PATH
1052
+ # 3. Relative path fallback (backward compatibility)
1053
+
1054
+ if redirect_uri:
1055
+ # Parse existing query parameters from redirect_uri
1056
+ parsed = urllib.parse.urlparse(redirect_uri)
1057
+ base_redirect_uri = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
1058
+
1059
+ # Merge existing params with new params (new params take precedence for security)
1060
+ existing_params = dict(urllib.parse.parse_qsl(parsed.query))
1061
+ merged_params = {**existing_params, **redirect_params} # Server params override if conflict
1062
+
1063
+ query_string = urllib.parse.urlencode(merged_params)
1064
+ redirect_url = f"{base_redirect_uri}?{query_string}"
1065
+ logger.info(
1066
+ f"Redirecting OAuth user to provided redirect_uri with {len(existing_params)} existing params: {base_redirect_uri}"
1067
+ )
1068
+ elif OAUTH_FRONTEND_URL:
1069
+ # Use configured frontend URL
1070
+ redirect_url = f"{OAUTH_FRONTEND_URL}{OAUTH_FRONTEND_PATH}?{query_string}"
1071
+ logger.info(f"Redirecting OAuth user to configured frontend: {OAUTH_FRONTEND_URL}{OAUTH_FRONTEND_PATH}")
1072
+ else:
1073
+ # Backward compatibility: relative path
1074
+ gui_callback_url = f"/oauth/{AGENT_ID}/{provider}/callback"
1075
+ redirect_url = f"{gui_callback_url}?{query_string}"
1076
+ # Do NOT log the full redirect_url with sensitive credentials (access_token, api_key)
1077
+ logger.warning(
1078
+ f"No redirect_uri or OAUTH_FRONTEND_URL configured, using relative path: {gui_callback_url} "
1079
+ "(query params redacted for security)"
1080
+ )
1081
+
1082
+ return RedirectResponse(url=redirect_url, status_code=302)
1083
+
1084
+
1085
+ async def _trigger_billing_credit_check_if_enabled(
1086
+ request: Request,
1087
+ oauth_user: OAuthUser,
1088
+ user_email: Optional[str] = None,
1089
+ marketing_opt_in: Optional[bool] = None,
1090
+ ) -> None:
1091
+ """
1092
+ Trigger billing credit check if billing is enabled.
1093
+
1094
+ This ensures the billing user is created on first OAuth login so the frontend
1095
+ can display available credits immediately. Only runs if resource_monitor with
1096
+ credit_provider is configured.
1097
+
1098
+ Args:
1099
+ request: FastAPI request object
1100
+ oauth_user: OAuth user object with provider, external_id, user_id, role
1101
+ user_email: User email from OAuth provider (REQUIRED for billing backend)
1102
+ marketing_opt_in: Marketing opt-in preference (REQUIRED for billing backend)
1103
+ """
1104
+ # Check if resource_monitor exists (billing may not be enabled)
1105
+ if not hasattr(request.app.state, "resource_monitor"):
1106
+ logger.debug("No resource_monitor configured - skipping billing credit check")
1107
+ return
1108
+
1109
+ resource_monitor = request.app.state.resource_monitor
1110
+
1111
+ # Check if credit provider is configured
1112
+ if not hasattr(resource_monitor, "credit_provider") or resource_monitor.credit_provider is None:
1113
+ logger.debug("No credit_provider configured - skipping billing credit check")
1114
+ return
1115
+
1116
+ # Perform credit check to ensure billing user is created
1117
+ try:
1118
+ from ciris_engine.schemas.services.credit_gate import CreditAccount, CreditContext
1119
+
1120
+ # Extract provider and external_id from oauth_user.user_id (format: "provider:external_id")
1121
+ oauth_provider = oauth_user.provider
1122
+ external_id = oauth_user.external_id
1123
+
1124
+ account = CreditAccount(
1125
+ provider=f"oauth:{oauth_provider}",
1126
+ account_id=external_id,
1127
+ authority_id=oauth_user.user_id,
1128
+ tenant_id=None,
1129
+ customer_email=user_email, # Pass email to billing backend
1130
+ marketing_opt_in=marketing_opt_in, # Pass marketing preference to billing backend
1131
+ )
1132
+
1133
+ context = CreditContext(
1134
+ agent_id=AGENT_ID,
1135
+ channel_id="oauth:callback",
1136
+ request_id=None,
1137
+ user_role=oauth_user.role.value.lower(), # Pass user role to billing backend
1138
+ )
1139
+
1140
+ result = await resource_monitor.check_credit(account, context)
1141
+
1142
+ logger.info(
1143
+ f"Billing credit check for {oauth_user.user_id}: has_credit={result.has_credit}, "
1144
+ f"email={user_email}, marketing_opt_in={marketing_opt_in}, role={oauth_user.role.value}, "
1145
+ f"provider={resource_monitor.credit_provider.__class__.__name__}"
1146
+ )
1147
+
1148
+ except Exception as e:
1149
+ # Don't fail OAuth login if billing check fails
1150
+ logger.warning(f"Billing credit check failed for {oauth_user.user_id}: {e}")
1151
+
1152
+
1153
+ @router.get("/auth/oauth/{provider}/callback")
1154
+ async def oauth_callback(
1155
+ provider: str,
1156
+ code: str,
1157
+ state: str,
1158
+ request: Request,
1159
+ auth_service: APIAuthService = Depends(get_auth_service),
1160
+ marketing_opt_in: bool = False,
1161
+ ) -> RedirectResponse:
1162
+ """
1163
+ Handle OAuth callback.
1164
+
1165
+ Exchanges authorization code for tokens and creates/updates user.
1166
+ Extracts marketing_opt_in from redirect_uri if present.
1167
+ """
1168
+ try:
1169
+ # Decode state parameter to extract redirect_uri
1170
+ import base64
1171
+ import json
1172
+
1173
+ redirect_uri = None
1174
+ marketing_opt_in_from_uri = None
1175
+
1176
+ try:
1177
+ state_json = base64.urlsafe_b64decode(state.encode()).decode()
1178
+ state_data = json.loads(state_json)
1179
+ redirect_uri = state_data.get("redirect_uri")
1180
+
1181
+ # Defense-in-depth: Re-validate redirect_uri even from state
1182
+ # (state could theoretically be tampered with)
1183
+ redirect_uri = validate_redirect_uri(redirect_uri)
1184
+
1185
+ # Extract marketing_opt_in from redirect_uri query parameters
1186
+ if redirect_uri:
1187
+ uri_params = extract_query_params(redirect_uri)
1188
+ marketing_opt_in_str = uri_params.get("marketing_opt_in", "").lower()
1189
+ if marketing_opt_in_str in ("true", "1", "yes"):
1190
+ marketing_opt_in_from_uri = True
1191
+ elif marketing_opt_in_str in ("false", "0", "no"):
1192
+ marketing_opt_in_from_uri = False
1193
+
1194
+ logger.debug(f"Decoded state: redirect_uri={redirect_uri}, marketing_opt_in={marketing_opt_in_from_uri}")
1195
+ except Exception as e:
1196
+ # If state decode fails, log but continue (backward compatibility)
1197
+ logger.warning(f"Failed to decode state parameter: {e}. Using default redirect.")
1198
+
1199
+ # Use marketing_opt_in from redirect_uri if available, otherwise use query param
1200
+ final_marketing_opt_in = (
1201
+ marketing_opt_in_from_uri if marketing_opt_in_from_uri is not None else marketing_opt_in
1202
+ )
1203
+
1204
+ # Load OAuth configuration
1205
+ provider_config = _load_oauth_config(provider)
1206
+ client_id = provider_config["client_id"]
1207
+ client_secret = provider_config["client_secret"]
1208
+
1209
+ # Handle provider-specific OAuth flow
1210
+ if provider == "google":
1211
+ user_data = await _handle_google_oauth(code, client_id, client_secret)
1212
+ elif provider == "github":
1213
+ user_data = await _handle_github_oauth(code, client_id, client_secret)
1214
+ elif provider == "discord":
1215
+ user_data = await _handle_discord_oauth(code, client_id, client_secret)
1216
+ else:
1217
+ raise HTTPException(
1218
+ status_code=status.HTTP_400_BAD_REQUEST, detail=f"Unsupported OAuth provider: {provider}"
1219
+ )
1220
+
1221
+ # Validate required fields first (need external_id for role determination)
1222
+ external_id = user_data["external_id"]
1223
+ if not external_id:
1224
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="OAuth provider did not return user ID")
1225
+
1226
+ # Determine user role (preserves existing role if user already exists)
1227
+ user_email = user_data["email"]
1228
+ user_role = _determine_user_role(user_email, auth_service, external_id=external_id, provider=provider)
1229
+
1230
+ oauth_user = auth_service.create_oauth_user(
1231
+ provider=provider,
1232
+ external_id=external_id,
1233
+ email=user_email,
1234
+ name=user_data["name"],
1235
+ role=user_role,
1236
+ marketing_opt_in=final_marketing_opt_in,
1237
+ )
1238
+
1239
+ # Store OAuth profile data
1240
+ name = user_data["name"] or "Unknown"
1241
+ _store_oauth_profile(auth_service, oauth_user.user_id, name, user_data["picture"])
1242
+
1243
+ # Generate API key and store it
1244
+ api_key = _generate_api_key_and_store(auth_service, oauth_user, provider)
1245
+
1246
+ logger.info(f"OAuth user {oauth_user.user_id} logged in successfully via {provider}")
1247
+
1248
+ # Trigger billing credit check if billing is enabled
1249
+ # This ensures billing user is created and credits are initialized
1250
+ # so the frontend can display available credits immediately
1251
+ await _trigger_billing_credit_check_if_enabled(
1252
+ request, oauth_user, user_email=user_email, marketing_opt_in=final_marketing_opt_in
1253
+ )
1254
+
1255
+ # Build and return redirect response with email and marketing preference
1256
+ return _build_redirect_response(
1257
+ api_key=api_key,
1258
+ oauth_user=oauth_user,
1259
+ provider=provider,
1260
+ redirect_uri=redirect_uri,
1261
+ email=user_email,
1262
+ marketing_opt_in=final_marketing_opt_in,
1263
+ )
1264
+
1265
+ except HTTPException:
1266
+ raise
1267
+ except Exception as e:
1268
+ logger.error(f"OAuth callback error: {e}")
1269
+ raise HTTPException(
1270
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"OAuth callback failed: {str(e)}"
1271
+ )
1272
+
1273
+
1274
+ # ========== Native App Token Exchange Endpoints ==========
1275
+
1276
+
1277
+ class NativeTokenRequest(BaseModel):
1278
+ """Request model for native app token exchange."""
1279
+
1280
+ id_token: str = Field(..., description="Google ID token from native Sign-In")
1281
+ provider: str = Field(default="google", description="OAuth provider (currently only 'google' supported)")
1282
+
1283
+
1284
+ class NativeTokenResponse(BaseModel):
1285
+ """Response model for native app token exchange."""
1286
+
1287
+ access_token: str
1288
+ token_type: str = "bearer"
1289
+ expires_in: int
1290
+ user_id: str
1291
+ role: str
1292
+ email: Optional[str] = None
1293
+ name: Optional[str] = None
1294
+
1295
+
1296
+ # =============================================================================
1297
+ # TOKEN VERIFICATION HELPER FUNCTIONS (extracted for cognitive complexity reduction)
1298
+ # =============================================================================
1299
+
1300
+ # Valid Google issuers - constant
1301
+ VALID_GOOGLE_ISSUERS = {"accounts.google.com", "https://accounts.google.com"}
1302
+
1303
+
1304
+ def _get_allowed_audiences_from_config() -> Optional[Set[str]]:
1305
+ """Load allowed audiences from OAuth config.
1306
+
1307
+ Returns None if OAuth is not configured (on-device mode).
1308
+ On-device mode skips audience validation since the Android app
1309
+ has its own client ID and we can't know it ahead of time.
1310
+ """
1311
+ try:
1312
+ provider_config = _load_oauth_config("google")
1313
+ expected_client_id = provider_config.get("client_id")
1314
+ android_client_id = provider_config.get("android_client_id")
1315
+ allowed_audiences: Set[str] = set()
1316
+ if expected_client_id:
1317
+ allowed_audiences.add(expected_client_id)
1318
+ if android_client_id:
1319
+ allowed_audiences.add(android_client_id)
1320
+ logger.info(
1321
+ f"[NativeAuth] Configured allowed audiences: {allowed_audiences}"
1322
+ ) # NOSONAR - client IDs are public config
1323
+ return allowed_audiences if allowed_audiences else None
1324
+ except HTTPException:
1325
+ # On-device mode: OAuth not configured, skip audience validation
1326
+ logger.info("[NativeAuth] No OAuth config found - running in on-device mode, skipping audience validation")
1327
+ return None
1328
+
1329
+
1330
+ def _validate_token_audience(token_aud: Optional[str], allowed_audiences: Optional[Set[str]]) -> None:
1331
+ """Validate token audience matches our configured client ID.
1332
+
1333
+ If allowed_audiences is None (on-device mode), validation is skipped.
1334
+ Raises HTTPException if validation fails.
1335
+ """
1336
+ if allowed_audiences is None:
1337
+ # On-device mode: skip audience validation, just log the audience
1338
+ logger.info(f"[NativeAuth] On-device mode: skipping audience validation (aud: {token_aud})")
1339
+ return
1340
+
1341
+ if not token_aud or token_aud not in allowed_audiences:
1342
+ logger.error( # NOSONAR - security audit logging, client IDs are public config
1343
+ f"[NativeAuth] SECURITY: Token audience mismatch! "
1344
+ f"Got: {token_aud}, Expected one of: {allowed_audiences}"
1345
+ )
1346
+ raise HTTPException(
1347
+ status_code=status.HTTP_401_UNAUTHORIZED,
1348
+ detail="Token was not issued for this application (audience mismatch).",
1349
+ )
1350
+
1351
+
1352
+ def _validate_token_issuer(token_iss: Optional[str]) -> None:
1353
+ """Validate token issuer is Google.
1354
+
1355
+ Raises HTTPException if validation fails.
1356
+ """
1357
+ if not token_iss or token_iss not in VALID_GOOGLE_ISSUERS:
1358
+ logger.error(f"[NativeAuth] SECURITY: Invalid issuer! Got: {token_iss}, Expected: {VALID_GOOGLE_ISSUERS}")
1359
+ raise HTTPException(
1360
+ status_code=status.HTTP_401_UNAUTHORIZED,
1361
+ detail="Token was not issued by Google (issuer mismatch).",
1362
+ )
1363
+
1364
+
1365
+ def _validate_token_expiry(token_exp: Optional[str]) -> None:
1366
+ """Validate token is not expired.
1367
+
1368
+ Raises HTTPException if validation fails.
1369
+ """
1370
+ import time
1371
+
1372
+ if not token_exp:
1373
+ return
1374
+
1375
+ try:
1376
+ exp_timestamp = int(token_exp)
1377
+ current_time = int(time.time())
1378
+ if exp_timestamp < current_time:
1379
+ logger.error(f"[NativeAuth] SECURITY: Token expired! exp: {exp_timestamp}, now: {current_time}")
1380
+ raise HTTPException(
1381
+ status_code=status.HTTP_401_UNAUTHORIZED,
1382
+ detail="Google ID token has expired. Please sign in again.",
1383
+ )
1384
+ except (ValueError, TypeError):
1385
+ logger.error(f"[NativeAuth] Invalid exp claim format: {token_exp}")
1386
+ raise HTTPException(
1387
+ status_code=status.HTTP_401_UNAUTHORIZED,
1388
+ detail="Token has invalid expiry format.",
1389
+ )
1390
+
1391
+
1392
+ def _validate_token_sub_claim(sub: Optional[str]) -> None:
1393
+ """Validate that the sub (user ID) claim exists.
1394
+
1395
+ Raises HTTPException if validation fails.
1396
+ """
1397
+ if not sub:
1398
+ logger.error("[NativeAuth] Token missing required 'sub' claim")
1399
+ raise HTTPException(
1400
+ status_code=status.HTTP_401_UNAUTHORIZED,
1401
+ detail="Google ID token missing user ID (sub claim).",
1402
+ )
1403
+
1404
+
1405
+ def _log_email_verification_warning(token_info: Dict[str, Any]) -> None:
1406
+ """Log a warning if email is not verified."""
1407
+ email_verified = token_info.get("email_verified")
1408
+ if email_verified is not None and str(email_verified).lower() not in ("true", "1"):
1409
+ logger.warning(f"[NativeAuth] Email not verified for user {token_info.get('sub')}")
1410
+
1411
+
1412
+ async def _call_google_tokeninfo_api(id_token: str) -> Dict[str, Any]:
1413
+ """Call Google's tokeninfo API and return the response JSON."""
1414
+ import httpx
1415
+
1416
+ logger.info("[NativeAuth] Calling Google tokeninfo API...")
1417
+
1418
+ async with httpx.AsyncClient(timeout=10.0) as client:
1419
+ # Use params dict for proper URL encoding of the token
1420
+ response = await client.get("https://oauth2.googleapis.com/tokeninfo", params={"id_token": id_token})
1421
+ logger.info(f"[NativeAuth] Google tokeninfo response: {response.status_code}")
1422
+
1423
+ if response.status_code != 200:
1424
+ logger.error(f"[NativeAuth] Google API rejected token: {response.status_code} - {response.text}")
1425
+ raise HTTPException(
1426
+ status_code=status.HTTP_401_UNAUTHORIZED,
1427
+ detail="Google could not verify this ID token. It may be expired, malformed, or invalid.",
1428
+ )
1429
+
1430
+ token_info: Dict[str, Any] = response.json()
1431
+ return token_info
1432
+
1433
+
1434
+ def _validate_all_token_claims(token_info: Dict[str, Any], allowed_audiences: Optional[Set[str]]) -> None:
1435
+ """Validate all required token claims (audience, issuer, expiry, sub)."""
1436
+ # SECURITY: Validate all token claims
1437
+ _validate_token_audience(token_info.get("aud"), allowed_audiences)
1438
+ _validate_token_issuer(token_info.get("iss"))
1439
+ _validate_token_expiry(token_info.get("exp"))
1440
+ _log_email_verification_warning(token_info)
1441
+ _validate_token_sub_claim(token_info.get("sub"))
1442
+
1443
+
1444
+ def _extract_user_info_from_token(token_info: Dict[str, Any]) -> Dict[str, Optional[str]]:
1445
+ """Extract user information from validated token."""
1446
+ sub = token_info.get("sub")
1447
+ logger.info(f"[NativeAuth] Token VERIFIED successfully - sub: {sub}, email: {token_info.get('email')}")
1448
+
1449
+ return {
1450
+ "external_id": sub,
1451
+ "email": token_info.get("email"),
1452
+ "name": token_info.get("name"),
1453
+ "picture": token_info.get("picture"),
1454
+ }
1455
+
1456
+
1457
+ async def _verify_google_id_token(id_token: str) -> Dict[str, Optional[str]]:
1458
+ """
1459
+ Verify a Google ID token and extract user info.
1460
+
1461
+ This verifies tokens from native Android/iOS Google Sign-In using
1462
+ Google's tokeninfo API with full security validation:
1463
+ - Validates audience (aud) matches our configured client ID
1464
+ - Validates issuer (iss) is accounts.google.com
1465
+ - Validates token is not expired (exp)
1466
+ - Validates email is verified
1467
+
1468
+ SECURITY: No fallback path exists. Tokens MUST be verified by Google
1469
+ with proper audience/issuer/expiry validation before user creation.
1470
+ """
1471
+ import httpx
1472
+
1473
+ logger.info(f"[NativeAuth] Verifying Google ID token (length: {len(id_token)}, prefix: {id_token[:20]}...)")
1474
+
1475
+ # Load our expected client ID from OAuth config
1476
+ allowed_audiences = _get_allowed_audiences_from_config()
1477
+
1478
+ # Verify with Google's tokeninfo endpoint
1479
+ try:
1480
+ token_info = await _call_google_tokeninfo_api(id_token)
1481
+
1482
+ logger.info(
1483
+ f"[NativeAuth] Token info received - sub: {token_info.get('sub')}, "
1484
+ f"email: {token_info.get('email')}, aud: {token_info.get('aud')}, "
1485
+ f"iss: {token_info.get('iss')}, exp: {token_info.get('exp')}"
1486
+ )
1487
+
1488
+ # Validate all token claims
1489
+ _validate_all_token_claims(token_info, allowed_audiences)
1490
+
1491
+ # Extract and return user info
1492
+ return _extract_user_info_from_token(token_info)
1493
+
1494
+ except HTTPException:
1495
+ raise
1496
+ except httpx.TimeoutException:
1497
+ logger.error("[NativeAuth] Google tokeninfo API timed out")
1498
+ raise HTTPException(
1499
+ status_code=status.HTTP_504_GATEWAY_TIMEOUT,
1500
+ detail="Google verification service timed out. Please try again.",
1501
+ )
1502
+ except httpx.RequestError as e:
1503
+ logger.error(f"[NativeAuth] Network error calling Google API: {type(e).__name__}: {e}")
1504
+ raise HTTPException(
1505
+ status_code=status.HTTP_502_BAD_GATEWAY,
1506
+ detail="Could not reach Google verification service. Please check your connection.",
1507
+ )
1508
+ except Exception as e:
1509
+ logger.error(f"[NativeAuth] Unexpected error during token verification: {type(e).__name__}: {e}")
1510
+ raise HTTPException(
1511
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1512
+ detail="Token verification failed due to an internal error.",
1513
+ )
1514
+
1515
+
1516
+ @router.post("/auth/native/google", response_model=NativeTokenResponse)
1517
+ async def native_google_token_exchange(
1518
+ request: NativeTokenRequest,
1519
+ auth_service: APIAuthService = Depends(get_auth_service),
1520
+ ) -> NativeTokenResponse:
1521
+ """
1522
+ Exchange a native Google ID token for a CIRIS API token.
1523
+
1524
+ This endpoint is used by native Android/iOS apps that perform Google Sign-In
1525
+ directly and need to exchange their Google ID token for a CIRIS API token.
1526
+
1527
+ Unlike the web OAuth flow (which uses authorization codes), native apps get
1528
+ ID tokens directly from Google Sign-In SDK and send them here.
1529
+ """
1530
+ logger.info(f"[NativeAuth] Native Google token exchange request - provider: {request.provider}")
1531
+
1532
+ if request.provider != "google":
1533
+ logger.warning(f"[NativeAuth] Unsupported provider: {request.provider}")
1534
+ raise HTTPException(
1535
+ status_code=status.HTTP_400_BAD_REQUEST,
1536
+ detail="Only 'google' provider is currently supported for native token exchange",
1537
+ )
1538
+
1539
+ try:
1540
+ # Verify the Google ID token and get user info
1541
+ logger.info("[NativeAuth] Starting token verification...")
1542
+ user_data = await _verify_google_id_token(request.id_token)
1543
+ logger.info(f"[NativeAuth] Token verification complete - external_id: {user_data.get('external_id')}")
1544
+
1545
+ external_id = user_data.get("external_id")
1546
+ if not external_id:
1547
+ logger.error("[NativeAuth] No external_id in user_data")
1548
+ raise HTTPException(
1549
+ status_code=status.HTTP_400_BAD_REQUEST, detail="Google ID token did not contain user ID"
1550
+ )
1551
+
1552
+ user_email = user_data.get("email")
1553
+ # Pass external_id to preserve existing user's role (don't demote on re-auth!)
1554
+ user_role = _determine_user_role(user_email, auth_service, external_id=external_id, provider="google")
1555
+ logger.info(f"[NativeAuth] Determined role for {user_email}: {user_role}")
1556
+
1557
+ # Check if this is the first OAuth user (for auto-minting)
1558
+ is_first_oauth_user = user_role == UserRole.SYSTEM_ADMIN
1559
+
1560
+ # Create or get OAuth user
1561
+ logger.info(f"[NativeAuth] Creating/getting OAuth user - external_id: {external_id}, email: {user_email}")
1562
+ oauth_user = auth_service.create_oauth_user(
1563
+ provider="google",
1564
+ external_id=external_id,
1565
+ email=user_email,
1566
+ name=user_data.get("name"),
1567
+ role=user_role,
1568
+ marketing_opt_in=False,
1569
+ )
1570
+ logger.info(f"[NativeAuth] OAuth user created/retrieved - user_id: {oauth_user.user_id}")
1571
+
1572
+ # Store OAuth profile data
1573
+ name = user_data.get("name") or "Unknown"
1574
+ _store_oauth_profile(auth_service, oauth_user.user_id, name, user_data.get("picture"))
1575
+
1576
+ # Auto-mint SYSTEM_ADMIN users as WA with ROOT role so they can handle deferrals
1577
+ # This handles both first-time users and existing users who weren't minted
1578
+ logger.info(
1579
+ f"CIRIS_USER_CREATE: [NativeAuth] Checking auto-mint for {oauth_user.user_id} with role {oauth_user.role}"
1580
+ )
1581
+ if oauth_user.role == UserRole.SYSTEM_ADMIN:
1582
+ # Check if user is already minted by looking up their user record
1583
+ existing_user = auth_service.get_user(oauth_user.user_id)
1584
+ logger.info(f"CIRIS_USER_CREATE: [NativeAuth] existing_user lookup: {existing_user}")
1585
+ if existing_user:
1586
+ logger.info(
1587
+ f"CIRIS_USER_CREATE: [NativeAuth] wa_id={existing_user.wa_id}, wa_role={existing_user.wa_role}"
1588
+ )
1589
+
1590
+ needs_minting = not existing_user or not existing_user.wa_id or existing_user.wa_id == oauth_user.user_id
1591
+
1592
+ if needs_minting:
1593
+ logger.info(
1594
+ f"CIRIS_USER_CREATE: [NativeAuth] Auto-minting SYSTEM_ADMIN user {oauth_user.user_id} as WA with ROOT role"
1595
+ )
1596
+ try:
1597
+ from ciris_engine.schemas.services.authority_core import WARole
1598
+
1599
+ await auth_service.mint_wise_authority(
1600
+ user_id=oauth_user.user_id,
1601
+ wa_role=WARole.ROOT,
1602
+ minted_by="system_auto_mint",
1603
+ )
1604
+ logger.info(
1605
+ f"CIRIS_USER_CREATE: [NativeAuth] ✅ Successfully auto-minted {oauth_user.user_id} as ROOT WA"
1606
+ )
1607
+ except Exception as mint_error:
1608
+ # Don't fail login if minting fails - user can mint manually later
1609
+ logger.warning(
1610
+ f"CIRIS_USER_CREATE: [NativeAuth] Auto-mint failed (user can mint manually): {mint_error}"
1611
+ )
1612
+ else:
1613
+ logger.info(
1614
+ f"CIRIS_USER_CREATE: [NativeAuth] User {oauth_user.user_id} already minted as WA - skipping auto-mint"
1615
+ )
1616
+ else:
1617
+ logger.info(f"CIRIS_USER_CREATE: [NativeAuth] Not SYSTEM_ADMIN, skipping auto-mint")
1618
+
1619
+ # Generate API key
1620
+ logger.info(f"[NativeAuth] Generating API key for user {oauth_user.user_id}")
1621
+ api_key = _generate_api_key_and_store(auth_service, oauth_user, "google")
1622
+
1623
+ # Update billing provider with the Google ID token for credit checks
1624
+ # This ensures billing is available immediately after login
1625
+ _update_billing_provider_token(request.id_token)
1626
+
1627
+ logger.info(f"[NativeAuth] SUCCESS - Native Google user {oauth_user.user_id} logged in, token generated")
1628
+
1629
+ return NativeTokenResponse(
1630
+ access_token=api_key,
1631
+ token_type="bearer",
1632
+ expires_in=2592000, # 30 days in seconds
1633
+ user_id=oauth_user.user_id,
1634
+ role=oauth_user.role.value,
1635
+ email=user_email,
1636
+ name=user_data.get("name"),
1637
+ )
1638
+
1639
+ except HTTPException as e:
1640
+ logger.error(f"[NativeAuth] HTTP error: {e.status_code} - {e.detail}")
1641
+ raise
1642
+ except Exception as e:
1643
+ logger.error(f"[NativeAuth] Unexpected error: {type(e).__name__}: {e}", exc_info=True)
1644
+ raise HTTPException(
1645
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Native token exchange failed: {str(e)}"
1646
+ )
1647
+
1648
+
1649
+ # ========== API Key Management Endpoints ==========
1650
+
1651
+
1652
+ @router.post("/auth/api-keys", response_model=APIKeyResponse)
1653
+ async def create_api_key(
1654
+ request: APIKeyCreateRequest,
1655
+ auth: AuthContext = Depends(get_auth_context),
1656
+ auth_service: APIAuthService = Depends(get_auth_service),
1657
+ ) -> APIKeyResponse:
1658
+ """
1659
+ Create a new API key for the authenticated user.
1660
+
1661
+ Users can create API keys for their OAuth identity with configurable expiry (30min - 7 days).
1662
+ The key is only shown once and cannot be retrieved later.
1663
+ """
1664
+ # Calculate expiration based on minutes
1665
+ expires_at = datetime.now(timezone.utc) + timedelta(minutes=request.expires_in_minutes)
1666
+
1667
+ # Generate API key with user's current role
1668
+ api_key = f"ciris_{auth.role.value.lower()}_{secrets.token_urlsafe(32)}"
1669
+
1670
+ # Store API key
1671
+ auth_service.store_api_key(
1672
+ key=api_key,
1673
+ user_id=auth.user_id,
1674
+ role=auth.role,
1675
+ expires_at=expires_at,
1676
+ description=request.description,
1677
+ created_by=auth.user_id,
1678
+ )
1679
+
1680
+ logger.info(f"User {auth.user_id} created API key with {request.expires_in_minutes}min expiry")
1681
+
1682
+ return APIKeyResponse(
1683
+ api_key=api_key,
1684
+ role=auth.role,
1685
+ expires_at=expires_at,
1686
+ description=request.description,
1687
+ created_at=datetime.now(timezone.utc),
1688
+ created_by=auth.user_id,
1689
+ )
1690
+
1691
+
1692
+ @router.get("/auth/api-keys", response_model=APIKeyListResponse)
1693
+ async def list_api_keys(
1694
+ auth: AuthContext = Depends(get_auth_context), auth_service: APIAuthService = Depends(get_auth_service)
1695
+ ) -> APIKeyListResponse:
1696
+ """
1697
+ List all API keys for the authenticated user.
1698
+
1699
+ Returns information about all API keys created by the user (excluding the actual key values).
1700
+ """
1701
+ # Get all keys for this user
1702
+ stored_keys = auth_service.list_user_api_keys(auth.user_id)
1703
+
1704
+ # Convert to response format
1705
+ api_keys = [
1706
+ APIKeyInfo(
1707
+ key_id=key.key_id,
1708
+ role=key.role,
1709
+ expires_at=key.expires_at,
1710
+ description=key.description,
1711
+ created_at=key.created_at,
1712
+ created_by=key.created_by,
1713
+ last_used=key.last_used,
1714
+ is_active=key.is_active,
1715
+ )
1716
+ for key in stored_keys
1717
+ ]
1718
+
1719
+ return APIKeyListResponse(api_keys=api_keys, total=len(api_keys))
1720
+
1721
+
1722
+ @router.delete("/auth/api-keys/{key_id}", status_code=status.HTTP_204_NO_CONTENT)
1723
+ async def delete_api_key(
1724
+ key_id: str,
1725
+ auth: AuthContext = Depends(get_auth_context),
1726
+ auth_service: APIAuthService = Depends(get_auth_service),
1727
+ ) -> None:
1728
+ """
1729
+ Delete an API key.
1730
+
1731
+ Users can only delete their own API keys.
1732
+ """
1733
+ # Get the key to verify ownership
1734
+ all_keys = auth_service.list_user_api_keys(auth.user_id)
1735
+ key_to_delete = next((k for k in all_keys if k.key_id == key_id), None)
1736
+
1737
+ if not key_to_delete:
1738
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="API key not found")
1739
+
1740
+ # Revoke the key
1741
+ auth_service.revoke_api_key(key_id)
1742
+
1743
+ logger.info(f"User {auth.user_id} deleted API key {key_id}")
1744
+
1745
+ return None