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,3049 @@
1
+ """
2
+ System management endpoints for CIRIS API v3.0 (Simplified).
3
+
4
+ Consolidates health, time, resources, runtime control, services, and shutdown
5
+ into a unified system operations interface.
6
+ """
7
+
8
+ import asyncio
9
+ import html
10
+ import json
11
+ import logging
12
+ import time
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from fastapi import APIRouter, Body, Depends, HTTPException, Request, Response
18
+ from pydantic import BaseModel, Field, ValidationError, field_serializer
19
+ from starlette.responses import JSONResponse
20
+
21
+ from ciris_engine.constants import CIRIS_VERSION
22
+ from ciris_engine.logic.utils.path_resolution import get_package_root
23
+ from ciris_engine.protocols.services.lifecycle.time import TimeServiceProtocol
24
+ from ciris_engine.schemas.adapters.tools import ToolParameterSchema
25
+ from ciris_engine.schemas.api.responses import SuccessResponse
26
+ from ciris_engine.schemas.api.telemetry import ServiceMetrics, TimeSyncStatus
27
+ from ciris_engine.schemas.runtime.adapter_management import (
28
+ AdapterConfig,
29
+ AdapterListResponse,
30
+ AdapterMetrics,
31
+ AdapterOperationResult,
32
+ ModuleConfigParameter,
33
+ ModuleTypeInfo,
34
+ ModuleTypesResponse,
35
+ )
36
+ from ciris_engine.schemas.runtime.adapter_management import RuntimeAdapterStatus as AdapterStatusSchema
37
+ from ciris_engine.schemas.runtime.enums import ServiceType
38
+ from ciris_engine.schemas.runtime.manifest import ConfigurationStep
39
+ from ciris_engine.schemas.services.core.runtime import ProcessorStatus
40
+ from ciris_engine.schemas.services.resources_core import ResourceBudget, ResourceSnapshot
41
+ from ciris_engine.schemas.types import JSONDict
42
+ from ciris_engine.utils.serialization import serialize_timestamp
43
+
44
+ from ..constants import (
45
+ DESC_CURRENT_COGNITIVE_STATE,
46
+ DESC_HUMAN_READABLE_STATUS,
47
+ ERROR_RESOURCE_MONITOR_NOT_AVAILABLE,
48
+ ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE,
49
+ ERROR_SHUTDOWN_SERVICE_NOT_AVAILABLE,
50
+ ERROR_TIME_SERVICE_NOT_AVAILABLE,
51
+ )
52
+ from ..dependencies.auth import AuthContext, require_admin, require_observer
53
+
54
+ router = APIRouter(prefix="/system", tags=["system"])
55
+
56
+ # Capability constants (avoid duplication)
57
+ CAP_COMM_SEND_MESSAGE = "communication:send_message"
58
+ CAP_COMM_FETCH_MESSAGES = "communication:fetch_messages"
59
+ MANIFEST_FILENAME = "manifest.json"
60
+ ERROR_ADAPTER_CONFIG_SERVICE_NOT_AVAILABLE = "Adapter configuration service not available"
61
+
62
+ # Common communication capabilities for adapters
63
+ COMM_CAPABILITIES = [CAP_COMM_SEND_MESSAGE, CAP_COMM_FETCH_MESSAGES]
64
+ logger = logging.getLogger(__name__)
65
+
66
+
67
+ # Request/Response Models
68
+
69
+
70
+ class SystemHealthResponse(BaseModel):
71
+ """Overall system health status."""
72
+
73
+ status: str = Field(..., description="Overall health status (healthy/degraded/critical)")
74
+ version: str = Field(..., description="System version")
75
+ uptime_seconds: float = Field(..., description="System uptime in seconds")
76
+ services: Dict[str, Dict[str, int]] = Field(..., description="Service health summary")
77
+ initialization_complete: bool = Field(..., description="Whether system initialization is complete")
78
+ cognitive_state: Optional[str] = Field(None, description="Current cognitive state if available")
79
+ timestamp: datetime = Field(..., description="Current server time")
80
+
81
+ @field_serializer("timestamp")
82
+ def serialize_ts(self, timestamp: datetime, _info: Any) -> Optional[str]:
83
+ return serialize_timestamp(timestamp, _info)
84
+
85
+
86
+ class SystemTimeResponse(BaseModel):
87
+ """System and agent time information."""
88
+
89
+ system_time: datetime = Field(..., description="Host system time (OS time)")
90
+ agent_time: datetime = Field(..., description="Agent's TimeService time")
91
+ uptime_seconds: float = Field(..., description="Service uptime in seconds")
92
+ time_sync: TimeSyncStatus = Field(..., description="Time synchronization status")
93
+
94
+ @field_serializer("system_time", "agent_time")
95
+ def serialize_times(self, dt: datetime, _info: Any) -> Optional[str]:
96
+ return serialize_timestamp(dt, _info)
97
+
98
+
99
+ class ResourceUsageResponse(BaseModel):
100
+ """System resource usage and limits."""
101
+
102
+ current_usage: ResourceSnapshot = Field(..., description="Current resource usage")
103
+ limits: ResourceBudget = Field(..., description="Configured resource limits")
104
+ health_status: str = Field(..., description="Resource health (healthy/warning/critical)")
105
+ warnings: List[str] = Field(default_factory=list, description="Resource warnings")
106
+ critical: List[str] = Field(default_factory=list, description="Critical resource issues")
107
+
108
+
109
+ class RuntimeAction(BaseModel):
110
+ """Runtime control action request."""
111
+
112
+ reason: Optional[str] = Field(None, description="Reason for the action")
113
+
114
+
115
+ class StateTransitionRequest(BaseModel):
116
+ """Request to transition cognitive state."""
117
+
118
+ target_state: str = Field(..., description="Target cognitive state (WORK, DREAM, PLAY, SOLITUDE)")
119
+ reason: Optional[str] = Field(None, description="Reason for the transition")
120
+
121
+
122
+ class StateTransitionResponse(BaseModel):
123
+ """Response to cognitive state transition request."""
124
+
125
+ success: bool = Field(..., description="Whether transition was initiated")
126
+ message: str = Field(..., description="Human-readable status message")
127
+ previous_state: Optional[str] = Field(None, description="State before transition")
128
+ current_state: str = Field(..., description="Current cognitive state after transition attempt")
129
+
130
+
131
+ class RuntimeControlResponse(BaseModel):
132
+ """Response to runtime control actions."""
133
+
134
+ success: bool = Field(..., description="Whether action succeeded")
135
+ message: str = Field(..., description=DESC_HUMAN_READABLE_STATUS)
136
+ processor_state: str = Field(..., description="Current processor state")
137
+ cognitive_state: Optional[str] = Field(None, description=DESC_CURRENT_COGNITIVE_STATE)
138
+ queue_depth: int = Field(0, description="Number of items in processing queue")
139
+
140
+ # Enhanced pause response fields for UI display
141
+ current_step: Optional[str] = Field(None, description="Current pipeline step when paused")
142
+ current_step_schema: Optional[JSONDict] = Field(None, description="Full schema object for current step")
143
+ pipeline_state: Optional[JSONDict] = Field(None, description="Complete pipeline state when paused")
144
+
145
+
146
+ class ServiceStatus(BaseModel):
147
+ """Individual service status."""
148
+
149
+ name: str = Field(..., description="Service name")
150
+ type: str = Field(..., description="Service type")
151
+ healthy: bool = Field(..., description="Whether service is healthy")
152
+ available: bool = Field(..., description="Whether service is available")
153
+ uptime_seconds: Optional[float] = Field(None, description="Service uptime if tracked")
154
+ metrics: ServiceMetrics = Field(
155
+ default_factory=lambda: ServiceMetrics(
156
+ uptime_seconds=None,
157
+ requests_handled=None,
158
+ error_count=None,
159
+ avg_response_time_ms=None,
160
+ memory_mb=None,
161
+ custom_metrics=None,
162
+ ),
163
+ description="Service-specific metrics",
164
+ )
165
+
166
+
167
+ class ServicesStatusResponse(BaseModel):
168
+ """Status of all system services."""
169
+
170
+ services: List[ServiceStatus] = Field(..., description="List of service statuses")
171
+ total_services: int = Field(..., description="Total number of services")
172
+ healthy_services: int = Field(..., description="Number of healthy services")
173
+ timestamp: datetime = Field(..., description="When status was collected")
174
+
175
+ @field_serializer("timestamp")
176
+ def serialize_ts(self, timestamp: datetime, _info: Any) -> Optional[str]:
177
+ return serialize_timestamp(timestamp, _info)
178
+
179
+
180
+ class ShutdownRequest(BaseModel):
181
+ """Graceful shutdown request."""
182
+
183
+ reason: str = Field(..., description="Reason for shutdown")
184
+ force: bool = Field(False, description="Force immediate shutdown")
185
+ confirm: bool = Field(..., description="Confirmation flag (must be true)")
186
+
187
+
188
+ class ShutdownResponse(BaseModel):
189
+ """Response to shutdown request."""
190
+
191
+ status: str = Field(..., description="Shutdown status")
192
+ message: str = Field(..., description=DESC_HUMAN_READABLE_STATUS)
193
+ shutdown_initiated: bool = Field(..., description="Whether shutdown was initiated")
194
+ timestamp: datetime = Field(..., description="When shutdown was initiated")
195
+
196
+ @field_serializer("timestamp")
197
+ def serialize_ts(self, timestamp: datetime, _info: Any) -> Optional[str]:
198
+ return serialize_timestamp(timestamp, _info)
199
+
200
+
201
+ class AdapterActionRequest(BaseModel):
202
+ """Request for adapter operations."""
203
+
204
+ config: Optional[AdapterConfig] = Field(None, description="Adapter configuration")
205
+ auto_start: bool = Field(True, description="Whether to auto-start the adapter")
206
+ force: bool = Field(False, description="Force the operation")
207
+
208
+
209
+ class ToolInfoResponse(BaseModel):
210
+ """Tool information response with provider details."""
211
+
212
+ name: str = Field(..., description="Tool name")
213
+ description: str = Field(..., description="Tool description")
214
+ provider: str = Field(..., description="Provider service name")
215
+ parameters: Optional[ToolParameterSchema] = Field(None, description="Tool parameter schema")
216
+ category: str = Field("general", description="Tool category")
217
+ cost: float = Field(0.0, description="Cost to execute the tool")
218
+ when_to_use: Optional[str] = Field(None, description="Guidance on when to use the tool")
219
+
220
+
221
+ # Adapter Configuration Response Models
222
+
223
+
224
+ class ConfigStepInfo(BaseModel):
225
+ """Information about a configuration step."""
226
+
227
+ step_id: str = Field(..., description="Unique step identifier")
228
+ step_type: str = Field(..., description="Type of step (discovery, oauth, select, confirm)")
229
+ title: str = Field(..., description="Step title")
230
+ description: str = Field(..., description="Step description")
231
+ optional: bool = Field(False, description="Whether this step is optional")
232
+
233
+
234
+ class ConfigurableAdapterInfo(BaseModel):
235
+ """Information about an adapter that supports interactive configuration."""
236
+
237
+ adapter_type: str = Field(..., description="Type identifier for the adapter")
238
+ name: str = Field(..., description="Human-readable name")
239
+ description: str = Field(..., description="Description of the adapter")
240
+ workflow_type: str = Field(..., description="Type of configuration workflow")
241
+ step_count: int = Field(..., description="Number of steps in the configuration workflow")
242
+ requires_oauth: bool = Field(False, description="Whether this adapter requires OAuth authentication")
243
+ steps: List[ConfigStepInfo] = Field(default_factory=list, description="Configuration steps")
244
+
245
+
246
+ class ConfigurableAdaptersResponse(BaseModel):
247
+ """Response containing list of configurable adapters."""
248
+
249
+ adapters: List[ConfigurableAdapterInfo] = Field(..., description="List of configurable adapters")
250
+ total_count: int = Field(..., description="Total number of configurable adapters")
251
+
252
+
253
+ class ConfigurationSessionResponse(BaseModel):
254
+ """Response for starting a configuration session."""
255
+
256
+ session_id: str = Field(..., description="Unique session identifier")
257
+ adapter_type: str = Field(..., description="Adapter being configured")
258
+ status: str = Field(..., description="Current session status")
259
+ current_step_index: int = Field(..., description="Index of current step")
260
+ current_step: Optional[ConfigurationStep] = Field(None, description="Current step information")
261
+ total_steps: int = Field(..., description="Total number of steps in workflow")
262
+ created_at: datetime = Field(..., description="When session was created")
263
+
264
+ @field_serializer("created_at")
265
+ def serialize_ts(self, created_at: datetime, _info: Any) -> Optional[str]:
266
+ return serialize_timestamp(created_at, _info)
267
+
268
+
269
+ class ConfigurationStatusResponse(BaseModel):
270
+ """Response for configuration session status."""
271
+
272
+ session_id: str = Field(..., description="Session identifier")
273
+ adapter_type: str = Field(..., description="Adapter being configured")
274
+ status: str = Field(..., description="Current session status")
275
+ current_step_index: int = Field(..., description="Index of current step")
276
+ current_step: Optional[ConfigurationStep] = Field(None, description="Current step information")
277
+ total_steps: int = Field(..., description="Total number of steps in workflow")
278
+ collected_config: Dict[str, Any] = Field(..., description="Configuration collected so far")
279
+ created_at: datetime = Field(..., description="When session was created")
280
+ updated_at: datetime = Field(..., description="When session was last updated")
281
+
282
+ @field_serializer("created_at", "updated_at")
283
+ def serialize_times(self, dt: datetime, _info: Any) -> Optional[str]:
284
+ return serialize_timestamp(dt, _info)
285
+
286
+
287
+ class StepExecutionRequest(BaseModel):
288
+ """Request to execute a configuration step."""
289
+
290
+ step_data: Dict[str, Any] = Field(default_factory=dict, description="Data for step execution")
291
+
292
+
293
+ class StepExecutionResponse(BaseModel):
294
+ """Response from executing a configuration step."""
295
+
296
+ step_id: str = Field(..., description="ID of the executed step")
297
+ success: bool = Field(..., description="Whether step execution succeeded")
298
+ data: Dict[str, Any] = Field(default_factory=dict, description="Data returned by the step")
299
+ next_step_index: Optional[int] = Field(None, description="Index of next step to execute")
300
+ error: Optional[str] = Field(None, description="Error message if execution failed")
301
+ awaiting_callback: bool = Field(False, description="Whether step is waiting for external callback")
302
+
303
+
304
+ class ConfigurationCompleteRequest(BaseModel):
305
+ """Request body for completing a configuration session."""
306
+
307
+ persist: bool = Field(default=False, description="If True, persist configuration for automatic loading on startup")
308
+
309
+
310
+ class ConfigurationCompleteResponse(BaseModel):
311
+ """Response from completing a configuration session."""
312
+
313
+ success: bool = Field(..., description="Whether configuration was applied successfully")
314
+ adapter_type: str = Field(..., description="Adapter that was configured")
315
+ message: str = Field(..., description="Human-readable result message")
316
+ applied_config: Dict[str, Any] = Field(default_factory=dict, description="Configuration that was applied")
317
+ persisted: bool = Field(default=False, description="Whether configuration was persisted for startup")
318
+
319
+
320
+ # Endpoints
321
+
322
+
323
+ @router.get("/health", response_model=SuccessResponse[SystemHealthResponse])
324
+ async def get_system_health(request: Request) -> SuccessResponse[SystemHealthResponse]:
325
+ """
326
+ Overall system health.
327
+
328
+ Returns comprehensive system health including service status,
329
+ initialization state, and current cognitive state.
330
+ """
331
+ # Get basic system info
332
+ uptime_seconds = _get_system_uptime(request)
333
+ current_time = _get_current_time(request)
334
+ cognitive_state = _get_cognitive_state_safe(request)
335
+ init_complete = _check_initialization_status(request)
336
+
337
+ # Collect service health data
338
+ services = await _collect_service_health(request)
339
+ processor_healthy = await _check_processor_health(request)
340
+
341
+ # Determine overall system status
342
+ status = _determine_overall_status(init_complete, processor_healthy, services)
343
+
344
+ response = SystemHealthResponse(
345
+ status=status,
346
+ version=CIRIS_VERSION,
347
+ uptime_seconds=uptime_seconds,
348
+ services=services,
349
+ initialization_complete=init_complete,
350
+ cognitive_state=cognitive_state,
351
+ timestamp=current_time,
352
+ )
353
+
354
+ return SuccessResponse(data=response)
355
+
356
+
357
+ @router.get("/time", response_model=SuccessResponse[SystemTimeResponse])
358
+ async def get_system_time(
359
+ request: Request, auth: AuthContext = Depends(require_observer)
360
+ ) -> SuccessResponse[SystemTimeResponse]:
361
+ """
362
+ System time information.
363
+
364
+ Returns both system time (host OS) and agent time (TimeService),
365
+ along with synchronization status.
366
+ """
367
+ # Get time service
368
+ time_service: Optional[TimeServiceProtocol] = getattr(request.app.state, "time_service", None)
369
+ if not time_service:
370
+ raise HTTPException(status_code=503, detail=ERROR_TIME_SERVICE_NOT_AVAILABLE)
371
+
372
+ try:
373
+ # Get system time (actual OS time)
374
+ system_time = datetime.now(timezone.utc)
375
+
376
+ # Get agent time (from TimeService)
377
+ agent_time = time_service.now()
378
+
379
+ # Calculate uptime
380
+ start_time = getattr(time_service, "_start_time", None)
381
+ if not start_time:
382
+ start_time = agent_time
383
+ uptime_seconds = 0.0
384
+ else:
385
+ uptime_seconds = (agent_time - start_time).total_seconds()
386
+
387
+ # Calculate time sync status
388
+ is_mocked = getattr(time_service, "_mock_time", None) is not None
389
+ time_diff_ms = (agent_time - system_time).total_seconds() * 1000
390
+
391
+ time_sync = TimeSyncStatus(
392
+ synchronized=not is_mocked and abs(time_diff_ms) < 1000, # Within 1 second
393
+ drift_ms=time_diff_ms,
394
+ last_sync=getattr(time_service, "_last_sync", agent_time),
395
+ sync_source="mock" if is_mocked else "system",
396
+ )
397
+
398
+ response = SystemTimeResponse(
399
+ system_time=system_time, agent_time=agent_time, uptime_seconds=uptime_seconds, time_sync=time_sync
400
+ )
401
+
402
+ return SuccessResponse(data=response)
403
+ except Exception as e:
404
+ raise HTTPException(status_code=500, detail=f"Failed to get time information: {str(e)}")
405
+
406
+
407
+ @router.get("/resources", response_model=SuccessResponse[ResourceUsageResponse])
408
+ async def get_resource_usage(
409
+ request: Request, auth: AuthContext = Depends(require_observer)
410
+ ) -> SuccessResponse[ResourceUsageResponse]:
411
+ """
412
+ Resource usage and limits.
413
+
414
+ Returns current resource consumption, configured limits,
415
+ and health status.
416
+ """
417
+ resource_monitor = getattr(request.app.state, "resource_monitor", None)
418
+ if not resource_monitor:
419
+ raise HTTPException(status_code=503, detail=ERROR_RESOURCE_MONITOR_NOT_AVAILABLE)
420
+
421
+ try:
422
+ # Get current snapshot and budget
423
+ snapshot = resource_monitor.snapshot
424
+ budget = resource_monitor.budget
425
+
426
+ # Determine health status
427
+ if snapshot.critical:
428
+ health_status = "critical"
429
+ elif snapshot.warnings:
430
+ health_status = "warning"
431
+ else:
432
+ health_status = "healthy"
433
+
434
+ response = ResourceUsageResponse(
435
+ current_usage=snapshot,
436
+ limits=budget,
437
+ health_status=health_status,
438
+ warnings=snapshot.warnings,
439
+ critical=snapshot.critical,
440
+ )
441
+
442
+ return SuccessResponse(data=response)
443
+
444
+ except Exception as e:
445
+ logger.error(f"Error getting resource usage: {e}")
446
+ raise HTTPException(status_code=500, detail=str(e))
447
+
448
+
449
+ def _get_runtime_control_service(request: Request) -> Any:
450
+ """Get runtime control service from request, trying main service first."""
451
+ runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
452
+ if not runtime_control:
453
+ runtime_control = getattr(request.app.state, "runtime_control_service", None)
454
+ if not runtime_control:
455
+ raise HTTPException(status_code=503, detail=ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE)
456
+ return runtime_control
457
+
458
+
459
+ def _validate_runtime_action(action: str) -> None:
460
+ """Validate the runtime control action."""
461
+ valid_actions = ["pause", "resume", "state"]
462
+ if action not in valid_actions:
463
+ raise HTTPException(status_code=400, detail=f"Invalid action. Must be one of: {', '.join(valid_actions)}")
464
+
465
+
466
+ async def _execute_pause_action(runtime_control: Any, body: RuntimeAction) -> bool:
467
+ """Execute pause action and return success status."""
468
+ # Check if the service expects a reason parameter (API runtime control) or not (main runtime control)
469
+ import inspect
470
+
471
+ sig = inspect.signature(runtime_control.pause_processing)
472
+ if len(sig.parameters) > 0: # API runtime control service
473
+ success: bool = await runtime_control.pause_processing(body.reason or "API request")
474
+ else: # Main runtime control service
475
+ control_response = await runtime_control.pause_processing()
476
+ success = control_response.success
477
+ return success
478
+
479
+
480
+ def _extract_pipeline_state_info(
481
+ request: Request,
482
+ ) -> tuple[Optional[str], Optional[JSONDict], Optional[JSONDict]]:
483
+ """
484
+ Extract pipeline state information for UI display.
485
+
486
+ Returns:
487
+ Tuple of (current_step, current_step_schema, pipeline_state)
488
+ """
489
+ current_step: Optional[str] = None
490
+ current_step_schema: Optional[JSONDict] = None
491
+ pipeline_state: Optional[JSONDict] = None
492
+
493
+ try:
494
+ # Try to get current pipeline state from the runtime
495
+ runtime = getattr(request.app.state, "runtime", None)
496
+ if runtime and hasattr(runtime, "agent_processor") and runtime.agent_processor:
497
+ if (
498
+ hasattr(runtime.agent_processor, "_pipeline_controller")
499
+ and runtime.agent_processor._pipeline_controller
500
+ ):
501
+ pipeline_controller = runtime.agent_processor._pipeline_controller
502
+
503
+ # Get current pipeline state
504
+ try:
505
+ pipeline_state_obj = pipeline_controller.get_current_state()
506
+ if pipeline_state_obj and hasattr(pipeline_state_obj, "current_step"):
507
+ current_step = pipeline_state_obj.current_step
508
+ if pipeline_state_obj and hasattr(pipeline_state_obj, "pipeline_state"):
509
+ pipeline_state = pipeline_state_obj.pipeline_state
510
+ except Exception as e:
511
+ logger.debug(f"Could not get current step from pipeline: {e}")
512
+
513
+ # Get the full step schema/metadata
514
+ if current_step:
515
+ try:
516
+ # Get step schema - this would include all step metadata
517
+ current_step_schema = {
518
+ "step_point": current_step,
519
+ "description": f"System paused at step: {current_step}",
520
+ "timestamp": datetime.now().isoformat(),
521
+ "can_single_step": True,
522
+ "next_actions": ["single_step", "resume"],
523
+ }
524
+ except Exception as e:
525
+ logger.debug(f"Could not get step schema: {e}")
526
+ except Exception as e:
527
+ logger.debug(f"Could not get pipeline information: {e}")
528
+
529
+ return current_step, current_step_schema, pipeline_state
530
+
531
+
532
+ def _create_pause_response(
533
+ success: bool,
534
+ current_step: Optional[str],
535
+ current_step_schema: Optional[JSONDict],
536
+ pipeline_state: Optional[JSONDict],
537
+ ) -> RuntimeControlResponse:
538
+ """Create pause action response."""
539
+ # Create clear message based on success state
540
+ if success:
541
+ step_suffix = f" at step: {current_step}" if current_step else ""
542
+ message = f"Processing paused{step_suffix}"
543
+ else:
544
+ message = "Already paused"
545
+
546
+ result = RuntimeControlResponse(
547
+ success=success,
548
+ message=message,
549
+ processor_state="paused" if success else "unknown",
550
+ cognitive_state="UNKNOWN",
551
+ )
552
+
553
+ # Add current step information to response for UI
554
+ if current_step:
555
+ result.current_step = current_step
556
+ result.current_step_schema = current_step_schema
557
+ result.pipeline_state = pipeline_state
558
+
559
+ return result
560
+
561
+
562
+ async def _execute_resume_action(runtime_control: Any) -> RuntimeControlResponse:
563
+ """Execute resume action."""
564
+ # Check if the service returns a control response or just boolean
565
+ resume_result = await runtime_control.resume_processing()
566
+ if hasattr(resume_result, "success"): # Main runtime control service
567
+ success = resume_result.success
568
+ else: # API runtime control service
569
+ success = resume_result
570
+
571
+ return RuntimeControlResponse(
572
+ success=success,
573
+ message="Processing resumed" if success else "Not paused",
574
+ processor_state="active" if success else "unknown",
575
+ cognitive_state="UNKNOWN",
576
+ queue_depth=0,
577
+ )
578
+
579
+
580
+ async def _execute_state_action(runtime_control: Any) -> RuntimeControlResponse:
581
+ """Execute state query action."""
582
+ # Get current state without changing it
583
+ status = await runtime_control.get_runtime_status()
584
+ # Get queue depth from the same source as queue endpoint
585
+ queue_status = await runtime_control.get_processor_queue_status()
586
+ actual_queue_depth = queue_status.queue_size if queue_status else 0
587
+
588
+ return RuntimeControlResponse(
589
+ success=True,
590
+ message="Current runtime state retrieved",
591
+ processor_state="paused" if status.processor_status == ProcessorStatus.PAUSED else "active",
592
+ cognitive_state=status.cognitive_state or "UNKNOWN",
593
+ queue_depth=actual_queue_depth,
594
+ )
595
+
596
+
597
+ def _get_system_uptime(request: Request) -> float:
598
+ """Get system uptime in seconds."""
599
+ time_service: Optional[TimeServiceProtocol] = getattr(request.app.state, "time_service", None)
600
+ start_time = getattr(time_service, "_start_time", None) if time_service else None
601
+ current_time = time_service.now() if time_service else datetime.now(timezone.utc)
602
+ return (current_time - start_time).total_seconds() if start_time else 0.0
603
+
604
+
605
+ def _get_current_time(request: Request) -> datetime:
606
+ """Get current system time."""
607
+ time_service: Optional[TimeServiceProtocol] = getattr(request.app.state, "time_service", None)
608
+ return time_service.now() if time_service else datetime.now(timezone.utc)
609
+
610
+
611
+ def _get_cognitive_state_safe(request: Request) -> Optional[str]:
612
+ """Safely get cognitive state from agent processor."""
613
+ runtime = getattr(request.app.state, "runtime", None)
614
+ if not (runtime and hasattr(runtime, "agent_processor") and runtime.agent_processor is not None):
615
+ return None
616
+
617
+ try:
618
+ state: str = runtime.agent_processor.get_current_state()
619
+ return state
620
+ except Exception as e:
621
+ logger.warning(
622
+ f"Failed to retrieve cognitive state: {type(e).__name__}: {str(e)} - Agent processor may not be initialized"
623
+ )
624
+ return None
625
+
626
+
627
+ def _check_initialization_status(request: Request) -> bool:
628
+ """Check if system initialization is complete."""
629
+ init_service = getattr(request.app.state, "initialization_service", None)
630
+ if init_service and hasattr(init_service, "is_initialized"):
631
+ result: bool = init_service.is_initialized()
632
+ return result
633
+ return True
634
+
635
+
636
+ async def _check_provider_health(provider: Any) -> bool:
637
+ """Check if a single provider is healthy."""
638
+ try:
639
+ if hasattr(provider, "is_healthy"):
640
+ if asyncio.iscoroutinefunction(provider.is_healthy):
641
+ result: bool = await provider.is_healthy()
642
+ return result
643
+ else:
644
+ result_sync: bool = provider.is_healthy()
645
+ return result_sync
646
+ else:
647
+ return True # Assume healthy if no method
648
+ except Exception:
649
+ return False
650
+
651
+
652
+ async def _collect_service_health(request: Request) -> Dict[str, Dict[str, int]]:
653
+ """Collect service health data from service registry."""
654
+ services: Dict[str, Dict[str, int]] = {}
655
+ if not (hasattr(request.app.state, "service_registry") and request.app.state.service_registry is not None):
656
+ return services
657
+
658
+ service_registry = request.app.state.service_registry
659
+ try:
660
+ for service_type in list(ServiceType):
661
+ providers = service_registry.get_services_by_type(service_type)
662
+ if providers:
663
+ healthy_count = 0
664
+ for provider in providers:
665
+ if await _check_provider_health(provider):
666
+ healthy_count += 1
667
+ else:
668
+ logger.debug(f"Service health check returned unhealthy for {service_type.value}")
669
+ services[service_type.value] = {"available": len(providers), "healthy": healthy_count}
670
+ except Exception as e:
671
+ logger.error(f"Error checking service health: {e}")
672
+
673
+ return services
674
+
675
+
676
+ def _check_processor_via_runtime(runtime: Any) -> Optional[bool]:
677
+ """Check processor health via runtime's agent_processor directly.
678
+
679
+ Returns True if healthy, False if unhealthy, None if cannot determine.
680
+ """
681
+ if not runtime:
682
+ return None
683
+ agent_processor = getattr(runtime, "agent_processor", None)
684
+ if not agent_processor:
685
+ return None
686
+ # Agent processor exists - check if it's running
687
+ is_running = getattr(agent_processor, "_running", False)
688
+ if is_running:
689
+ return True
690
+ # Also check via _agent_task if available
691
+ agent_task = getattr(runtime, "_agent_task", None)
692
+ if agent_task and not agent_task.done():
693
+ return True
694
+ return None
695
+
696
+
697
+ def _get_runtime_control_from_app(request: Request) -> Any:
698
+ """Get RuntimeControlService from app state, trying multiple locations."""
699
+ runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
700
+ if not runtime_control:
701
+ runtime_control = getattr(request.app.state, "runtime_control_service", None)
702
+ return runtime_control
703
+
704
+
705
+ async def _check_health_via_runtime_control(runtime_control: Any) -> Optional[bool]:
706
+ """Check processor health via RuntimeControlService.
707
+
708
+ Returns True if healthy, False if unhealthy, None if cannot determine.
709
+ """
710
+ if not runtime_control:
711
+ return None
712
+ try:
713
+ # Try get_processor_queue_status if available
714
+ if hasattr(runtime_control, "get_processor_queue_status"):
715
+ queue_status = await runtime_control.get_processor_queue_status()
716
+ processor_healthy = queue_status.processor_name != "unknown"
717
+ runtime_status = await runtime_control.get_runtime_status()
718
+ return bool(processor_healthy and runtime_status.is_running)
719
+ # Fallback: Check runtime status dict (APIRuntimeControlService)
720
+ elif hasattr(runtime_control, "get_runtime_status"):
721
+ status = runtime_control.get_runtime_status()
722
+ if isinstance(status, dict):
723
+ # APIRuntimeControlService returns dict, not paused = healthy
724
+ return not status.get("paused", False)
725
+ except Exception as e:
726
+ logger.warning(f"Failed to check processor health via runtime_control: {e}")
727
+ return None
728
+
729
+
730
+ async def _check_processor_health(request: Request) -> bool:
731
+ """Check if processor thread is healthy."""
732
+ runtime = getattr(request.app.state, "runtime", None)
733
+
734
+ # First try: Check the runtime's agent_processor directly
735
+ runtime_result = _check_processor_via_runtime(runtime)
736
+ if runtime_result is True:
737
+ return True
738
+
739
+ # Second try: Use RuntimeControlService if available (for full API)
740
+ runtime_control = _get_runtime_control_from_app(request)
741
+ control_result = await _check_health_via_runtime_control(runtime_control)
742
+ if control_result is not None:
743
+ return control_result
744
+
745
+ # If we have a runtime with agent_processor, consider healthy
746
+ if runtime and getattr(runtime, "agent_processor", None) is not None:
747
+ return True
748
+
749
+ return False
750
+
751
+
752
+ def _determine_overall_status(init_complete: bool, processor_healthy: bool, services: Dict[str, Dict[str, int]]) -> str:
753
+ """Determine overall system status based on components."""
754
+ total_services = sum(s.get("available", 0) for s in services.values())
755
+ healthy_services = sum(s.get("healthy", 0) for s in services.values())
756
+
757
+ if not init_complete:
758
+ return "initializing"
759
+ elif not processor_healthy:
760
+ return "critical" # Processor thread dead = critical
761
+ elif healthy_services == total_services:
762
+ return "healthy"
763
+ elif healthy_services >= total_services * 0.8:
764
+ return "degraded"
765
+ else:
766
+ return "critical"
767
+
768
+
769
+ def _get_cognitive_state(request: Request) -> Optional[str]:
770
+ """Get cognitive state from agent processor if available."""
771
+ cognitive_state: Optional[str] = None
772
+ runtime = getattr(request.app.state, "runtime", None)
773
+ if runtime and hasattr(runtime, "agent_processor") and runtime.agent_processor is not None:
774
+ try:
775
+ cognitive_state = runtime.agent_processor.get_current_state()
776
+ except Exception as e:
777
+ logger.warning(
778
+ f"Failed to retrieve cognitive state: {type(e).__name__}: {str(e)} - Agent processor may not be initialized"
779
+ )
780
+ return cognitive_state
781
+
782
+
783
+ def _create_final_response(
784
+ base_result: RuntimeControlResponse, cognitive_state: Optional[str]
785
+ ) -> RuntimeControlResponse:
786
+ """Create final response with cognitive state and any enhanced fields."""
787
+ response = RuntimeControlResponse(
788
+ success=base_result.success,
789
+ message=base_result.message,
790
+ processor_state=base_result.processor_state,
791
+ cognitive_state=cognitive_state or base_result.cognitive_state or "UNKNOWN",
792
+ queue_depth=base_result.queue_depth,
793
+ )
794
+
795
+ # Copy enhanced fields if they exist
796
+ if hasattr(base_result, "current_step"):
797
+ response.current_step = base_result.current_step
798
+ if hasattr(base_result, "current_step_schema"):
799
+ response.current_step_schema = base_result.current_step_schema
800
+ if hasattr(base_result, "pipeline_state"):
801
+ response.pipeline_state = base_result.pipeline_state
802
+
803
+ return response
804
+
805
+
806
+ @router.post("/runtime/{action}", response_model=SuccessResponse[RuntimeControlResponse])
807
+ async def control_runtime(
808
+ action: str, request: Request, body: RuntimeAction = Body(...), auth: AuthContext = Depends(require_admin)
809
+ ) -> SuccessResponse[RuntimeControlResponse]:
810
+ """
811
+ Runtime control actions.
812
+
813
+ Control agent runtime behavior. Valid actions:
814
+ - pause: Pause message processing
815
+ - resume: Resume message processing
816
+ - state: Get current runtime state
817
+
818
+ Requires ADMIN role.
819
+ """
820
+ try:
821
+ runtime_control = _get_runtime_control_service(request)
822
+ _validate_runtime_action(action)
823
+
824
+ # Execute action
825
+ if action == "pause":
826
+ success = await _execute_pause_action(runtime_control, body)
827
+ current_step, current_step_schema, pipeline_state = _extract_pipeline_state_info(request)
828
+ result = _create_pause_response(success, current_step, current_step_schema, pipeline_state)
829
+ elif action == "resume":
830
+ result = await _execute_resume_action(runtime_control)
831
+ elif action == "state":
832
+ result = await _execute_state_action(runtime_control)
833
+ return SuccessResponse(data=result)
834
+
835
+ # Get cognitive state and create final response
836
+ cognitive_state = _get_cognitive_state(request)
837
+ response = _create_final_response(result, cognitive_state)
838
+
839
+ return SuccessResponse(data=response)
840
+
841
+ except HTTPException:
842
+ raise
843
+ except Exception as e:
844
+ raise HTTPException(status_code=500, detail=str(e))
845
+
846
+
847
+ # Valid cognitive states for transition
848
+ VALID_COGNITIVE_STATES = {"WORK", "DREAM", "PLAY", "SOLITUDE"}
849
+
850
+
851
+ @router.post("/state/transition", response_model=SuccessResponse[StateTransitionResponse])
852
+ async def transition_cognitive_state(
853
+ request: Request,
854
+ body: StateTransitionRequest = Body(...),
855
+ auth: AuthContext = Depends(require_admin),
856
+ ) -> SuccessResponse[StateTransitionResponse]:
857
+ """
858
+ Request a cognitive state transition.
859
+
860
+ Transitions the agent to a different cognitive state (WORK, DREAM, PLAY, SOLITUDE).
861
+ Valid transitions depend on the current state:
862
+ - From WORK: Can transition to DREAM, PLAY, or SOLITUDE
863
+ - From PLAY: Can transition to WORK or SOLITUDE
864
+ - From SOLITUDE: Can transition to WORK
865
+ - From DREAM: Typically transitions back to WORK when complete
866
+
867
+ Requires ADMIN role.
868
+ """
869
+ try:
870
+ target_state = body.target_state.upper()
871
+ logger.info(f"[STATE_TRANSITION] Request received: target_state={target_state}, reason={body.reason}")
872
+
873
+ # Validate target state
874
+ if target_state not in VALID_COGNITIVE_STATES:
875
+ logger.error(f"[STATE_TRANSITION] FAIL: Invalid target state '{target_state}'")
876
+ raise HTTPException(
877
+ status_code=400,
878
+ detail=f"Invalid target state '{target_state}'. Must be one of: {', '.join(sorted(VALID_COGNITIVE_STATES))}",
879
+ )
880
+
881
+ # Get current state
882
+ previous_state = _get_cognitive_state(request)
883
+ logger.info(f"[STATE_TRANSITION] Current state: {previous_state}")
884
+
885
+ # Get runtime control service - FAIL FAST with detailed logging
886
+ runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
887
+ if not runtime_control:
888
+ runtime_control = getattr(request.app.state, "runtime_control_service", None)
889
+
890
+ if not runtime_control:
891
+ logger.error("[STATE_TRANSITION] FAIL: No runtime control service available in app.state")
892
+ logger.error(f"[STATE_TRANSITION] Available app.state attrs: {dir(request.app.state)}")
893
+ raise HTTPException(status_code=503, detail=ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE)
894
+
895
+ # Log service type for debugging
896
+ service_type = type(runtime_control).__name__
897
+ service_module = type(runtime_control).__module__
898
+ logger.info(f"[STATE_TRANSITION] Runtime control service: {service_type} from {service_module}")
899
+
900
+ # Check if request_state_transition is available - FAIL LOUD
901
+ has_method = hasattr(runtime_control, "request_state_transition")
902
+ logger.info(f"[STATE_TRANSITION] Has request_state_transition method: {has_method}")
903
+
904
+ if not has_method:
905
+ available_methods = [m for m in dir(runtime_control) if not m.startswith("_")]
906
+ logger.error(f"[STATE_TRANSITION] FAIL: Service {service_type} missing request_state_transition")
907
+ logger.error(f"[STATE_TRANSITION] Available methods: {available_methods}")
908
+ raise HTTPException(
909
+ status_code=503,
910
+ detail=f"State transition not supported by {service_type}. Missing request_state_transition method.",
911
+ )
912
+
913
+ # Request the transition
914
+ reason = body.reason or f"Requested via API from {previous_state or 'UNKNOWN'}"
915
+ logger.info(f"[STATE_TRANSITION] Calling request_state_transition({target_state}, {reason})")
916
+ success = await runtime_control.request_state_transition(target_state, reason)
917
+ logger.info(f"[STATE_TRANSITION] Transition result: success={success}")
918
+
919
+ # Get current state after transition attempt
920
+ current_state = _get_cognitive_state(request) or target_state
921
+ logger.info(f"[STATE_TRANSITION] Post-transition state: {current_state}")
922
+
923
+ if success:
924
+ message = f"Transition to {target_state} initiated successfully"
925
+ else:
926
+ message = f"Transition to {target_state} could not be initiated"
927
+
928
+ return SuccessResponse(
929
+ data=StateTransitionResponse(
930
+ success=success,
931
+ message=message,
932
+ previous_state=previous_state,
933
+ current_state=current_state,
934
+ )
935
+ )
936
+
937
+ except HTTPException:
938
+ raise
939
+ except Exception as e:
940
+ logger.error(f"[STATE_TRANSITION] FAIL: Unexpected error: {type(e).__name__}: {e}")
941
+ import traceback
942
+
943
+ logger.error(f"[STATE_TRANSITION] Traceback:\n{traceback.format_exc()}")
944
+ raise HTTPException(status_code=500, detail=str(e))
945
+
946
+
947
+ def _parse_direct_service_key(service_key: str) -> tuple[str, str]:
948
+ """Parse direct service key and return service_type and display_name."""
949
+ parts = service_key.split(".")
950
+ if len(parts) >= 3:
951
+ service_type = parts[1] # 'graph', 'infrastructure', etc.
952
+ service_name = parts[2] # 'memory_service', 'time_service', etc.
953
+
954
+ # Convert snake_case to PascalCase for display
955
+ display_name = "".join(word.capitalize() for word in service_name.split("_"))
956
+ return service_type, display_name
957
+ return "unknown", service_key
958
+
959
+
960
+ def _parse_registry_service_key(service_key: str) -> tuple[str, str]:
961
+ """Parse registry service key and return service_type and display_name."""
962
+ parts = service_key.split(".")
963
+ logger.debug(f"Parsing registry key: {service_key}, parts: {parts}")
964
+
965
+ # Handle both 3-part and 4-part keys
966
+ if len(parts) >= 4 and parts[1] == "ServiceType":
967
+ # Format: registry.ServiceType.ENUM.ServiceName_id
968
+ service_type_enum = f"{parts[1]}.{parts[2]}" # 'ServiceType.TOOL'
969
+ service_name = parts[3] # 'APIToolService_127803015745648'
970
+ logger.debug(f"4-part key: {service_key}, service_name: {service_name}")
971
+ else:
972
+ # Fallback: registry.ENUM.ServiceName
973
+ service_type_enum = parts[1] # 'ServiceType.COMMUNICATION', etc.
974
+ service_name = parts[2] if len(parts) > 2 else parts[1] # Service name or enum value
975
+ logger.debug(f"3-part key: {service_key}, service_name: {service_name}")
976
+
977
+ # Clean up service name (remove instance ID)
978
+ if "_" in service_name:
979
+ service_name = service_name.split("_")[0]
980
+
981
+ # Extract adapter type from service name
982
+ adapter_prefix = ""
983
+ if "Discord" in service_name:
984
+ adapter_prefix = "DISCORD"
985
+ elif "API" in service_name:
986
+ adapter_prefix = "API"
987
+ elif "CLI" in service_name:
988
+ adapter_prefix = "CLI"
989
+
990
+ # Map ServiceType enum to category and set display name
991
+ service_type, display_name = _map_service_type_enum(service_type_enum, service_name, adapter_prefix)
992
+
993
+ return service_type, display_name
994
+
995
+
996
+ def _map_service_type_enum(service_type_enum: str, service_name: str, adapter_prefix: str) -> tuple[str, str]:
997
+ """Map ServiceType enum to category and create display name."""
998
+ service_type = _get_service_category(service_type_enum)
999
+ display_name = _create_display_name(service_type_enum, service_name, adapter_prefix)
1000
+
1001
+ return service_type, display_name
1002
+
1003
+
1004
+ def _get_service_category(service_type_enum: str) -> str:
1005
+ """Get the service category based on the service type enum."""
1006
+ # Tool Services (need to check first due to SECRETS_TOOL containing SECRETS)
1007
+ if "TOOL" in service_type_enum:
1008
+ return "tool"
1009
+
1010
+ # Adapter Services (Communication is adapter-specific)
1011
+ elif "COMMUNICATION" in service_type_enum:
1012
+ return "adapter"
1013
+
1014
+ # Runtime Services (need to check RUNTIME_CONTROL before SECRETS in infrastructure)
1015
+ elif any(service in service_type_enum for service in ["LLM", "RUNTIME_CONTROL", "TASK_SCHEDULER"]):
1016
+ return "runtime"
1017
+
1018
+ # Graph Services (6)
1019
+ elif any(
1020
+ service in service_type_enum
1021
+ for service in ["MEMORY", "CONFIG", "TELEMETRY", "AUDIT", "INCIDENT_MANAGEMENT", "TSDB_CONSOLIDATION"]
1022
+ ):
1023
+ return "graph"
1024
+
1025
+ # Infrastructure Services (7)
1026
+ elif any(
1027
+ service in service_type_enum
1028
+ for service in [
1029
+ "TIME",
1030
+ "SECRETS",
1031
+ "AUTHENTICATION",
1032
+ "RESOURCE_MONITOR",
1033
+ "DATABASE_MAINTENANCE",
1034
+ "INITIALIZATION",
1035
+ "SHUTDOWN",
1036
+ ]
1037
+ ):
1038
+ return "infrastructure"
1039
+
1040
+ # Governance Services (4)
1041
+ elif any(
1042
+ service in service_type_enum
1043
+ for service in ["WISE_AUTHORITY", "ADAPTIVE_FILTER", "VISIBILITY", "SELF_OBSERVATION"]
1044
+ ):
1045
+ return "governance"
1046
+
1047
+ else:
1048
+ return "unknown"
1049
+
1050
+
1051
+ def _create_display_name(service_type_enum: str, service_name: str, adapter_prefix: str) -> str:
1052
+ """Create appropriate display name based on service type and adapter prefix."""
1053
+ if not adapter_prefix:
1054
+ return service_name
1055
+
1056
+ if "COMMUNICATION" in service_type_enum:
1057
+ return f"{adapter_prefix}-COMM"
1058
+ elif "RUNTIME_CONTROL" in service_type_enum:
1059
+ return f"{adapter_prefix}-RUNTIME"
1060
+ elif "TOOL" in service_type_enum:
1061
+ return f"{adapter_prefix}-TOOL"
1062
+ elif "WISE_AUTHORITY" in service_type_enum:
1063
+ return f"{adapter_prefix}-WISE"
1064
+ else:
1065
+ return service_name
1066
+
1067
+
1068
+ def _parse_service_key(service_key: str) -> tuple[str, str]:
1069
+ """Parse any service key and return service_type and display_name."""
1070
+ parts = service_key.split(".")
1071
+
1072
+ # Handle direct services (format: direct.service_type.service_name)
1073
+ if service_key.startswith("direct.") and len(parts) >= 3:
1074
+ return _parse_direct_service_key(service_key)
1075
+
1076
+ # Handle registry services (format: registry.ServiceType.ENUM.ServiceName_id)
1077
+ elif service_key.startswith("registry.") and len(parts) >= 3:
1078
+ return _parse_registry_service_key(service_key)
1079
+
1080
+ else:
1081
+ return "unknown", service_key
1082
+
1083
+
1084
+ def _create_service_status(service_key: str, details: JSONDict) -> ServiceStatus:
1085
+ """Create ServiceStatus from service key and details."""
1086
+ service_type, display_name = _parse_service_key(service_key)
1087
+
1088
+ return ServiceStatus(
1089
+ name=display_name,
1090
+ type=service_type,
1091
+ healthy=details.get("healthy", False),
1092
+ available=details.get("healthy", False), # Use healthy as available
1093
+ uptime_seconds=None, # Not available in simplified view
1094
+ metrics=ServiceMetrics(),
1095
+ )
1096
+
1097
+
1098
+ def _update_service_summary(service_summary: Dict[str, Dict[str, int]], service_type: str, is_healthy: bool) -> None:
1099
+ """Update service summary with service type and health status."""
1100
+ if service_type not in service_summary:
1101
+ service_summary[service_type] = {"total": 0, "healthy": 0}
1102
+ service_summary[service_type]["total"] += 1
1103
+ if is_healthy:
1104
+ service_summary[service_type]["healthy"] += 1
1105
+
1106
+
1107
+ @router.get("/services", response_model=SuccessResponse[ServicesStatusResponse])
1108
+ async def get_services_status(
1109
+ request: Request, auth: AuthContext = Depends(require_observer)
1110
+ ) -> SuccessResponse[ServicesStatusResponse]:
1111
+ """
1112
+ Service status.
1113
+
1114
+ Returns status of all system services including health,
1115
+ availability, and basic metrics.
1116
+ """
1117
+ # Use the runtime control service to get all services
1118
+ try:
1119
+ runtime_control = _get_runtime_control_service(request)
1120
+ except HTTPException:
1121
+ # Handle case where no runtime control service is available
1122
+ return SuccessResponse(
1123
+ data=ServicesStatusResponse(
1124
+ services=[], total_services=0, healthy_services=0, timestamp=datetime.now(timezone.utc)
1125
+ )
1126
+ )
1127
+
1128
+ # Get service health status from runtime control
1129
+ try:
1130
+ health_status = await runtime_control.get_service_health_status()
1131
+
1132
+ # Convert service details to ServiceStatus list using helper functions
1133
+ services = []
1134
+ service_summary: Dict[str, Dict[str, int]] = {}
1135
+
1136
+ # Include ALL services (both direct and registry)
1137
+ for service_key, details in health_status.service_details.items():
1138
+ status = _create_service_status(service_key, details)
1139
+ services.append(status)
1140
+ _update_service_summary(service_summary, status.type, status.healthy)
1141
+
1142
+ return SuccessResponse(
1143
+ data=ServicesStatusResponse(
1144
+ services=services,
1145
+ total_services=len(services),
1146
+ healthy_services=sum(1 for s in services if s.healthy),
1147
+ timestamp=datetime.now(timezone.utc),
1148
+ )
1149
+ )
1150
+ except Exception as e:
1151
+ logger.error(f"Error getting service status: {e}")
1152
+ return SuccessResponse(
1153
+ data=ServicesStatusResponse(
1154
+ services=[], total_services=0, healthy_services=0, timestamp=datetime.now(timezone.utc)
1155
+ )
1156
+ )
1157
+
1158
+
1159
+ def _validate_shutdown_request(body: ShutdownRequest) -> None:
1160
+ """Validate shutdown request confirmation."""
1161
+ if not body.confirm:
1162
+ raise HTTPException(status_code=400, detail="Confirmation required (confirm=true)")
1163
+
1164
+
1165
+ def _get_shutdown_service(request: Request) -> Any:
1166
+ """Get shutdown service from runtime, raising HTTPException if not available."""
1167
+ runtime = getattr(request.app.state, "runtime", None)
1168
+ if not runtime:
1169
+ raise HTTPException(status_code=503, detail="Runtime not available")
1170
+
1171
+ shutdown_service = getattr(runtime, "shutdown_service", None)
1172
+ if not shutdown_service:
1173
+ raise HTTPException(status_code=503, detail=ERROR_SHUTDOWN_SERVICE_NOT_AVAILABLE)
1174
+
1175
+ return shutdown_service, runtime
1176
+
1177
+
1178
+ def _check_shutdown_already_requested(shutdown_service: Any) -> None:
1179
+ """Check if shutdown is already in progress."""
1180
+ if shutdown_service.is_shutdown_requested():
1181
+ existing_reason = shutdown_service.get_shutdown_reason()
1182
+ raise HTTPException(status_code=409, detail=f"Shutdown already requested: {existing_reason}")
1183
+
1184
+
1185
+ def _build_shutdown_reason(body: ShutdownRequest, auth: AuthContext) -> str:
1186
+ """Build and sanitize shutdown reason."""
1187
+ reason = f"{body.reason} (API shutdown by {auth.user_id})"
1188
+ if body.force:
1189
+ reason += " [FORCED]"
1190
+
1191
+ # Sanitize reason for logging to prevent log injection
1192
+ # Replace newlines and control characters with spaces
1193
+ safe_reason = "".join(c if c.isprintable() and c not in "\n\r\t" else " " for c in reason)
1194
+
1195
+ return safe_reason
1196
+
1197
+
1198
+ def _create_audit_metadata(body: ShutdownRequest, auth: AuthContext, request: Request) -> Dict[str, Any]:
1199
+ """Create metadata dict for shutdown audit event."""
1200
+ is_service_account = auth.role.value == "SERVICE_ACCOUNT"
1201
+ return {
1202
+ "force": body.force,
1203
+ "is_service_account": is_service_account,
1204
+ "auth_role": auth.role.value,
1205
+ "ip_address": request.client.host if request.client else "unknown",
1206
+ "user_agent": request.headers.get("user-agent", "unknown"),
1207
+ "request_path": str(request.url.path),
1208
+ }
1209
+
1210
+
1211
+ async def _audit_shutdown_request(
1212
+ request: Request, body: ShutdownRequest, auth: AuthContext, safe_reason: str
1213
+ ) -> None: # NOSONAR - async required for create_task
1214
+ """Audit the shutdown request for security tracking."""
1215
+ audit_service = getattr(request.app.state, "audit_service", None)
1216
+ if not audit_service:
1217
+ return
1218
+
1219
+ from ciris_engine.schemas.services.graph.audit import AuditEventData
1220
+
1221
+ audit_event = AuditEventData(
1222
+ entity_id="system",
1223
+ actor=auth.user_id,
1224
+ outcome="initiated",
1225
+ severity="high" if body.force else "warning",
1226
+ action="system_shutdown",
1227
+ resource="system",
1228
+ reason=safe_reason,
1229
+ metadata=_create_audit_metadata(body, auth, request),
1230
+ )
1231
+
1232
+ import asyncio
1233
+
1234
+ # Store task reference to prevent garbage collection
1235
+ # Using _ prefix to indicate we're intentionally not awaiting
1236
+ _audit_task = asyncio.create_task(audit_service.log_event("system_shutdown_request", audit_event))
1237
+
1238
+
1239
+ async def _execute_shutdown(shutdown_service: Any, runtime: Any, body: ShutdownRequest, reason: str) -> None:
1240
+ """Execute the shutdown with appropriate method based on force flag."""
1241
+ if body.force:
1242
+ # Forced shutdown: bypass thought processing, immediate termination
1243
+ await shutdown_service.emergency_shutdown(reason, timeout_seconds=5)
1244
+ else:
1245
+ # Normal shutdown: allow thoughtful consideration via runtime
1246
+ # The runtime's request_shutdown will call the shutdown service AND set global flags
1247
+ runtime.request_shutdown(reason)
1248
+
1249
+
1250
+ @router.post("/shutdown", response_model=SuccessResponse[ShutdownResponse])
1251
+ async def shutdown_system(
1252
+ body: ShutdownRequest, request: Request, auth: AuthContext = Depends(require_admin)
1253
+ ) -> SuccessResponse[ShutdownResponse]:
1254
+ """
1255
+ Graceful shutdown.
1256
+
1257
+ Initiates graceful system shutdown. Requires confirmation
1258
+ flag to prevent accidental shutdowns.
1259
+
1260
+ Requires ADMIN role.
1261
+ """
1262
+ try:
1263
+ # Validate and get required services
1264
+ _validate_shutdown_request(body)
1265
+ shutdown_service, runtime = _get_shutdown_service(request)
1266
+
1267
+ # Check if already shutting down
1268
+ _check_shutdown_already_requested(shutdown_service)
1269
+
1270
+ # Build and sanitize shutdown reason
1271
+ safe_reason = _build_shutdown_reason(body, auth)
1272
+
1273
+ # Log shutdown request
1274
+ logger.warning(f"SHUTDOWN requested: {safe_reason}")
1275
+
1276
+ # Audit shutdown request
1277
+ await _audit_shutdown_request(request, body, auth, safe_reason)
1278
+
1279
+ # Execute shutdown
1280
+ await _execute_shutdown(shutdown_service, runtime, body, safe_reason)
1281
+
1282
+ # Create response
1283
+ response = ShutdownResponse(
1284
+ status="initiated",
1285
+ message=f"System shutdown initiated: {safe_reason}",
1286
+ shutdown_initiated=True,
1287
+ timestamp=datetime.now(timezone.utc),
1288
+ )
1289
+
1290
+ return SuccessResponse(data=response)
1291
+
1292
+ except HTTPException:
1293
+ raise
1294
+ except Exception as e:
1295
+ logger.error(f"Error initiating shutdown: {e}")
1296
+ raise HTTPException(status_code=500, detail=str(e))
1297
+
1298
+
1299
+ def _is_localhost_request(request: Request) -> bool:
1300
+ """Check if request originates from localhost (safe for unauthenticated shutdown)."""
1301
+ client_host = request.client.host if request.client else None
1302
+ # Accept localhost variants: 127.0.0.1, ::1, localhost
1303
+ return client_host in ("127.0.0.1", "::1", "localhost", None)
1304
+
1305
+
1306
+ # Constants for local shutdown
1307
+ _RESUME_TIMEOUT_SECONDS = 30.0
1308
+
1309
+
1310
+ def _get_server_state(runtime: Any) -> Dict[str, Any]:
1311
+ """Get server state info for logging and responses.
1312
+
1313
+ Args:
1314
+ runtime: The runtime instance (may be None)
1315
+
1316
+ Returns:
1317
+ Dict with server_state, uptime_seconds, resume_in_progress, resume_elapsed_seconds
1318
+ """
1319
+ if not runtime:
1320
+ return {
1321
+ "server_state": "STARTING",
1322
+ "uptime_seconds": 0,
1323
+ "resume_in_progress": False,
1324
+ "resume_elapsed_seconds": None,
1325
+ }
1326
+
1327
+ uptime = time.time() - getattr(runtime, "_startup_time", time.time())
1328
+ resume_in_progress = getattr(runtime, "_resume_in_progress", False)
1329
+ resume_started = getattr(runtime, "_resume_started_at", None)
1330
+ resume_elapsed = (time.time() - resume_started) if resume_started else None
1331
+ shutdown_in_progress = getattr(runtime, "_shutdown_in_progress", False)
1332
+
1333
+ state = _determine_server_state(runtime, shutdown_in_progress, resume_in_progress)
1334
+
1335
+ return {
1336
+ "server_state": state,
1337
+ "uptime_seconds": round(uptime, 2),
1338
+ "resume_in_progress": resume_in_progress,
1339
+ "resume_elapsed_seconds": round(resume_elapsed, 2) if resume_elapsed else None,
1340
+ }
1341
+
1342
+
1343
+ def _determine_server_state(runtime: Any, shutdown_in_progress: bool, resume_in_progress: bool) -> str:
1344
+ """Determine the current server state string.
1345
+
1346
+ Args:
1347
+ runtime: The runtime instance
1348
+ shutdown_in_progress: Whether shutdown is in progress
1349
+ resume_in_progress: Whether resume is in progress
1350
+
1351
+ Returns:
1352
+ State string: SHUTTING_DOWN, RESUMING, READY, or INITIALIZING
1353
+ """
1354
+ if shutdown_in_progress:
1355
+ return "SHUTTING_DOWN"
1356
+ if resume_in_progress:
1357
+ return "RESUMING"
1358
+ if runtime and getattr(runtime, "_initialized", False):
1359
+ return "READY"
1360
+ return "INITIALIZING"
1361
+
1362
+
1363
+ def _check_resume_blocking(runtime: Any, state_info: Dict[str, Any]) -> Optional[Response]:
1364
+ """Check if resume is in progress and should block shutdown.
1365
+
1366
+ Args:
1367
+ runtime: The runtime instance
1368
+ state_info: Current server state info dict
1369
+
1370
+ Returns:
1371
+ JSONResponse if shutdown should be blocked, None if OK to proceed
1372
+ """
1373
+ resume_in_progress = getattr(runtime, "_resume_in_progress", False)
1374
+ if not resume_in_progress:
1375
+ return None
1376
+
1377
+ resume_started_at = getattr(runtime, "_resume_started_at", None)
1378
+ resume_elapsed = (time.time() - resume_started_at) if resume_started_at else 0
1379
+
1380
+ if resume_elapsed >= _RESUME_TIMEOUT_SECONDS:
1381
+ # Resume stuck - allow shutdown
1382
+ logger.warning(
1383
+ f"[LOCAL_SHUTDOWN] Resume exceeded timeout ({resume_elapsed:.1f}s > "
1384
+ f"{_RESUME_TIMEOUT_SECONDS}s) - treating as stuck, allowing shutdown"
1385
+ )
1386
+ return None
1387
+
1388
+ # Resume actively happening - ask caller to retry
1389
+ remaining = _RESUME_TIMEOUT_SECONDS - resume_elapsed
1390
+ retry_after_ms = min(2000, int(remaining * 1000))
1391
+
1392
+ logger.warning(
1393
+ f"[LOCAL_SHUTDOWN] Rejected (409) - resume in progress for {resume_elapsed:.1f}s, "
1394
+ f"retry in {retry_after_ms}ms (timeout at {_RESUME_TIMEOUT_SECONDS}s)"
1395
+ )
1396
+ return JSONResponse(
1397
+ status_code=409,
1398
+ content={
1399
+ "status": "busy",
1400
+ "reason": f"Resume from first-run in progress ({resume_elapsed:.1f}s elapsed)",
1401
+ "retry_after_ms": retry_after_ms,
1402
+ "resume_timeout_seconds": _RESUME_TIMEOUT_SECONDS,
1403
+ **state_info,
1404
+ },
1405
+ )
1406
+
1407
+
1408
+ def _check_shutdown_already_in_progress(runtime: Any, state_info: Dict[str, Any]) -> Optional[Response]:
1409
+ """Check if shutdown is already in progress.
1410
+
1411
+ Args:
1412
+ runtime: The runtime instance
1413
+ state_info: Current server state info dict
1414
+
1415
+ Returns:
1416
+ JSONResponse if shutdown already in progress, None otherwise
1417
+ """
1418
+ shutdown_service = getattr(runtime, "shutdown_service", None)
1419
+ shutdown_in_progress = getattr(runtime, "_shutdown_in_progress", False)
1420
+
1421
+ is_shutting_down = shutdown_in_progress or (shutdown_service and shutdown_service.is_shutdown_requested())
1422
+
1423
+ if not is_shutting_down:
1424
+ return None
1425
+
1426
+ existing_reason = shutdown_service.get_shutdown_reason() if shutdown_service else "unknown"
1427
+ logger.info(f"[LOCAL_SHUTDOWN] Shutdown already in progress: {existing_reason}")
1428
+ return JSONResponse(
1429
+ status_code=202,
1430
+ content={
1431
+ "status": "accepted",
1432
+ "reason": f"Shutdown already in progress: {existing_reason}",
1433
+ **state_info,
1434
+ },
1435
+ )
1436
+
1437
+
1438
+ def _initiate_force_shutdown(runtime: Any, reason: str) -> None:
1439
+ """Initiate forced shutdown with background exit thread.
1440
+
1441
+ Args:
1442
+ runtime: The runtime instance
1443
+ reason: Shutdown reason string
1444
+ """
1445
+ import os
1446
+ import threading
1447
+
1448
+ runtime._shutdown_in_progress = True
1449
+
1450
+ def _force_exit() -> None:
1451
+ """Force process exit after brief delay to allow response to be sent."""
1452
+ time.sleep(0.5)
1453
+ logger.warning("[LOCAL_SHUTDOWN] Force exiting process NOW")
1454
+ os._exit(0)
1455
+
1456
+ exit_thread = threading.Thread(target=_force_exit, daemon=True)
1457
+ exit_thread.start()
1458
+
1459
+ # Also request normal shutdown in case force exit fails
1460
+ runtime.request_shutdown(reason)
1461
+
1462
+
1463
+ @router.post("/local-shutdown", response_model=SuccessResponse[ShutdownResponse])
1464
+ async def local_shutdown(request: Request) -> Response:
1465
+ """
1466
+ Localhost-only shutdown endpoint (no authentication required).
1467
+
1468
+ This endpoint is designed for Android/mobile apps where:
1469
+ - App data may be cleared (losing auth tokens)
1470
+ - Previous Python process may still be running
1471
+ - Need to gracefully shut down before starting new instance
1472
+
1473
+ Security: Only accepts requests from localhost (127.0.0.1, ::1).
1474
+ This is safe because only processes on the same device can call it.
1475
+
1476
+ Response codes for SmartStartup negotiation:
1477
+ - 200: Shutdown initiated successfully
1478
+ - 202: Shutdown already in progress
1479
+ - 403: Not localhost (security rejection)
1480
+ - 409: Resume in progress, retry later (with retry_after_ms)
1481
+ - 503: Server not ready
1482
+ """
1483
+ # Verify request is from localhost
1484
+ client_host = request.client.host if request.client else "unknown"
1485
+ if not _is_localhost_request(request):
1486
+ logger.warning(f"[LOCAL_SHUTDOWN] Rejected from non-local client: {client_host}")
1487
+ raise HTTPException(status_code=403, detail="This endpoint only accepts requests from localhost")
1488
+
1489
+ logger.info(f"[LOCAL_SHUTDOWN] Request received from {client_host}")
1490
+
1491
+ # Get runtime
1492
+ runtime = getattr(request.app.state, "runtime", None)
1493
+ if not runtime:
1494
+ logger.warning("[LOCAL_SHUTDOWN] Runtime not available (503)")
1495
+ return JSONResponse(
1496
+ status_code=503,
1497
+ content={
1498
+ "status": "error",
1499
+ "reason": "Runtime not available",
1500
+ "retry_after_ms": 1000,
1501
+ "server_state": "STARTING",
1502
+ },
1503
+ )
1504
+
1505
+ state_info = _get_server_state(runtime)
1506
+ logger.info(f"[LOCAL_SHUTDOWN] Server state: {state_info}")
1507
+
1508
+ # Check if resume is blocking shutdown
1509
+ resume_response = _check_resume_blocking(runtime, state_info)
1510
+ if resume_response:
1511
+ return resume_response
1512
+
1513
+ # Check if already shutting down
1514
+ shutdown_response = _check_shutdown_already_in_progress(runtime, state_info)
1515
+ if shutdown_response:
1516
+ return shutdown_response
1517
+
1518
+ # Verify shutdown service is available
1519
+ shutdown_service = getattr(runtime, "shutdown_service", None)
1520
+ if not shutdown_service:
1521
+ logger.warning("[LOCAL_SHUTDOWN] Shutdown service not available (503)")
1522
+ return JSONResponse(
1523
+ status_code=503,
1524
+ content={
1525
+ "status": "error",
1526
+ "reason": "Shutdown service not available",
1527
+ "retry_after_ms": 1000,
1528
+ **state_info,
1529
+ },
1530
+ )
1531
+
1532
+ # Initiate shutdown
1533
+ reason = "Local shutdown requested (Android SmartStartup)"
1534
+ logger.warning(f"[LOCAL_SHUTDOWN] Initiating IMMEDIATE shutdown: {reason}")
1535
+ _initiate_force_shutdown(runtime, reason)
1536
+
1537
+ logger.info("[LOCAL_SHUTDOWN] Shutdown initiated successfully (200)")
1538
+ return JSONResponse(
1539
+ status_code=200,
1540
+ content={
1541
+ "status": "accepted",
1542
+ "reason": reason,
1543
+ **state_info,
1544
+ },
1545
+ )
1546
+
1547
+
1548
+ # Adapter Management Endpoints
1549
+
1550
+
1551
+ @router.get("/adapters", response_model=SuccessResponse[AdapterListResponse])
1552
+ async def list_adapters(
1553
+ request: Request, auth: AuthContext = Depends(require_observer)
1554
+ ) -> SuccessResponse[AdapterListResponse]:
1555
+ """
1556
+ List all loaded adapters.
1557
+
1558
+ Returns information about all currently loaded adapter instances
1559
+ including their type, status, and basic metrics.
1560
+ """
1561
+ runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
1562
+ if not runtime_control:
1563
+ raise HTTPException(status_code=503, detail=ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE)
1564
+
1565
+ try:
1566
+ # Get adapter list from runtime control service
1567
+ adapters = await runtime_control.list_adapters()
1568
+
1569
+ # Convert to response format
1570
+ adapter_statuses = []
1571
+ for adapter in adapters:
1572
+ # Convert AdapterInfo to AdapterStatusSchema
1573
+ # Check status using the enum value (which is lowercase)
1574
+ from ciris_engine.schemas.services.core.runtime import AdapterStatus
1575
+
1576
+ is_running = adapter.status == AdapterStatus.RUNNING or str(adapter.status).lower() == "running"
1577
+
1578
+ config = AdapterConfig(adapter_type=adapter.adapter_type, enabled=is_running, settings={})
1579
+
1580
+ metrics = None
1581
+ if adapter.messages_processed > 0 or adapter.error_count > 0:
1582
+ metrics = AdapterMetrics(
1583
+ messages_processed=adapter.messages_processed,
1584
+ errors_count=adapter.error_count,
1585
+ uptime_seconds=(
1586
+ (datetime.now(timezone.utc) - adapter.started_at).total_seconds() if adapter.started_at else 0
1587
+ ),
1588
+ last_error=adapter.last_error,
1589
+ last_error_time=None,
1590
+ )
1591
+
1592
+ adapter_statuses.append(
1593
+ AdapterStatusSchema(
1594
+ adapter_id=adapter.adapter_id,
1595
+ adapter_type=adapter.adapter_type,
1596
+ is_running=is_running,
1597
+ loaded_at=adapter.started_at or datetime.now(timezone.utc),
1598
+ services_registered=[], # Not available from AdapterInfo
1599
+ config_params=config,
1600
+ metrics=metrics, # Pass the AdapterMetrics object directly
1601
+ last_activity=None,
1602
+ tools=adapter.tools, # Include tools information
1603
+ )
1604
+ )
1605
+
1606
+ running_count = sum(1 for a in adapter_statuses if a.is_running)
1607
+
1608
+ response = AdapterListResponse(
1609
+ adapters=adapter_statuses, total_count=len(adapter_statuses), running_count=running_count
1610
+ )
1611
+
1612
+ return SuccessResponse(data=response)
1613
+
1614
+ except ValidationError as e:
1615
+ logger.error(f"Validation error listing adapters: {e}")
1616
+ logger.error(f"Validation errors detail: {e.errors()}")
1617
+ # Return empty list on validation error to avoid breaking GUI
1618
+ return SuccessResponse(data=AdapterListResponse(adapters=[], total_count=0, running_count=0))
1619
+ except Exception as e:
1620
+ logger.error(f"Error listing adapters: {e}")
1621
+ logger.error(f"Error type: {type(e).__name__}")
1622
+ import traceback
1623
+
1624
+ logger.error(f"Traceback: {traceback.format_exc()}")
1625
+ raise HTTPException(status_code=500, detail=str(e))
1626
+
1627
+
1628
+ # Module types helper functions
1629
+
1630
+
1631
+ def _get_core_adapter_info(adapter_type: str) -> ModuleTypeInfo:
1632
+ """Generate ModuleTypeInfo for a core adapter."""
1633
+ core_adapters: Dict[str, Dict[str, Any]] = {
1634
+ "api": {
1635
+ "name": "API Adapter",
1636
+ "description": "REST API adapter providing HTTP endpoints for CIRIS interaction",
1637
+ "service_types": ["COMMUNICATION", "TOOL", "RUNTIME_CONTROL"],
1638
+ "capabilities": [*COMM_CAPABILITIES, "tool:api", "runtime_control"],
1639
+ "configuration": [
1640
+ ModuleConfigParameter(
1641
+ name="host",
1642
+ param_type="string",
1643
+ default="127.0.0.1",
1644
+ description="Host address to bind to",
1645
+ env_var="CIRIS_API_HOST",
1646
+ required=False,
1647
+ ),
1648
+ ModuleConfigParameter(
1649
+ name="port",
1650
+ param_type="integer",
1651
+ default=8000,
1652
+ description="Port to listen on",
1653
+ env_var="CIRIS_API_PORT",
1654
+ required=False,
1655
+ ),
1656
+ ModuleConfigParameter(
1657
+ name="debug",
1658
+ param_type="boolean",
1659
+ default=False,
1660
+ description="Enable debug mode",
1661
+ env_var="CIRIS_API_DEBUG",
1662
+ required=False,
1663
+ ),
1664
+ ],
1665
+ },
1666
+ "cli": {
1667
+ "name": "CLI Adapter",
1668
+ "description": "Command-line interface adapter for interactive terminal sessions",
1669
+ "service_types": ["COMMUNICATION"],
1670
+ "capabilities": COMM_CAPABILITIES,
1671
+ "configuration": [
1672
+ ModuleConfigParameter(
1673
+ name="prompt",
1674
+ param_type="string",
1675
+ default="CIRIS> ",
1676
+ description="CLI prompt string",
1677
+ required=False,
1678
+ ),
1679
+ ],
1680
+ },
1681
+ "discord": {
1682
+ "name": "Discord Adapter",
1683
+ "description": "Discord bot adapter for community interaction",
1684
+ "service_types": ["COMMUNICATION", "TOOL"],
1685
+ "capabilities": [*COMM_CAPABILITIES, "tool:discord"],
1686
+ "configuration": [
1687
+ ModuleConfigParameter(
1688
+ name="discord_token",
1689
+ param_type="string",
1690
+ description="Discord bot token",
1691
+ env_var="CIRIS_DISCORD_TOKEN",
1692
+ required=True,
1693
+ sensitivity="HIGH",
1694
+ ),
1695
+ ModuleConfigParameter(
1696
+ name="guild_id",
1697
+ param_type="string",
1698
+ description="Discord guild ID to operate in",
1699
+ env_var="CIRIS_DISCORD_GUILD_ID",
1700
+ required=False,
1701
+ ),
1702
+ ModuleConfigParameter(
1703
+ name="channel_id",
1704
+ param_type="string",
1705
+ description="Default channel ID for messages",
1706
+ env_var="CIRIS_DISCORD_CHANNEL_ID",
1707
+ required=False,
1708
+ ),
1709
+ ],
1710
+ },
1711
+ }
1712
+
1713
+ adapter_info = core_adapters.get(adapter_type, {})
1714
+ return ModuleTypeInfo(
1715
+ module_id=adapter_type,
1716
+ name=adapter_info.get("name", adapter_type.title()),
1717
+ version="1.0.0",
1718
+ description=adapter_info.get("description", f"Core {adapter_type} adapter"),
1719
+ author="CIRIS Team",
1720
+ module_source="core",
1721
+ service_types=adapter_info.get("service_types", []),
1722
+ capabilities=adapter_info.get("capabilities", []),
1723
+ configuration_schema=adapter_info.get("configuration", []),
1724
+ requires_external_deps=adapter_type == "discord",
1725
+ external_dependencies={"discord.py": ">=2.0.0"} if adapter_type == "discord" else {},
1726
+ is_mock=False,
1727
+ safe_domain=None,
1728
+ prohibited=[],
1729
+ metadata=None,
1730
+ )
1731
+
1732
+
1733
+ def _check_platform_requirements_satisfied(platform_requirements: List[str]) -> bool:
1734
+ """Check if current platform satisfies the given requirements.
1735
+
1736
+ Args:
1737
+ platform_requirements: List of requirement strings
1738
+
1739
+ Returns:
1740
+ True if platform satisfies all requirements, False otherwise
1741
+ """
1742
+ if not platform_requirements:
1743
+ return True
1744
+
1745
+ from ciris_engine.logic.utils.platform_detection import detect_platform_capabilities
1746
+ from ciris_engine.schemas.platform import PlatformRequirement
1747
+
1748
+ try:
1749
+ caps = detect_platform_capabilities()
1750
+ req_enums = []
1751
+ for req_str in platform_requirements:
1752
+ try:
1753
+ req_enums.append(PlatformRequirement(req_str))
1754
+ except ValueError:
1755
+ pass # Unknown requirement, skip
1756
+ return caps.satisfies(req_enums)
1757
+ except Exception:
1758
+ return False
1759
+
1760
+
1761
+ def _should_filter_adapter(manifest_data: Dict[str, Any], filter_by_platform: bool = True) -> bool:
1762
+ """Check if an adapter should be filtered from public listings.
1763
+
1764
+ Filters out:
1765
+ - Mock adapters (module.MOCK: true)
1766
+ - Library modules (metadata.type: "library")
1767
+ - Modules with no services (empty services array)
1768
+ - Common/utility modules (name ends with _common)
1769
+ - Adapters that don't meet platform requirements (if filter_by_platform=True)
1770
+
1771
+ Args:
1772
+ manifest_data: The manifest JSON data
1773
+ filter_by_platform: If True, also filter adapters that don't meet platform requirements
1774
+
1775
+ Returns:
1776
+ True if the adapter should be filtered (hidden), False otherwise
1777
+ """
1778
+ module_info = manifest_data.get("module", {})
1779
+ metadata = manifest_data.get("metadata", {})
1780
+ services = manifest_data.get("services", [])
1781
+
1782
+ # Filter mock adapters
1783
+ if module_info.get("MOCK", False):
1784
+ return True
1785
+
1786
+ # Filter library modules
1787
+ if isinstance(metadata, dict) and metadata.get("type") == "library":
1788
+ return True
1789
+
1790
+ # Filter modules with no services (utility/common modules)
1791
+ if not services:
1792
+ return True
1793
+
1794
+ # Filter common modules by name pattern
1795
+ module_name = module_info.get("name", "")
1796
+ if module_name.endswith("_common") or module_name.endswith("common"):
1797
+ return True
1798
+
1799
+ # Filter adapters that don't meet platform requirements
1800
+ if filter_by_platform:
1801
+ platform_requirements = manifest_data.get("platform_requirements", [])
1802
+ if not _check_platform_requirements_satisfied(platform_requirements):
1803
+ return True
1804
+
1805
+ return False
1806
+
1807
+
1808
+ def _extract_service_types(manifest_data: Dict[str, Any]) -> List[str]:
1809
+ """Extract unique service types from manifest services list."""
1810
+ service_types = []
1811
+ for svc in manifest_data.get("services", []):
1812
+ svc_type = svc.get("type", "")
1813
+ if svc_type and svc_type not in service_types:
1814
+ service_types.append(svc_type)
1815
+ return service_types
1816
+
1817
+
1818
+ def _parse_config_parameters(manifest_data: Dict[str, Any]) -> List[ModuleConfigParameter]:
1819
+ """Parse configuration parameters from manifest."""
1820
+ config_params: List[ModuleConfigParameter] = []
1821
+ for param_name, param_data in manifest_data.get("configuration", {}).items():
1822
+ if isinstance(param_data, dict):
1823
+ config_params.append(
1824
+ ModuleConfigParameter(
1825
+ name=param_name,
1826
+ param_type=param_data.get("type", "string"),
1827
+ default=param_data.get("default"),
1828
+ description=param_data.get("description", ""),
1829
+ env_var=param_data.get("env"),
1830
+ required=param_data.get("required", True),
1831
+ sensitivity=param_data.get("sensitivity"),
1832
+ )
1833
+ )
1834
+ return config_params
1835
+
1836
+
1837
+ def _parse_manifest_to_module_info(manifest_data: Dict[str, Any], module_id: str) -> ModuleTypeInfo:
1838
+ """Parse a module manifest into a ModuleTypeInfo."""
1839
+ module_info = manifest_data.get("module", {})
1840
+
1841
+ # Extract service types and config params using helpers
1842
+ service_types = _extract_service_types(manifest_data)
1843
+ config_params = _parse_config_parameters(manifest_data)
1844
+
1845
+ # Extract external dependencies
1846
+ deps = manifest_data.get("dependencies", {})
1847
+ external_deps = deps.get("external", {}) if isinstance(deps, dict) else {}
1848
+ external_deps = external_deps or {}
1849
+
1850
+ # Extract metadata
1851
+ metadata = manifest_data.get("metadata", {})
1852
+ safe_domain = metadata.get("safe_domain") if isinstance(metadata, dict) else None
1853
+ prohibited = metadata.get("prohibited", []) if isinstance(metadata, dict) else []
1854
+
1855
+ # Extract platform requirements
1856
+ platform_requirements = manifest_data.get("platform_requirements", [])
1857
+ platform_requirements_rationale = manifest_data.get("platform_requirements_rationale")
1858
+
1859
+ # Check platform availability using shared helper
1860
+ platform_available = _check_platform_requirements_satisfied(platform_requirements)
1861
+
1862
+ return ModuleTypeInfo(
1863
+ module_id=module_id,
1864
+ name=module_info.get("name", module_id),
1865
+ version=module_info.get("version", "1.0.0"),
1866
+ description=module_info.get("description", ""),
1867
+ author=module_info.get("author", "Unknown"),
1868
+ module_source="modular",
1869
+ service_types=service_types,
1870
+ capabilities=manifest_data.get("capabilities", []),
1871
+ configuration_schema=config_params,
1872
+ requires_external_deps=bool(external_deps),
1873
+ external_dependencies=external_deps,
1874
+ is_mock=module_info.get("MOCK", False) or module_info.get("is_mock", False),
1875
+ safe_domain=safe_domain if isinstance(safe_domain, str) else None,
1876
+ prohibited=prohibited if isinstance(prohibited, list) else [],
1877
+ metadata=metadata if isinstance(metadata, dict) else None,
1878
+ platform_requirements=platform_requirements,
1879
+ platform_requirements_rationale=platform_requirements_rationale,
1880
+ platform_available=platform_available,
1881
+ )
1882
+
1883
+
1884
+ # Entry point group for adapter discovery (defined in setup.py)
1885
+ ADAPTER_ENTRY_POINT_GROUP = "ciris.adapters"
1886
+
1887
+
1888
+ async def _read_manifest_async(manifest_path: Path) -> Optional[Dict[str, Any]]:
1889
+ """Read and parse a manifest file asynchronously."""
1890
+ import aiofiles
1891
+
1892
+ try:
1893
+ async with aiofiles.open(manifest_path, mode="r") as f:
1894
+ content = await f.read()
1895
+ result: Dict[str, Any] = json.loads(content)
1896
+ return result
1897
+ except Exception:
1898
+ return None
1899
+
1900
+
1901
+ def _try_load_service_manifest(service_name: str, apply_filter: bool = True) -> Optional[ModuleTypeInfo]:
1902
+ """Try to load a modular service manifest by name.
1903
+
1904
+ Args:
1905
+ service_name: Name of the service to load
1906
+ apply_filter: If True, filter out mock/common/library modules
1907
+
1908
+ Returns:
1909
+ ModuleTypeInfo if found and not filtered, None otherwise
1910
+ """
1911
+ import importlib
1912
+
1913
+ try:
1914
+ submodule = importlib.import_module(f"ciris_adapters.{service_name}")
1915
+ if not hasattr(submodule, "__path__"):
1916
+ return None
1917
+ manifest_file = Path(submodule.__path__[0]) / MANIFEST_FILENAME
1918
+ if not manifest_file.exists():
1919
+ return None
1920
+ with open(manifest_file) as f:
1921
+ manifest_data = json.load(f)
1922
+
1923
+ # Filter out mock/common/library modules from public listings
1924
+ if apply_filter and _should_filter_adapter(manifest_data):
1925
+ logger.debug("Filtering adapter %s from listings (mock/common/library)", service_name)
1926
+ return None
1927
+
1928
+ return _parse_manifest_to_module_info(manifest_data, service_name)
1929
+ except Exception as e:
1930
+ logger.debug("Service %s not available: %s", service_name, e)
1931
+ return None
1932
+
1933
+
1934
+ async def _discover_services_from_directory(services_base: Path) -> List[ModuleTypeInfo]:
1935
+ """Discover modular services by iterating the services directory.
1936
+
1937
+ Filters out mock, common, and library modules from the listing.
1938
+ """
1939
+ adapters: List[ModuleTypeInfo] = []
1940
+
1941
+ for item in services_base.iterdir():
1942
+ if not item.is_dir() or item.name.startswith("_"):
1943
+ continue
1944
+
1945
+ # Try importlib-based loading first (Android compatibility)
1946
+ # Filter is applied inside _try_load_service_manifest
1947
+ module_info = _try_load_service_manifest(item.name)
1948
+ if module_info:
1949
+ adapters.append(module_info)
1950
+ logger.debug("Discovered modular service: %s", item.name)
1951
+ continue
1952
+
1953
+ # Fallback to direct file access
1954
+ manifest_path = item / MANIFEST_FILENAME
1955
+ manifest_data = await _read_manifest_async(manifest_path)
1956
+ if manifest_data:
1957
+ # Apply filter for direct file access path
1958
+ if _should_filter_adapter(manifest_data):
1959
+ logger.debug("Filtering adapter %s from listings (mock/common/library)", item.name)
1960
+ continue
1961
+
1962
+ module_info = _parse_manifest_to_module_info(manifest_data, item.name)
1963
+ adapters.append(module_info)
1964
+ logger.debug("Discovered modular service (direct): %s", item.name)
1965
+
1966
+ return adapters
1967
+
1968
+
1969
+ async def _discover_services_via_entry_points() -> List[ModuleTypeInfo]:
1970
+ """Discover modular services via importlib.metadata entry points.
1971
+
1972
+ This is the preferred discovery method as it works across all platforms
1973
+ including Android where filesystem iteration may fail. Entry points are
1974
+ defined in setup.py under the 'ciris.adapters' group.
1975
+
1976
+ Note: This function is async for API consistency even though the underlying
1977
+ operations are synchronous. This allows uniform await usage in callers.
1978
+ """
1979
+ from importlib.metadata import entry_points
1980
+ from typing import Iterable
1981
+
1982
+ adapters: List[ModuleTypeInfo] = []
1983
+
1984
+ try:
1985
+ # Get entry points - API varies by Python version
1986
+ eps = entry_points()
1987
+
1988
+ # Try the modern API first (Python 3.10+)
1989
+ adapter_eps: Iterable[Any]
1990
+ if hasattr(eps, "select"):
1991
+ # Python 3.10+ with SelectableGroups
1992
+ adapter_eps = eps.select(group=ADAPTER_ENTRY_POINT_GROUP)
1993
+ elif isinstance(eps, dict):
1994
+ # Python 3.9 style dict-like access
1995
+ adapter_eps = eps.get(ADAPTER_ENTRY_POINT_GROUP, [])
1996
+ else:
1997
+ # Fallback - try to iterate or access as needed
1998
+ adapter_eps = getattr(eps, ADAPTER_ENTRY_POINT_GROUP, [])
1999
+
2000
+ for ep in adapter_eps:
2001
+ module_info = _try_load_service_manifest(ep.name)
2002
+ if module_info:
2003
+ adapters.append(module_info)
2004
+ logger.debug("Discovered adapter via entry point: %s", ep.name)
2005
+
2006
+ except Exception as e:
2007
+ logger.warning("Entry point discovery failed: %s", e)
2008
+
2009
+ return adapters
2010
+
2011
+
2012
+ async def _discover_adapters() -> List[ModuleTypeInfo]:
2013
+ """Discover all available modular services.
2014
+
2015
+ Uses a fallback chain:
2016
+ 1. Try filesystem iteration (fastest, works in dev)
2017
+ 2. Fall back to entry points (works on Android and installed packages)
2018
+ """
2019
+ try:
2020
+ import ciris_adapters
2021
+
2022
+ if not hasattr(ciris_adapters, "__path__"):
2023
+ return await _discover_services_via_entry_points()
2024
+
2025
+ services_base = Path(ciris_adapters.__path__[0])
2026
+ logger.debug("Modular services base path: %s", services_base)
2027
+
2028
+ try:
2029
+ return await _discover_services_from_directory(services_base)
2030
+ except OSError as e:
2031
+ logger.debug("iterdir failed (%s), falling back to entry points", e)
2032
+ return await _discover_services_via_entry_points()
2033
+
2034
+ except ImportError as e:
2035
+ logger.debug("ciris_adapters not available: %s", e)
2036
+ return await _discover_services_via_entry_points()
2037
+
2038
+
2039
+ @router.get("/adapters/types", response_model=SuccessResponse[ModuleTypesResponse])
2040
+ async def list_module_types(
2041
+ request: Request, auth: AuthContext = Depends(require_observer)
2042
+ ) -> SuccessResponse[ModuleTypesResponse]:
2043
+ """
2044
+ List all available module/adapter types.
2045
+
2046
+ Returns both core adapters (api, cli, discord) and modular services
2047
+ (mcp_client, mcp_server, reddit, etc.) with their typed configuration schemas.
2048
+
2049
+ This endpoint is useful for:
2050
+ - Dynamic adapter loading UI
2051
+ - Configuration validation
2052
+ - Capability discovery
2053
+
2054
+ Requires OBSERVER role.
2055
+ """
2056
+ try:
2057
+ # Get core adapters
2058
+ core_adapter_types = ["api", "cli", "discord"]
2059
+ core_modules = [_get_core_adapter_info(t) for t in core_adapter_types]
2060
+
2061
+ # Discover modular services
2062
+ adapters = await _discover_adapters()
2063
+
2064
+ response = ModuleTypesResponse(
2065
+ core_modules=core_modules,
2066
+ adapters=adapters,
2067
+ total_core=len(core_modules),
2068
+ total_adapters=len(adapters),
2069
+ )
2070
+
2071
+ return SuccessResponse(data=response)
2072
+
2073
+ except Exception as e:
2074
+ logger.error("Error listing module types: %s", e)
2075
+ raise HTTPException(status_code=500, detail=str(e))
2076
+
2077
+
2078
+ # NOTE: Static routes like /adapters/persisted and /adapters/configurable must come BEFORE
2079
+ # the parametrized route /adapters/{adapter_id} to avoid being captured by the path parameter.
2080
+
2081
+
2082
+ class PersistedConfigsResponse(BaseModel):
2083
+ """Response for persisted adapter configurations."""
2084
+
2085
+ persisted_configs: Dict[str, Dict[str, Any]] = Field(
2086
+ default_factory=dict,
2087
+ description="Map of adapter_type to configuration data",
2088
+ )
2089
+ count: int = Field(..., description="Number of persisted configurations")
2090
+
2091
+
2092
+ @router.get(
2093
+ "/adapters/persisted",
2094
+ response_model=SuccessResponse[PersistedConfigsResponse],
2095
+ )
2096
+ async def list_persisted_configurations(
2097
+ request: Request,
2098
+ auth: AuthContext = Depends(require_admin),
2099
+ ) -> SuccessResponse[PersistedConfigsResponse]:
2100
+ """
2101
+ List all persisted adapter configurations.
2102
+
2103
+ Returns configurations that are set to load on startup.
2104
+
2105
+ Requires ADMIN role.
2106
+ """
2107
+ try:
2108
+ adapter_config_service = getattr(request.app.state, "adapter_configuration_service", None)
2109
+ config_service = getattr(request.app.state, "config_service", None)
2110
+
2111
+ if not adapter_config_service:
2112
+ raise HTTPException(status_code=503, detail=ERROR_ADAPTER_CONFIG_SERVICE_NOT_AVAILABLE)
2113
+
2114
+ persisted_configs: Dict[str, Dict[str, Any]] = {}
2115
+ if config_service:
2116
+ persisted_configs = await adapter_config_service.load_persisted_configs(config_service)
2117
+
2118
+ response = PersistedConfigsResponse(
2119
+ persisted_configs=persisted_configs,
2120
+ count=len(persisted_configs),
2121
+ )
2122
+ return SuccessResponse(data=response)
2123
+
2124
+ except HTTPException:
2125
+ raise
2126
+ except Exception as e:
2127
+ logger.error(f"Error listing persisted configurations: {e}")
2128
+ raise HTTPException(status_code=500, detail=str(e))
2129
+
2130
+
2131
+ class RemovePersistedResponse(BaseModel):
2132
+ """Response for removing a persisted configuration."""
2133
+
2134
+ success: bool = Field(..., description="Whether the removal succeeded")
2135
+ adapter_type: str = Field(..., description="Adapter type that was removed")
2136
+ message: str = Field(..., description="Status message")
2137
+
2138
+
2139
+ @router.delete(
2140
+ "/adapters/{adapter_type}/persisted",
2141
+ response_model=SuccessResponse[RemovePersistedResponse],
2142
+ )
2143
+ async def remove_persisted_configuration(
2144
+ adapter_type: str,
2145
+ request: Request,
2146
+ auth: AuthContext = Depends(require_admin),
2147
+ ) -> SuccessResponse[RemovePersistedResponse]:
2148
+ """
2149
+ Remove a persisted adapter configuration.
2150
+
2151
+ This prevents the adapter from being automatically loaded on startup.
2152
+
2153
+ Requires ADMIN role.
2154
+ """
2155
+ try:
2156
+ adapter_config_service = getattr(request.app.state, "adapter_configuration_service", None)
2157
+ config_service = getattr(request.app.state, "config_service", None)
2158
+
2159
+ if not adapter_config_service:
2160
+ raise HTTPException(status_code=503, detail=ERROR_ADAPTER_CONFIG_SERVICE_NOT_AVAILABLE)
2161
+
2162
+ if not config_service:
2163
+ raise HTTPException(status_code=503, detail="Config service not available")
2164
+
2165
+ success = await adapter_config_service.remove_persisted_config(
2166
+ adapter_type=adapter_type,
2167
+ config_service=config_service,
2168
+ )
2169
+
2170
+ if success:
2171
+ message = f"Removed persisted configuration for {adapter_type}"
2172
+ else:
2173
+ message = f"No persisted configuration found for {adapter_type}"
2174
+
2175
+ response = RemovePersistedResponse(
2176
+ success=success,
2177
+ adapter_type=adapter_type,
2178
+ message=message,
2179
+ )
2180
+ return SuccessResponse(data=response)
2181
+
2182
+ except HTTPException:
2183
+ raise
2184
+ except Exception as e:
2185
+ logger.error(f"Error removing persisted configuration: {e}")
2186
+ raise HTTPException(status_code=500, detail=str(e))
2187
+
2188
+
2189
+ def _get_adapter_config_service(request: Request) -> Any:
2190
+ """Get AdapterConfigurationService from app state."""
2191
+ service = getattr(request.app.state, "adapter_configuration_service", None)
2192
+ if not service:
2193
+ raise HTTPException(status_code=503, detail=ERROR_ADAPTER_CONFIG_SERVICE_NOT_AVAILABLE)
2194
+ return service
2195
+
2196
+
2197
+ @router.get("/adapters/configurable", response_model=SuccessResponse[ConfigurableAdaptersResponse])
2198
+ async def list_configurable_adapters(
2199
+ request: Request, auth: AuthContext = Depends(require_admin)
2200
+ ) -> SuccessResponse[ConfigurableAdaptersResponse]:
2201
+ """
2202
+ List adapters that support interactive configuration.
2203
+
2204
+ Returns information about all adapters that have defined interactive
2205
+ configuration workflows, including their workflow types and step counts.
2206
+
2207
+ Requires ADMIN role.
2208
+ """
2209
+ try:
2210
+ config_service = _get_adapter_config_service(request)
2211
+ adapter_types = config_service.get_configurable_adapters()
2212
+
2213
+ # Build detailed info for each adapter
2214
+ adapters = []
2215
+ for adapter_type in adapter_types:
2216
+ manifest = config_service._adapter_manifests.get(adapter_type)
2217
+ if not manifest:
2218
+ continue
2219
+
2220
+ # Check if any step is OAuth
2221
+ requires_oauth = any(step.step_type == "oauth" for step in manifest.steps)
2222
+
2223
+ adapters.append(
2224
+ ConfigurableAdapterInfo(
2225
+ adapter_type=adapter_type,
2226
+ name=adapter_type.replace("_", " ").title(),
2227
+ description=f"Interactive configuration for {adapter_type}",
2228
+ workflow_type=manifest.workflow_type,
2229
+ step_count=len(manifest.steps),
2230
+ requires_oauth=requires_oauth,
2231
+ steps=[
2232
+ ConfigStepInfo(
2233
+ step_id=step.step_id,
2234
+ step_type=step.step_type,
2235
+ title=step.title,
2236
+ description=step.description,
2237
+ optional=getattr(step, "optional", False),
2238
+ )
2239
+ for step in manifest.steps
2240
+ ],
2241
+ )
2242
+ )
2243
+
2244
+ response = ConfigurableAdaptersResponse(adapters=adapters, total_count=len(adapters))
2245
+ return SuccessResponse(data=response)
2246
+
2247
+ except HTTPException:
2248
+ raise
2249
+ except Exception as e:
2250
+ logger.error(f"Error listing configurable adapters: {e}")
2251
+ raise HTTPException(status_code=500, detail=str(e))
2252
+
2253
+
2254
+ @router.get("/adapters/{adapter_id}", response_model=SuccessResponse[AdapterStatusSchema])
2255
+ async def get_adapter_status(
2256
+ adapter_id: str, request: Request, auth: AuthContext = Depends(require_observer)
2257
+ ) -> SuccessResponse[AdapterStatusSchema]:
2258
+ """
2259
+ Get detailed status of a specific adapter.
2260
+
2261
+ Returns comprehensive information about an adapter instance
2262
+ including configuration, metrics, and service registrations.
2263
+ """
2264
+ runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
2265
+ if not runtime_control:
2266
+ raise HTTPException(status_code=503, detail=ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE)
2267
+
2268
+ try:
2269
+ # Get adapter info from runtime control service
2270
+ adapter_info = await runtime_control.get_adapter_info(adapter_id)
2271
+
2272
+ if not adapter_info:
2273
+ raise HTTPException(status_code=404, detail=f"Adapter '{adapter_id}' not found")
2274
+
2275
+ # Debug logging
2276
+ logger.info(f"Adapter info type: {type(adapter_info)}, value: {adapter_info}")
2277
+
2278
+ # Convert to response format
2279
+ metrics_dict = None
2280
+ if adapter_info.messages_processed > 0 or adapter_info.error_count > 0:
2281
+ metrics = AdapterMetrics(
2282
+ messages_processed=adapter_info.messages_processed,
2283
+ errors_count=adapter_info.error_count,
2284
+ uptime_seconds=(
2285
+ (datetime.now(timezone.utc) - adapter_info.started_at).total_seconds()
2286
+ if adapter_info.started_at
2287
+ else 0
2288
+ ),
2289
+ last_error=adapter_info.last_error,
2290
+ last_error_time=None,
2291
+ )
2292
+ metrics_dict = metrics.__dict__
2293
+
2294
+ status = AdapterStatusSchema(
2295
+ adapter_id=adapter_info.adapter_id,
2296
+ adapter_type=adapter_info.adapter_type,
2297
+ is_running=adapter_info.status == "RUNNING",
2298
+ loaded_at=adapter_info.started_at,
2299
+ services_registered=[], # Not exposed via AdapterInfo
2300
+ config_params=AdapterConfig(adapter_type=adapter_info.adapter_type, enabled=True, settings={}),
2301
+ metrics=metrics_dict,
2302
+ last_activity=None,
2303
+ tools=adapter_info.tools, # Include tools information
2304
+ )
2305
+
2306
+ return SuccessResponse(data=status)
2307
+
2308
+ except HTTPException:
2309
+ raise
2310
+ except Exception as e:
2311
+ logger.error(f"Error getting adapter status: {e}")
2312
+ raise HTTPException(status_code=500, detail=str(e))
2313
+
2314
+
2315
+ @router.post("/adapters/{adapter_type}", response_model=SuccessResponse[AdapterOperationResult])
2316
+ async def load_adapter(
2317
+ adapter_type: str,
2318
+ body: AdapterActionRequest,
2319
+ request: Request,
2320
+ adapter_id: Optional[str] = None,
2321
+ auth: AuthContext = Depends(require_admin),
2322
+ ) -> SuccessResponse[AdapterOperationResult]:
2323
+ """
2324
+ Load a new adapter instance.
2325
+
2326
+ Dynamically loads and starts a new adapter of the specified type.
2327
+ Requires ADMIN role.
2328
+
2329
+ Adapter types: cli, api, discord, mcp, mcp_server
2330
+
2331
+ Args:
2332
+ adapter_type: Type of adapter to load
2333
+ adapter_id: Optional unique ID for the adapter (auto-generated if not provided)
2334
+ """
2335
+ runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
2336
+ if not runtime_control:
2337
+ raise HTTPException(status_code=503, detail=ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE)
2338
+
2339
+ try:
2340
+ # Generate adapter ID if not provided
2341
+ import uuid
2342
+
2343
+ if not adapter_id:
2344
+ adapter_id = f"{adapter_type}_{uuid.uuid4().hex[:8]}"
2345
+
2346
+ logger.info(f"[LOAD_ADAPTER] Loading adapter: type={adapter_type}, id={adapter_id}")
2347
+ logger.debug(f"[LOAD_ADAPTER] Config: {body.config}, auto_start={body.auto_start}")
2348
+
2349
+ result = await runtime_control.load_adapter(
2350
+ adapter_type=adapter_type, adapter_id=adapter_id, config=body.config, auto_start=body.auto_start
2351
+ )
2352
+
2353
+ logger.info(
2354
+ f"[LOAD_ADAPTER] Result: success={result.success}, adapter_id={result.adapter_id}, error={result.error}"
2355
+ )
2356
+
2357
+ # Convert response
2358
+ response = AdapterOperationResult(
2359
+ success=result.success,
2360
+ adapter_id=result.adapter_id,
2361
+ adapter_type=adapter_type,
2362
+ message=result.error if not result.success else f"Adapter {result.adapter_id} loaded successfully",
2363
+ error=result.error,
2364
+ details={"timestamp": result.timestamp.isoformat()},
2365
+ )
2366
+
2367
+ return SuccessResponse(data=response)
2368
+
2369
+ except Exception as e:
2370
+ logger.error(f"[LOAD_ADAPTER] Error loading adapter type={adapter_type}, id={adapter_id}: {e}", exc_info=True)
2371
+ raise HTTPException(status_code=500, detail=str(e))
2372
+
2373
+
2374
+ @router.delete("/adapters/{adapter_id}", response_model=SuccessResponse[AdapterOperationResult])
2375
+ async def unload_adapter(
2376
+ adapter_id: str, request: Request, auth: AuthContext = Depends(require_admin)
2377
+ ) -> SuccessResponse[AdapterOperationResult]:
2378
+ """
2379
+ Unload an adapter instance.
2380
+
2381
+ Stops and removes an adapter from the runtime.
2382
+ Will fail if it's the last communication-capable adapter.
2383
+ Requires ADMIN role.
2384
+ """
2385
+ runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
2386
+ if not runtime_control:
2387
+ raise HTTPException(status_code=503, detail=ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE)
2388
+
2389
+ try:
2390
+ # Unload adapter through runtime control service
2391
+ result = await runtime_control.unload_adapter(
2392
+ adapter_id=adapter_id, force=False # Never force, respect safety checks
2393
+ )
2394
+
2395
+ # Log failures explicitly
2396
+ if not result.success:
2397
+ logger.error(f"Adapter unload failed: {result.error}")
2398
+
2399
+ # Convert response
2400
+ response = AdapterOperationResult(
2401
+ success=result.success,
2402
+ adapter_id=result.adapter_id,
2403
+ adapter_type=result.adapter_type,
2404
+ message=result.error if not result.success else f"Adapter {result.adapter_id} unloaded successfully",
2405
+ error=result.error,
2406
+ details={"timestamp": result.timestamp.isoformat()},
2407
+ )
2408
+
2409
+ return SuccessResponse(data=response)
2410
+
2411
+ except Exception as e:
2412
+ logger.error(f"Error unloading adapter: {e}")
2413
+ raise HTTPException(status_code=500, detail=str(e))
2414
+
2415
+
2416
+ @router.put("/adapters/{adapter_id}/reload", response_model=SuccessResponse[AdapterOperationResult])
2417
+ async def reload_adapter(
2418
+ adapter_id: str, body: AdapterActionRequest, request: Request, auth: AuthContext = Depends(require_admin)
2419
+ ) -> SuccessResponse[AdapterOperationResult]:
2420
+ """
2421
+ Reload an adapter with new configuration.
2422
+
2423
+ Stops the adapter and restarts it with new configuration.
2424
+ Useful for applying configuration changes without full restart.
2425
+ Requires ADMIN role.
2426
+ """
2427
+ runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
2428
+ if not runtime_control:
2429
+ raise HTTPException(status_code=503, detail=ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE)
2430
+
2431
+ try:
2432
+ # Get current adapter info to preserve type
2433
+ adapter_info = await runtime_control.get_adapter_info(adapter_id)
2434
+ if not adapter_info:
2435
+ raise HTTPException(status_code=404, detail=f"Adapter '{adapter_id}' not found")
2436
+
2437
+ # First unload the adapter
2438
+ unload_result = await runtime_control.unload_adapter(adapter_id, force=False)
2439
+ if not unload_result.success:
2440
+ raise HTTPException(status_code=400, detail=f"Failed to unload adapter: {unload_result.error}")
2441
+
2442
+ # Then reload with new config
2443
+ load_result = await runtime_control.load_adapter(
2444
+ adapter_type=adapter_info.adapter_type,
2445
+ adapter_id=adapter_id,
2446
+ config=body.config,
2447
+ auto_start=body.auto_start,
2448
+ )
2449
+
2450
+ # Convert response
2451
+ response = AdapterOperationResult(
2452
+ success=load_result.success,
2453
+ adapter_id=load_result.adapter_id,
2454
+ adapter_type=adapter_info.adapter_type,
2455
+ message=(
2456
+ f"Adapter {adapter_id} reloaded successfully"
2457
+ if load_result.success
2458
+ else f"Reload failed: {load_result.error}"
2459
+ ),
2460
+ error=load_result.error,
2461
+ details={"timestamp": load_result.timestamp.isoformat()},
2462
+ )
2463
+
2464
+ return SuccessResponse(data=response)
2465
+
2466
+ except HTTPException:
2467
+ raise
2468
+ except Exception as e:
2469
+ logger.error(f"Error reloading adapter: {e}")
2470
+ raise HTTPException(status_code=500, detail=str(e))
2471
+
2472
+
2473
+ # Adapter Configuration Workflow Endpoints
2474
+
2475
+
2476
+ @router.post("/adapters/{adapter_type}/configure/start", response_model=SuccessResponse[ConfigurationSessionResponse])
2477
+ async def start_adapter_configuration(
2478
+ adapter_type: str,
2479
+ request: Request,
2480
+ auth: AuthContext = Depends(require_admin),
2481
+ ) -> SuccessResponse[ConfigurationSessionResponse]:
2482
+ """
2483
+ Start interactive configuration session for an adapter.
2484
+
2485
+ Creates a new configuration session and returns the session ID along with
2486
+ information about the first step in the workflow.
2487
+
2488
+ Requires ADMIN role.
2489
+ """
2490
+ try:
2491
+ config_service = _get_adapter_config_service(request)
2492
+
2493
+ # Start the session
2494
+ session = await config_service.start_session(adapter_type=adapter_type, user_id=auth.user_id)
2495
+
2496
+ # Get manifest to access steps
2497
+ manifest = config_service._adapter_manifests.get(adapter_type)
2498
+ if not manifest:
2499
+ raise HTTPException(status_code=404, detail=f"Adapter '{adapter_type}' not found")
2500
+
2501
+ # Get current step
2502
+ current_step = manifest.steps[0] if manifest.steps else None
2503
+
2504
+ response = ConfigurationSessionResponse(
2505
+ session_id=session.session_id,
2506
+ adapter_type=session.adapter_type,
2507
+ status=session.status.value,
2508
+ current_step_index=session.current_step_index,
2509
+ current_step=current_step,
2510
+ total_steps=len(manifest.steps),
2511
+ created_at=session.created_at,
2512
+ )
2513
+
2514
+ return SuccessResponse(data=response)
2515
+
2516
+ except ValueError as e:
2517
+ raise HTTPException(status_code=404, detail=str(e))
2518
+ except HTTPException:
2519
+ raise
2520
+ except Exception as e:
2521
+ logger.error(f"Error starting configuration session: {e}")
2522
+ raise HTTPException(status_code=500, detail=str(e))
2523
+
2524
+
2525
+ @router.get("/adapters/configure/{session_id}", response_model=SuccessResponse[ConfigurationStatusResponse])
2526
+ async def get_configuration_status(
2527
+ session_id: str,
2528
+ request: Request,
2529
+ auth: AuthContext = Depends(require_observer),
2530
+ ) -> SuccessResponse[ConfigurationStatusResponse]:
2531
+ """
2532
+ Get current status of a configuration session.
2533
+
2534
+ Returns complete session state including current step, collected configuration,
2535
+ and session status.
2536
+
2537
+ Requires OBSERVER role.
2538
+ """
2539
+ try:
2540
+ config_service = _get_adapter_config_service(request)
2541
+ session = config_service.get_session(session_id)
2542
+
2543
+ if not session:
2544
+ raise HTTPException(status_code=404, detail=f"Session '{session_id}' not found")
2545
+
2546
+ # Get manifest to access steps
2547
+ manifest = config_service._adapter_manifests.get(session.adapter_type)
2548
+ if not manifest:
2549
+ raise HTTPException(status_code=500, detail=f"Manifest for '{session.adapter_type}' not found")
2550
+
2551
+ # Get current step
2552
+ current_step = None
2553
+ if session.current_step_index < len(manifest.steps):
2554
+ current_step = manifest.steps[session.current_step_index]
2555
+
2556
+ response = ConfigurationStatusResponse(
2557
+ session_id=session.session_id,
2558
+ adapter_type=session.adapter_type,
2559
+ status=session.status.value,
2560
+ current_step_index=session.current_step_index,
2561
+ current_step=current_step,
2562
+ total_steps=len(manifest.steps),
2563
+ collected_config=session.collected_config,
2564
+ created_at=session.created_at,
2565
+ updated_at=session.updated_at,
2566
+ )
2567
+
2568
+ return SuccessResponse(data=response)
2569
+
2570
+ except HTTPException:
2571
+ raise
2572
+ except Exception as e:
2573
+ logger.error(f"Error getting configuration status: {e}")
2574
+ raise HTTPException(status_code=500, detail=str(e))
2575
+
2576
+
2577
+ @router.post("/adapters/configure/{session_id}/step", response_model=SuccessResponse[StepExecutionResponse])
2578
+ async def execute_configuration_step(
2579
+ session_id: str,
2580
+ request: Request,
2581
+ body: StepExecutionRequest = Body(...),
2582
+ auth: AuthContext = Depends(require_admin),
2583
+ ) -> SuccessResponse[StepExecutionResponse]:
2584
+ """
2585
+ Execute the current configuration step.
2586
+
2587
+ The body contains step-specific data such as user selections, input values,
2588
+ or OAuth callback data. The step type determines what data is expected.
2589
+
2590
+ Requires ADMIN role.
2591
+ """
2592
+ try:
2593
+ config_service = _get_adapter_config_service(request)
2594
+
2595
+ # Execute the step
2596
+ result = await config_service.execute_step(session_id, body.step_data)
2597
+
2598
+ response = StepExecutionResponse(
2599
+ step_id=result.step_id,
2600
+ success=result.success,
2601
+ data=result.data,
2602
+ next_step_index=result.next_step_index,
2603
+ error=result.error,
2604
+ awaiting_callback=result.awaiting_callback,
2605
+ )
2606
+
2607
+ return SuccessResponse(data=response)
2608
+
2609
+ except HTTPException:
2610
+ raise
2611
+ except Exception as e:
2612
+ logger.error(f"Error executing configuration step: {e}")
2613
+ raise HTTPException(status_code=500, detail=str(e))
2614
+
2615
+
2616
+ @router.get("/adapters/configure/{session_id}/status")
2617
+ async def get_session_status(
2618
+ session_id: str,
2619
+ request: Request,
2620
+ ) -> SuccessResponse[ConfigurationSessionResponse]:
2621
+ """
2622
+ Get the current status of a configuration session.
2623
+
2624
+ Useful for polling after OAuth callback to check if authentication completed.
2625
+ """
2626
+ try:
2627
+ config_service = _get_adapter_config_service(request)
2628
+ session = config_service.get_session(session_id)
2629
+
2630
+ if not session:
2631
+ raise HTTPException(status_code=404, detail="Session not found")
2632
+
2633
+ # Get adapter steps from the adapter manifest (InteractiveConfiguration)
2634
+ current_step = None
2635
+ total_steps = 0
2636
+ manifest = config_service._adapter_manifests.get(session.adapter_type)
2637
+ if manifest and manifest.steps:
2638
+ steps = manifest.steps
2639
+ total_steps = len(steps)
2640
+ if session.current_step_index < len(steps):
2641
+ # Use the ConfigurationStep directly from the manifest
2642
+ current_step = steps[session.current_step_index]
2643
+
2644
+ response = ConfigurationSessionResponse(
2645
+ session_id=session.session_id,
2646
+ adapter_type=session.adapter_type,
2647
+ status=session.status.value,
2648
+ current_step_index=session.current_step_index,
2649
+ current_step=current_step,
2650
+ total_steps=total_steps,
2651
+ created_at=session.created_at,
2652
+ )
2653
+
2654
+ return SuccessResponse(data=response)
2655
+
2656
+ except HTTPException:
2657
+ raise
2658
+ except Exception as e:
2659
+ logger.error(f"Error getting session status: {e}")
2660
+ raise HTTPException(status_code=500, detail=str(e))
2661
+
2662
+
2663
+ @router.get("/adapters/configure/{session_id}/oauth/callback")
2664
+ async def oauth_callback(
2665
+ session_id: str,
2666
+ code: str,
2667
+ state: str,
2668
+ request: Request,
2669
+ ) -> Response:
2670
+ """
2671
+ Handle OAuth callback from external service.
2672
+
2673
+ This endpoint is called by OAuth providers after user authorization.
2674
+ It processes the authorization code and advances the configuration workflow.
2675
+ Returns HTML that redirects back to the app or shows success message.
2676
+
2677
+ No authentication required (OAuth state validation provides security).
2678
+ """
2679
+ logger.info("=" * 60)
2680
+ logger.info("[OAUTH CALLBACK] *** CALLBACK RECEIVED ***")
2681
+ logger.info(f"[OAUTH CALLBACK] Full URL: {request.url}")
2682
+ logger.info(f"[OAUTH CALLBACK] Path: {request.url.path}")
2683
+ logger.info(f"[OAUTH CALLBACK] session_id: {session_id}")
2684
+ logger.info(f"[OAUTH CALLBACK] state: {state}")
2685
+ logger.info(f"[OAUTH CALLBACK] code length: {len(code)}")
2686
+ logger.info(
2687
+ f"[OAUTH CALLBACK] code preview: {code[:20]}..." if len(code) > 20 else f"[OAUTH CALLBACK] code: {code}"
2688
+ )
2689
+ logger.info(f"[OAUTH CALLBACK] Headers: {dict(request.headers)}")
2690
+ logger.info("=" * 60)
2691
+ try:
2692
+ config_service = _get_adapter_config_service(request)
2693
+
2694
+ # Verify session exists and state matches
2695
+ session = config_service.get_session(session_id)
2696
+ if not session:
2697
+ raise HTTPException(status_code=404, detail="Session not found")
2698
+
2699
+ if state != session_id:
2700
+ raise HTTPException(status_code=400, detail="Invalid OAuth state")
2701
+
2702
+ # Execute the OAuth callback step
2703
+ result = await config_service.execute_step(session_id, {"code": code, "state": state})
2704
+
2705
+ if not result.success:
2706
+ error_html = f"""<!DOCTYPE html>
2707
+ <html>
2708
+ <head><title>OAuth Failed</title></head>
2709
+ <body style="font-family: sans-serif; text-align: center; padding: 50px;">
2710
+ <h1 style="color: #d32f2f;">Authentication Failed</h1>
2711
+ <p>{html.escape(result.error or "OAuth callback failed")}</p>
2712
+ <p>Please close this window and try again in the app.</p>
2713
+ </body>
2714
+ </html>"""
2715
+ return Response(content=error_html, media_type="text/html")
2716
+
2717
+ # Return HTML that tells user to go back to app
2718
+ # Try to use deep link to return to app automatically
2719
+ success_html = f"""<!DOCTYPE html>
2720
+ <html>
2721
+ <head>
2722
+ <title>OAuth Success</title>
2723
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2724
+ </head>
2725
+ <body style="font-family: sans-serif; text-align: center; padding: 50px; background: #f5f5f5;">
2726
+ <div style="background: white; padding: 40px; border-radius: 10px; max-width: 400px; margin: 0 auto; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
2727
+ <h1 style="color: #4caf50; margin-bottom: 20px;">✓ Connected!</h1>
2728
+ <p style="color: #666; font-size: 18px;">Authentication successful.</p>
2729
+ <p style="color: #888; margin-top: 20px;">You can close this window and return to the CIRIS app.</p>
2730
+ <p style="color: #aaa; font-size: 12px; margin-top: 30px;">Session: {html.escape(session_id[:8])}...</p>
2731
+ </div>
2732
+ </body>
2733
+ </html>"""
2734
+ return Response(content=success_html, media_type="text/html")
2735
+
2736
+ except HTTPException:
2737
+ raise
2738
+ except Exception as e:
2739
+ logger.error(f"Error handling OAuth callback: {e}")
2740
+ raise HTTPException(status_code=500, detail=str(e))
2741
+
2742
+
2743
+ @router.get("/adapters/oauth/callback")
2744
+ async def oauth_deeplink_callback(
2745
+ code: str,
2746
+ state: str,
2747
+ request: Request,
2748
+ provider: Optional[str] = None,
2749
+ source: Optional[str] = None,
2750
+ ) -> SuccessResponse[Dict[str, Any]]:
2751
+ """
2752
+ Handle OAuth callback forwarded from Android deep link (ciris://oauth/callback).
2753
+
2754
+ This endpoint receives OAuth callbacks that were forwarded from OAuthCallbackActivity
2755
+ on Android. The Android app uses a deep link (ciris://oauth/callback) to receive
2756
+ the OAuth redirect from the system browser, then forwards to this endpoint.
2757
+
2758
+ This is a generic endpoint that works for any OAuth2 provider (Home Assistant,
2759
+ Discord, Google, Microsoft, Reddit, etc.) - the state parameter contains the
2760
+ session_id which identifies the configuration session.
2761
+
2762
+ Args:
2763
+ code: Authorization code from OAuth provider
2764
+ state: State parameter (contains session_id for session lookup)
2765
+ provider: Optional provider hint (home_assistant, discord, etc.)
2766
+ source: Source of callback (deeplink indicates forwarded from Android)
2767
+
2768
+ Returns:
2769
+ Success response with callback processing result
2770
+ """
2771
+ logger.info("=" * 60)
2772
+ logger.info("[OAUTH DEEPLINK CALLBACK] *** FORWARDED CALLBACK RECEIVED ***")
2773
+ logger.info(f"[OAUTH DEEPLINK CALLBACK] Full URL: {request.url}")
2774
+ logger.info(f"[OAUTH DEEPLINK CALLBACK] state (session_id): {state}")
2775
+ logger.info(f"[OAUTH DEEPLINK CALLBACK] provider: {provider}")
2776
+ logger.info(f"[OAUTH DEEPLINK CALLBACK] source: {source}")
2777
+ logger.info(f"[OAUTH DEEPLINK CALLBACK] code length: {len(code)}")
2778
+ logger.info("=" * 60)
2779
+
2780
+ try:
2781
+ config_service = _get_adapter_config_service(request)
2782
+
2783
+ # The state parameter IS the session_id
2784
+ session_id = state
2785
+
2786
+ # Handle provider-prefixed state (e.g., "ha:actual_session_id")
2787
+ if ":" in state:
2788
+ parts = state.split(":", 1)
2789
+ if len(parts) == 2 and len(parts[0]) < 20:
2790
+ # Looks like "provider:session_id"
2791
+ provider = provider or parts[0]
2792
+ session_id = parts[1]
2793
+ logger.info(f"[OAUTH DEEPLINK CALLBACK] Extracted provider={provider}, session_id={session_id}")
2794
+
2795
+ # Verify session exists
2796
+ session = config_service.get_session(session_id)
2797
+ if not session:
2798
+ logger.error(f"[OAUTH DEEPLINK CALLBACK] Session not found: {session_id}")
2799
+ raise HTTPException(status_code=404, detail=f"Session not found: {session_id}")
2800
+
2801
+ # Execute the OAuth callback step
2802
+ result = await config_service.execute_step(session_id, {"code": code, "state": state})
2803
+
2804
+ if not result.success:
2805
+ logger.error(f"[OAUTH DEEPLINK CALLBACK] OAuth step failed: {result.error}")
2806
+ raise HTTPException(status_code=400, detail=result.error or "OAuth callback failed")
2807
+
2808
+ logger.info(f"[OAUTH DEEPLINK CALLBACK] Successfully processed OAuth callback for session {session_id}")
2809
+
2810
+ return SuccessResponse(
2811
+ data={
2812
+ "session_id": session_id,
2813
+ "success": True,
2814
+ "message": "OAuth callback processed successfully",
2815
+ "next_step": result.next_step_index,
2816
+ }
2817
+ )
2818
+
2819
+ except HTTPException:
2820
+ raise
2821
+ except Exception as e:
2822
+ logger.error(f"[OAUTH DEEPLINK CALLBACK] Error: {e}")
2823
+ raise HTTPException(status_code=500, detail=str(e))
2824
+
2825
+
2826
+ async def _get_runtime_control_service_for_adapter_load(request: Request) -> Any:
2827
+ """Get RuntimeControlService for adapter loading (returns None if unavailable)."""
2828
+ from ciris_engine.schemas.runtime.enums import ServiceType
2829
+
2830
+ runtime_control_service = getattr(request.app.state, "main_runtime_control_service", None)
2831
+ if runtime_control_service:
2832
+ return runtime_control_service
2833
+
2834
+ runtime_control_service = getattr(request.app.state, "runtime_control_service", None)
2835
+ if runtime_control_service:
2836
+ return runtime_control_service
2837
+
2838
+ service_registry = getattr(request.app.state, "service_registry", None)
2839
+ if service_registry:
2840
+ return await service_registry.get_service(handler="api", service_type=ServiceType.RUNTIME_CONTROL)
2841
+
2842
+ return None
2843
+
2844
+
2845
+ async def _load_adapter_after_config(request: Request, session: Any) -> str:
2846
+ """Load adapter after configuration and return status message."""
2847
+ import uuid
2848
+
2849
+ runtime_control_service = await _get_runtime_control_service_for_adapter_load(request)
2850
+ if not runtime_control_service:
2851
+ logger.warning("[COMPLETE_CONFIG] RuntimeControlService not available, adapter not loaded")
2852
+ return " - runtime control service unavailable"
2853
+
2854
+ logger.info("[COMPLETE_CONFIG] Loading adapter via RuntimeControlService.load_adapter")
2855
+ adapter_config = dict(session.collected_config)
2856
+ adapter_id = f"{session.adapter_type}_{uuid.uuid4().hex[:8]}"
2857
+
2858
+ load_result = await runtime_control_service.load_adapter(
2859
+ adapter_type=session.adapter_type,
2860
+ adapter_id=adapter_id,
2861
+ config=adapter_config,
2862
+ )
2863
+
2864
+ if load_result.success:
2865
+ logger.info(f"[COMPLETE_CONFIG] Adapter loaded successfully: {adapter_id}")
2866
+ return f" - adapter '{adapter_id}' loaded and started"
2867
+ else:
2868
+ logger.error(f"[COMPLETE_CONFIG] Adapter load failed: {load_result.error}")
2869
+ return f" - adapter load failed: {load_result.error}"
2870
+
2871
+
2872
+ async def _persist_config_if_requested(
2873
+ body: ConfigurationCompleteRequest, session: Any, adapter_config_service: Any, request: Request
2874
+ ) -> tuple[bool, str]:
2875
+ """Persist configuration if requested. Returns (persisted, message_suffix)."""
2876
+ if not body.persist:
2877
+ return False, ""
2878
+
2879
+ graph_config_service = getattr(request.app.state, "config_service", None)
2880
+ persisted = await adapter_config_service.persist_adapter_config(
2881
+ adapter_type=session.adapter_type,
2882
+ config=session.collected_config,
2883
+ config_service=graph_config_service,
2884
+ )
2885
+ return persisted, " and persisted for startup" if persisted else " (persistence failed)"
2886
+
2887
+
2888
+ @router.post("/adapters/configure/{session_id}/complete", response_model=SuccessResponse[ConfigurationCompleteResponse])
2889
+ async def complete_configuration(
2890
+ session_id: str,
2891
+ request: Request,
2892
+ body: ConfigurationCompleteRequest = Body(default=ConfigurationCompleteRequest()),
2893
+ auth: AuthContext = Depends(require_admin),
2894
+ ) -> SuccessResponse[ConfigurationCompleteResponse]:
2895
+ """
2896
+ Finalize and apply the configuration.
2897
+
2898
+ Validates the collected configuration and applies it to the adapter.
2899
+ Once completed, the adapter should be ready to use with the new configuration.
2900
+
2901
+ If `persist` is True, the configuration will be saved for automatic loading
2902
+ on startup, allowing the adapter to be automatically configured when the
2903
+ system restarts.
2904
+
2905
+ Requires ADMIN role.
2906
+ """
2907
+ try:
2908
+ adapter_config_service = _get_adapter_config_service(request)
2909
+
2910
+ session = adapter_config_service.get_session(session_id)
2911
+ if not session:
2912
+ raise HTTPException(status_code=404, detail=f"Session '{session_id}' not found")
2913
+
2914
+ success = await adapter_config_service.complete_session(session_id)
2915
+ persisted = False
2916
+ message = ""
2917
+
2918
+ if success:
2919
+ message = f"Configuration applied successfully for {session.adapter_type}"
2920
+ logger.info(f"[COMPLETE_CONFIG] Config applied, attempting to start adapter for {session.adapter_type}")
2921
+
2922
+ try:
2923
+ message += await _load_adapter_after_config(request, session)
2924
+ except Exception as e:
2925
+ logger.error(f"Error loading adapter after config: {e}", exc_info=True)
2926
+ message += f" - adapter load error: {e}"
2927
+
2928
+ persisted, persist_msg = await _persist_config_if_requested(body, session, adapter_config_service, request)
2929
+ message += persist_msg
2930
+ else:
2931
+ message = f"Configuration validation or application failed for {session.adapter_type}"
2932
+
2933
+ response = ConfigurationCompleteResponse(
2934
+ success=success,
2935
+ adapter_type=session.adapter_type,
2936
+ message=message,
2937
+ applied_config=session.collected_config if success else {},
2938
+ persisted=persisted,
2939
+ )
2940
+
2941
+ return SuccessResponse(data=response)
2942
+
2943
+ except HTTPException:
2944
+ raise
2945
+ except Exception as e:
2946
+ logger.error(f"Error completing configuration: {e}")
2947
+ raise HTTPException(status_code=500, detail=str(e))
2948
+
2949
+
2950
+ # Tool endpoints
2951
+ @router.get("/tools")
2952
+ async def get_available_tools(request: Request, auth: AuthContext = Depends(require_observer)) -> JSONDict:
2953
+ """
2954
+ Get list of all available tools from all tool providers.
2955
+
2956
+ Returns tools from:
2957
+ - Core tool services (secrets, self_help)
2958
+ - Adapter tool services (API, Discord, etc.)
2959
+
2960
+ Requires OBSERVER role.
2961
+ """
2962
+
2963
+ try:
2964
+ all_tools = []
2965
+ tool_providers = set() # Use set to avoid counting duplicates
2966
+
2967
+ # Get all tool providers from the service registry
2968
+ service_registry = getattr(request.app.state, "service_registry", None)
2969
+ if service_registry:
2970
+ # Get provider info for TOOL services
2971
+ provider_info = service_registry.get_provider_info(service_type=ServiceType.TOOL.value)
2972
+ provider_info.get("services", {}).get(ServiceType.TOOL.value, [])
2973
+
2974
+ # Get the actual provider instances from the registry
2975
+ if hasattr(service_registry, "_services") and ServiceType.TOOL in service_registry._services:
2976
+ for provider_data in service_registry._services[ServiceType.TOOL]:
2977
+ try:
2978
+ provider = provider_data.instance
2979
+ provider_name = provider.__class__.__name__
2980
+ tool_providers.add(provider_name) # Use add to avoid duplicates
2981
+
2982
+ if hasattr(provider, "get_all_tool_info"):
2983
+ # Modern interface with ToolInfo objects
2984
+ tool_infos = await provider.get_all_tool_info()
2985
+ for info in tool_infos:
2986
+ all_tools.append(
2987
+ ToolInfoResponse(
2988
+ name=info.name,
2989
+ description=info.description,
2990
+ provider=provider_name,
2991
+ parameters=info.parameters if hasattr(info, "parameters") else None,
2992
+ category=getattr(info, "category", "general"),
2993
+ cost=getattr(info, "cost", 0.0),
2994
+ when_to_use=getattr(info, "when_to_use", None),
2995
+ )
2996
+ )
2997
+ elif hasattr(provider, "list_tools"):
2998
+ # Legacy interface
2999
+ tool_names = await provider.list_tools()
3000
+ for name in tool_names:
3001
+ all_tools.append(
3002
+ ToolInfoResponse(
3003
+ name=name,
3004
+ description=f"{name} tool",
3005
+ provider=provider_name,
3006
+ parameters=None,
3007
+ category="general",
3008
+ cost=0.0,
3009
+ when_to_use=None,
3010
+ )
3011
+ )
3012
+ except Exception as e:
3013
+ logger.warning(f"Failed to get tools from provider {provider_name}: {e}", exc_info=True)
3014
+
3015
+ # Deduplicate tools by name (in case multiple providers offer the same tool)
3016
+ seen_tools = {}
3017
+ unique_tools = []
3018
+ for tool in all_tools:
3019
+ if tool.name not in seen_tools:
3020
+ seen_tools[tool.name] = tool
3021
+ unique_tools.append(tool)
3022
+ else:
3023
+ # If we see the same tool from multiple providers, add provider info
3024
+ existing = seen_tools[tool.name]
3025
+ if existing.provider != tool.provider:
3026
+ existing.provider = f"{existing.provider}, {tool.provider}"
3027
+
3028
+ # Log provider information for debugging
3029
+ logger.info(f"Tool providers found: {len(tool_providers)} unique providers: {tool_providers}")
3030
+ logger.info(f"Total tools collected: {len(all_tools)}, Unique tools: {len(unique_tools)}")
3031
+ logger.info(f"Tool provider summary: {list(tool_providers)}")
3032
+
3033
+ # Create response with additional metadata for tool providers
3034
+ # Since ResponseMetadata is immutable, we need to create a dict response
3035
+ return {
3036
+ "data": [tool.model_dump() for tool in unique_tools],
3037
+ "metadata": {
3038
+ "timestamp": datetime.now(timezone.utc).isoformat(),
3039
+ "request_id": None,
3040
+ "duration_ms": None,
3041
+ "providers": list(tool_providers),
3042
+ "provider_count": len(tool_providers),
3043
+ "total_tools": len(unique_tools),
3044
+ },
3045
+ }
3046
+
3047
+ except Exception as e:
3048
+ logger.error(f"Error getting available tools: {e}")
3049
+ raise HTTPException(status_code=500, detail=str(e))