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.
- ciris_adapters/README.md +113 -0
- ciris_adapters/__init__.py +30 -0
- ciris_adapters/ciris_covenant_metrics/README.md +144 -0
- ciris_adapters/ciris_covenant_metrics/__init__.py +36 -0
- ciris_adapters/ciris_covenant_metrics/adapter.py +249 -0
- ciris_adapters/ciris_covenant_metrics/manifest.json +152 -0
- ciris_adapters/ciris_covenant_metrics/services.py +403 -0
- ciris_adapters/ciris_hosted_tools/__init__.py +24 -0
- ciris_adapters/ciris_hosted_tools/adapter.py +169 -0
- ciris_adapters/ciris_hosted_tools/manifest.json +94 -0
- ciris_adapters/ciris_hosted_tools/services.py +744 -0
- ciris_adapters/external_data_sql/README.md +559 -0
- ciris_adapters/external_data_sql/__init__.py +43 -0
- ciris_adapters/external_data_sql/adapter.py +144 -0
- ciris_adapters/external_data_sql/configurable.py +315 -0
- ciris_adapters/external_data_sql/dialects/__init__.py +37 -0
- ciris_adapters/external_data_sql/dialects/base.py +133 -0
- ciris_adapters/external_data_sql/dialects/mysql.py +63 -0
- ciris_adapters/external_data_sql/dialects/postgresql.py +59 -0
- ciris_adapters/external_data_sql/dialects/sqlite.py +62 -0
- ciris_adapters/external_data_sql/example_config.json +88 -0
- ciris_adapters/external_data_sql/example_privacy_schema.yaml +127 -0
- ciris_adapters/external_data_sql/manifest.json +195 -0
- ciris_adapters/external_data_sql/privacy_schema_loader.py +189 -0
- ciris_adapters/external_data_sql/protocol.py +101 -0
- ciris_adapters/external_data_sql/schemas.py +146 -0
- ciris_adapters/external_data_sql/service.py +1547 -0
- ciris_adapters/external_data_sql/service_old.py +492 -0
- ciris_adapters/home_assistant/__init__.py +63 -0
- ciris_adapters/home_assistant/adapter.py +201 -0
- ciris_adapters/home_assistant/communication_service.py +347 -0
- ciris_adapters/home_assistant/configurable.py +667 -0
- ciris_adapters/home_assistant/manifest.json +203 -0
- ciris_adapters/home_assistant/schemas.py +129 -0
- ciris_adapters/home_assistant/service.py +751 -0
- ciris_adapters/home_assistant/tool_service.py +441 -0
- ciris_adapters/mcp_client/__init__.py +82 -0
- ciris_adapters/mcp_client/adapter.py +847 -0
- ciris_adapters/mcp_client/config.py +280 -0
- ciris_adapters/mcp_client/configurable.py +422 -0
- ciris_adapters/mcp_client/manifest.json +185 -0
- ciris_adapters/mcp_client/mcp_communication_service.py +393 -0
- ciris_adapters/mcp_client/mcp_tool_service.py +463 -0
- ciris_adapters/mcp_client/mcp_wise_service.py +394 -0
- ciris_adapters/mcp_client/schemas.py +149 -0
- ciris_adapters/mcp_client/security.py +592 -0
- ciris_adapters/mcp_common/__init__.py +44 -0
- ciris_adapters/mcp_common/manifest.json +25 -0
- ciris_adapters/mcp_common/protocol.py +315 -0
- ciris_adapters/mcp_common/schemas.py +225 -0
- ciris_adapters/mcp_server/__init__.py +47 -0
- ciris_adapters/mcp_server/adapter.py +581 -0
- ciris_adapters/mcp_server/config.py +260 -0
- ciris_adapters/mcp_server/configurable.py +393 -0
- ciris_adapters/mcp_server/handlers.py +663 -0
- ciris_adapters/mcp_server/manifest.json +211 -0
- ciris_adapters/mcp_server/security.py +500 -0
- ciris_adapters/mock_llm/README.md +117 -0
- ciris_adapters/mock_llm/__init__.py +21 -0
- ciris_adapters/mock_llm/adapter.py +131 -0
- ciris_adapters/mock_llm/configurable.py +237 -0
- ciris_adapters/mock_llm/manifest.json +106 -0
- ciris_adapters/mock_llm/protocol.py +37 -0
- ciris_adapters/mock_llm/responses.py +520 -0
- ciris_adapters/mock_llm/responses_action_selection.py +1041 -0
- ciris_adapters/mock_llm/responses_epistemic.py +17 -0
- ciris_adapters/mock_llm/responses_feedback.py +27 -0
- ciris_adapters/mock_llm/schemas.py +35 -0
- ciris_adapters/mock_llm/service.py +294 -0
- ciris_adapters/navigation/__init__.py +21 -0
- ciris_adapters/navigation/adapter.py +129 -0
- ciris_adapters/navigation/configurable.py +239 -0
- ciris_adapters/navigation/manifest.json +104 -0
- ciris_adapters/navigation/service.py +487 -0
- ciris_adapters/reddit/README.md +132 -0
- ciris_adapters/reddit/REDDIT_ADAPTER_ANALYSIS.md +715 -0
- ciris_adapters/reddit/REDDIT_ADAPTER_SUMMARY.txt +278 -0
- ciris_adapters/reddit/REDDIT_ANALYSIS_INDEX.md +307 -0
- ciris_adapters/reddit/REDDIT_PRODUCTION_READINESS_PLAN.md +518 -0
- ciris_adapters/reddit/__init__.py +15 -0
- ciris_adapters/reddit/adapter.py +189 -0
- ciris_adapters/reddit/configurable.py +274 -0
- ciris_adapters/reddit/error_handler.py +307 -0
- ciris_adapters/reddit/manifest.json +218 -0
- ciris_adapters/reddit/observer.py +532 -0
- ciris_adapters/reddit/protocol.py +34 -0
- ciris_adapters/reddit/schemas.py +433 -0
- ciris_adapters/reddit/service.py +1471 -0
- ciris_adapters/sample_adapter/README.md +474 -0
- ciris_adapters/sample_adapter/__init__.py +45 -0
- ciris_adapters/sample_adapter/adapter.py +208 -0
- ciris_adapters/sample_adapter/configurable.py +469 -0
- ciris_adapters/sample_adapter/manifest.json +247 -0
- ciris_adapters/sample_adapter/services.py +486 -0
- ciris_adapters/weather/__init__.py +16 -0
- ciris_adapters/weather/adapter.py +130 -0
- ciris_adapters/weather/configurable.py +240 -0
- ciris_adapters/weather/manifest.json +156 -0
- ciris_adapters/weather/service.py +600 -0
- ciris_agent-1.7.7.dist-info/METADATA +284 -0
- ciris_agent-1.7.7.dist-info/RECORD +986 -0
- ciris_agent-1.7.7.dist-info/WHEEL +5 -0
- ciris_agent-1.7.7.dist-info/entry_points.txt +15 -0
- ciris_agent-1.7.7.dist-info/licenses/LICENSE +205 -0
- ciris_agent-1.7.7.dist-info/licenses/NOTICE +82 -0
- ciris_agent-1.7.7.dist-info/top_level.txt +4 -0
- ciris_engine/__init__.py +15 -0
- ciris_engine/ciris_templates/ally.yaml +632 -0
- ciris_engine/ciris_templates/default.yaml +411 -0
- ciris_engine/ciris_templates/echo-core.yaml +629 -0
- ciris_engine/ciris_templates/echo-speculative.yaml +764 -0
- ciris_engine/ciris_templates/echo.yaml +647 -0
- ciris_engine/ciris_templates/sage.yaml +332 -0
- ciris_engine/ciris_templates/scout.yaml +338 -0
- ciris_engine/ciris_templates/test.yaml +168 -0
- ciris_engine/cli.py +42 -0
- ciris_engine/config/CIRIS_SERVICES.json +19 -0
- ciris_engine/config/MODEL_CAPABILITIES.json +419 -0
- ciris_engine/config/PRICING_DATA.json +179 -0
- ciris_engine/config/__init__.py +50 -0
- ciris_engine/config/ciris_services.py +113 -0
- ciris_engine/config/model_capabilities.py +388 -0
- ciris_engine/config/pricing_models.py +276 -0
- ciris_engine/constants.py +35 -0
- ciris_engine/data/__init__.py +1 -0
- ciris_engine/data/covenant_1.0b.txt +978 -0
- ciris_engine/gui_static/11steps.svg +107 -0
- ciris_engine/gui_static/2x-schematics.png +0 -0
- ciris_engine/gui_static/404/index.html +1 -0
- ciris_engine/gui_static/404.html +1 -0
- ciris_engine/gui_static/_next/static/0edhkwDxd5UccTsCmtaBi/_buildManifest.js +1 -0
- ciris_engine/gui_static/_next/static/0edhkwDxd5UccTsCmtaBi/_ssgManifest.js +1 -0
- ciris_engine/gui_static/_next/static/U-3xTQao7hc2wnAi-Uekm/_buildManifest.js +1 -0
- ciris_engine/gui_static/_next/static/U-3xTQao7hc2wnAi-Uekm/_ssgManifest.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/3297-60e86ba0f8a7b040.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/3835-2aad4b7f5f8e4643.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/4499-99a0bc47de0b8975.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/4534-af88cd4ba6e99bff.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/4541-84b455f9e0dc4cfe.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/4789-61412711484754bb.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/6539-c6398bc9d7018430.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/704-8e827b26cc8c2d32.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/704-fb45d630f3192c6f.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/8072-de4952a2e6d2b33f.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/8315-b91d03a3949db0af.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/8386-f93a83ccbd789bd9.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/87c73c54-781a7f35148d5433.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/8903-fefea3339a02d41b.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/9090-e66485adf8d9d990.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/_not-found/page-a67d9808462c23b1.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/api-keys/page-2d7ee1583bbbd02e.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/api-keys/page-6a3c2bae6fe92b7b.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/consent/page-2ed3a035136bc4e8.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/consent/page-b2f5c91844a32422.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/page-25b90f89af3ea58c.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/page-b65d16c94ecaf69c.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/privacy/page-675b6d05c8f9184f.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/privacy/page-cbee2e1c8ab52145.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/settings/page-0f44da06697cf9f0.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/settings/page-563420253577edbf.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/adapters/page-1854631018bc32be.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/agents/page-8353752c176a7c70.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/agents/page-f61a529f110a6040.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/api-demo/page-7f19b9d20d39be28.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/api-demo/page-d1063938f249b8bd.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/audit/page-321b6728b8fff0bb.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/audit/page-ebac35ca961a1277.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/billing/page-6f3dc3bd02924f8e.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/billing/page-fa4a469f814c821a.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/comms/page-0d4f734269addd8f.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/comms/page-79227d426050089c.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/config/page-018d21d683b6e5bc.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/config/page-2aa5a5363ca2a371.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/consent/page-198373205fd316e2.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/consent/page-f2ca39e7713b13f8.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/dashboard/page-1dd5a196f643c60d.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/dashboard/page-530a04d3abbb8cda.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/docs/page-3193b06d094ab654.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/docs/page-330e996dedb87aba.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/layout-0a70f5fc460298b1.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/layout-21f2f99dd5b336e9.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/login/page-33240e6c6034a49d.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/login/page-68ffab6d54a7fdcd.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/logs/page-8a6167aecc4a475c.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/memory/page-9ca8c5d0056de3ff.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/memory/page-e961226941c18f81.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/page-6fdb065a787a4974.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/page-89f87d431be6064a.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/runtime/page-2e728b9c43aa164d.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/runtime/page-c7dd033dc40a72f0.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/services/page-ae9f0bdf11d01a95.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/services/page-b10feb79ca5d75e5.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/sessions/page-13ebe7ef1c16ae11.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/sessions/page-e6c82b16d617f785.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/setup/page-0beb5f5b5a5c20fc.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/setup/page-2595e729eae30c0e.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/status-dashboard/page-1037c987aecc3653.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/status-dashboard/page-2ffd147f6d3162ff.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/system/page-2c5798d58cafcd91.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/system/page-505b1ba4eceb01c3.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/test-auth/page-b0cad31d5cb1b2fa.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/test-auth/page-f3ecd7a8012df230.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/test-login/page-f35117fdc4105801.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/test-login/page-fb583a7924114906.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/test-sdk/page-50f116fd76935563.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/test-sdk/page-c37d8aa5ba623a44.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/tools/page-429aec7a707777ef.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/tools/page-5f705aad60e0c04e.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/users/page-13476b8b0f3808cc.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/users/page-7e500d154ed5bba4.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/wa/page-cc4a9d8a5cb44d08.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/wa/page-ec3e429efbc79230.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/framework-9d29490f5ba089ba.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/main-1f554952e47a82c4.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/main-app-26fa8aed029082e5.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/main-app-97b0486ef6bcef25.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/pages/_app-6ce685456e616eb2.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/pages/_error-d4bce98d93fe21e7.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/webpack-fcebd240b7f8477d.js +1 -0
- ciris_engine/gui_static/_next/static/css/16b94b1fe0cc6e37.css +3 -0
- ciris_engine/gui_static/_next/static/css/77a24ceaae86deff.css +3 -0
- ciris_engine/gui_static/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- ciris_engine/gui_static/_next/static/media/747892c23ea88013-s.woff2 +0 -0
- ciris_engine/gui_static/_next/static/media/8d697b304b401681-s.woff2 +0 -0
- ciris_engine/gui_static/_next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
- ciris_engine/gui_static/_next/static/media/9610d9e46709d722-s.woff2 +0 -0
- ciris_engine/gui_static/_next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
- ciris_engine/gui_static/_next/static/media/d8298875641ec7d4-s.p.woff2 +0 -0
- ciris_engine/gui_static/account/api-keys/index.html +1 -0
- ciris_engine/gui_static/account/api-keys/index.txt +27 -0
- ciris_engine/gui_static/account/consent/index.html +1 -0
- ciris_engine/gui_static/account/consent/index.txt +27 -0
- ciris_engine/gui_static/account/index.html +1 -0
- ciris_engine/gui_static/account/index.txt +27 -0
- ciris_engine/gui_static/account/privacy/index.html +1 -0
- ciris_engine/gui_static/account/privacy/index.txt +27 -0
- ciris_engine/gui_static/account/settings/index.html +1 -0
- ciris_engine/gui_static/account/settings/index.txt +27 -0
- ciris_engine/gui_static/adapters/index.html +1 -0
- ciris_engine/gui_static/adapters/index.txt +27 -0
- ciris_engine/gui_static/agents/index.html +1 -0
- ciris_engine/gui_static/agents/index.txt +27 -0
- ciris_engine/gui_static/andrew-roberts-euBRXcx57T4-unsplash.jpg +0 -0
- ciris_engine/gui_static/api-demo/index.html +1 -0
- ciris_engine/gui_static/api-demo/index.txt +27 -0
- ciris_engine/gui_static/audit/index.html +1 -0
- ciris_engine/gui_static/audit/index.txt +27 -0
- ciris_engine/gui_static/billing/index.html +1 -0
- ciris_engine/gui_static/billing/index.txt +27 -0
- ciris_engine/gui_static/blurryinfo.png +0 -0
- ciris_engine/gui_static/chip-vincent-PkQDwfl9Flc-unsplash.jpg +0 -0
- ciris_engine/gui_static/ciris-architecture.svg +338 -0
- ciris_engine/gui_static/comms/index.html +1 -0
- ciris_engine/gui_static/comms/index.txt +27 -0
- ciris_engine/gui_static/config/index.html +1 -0
- ciris_engine/gui_static/config/index.txt +27 -0
- ciris_engine/gui_static/consent/index.html +1 -0
- ciris_engine/gui_static/consent/index.txt +27 -0
- ciris_engine/gui_static/dashboard/index.html +1 -0
- ciris_engine/gui_static/dashboard/index.txt +27 -0
- ciris_engine/gui_static/docs/index.html +1 -0
- ciris_engine/gui_static/docs/index.txt +27 -0
- ciris_engine/gui_static/eric.png +0 -0
- ciris_engine/gui_static/file.svg +1 -0
- ciris_engine/gui_static/globe.svg +1 -0
- ciris_engine/gui_static/index.html +1 -0
- ciris_engine/gui_static/index.txt +27 -0
- ciris_engine/gui_static/infogfx-1@2x.png +0 -0
- ciris_engine/gui_static/infogfx-2.png +0 -0
- ciris_engine/gui_static/infogfx-dark-1.png +0 -0
- ciris_engine/gui_static/kelly-vohs-soSTXmIxTDU-unsplash.jpg +0 -0
- ciris_engine/gui_static/login/index.html +1 -0
- ciris_engine/gui_static/login/index.txt +27 -0
- ciris_engine/gui_static/logs/index.html +1 -0
- ciris_engine/gui_static/logs/index.txt +27 -0
- ciris_engine/gui_static/memory/index.html +1 -0
- ciris_engine/gui_static/memory/index.txt +27 -0
- ciris_engine/gui_static/nathan-farrish-ArcTfEoBgzs-unsplash.jpg +0 -0
- ciris_engine/gui_static/next.svg +1 -0
- ciris_engine/gui_static/overview.svg +512 -0
- ciris_engine/gui_static/overview1.svg +407 -0
- ciris_engine/gui_static/overview2.svg +370 -0
- ciris_engine/gui_static/pipeline-visualization.svg +278 -0
- ciris_engine/gui_static/privacy-policy.html +160 -0
- ciris_engine/gui_static/runtime/index.html +8 -0
- ciris_engine/gui_static/runtime/index.txt +27 -0
- ciris_engine/gui_static/services/index.html +1 -0
- ciris_engine/gui_static/services/index.txt +27 -0
- ciris_engine/gui_static/sessions/index.html +1 -0
- ciris_engine/gui_static/sessions/index.txt +27 -0
- ciris_engine/gui_static/setup/index.html +1 -0
- ciris_engine/gui_static/setup/index.txt +27 -0
- ciris_engine/gui_static/status-dashboard/index.html +1 -0
- ciris_engine/gui_static/status-dashboard/index.txt +27 -0
- ciris_engine/gui_static/system/index.html +1 -0
- ciris_engine/gui_static/system/index.txt +27 -0
- ciris_engine/gui_static/terms-of-service.html +174 -0
- ciris_engine/gui_static/test-auth/index.html +1 -0
- ciris_engine/gui_static/test-auth/index.txt +27 -0
- ciris_engine/gui_static/test-login/index.html +1 -0
- ciris_engine/gui_static/test-login/index.txt +27 -0
- ciris_engine/gui_static/test-sdk/index.html +1 -0
- ciris_engine/gui_static/test-sdk/index.txt +27 -0
- ciris_engine/gui_static/tools/index.html +1 -0
- ciris_engine/gui_static/tools/index.txt +27 -0
- ciris_engine/gui_static/users/index.html +1 -0
- ciris_engine/gui_static/users/index.txt +27 -0
- ciris_engine/gui_static/vercel.svg +1 -0
- ciris_engine/gui_static/videos/video1.mp4 +0 -0
- ciris_engine/gui_static/videos/video3.mp4 +0 -0
- ciris_engine/gui_static/wa/index.html +1 -0
- ciris_engine/gui_static/wa/index.txt +27 -0
- ciris_engine/gui_static/window.svg +1 -0
- ciris_engine/logic/__init__.py +8 -0
- ciris_engine/logic/adapters/__init__.py +74 -0
- ciris_engine/logic/adapters/api/__init__.py +5 -0
- ciris_engine/logic/adapters/api/adapter.py +1037 -0
- ciris_engine/logic/adapters/api/api_communication.py +370 -0
- ciris_engine/logic/adapters/api/api_document.py +330 -0
- ciris_engine/logic/adapters/api/api_observer.py +24 -0
- ciris_engine/logic/adapters/api/api_runtime_control.py +388 -0
- ciris_engine/logic/adapters/api/api_tools.py +299 -0
- ciris_engine/logic/adapters/api/api_vision.py +215 -0
- ciris_engine/logic/adapters/api/app.py +272 -0
- ciris_engine/logic/adapters/api/auth.py +159 -0
- ciris_engine/logic/adapters/api/config.py +101 -0
- ciris_engine/logic/adapters/api/constants.py +55 -0
- ciris_engine/logic/adapters/api/dependencies/__init__.py +1 -0
- ciris_engine/logic/adapters/api/dependencies/auth.py +260 -0
- ciris_engine/logic/adapters/api/endpoints/__init__.py +1 -0
- ciris_engine/logic/adapters/api/endpoints/emergency.py +86 -0
- ciris_engine/logic/adapters/api/middleware/__init__.py +1 -0
- ciris_engine/logic/adapters/api/middleware/rate_limiter.py +302 -0
- ciris_engine/logic/adapters/api/models.py +29 -0
- ciris_engine/logic/adapters/api/routes/__init__.py +52 -0
- ciris_engine/logic/adapters/api/routes/agent.py +1762 -0
- ciris_engine/logic/adapters/api/routes/audit.py +707 -0
- ciris_engine/logic/adapters/api/routes/auth.py +1745 -0
- ciris_engine/logic/adapters/api/routes/billing.py +895 -0
- ciris_engine/logic/adapters/api/routes/config.py +329 -0
- ciris_engine/logic/adapters/api/routes/connectors.py +534 -0
- ciris_engine/logic/adapters/api/routes/consent.py +637 -0
- ciris_engine/logic/adapters/api/routes/dsar.py +637 -0
- ciris_engine/logic/adapters/api/routes/dsar_multi_source.py +484 -0
- ciris_engine/logic/adapters/api/routes/emergency.py +302 -0
- ciris_engine/logic/adapters/api/routes/memory.py +733 -0
- ciris_engine/logic/adapters/api/routes/memory_filters.py +230 -0
- ciris_engine/logic/adapters/api/routes/memory_models.py +112 -0
- ciris_engine/logic/adapters/api/routes/memory_queries.py +236 -0
- ciris_engine/logic/adapters/api/routes/memory_query_helpers.py +394 -0
- ciris_engine/logic/adapters/api/routes/memory_visualization.py +359 -0
- ciris_engine/logic/adapters/api/routes/memory_visualization_helpers.py +110 -0
- ciris_engine/logic/adapters/api/routes/partnership.py +541 -0
- ciris_engine/logic/adapters/api/routes/setup.py +1374 -0
- ciris_engine/logic/adapters/api/routes/system.py +3049 -0
- ciris_engine/logic/adapters/api/routes/system_extensions.py +952 -0
- ciris_engine/logic/adapters/api/routes/telemetry.py +1987 -0
- ciris_engine/logic/adapters/api/routes/telemetry_converters.py +141 -0
- ciris_engine/logic/adapters/api/routes/telemetry_helpers.py +111 -0
- ciris_engine/logic/adapters/api/routes/telemetry_logs_reader.py +280 -0
- ciris_engine/logic/adapters/api/routes/telemetry_metrics.py +131 -0
- ciris_engine/logic/adapters/api/routes/telemetry_models.py +190 -0
- ciris_engine/logic/adapters/api/routes/telemetry_otlp.py +878 -0
- ciris_engine/logic/adapters/api/routes/telemetry_resource_helpers.py +191 -0
- ciris_engine/logic/adapters/api/routes/tickets.py +541 -0
- ciris_engine/logic/adapters/api/routes/tools.py +556 -0
- ciris_engine/logic/adapters/api/routes/transparency.py +281 -0
- ciris_engine/logic/adapters/api/routes/users.py +981 -0
- ciris_engine/logic/adapters/api/routes/verification.py +373 -0
- ciris_engine/logic/adapters/api/routes/wa.py +369 -0
- ciris_engine/logic/adapters/api/service_configuration.py +177 -0
- ciris_engine/logic/adapters/api/services/__init__.py +1 -0
- ciris_engine/logic/adapters/api/services/auth_service.py +1417 -0
- ciris_engine/logic/adapters/api/services/oauth_security.py +68 -0
- ciris_engine/logic/adapters/base.py +141 -0
- ciris_engine/logic/adapters/base_adapter.py +73 -0
- ciris_engine/logic/adapters/base_observer.py +1141 -0
- ciris_engine/logic/adapters/base_vision.py +312 -0
- ciris_engine/logic/adapters/cirisnode_client.py +307 -0
- ciris_engine/logic/adapters/cli/__init__.py +3 -0
- ciris_engine/logic/adapters/cli/adapter.py +207 -0
- ciris_engine/logic/adapters/cli/cli_adapter.py +902 -0
- ciris_engine/logic/adapters/cli/cli_observer.py +268 -0
- ciris_engine/logic/adapters/cli/cli_tools.py +427 -0
- ciris_engine/logic/adapters/cli/cli_wa_service.py +134 -0
- ciris_engine/logic/adapters/cli/config.py +73 -0
- ciris_engine/logic/adapters/discord/__init__.py +3 -0
- ciris_engine/logic/adapters/discord/adapter.py +783 -0
- ciris_engine/logic/adapters/discord/ciris_discord_client.py +159 -0
- ciris_engine/logic/adapters/discord/config.py +177 -0
- ciris_engine/logic/adapters/discord/constants.py +185 -0
- ciris_engine/logic/adapters/discord/discord-stubs.pyi +50 -0
- ciris_engine/logic/adapters/discord/discord_adapter.py +1584 -0
- ciris_engine/logic/adapters/discord/discord_audit.py +150 -0
- ciris_engine/logic/adapters/discord/discord_channel_manager.py +351 -0
- ciris_engine/logic/adapters/discord/discord_connection_manager.py +313 -0
- ciris_engine/logic/adapters/discord/discord_embed_formatter.py +369 -0
- ciris_engine/logic/adapters/discord/discord_error_classifier.py +302 -0
- ciris_engine/logic/adapters/discord/discord_error_handler.py +316 -0
- ciris_engine/logic/adapters/discord/discord_guidance_handler.py +460 -0
- ciris_engine/logic/adapters/discord/discord_message_handler.py +207 -0
- ciris_engine/logic/adapters/discord/discord_observer.py +670 -0
- ciris_engine/logic/adapters/discord/discord_rate_limiter.py +249 -0
- ciris_engine/logic/adapters/discord/discord_reaction_handler.py +278 -0
- ciris_engine/logic/adapters/discord/discord_tool_handler.py +465 -0
- ciris_engine/logic/adapters/discord/discord_tool_service.py +790 -0
- ciris_engine/logic/adapters/discord/discord_tools.py +90 -0
- ciris_engine/logic/adapters/discord/discord_vision_helper.py +148 -0
- ciris_engine/logic/adapters/discord/py.typed +0 -0
- ciris_engine/logic/adapters/document_parser.py +320 -0
- ciris_engine/logic/audit/__init__.py +10 -0
- ciris_engine/logic/audit/hash_chain.py +313 -0
- ciris_engine/logic/audit/signature_manager.py +352 -0
- ciris_engine/logic/audit/verifier.py +408 -0
- ciris_engine/logic/buses/__init__.py +21 -0
- ciris_engine/logic/buses/base_bus.py +178 -0
- ciris_engine/logic/buses/bus_manager.py +121 -0
- ciris_engine/logic/buses/communication_bus.py +387 -0
- ciris_engine/logic/buses/llm_bus.py +722 -0
- ciris_engine/logic/buses/memory_bus.py +577 -0
- ciris_engine/logic/buses/prohibitions.py +502 -0
- ciris_engine/logic/buses/runtime_control_bus.py +539 -0
- ciris_engine/logic/buses/tool_bus.py +482 -0
- ciris_engine/logic/buses/wise_bus.py +684 -0
- ciris_engine/logic/config/__init__.py +25 -0
- ciris_engine/logic/config/bootstrap.py +255 -0
- ciris_engine/logic/config/config_accessor.py +202 -0
- ciris_engine/logic/config/db_paths.py +194 -0
- ciris_engine/logic/config/env_utils.py +39 -0
- ciris_engine/logic/conscience/__init__.py +16 -0
- ciris_engine/logic/conscience/build_deferral_package.py +0 -0
- ciris_engine/logic/conscience/core.py +688 -0
- ciris_engine/logic/conscience/interface.py +33 -0
- ciris_engine/logic/conscience/registry.py +76 -0
- ciris_engine/logic/conscience/thought_depth_guardrail.py +231 -0
- ciris_engine/logic/conscience/updated_status_conscience.py +156 -0
- ciris_engine/logic/context/__init__.py +10 -0
- ciris_engine/logic/context/batch_context.py +550 -0
- ciris_engine/logic/context/builder.py +149 -0
- ciris_engine/logic/context/channel_resolution.py +136 -0
- ciris_engine/logic/context/secrets_snapshot.py +52 -0
- ciris_engine/logic/context/system_snapshot.py +116 -0
- ciris_engine/logic/context/system_snapshot_helpers.py +1651 -0
- ciris_engine/logic/covenant/__init__.py +33 -0
- ciris_engine/logic/covenant/executor.py +303 -0
- ciris_engine/logic/covenant/extractor.py +382 -0
- ciris_engine/logic/covenant/handler.py +241 -0
- ciris_engine/logic/covenant/verifier.py +383 -0
- ciris_engine/logic/dma/__init__.py +15 -0
- ciris_engine/logic/dma/action_selection/__init__.py +11 -0
- ciris_engine/logic/dma/action_selection/action_instruction_generator.py +444 -0
- ciris_engine/logic/dma/action_selection/context_builder.py +508 -0
- ciris_engine/logic/dma/action_selection/faculty_integration.py +193 -0
- ciris_engine/logic/dma/action_selection/special_cases.py +132 -0
- ciris_engine/logic/dma/action_selection_pdma.py +365 -0
- ciris_engine/logic/dma/base_dma.py +335 -0
- ciris_engine/logic/dma/csdma.py +239 -0
- ciris_engine/logic/dma/dma_executor.py +575 -0
- ciris_engine/logic/dma/dsdma_base.py +410 -0
- ciris_engine/logic/dma/exceptions.py +4 -0
- ciris_engine/logic/dma/factory.py +150 -0
- ciris_engine/logic/dma/pdma.py +120 -0
- ciris_engine/logic/dma/prompt_loader.py +189 -0
- ciris_engine/logic/dma/prompts/action_selection_pdma.yml +58 -0
- ciris_engine/logic/dma/prompts/csdma_common_sense.yml +28 -0
- ciris_engine/logic/dma/prompts/dsdma_base.yml +17 -0
- ciris_engine/logic/dma/prompts/pdma_ethical.yml +42 -0
- ciris_engine/logic/formatters/__init__.py +26 -0
- ciris_engine/logic/formatters/crisis_resources.py +80 -0
- ciris_engine/logic/formatters/escalation.py +21 -0
- ciris_engine/logic/formatters/identity.py +224 -0
- ciris_engine/logic/formatters/prompt_blocks.py +64 -0
- ciris_engine/logic/formatters/system_snapshot.py +193 -0
- ciris_engine/logic/formatters/user_profiles.py +108 -0
- ciris_engine/logic/handlers/__init__.py +1 -0
- ciris_engine/logic/handlers/control/__init__.py +1 -0
- ciris_engine/logic/handlers/control/defer_handler.py +195 -0
- ciris_engine/logic/handlers/control/ponder_handler.py +154 -0
- ciris_engine/logic/handlers/control/reject_handler.py +81 -0
- ciris_engine/logic/handlers/external/__init__.py +1 -0
- ciris_engine/logic/handlers/external/observe_handler.py +154 -0
- ciris_engine/logic/handlers/external/speak_handler.py +250 -0
- ciris_engine/logic/handlers/external/tool_handler.py +148 -0
- ciris_engine/logic/handlers/memory/__init__.py +1 -0
- ciris_engine/logic/handlers/memory/forget_handler.py +107 -0
- ciris_engine/logic/handlers/memory/memorize_handler.py +391 -0
- ciris_engine/logic/handlers/memory/recall_handler.py +213 -0
- ciris_engine/logic/handlers/terminal/__init__.py +1 -0
- ciris_engine/logic/handlers/terminal/task_complete_handler.py +299 -0
- ciris_engine/logic/infrastructure/__init__.py +1 -0
- ciris_engine/logic/infrastructure/handlers/__init__.py +8 -0
- ciris_engine/logic/infrastructure/handlers/action_dispatcher.py +382 -0
- ciris_engine/logic/infrastructure/handlers/base_handler.py +450 -0
- ciris_engine/logic/infrastructure/handlers/exceptions.py +2 -0
- ciris_engine/logic/infrastructure/handlers/handler_registry.py +59 -0
- ciris_engine/logic/infrastructure/handlers/helpers.py +55 -0
- ciris_engine/logic/infrastructure/step_streaming.py +149 -0
- ciris_engine/logic/infrastructure/sub_services/__init__.py +1 -0
- ciris_engine/logic/infrastructure/sub_services/identity_variance_monitor.py +1035 -0
- ciris_engine/logic/infrastructure/sub_services/pattern_analysis_loop.py +758 -0
- ciris_engine/logic/infrastructure/sub_services/wa_cli_bootstrap.py +229 -0
- ciris_engine/logic/infrastructure/sub_services/wa_cli_display.py +176 -0
- ciris_engine/logic/infrastructure/sub_services/wa_cli_oauth.py +404 -0
- ciris_engine/logic/infrastructure/sub_services/wa_cli_wizard.py +181 -0
- ciris_engine/logic/persistence/__init__.py +130 -0
- ciris_engine/logic/persistence/analytics.py +97 -0
- ciris_engine/logic/persistence/db/__init__.py +28 -0
- ciris_engine/logic/persistence/db/core.py +520 -0
- ciris_engine/logic/persistence/db/dialect.py +380 -0
- ciris_engine/logic/persistence/db/execution_helpers.py +216 -0
- ciris_engine/logic/persistence/db/migration_runner.py +191 -0
- ciris_engine/logic/persistence/db/operations.py +313 -0
- ciris_engine/logic/persistence/db/query_builder.py +232 -0
- ciris_engine/logic/persistence/db/retry.py +154 -0
- ciris_engine/logic/persistence/db/setup.py +18 -0
- ciris_engine/logic/persistence/migrations/postgres/001_initial_schema.sql +4 -0
- ciris_engine/logic/persistence/migrations/postgres/002_add_retry_status.sql +3 -0
- ciris_engine/logic/persistence/migrations/postgres/003_add_task_update_tracking.sql +8 -0
- ciris_engine/logic/persistence/migrations/postgres/004_add_occurrence_id.sql +54 -0
- ciris_engine/logic/persistence/migrations/postgres/005_add_consolidation_locks.sql +22 -0
- ciris_engine/logic/persistence/migrations/postgres/006_add_correlation_id_unique_index.sql +16 -0
- ciris_engine/logic/persistence/migrations/postgres/007_add_dsar_tickets.sql +39 -0
- ciris_engine/logic/persistence/migrations/postgres/008_rename_to_tickets_add_sop.sql +123 -0
- ciris_engine/logic/persistence/migrations/postgres/009_add_ticket_status_columns.sql +39 -0
- ciris_engine/logic/persistence/migrations/postgres/010_add_images_to_tasks.sql +5 -0
- ciris_engine/logic/persistence/migrations/sqlite/001_initial_schema.sql +357 -0
- ciris_engine/logic/persistence/migrations/sqlite/002_add_retry_status.sql +3 -0
- ciris_engine/logic/persistence/migrations/sqlite/003_add_task_update_tracking.sql +8 -0
- ciris_engine/logic/persistence/migrations/sqlite/004_add_occurrence_id.sql +45 -0
- ciris_engine/logic/persistence/migrations/sqlite/005_add_consolidation_locks.sql +22 -0
- ciris_engine/logic/persistence/migrations/sqlite/006_add_correlation_id_unique_index.sql +16 -0
- ciris_engine/logic/persistence/migrations/sqlite/007_add_dsar_tickets.sql +39 -0
- ciris_engine/logic/persistence/migrations/sqlite/008_rename_to_tickets_add_sop.sql +120 -0
- ciris_engine/logic/persistence/migrations/sqlite/009_add_ticket_status_columns.sql +129 -0
- ciris_engine/logic/persistence/migrations/sqlite/010_add_images_to_tasks.sql +17 -0
- ciris_engine/logic/persistence/models/__init__.py +141 -0
- ciris_engine/logic/persistence/models/correlations.py +881 -0
- ciris_engine/logic/persistence/models/deferral.py +68 -0
- ciris_engine/logic/persistence/models/dsar.py +286 -0
- ciris_engine/logic/persistence/models/graph.py +362 -0
- ciris_engine/logic/persistence/models/identity.py +264 -0
- ciris_engine/logic/persistence/models/queue_status.py +139 -0
- ciris_engine/logic/persistence/models/tasks.py +1043 -0
- ciris_engine/logic/persistence/models/thoughts.py +400 -0
- ciris_engine/logic/persistence/models/tickets.py +518 -0
- ciris_engine/logic/persistence/stores/__init__.py +13 -0
- ciris_engine/logic/persistence/stores/auth_helpers.py +117 -0
- ciris_engine/logic/persistence/stores/authentication_store.py +414 -0
- ciris_engine/logic/persistence/utils.py +212 -0
- ciris_engine/logic/processors/__init__.py +30 -0
- ciris_engine/logic/processors/core/__init__.py +1 -0
- ciris_engine/logic/processors/core/base_processor.py +280 -0
- ciris_engine/logic/processors/core/main_processor.py +1777 -0
- ciris_engine/logic/processors/core/step_decorators.py +1583 -0
- ciris_engine/logic/processors/core/thought_processor/__init__.py +20 -0
- ciris_engine/logic/processors/core/thought_processor/action_execution.py +49 -0
- ciris_engine/logic/processors/core/thought_processor/conscience_execution.py +382 -0
- ciris_engine/logic/processors/core/thought_processor/finalize_action.py +66 -0
- ciris_engine/logic/processors/core/thought_processor/gather_context.py +120 -0
- ciris_engine/logic/processors/core/thought_processor/main.py +920 -0
- ciris_engine/logic/processors/core/thought_processor/perform_aspdma.py +86 -0
- ciris_engine/logic/processors/core/thought_processor/perform_dmas.py +106 -0
- ciris_engine/logic/processors/core/thought_processor/recursive_processing.py +237 -0
- ciris_engine/logic/processors/core/thought_processor/round_complete.py +52 -0
- ciris_engine/logic/processors/core/thought_processor/start_round.py +64 -0
- ciris_engine/logic/processors/exceptions.py +59 -0
- ciris_engine/logic/processors/states/__init__.py +1 -0
- ciris_engine/logic/processors/states/dream_processor.py +1381 -0
- ciris_engine/logic/processors/states/play_processor.py +141 -0
- ciris_engine/logic/processors/states/shutdown_processor.py +623 -0
- ciris_engine/logic/processors/states/solitude_processor.py +305 -0
- ciris_engine/logic/processors/states/wakeup_processor.py +802 -0
- ciris_engine/logic/processors/states/work_processor.py +742 -0
- ciris_engine/logic/processors/support/__init__.py +1 -0
- ciris_engine/logic/processors/support/dma_orchestrator.py +336 -0
- ciris_engine/logic/processors/support/processing_queue.py +133 -0
- ciris_engine/logic/processors/support/shutdown_condition_evaluator.py +294 -0
- ciris_engine/logic/processors/support/state_manager.py +358 -0
- ciris_engine/logic/processors/support/task_manager.py +303 -0
- ciris_engine/logic/processors/support/thought_escalation.py +116 -0
- ciris_engine/logic/processors/support/thought_manager.py +328 -0
- ciris_engine/logic/processors/support/thought_manager_enhanced.py +105 -0
- ciris_engine/logic/registries/__init__.py +34 -0
- ciris_engine/logic/registries/base.py +653 -0
- ciris_engine/logic/registries/circuit_breaker.py +275 -0
- ciris_engine/logic/registries/typed_registries.py +184 -0
- ciris_engine/logic/runtime/__init__.py +7 -0
- ciris_engine/logic/runtime/adapter_loader.py +261 -0
- ciris_engine/logic/runtime/adapter_manager.py +1053 -0
- ciris_engine/logic/runtime/ciris_runtime.py +2342 -0
- ciris_engine/logic/runtime/ciris_runtime_helpers.py +923 -0
- ciris_engine/logic/runtime/component_builder.py +361 -0
- ciris_engine/logic/runtime/identity_manager.py +219 -0
- ciris_engine/logic/runtime/module_loader.py +207 -0
- ciris_engine/logic/runtime/prevent_sideeffects.py +30 -0
- ciris_engine/logic/runtime/runtime_interface.py +23 -0
- ciris_engine/logic/runtime/service_initializer.py +1623 -0
- ciris_engine/logic/secrets/__init__.py +30 -0
- ciris_engine/logic/secrets/encryption.py +175 -0
- ciris_engine/logic/secrets/filter.py +295 -0
- ciris_engine/logic/secrets/service.py +652 -0
- ciris_engine/logic/secrets/store.py +669 -0
- ciris_engine/logic/services/__init__.py +1 -0
- ciris_engine/logic/services/adaptation/__init__.py +3 -0
- ciris_engine/logic/services/base_graph_service.py +142 -0
- ciris_engine/logic/services/base_infrastructure_service.py +69 -0
- ciris_engine/logic/services/base_scheduled_service.py +136 -0
- ciris_engine/logic/services/base_service.py +247 -0
- ciris_engine/logic/services/governance/__init__.py +3 -0
- ciris_engine/logic/services/governance/adaptive_filter/__init__.py +14 -0
- ciris_engine/logic/services/governance/adaptive_filter/service.py +818 -0
- ciris_engine/logic/services/governance/consent/__init__.py +53 -0
- ciris_engine/logic/services/governance/consent/air.py +403 -0
- ciris_engine/logic/services/governance/consent/decay.py +324 -0
- ciris_engine/logic/services/governance/consent/dsar_automation.py +589 -0
- ciris_engine/logic/services/governance/consent/exceptions.py +106 -0
- ciris_engine/logic/services/governance/consent/metrics.py +270 -0
- ciris_engine/logic/services/governance/consent/partnership.py +533 -0
- ciris_engine/logic/services/governance/consent/service.py +1256 -0
- ciris_engine/logic/services/governance/dsar/__init__.py +29 -0
- ciris_engine/logic/services/governance/dsar/orchestrator.py +977 -0
- ciris_engine/logic/services/governance/dsar/schemas.py +141 -0
- ciris_engine/logic/services/governance/dsar/signature_service.py +283 -0
- ciris_engine/logic/services/governance/self_observation/__init__.py +20 -0
- ciris_engine/logic/services/governance/self_observation/service.py +1153 -0
- ciris_engine/logic/services/governance/visibility/__init__.py +17 -0
- ciris_engine/logic/services/governance/visibility/service.py +512 -0
- ciris_engine/logic/services/governance/wise_authority/__init__.py +15 -0
- ciris_engine/logic/services/governance/wise_authority/service.py +827 -0
- ciris_engine/logic/services/graph/__init__.py +5 -0
- ciris_engine/logic/services/graph/audit_service/__init__.py +5 -0
- ciris_engine/logic/services/graph/audit_service/service.py +1675 -0
- ciris_engine/logic/services/graph/base.py +208 -0
- ciris_engine/logic/services/graph/config_service/__init__.py +5 -0
- ciris_engine/logic/services/graph/config_service/service.py +372 -0
- ciris_engine/logic/services/graph/incident_service/__init__.py +5 -0
- ciris_engine/logic/services/graph/incident_service/service.py +803 -0
- ciris_engine/logic/services/graph/memory_service.py +1120 -0
- ciris_engine/logic/services/graph/telemetry_service/__init__.py +5 -0
- ciris_engine/logic/services/graph/telemetry_service/exceptions.py +104 -0
- ciris_engine/logic/services/graph/telemetry_service/helpers.py +1337 -0
- ciris_engine/logic/services/graph/telemetry_service/service.py +2429 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/__init__.py +17 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/aggregation_helpers.py +355 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/cleanup_helpers.py +438 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/compressor.py +260 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/__init__.py +27 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/audit.py +326 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/conversation.py +291 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/memory.py +197 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/metrics.py +251 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/task.py +257 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/trace.py +363 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/data_converter.py +545 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/date_calculation_helpers.py +193 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/db_query_helpers.py +296 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/edge_helpers.py +92 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/edge_manager.py +896 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/extensive_helpers.py +322 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/period_manager.py +152 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/profound_helpers.py +277 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/query_manager.py +812 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/service.py +1692 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/sql_builders.py +363 -0
- ciris_engine/logic/services/infrastructure/__init__.py +1 -0
- ciris_engine/logic/services/infrastructure/authentication/__init__.py +5 -0
- ciris_engine/logic/services/infrastructure/authentication/service.py +1634 -0
- ciris_engine/logic/services/infrastructure/database_maintenance/__init__.py +15 -0
- ciris_engine/logic/services/infrastructure/database_maintenance/service.py +764 -0
- ciris_engine/logic/services/infrastructure/resource_monitor/__init__.py +7 -0
- ciris_engine/logic/services/infrastructure/resource_monitor/ciris_billing_provider.py +755 -0
- ciris_engine/logic/services/infrastructure/resource_monitor/service.py +409 -0
- ciris_engine/logic/services/infrastructure/resource_monitor/simple_credit_provider.py +129 -0
- ciris_engine/logic/services/lifecycle/__init__.py +3 -0
- ciris_engine/logic/services/lifecycle/initialization/__init__.py +10 -0
- ciris_engine/logic/services/lifecycle/initialization/service.py +312 -0
- ciris_engine/logic/services/lifecycle/scheduler/__init__.py +5 -0
- ciris_engine/logic/services/lifecycle/scheduler/service.py +607 -0
- ciris_engine/logic/services/lifecycle/shutdown/__init__.py +9 -0
- ciris_engine/logic/services/lifecycle/shutdown/service.py +378 -0
- ciris_engine/logic/services/lifecycle/time/__init__.py +15 -0
- ciris_engine/logic/services/lifecycle/time/service.py +259 -0
- ciris_engine/logic/services/memory_service/__init__.py +8 -0
- ciris_engine/logic/services/mixins/__init__.py +13 -0
- ciris_engine/logic/services/mixins/example_usage.py +200 -0
- ciris_engine/logic/services/mixins/request_metrics.py +179 -0
- ciris_engine/logic/services/runtime/__init__.py +3 -0
- ciris_engine/logic/services/runtime/adapter_configuration/__init__.py +16 -0
- ciris_engine/logic/services/runtime/adapter_configuration/service.py +674 -0
- ciris_engine/logic/services/runtime/adapter_configuration/session.py +67 -0
- ciris_engine/logic/services/runtime/control_service/__init__.py +5 -0
- ciris_engine/logic/services/runtime/control_service/service.py +2269 -0
- ciris_engine/logic/services/runtime/llm_service/__init__.py +14 -0
- ciris_engine/logic/services/runtime/llm_service/pricing_calculator.py +279 -0
- ciris_engine/logic/services/runtime/llm_service/service.py +930 -0
- ciris_engine/logic/services/tools/__init__.py +5 -0
- ciris_engine/logic/services/tools/core_tool_service/__init__.py +8 -0
- ciris_engine/logic/services/tools/core_tool_service/service.py +852 -0
- ciris_engine/logic/setup/__init__.py +1 -0
- ciris_engine/logic/setup/first_run.py +250 -0
- ciris_engine/logic/setup/wizard.py +327 -0
- ciris_engine/logic/telemetry/__init__.py +46 -0
- ciris_engine/logic/telemetry/core.py +239 -0
- ciris_engine/logic/telemetry/hot_cold_config.py +133 -0
- ciris_engine/logic/telemetry/log_collector.py +190 -0
- ciris_engine/logic/telemetry/resource_monitor.py +7 -0
- ciris_engine/logic/telemetry/security.py +79 -0
- ciris_engine/logic/utils/__init__.py +18 -0
- ciris_engine/logic/utils/channel_utils.py +75 -0
- ciris_engine/logic/utils/consent/__init__.py +1 -0
- ciris_engine/logic/utils/consent/partnership_utils.py +172 -0
- ciris_engine/logic/utils/constants.py +92 -0
- ciris_engine/logic/utils/context_utils.py +145 -0
- ciris_engine/logic/utils/directory_setup.py +533 -0
- ciris_engine/logic/utils/graphql_context_provider.py +152 -0
- ciris_engine/logic/utils/identity_resolution.py +843 -0
- ciris_engine/logic/utils/incident_capture_handler.py +303 -0
- ciris_engine/logic/utils/initialization_manager.py +74 -0
- ciris_engine/logic/utils/jsondict_helpers.py +290 -0
- ciris_engine/logic/utils/log_sanitizer.py +97 -0
- ciris_engine/logic/utils/logging_config.py +151 -0
- ciris_engine/logic/utils/observability_decorators.py +544 -0
- ciris_engine/logic/utils/occurrence_utils.py +155 -0
- ciris_engine/logic/utils/path_resolution.py +281 -0
- ciris_engine/logic/utils/platform_detection.py +286 -0
- ciris_engine/logic/utils/privacy.py +266 -0
- ciris_engine/logic/utils/profile_loader.py +124 -0
- ciris_engine/logic/utils/profile_manager.py +16 -0
- ciris_engine/logic/utils/runtime_utils.py +69 -0
- ciris_engine/logic/utils/shutdown_manager.py +107 -0
- ciris_engine/logic/utils/task_formatters.py +60 -0
- ciris_engine/logic/utils/task_thought_factory.py +404 -0
- ciris_engine/logic/utils/thought_utils.py +54 -0
- ciris_engine/logic/utils/user_utils.py +70 -0
- ciris_engine/protocols/__init__.py +0 -0
- ciris_engine/protocols/adapters/__init__.py +35 -0
- ciris_engine/protocols/adapters/base.py +149 -0
- ciris_engine/protocols/adapters/configurable.py +265 -0
- ciris_engine/protocols/adapters/message.py +90 -0
- ciris_engine/protocols/audit/__init__.py +1 -0
- ciris_engine/protocols/buses/__init__.py +1 -0
- ciris_engine/protocols/config/__init__.py +1 -0
- ciris_engine/protocols/conscience/__init__.py +1 -0
- ciris_engine/protocols/consent.py +88 -0
- ciris_engine/protocols/context/__init__.py +1 -0
- ciris_engine/protocols/data/__init__.py +1 -0
- ciris_engine/protocols/dma/__init__.py +1 -0
- ciris_engine/protocols/dma/base.py +107 -0
- ciris_engine/protocols/faculties.py +34 -0
- ciris_engine/protocols/formatters/__init__.py +1 -0
- ciris_engine/protocols/handlers/__init__.py +1 -0
- ciris_engine/protocols/infrastructure/__init__.py +25 -0
- ciris_engine/protocols/infrastructure/base.py +377 -0
- ciris_engine/protocols/persistence/__init__.py +1 -0
- ciris_engine/protocols/pipeline_control.py +609 -0
- ciris_engine/protocols/processors/__init__.py +19 -0
- ciris_engine/protocols/processors/agent.py +299 -0
- ciris_engine/protocols/processors/base.py +130 -0
- ciris_engine/protocols/processors/orchestration.py +62 -0
- ciris_engine/protocols/registries/__init__.py +1 -0
- ciris_engine/protocols/runtime/__init__.py +1 -0
- ciris_engine/protocols/runtime/base.py +163 -0
- ciris_engine/protocols/secrets/__init__.py +1 -0
- ciris_engine/protocols/services/__init__.py +80 -0
- ciris_engine/protocols/services/adaptation/__init__.py +7 -0
- ciris_engine/protocols/services/adaptation/self_observation.py +265 -0
- ciris_engine/protocols/services/governance/__init__.py +20 -0
- ciris_engine/protocols/services/governance/communication.py +58 -0
- ciris_engine/protocols/services/governance/filter.py +56 -0
- ciris_engine/protocols/services/governance/visibility.py +32 -0
- ciris_engine/protocols/services/governance/wa_auth.py +192 -0
- ciris_engine/protocols/services/governance/wise_authority.py +75 -0
- ciris_engine/protocols/services/graph/__init__.py +19 -0
- ciris_engine/protocols/services/graph/audit.py +92 -0
- ciris_engine/protocols/services/graph/config.py +54 -0
- ciris_engine/protocols/services/graph/incident_management.py +103 -0
- ciris_engine/protocols/services/graph/memory.py +110 -0
- ciris_engine/protocols/services/graph/telemetry.py +51 -0
- ciris_engine/protocols/services/graph/tsdb_consolidation.py +87 -0
- ciris_engine/protocols/services/infrastructure/__init__.py +11 -0
- ciris_engine/protocols/services/infrastructure/authentication.py +159 -0
- ciris_engine/protocols/services/infrastructure/credit_gate.py +46 -0
- ciris_engine/protocols/services/infrastructure/database_maintenance.py +25 -0
- ciris_engine/protocols/services/infrastructure/resource_monitor.py +83 -0
- ciris_engine/protocols/services/lifecycle/__init__.py +13 -0
- ciris_engine/protocols/services/lifecycle/initialization.py +41 -0
- ciris_engine/protocols/services/lifecycle/scheduler.py +42 -0
- ciris_engine/protocols/services/lifecycle/shutdown.py +50 -0
- ciris_engine/protocols/services/lifecycle/time.py +31 -0
- ciris_engine/protocols/services/runtime/__init__.py +13 -0
- ciris_engine/protocols/services/runtime/llm.py +50 -0
- ciris_engine/protocols/services/runtime/runtime_control.py +193 -0
- ciris_engine/protocols/services/runtime/secrets.py +100 -0
- ciris_engine/protocols/services/runtime/tool.py +123 -0
- ciris_engine/protocols/telemetry/__init__.py +1 -0
- ciris_engine/protocols/utils/__init__.py +1 -0
- ciris_engine/schemas/__init__.py +112 -0
- ciris_engine/schemas/actions/__init__.py +37 -0
- ciris_engine/schemas/actions/parameters.py +137 -0
- ciris_engine/schemas/adapters/__init__.py +13 -0
- ciris_engine/schemas/adapters/cirisnode.py +135 -0
- ciris_engine/schemas/adapters/cli.py +97 -0
- ciris_engine/schemas/adapters/cli_tools.py +98 -0
- ciris_engine/schemas/adapters/discord.py +125 -0
- ciris_engine/schemas/adapters/graphql_core.py +144 -0
- ciris_engine/schemas/adapters/registration.py +47 -0
- ciris_engine/schemas/adapters/runtime_context.py +48 -0
- ciris_engine/schemas/adapters/tool_execution.py +45 -0
- ciris_engine/schemas/adapters/tools.py +96 -0
- ciris_engine/schemas/api/__init__.py +1 -0
- ciris_engine/schemas/api/agent.py +50 -0
- ciris_engine/schemas/api/audit.py +38 -0
- ciris_engine/schemas/api/auth.py +351 -0
- ciris_engine/schemas/api/config_security.py +242 -0
- ciris_engine/schemas/api/emergency.py +111 -0
- ciris_engine/schemas/api/responses.py +72 -0
- ciris_engine/schemas/api/runtime.py +26 -0
- ciris_engine/schemas/api/telemetry.py +109 -0
- ciris_engine/schemas/api/wa.py +90 -0
- ciris_engine/schemas/audit/__init__.py +13 -0
- ciris_engine/schemas/audit/core.py +139 -0
- ciris_engine/schemas/audit/hash_chain.py +58 -0
- ciris_engine/schemas/audit/verification.py +131 -0
- ciris_engine/schemas/buses/__init__.py +1 -0
- ciris_engine/schemas/config/__init__.py +41 -0
- ciris_engine/schemas/config/agent.py +279 -0
- ciris_engine/schemas/config/cognitive_state_behaviors.py +194 -0
- ciris_engine/schemas/config/default_dsar_sops.py +178 -0
- ciris_engine/schemas/config/essential.py +195 -0
- ciris_engine/schemas/config/tickets.py +86 -0
- ciris_engine/schemas/conscience/__init__.py +25 -0
- ciris_engine/schemas/conscience/context.py +34 -0
- ciris_engine/schemas/conscience/core.py +145 -0
- ciris_engine/schemas/conscience/results.py +24 -0
- ciris_engine/schemas/consent/__init__.py +5 -0
- ciris_engine/schemas/consent/core.py +404 -0
- ciris_engine/schemas/context/__init__.py +1 -0
- ciris_engine/schemas/covenant.py +382 -0
- ciris_engine/schemas/data/__init__.py +1 -0
- ciris_engine/schemas/dma/__init__.py +16 -0
- ciris_engine/schemas/dma/core.py +199 -0
- ciris_engine/schemas/dma/faculty.py +192 -0
- ciris_engine/schemas/dma/prompts.py +172 -0
- ciris_engine/schemas/dma/results.py +103 -0
- ciris_engine/schemas/formatters/__init__.py +1 -0
- ciris_engine/schemas/handlers/__init__.py +10 -0
- ciris_engine/schemas/handlers/context.py +119 -0
- ciris_engine/schemas/handlers/contexts.py +100 -0
- ciris_engine/schemas/handlers/core.py +167 -0
- ciris_engine/schemas/handlers/memory_schemas.py +67 -0
- ciris_engine/schemas/handlers/schemas.py +95 -0
- ciris_engine/schemas/identity.py +149 -0
- ciris_engine/schemas/infrastructure/__init__.py +1 -0
- ciris_engine/schemas/infrastructure/base.py +256 -0
- ciris_engine/schemas/infrastructure/behavioral_patterns.py +129 -0
- ciris_engine/schemas/infrastructure/feedback_loop.py +57 -0
- ciris_engine/schemas/infrastructure/identity_variance.py +141 -0
- ciris_engine/schemas/infrastructure/oauth.py +175 -0
- ciris_engine/schemas/infrastructure/wa_cli_wizard.py +54 -0
- ciris_engine/schemas/persistence/__init__.py +34 -0
- ciris_engine/schemas/persistence/core.py +140 -0
- ciris_engine/schemas/persistence/correlations.py +73 -0
- ciris_engine/schemas/persistence/postgres/__init__.py +1 -0
- ciris_engine/schemas/persistence/postgres/tables.py +280 -0
- ciris_engine/schemas/persistence/sqlite/__init__.py +1 -0
- ciris_engine/schemas/persistence/sqlite/tables.py +281 -0
- ciris_engine/schemas/platform.py +149 -0
- ciris_engine/schemas/processors/__init__.py +26 -0
- ciris_engine/schemas/processors/base.py +130 -0
- ciris_engine/schemas/processors/cognitive.py +77 -0
- ciris_engine/schemas/processors/context.py +35 -0
- ciris_engine/schemas/processors/core.py +152 -0
- ciris_engine/schemas/processors/dma.py +105 -0
- ciris_engine/schemas/processors/error.py +122 -0
- ciris_engine/schemas/processors/main.py +109 -0
- ciris_engine/schemas/processors/phase_results.py +21 -0
- ciris_engine/schemas/processors/results.py +99 -0
- ciris_engine/schemas/processors/solitude.py +79 -0
- ciris_engine/schemas/processors/state.py +202 -0
- ciris_engine/schemas/processors/state_example.py +177 -0
- ciris_engine/schemas/processors/states.py +21 -0
- ciris_engine/schemas/processors/status.py +34 -0
- ciris_engine/schemas/registries/__init__.py +1 -0
- ciris_engine/schemas/registries/base.py +66 -0
- ciris_engine/schemas/resources/__init__.py +15 -0
- ciris_engine/schemas/resources/crisis.py +315 -0
- ciris_engine/schemas/runtime/__init__.py +42 -0
- ciris_engine/schemas/runtime/adapter_management.py +186 -0
- ciris_engine/schemas/runtime/api.py +58 -0
- ciris_engine/schemas/runtime/audit.py +50 -0
- ciris_engine/schemas/runtime/bootstrap.py +33 -0
- ciris_engine/schemas/runtime/contexts.py +61 -0
- ciris_engine/schemas/runtime/core.py +161 -0
- ciris_engine/schemas/runtime/enums.py +167 -0
- ciris_engine/schemas/runtime/extended.py +232 -0
- ciris_engine/schemas/runtime/manifest.py +311 -0
- ciris_engine/schemas/runtime/memory.py +60 -0
- ciris_engine/schemas/runtime/messages.py +108 -0
- ciris_engine/schemas/runtime/models.py +156 -0
- ciris_engine/schemas/runtime/processing_context.py +43 -0
- ciris_engine/schemas/runtime/protocols_core.py +96 -0
- ciris_engine/schemas/runtime/resources.py +33 -0
- ciris_engine/schemas/runtime/system_context.py +417 -0
- ciris_engine/schemas/secrets/__init__.py +1 -0
- ciris_engine/schemas/secrets/core.py +267 -0
- ciris_engine/schemas/secrets/service.py +95 -0
- ciris_engine/schemas/services/__init__.py +33 -0
- ciris_engine/schemas/services/audit_summary_node.py +172 -0
- ciris_engine/schemas/services/authority/__init__.py +39 -0
- ciris_engine/schemas/services/authority/jwt.py +158 -0
- ciris_engine/schemas/services/authority/wa_updates.py +138 -0
- ciris_engine/schemas/services/authority/wise_authority.py +163 -0
- ciris_engine/schemas/services/authority_core.py +370 -0
- ciris_engine/schemas/services/capabilities.py +72 -0
- ciris_engine/schemas/services/community_core.py +95 -0
- ciris_engine/schemas/services/context.py +111 -0
- ciris_engine/schemas/services/conversation_summary_node.py +189 -0
- ciris_engine/schemas/services/core/__init__.py +153 -0
- ciris_engine/schemas/services/core/runtime.py +262 -0
- ciris_engine/schemas/services/core/runtime_config.py +117 -0
- ciris_engine/schemas/services/core/secrets.py +65 -0
- ciris_engine/schemas/services/correlation_node.py +179 -0
- ciris_engine/schemas/services/credit_gate.py +92 -0
- ciris_engine/schemas/services/discord_nodes.py +299 -0
- ciris_engine/schemas/services/feedback_core.py +131 -0
- ciris_engine/schemas/services/filters_core.py +270 -0
- ciris_engine/schemas/services/governance.py +26 -0
- ciris_engine/schemas/services/graph/__init__.py +26 -0
- ciris_engine/schemas/services/graph/attributes.py +254 -0
- ciris_engine/schemas/services/graph/audit.py +98 -0
- ciris_engine/schemas/services/graph/consolidation.py +338 -0
- ciris_engine/schemas/services/graph/edge_types.py +43 -0
- ciris_engine/schemas/services/graph/edges.py +88 -0
- ciris_engine/schemas/services/graph/incident.py +312 -0
- ciris_engine/schemas/services/graph/memory.py +84 -0
- ciris_engine/schemas/services/graph/node_data.py +174 -0
- ciris_engine/schemas/services/graph/query_results.py +82 -0
- ciris_engine/schemas/services/graph/telemetry.py +250 -0
- ciris_engine/schemas/services/graph/tsdb_consolidation.py +27 -0
- ciris_engine/schemas/services/graph/tsdb_models.py +107 -0
- ciris_engine/schemas/services/graph_core.py +196 -0
- ciris_engine/schemas/services/graph_typed_nodes.py +194 -0
- ciris_engine/schemas/services/infrastructure/__init__.py +1 -0
- ciris_engine/schemas/services/infrastructure/resource_monitor.py +20 -0
- ciris_engine/schemas/services/lifecycle/__init__.py +9 -0
- ciris_engine/schemas/services/lifecycle/initialization.py +33 -0
- ciris_engine/schemas/services/lifecycle/time.py +50 -0
- ciris_engine/schemas/services/llm.py +187 -0
- ciris_engine/schemas/services/metadata.py +43 -0
- ciris_engine/schemas/services/nodes.py +704 -0
- ciris_engine/schemas/services/operations.py +126 -0
- ciris_engine/schemas/services/requests.py +128 -0
- ciris_engine/schemas/services/resources_core.py +182 -0
- ciris_engine/schemas/services/runtime_control.py +1010 -0
- ciris_engine/schemas/services/shutdown.py +88 -0
- ciris_engine/schemas/services/special/__init__.py +0 -0
- ciris_engine/schemas/services/special/self_observation.py +396 -0
- ciris_engine/schemas/services/trace_summary_node.py +199 -0
- ciris_engine/schemas/services/visibility.py +98 -0
- ciris_engine/schemas/streaming/__init__.py +10 -0
- ciris_engine/schemas/streaming/reasoning_stream.py +95 -0
- ciris_engine/schemas/telemetry/__init__.py +0 -0
- ciris_engine/schemas/telemetry/collector.py +67 -0
- ciris_engine/schemas/telemetry/core.py +252 -0
- ciris_engine/schemas/telemetry/unified.py +59 -0
- ciris_engine/schemas/tools.py +72 -0
- ciris_engine/schemas/types.py +47 -0
- ciris_engine/schemas/utils/__init__.py +1 -0
- ciris_engine/schemas/utils/config_validator.py +54 -0
- ciris_engine/utils/__init__.py +1 -0
- ciris_engine/utils/serialization.py +35 -0
- ciris_sdk/__init__.py +124 -0
- ciris_sdk/auth_store.py +261 -0
- ciris_sdk/client.py +261 -0
- ciris_sdk/exceptions.py +73 -0
- ciris_sdk/model_types.py +258 -0
- ciris_sdk/models.py +354 -0
- ciris_sdk/pagination.py +214 -0
- ciris_sdk/rate_limiter.py +188 -0
- ciris_sdk/setup.py +17 -0
- ciris_sdk/telemetry_models.py +257 -0
- ciris_sdk/telemetry_responses.py +199 -0
- ciris_sdk/transport.py +177 -0
- ciris_sdk/websocket.py +400 -0
- main.py +766 -0
|
@@ -0,0 +1,1417 @@
|
|
|
1
|
+
"""Authentication service for API v2.0.
|
|
2
|
+
|
|
3
|
+
Manages API keys, OAuth users, and authentication state.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import hashlib
|
|
8
|
+
import logging
|
|
9
|
+
import secrets
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
import aiofiles
|
|
15
|
+
import bcrypt
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
from ciris_engine.protocols.services.infrastructure.authentication import AuthenticationServiceProtocol
|
|
20
|
+
from ciris_engine.schemas.api.auth import UserRole
|
|
21
|
+
from ciris_engine.schemas.runtime.api import APIRole
|
|
22
|
+
from ciris_engine.schemas.services.authority.wise_authority import WAUpdate
|
|
23
|
+
from ciris_engine.schemas.services.authority_core import OAuthIdentityLink, WACertificate, WARole
|
|
24
|
+
|
|
25
|
+
# Permission constants to avoid duplication
|
|
26
|
+
PERMISSION_SYSTEM_READ = "system.read"
|
|
27
|
+
PERMISSION_SYSTEM_WRITE = "system.write"
|
|
28
|
+
PERMISSION_MEMORY_READ = "memory.read"
|
|
29
|
+
PERMISSION_MEMORY_WRITE = "memory.write"
|
|
30
|
+
PERMISSION_TELEMETRY_READ = "telemetry.read"
|
|
31
|
+
PERMISSION_CONFIG_READ = "config.read"
|
|
32
|
+
PERMISSION_CONFIG_WRITE = "config.write"
|
|
33
|
+
PERMISSION_AUDIT_READ = "audit.read"
|
|
34
|
+
PERMISSION_AUDIT_WRITE = "audit.write"
|
|
35
|
+
PERMISSION_USERS_READ = "users.read"
|
|
36
|
+
PERMISSION_USERS_WRITE = "users.write"
|
|
37
|
+
PERMISSION_USERS_DELETE = "users.delete"
|
|
38
|
+
PERMISSION_WA_READ = "wa.read"
|
|
39
|
+
PERMISSION_WA_WRITE = "wa.write"
|
|
40
|
+
PERMISSION_WA_MINT = "wa.mint"
|
|
41
|
+
PERMISSION_EMERGENCY_SHUTDOWN = "emergency.shutdown"
|
|
42
|
+
PERMISSION_MANAGE_USER_PERMISSIONS = "manage_user_permissions"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class StoredAPIKey:
|
|
47
|
+
"""Internal representation of an API key."""
|
|
48
|
+
|
|
49
|
+
key_id: str # Unique ID for the key
|
|
50
|
+
key_hash: str
|
|
51
|
+
key_value: str # Masked version for display
|
|
52
|
+
user_id: str
|
|
53
|
+
role: UserRole
|
|
54
|
+
expires_at: Optional[datetime]
|
|
55
|
+
description: Optional[str]
|
|
56
|
+
created_at: datetime
|
|
57
|
+
created_by: str
|
|
58
|
+
last_used: Optional[datetime]
|
|
59
|
+
is_active: bool
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class OAuthUser:
|
|
64
|
+
"""OAuth user information."""
|
|
65
|
+
|
|
66
|
+
user_id: str # Format: provider:external_id
|
|
67
|
+
provider: str
|
|
68
|
+
external_id: str
|
|
69
|
+
email: Optional[str]
|
|
70
|
+
name: Optional[str]
|
|
71
|
+
role: UserRole
|
|
72
|
+
created_at: datetime
|
|
73
|
+
last_login: datetime
|
|
74
|
+
marketing_opt_in: bool = False # User consent for marketing communications
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class User:
|
|
79
|
+
"""Unified user representation combining auth methods and WA status."""
|
|
80
|
+
|
|
81
|
+
wa_id: str # Primary ID (from WA cert)
|
|
82
|
+
name: str
|
|
83
|
+
auth_type: str # "password", "oauth", "api_key"
|
|
84
|
+
api_role: APIRole
|
|
85
|
+
wa_role: Optional[WARole] = None
|
|
86
|
+
oauth_provider: Optional[str] = None
|
|
87
|
+
oauth_email: Optional[str] = None
|
|
88
|
+
oauth_external_id: Optional[str] = None
|
|
89
|
+
created_at: Optional[datetime] = None
|
|
90
|
+
last_login: Optional[datetime] = None
|
|
91
|
+
is_active: bool = True
|
|
92
|
+
wa_parent_id: Optional[str] = None
|
|
93
|
+
wa_auto_minted: bool = False
|
|
94
|
+
password_hash: Optional[str] = None
|
|
95
|
+
custom_permissions: Optional[List[str]] = None # Additional permissions beyond role defaults
|
|
96
|
+
# OAuth profile fields for permission request system
|
|
97
|
+
oauth_name: Optional[str] = None # Full name from OAuth provider
|
|
98
|
+
oauth_picture: Optional[str] = None # Profile picture URL from OAuth provider
|
|
99
|
+
permission_requested_at: Optional[datetime] = None # Timestamp when user requested permissions
|
|
100
|
+
oauth_links: List[OAuthIdentityLink] = field(default_factory=list)
|
|
101
|
+
marketing_opt_in: bool = False # User consent for marketing communications
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class APIAuthService:
|
|
105
|
+
"""Simple in-memory authentication service with database persistence."""
|
|
106
|
+
|
|
107
|
+
# Class-level instance counter to track re-initialization
|
|
108
|
+
_instance_counter = 0
|
|
109
|
+
|
|
110
|
+
def __init__(self, auth_service: Optional[AuthenticationServiceProtocol] = None) -> None:
|
|
111
|
+
# Track instance creation for debugging
|
|
112
|
+
APIAuthService._instance_counter += 1
|
|
113
|
+
self._instance_id = APIAuthService._instance_counter
|
|
114
|
+
logger.debug(
|
|
115
|
+
f"[AUTH SERVICE DEBUG] APIAuthService.__init__ called - INSTANCE #{self._instance_id} created (id={id(self)})"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# In-memory caches for performance
|
|
119
|
+
self._api_keys: Dict[str, StoredAPIKey] = {}
|
|
120
|
+
self._oauth_users: Dict[str, OAuthUser] = {}
|
|
121
|
+
self._users: Dict[str, User] = {}
|
|
122
|
+
|
|
123
|
+
logger.debug(
|
|
124
|
+
f"[AUTH SERVICE DEBUG] Instance #{self._instance_id} - _api_keys initialized as EMPTY dict (id={id(self._api_keys)})"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Store reference to the actual authentication service
|
|
128
|
+
self._auth_service = auth_service
|
|
129
|
+
|
|
130
|
+
# Flag to track if we've loaded users from DB
|
|
131
|
+
self._users_loaded = False
|
|
132
|
+
|
|
133
|
+
# Don't load from DB in __init__ - this causes asyncio.run() errors
|
|
134
|
+
# Instead, we'll load lazily on first access
|
|
135
|
+
if not self._auth_service:
|
|
136
|
+
# Fallback: Initialize with system admin user if no auth service
|
|
137
|
+
now = datetime.now(timezone.utc)
|
|
138
|
+
admin_user = User(
|
|
139
|
+
wa_id="wa-system-admin",
|
|
140
|
+
name="admin",
|
|
141
|
+
auth_type="password",
|
|
142
|
+
api_role=APIRole.SYSTEM_ADMIN,
|
|
143
|
+
wa_role=None, # System admin is not a WA by default
|
|
144
|
+
created_at=now,
|
|
145
|
+
is_active=True,
|
|
146
|
+
password_hash=self._hash_password("ciris_admin_password"),
|
|
147
|
+
)
|
|
148
|
+
self._users[admin_user.wa_id] = admin_user
|
|
149
|
+
self._users_loaded = True
|
|
150
|
+
|
|
151
|
+
async def _ensure_users_loaded(self) -> None:
|
|
152
|
+
"""Ensure users are loaded from database (lazy loading)."""
|
|
153
|
+
if self._users_loaded:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
await self._load_users_from_db()
|
|
157
|
+
self._users_loaded = True
|
|
158
|
+
|
|
159
|
+
async def reload_users_from_db(self) -> None:
|
|
160
|
+
"""Force reload users from database, invalidating cache.
|
|
161
|
+
|
|
162
|
+
Use this after external changes to the user database (e.g., setup wizard).
|
|
163
|
+
"""
|
|
164
|
+
self._users_loaded = False
|
|
165
|
+
await self._ensure_users_loaded()
|
|
166
|
+
|
|
167
|
+
def _update_existing_oauth_user(self, oauth_user_id: str, wa: "WACertificate") -> None:
|
|
168
|
+
"""Update existing OAuth user record with WA info."""
|
|
169
|
+
existing_user = self._users[oauth_user_id]
|
|
170
|
+
existing_user.wa_role = wa.role
|
|
171
|
+
existing_user.wa_id = wa.wa_id
|
|
172
|
+
existing_user.wa_parent_id = wa.parent_wa_id
|
|
173
|
+
existing_user.wa_auto_minted = wa.auto_minted
|
|
174
|
+
existing_user.api_role = self._wa_role_to_api_role(wa.role)
|
|
175
|
+
|
|
176
|
+
def _create_user_from_wa(self, wa: "WACertificate") -> User:
|
|
177
|
+
"""Convert WA certificate to User."""
|
|
178
|
+
# Extract email from oauth_links if available
|
|
179
|
+
oauth_email = None
|
|
180
|
+
if wa.oauth_links:
|
|
181
|
+
logger.debug(f"[AUTH DEBUG] Found {len(wa.oauth_links)} OAuth links for {wa.wa_id}")
|
|
182
|
+
for i, link in enumerate(wa.oauth_links):
|
|
183
|
+
logger.debug(
|
|
184
|
+
f"[AUTH DEBUG] Link {i}: provider={link.provider}, external_id={link.external_id}, metadata={link.metadata}"
|
|
185
|
+
)
|
|
186
|
+
# Check if link has email in metadata or as direct attribute
|
|
187
|
+
if hasattr(link, "email") and link.email:
|
|
188
|
+
oauth_email = link.email
|
|
189
|
+
masked = oauth_email[:3] + "***" if oauth_email else "None" # NOSONAR - masked email for debug
|
|
190
|
+
logger.debug(f"[AUTH DEBUG] Extracted email from link.email: {masked}")
|
|
191
|
+
break
|
|
192
|
+
elif hasattr(link, "metadata") and isinstance(link.metadata, dict):
|
|
193
|
+
if "email" in link.metadata:
|
|
194
|
+
oauth_email = link.metadata["email"]
|
|
195
|
+
masked = oauth_email[:3] + "***" if oauth_email else "None" # NOSONAR - masked email for debug
|
|
196
|
+
logger.debug(f"[AUTH DEBUG] Extracted email from link.metadata['email']: {masked}")
|
|
197
|
+
break
|
|
198
|
+
else:
|
|
199
|
+
logger.debug(f"[AUTH DEBUG] No OAuth links found for {wa.wa_id}")
|
|
200
|
+
|
|
201
|
+
return User(
|
|
202
|
+
wa_id=wa.wa_id,
|
|
203
|
+
name=wa.name,
|
|
204
|
+
auth_type="password" if wa.password_hash else "certificate",
|
|
205
|
+
api_role=self._wa_role_to_api_role(wa.role),
|
|
206
|
+
wa_role=wa.role,
|
|
207
|
+
created_at=wa.created_at,
|
|
208
|
+
last_login=wa.last_auth,
|
|
209
|
+
is_active=True, # Assume active if in database
|
|
210
|
+
wa_parent_id=wa.parent_wa_id,
|
|
211
|
+
wa_auto_minted=wa.auto_minted,
|
|
212
|
+
password_hash=wa.password_hash,
|
|
213
|
+
oauth_provider=wa.oauth_provider,
|
|
214
|
+
oauth_email=oauth_email, # Extract from oauth_links
|
|
215
|
+
oauth_external_id=wa.oauth_external_id,
|
|
216
|
+
custom_permissions=wa.custom_permissions if hasattr(wa, "custom_permissions") else None,
|
|
217
|
+
oauth_links=list(wa.oauth_links),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
async def _process_wa_record(self, wa: "WACertificate") -> None:
|
|
221
|
+
"""Process a single WA record and add/update user."""
|
|
222
|
+
logger.debug(f"[AUTH DEBUG] _process_wa_record: wa_id={wa.wa_id}, name={wa.name}")
|
|
223
|
+
|
|
224
|
+
# Remove stale cache entries for this WA
|
|
225
|
+
to_remove = [key for key, value in self._users.items() if getattr(value, "wa_id", None) == wa.wa_id]
|
|
226
|
+
if to_remove:
|
|
227
|
+
logger.debug(f"[AUTH DEBUG] Removing {len(to_remove)} stale entries for {wa.wa_id}")
|
|
228
|
+
for key in to_remove:
|
|
229
|
+
self._users.pop(key, None)
|
|
230
|
+
|
|
231
|
+
user = self._create_user_from_wa(wa)
|
|
232
|
+
logger.debug(
|
|
233
|
+
f"[AUTH DEBUG] Created User: name={user.name}, auth_type={user.auth_type}, has_password={user.password_hash is not None}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
self._users[wa.wa_id] = user
|
|
237
|
+
logger.debug(f"[AUTH DEBUG] Stored user under key: '{wa.wa_id}'")
|
|
238
|
+
|
|
239
|
+
if wa.oauth_provider and wa.oauth_external_id:
|
|
240
|
+
primary_key = f"{wa.oauth_provider}:{wa.oauth_external_id}"
|
|
241
|
+
self._users[primary_key] = user
|
|
242
|
+
logger.debug(f"[AUTH DEBUG] Stored user under OAuth key: '{primary_key}'")
|
|
243
|
+
# Clear from _oauth_users cache - DB record is authoritative
|
|
244
|
+
if primary_key in self._oauth_users:
|
|
245
|
+
logger.debug(f"[AUTH DEBUG] Clearing stale _oauth_users entry: '{primary_key}'")
|
|
246
|
+
del self._oauth_users[primary_key]
|
|
247
|
+
|
|
248
|
+
for link in wa.oauth_links:
|
|
249
|
+
link_key = f"{link.provider}:{link.external_id}"
|
|
250
|
+
self._users[link_key] = user
|
|
251
|
+
logger.debug(f"[AUTH DEBUG] Stored user under link key: '{link_key}'")
|
|
252
|
+
|
|
253
|
+
async def _load_users_from_db(self) -> None:
|
|
254
|
+
"""Load existing users from the database."""
|
|
255
|
+
logger.info("=" * 70)
|
|
256
|
+
logger.info("CIRIS_USER_CREATE: _load_users_from_db() called")
|
|
257
|
+
logger.info("=" * 70)
|
|
258
|
+
|
|
259
|
+
if not self._auth_service:
|
|
260
|
+
logger.info("CIRIS_USER_CREATE: No auth service - skipping DB load")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
was = await self._auth_service.list_was(active_only=False)
|
|
265
|
+
logger.info(f"CIRIS_USER_CREATE: Loaded {len(was)} WA certificates from database")
|
|
266
|
+
|
|
267
|
+
for i, wa in enumerate(was, 1):
|
|
268
|
+
logger.info(
|
|
269
|
+
f"CIRIS_USER_CREATE: Processing WA {i}/{len(was)}: wa_id={wa.wa_id}, name={wa.name}, role={wa.role}"
|
|
270
|
+
)
|
|
271
|
+
await self._process_wa_record(wa)
|
|
272
|
+
|
|
273
|
+
# Check if we need to create a default admin
|
|
274
|
+
# Skip if:
|
|
275
|
+
# 1. Any user named 'admin' exists, OR
|
|
276
|
+
# 2. Any ROOT user exists (setup wizard creates ROOT user with custom name)
|
|
277
|
+
has_admin_user = any(u.name == "admin" for u in self._users.values())
|
|
278
|
+
has_root_user = any(u.wa_role == WARole.ROOT for u in self._users.values())
|
|
279
|
+
|
|
280
|
+
logger.info(f"CIRIS_USER_CREATE: Check default admin: has_admin={has_admin_user}, has_root={has_root_user}")
|
|
281
|
+
|
|
282
|
+
if not has_admin_user and not has_root_user:
|
|
283
|
+
logger.info("CIRIS_USER_CREATE: No admin/ROOT user found - will create default admin")
|
|
284
|
+
await self._create_default_admin()
|
|
285
|
+
else:
|
|
286
|
+
logger.info("CIRIS_USER_CREATE: Skipping default admin creation - admin or ROOT already exists")
|
|
287
|
+
|
|
288
|
+
# Clear the fallback admin if it wasn't loaded from the database
|
|
289
|
+
# The fallback admin is only meant for when there's no auth_service
|
|
290
|
+
# If wa-system-admin is in the DB, it's a real user and should be kept
|
|
291
|
+
loaded_wa_ids = {wa.wa_id for wa in was}
|
|
292
|
+
if "wa-system-admin" in self._users and "wa-system-admin" not in loaded_wa_ids:
|
|
293
|
+
logger.info("CIRIS_USER_CREATE: Removing fallback 'wa-system-admin' - not in DB, real users loaded")
|
|
294
|
+
del self._users["wa-system-admin"]
|
|
295
|
+
|
|
296
|
+
logger.info(f"CIRIS_USER_CREATE: User loading complete. Total users in cache: {len(self._users)}")
|
|
297
|
+
unique_users = {u.wa_id: u for u in self._users.values()}
|
|
298
|
+
for wa_id, user in unique_users.items():
|
|
299
|
+
logger.info(
|
|
300
|
+
f"CIRIS_USER_CREATE: - {wa_id}: name={user.name}, wa_role={user.wa_role}, api_role={user.api_role}"
|
|
301
|
+
)
|
|
302
|
+
logger.info("=" * 70)
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.error(f"CIRIS_USER_CREATE: Error loading users from database: {e}", exc_info=True)
|
|
306
|
+
raise
|
|
307
|
+
|
|
308
|
+
async def _create_default_admin(self) -> None:
|
|
309
|
+
"""Create the default admin user in the database.
|
|
310
|
+
|
|
311
|
+
NOTE: This is only called if no user named 'admin' exists in the database.
|
|
312
|
+
During first-run setup, the setup wizard creates the ROOT user, so this
|
|
313
|
+
should NOT be called in that flow.
|
|
314
|
+
"""
|
|
315
|
+
if not self._auth_service:
|
|
316
|
+
logger.info("CIRIS_USER_CREATE: _create_default_admin skipped - no auth_service")
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
logger.info("=" * 70)
|
|
320
|
+
logger.info("CIRIS_USER_CREATE: _create_default_admin() called")
|
|
321
|
+
logger.info("=" * 70)
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
# Check existing WAs before creating admin
|
|
325
|
+
existing_was = await self._auth_service.list_was(active_only=False)
|
|
326
|
+
logger.info(f"CIRIS_USER_CREATE: Existing WAs before default admin: {len(existing_was)}")
|
|
327
|
+
for wa in existing_was:
|
|
328
|
+
logger.info(f"CIRIS_USER_CREATE: - {wa.wa_id}: name={wa.name}, role={wa.role}")
|
|
329
|
+
|
|
330
|
+
# Check if any ROOT user already exists - DON'T create another one
|
|
331
|
+
root_was = [wa for wa in existing_was if wa.role == WARole.ROOT]
|
|
332
|
+
if root_was:
|
|
333
|
+
logger.info(
|
|
334
|
+
f"CIRIS_USER_CREATE: ROOT WA already exists ({root_was[0].wa_id}) - skipping default admin creation"
|
|
335
|
+
)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
logger.info("CIRIS_USER_CREATE: No ROOT WA exists - creating default admin")
|
|
339
|
+
|
|
340
|
+
# Create admin WA certificate
|
|
341
|
+
wa_cert = await self._auth_service.create_wa(
|
|
342
|
+
name="admin",
|
|
343
|
+
email="admin@ciris.local",
|
|
344
|
+
scopes=["*"], # All permissions
|
|
345
|
+
role=WARole.ROOT, # System admin gets ROOT role
|
|
346
|
+
)
|
|
347
|
+
logger.info(f"CIRIS_USER_CREATE: ✅ Created default admin WA: {wa_cert.wa_id}")
|
|
348
|
+
|
|
349
|
+
# Update with password hash
|
|
350
|
+
await self._auth_service.update_wa(
|
|
351
|
+
wa_cert.wa_id, updates=None, password_hash=self._hash_password("ciris_admin_password")
|
|
352
|
+
)
|
|
353
|
+
logger.info(f"CIRIS_USER_CREATE: Password set for default admin: {wa_cert.wa_id}")
|
|
354
|
+
|
|
355
|
+
# Add to cache
|
|
356
|
+
admin_user = User(
|
|
357
|
+
wa_id=wa_cert.wa_id,
|
|
358
|
+
name="admin",
|
|
359
|
+
auth_type="password",
|
|
360
|
+
api_role=APIRole.SYSTEM_ADMIN,
|
|
361
|
+
wa_role=WARole.ROOT,
|
|
362
|
+
created_at=wa_cert.created_at,
|
|
363
|
+
is_active=True,
|
|
364
|
+
password_hash=self._hash_password("ciris_admin_password"),
|
|
365
|
+
)
|
|
366
|
+
self._users[admin_user.wa_id] = admin_user
|
|
367
|
+
logger.info(f"CIRIS_USER_CREATE: Added default admin to user cache")
|
|
368
|
+
|
|
369
|
+
except Exception as e:
|
|
370
|
+
logger.error(f"CIRIS_USER_CREATE: Error creating default admin: {e}", exc_info=True)
|
|
371
|
+
|
|
372
|
+
def _wa_role_to_api_role(self, wa_role: Optional[WARole]) -> APIRole:
|
|
373
|
+
"""Convert WA role to API role."""
|
|
374
|
+
if not wa_role:
|
|
375
|
+
return APIRole.OBSERVER
|
|
376
|
+
|
|
377
|
+
role_map = {
|
|
378
|
+
WARole.ROOT: APIRole.SYSTEM_ADMIN,
|
|
379
|
+
WARole.AUTHORITY: APIRole.AUTHORITY,
|
|
380
|
+
WARole.OBSERVER: APIRole.OBSERVER,
|
|
381
|
+
}
|
|
382
|
+
return role_map.get(wa_role, APIRole.OBSERVER)
|
|
383
|
+
|
|
384
|
+
def _hash_key(self, api_key: str) -> str:
|
|
385
|
+
"""Hash an API key for storage using bcrypt."""
|
|
386
|
+
# Use bcrypt for secure key hashing (same as passwords)
|
|
387
|
+
salt = bcrypt.gensalt(rounds=12)
|
|
388
|
+
hashed = bcrypt.hashpw(api_key.encode("utf-8"), salt)
|
|
389
|
+
return hashed.decode("utf-8")
|
|
390
|
+
|
|
391
|
+
def _verify_key(self, api_key: str, key_hash: str) -> bool:
|
|
392
|
+
"""Verify an API key against its hash."""
|
|
393
|
+
try:
|
|
394
|
+
return bcrypt.checkpw(api_key.encode("utf-8"), key_hash.encode("utf-8"))
|
|
395
|
+
except Exception:
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
def _get_key_id(self, api_key: str) -> str:
|
|
399
|
+
"""Extract key ID from full API key."""
|
|
400
|
+
# Key format: ciris_role_randomstring
|
|
401
|
+
# Key ID is first 8 chars of SHA256 hash (for display purposes only)
|
|
402
|
+
return hashlib.sha256(api_key.encode()).hexdigest()[:8]
|
|
403
|
+
|
|
404
|
+
def store_api_key(
|
|
405
|
+
self,
|
|
406
|
+
key: str,
|
|
407
|
+
user_id: str,
|
|
408
|
+
role: UserRole,
|
|
409
|
+
expires_at: Optional[datetime] = None,
|
|
410
|
+
description: Optional[str] = None,
|
|
411
|
+
created_by: Optional[str] = None,
|
|
412
|
+
) -> None:
|
|
413
|
+
"""Store a new API key."""
|
|
414
|
+
key_hash = self._hash_key(key)
|
|
415
|
+
key_id = self._get_key_id(key)
|
|
416
|
+
stored_key = StoredAPIKey(
|
|
417
|
+
key_id=key_id,
|
|
418
|
+
key_hash=key_hash,
|
|
419
|
+
key_value=key[:12] + "..." + key[-4:], # Masked version
|
|
420
|
+
user_id=user_id,
|
|
421
|
+
role=role,
|
|
422
|
+
expires_at=expires_at,
|
|
423
|
+
description=description,
|
|
424
|
+
created_at=datetime.now(timezone.utc),
|
|
425
|
+
created_by=created_by or user_id,
|
|
426
|
+
last_used=None,
|
|
427
|
+
is_active=True,
|
|
428
|
+
)
|
|
429
|
+
# Store by key_id instead of hash (bcrypt hashes are unique per call)
|
|
430
|
+
self._api_keys[key_id] = stored_key
|
|
431
|
+
logger.debug(
|
|
432
|
+
f"[AUTH SERVICE DEBUG] store_api_key: Instance #{self._instance_id} - Stored key_id={key_id} for user={user_id}, role={role}. Total keys now: {len(self._api_keys)}, dict_id={id(self._api_keys)}"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
def validate_api_key(self, api_key: str) -> Optional[StoredAPIKey]:
|
|
436
|
+
"""Validate an API key and return its info."""
|
|
437
|
+
# Get the key_id for fast lookup
|
|
438
|
+
key_id = self._get_key_id(api_key)
|
|
439
|
+
stored_key = self._api_keys.get(key_id)
|
|
440
|
+
|
|
441
|
+
# DEBUG: Log validation attempt with minimal context (key_id only, no key content)
|
|
442
|
+
all_key_ids = list(self._api_keys.keys()) # NOSONAR - key IDs are hashes, not secrets
|
|
443
|
+
logger.debug(
|
|
444
|
+
f"[AUTH SERVICE DEBUG] validate_api_key: Instance #{self._instance_id} - Validating key_id={key_id}"
|
|
445
|
+
)
|
|
446
|
+
logger.debug(
|
|
447
|
+
f"[AUTH SERVICE DEBUG] validate_api_key: Instance #{self._instance_id} - _api_keys has {len(self._api_keys)} keys, dict_id={id(self._api_keys)}"
|
|
448
|
+
)
|
|
449
|
+
logger.debug(
|
|
450
|
+
f"[AUTH SERVICE DEBUG] validate_api_key: Instance #{self._instance_id} - stored_key found: {stored_key is not None}"
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# Verify the key using bcrypt
|
|
454
|
+
if not stored_key or not stored_key.is_active:
|
|
455
|
+
logger.debug(
|
|
456
|
+
f"[AUTH SERVICE DEBUG] validate_api_key: Instance #{self._instance_id} - FAILED: key not found or inactive"
|
|
457
|
+
)
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
if not self._verify_key(api_key, stored_key.key_hash):
|
|
461
|
+
logger.debug(
|
|
462
|
+
f"[AUTH SERVICE DEBUG] validate_api_key: Instance #{self._instance_id} - FAILED: bcrypt verification failed"
|
|
463
|
+
)
|
|
464
|
+
return None
|
|
465
|
+
|
|
466
|
+
# Check expiration
|
|
467
|
+
if stored_key.expires_at and stored_key.expires_at < datetime.now(timezone.utc):
|
|
468
|
+
logger.debug(f"[AUTH SERVICE DEBUG] validate_api_key: Instance #{self._instance_id} - FAILED: key expired")
|
|
469
|
+
return None
|
|
470
|
+
|
|
471
|
+
# Update last used
|
|
472
|
+
stored_key.last_used = datetime.now(timezone.utc)
|
|
473
|
+
|
|
474
|
+
# Ensure system admin user exists in _users
|
|
475
|
+
if stored_key.user_id == "wa-system-admin" and stored_key.user_id not in self._users:
|
|
476
|
+
# Re-create the system admin user
|
|
477
|
+
admin_user = User(
|
|
478
|
+
wa_id="wa-system-admin",
|
|
479
|
+
name="admin",
|
|
480
|
+
auth_type="password",
|
|
481
|
+
api_role=APIRole.SYSTEM_ADMIN,
|
|
482
|
+
wa_role=None,
|
|
483
|
+
created_at=datetime.now(timezone.utc),
|
|
484
|
+
is_active=True,
|
|
485
|
+
password_hash=self._hash_password("ciris_admin_password"),
|
|
486
|
+
)
|
|
487
|
+
self._users[admin_user.wa_id] = admin_user
|
|
488
|
+
|
|
489
|
+
logger.debug(
|
|
490
|
+
f"[AUTH SERVICE DEBUG] validate_api_key: Instance #{self._instance_id} - SUCCESS: key valid for user={stored_key.user_id}, role={stored_key.role}"
|
|
491
|
+
)
|
|
492
|
+
return stored_key
|
|
493
|
+
|
|
494
|
+
def revoke_api_key(self, key_id: str) -> None:
|
|
495
|
+
"""Revoke an API key."""
|
|
496
|
+
# Lookup by key_id directly
|
|
497
|
+
stored_key = self._api_keys.get(key_id)
|
|
498
|
+
if stored_key:
|
|
499
|
+
stored_key.is_active = False
|
|
500
|
+
|
|
501
|
+
def create_oauth_user(
|
|
502
|
+
self,
|
|
503
|
+
provider: str,
|
|
504
|
+
external_id: str,
|
|
505
|
+
email: Optional[str],
|
|
506
|
+
name: Optional[str],
|
|
507
|
+
role: UserRole,
|
|
508
|
+
marketing_opt_in: bool = False,
|
|
509
|
+
) -> OAuthUser:
|
|
510
|
+
"""Create or update an OAuth user."""
|
|
511
|
+
user_id = f"{provider}:{external_id}"
|
|
512
|
+
now = datetime.now(timezone.utc)
|
|
513
|
+
|
|
514
|
+
if user_id in self._oauth_users:
|
|
515
|
+
# Update existing user
|
|
516
|
+
user = self._oauth_users[user_id]
|
|
517
|
+
user.last_login = now
|
|
518
|
+
if email:
|
|
519
|
+
user.email = email
|
|
520
|
+
if name:
|
|
521
|
+
user.name = name
|
|
522
|
+
# Update marketing opt-in (user can change their preference)
|
|
523
|
+
user.marketing_opt_in = marketing_opt_in
|
|
524
|
+
else:
|
|
525
|
+
# Create new user
|
|
526
|
+
user = OAuthUser(
|
|
527
|
+
user_id=user_id,
|
|
528
|
+
provider=provider,
|
|
529
|
+
external_id=external_id,
|
|
530
|
+
email=email,
|
|
531
|
+
name=name,
|
|
532
|
+
role=role,
|
|
533
|
+
created_at=now,
|
|
534
|
+
last_login=now,
|
|
535
|
+
marketing_opt_in=marketing_opt_in,
|
|
536
|
+
)
|
|
537
|
+
self._oauth_users[user_id] = user
|
|
538
|
+
|
|
539
|
+
return user
|
|
540
|
+
|
|
541
|
+
# ========== User Management Methods ==========
|
|
542
|
+
|
|
543
|
+
def _hash_password(self, password: str) -> str:
|
|
544
|
+
"""Hash a password for storage using PBKDF2."""
|
|
545
|
+
import base64
|
|
546
|
+
import secrets
|
|
547
|
+
|
|
548
|
+
from cryptography.hazmat.primitives import hashes
|
|
549
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
550
|
+
|
|
551
|
+
salt = secrets.token_bytes(32)
|
|
552
|
+
kdf = PBKDF2HMAC(
|
|
553
|
+
algorithm=hashes.SHA256(),
|
|
554
|
+
length=32,
|
|
555
|
+
salt=salt,
|
|
556
|
+
iterations=100000,
|
|
557
|
+
)
|
|
558
|
+
key = kdf.derive(password.encode())
|
|
559
|
+
return base64.b64encode(salt + key).decode()
|
|
560
|
+
|
|
561
|
+
def _verify_password(self, password: str, password_hash: str) -> bool:
|
|
562
|
+
"""Verify a password against its hash using PBKDF2."""
|
|
563
|
+
try:
|
|
564
|
+
import base64
|
|
565
|
+
import hmac
|
|
566
|
+
|
|
567
|
+
from cryptography.hazmat.primitives import hashes
|
|
568
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
569
|
+
|
|
570
|
+
decoded = base64.b64decode(password_hash)
|
|
571
|
+
salt = decoded[:32]
|
|
572
|
+
stored_key = decoded[32:]
|
|
573
|
+
|
|
574
|
+
kdf = PBKDF2HMAC(
|
|
575
|
+
algorithm=hashes.SHA256(),
|
|
576
|
+
length=32,
|
|
577
|
+
salt=salt,
|
|
578
|
+
iterations=100000,
|
|
579
|
+
)
|
|
580
|
+
key = kdf.derive(password.encode())
|
|
581
|
+
# Use constant-time comparison to prevent timing attacks
|
|
582
|
+
return hmac.compare_digest(key, stored_key)
|
|
583
|
+
except Exception:
|
|
584
|
+
# If verification fails (e.g., invalid hash format), return False
|
|
585
|
+
return False
|
|
586
|
+
|
|
587
|
+
async def verify_user_password(self, username: str, password: str) -> Optional[User]:
|
|
588
|
+
"""Verify a user's password and return the user if valid."""
|
|
589
|
+
logger.debug("=" * 80)
|
|
590
|
+
logger.debug(f"[AUTH DEBUG] verify_user_password('{username}') called")
|
|
591
|
+
logger.debug(f"[AUTH DEBUG] _users_loaded flag: {self._users_loaded}")
|
|
592
|
+
|
|
593
|
+
# Ensure users are loaded from database
|
|
594
|
+
await self._ensure_users_loaded()
|
|
595
|
+
|
|
596
|
+
logger.debug(f"[AUTH DEBUG] After _ensure_users_loaded, _users_loaded: {self._users_loaded}")
|
|
597
|
+
logger.debug(f"[AUTH DEBUG] _users dict size: {len(self._users)}")
|
|
598
|
+
|
|
599
|
+
user = self.get_user_by_username(username)
|
|
600
|
+
if not user:
|
|
601
|
+
logger.debug("[AUTH DEBUG] User lookup failed - returning None")
|
|
602
|
+
logger.debug("=" * 80)
|
|
603
|
+
return None
|
|
604
|
+
|
|
605
|
+
logger.debug(f"[AUTH DEBUG] User found: wa_id={user.wa_id}")
|
|
606
|
+
logger.debug(f"[AUTH DEBUG] User.name: '{user.name}'")
|
|
607
|
+
logger.debug(f"[AUTH DEBUG] User.auth_type: '{user.auth_type}'")
|
|
608
|
+
logger.debug(f"[AUTH DEBUG] Has password_hash: {user.password_hash is not None}")
|
|
609
|
+
|
|
610
|
+
if user.password_hash:
|
|
611
|
+
logger.debug(f"[AUTH DEBUG] password_hash length: {len(user.password_hash)}")
|
|
612
|
+
logger.debug(f"[AUTH DEBUG] password_hash prefix: {user.password_hash[:10]}")
|
|
613
|
+
|
|
614
|
+
verify_result = self._verify_password(password, user.password_hash)
|
|
615
|
+
logger.debug(f"[AUTH DEBUG] Password verification result: {verify_result}")
|
|
616
|
+
|
|
617
|
+
if verify_result:
|
|
618
|
+
logger.debug(f"[AUTH DEBUG] Authentication SUCCESS for '{username}'")
|
|
619
|
+
logger.debug("=" * 80)
|
|
620
|
+
return user
|
|
621
|
+
else:
|
|
622
|
+
logger.debug("[AUTH DEBUG] Password verification FAILED")
|
|
623
|
+
logger.debug("=" * 80)
|
|
624
|
+
return None
|
|
625
|
+
else:
|
|
626
|
+
logger.debug("[AUTH DEBUG] No password_hash for user")
|
|
627
|
+
logger.debug("=" * 80)
|
|
628
|
+
return None
|
|
629
|
+
|
|
630
|
+
def get_user_by_username(self, username: str) -> Optional[User]:
|
|
631
|
+
"""Get a user by username."""
|
|
632
|
+
logger.debug(f"[AUTH DEBUG] get_user_by_username('{username}') called")
|
|
633
|
+
logger.debug(f"[AUTH DEBUG] _users dict has {len(self._users)} entries")
|
|
634
|
+
|
|
635
|
+
# Get unique usernames (since users can be stored under multiple keys)
|
|
636
|
+
unique_users = {}
|
|
637
|
+
for key, user in self._users.items():
|
|
638
|
+
if user.wa_id not in unique_users:
|
|
639
|
+
unique_users[user.wa_id] = user
|
|
640
|
+
|
|
641
|
+
usernames = [u.name for u in unique_users.values()]
|
|
642
|
+
logger.debug(f"[AUTH DEBUG] Available usernames: {usernames}")
|
|
643
|
+
|
|
644
|
+
for user in self._users.values():
|
|
645
|
+
if user.name == username:
|
|
646
|
+
logger.debug(
|
|
647
|
+
f"[AUTH DEBUG] FOUND user: wa_id={user.wa_id}, name={user.name}, has_password={user.password_hash is not None}"
|
|
648
|
+
)
|
|
649
|
+
return user
|
|
650
|
+
|
|
651
|
+
logger.debug(f"[AUTH DEBUG] User '{username}' NOT FOUND")
|
|
652
|
+
return None
|
|
653
|
+
|
|
654
|
+
async def create_user(self, username: str, password: str, api_role: APIRole = APIRole.OBSERVER) -> Optional[User]:
|
|
655
|
+
"""Create a new user account."""
|
|
656
|
+
# Check if username already exists
|
|
657
|
+
existing = self.get_user_by_username(username)
|
|
658
|
+
if existing:
|
|
659
|
+
return None
|
|
660
|
+
|
|
661
|
+
# Map API role to WA role
|
|
662
|
+
wa_role_map = {
|
|
663
|
+
APIRole.SYSTEM_ADMIN: WARole.ROOT,
|
|
664
|
+
APIRole.AUTHORITY: WARole.AUTHORITY,
|
|
665
|
+
APIRole.ADMIN: WARole.AUTHORITY, # Admin also gets AUTHORITY
|
|
666
|
+
APIRole.OBSERVER: WARole.OBSERVER,
|
|
667
|
+
}
|
|
668
|
+
wa_role = wa_role_map.get(api_role, WARole.OBSERVER)
|
|
669
|
+
|
|
670
|
+
# If we have an auth service, create in database
|
|
671
|
+
if self._auth_service:
|
|
672
|
+
try:
|
|
673
|
+
# Create WA certificate
|
|
674
|
+
wa_cert = await self._auth_service.create_wa(
|
|
675
|
+
name=username,
|
|
676
|
+
email=f"{username}@ciris.local",
|
|
677
|
+
scopes=self.get_permissions_for_role(api_role),
|
|
678
|
+
role=wa_role,
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# Update with password hash
|
|
682
|
+
await self._auth_service.update_wa(
|
|
683
|
+
wa_cert.wa_id, updates=None, password_hash=self._hash_password(password)
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
# Create user object
|
|
687
|
+
user = User(
|
|
688
|
+
wa_id=wa_cert.wa_id,
|
|
689
|
+
name=username,
|
|
690
|
+
auth_type="password",
|
|
691
|
+
api_role=api_role,
|
|
692
|
+
wa_role=wa_role,
|
|
693
|
+
created_at=wa_cert.created_at,
|
|
694
|
+
is_active=True,
|
|
695
|
+
password_hash=self._hash_password(password),
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# Store in cache
|
|
699
|
+
self._users[wa_cert.wa_id] = user
|
|
700
|
+
return user
|
|
701
|
+
|
|
702
|
+
except Exception as e:
|
|
703
|
+
logger.debug(f"[AUTH DEBUG] Error creating user in database: {e}")
|
|
704
|
+
# Fall through to in-memory creation
|
|
705
|
+
|
|
706
|
+
# Fallback: in-memory only
|
|
707
|
+
user_id = f"wa-user-{secrets.token_hex(8)}"
|
|
708
|
+
now = datetime.now(timezone.utc)
|
|
709
|
+
user = User(
|
|
710
|
+
wa_id=user_id,
|
|
711
|
+
name=username,
|
|
712
|
+
auth_type="password",
|
|
713
|
+
api_role=api_role,
|
|
714
|
+
created_at=now,
|
|
715
|
+
is_active=True,
|
|
716
|
+
password_hash=self._hash_password(password),
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
# Store user
|
|
720
|
+
self._users[user_id] = user
|
|
721
|
+
|
|
722
|
+
return user
|
|
723
|
+
|
|
724
|
+
async def list_users(
|
|
725
|
+
self,
|
|
726
|
+
search: Optional[str] = None,
|
|
727
|
+
auth_type: Optional[str] = None,
|
|
728
|
+
api_role: Optional[APIRole] = None,
|
|
729
|
+
wa_role: Optional[WARole] = None,
|
|
730
|
+
is_active: Optional[bool] = None,
|
|
731
|
+
) -> List[tuple[str, User]]:
|
|
732
|
+
"""List all users with optional filtering. Returns (user_id, user) tuples."""
|
|
733
|
+
# Ensure users are loaded from database before listing
|
|
734
|
+
await self._ensure_users_loaded()
|
|
735
|
+
|
|
736
|
+
users = []
|
|
737
|
+
seen_wa_ids: set[str] = set() # Dedupe by wa_id
|
|
738
|
+
|
|
739
|
+
# Add all stored users with their keys (deduplicated by wa_id)
|
|
740
|
+
for user_id, user in self._users.items():
|
|
741
|
+
# Skip duplicates - _users has multiple keys (wa_id, google:xxx) for same user
|
|
742
|
+
if user.wa_id in seen_wa_ids:
|
|
743
|
+
continue
|
|
744
|
+
seen_wa_ids.add(user.wa_id)
|
|
745
|
+
|
|
746
|
+
# Apply filters
|
|
747
|
+
if search and search.lower() not in user.name.lower():
|
|
748
|
+
continue
|
|
749
|
+
if auth_type and user.auth_type != auth_type:
|
|
750
|
+
continue
|
|
751
|
+
if api_role and user.api_role != api_role:
|
|
752
|
+
continue
|
|
753
|
+
if wa_role and user.wa_role != wa_role:
|
|
754
|
+
continue
|
|
755
|
+
if is_active is not None and user.is_active != is_active:
|
|
756
|
+
continue
|
|
757
|
+
|
|
758
|
+
users.append((user_id, user)) # Use the dict key as the canonical user_id
|
|
759
|
+
|
|
760
|
+
# Add OAuth users not in _users
|
|
761
|
+
for oauth_user in self._oauth_users.values():
|
|
762
|
+
oauth_user_id = oauth_user.user_id
|
|
763
|
+
# Check if already in users by matching oauth_external_id
|
|
764
|
+
# This handles cases where the DB WA has a different wa_id (e.g., wa-2025-12-03-xxx)
|
|
765
|
+
# but represents the same OAuth user (same oauth_external_id)
|
|
766
|
+
if any(uid == oauth_user_id or u.oauth_external_id == oauth_user.external_id for uid, u in users):
|
|
767
|
+
continue
|
|
768
|
+
|
|
769
|
+
# Convert OAuth user to User
|
|
770
|
+
user = User(
|
|
771
|
+
wa_id=oauth_user.user_id,
|
|
772
|
+
name=oauth_user.name or oauth_user.email or oauth_user.user_id,
|
|
773
|
+
auth_type="oauth",
|
|
774
|
+
api_role=self._user_role_to_api_role(oauth_user.role),
|
|
775
|
+
oauth_provider=oauth_user.provider,
|
|
776
|
+
oauth_email=oauth_user.email,
|
|
777
|
+
oauth_external_id=oauth_user.external_id,
|
|
778
|
+
oauth_name=oauth_user.name, # Map OAuth name to oauth_name field
|
|
779
|
+
created_at=oauth_user.created_at,
|
|
780
|
+
last_login=oauth_user.last_login,
|
|
781
|
+
is_active=True,
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
# Apply filters
|
|
785
|
+
if search and search.lower() not in user.name.lower():
|
|
786
|
+
continue
|
|
787
|
+
if auth_type and user.auth_type != auth_type:
|
|
788
|
+
continue
|
|
789
|
+
if api_role and user.api_role != api_role:
|
|
790
|
+
continue
|
|
791
|
+
if wa_role is not None:
|
|
792
|
+
continue # OAuth users without WA role
|
|
793
|
+
if is_active is not None and user.is_active != is_active:
|
|
794
|
+
continue
|
|
795
|
+
|
|
796
|
+
users.append((oauth_user_id, user))
|
|
797
|
+
|
|
798
|
+
return sorted(users, key=lambda tu: tu[1].created_at or datetime.min, reverse=True)
|
|
799
|
+
|
|
800
|
+
def _user_role_to_api_role(self, role: UserRole) -> APIRole:
|
|
801
|
+
"""Convert UserRole to APIRole."""
|
|
802
|
+
mapping = {
|
|
803
|
+
UserRole.OBSERVER: APIRole.OBSERVER,
|
|
804
|
+
UserRole.ADMIN: APIRole.ADMIN,
|
|
805
|
+
UserRole.SYSTEM_ADMIN: APIRole.SYSTEM_ADMIN,
|
|
806
|
+
}
|
|
807
|
+
return mapping.get(role, APIRole.OBSERVER)
|
|
808
|
+
|
|
809
|
+
def _merge_oauth_with_stored_user(self, oauth_user: OAuthUser, stored_user: User) -> User:
|
|
810
|
+
"""Merge OAuth session data with persistent database user data."""
|
|
811
|
+
return User(
|
|
812
|
+
wa_id=stored_user.wa_id, # Use the actual WA ID from database!
|
|
813
|
+
name=stored_user.name or oauth_user.name or oauth_user.email or oauth_user.user_id,
|
|
814
|
+
auth_type="oauth",
|
|
815
|
+
api_role=stored_user.api_role, # Preserve DB role
|
|
816
|
+
wa_role=stored_user.wa_role, # Preserve WA role
|
|
817
|
+
oauth_provider=oauth_user.provider,
|
|
818
|
+
oauth_email=oauth_user.email,
|
|
819
|
+
oauth_external_id=oauth_user.external_id,
|
|
820
|
+
created_at=stored_user.created_at or oauth_user.created_at,
|
|
821
|
+
last_login=oauth_user.last_login,
|
|
822
|
+
is_active=stored_user.is_active,
|
|
823
|
+
wa_parent_id=stored_user.wa_parent_id,
|
|
824
|
+
wa_auto_minted=stored_user.wa_auto_minted,
|
|
825
|
+
oauth_name=stored_user.oauth_name or oauth_user.name,
|
|
826
|
+
oauth_picture=stored_user.oauth_picture,
|
|
827
|
+
permission_requested_at=stored_user.permission_requested_at,
|
|
828
|
+
custom_permissions=stored_user.custom_permissions,
|
|
829
|
+
oauth_links=stored_user.oauth_links,
|
|
830
|
+
marketing_opt_in=oauth_user.marketing_opt_in,
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
def _create_user_from_oauth(self, oauth_user: OAuthUser) -> User:
|
|
834
|
+
"""Create a User object from an OAuth user (not yet minted as WA)."""
|
|
835
|
+
return User(
|
|
836
|
+
wa_id=oauth_user.user_id, # OAuth user_id as placeholder
|
|
837
|
+
name=oauth_user.name or oauth_user.email or oauth_user.user_id,
|
|
838
|
+
auth_type="oauth",
|
|
839
|
+
api_role=self._user_role_to_api_role(oauth_user.role),
|
|
840
|
+
oauth_provider=oauth_user.provider,
|
|
841
|
+
oauth_email=oauth_user.email,
|
|
842
|
+
oauth_external_id=oauth_user.external_id,
|
|
843
|
+
created_at=oauth_user.created_at,
|
|
844
|
+
last_login=oauth_user.last_login,
|
|
845
|
+
is_active=True,
|
|
846
|
+
oauth_name=oauth_user.name,
|
|
847
|
+
marketing_opt_in=oauth_user.marketing_opt_in,
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
def _lookup_oauth_user(self, user_id: str) -> Optional[User]:
|
|
851
|
+
"""Look up user in OAuth users dictionary."""
|
|
852
|
+
if user_id not in self._oauth_users:
|
|
853
|
+
return None
|
|
854
|
+
|
|
855
|
+
oauth_user = self._oauth_users[user_id]
|
|
856
|
+
stored_user = self._users.get(user_id)
|
|
857
|
+
|
|
858
|
+
# If stored_user exists, merge OAuth session data with persistent DB data
|
|
859
|
+
if stored_user:
|
|
860
|
+
return self._merge_oauth_with_stored_user(oauth_user, stored_user)
|
|
861
|
+
|
|
862
|
+
# No stored user - pure OAuth user not yet minted as WA
|
|
863
|
+
return self._create_user_from_oauth(oauth_user)
|
|
864
|
+
|
|
865
|
+
def _lookup_by_external_id(self, user_id: str) -> Optional[User]:
|
|
866
|
+
"""Fallback lookup by OAuth external_id (without provider prefix)."""
|
|
867
|
+
# Try stored users first
|
|
868
|
+
for key, user in self._users.items():
|
|
869
|
+
if user.oauth_external_id == user_id:
|
|
870
|
+
return user
|
|
871
|
+
|
|
872
|
+
# Try OAuth users
|
|
873
|
+
for key, oauth_user in self._oauth_users.items():
|
|
874
|
+
if oauth_user.external_id == user_id:
|
|
875
|
+
stored_user = self._users.get(key)
|
|
876
|
+
if stored_user:
|
|
877
|
+
return stored_user
|
|
878
|
+
return self._create_user_from_oauth(oauth_user)
|
|
879
|
+
|
|
880
|
+
return None
|
|
881
|
+
|
|
882
|
+
def get_user(self, user_id: str) -> Optional[User]:
|
|
883
|
+
"""Get a specific user by ID."""
|
|
884
|
+
# Check stored users first (includes users loaded from DB with OAuth links)
|
|
885
|
+
if user_id in self._users:
|
|
886
|
+
return self._users[user_id]
|
|
887
|
+
|
|
888
|
+
# Check OAuth users (in-memory only, for users who haven't been minted as WA yet)
|
|
889
|
+
oauth_result = self._lookup_oauth_user(user_id)
|
|
890
|
+
if oauth_result:
|
|
891
|
+
return oauth_result
|
|
892
|
+
|
|
893
|
+
# Fallback: Try to find user by OAuth external_id (without provider prefix)
|
|
894
|
+
# This handles cases where frontend passes just "googleUserId" without "google:" prefix
|
|
895
|
+
return self._lookup_by_external_id(user_id)
|
|
896
|
+
|
|
897
|
+
async def update_user(
|
|
898
|
+
self, user_id: str, api_role: Optional[APIRole] = None, is_active: Optional[bool] = None
|
|
899
|
+
) -> Optional[User]:
|
|
900
|
+
"""Update user information."""
|
|
901
|
+
user = self.get_user(user_id)
|
|
902
|
+
if not user:
|
|
903
|
+
return None
|
|
904
|
+
|
|
905
|
+
# Update fields
|
|
906
|
+
if api_role is not None:
|
|
907
|
+
user.api_role = api_role
|
|
908
|
+
# Also update WA role to match
|
|
909
|
+
wa_role_map = {
|
|
910
|
+
APIRole.SYSTEM_ADMIN: WARole.ROOT,
|
|
911
|
+
APIRole.AUTHORITY: WARole.AUTHORITY,
|
|
912
|
+
APIRole.ADMIN: WARole.AUTHORITY, # Admin also gets AUTHORITY
|
|
913
|
+
APIRole.OBSERVER: WARole.OBSERVER,
|
|
914
|
+
}
|
|
915
|
+
user.wa_role = wa_role_map.get(api_role, WARole.OBSERVER)
|
|
916
|
+
if is_active is not None:
|
|
917
|
+
user.is_active = is_active
|
|
918
|
+
|
|
919
|
+
# Store updated user
|
|
920
|
+
self._users[user_id] = user
|
|
921
|
+
|
|
922
|
+
# Also update in database if we have auth service
|
|
923
|
+
if self._auth_service:
|
|
924
|
+
try:
|
|
925
|
+
# Update role in database
|
|
926
|
+
if api_role is not None and user.wa_role:
|
|
927
|
+
await self._auth_service.update_wa(
|
|
928
|
+
user_id,
|
|
929
|
+
updates=WAUpdate(
|
|
930
|
+
role=user.wa_role.value if hasattr(user.wa_role, "value") else str(user.wa_role)
|
|
931
|
+
),
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
# Update active status in database
|
|
935
|
+
if is_active is not None:
|
|
936
|
+
if is_active:
|
|
937
|
+
# Reactivate - note: this may not work if cert was revoked
|
|
938
|
+
await self._auth_service.update_wa(user_id, updates=WAUpdate(is_active=True))
|
|
939
|
+
else:
|
|
940
|
+
# Deactivate
|
|
941
|
+
await self._auth_service.revoke_wa(user_id, reason="User deactivated via API")
|
|
942
|
+
except Exception as e:
|
|
943
|
+
logger.debug(f"[AUTH DEBUG] Error updating user in database: {e}")
|
|
944
|
+
|
|
945
|
+
# Also update OAuth user if applicable
|
|
946
|
+
if user_id in self._oauth_users:
|
|
947
|
+
oauth_user = self._oauth_users[user_id]
|
|
948
|
+
if api_role is not None:
|
|
949
|
+
# Convert APIRole back to UserRole
|
|
950
|
+
role_mapping = {
|
|
951
|
+
APIRole.OBSERVER: UserRole.OBSERVER,
|
|
952
|
+
APIRole.ADMIN: UserRole.ADMIN,
|
|
953
|
+
APIRole.AUTHORITY: UserRole.ADMIN, # No direct mapping
|
|
954
|
+
APIRole.SYSTEM_ADMIN: UserRole.SYSTEM_ADMIN,
|
|
955
|
+
}
|
|
956
|
+
oauth_user.role = role_mapping.get(api_role, UserRole.OBSERVER)
|
|
957
|
+
|
|
958
|
+
return user
|
|
959
|
+
|
|
960
|
+
async def link_user_oauth(
|
|
961
|
+
self,
|
|
962
|
+
wa_id: str,
|
|
963
|
+
provider: str,
|
|
964
|
+
external_id: str,
|
|
965
|
+
*,
|
|
966
|
+
account_name: Optional[str] = None,
|
|
967
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
968
|
+
primary: bool = False,
|
|
969
|
+
) -> Optional[User]:
|
|
970
|
+
if not self._auth_service:
|
|
971
|
+
raise ValueError("Authentication service not configured")
|
|
972
|
+
|
|
973
|
+
updated = await self._auth_service.link_oauth_identity(
|
|
974
|
+
wa_id,
|
|
975
|
+
provider,
|
|
976
|
+
external_id,
|
|
977
|
+
account_name=account_name,
|
|
978
|
+
metadata=metadata,
|
|
979
|
+
primary=primary,
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
if not updated:
|
|
983
|
+
return None
|
|
984
|
+
|
|
985
|
+
await self._process_wa_record(updated)
|
|
986
|
+
return self.get_user(wa_id)
|
|
987
|
+
|
|
988
|
+
async def unlink_user_oauth(self, wa_id: str, provider: str, external_id: str) -> Optional[User]:
|
|
989
|
+
if not self._auth_service:
|
|
990
|
+
raise ValueError("Authentication service not configured")
|
|
991
|
+
|
|
992
|
+
updated = await self._auth_service.unlink_oauth_identity(wa_id, provider, external_id)
|
|
993
|
+
if not updated:
|
|
994
|
+
return None
|
|
995
|
+
|
|
996
|
+
await self._process_wa_record(updated)
|
|
997
|
+
return self.get_user(wa_id)
|
|
998
|
+
|
|
999
|
+
async def change_password(
|
|
1000
|
+
self, user_id: str, new_password: str, current_password: Optional[str] = None, skip_current_check: bool = False
|
|
1001
|
+
) -> bool:
|
|
1002
|
+
"""Change user password."""
|
|
1003
|
+
user = self.get_user(user_id)
|
|
1004
|
+
if not user or user.auth_type != "password":
|
|
1005
|
+
return False
|
|
1006
|
+
|
|
1007
|
+
# Verify current password unless skip_current_check is True
|
|
1008
|
+
if not skip_current_check and current_password:
|
|
1009
|
+
if not user.password_hash or not self._verify_password(current_password, user.password_hash):
|
|
1010
|
+
return False
|
|
1011
|
+
|
|
1012
|
+
# Update password
|
|
1013
|
+
user.password_hash = self._hash_password(new_password)
|
|
1014
|
+
self._users[user_id] = user
|
|
1015
|
+
|
|
1016
|
+
# Also update in database if we have auth service
|
|
1017
|
+
if self._auth_service:
|
|
1018
|
+
try:
|
|
1019
|
+
# Use await instead of asyncio.run() - we're already in an async context
|
|
1020
|
+
await self._auth_service.update_wa(
|
|
1021
|
+
user_id, updates=None, password_hash=self._hash_password(new_password)
|
|
1022
|
+
)
|
|
1023
|
+
except Exception as e:
|
|
1024
|
+
logger.debug(f"[AUTH DEBUG] Error updating password in database: {e}")
|
|
1025
|
+
|
|
1026
|
+
return True
|
|
1027
|
+
|
|
1028
|
+
async def deactivate_user(self, user_id: str) -> bool:
|
|
1029
|
+
"""Deactivate a user account."""
|
|
1030
|
+
user = self.get_user(user_id)
|
|
1031
|
+
if not user:
|
|
1032
|
+
return False
|
|
1033
|
+
|
|
1034
|
+
user.is_active = False
|
|
1035
|
+
self._users[user_id] = user
|
|
1036
|
+
|
|
1037
|
+
# Also update in database if we have auth service
|
|
1038
|
+
if self._auth_service:
|
|
1039
|
+
try:
|
|
1040
|
+
await self._auth_service.revoke_wa(user_id, reason="User deactivated via API")
|
|
1041
|
+
except Exception as e:
|
|
1042
|
+
logger.debug(f"[AUTH DEBUG] Error deactivating user in database: {e}")
|
|
1043
|
+
|
|
1044
|
+
# Also deactivate OAuth user if applicable
|
|
1045
|
+
if user_id in self._oauth_users:
|
|
1046
|
+
# Can't really deactivate OAuth users in this simple implementation
|
|
1047
|
+
pass
|
|
1048
|
+
|
|
1049
|
+
return True
|
|
1050
|
+
|
|
1051
|
+
def get_permissions_for_role(self, role: APIRole) -> List[str]:
|
|
1052
|
+
"""Get permissions for a given API role."""
|
|
1053
|
+
# Define role permissions
|
|
1054
|
+
permissions = {
|
|
1055
|
+
APIRole.OBSERVER: [
|
|
1056
|
+
PERMISSION_SYSTEM_READ,
|
|
1057
|
+
PERMISSION_MEMORY_READ,
|
|
1058
|
+
PERMISSION_TELEMETRY_READ,
|
|
1059
|
+
PERMISSION_CONFIG_READ,
|
|
1060
|
+
PERMISSION_AUDIT_READ,
|
|
1061
|
+
],
|
|
1062
|
+
APIRole.ADMIN: [
|
|
1063
|
+
PERMISSION_SYSTEM_READ,
|
|
1064
|
+
PERMISSION_SYSTEM_WRITE,
|
|
1065
|
+
PERMISSION_MEMORY_READ,
|
|
1066
|
+
PERMISSION_MEMORY_WRITE,
|
|
1067
|
+
PERMISSION_TELEMETRY_READ,
|
|
1068
|
+
PERMISSION_CONFIG_READ,
|
|
1069
|
+
PERMISSION_CONFIG_WRITE,
|
|
1070
|
+
PERMISSION_AUDIT_READ,
|
|
1071
|
+
PERMISSION_AUDIT_WRITE,
|
|
1072
|
+
PERMISSION_USERS_READ,
|
|
1073
|
+
PERMISSION_MANAGE_USER_PERMISSIONS,
|
|
1074
|
+
],
|
|
1075
|
+
APIRole.AUTHORITY: [
|
|
1076
|
+
PERMISSION_SYSTEM_READ,
|
|
1077
|
+
PERMISSION_SYSTEM_WRITE,
|
|
1078
|
+
PERMISSION_MEMORY_READ,
|
|
1079
|
+
PERMISSION_MEMORY_WRITE,
|
|
1080
|
+
PERMISSION_TELEMETRY_READ,
|
|
1081
|
+
PERMISSION_CONFIG_READ,
|
|
1082
|
+
PERMISSION_CONFIG_WRITE,
|
|
1083
|
+
PERMISSION_AUDIT_READ,
|
|
1084
|
+
PERMISSION_AUDIT_WRITE,
|
|
1085
|
+
PERMISSION_USERS_READ,
|
|
1086
|
+
PERMISSION_WA_READ,
|
|
1087
|
+
PERMISSION_WA_WRITE,
|
|
1088
|
+
"wa.resolve_deferral", # AUTHORITY role can resolve deferrals
|
|
1089
|
+
],
|
|
1090
|
+
APIRole.SYSTEM_ADMIN: [
|
|
1091
|
+
PERMISSION_SYSTEM_READ,
|
|
1092
|
+
PERMISSION_SYSTEM_WRITE,
|
|
1093
|
+
PERMISSION_MEMORY_READ,
|
|
1094
|
+
PERMISSION_MEMORY_WRITE,
|
|
1095
|
+
PERMISSION_TELEMETRY_READ,
|
|
1096
|
+
PERMISSION_CONFIG_READ,
|
|
1097
|
+
PERMISSION_CONFIG_WRITE,
|
|
1098
|
+
PERMISSION_AUDIT_READ,
|
|
1099
|
+
PERMISSION_AUDIT_WRITE,
|
|
1100
|
+
PERMISSION_USERS_READ,
|
|
1101
|
+
PERMISSION_USERS_WRITE,
|
|
1102
|
+
PERMISSION_USERS_DELETE,
|
|
1103
|
+
PERMISSION_WA_READ,
|
|
1104
|
+
PERMISSION_WA_WRITE,
|
|
1105
|
+
PERMISSION_WA_MINT,
|
|
1106
|
+
PERMISSION_EMERGENCY_SHUTDOWN,
|
|
1107
|
+
PERMISSION_MANAGE_USER_PERMISSIONS,
|
|
1108
|
+
],
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
return permissions.get(role, [])
|
|
1112
|
+
|
|
1113
|
+
def get_effective_permissions(self, user: "User") -> List[str]:
|
|
1114
|
+
"""Get effective permissions for a user including WA role inheritance.
|
|
1115
|
+
|
|
1116
|
+
This applies the following inheritance rules:
|
|
1117
|
+
- ROOT WA users get SYSTEM_ADMIN + AUTHORITY permissions
|
|
1118
|
+
- AUTHORITY WA users get their role's permissions (which include wa.resolve_deferral)
|
|
1119
|
+
- All other users get just their API role's permissions
|
|
1120
|
+
- Custom permissions are always added on top
|
|
1121
|
+
"""
|
|
1122
|
+
# Start with base permissions from API role
|
|
1123
|
+
permissions_set = set(self.get_permissions_for_role(user.api_role))
|
|
1124
|
+
|
|
1125
|
+
# ROOT WA role inherits AUTHORITY permissions (for deferral resolution, etc.)
|
|
1126
|
+
# This is the key rule: ROOT maps to SYSTEM_ADMIN API role, but also gets AUTHORITY perms
|
|
1127
|
+
if user.wa_role == WARole.ROOT:
|
|
1128
|
+
authority_perms = self.get_permissions_for_role(APIRole.AUTHORITY)
|
|
1129
|
+
permissions_set.update(authority_perms)
|
|
1130
|
+
|
|
1131
|
+
# AUTHORITY WA role already has wa.resolve_deferral in their API role permissions
|
|
1132
|
+
# No extra inheritance needed since AUTHORITY maps to APIRole.AUTHORITY
|
|
1133
|
+
|
|
1134
|
+
# Add custom permissions
|
|
1135
|
+
if user.custom_permissions:
|
|
1136
|
+
permissions_set.update(user.custom_permissions)
|
|
1137
|
+
|
|
1138
|
+
return list(permissions_set)
|
|
1139
|
+
|
|
1140
|
+
async def update_user_permissions(self, user_id: str, permissions: List[str]) -> Optional[User]:
|
|
1141
|
+
"""Update a user's custom permissions."""
|
|
1142
|
+
user = self.get_user(user_id)
|
|
1143
|
+
if not user:
|
|
1144
|
+
return None
|
|
1145
|
+
|
|
1146
|
+
# Update custom permissions
|
|
1147
|
+
user.custom_permissions = permissions
|
|
1148
|
+
self._users[user_id] = user
|
|
1149
|
+
|
|
1150
|
+
# Also update in database if we have auth service
|
|
1151
|
+
if self._auth_service:
|
|
1152
|
+
try:
|
|
1153
|
+
|
|
1154
|
+
# Update the WA certificate with custom permissions
|
|
1155
|
+
# Don't pass custom_permissions_json as a kwarg, it's not in the protocol
|
|
1156
|
+
# Instead, we should store this separately or handle it differently
|
|
1157
|
+
await self._auth_service.update_wa(
|
|
1158
|
+
user_id, updates=WAUpdate(permissions=permissions) if permissions else None
|
|
1159
|
+
)
|
|
1160
|
+
except Exception as e:
|
|
1161
|
+
logger.debug(f"[AUTH DEBUG] Error updating permissions in database: {e}")
|
|
1162
|
+
|
|
1163
|
+
return user
|
|
1164
|
+
|
|
1165
|
+
def validate_service_token(self, token: str) -> Optional[User]:
|
|
1166
|
+
"""Validate a service token and return a service account user.
|
|
1167
|
+
|
|
1168
|
+
Service tokens are compared against CIRIS_SERVICE_TOKEN environment variable.
|
|
1169
|
+
Uses constant-time comparison to prevent timing attacks.
|
|
1170
|
+
"""
|
|
1171
|
+
import hmac
|
|
1172
|
+
import os
|
|
1173
|
+
|
|
1174
|
+
# Get expected service token from environment
|
|
1175
|
+
expected_token = os.environ.get("CIRIS_SERVICE_TOKEN")
|
|
1176
|
+
if not expected_token:
|
|
1177
|
+
return None
|
|
1178
|
+
|
|
1179
|
+
# Use constant-time comparison
|
|
1180
|
+
if not hmac.compare_digest(token, expected_token):
|
|
1181
|
+
return None
|
|
1182
|
+
|
|
1183
|
+
# Create and return service account user
|
|
1184
|
+
return User(
|
|
1185
|
+
wa_id="service-account",
|
|
1186
|
+
name="Service Account",
|
|
1187
|
+
auth_type="service_token",
|
|
1188
|
+
api_role=APIRole.SERVICE_ACCOUNT,
|
|
1189
|
+
wa_role=None,
|
|
1190
|
+
created_at=datetime.now(timezone.utc),
|
|
1191
|
+
last_login=datetime.now(timezone.utc),
|
|
1192
|
+
is_active=True,
|
|
1193
|
+
custom_permissions=None,
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
def list_user_api_keys(self, user_id: str) -> List[StoredAPIKey]:
|
|
1197
|
+
"""List all API keys for a specific user."""
|
|
1198
|
+
keys = []
|
|
1199
|
+
for stored_key in self._api_keys.values():
|
|
1200
|
+
if stored_key.user_id == user_id:
|
|
1201
|
+
keys.append(stored_key)
|
|
1202
|
+
return sorted(keys, key=lambda k: k.created_at, reverse=True)
|
|
1203
|
+
|
|
1204
|
+
async def verify_root_signature(self, user_id: str, wa_role: WARole, signature: str) -> bool:
|
|
1205
|
+
"""Verify a ROOT signature for WA minting.
|
|
1206
|
+
|
|
1207
|
+
The signature should be over the message:
|
|
1208
|
+
"MINT_WA:{user_id}:{wa_role}:{timestamp}"
|
|
1209
|
+
|
|
1210
|
+
Where timestamp is in ISO format.
|
|
1211
|
+
"""
|
|
1212
|
+
import json
|
|
1213
|
+
from pathlib import Path
|
|
1214
|
+
|
|
1215
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
1216
|
+
|
|
1217
|
+
try:
|
|
1218
|
+
# Load ROOT public key from seed/
|
|
1219
|
+
root_pub_path = Path(__file__).parent.parent.parent.parent.parent.parent / "seed" / "root_pub.json"
|
|
1220
|
+
async with aiofiles.open(root_pub_path, "r") as f:
|
|
1221
|
+
content = await f.read()
|
|
1222
|
+
root_data = json.loads(content)
|
|
1223
|
+
|
|
1224
|
+
# Get the public key (base64url encoded)
|
|
1225
|
+
pubkey_b64 = root_data["pubkey"]
|
|
1226
|
+
|
|
1227
|
+
# Decode from base64url to bytes
|
|
1228
|
+
# Add padding if needed
|
|
1229
|
+
pubkey_b64_padded = pubkey_b64 + "=" * (4 - len(pubkey_b64) % 4)
|
|
1230
|
+
pubkey_bytes = base64.urlsafe_b64decode(pubkey_b64_padded)
|
|
1231
|
+
|
|
1232
|
+
# Create Ed25519 public key object
|
|
1233
|
+
public_key = ed25519.Ed25519PublicKey.from_public_bytes(pubkey_bytes)
|
|
1234
|
+
|
|
1235
|
+
# The signature should include a timestamp
|
|
1236
|
+
# For verification, we'll accept signatures from the last hour
|
|
1237
|
+
now = datetime.now(timezone.utc)
|
|
1238
|
+
|
|
1239
|
+
# Try multiple timestamp formats within the last hour
|
|
1240
|
+
for minutes_ago in range(0, 60, 1): # Check last 60 minutes
|
|
1241
|
+
timestamp = (now - timedelta(minutes=minutes_ago)).isoformat()
|
|
1242
|
+
message = f"MINT_WA:{user_id}:{wa_role.value}:{timestamp}"
|
|
1243
|
+
|
|
1244
|
+
try:
|
|
1245
|
+
# Decode signature from base64url
|
|
1246
|
+
sig_padded = signature + "=" * (4 - len(signature) % 4)
|
|
1247
|
+
sig_bytes = base64.urlsafe_b64decode(sig_padded)
|
|
1248
|
+
|
|
1249
|
+
# Verify signature
|
|
1250
|
+
public_key.verify(sig_bytes, message.encode())
|
|
1251
|
+
|
|
1252
|
+
# If we get here, signature is valid
|
|
1253
|
+
return True
|
|
1254
|
+
except Exception:
|
|
1255
|
+
# Try next timestamp
|
|
1256
|
+
continue
|
|
1257
|
+
|
|
1258
|
+
# Also try without timestamp for backwards compatibility
|
|
1259
|
+
message_no_ts = f"MINT_WA:{user_id}:{wa_role.value}"
|
|
1260
|
+
|
|
1261
|
+
# Try standard base64 first (what our signing script produces)
|
|
1262
|
+
try:
|
|
1263
|
+
sig_bytes = base64.b64decode(signature)
|
|
1264
|
+
public_key.verify(sig_bytes, message_no_ts.encode())
|
|
1265
|
+
return True
|
|
1266
|
+
except Exception:
|
|
1267
|
+
pass
|
|
1268
|
+
|
|
1269
|
+
# Try urlsafe base64
|
|
1270
|
+
try:
|
|
1271
|
+
sig_padded = signature + "=" * (4 - len(signature) % 4)
|
|
1272
|
+
sig_bytes = base64.urlsafe_b64decode(sig_padded)
|
|
1273
|
+
public_key.verify(sig_bytes, message_no_ts.encode())
|
|
1274
|
+
return True
|
|
1275
|
+
except Exception:
|
|
1276
|
+
pass
|
|
1277
|
+
|
|
1278
|
+
return False
|
|
1279
|
+
|
|
1280
|
+
except Exception as e:
|
|
1281
|
+
# Log error but don't expose internal details
|
|
1282
|
+
logger.debug(f"[AUTH DEBUG] Signature verification error: {e}")
|
|
1283
|
+
return False
|
|
1284
|
+
|
|
1285
|
+
def _update_user_wa_role(self, user: User, wa_role: WARole, minted_by: str) -> None:
|
|
1286
|
+
"""Update user's WA role and related fields."""
|
|
1287
|
+
user.wa_role = wa_role
|
|
1288
|
+
user.wa_parent_id = minted_by
|
|
1289
|
+
user.wa_auto_minted = False
|
|
1290
|
+
|
|
1291
|
+
def _upgrade_api_role_if_needed(self, user: User, wa_role: WARole) -> None:
|
|
1292
|
+
"""Upgrade user's API role if WA role requires higher access."""
|
|
1293
|
+
# ROOT and AUTHORITY WA roles both grant AUTHORITY API role
|
|
1294
|
+
if wa_role in (WARole.ROOT, WARole.AUTHORITY) and user.api_role.value < APIRole.AUTHORITY.value:
|
|
1295
|
+
user.api_role = APIRole.AUTHORITY
|
|
1296
|
+
elif wa_role == WARole.OBSERVER and user.api_role.value < APIRole.OBSERVER.value:
|
|
1297
|
+
user.api_role = APIRole.OBSERVER
|
|
1298
|
+
|
|
1299
|
+
async def _update_existing_wa(self, user_id: str, wa_role: WARole) -> None:
|
|
1300
|
+
"""Update existing WA certificate."""
|
|
1301
|
+
if not self._auth_service:
|
|
1302
|
+
return
|
|
1303
|
+
await self._auth_service.update_wa(
|
|
1304
|
+
user_id, updates=WAUpdate(role=wa_role.value if hasattr(wa_role, "value") else str(wa_role))
|
|
1305
|
+
)
|
|
1306
|
+
logger.debug(f"[AUTH DEBUG] Updated existing WA {user_id} to role {wa_role}")
|
|
1307
|
+
|
|
1308
|
+
def _create_wa_email(self, user_name: str) -> str:
|
|
1309
|
+
"""Create email for WA certificate."""
|
|
1310
|
+
return user_name + "@ciris.local" if "@" not in user_name else user_name
|
|
1311
|
+
|
|
1312
|
+
def _get_wa_permissions(self, user: User) -> List[str]:
|
|
1313
|
+
"""Get permissions for WA certificate."""
|
|
1314
|
+
base_permissions = self.get_permissions_for_role(user.api_role)
|
|
1315
|
+
return base_permissions + [
|
|
1316
|
+
"wa.resolve_deferral", # Critical for deferral resolution
|
|
1317
|
+
"wa.mint", # Allow WA to mint others
|
|
1318
|
+
]
|
|
1319
|
+
|
|
1320
|
+
async def _create_new_wa_for_oauth_user(self, user: User, user_id: str, wa_role: WARole) -> str:
|
|
1321
|
+
"""Create new WA certificate for OAuth user and return the wa_id."""
|
|
1322
|
+
if not self._auth_service:
|
|
1323
|
+
raise ValueError("Authentication service not available")
|
|
1324
|
+
|
|
1325
|
+
wa_permissions = self._get_wa_permissions(user)
|
|
1326
|
+
|
|
1327
|
+
# Create WA certificate with proper wa_id format, but link to OAuth user
|
|
1328
|
+
import json
|
|
1329
|
+
from datetime import datetime, timezone
|
|
1330
|
+
|
|
1331
|
+
from ciris_engine.schemas.services.authority_core import WACertificate
|
|
1332
|
+
|
|
1333
|
+
timestamp = datetime.now(timezone.utc)
|
|
1334
|
+
|
|
1335
|
+
# Generate proper wa_id (format: wa-YYYY-MM-DD-XXXXXX)
|
|
1336
|
+
# Must match pattern: ^wa-\d{4}-\d{2}-\d{2}-[A-Z0-9]{6}$
|
|
1337
|
+
import secrets
|
|
1338
|
+
|
|
1339
|
+
wa_id = f"wa-{timestamp.strftime('%Y-%m-%d')}-{secrets.token_hex(3).upper()}"
|
|
1340
|
+
jwt_kid = f"wa-jwt-oauth-{wa_id[-6:].lower()}"
|
|
1341
|
+
|
|
1342
|
+
# Extract OAuth info from user_id (format: "provider:external_id")
|
|
1343
|
+
oauth_provider = None
|
|
1344
|
+
oauth_external_id = None
|
|
1345
|
+
if ":" in user_id:
|
|
1346
|
+
oauth_provider, oauth_external_id = user_id.split(":", 1)
|
|
1347
|
+
|
|
1348
|
+
# Create WA certificate with proper wa_id but linked to OAuth identity
|
|
1349
|
+
wa_cert = WACertificate(
|
|
1350
|
+
wa_id=wa_id, # Proper wa_id format
|
|
1351
|
+
name=user.name,
|
|
1352
|
+
role=wa_role,
|
|
1353
|
+
pubkey=f"oauth-{oauth_provider}-{oauth_external_id}" if oauth_provider else user_id,
|
|
1354
|
+
jwt_kid=jwt_kid,
|
|
1355
|
+
oauth_provider=oauth_provider,
|
|
1356
|
+
oauth_external_id=oauth_external_id,
|
|
1357
|
+
auto_minted=True,
|
|
1358
|
+
scopes_json=json.dumps(wa_permissions),
|
|
1359
|
+
created_at=timestamp,
|
|
1360
|
+
last_auth=timestamp,
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
# Store WA certificate in database
|
|
1364
|
+
# Note: Accessing private method - ideally this would be a public API
|
|
1365
|
+
if hasattr(self._auth_service, "_store_wa_certificate"):
|
|
1366
|
+
store_method = getattr(self._auth_service, "_store_wa_certificate")
|
|
1367
|
+
await store_method(wa_cert)
|
|
1368
|
+
else:
|
|
1369
|
+
raise ValueError("Cannot store WA certificate - method not available")
|
|
1370
|
+
|
|
1371
|
+
logger.debug(f"[AUTH DEBUG] Created WA certificate {wa_id} for OAuth user {user_id} with role {wa_role}")
|
|
1372
|
+
return wa_id
|
|
1373
|
+
|
|
1374
|
+
# Removed _link_oauth_identity - no longer needed since OAuth users use their user_id as wa_id
|
|
1375
|
+
|
|
1376
|
+
def _update_user_records_for_oauth_wa(self, user: User, user_id: str, wa_id: str) -> None:
|
|
1377
|
+
"""Update OAuth user record with WA information (no duplicate records)."""
|
|
1378
|
+
user.wa_id = wa_id # Set the proper WA ID
|
|
1379
|
+
# Keep the user under their original OAuth user_id key
|
|
1380
|
+
self._users[user_id] = user # Update existing record, don't create duplicate
|
|
1381
|
+
|
|
1382
|
+
async def _handle_wa_database_operations(self, user: User, user_id: str, wa_role: WARole) -> None:
|
|
1383
|
+
"""Handle WA database create/update operations."""
|
|
1384
|
+
if not self._auth_service:
|
|
1385
|
+
return
|
|
1386
|
+
existing_wa = await self._auth_service.get_wa(user_id)
|
|
1387
|
+
|
|
1388
|
+
if existing_wa:
|
|
1389
|
+
await self._update_existing_wa(user_id, wa_role)
|
|
1390
|
+
else:
|
|
1391
|
+
# For OAuth users, create WA with proper wa_id but update existing OAuth user record
|
|
1392
|
+
wa_id = await self._create_new_wa_for_oauth_user(user, user_id, wa_role)
|
|
1393
|
+
self._update_user_records_for_oauth_wa(user, user_id, wa_id)
|
|
1394
|
+
|
|
1395
|
+
async def mint_wise_authority(self, user_id: str, wa_role: WARole, minted_by: str) -> Optional[User]:
|
|
1396
|
+
"""Mint a user as a Wise Authority."""
|
|
1397
|
+
user = self.get_user(user_id)
|
|
1398
|
+
if not user:
|
|
1399
|
+
return None
|
|
1400
|
+
|
|
1401
|
+
# Update user WA role and API role if needed
|
|
1402
|
+
self._update_user_wa_role(user, wa_role, minted_by)
|
|
1403
|
+
self._upgrade_api_role_if_needed(user, wa_role)
|
|
1404
|
+
|
|
1405
|
+
# Store updated user
|
|
1406
|
+
self._users[user_id] = user
|
|
1407
|
+
|
|
1408
|
+
# Handle database operations if auth service is available
|
|
1409
|
+
if self._auth_service:
|
|
1410
|
+
try:
|
|
1411
|
+
await self._handle_wa_database_operations(user, user_id, wa_role)
|
|
1412
|
+
# Note: parent_wa_id and auto_minted are not supported by the protocol's update_wa method
|
|
1413
|
+
# They would need to be set during creation or via a different mechanism
|
|
1414
|
+
except Exception as e:
|
|
1415
|
+
logger.debug(f"[AUTH DEBUG] Error updating/creating WA in database: {e}")
|
|
1416
|
+
|
|
1417
|
+
return user
|