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,1634 @@
|
|
|
1
|
+
"""WA Authentication Service - Core authentication logic implementation."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import base64
|
|
5
|
+
import functools
|
|
6
|
+
import hashlib
|
|
7
|
+
import inspect
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import secrets
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union, cast
|
|
15
|
+
|
|
16
|
+
import aiofiles
|
|
17
|
+
import jwt
|
|
18
|
+
from cryptography.exceptions import InvalidSignature
|
|
19
|
+
from cryptography.hazmat.backends import default_backend
|
|
20
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
21
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
22
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
23
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
24
|
+
|
|
25
|
+
from ciris_engine.logic.persistence.stores import authentication_store
|
|
26
|
+
from ciris_engine.logic.services.base_infrastructure_service import BaseInfrastructureService
|
|
27
|
+
from ciris_engine.logic.services.lifecycle.time import TimeService
|
|
28
|
+
from ciris_engine.protocols.services.infrastructure.authentication import AuthenticationServiceProtocol
|
|
29
|
+
from ciris_engine.schemas.runtime.enums import ServiceType
|
|
30
|
+
from ciris_engine.schemas.services.authority.wise_authority import AuthenticationResult, TokenVerification, WAUpdate
|
|
31
|
+
from ciris_engine.schemas.services.authority_core import (
|
|
32
|
+
AuthorizationContext,
|
|
33
|
+
ChannelIdentity,
|
|
34
|
+
JWTSubType,
|
|
35
|
+
OAuthIdentityLink,
|
|
36
|
+
TokenType,
|
|
37
|
+
WACertificate,
|
|
38
|
+
WARole,
|
|
39
|
+
)
|
|
40
|
+
from ciris_engine.schemas.services.core import ServiceCapabilities, ServiceStatus
|
|
41
|
+
from ciris_engine.schemas.types import JSONDict
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from ciris_engine.schemas.runtime.models import Task
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
# Type variable for decorators
|
|
49
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AuthenticationService(BaseInfrastructureService, AuthenticationServiceProtocol):
|
|
53
|
+
"""Infrastructure service for WA authentication and identity management."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, db_path: str, time_service: TimeService, key_dir: Optional[str] = None) -> None:
|
|
56
|
+
"""Initialize the WA Authentication Service.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
db_path: Path to SQLite database
|
|
60
|
+
time_service: TimeService instance for time operations (required)
|
|
61
|
+
key_dir: Directory for key storage (defaults to ~/.ciris/)
|
|
62
|
+
"""
|
|
63
|
+
super().__init__() # Initialize BaseService
|
|
64
|
+
self.db_path = db_path
|
|
65
|
+
self.key_dir = Path(key_dir or os.path.expanduser("~/.ciris"))
|
|
66
|
+
self.key_dir.mkdir(mode=0o700, exist_ok=True)
|
|
67
|
+
|
|
68
|
+
# Store injected time service
|
|
69
|
+
self._time_service = time_service
|
|
70
|
+
|
|
71
|
+
# Initialize gateway secret
|
|
72
|
+
self.gateway_secret = self._get_or_create_gateway_secret()
|
|
73
|
+
|
|
74
|
+
# Cache for tokens and WAs
|
|
75
|
+
self._token_cache: Dict[str, AuthorizationContext] = {}
|
|
76
|
+
self._channel_token_cache: Dict[str, str] = {}
|
|
77
|
+
|
|
78
|
+
# Initialize database
|
|
79
|
+
self._init_database()
|
|
80
|
+
|
|
81
|
+
# Track service state
|
|
82
|
+
self._started = False
|
|
83
|
+
self._start_time: Optional[datetime] = None
|
|
84
|
+
|
|
85
|
+
# Authentication metrics tracking
|
|
86
|
+
self._auth_attempts = 0
|
|
87
|
+
self._auth_successes = 0
|
|
88
|
+
self._auth_failures = 0
|
|
89
|
+
self._token_validations = 0
|
|
90
|
+
self._permission_checks = 0
|
|
91
|
+
self._role_assignments = 0
|
|
92
|
+
self._session_count = 0
|
|
93
|
+
self._expired_sessions = 0
|
|
94
|
+
self._active_tokens = 0
|
|
95
|
+
self._revoked_tokens = 0
|
|
96
|
+
|
|
97
|
+
def get_service_type(self) -> ServiceType:
|
|
98
|
+
"""Get service type - authentication is part of wise authority infrastructure."""
|
|
99
|
+
from ciris_engine.schemas.runtime.enums import ServiceType
|
|
100
|
+
|
|
101
|
+
return ServiceType.WISE_AUTHORITY
|
|
102
|
+
|
|
103
|
+
def _get_actions(self) -> List[str]:
|
|
104
|
+
"""Get list of actions this service provides."""
|
|
105
|
+
return [
|
|
106
|
+
# Authentication operations
|
|
107
|
+
"authenticate",
|
|
108
|
+
"create_token",
|
|
109
|
+
"verify_token",
|
|
110
|
+
"verify_token_sync",
|
|
111
|
+
"create_channel_token",
|
|
112
|
+
# WA management
|
|
113
|
+
"create_wa",
|
|
114
|
+
"get_wa",
|
|
115
|
+
"update_wa",
|
|
116
|
+
"revoke_wa",
|
|
117
|
+
"list_was",
|
|
118
|
+
"rotate_keys",
|
|
119
|
+
# Utility operations
|
|
120
|
+
"bootstrap_if_needed",
|
|
121
|
+
"update_last_login",
|
|
122
|
+
"sign_task",
|
|
123
|
+
"verify_task_signature",
|
|
124
|
+
# Key operations
|
|
125
|
+
"generate_keypair",
|
|
126
|
+
"sign_data",
|
|
127
|
+
"hash_password",
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
def _check_dependencies(self) -> bool:
|
|
131
|
+
"""Check if all required dependencies are available."""
|
|
132
|
+
# Only requires time service which is provided in __init__
|
|
133
|
+
return self._time_service is not None
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _encode_public_key(pubkey_bytes: bytes) -> str:
|
|
137
|
+
"""Encode public key using base64url without padding."""
|
|
138
|
+
return base64.urlsafe_b64encode(pubkey_bytes).decode().rstrip("=")
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def _decode_public_key(pubkey_str: str) -> bytes:
|
|
142
|
+
"""Decode base64url encoded public key, adding padding if needed."""
|
|
143
|
+
# Add padding if necessary
|
|
144
|
+
padding = 4 - (len(pubkey_str) % 4)
|
|
145
|
+
if padding != 4:
|
|
146
|
+
pubkey_str += "=" * padding
|
|
147
|
+
return base64.urlsafe_b64decode(pubkey_str)
|
|
148
|
+
|
|
149
|
+
def _derive_encryption_key(self, salt: bytes) -> bytes:
|
|
150
|
+
"""Derive an encryption key from machine-specific data.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
salt: Random salt for key derivation
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
32-byte derived encryption key
|
|
157
|
+
"""
|
|
158
|
+
# Use machine ID and hostname as key material
|
|
159
|
+
machine_id = ""
|
|
160
|
+
hostname = ""
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
# Try to get machine ID (Linux)
|
|
164
|
+
machine_id_path = Path("/etc/machine-id")
|
|
165
|
+
if machine_id_path.exists():
|
|
166
|
+
machine_id = machine_id_path.read_text().strip()
|
|
167
|
+
else:
|
|
168
|
+
# Fallback to hostname
|
|
169
|
+
import socket
|
|
170
|
+
|
|
171
|
+
hostname = socket.gethostname()
|
|
172
|
+
except Exception:
|
|
173
|
+
hostname = "default"
|
|
174
|
+
|
|
175
|
+
# Combine machine-specific data with purpose identifier
|
|
176
|
+
key_material = f"{machine_id}:{hostname}:gateway-secret-encryption".encode()
|
|
177
|
+
|
|
178
|
+
# Use PBKDF2 to derive a 32-byte key with the provided salt
|
|
179
|
+
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000, backend=default_backend())
|
|
180
|
+
return kdf.derive(key_material)
|
|
181
|
+
|
|
182
|
+
def _encrypt_secret(self, secret: bytes) -> bytes:
|
|
183
|
+
"""Encrypt a secret using AES-GCM with random salt.
|
|
184
|
+
|
|
185
|
+
Format: salt (32 bytes) + nonce (12 bytes) + ciphertext + tag (16 bytes)
|
|
186
|
+
"""
|
|
187
|
+
# Generate random salt for key derivation
|
|
188
|
+
salt = os.urandom(32)
|
|
189
|
+
|
|
190
|
+
# Derive encryption key with the salt
|
|
191
|
+
key = self._derive_encryption_key(salt)
|
|
192
|
+
|
|
193
|
+
# Generate a random 96-bit nonce for GCM
|
|
194
|
+
nonce = os.urandom(12)
|
|
195
|
+
|
|
196
|
+
# Create cipher
|
|
197
|
+
cipher = Cipher(algorithms.AES(key), modes.GCM(nonce), backend=default_backend())
|
|
198
|
+
encryptor = cipher.encryptor()
|
|
199
|
+
|
|
200
|
+
# Encrypt and get tag
|
|
201
|
+
ciphertext = encryptor.update(secret) + encryptor.finalize()
|
|
202
|
+
|
|
203
|
+
# Return salt + nonce + ciphertext + tag
|
|
204
|
+
return salt + nonce + ciphertext + encryptor.tag
|
|
205
|
+
|
|
206
|
+
def _decrypt_secret(self, encrypted: bytes) -> bytes:
|
|
207
|
+
"""Decrypt a secret using AES-GCM.
|
|
208
|
+
|
|
209
|
+
Expected format: salt (32 bytes) + nonce (12 bytes) + ciphertext + tag (16 bytes)
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
# Check minimum length: salt(32) + nonce(12) + tag(16) = 60 bytes minimum
|
|
213
|
+
if len(encrypted) < 60:
|
|
214
|
+
# Handle legacy format without salt for backward compatibility
|
|
215
|
+
# Legacy format: nonce (12 bytes) + ciphertext + tag (16 bytes)
|
|
216
|
+
legacy_salt = b"ciris-gateway-encryption-salt"
|
|
217
|
+
key = self._derive_encryption_key(legacy_salt)
|
|
218
|
+
|
|
219
|
+
nonce = encrypted[:12]
|
|
220
|
+
tag = encrypted[-16:]
|
|
221
|
+
ciphertext = encrypted[12:-16]
|
|
222
|
+
|
|
223
|
+
cipher = Cipher(algorithms.AES(key), modes.GCM(nonce, tag), backend=default_backend())
|
|
224
|
+
decryptor = cipher.decryptor()
|
|
225
|
+
return decryptor.update(ciphertext) + decryptor.finalize()
|
|
226
|
+
else:
|
|
227
|
+
# Extract components for new format
|
|
228
|
+
salt = encrypted[:32]
|
|
229
|
+
nonce = encrypted[32:44]
|
|
230
|
+
tag = encrypted[-16:]
|
|
231
|
+
ciphertext = encrypted[44:-16]
|
|
232
|
+
|
|
233
|
+
# Derive key with extracted salt
|
|
234
|
+
key = self._derive_encryption_key(salt)
|
|
235
|
+
|
|
236
|
+
# Create cipher
|
|
237
|
+
cipher = Cipher(algorithms.AES(key), modes.GCM(nonce, tag), backend=default_backend())
|
|
238
|
+
decryptor = cipher.decryptor()
|
|
239
|
+
|
|
240
|
+
# Decrypt
|
|
241
|
+
return decryptor.update(ciphertext) + decryptor.finalize()
|
|
242
|
+
except Exception as e:
|
|
243
|
+
# Log the actual error for debugging (not exposed to caller)
|
|
244
|
+
logger.debug(f"Decryption failed: {type(e).__name__}: {e}")
|
|
245
|
+
# Always raise consistent error regardless of format or failure type
|
|
246
|
+
raise ValueError("Invalid encrypted data format")
|
|
247
|
+
|
|
248
|
+
def _get_or_create_gateway_secret(self) -> bytes:
|
|
249
|
+
"""Get or create the gateway secret for JWT signing."""
|
|
250
|
+
secret_path = self.key_dir / "gateway.secret"
|
|
251
|
+
encrypted_path = self.key_dir / "gateway.secret.enc"
|
|
252
|
+
|
|
253
|
+
# Try to load existing encrypted secret first
|
|
254
|
+
if encrypted_path.exists():
|
|
255
|
+
try:
|
|
256
|
+
encrypted = encrypted_path.read_bytes()
|
|
257
|
+
return self._decrypt_secret(encrypted)
|
|
258
|
+
except Exception as e:
|
|
259
|
+
logger.warning(f"Failed to decrypt gateway secret: {type(e).__name__}")
|
|
260
|
+
# Fall through to regenerate
|
|
261
|
+
|
|
262
|
+
# Check for legacy unencrypted secret
|
|
263
|
+
if secret_path.exists():
|
|
264
|
+
# Read and encrypt the existing secret
|
|
265
|
+
secret = secret_path.read_bytes()
|
|
266
|
+
encrypted = self._encrypt_secret(secret)
|
|
267
|
+
encrypted_path.write_bytes(encrypted)
|
|
268
|
+
encrypted_path.chmod(0o600)
|
|
269
|
+
# Remove the unencrypted version
|
|
270
|
+
secret_path.unlink()
|
|
271
|
+
return secret
|
|
272
|
+
|
|
273
|
+
# Generate new 32-byte secret
|
|
274
|
+
secret = secrets.token_bytes(32)
|
|
275
|
+
encrypted = self._encrypt_secret(secret)
|
|
276
|
+
encrypted_path.write_bytes(encrypted)
|
|
277
|
+
encrypted_path.chmod(0o600)
|
|
278
|
+
return secret
|
|
279
|
+
|
|
280
|
+
def _init_database(self) -> None:
|
|
281
|
+
"""Initialize database tables if needed."""
|
|
282
|
+
authentication_store.init_auth_database(self.db_path)
|
|
283
|
+
|
|
284
|
+
def _row_to_wa(self, row_dict: JSONDict) -> WACertificate:
|
|
285
|
+
"""Convert a SQLite row dictionary to a WACertificate instance."""
|
|
286
|
+
|
|
287
|
+
oauth_links_json = row_dict.get("oauth_links_json")
|
|
288
|
+
oauth_links: List[OAuthIdentityLink] = []
|
|
289
|
+
if oauth_links_json:
|
|
290
|
+
try:
|
|
291
|
+
# Type narrow: json.loads expects str, not the broader JSONDict value type
|
|
292
|
+
if isinstance(oauth_links_json, str):
|
|
293
|
+
raw_links = json.loads(oauth_links_json)
|
|
294
|
+
for link in raw_links:
|
|
295
|
+
try:
|
|
296
|
+
oauth_links.append(OAuthIdentityLink(**link))
|
|
297
|
+
except Exception as exc:
|
|
298
|
+
logger.warning("Invalid OAuth link entry skipped: %s", exc)
|
|
299
|
+
except json.JSONDecodeError:
|
|
300
|
+
logger.warning("Invalid oauth_links_json for WA %s", row_dict.get("wa_id"))
|
|
301
|
+
|
|
302
|
+
wa_dict = {
|
|
303
|
+
"wa_id": row_dict["wa_id"],
|
|
304
|
+
"name": row_dict["name"],
|
|
305
|
+
"role": row_dict["role"],
|
|
306
|
+
"pubkey": row_dict["pubkey"],
|
|
307
|
+
"jwt_kid": row_dict["jwt_kid"],
|
|
308
|
+
"password_hash": row_dict.get("password_hash"),
|
|
309
|
+
"api_key_hash": row_dict.get("api_key_hash"),
|
|
310
|
+
"oauth_provider": row_dict.get("oauth_provider"),
|
|
311
|
+
"oauth_external_id": row_dict.get("oauth_external_id"),
|
|
312
|
+
"oauth_links": oauth_links,
|
|
313
|
+
"auto_minted": bool(row_dict.get("auto_minted", 0)),
|
|
314
|
+
"veilid_id": row_dict.get("veilid_id"),
|
|
315
|
+
"parent_wa_id": row_dict.get("parent_wa_id"),
|
|
316
|
+
"parent_signature": row_dict.get("parent_signature"),
|
|
317
|
+
"scopes_json": row_dict["scopes_json"],
|
|
318
|
+
"custom_permissions_json": row_dict.get("custom_permissions_json"),
|
|
319
|
+
"adapter_id": row_dict.get("adapter_id"),
|
|
320
|
+
"adapter_name": row_dict.get("adapter_name"),
|
|
321
|
+
"adapter_metadata_json": row_dict.get("adapter_metadata_json"),
|
|
322
|
+
"created_at": row_dict["created"],
|
|
323
|
+
"last_auth": row_dict.get("last_login"),
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return WACertificate(**wa_dict)
|
|
327
|
+
|
|
328
|
+
# WAStore Protocol Implementation
|
|
329
|
+
|
|
330
|
+
async def get_wa(self, wa_id: str) -> Optional[WACertificate]:
|
|
331
|
+
"""Get WA certificate by ID."""
|
|
332
|
+
return authentication_store.get_wa_by_id(wa_id, self.db_path)
|
|
333
|
+
|
|
334
|
+
async def _get_wa_by_kid(self, jwt_kid: str) -> Optional[WACertificate]:
|
|
335
|
+
"""Get WA certificate by JWT key ID."""
|
|
336
|
+
return authentication_store.get_wa_by_kid(jwt_kid, self.db_path)
|
|
337
|
+
|
|
338
|
+
async def get_wa_by_oauth(self, provider: str, external_id: str) -> Optional[WACertificate]:
|
|
339
|
+
"""Get WA certificate by OAuth identity."""
|
|
340
|
+
return authentication_store.get_wa_by_oauth(provider, external_id, self.db_path)
|
|
341
|
+
|
|
342
|
+
async def _get_wa_by_adapter(self, adapter_id: str) -> Optional[WACertificate]:
|
|
343
|
+
"""Get WA certificate by adapter ID."""
|
|
344
|
+
return authentication_store.get_wa_by_adapter(adapter_id, self.db_path)
|
|
345
|
+
|
|
346
|
+
async def link_oauth_identity(
|
|
347
|
+
self,
|
|
348
|
+
wa_id: str,
|
|
349
|
+
provider: str,
|
|
350
|
+
external_id: str,
|
|
351
|
+
*,
|
|
352
|
+
account_name: Optional[str] = None,
|
|
353
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
354
|
+
primary: bool = False,
|
|
355
|
+
) -> Optional[WACertificate]:
|
|
356
|
+
existing = await self.get_wa(wa_id)
|
|
357
|
+
if not existing:
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
# Prevent linking to a provider that already belongs to another WA
|
|
361
|
+
match = await self.get_wa_by_oauth(provider, external_id)
|
|
362
|
+
if match and match.wa_id != wa_id:
|
|
363
|
+
raise ValueError(f"OAuth identity {provider}:{external_id} already linked to another WA")
|
|
364
|
+
|
|
365
|
+
links = list(existing.oauth_links)
|
|
366
|
+
timestamp = self._time_service.now() if self._time_service else datetime.now(timezone.utc)
|
|
367
|
+
found = False
|
|
368
|
+
for idx, link in enumerate(links):
|
|
369
|
+
if link.provider == provider and link.external_id == external_id:
|
|
370
|
+
links[idx] = link.model_copy(
|
|
371
|
+
update={
|
|
372
|
+
"account_name": account_name or link.account_name,
|
|
373
|
+
"metadata": metadata or link.metadata,
|
|
374
|
+
"linked_at": link.linked_at or timestamp,
|
|
375
|
+
}
|
|
376
|
+
)
|
|
377
|
+
found = True
|
|
378
|
+
break
|
|
379
|
+
|
|
380
|
+
if not found:
|
|
381
|
+
links.append(
|
|
382
|
+
OAuthIdentityLink(
|
|
383
|
+
provider=provider,
|
|
384
|
+
external_id=external_id,
|
|
385
|
+
account_name=account_name,
|
|
386
|
+
metadata=metadata or {},
|
|
387
|
+
linked_at=timestamp,
|
|
388
|
+
is_primary=False,
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Add identity mapping to graph for DSAR coordination
|
|
393
|
+
# This creates a "same_as" edge between wa_id and oauth provider:external_id
|
|
394
|
+
if hasattr(self, "_memory_bus") and self._memory_bus:
|
|
395
|
+
try:
|
|
396
|
+
from ciris_engine.logic.utils.identity_resolution import add_identity_mapping
|
|
397
|
+
|
|
398
|
+
# Map wa_id to OAuth external_id in identity graph
|
|
399
|
+
await add_identity_mapping(
|
|
400
|
+
wa_id,
|
|
401
|
+
"wa_id",
|
|
402
|
+
external_id,
|
|
403
|
+
f"{provider}_id",
|
|
404
|
+
self._memory_bus,
|
|
405
|
+
confidence=1.0,
|
|
406
|
+
source="oauth",
|
|
407
|
+
)
|
|
408
|
+
logger.info(
|
|
409
|
+
f"Created identity mapping: wa_id:{wa_id} -> {provider}_id:{external_id}"
|
|
410
|
+
) # NOSONAR - IDs not secrets
|
|
411
|
+
except Exception as e:
|
|
412
|
+
logger.warning(f"Failed to create identity mapping for OAuth link: {e}")
|
|
413
|
+
# Non-fatal - OAuth link still works even if graph mapping fails
|
|
414
|
+
|
|
415
|
+
if primary or (not existing.oauth_provider and not existing.oauth_external_id):
|
|
416
|
+
existing = existing.model_copy(update={"oauth_provider": provider, "oauth_external_id": external_id})
|
|
417
|
+
for link in links:
|
|
418
|
+
link.is_primary = link.provider == provider and link.external_id == external_id
|
|
419
|
+
|
|
420
|
+
# Build update kwargs, filtering out None values
|
|
421
|
+
update_kwargs = {}
|
|
422
|
+
if existing.oauth_provider:
|
|
423
|
+
update_kwargs["oauth_provider"] = existing.oauth_provider
|
|
424
|
+
if existing.oauth_external_id:
|
|
425
|
+
update_kwargs["oauth_external_id"] = existing.oauth_external_id
|
|
426
|
+
oauth_links_json = json.dumps([link.model_dump(mode="json") for link in links]) if links else None
|
|
427
|
+
if oauth_links_json:
|
|
428
|
+
update_kwargs["oauth_links_json"] = oauth_links_json
|
|
429
|
+
|
|
430
|
+
await self.update_wa(wa_id, **update_kwargs) # type: ignore[arg-type]
|
|
431
|
+
return await self.get_wa(wa_id)
|
|
432
|
+
|
|
433
|
+
async def unlink_oauth_identity(self, wa_id: str, provider: str, external_id: str) -> Optional[WACertificate]:
|
|
434
|
+
existing = await self.get_wa(wa_id)
|
|
435
|
+
if not existing:
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
links = [
|
|
439
|
+
link for link in existing.oauth_links if not (link.provider == provider and link.external_id == external_id)
|
|
440
|
+
]
|
|
441
|
+
|
|
442
|
+
payload: JSONDict = {
|
|
443
|
+
"oauth_links_json": json.dumps([link.model_dump(mode="json") for link in links]) if links else None,
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
# If unlinking the primary mapping, fall back to remaining links or clear
|
|
447
|
+
if existing.oauth_provider == provider and existing.oauth_external_id == external_id:
|
|
448
|
+
if links:
|
|
449
|
+
new_primary = next((link for link in links if link.is_primary), links[0])
|
|
450
|
+
payload["oauth_provider"] = new_primary.provider
|
|
451
|
+
payload["oauth_external_id"] = new_primary.external_id
|
|
452
|
+
for link in links:
|
|
453
|
+
link.is_primary = link is new_primary
|
|
454
|
+
payload["oauth_links_json"] = json.dumps([link.model_dump(mode="json") for link in links])
|
|
455
|
+
else:
|
|
456
|
+
payload["oauth_provider"] = None
|
|
457
|
+
payload["oauth_external_id"] = None
|
|
458
|
+
|
|
459
|
+
# Filter out None values - update_wa kwargs expects Union[str, bool, datetime], not Optional
|
|
460
|
+
filtered_payload = {k: v for k, v in payload.items() if v is not None}
|
|
461
|
+
await self.update_wa(wa_id, **filtered_payload) # type: ignore[arg-type]
|
|
462
|
+
return await self.get_wa(wa_id)
|
|
463
|
+
|
|
464
|
+
async def _store_wa_certificate(self, wa: WACertificate) -> None:
|
|
465
|
+
"""Store a WA certificate in the database."""
|
|
466
|
+
authentication_store.store_wa_certificate(wa, self.db_path)
|
|
467
|
+
|
|
468
|
+
async def _create_adapter_observer(self, adapter_id: str, name: str) -> WACertificate:
|
|
469
|
+
"""Create or reactivate adapter observer WA."""
|
|
470
|
+
# Check if observer already exists
|
|
471
|
+
existing = await self._get_wa_by_adapter(adapter_id)
|
|
472
|
+
if existing:
|
|
473
|
+
# Observer already exists and is active (since _get_wa_by_adapter only returns active ones)
|
|
474
|
+
return existing
|
|
475
|
+
|
|
476
|
+
# Generate new observer WA
|
|
477
|
+
private_key, public_key = self.generate_keypair()
|
|
478
|
+
timestamp = self._time_service.now() if self._time_service else datetime.now(timezone.utc)
|
|
479
|
+
wa_id = self._generate_wa_id(timestamp)
|
|
480
|
+
jwt_kid = f"wa-jwt-{wa_id[-6:].lower()}"
|
|
481
|
+
|
|
482
|
+
observer = WACertificate(
|
|
483
|
+
wa_id=wa_id,
|
|
484
|
+
name=name,
|
|
485
|
+
role=WARole.OBSERVER,
|
|
486
|
+
pubkey=self._encode_public_key(public_key),
|
|
487
|
+
jwt_kid=jwt_kid,
|
|
488
|
+
scopes_json='["read:any", "write:message"]',
|
|
489
|
+
adapter_id=adapter_id,
|
|
490
|
+
created_at=timestamp,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
await self._store_wa_certificate(observer)
|
|
494
|
+
return observer
|
|
495
|
+
|
|
496
|
+
async def update_wa(
|
|
497
|
+
self, wa_id: str, updates: Optional[WAUpdate] = None, **kwargs: Union[str, bool, datetime]
|
|
498
|
+
) -> Optional[WACertificate]:
|
|
499
|
+
"""Update WA certificate fields."""
|
|
500
|
+
if updates:
|
|
501
|
+
# Convert WAUpdate to kwargs
|
|
502
|
+
update_kwargs = {}
|
|
503
|
+
if updates.name:
|
|
504
|
+
update_kwargs["name"] = updates.name
|
|
505
|
+
if updates.role:
|
|
506
|
+
update_kwargs["role"] = updates.role
|
|
507
|
+
if updates.permissions:
|
|
508
|
+
update_kwargs["scopes_json"] = json.dumps(updates.permissions)
|
|
509
|
+
if updates.metadata:
|
|
510
|
+
update_kwargs["metadata"] = json.dumps(updates.metadata)
|
|
511
|
+
if updates.is_active is not None:
|
|
512
|
+
update_kwargs["active"] = str(int(updates.is_active))
|
|
513
|
+
kwargs.update(update_kwargs)
|
|
514
|
+
if not kwargs:
|
|
515
|
+
return await self.get_wa(wa_id)
|
|
516
|
+
|
|
517
|
+
# Update via store
|
|
518
|
+
authentication_store.update_wa_certificate(wa_id, kwargs, self.db_path)
|
|
519
|
+
|
|
520
|
+
# Return updated WA
|
|
521
|
+
return await self.get_wa(wa_id)
|
|
522
|
+
|
|
523
|
+
async def revoke_wa(self, wa_id: str, reason: str) -> bool:
|
|
524
|
+
"""Revoke WA certificate."""
|
|
525
|
+
# First check if the WA exists
|
|
526
|
+
existing = await self.get_wa(wa_id)
|
|
527
|
+
if not existing:
|
|
528
|
+
return False
|
|
529
|
+
|
|
530
|
+
# Update to set active=False
|
|
531
|
+
await self.update_wa(wa_id, active=False)
|
|
532
|
+
|
|
533
|
+
# Add audit log entry for revocation
|
|
534
|
+
if hasattr(self, "_audit_service") and self._audit_service:
|
|
535
|
+
await self._audit_service.log_event(
|
|
536
|
+
event_type="wa_revocation",
|
|
537
|
+
source_service="authentication",
|
|
538
|
+
details={
|
|
539
|
+
"wa_id": wa_id,
|
|
540
|
+
"reason": reason,
|
|
541
|
+
"timestamp": self._time_service.now().isoformat() if self._time_service else None,
|
|
542
|
+
},
|
|
543
|
+
)
|
|
544
|
+
logger.info(f"Revoked WA {wa_id}: {reason}")
|
|
545
|
+
return True
|
|
546
|
+
|
|
547
|
+
async def _list_all_was(self, active_only: bool = True) -> List[WACertificate]:
|
|
548
|
+
"""List all WA certificates."""
|
|
549
|
+
return authentication_store.list_wa_certificates(active_only, self.db_path)
|
|
550
|
+
|
|
551
|
+
async def update_last_login(self, wa_id: str) -> None:
|
|
552
|
+
"""Update last login timestamp."""
|
|
553
|
+
await self.update_wa(
|
|
554
|
+
wa_id, last_login=self._time_service.now() if self._time_service else datetime.now(timezone.utc)
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# JWTService Protocol Implementation
|
|
558
|
+
|
|
559
|
+
async def create_channel_token(self, wa_id: str, channel_id: str, ttl: int = 3600) -> str:
|
|
560
|
+
"""Create channel-specific token (for observers, creates long-lived adapter tokens)."""
|
|
561
|
+
# Get the WA certificate
|
|
562
|
+
wa = await self.get_wa(wa_id)
|
|
563
|
+
if not wa:
|
|
564
|
+
raise ValueError(f"WA {wa_id} not found")
|
|
565
|
+
|
|
566
|
+
payload = {
|
|
567
|
+
"sub": wa.wa_id,
|
|
568
|
+
"sub_type": JWTSubType.ANON.value,
|
|
569
|
+
"name": wa.name,
|
|
570
|
+
"scope": wa.scopes,
|
|
571
|
+
"iat": int(
|
|
572
|
+
self._time_service.timestamp() if self._time_service else datetime.now(timezone.utc).timestamp()
|
|
573
|
+
),
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
# For observer tokens, use adapter_id and make them long-lived (no expiry)
|
|
577
|
+
if wa.role == WARole.OBSERVER and wa.adapter_id:
|
|
578
|
+
payload["adapter"] = wa.adapter_id
|
|
579
|
+
# No expiry for observer tokens by default
|
|
580
|
+
if ttl > 0:
|
|
581
|
+
# Only add expiry if explicitly requested
|
|
582
|
+
payload["exp"] = (
|
|
583
|
+
int(
|
|
584
|
+
self._time_service.timestamp() if self._time_service else datetime.now(timezone.utc).timestamp()
|
|
585
|
+
)
|
|
586
|
+
+ ttl
|
|
587
|
+
)
|
|
588
|
+
else:
|
|
589
|
+
# For non-observer tokens, include channel and expiry
|
|
590
|
+
payload["channel"] = channel_id
|
|
591
|
+
payload["exp"] = (
|
|
592
|
+
int(self._time_service.timestamp() if self._time_service else datetime.now(timezone.utc).timestamp())
|
|
593
|
+
+ ttl
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
return jwt.encode(payload, self.gateway_secret, algorithm="HS256", headers={"kid": wa.jwt_kid})
|
|
597
|
+
|
|
598
|
+
def create_gateway_token(self, wa: WACertificate, expires_hours: int = 8) -> str:
|
|
599
|
+
"""Create gateway-signed token (OAuth/password auth)."""
|
|
600
|
+
now = int(self._time_service.timestamp() if self._time_service else datetime.now(timezone.utc).timestamp())
|
|
601
|
+
|
|
602
|
+
payload = {
|
|
603
|
+
"sub": wa.wa_id,
|
|
604
|
+
"sub_type": JWTSubType.OAUTH.value if wa.oauth_provider else JWTSubType.USER.value,
|
|
605
|
+
"name": wa.name,
|
|
606
|
+
"scope": wa.scopes,
|
|
607
|
+
"iat": now,
|
|
608
|
+
"exp": now + (expires_hours * 3600),
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if wa.oauth_provider:
|
|
612
|
+
payload["oauth_provider"] = wa.oauth_provider
|
|
613
|
+
|
|
614
|
+
return jwt.encode(payload, self.gateway_secret, algorithm="HS256", headers={"kid": wa.jwt_kid})
|
|
615
|
+
|
|
616
|
+
def _create_authority_token(self, wa: WACertificate, private_key: bytes) -> str:
|
|
617
|
+
"""Create WA-signed authority token."""
|
|
618
|
+
now = int(self._time_service.timestamp() if self._time_service else datetime.now(timezone.utc).timestamp())
|
|
619
|
+
|
|
620
|
+
payload = {
|
|
621
|
+
"sub": wa.wa_id,
|
|
622
|
+
"sub_type": JWTSubType.AUTHORITY.value,
|
|
623
|
+
"name": wa.name,
|
|
624
|
+
"scope": wa.scopes,
|
|
625
|
+
"iat": now,
|
|
626
|
+
"exp": now + (24 * 3600), # 24 hours
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
# Load Ed25519 private key
|
|
630
|
+
signing_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key)
|
|
631
|
+
|
|
632
|
+
return jwt.encode(payload, signing_key, algorithm="EdDSA", headers={"kid": wa.jwt_kid})
|
|
633
|
+
|
|
634
|
+
async def _verify_jwt_and_get_context(
|
|
635
|
+
self, token: str
|
|
636
|
+
) -> Optional[Tuple[AuthorizationContext, Optional[datetime]]]:
|
|
637
|
+
"""Verify any JWT token and return auth context and expiration (internal method)."""
|
|
638
|
+
try:
|
|
639
|
+
logger.debug(f"[TOKEN_VERIFY] Starting verification for token (first 20 chars): {token[:20]}...")
|
|
640
|
+
|
|
641
|
+
# Decode header to get kid
|
|
642
|
+
header = jwt.get_unverified_header(token)
|
|
643
|
+
kid = header.get("kid")
|
|
644
|
+
logger.debug(f"[TOKEN_VERIFY] Extracted kid from header: {kid}")
|
|
645
|
+
|
|
646
|
+
if not kid:
|
|
647
|
+
logger.warning("[TOKEN_VERIFY] No kid found in token header")
|
|
648
|
+
return None
|
|
649
|
+
|
|
650
|
+
# Get WA by kid
|
|
651
|
+
wa = await self._get_wa_by_kid(kid)
|
|
652
|
+
logger.debug(f"[TOKEN_VERIFY] WA lookup by kid result: {wa is not None}, wa_id={wa.wa_id if wa else None}")
|
|
653
|
+
if not wa:
|
|
654
|
+
logger.warning(f"[TOKEN_VERIFY] No WA found for kid: {kid}")
|
|
655
|
+
return None
|
|
656
|
+
|
|
657
|
+
# Try to verify with different keys/algorithms based on the issuer (kid)
|
|
658
|
+
decoded = None
|
|
659
|
+
|
|
660
|
+
logger.debug("[TOKEN_VERIFY] Attempting gateway-signed token verification (HS256)")
|
|
661
|
+
# First try gateway-signed tokens (most common)
|
|
662
|
+
try:
|
|
663
|
+
decoded = jwt.decode(token, self.gateway_secret, algorithms=["HS256"])
|
|
664
|
+
logger.debug("[TOKEN_VERIFY] Gateway verification succeeded")
|
|
665
|
+
except jwt.InvalidTokenError as e:
|
|
666
|
+
logger.debug(f"[TOKEN_VERIFY] Gateway verification failed: {type(e).__name__}")
|
|
667
|
+
|
|
668
|
+
# If gateway verification failed, try WA-signed tokens
|
|
669
|
+
if not decoded:
|
|
670
|
+
logger.debug("[TOKEN_VERIFY] Attempting WA-signed token verification (EdDSA)")
|
|
671
|
+
try:
|
|
672
|
+
public_key_bytes = self._decode_public_key(wa.pubkey)
|
|
673
|
+
public_key = ed25519.Ed25519PublicKey.from_public_bytes(public_key_bytes)
|
|
674
|
+
decoded = jwt.decode(token, public_key, algorithms=["EdDSA"])
|
|
675
|
+
logger.debug("[TOKEN_VERIFY] WA verification succeeded")
|
|
676
|
+
except jwt.InvalidTokenError as e:
|
|
677
|
+
logger.info(f"[TOKEN_VERIFY] WA verification failed: {type(e).__name__}")
|
|
678
|
+
|
|
679
|
+
# If no verification succeeded, token is invalid
|
|
680
|
+
if not decoded:
|
|
681
|
+
logger.warning("[TOKEN_VERIFY] Both gateway and WA verification failed - token invalid")
|
|
682
|
+
return None
|
|
683
|
+
|
|
684
|
+
# Validate sub_type and algorithm after verification
|
|
685
|
+
# IMPORTANT: We must validate that the token was verified with the expected algorithm
|
|
686
|
+
# to prevent algorithm confusion attacks
|
|
687
|
+
sub_type = decoded.get("sub_type")
|
|
688
|
+
|
|
689
|
+
# Determine which verification succeeded based on the algorithm
|
|
690
|
+
verified_with_gateway = False
|
|
691
|
+
verified_with_wa_key = False
|
|
692
|
+
|
|
693
|
+
# Re-verify to determine which key actually verified the token
|
|
694
|
+
try:
|
|
695
|
+
jwt.decode(token, self.gateway_secret, algorithms=["HS256"])
|
|
696
|
+
verified_with_gateway = True
|
|
697
|
+
except jwt.InvalidTokenError:
|
|
698
|
+
pass
|
|
699
|
+
|
|
700
|
+
if not verified_with_gateway:
|
|
701
|
+
try:
|
|
702
|
+
public_key_bytes = self._decode_public_key(wa.pubkey)
|
|
703
|
+
public_key = ed25519.Ed25519PublicKey.from_public_bytes(public_key_bytes)
|
|
704
|
+
jwt.decode(token, public_key, algorithms=["EdDSA"])
|
|
705
|
+
verified_with_wa_key = True
|
|
706
|
+
except jwt.InvalidTokenError:
|
|
707
|
+
pass
|
|
708
|
+
|
|
709
|
+
# Validate that the token type matches the verification method
|
|
710
|
+
if sub_type == JWTSubType.AUTHORITY.value:
|
|
711
|
+
# Authority tokens must be verified with WA key (EdDSA)
|
|
712
|
+
if not verified_with_wa_key:
|
|
713
|
+
return None
|
|
714
|
+
elif sub_type in [JWTSubType.ANON.value, JWTSubType.OAUTH.value, JWTSubType.USER.value]:
|
|
715
|
+
# Gateway tokens must be verified with gateway secret (HS256)
|
|
716
|
+
if not verified_with_gateway:
|
|
717
|
+
return None
|
|
718
|
+
else:
|
|
719
|
+
return None
|
|
720
|
+
|
|
721
|
+
# Create authorization context
|
|
722
|
+
# Determine TokenType based on the WA certificate
|
|
723
|
+
if wa.adapter_id:
|
|
724
|
+
token_type = TokenType.CHANNEL
|
|
725
|
+
elif wa.oauth_provider:
|
|
726
|
+
token_type = TokenType.OAUTH
|
|
727
|
+
else:
|
|
728
|
+
token_type = TokenType.STANDARD
|
|
729
|
+
|
|
730
|
+
context = AuthorizationContext(
|
|
731
|
+
wa_id=decoded["sub"],
|
|
732
|
+
role=wa.role,
|
|
733
|
+
token_type=token_type,
|
|
734
|
+
sub_type=JWTSubType(decoded["sub_type"]),
|
|
735
|
+
scopes=decoded["scope"],
|
|
736
|
+
channel_id=decoded.get("channel"),
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
# Update last login
|
|
740
|
+
await self.update_last_login(wa.wa_id)
|
|
741
|
+
|
|
742
|
+
# Extract expiration if present
|
|
743
|
+
exp_timestamp = decoded.get("exp")
|
|
744
|
+
expiration = None
|
|
745
|
+
if exp_timestamp:
|
|
746
|
+
expiration = datetime.fromtimestamp(exp_timestamp, tz=timezone.utc)
|
|
747
|
+
|
|
748
|
+
return (context, expiration)
|
|
749
|
+
|
|
750
|
+
except jwt.InvalidTokenError as e:
|
|
751
|
+
logger.debug(f"[TOKEN_VERIFY] JWT InvalidTokenError: {type(e).__name__}: {str(e)}")
|
|
752
|
+
return None
|
|
753
|
+
except Exception as e:
|
|
754
|
+
logger.debug(f"[TOKEN_VERIFY] Unexpected exception: {type(e).__name__}: {str(e)}", exc_info=True)
|
|
755
|
+
return None
|
|
756
|
+
|
|
757
|
+
# WACrypto Protocol Implementation
|
|
758
|
+
|
|
759
|
+
def generate_keypair(self) -> Tuple[bytes, bytes]:
|
|
760
|
+
"""Generate Ed25519 keypair (private, public)."""
|
|
761
|
+
private_key = ed25519.Ed25519PrivateKey.generate()
|
|
762
|
+
public_key = private_key.public_key()
|
|
763
|
+
|
|
764
|
+
private_bytes = private_key.private_bytes(
|
|
765
|
+
encoding=serialization.Encoding.Raw,
|
|
766
|
+
format=serialization.PrivateFormat.Raw,
|
|
767
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
public_bytes = public_key.public_bytes(
|
|
771
|
+
encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
return private_bytes, public_bytes
|
|
775
|
+
|
|
776
|
+
def sign_data(self, data: bytes, private_key: bytes) -> str:
|
|
777
|
+
"""Sign data with Ed25519 private key."""
|
|
778
|
+
signing_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key)
|
|
779
|
+
signature = signing_key.sign(data)
|
|
780
|
+
return base64.b64encode(signature).decode()
|
|
781
|
+
|
|
782
|
+
def _verify_signature(self, data: bytes, signature: str, public_key: str) -> bool:
|
|
783
|
+
"""Verify Ed25519 signature."""
|
|
784
|
+
try:
|
|
785
|
+
public_key_bytes = self._decode_public_key(public_key)
|
|
786
|
+
verify_key = ed25519.Ed25519PublicKey.from_public_bytes(public_key_bytes)
|
|
787
|
+
signature_bytes = base64.b64decode(signature)
|
|
788
|
+
|
|
789
|
+
verify_key.verify(signature_bytes, data)
|
|
790
|
+
return True
|
|
791
|
+
except (InvalidSignature, Exception):
|
|
792
|
+
return False
|
|
793
|
+
|
|
794
|
+
def hash_password(self, password: str) -> str:
|
|
795
|
+
"""Hash password using PBKDF2."""
|
|
796
|
+
salt = secrets.token_bytes(32)
|
|
797
|
+
kdf = PBKDF2HMAC(
|
|
798
|
+
algorithm=hashes.SHA256(),
|
|
799
|
+
length=32,
|
|
800
|
+
salt=salt,
|
|
801
|
+
iterations=100000,
|
|
802
|
+
)
|
|
803
|
+
key = kdf.derive(password.encode())
|
|
804
|
+
return base64.b64encode(salt + key).decode()
|
|
805
|
+
|
|
806
|
+
def _verify_password(self, password: str, hash: str) -> bool:
|
|
807
|
+
"""Verify password against hash."""
|
|
808
|
+
try:
|
|
809
|
+
decoded = base64.b64decode(hash)
|
|
810
|
+
salt = decoded[:32]
|
|
811
|
+
stored_key = decoded[32:]
|
|
812
|
+
|
|
813
|
+
kdf = PBKDF2HMAC(
|
|
814
|
+
algorithm=hashes.SHA256(),
|
|
815
|
+
length=32,
|
|
816
|
+
salt=salt,
|
|
817
|
+
iterations=100000,
|
|
818
|
+
)
|
|
819
|
+
key = kdf.derive(password.encode())
|
|
820
|
+
# Use constant-time comparison to prevent timing attacks
|
|
821
|
+
import hmac
|
|
822
|
+
|
|
823
|
+
return hmac.compare_digest(key, stored_key)
|
|
824
|
+
except Exception:
|
|
825
|
+
return False
|
|
826
|
+
|
|
827
|
+
# codeql[py/weak-sensitive-data-hashing]
|
|
828
|
+
# The use of SHA256 here is for generating a unique API key, not for hashing passwords.
|
|
829
|
+
# It is combined with a cryptographically secure random salt, which makes it resistant
|
|
830
|
+
# to rainbow table attacks. For this purpose, SHA256 is considered a secure choice.
|
|
831
|
+
def _generate_api_key(self, wa_id: str) -> str:
|
|
832
|
+
"""Generate API key for WA."""
|
|
833
|
+
# Include wa_id in key derivation for uniqueness
|
|
834
|
+
key_material = f"{wa_id}:{secrets.token_hex(32)}"
|
|
835
|
+
return hashlib.sha256(key_material.encode()).hexdigest()
|
|
836
|
+
|
|
837
|
+
def _generate_wa_id(self, timestamp: datetime) -> str:
|
|
838
|
+
"""Generate a unique WA (Wise Authority) ID.
|
|
839
|
+
|
|
840
|
+
Format: wa-YYYY-MM-DD-XXXXXX
|
|
841
|
+
- wa: Fixed prefix for all WA IDs
|
|
842
|
+
- YYYY-MM-DD: Date from the provided timestamp
|
|
843
|
+
- XXXXXX: 6 uppercase hexadecimal characters (cryptographically random)
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
timestamp: The timestamp to use for the date portion
|
|
847
|
+
|
|
848
|
+
Returns:
|
|
849
|
+
A unique WA ID string
|
|
850
|
+
|
|
851
|
+
Example:
|
|
852
|
+
wa-2025-07-14-A3F2B1
|
|
853
|
+
"""
|
|
854
|
+
date_str = timestamp.strftime("%Y-%m-%d")
|
|
855
|
+
# Generate 3 random bytes = 6 hex characters
|
|
856
|
+
random_suffix = secrets.token_hex(3).upper()
|
|
857
|
+
return f"wa-{date_str}-{random_suffix}"
|
|
858
|
+
|
|
859
|
+
# AuthenticationServiceProtocol Implementation
|
|
860
|
+
|
|
861
|
+
async def authenticate(self, token: str) -> Optional[AuthenticationResult]:
|
|
862
|
+
"""Authenticate a WA token and return identity info."""
|
|
863
|
+
self._auth_attempts += 1
|
|
864
|
+
try:
|
|
865
|
+
claims = await self.verify_token(token)
|
|
866
|
+
if not claims:
|
|
867
|
+
self._auth_failures += 1
|
|
868
|
+
return None
|
|
869
|
+
|
|
870
|
+
if hasattr(claims, "get"):
|
|
871
|
+
wa_id = claims.get("wa_id")
|
|
872
|
+
else:
|
|
873
|
+
wa_id = getattr(claims, "wa_id", None)
|
|
874
|
+
if not wa_id:
|
|
875
|
+
self._auth_failures += 1
|
|
876
|
+
return None
|
|
877
|
+
|
|
878
|
+
# Update last login
|
|
879
|
+
await self.update_last_login(wa_id)
|
|
880
|
+
|
|
881
|
+
# Get WA details
|
|
882
|
+
wa = await self.get_wa(wa_id)
|
|
883
|
+
if not wa:
|
|
884
|
+
self._auth_failures += 1
|
|
885
|
+
return None
|
|
886
|
+
|
|
887
|
+
self._auth_successes += 1
|
|
888
|
+
self._session_count += 1 # Track active session
|
|
889
|
+
return AuthenticationResult(
|
|
890
|
+
authenticated=True,
|
|
891
|
+
wa_id=wa_id,
|
|
892
|
+
name=wa.name,
|
|
893
|
+
role=wa.role.value,
|
|
894
|
+
expires_at=(
|
|
895
|
+
datetime.fromtimestamp(claims.get("exp", 0), tz=timezone.utc)
|
|
896
|
+
if hasattr(claims, "get")
|
|
897
|
+
else (self._time_service.now() if self._time_service else datetime.now(timezone.utc))
|
|
898
|
+
),
|
|
899
|
+
permissions=wa.scopes,
|
|
900
|
+
metadata={},
|
|
901
|
+
)
|
|
902
|
+
except Exception as e:
|
|
903
|
+
logger.error(f"Authentication failed: {e}")
|
|
904
|
+
self._auth_failures += 1
|
|
905
|
+
return None
|
|
906
|
+
|
|
907
|
+
async def create_token(self, wa_id: str, token_type: TokenType, ttl: int = 3600) -> str:
|
|
908
|
+
"""Create a new authentication token."""
|
|
909
|
+
wa = await self.get_wa(wa_id)
|
|
910
|
+
if not wa:
|
|
911
|
+
raise ValueError(f"WA {wa_id} not found")
|
|
912
|
+
|
|
913
|
+
# Map TokenType enum to our token creation methods
|
|
914
|
+
if token_type == TokenType.CHANNEL:
|
|
915
|
+
# CHANNEL tokens require a channel_id, use create_channel_token directly
|
|
916
|
+
raise ValueError("CHANNEL tokens require channel_id - use create_channel_token directly")
|
|
917
|
+
elif token_type == TokenType.STANDARD:
|
|
918
|
+
return self.create_gateway_token(wa, expires_hours=ttl // 3600)
|
|
919
|
+
else:
|
|
920
|
+
raise ValueError(f"Unsupported token type: {token_type}")
|
|
921
|
+
|
|
922
|
+
async def verify_token(self, token: str) -> Optional[TokenVerification]:
|
|
923
|
+
"""Verify and decode a token (AuthenticationServiceProtocol version)."""
|
|
924
|
+
try:
|
|
925
|
+
# Directly call _verify_jwt_and_get_context to get both context and expiration
|
|
926
|
+
result = await self._verify_jwt_and_get_context(token)
|
|
927
|
+
if not result:
|
|
928
|
+
return None
|
|
929
|
+
|
|
930
|
+
context, expiration = result
|
|
931
|
+
|
|
932
|
+
# Get the WA name
|
|
933
|
+
wa = await self.get_wa(context.wa_id)
|
|
934
|
+
wa_name = wa.name if wa else context.wa_id
|
|
935
|
+
|
|
936
|
+
# Use expiration from token, or current time as fallback
|
|
937
|
+
expires_at = (
|
|
938
|
+
expiration
|
|
939
|
+
if expiration
|
|
940
|
+
else (self._time_service.now() if self._time_service else datetime.now(timezone.utc))
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
return TokenVerification(
|
|
944
|
+
valid=True,
|
|
945
|
+
wa_id=context.wa_id,
|
|
946
|
+
name=wa_name,
|
|
947
|
+
role=context.role.value,
|
|
948
|
+
expires_at=expires_at,
|
|
949
|
+
error=None,
|
|
950
|
+
)
|
|
951
|
+
except Exception as e:
|
|
952
|
+
logger.error(f"Token verification failed: {e}")
|
|
953
|
+
return TokenVerification(valid=False, wa_id=None, name=None, role=None, expires_at=None, error=str(e))
|
|
954
|
+
|
|
955
|
+
async def create_wa(
|
|
956
|
+
self, name: str, email: str, scopes: List[str], role: WARole = WARole.OBSERVER
|
|
957
|
+
) -> WACertificate:
|
|
958
|
+
"""Create a new Wise Authority identity."""
|
|
959
|
+
# Generate keypair
|
|
960
|
+
private_key, public_key = self.generate_keypair()
|
|
961
|
+
|
|
962
|
+
# Create certificate
|
|
963
|
+
timestamp = self._time_service.now() if self._time_service else datetime.now(timezone.utc)
|
|
964
|
+
wa_id = self._generate_wa_id(timestamp)
|
|
965
|
+
jwt_kid = f"wa-jwt-{wa_id[-6:].lower()}"
|
|
966
|
+
|
|
967
|
+
wa_cert = WACertificate(
|
|
968
|
+
wa_id=wa_id,
|
|
969
|
+
name=name,
|
|
970
|
+
role=role,
|
|
971
|
+
pubkey=self._encode_public_key(public_key),
|
|
972
|
+
jwt_kid=jwt_kid,
|
|
973
|
+
scopes_json=json.dumps(scopes),
|
|
974
|
+
created_at=timestamp,
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
# Store in database
|
|
978
|
+
await self._store_wa_certificate(wa_cert)
|
|
979
|
+
|
|
980
|
+
# Store private key (in production, this would be in a secure key store)
|
|
981
|
+
# For now, we're not storing it as it's managed externally
|
|
982
|
+
|
|
983
|
+
# Add audit log entry for WA creation (mint)
|
|
984
|
+
if hasattr(self, "_audit_service") and self._audit_service:
|
|
985
|
+
from ciris_engine.schemas.audit.core import EventPayload
|
|
986
|
+
|
|
987
|
+
event_data = EventPayload(
|
|
988
|
+
action="wa_mint",
|
|
989
|
+
service_name="authentication",
|
|
990
|
+
user_id=wa_id, # Use wa_id as user_id to track which WA was created
|
|
991
|
+
result="success",
|
|
992
|
+
)
|
|
993
|
+
await self._audit_service.log_event(event_type="wa_mint", event_data=event_data)
|
|
994
|
+
|
|
995
|
+
return wa_cert
|
|
996
|
+
|
|
997
|
+
async def list_was(self, active_only: bool = True) -> List[WACertificate]:
|
|
998
|
+
"""List Wise Authority identities."""
|
|
999
|
+
return await self._list_all_was(active_only=active_only)
|
|
1000
|
+
|
|
1001
|
+
async def rotate_keys(self, wa_id: str) -> bool:
|
|
1002
|
+
"""Rotate cryptographic keys for a WA."""
|
|
1003
|
+
wa = await self.get_wa(wa_id)
|
|
1004
|
+
if not wa:
|
|
1005
|
+
return False
|
|
1006
|
+
|
|
1007
|
+
# Generate new keypair
|
|
1008
|
+
private_key, public_key = self.generate_keypair()
|
|
1009
|
+
|
|
1010
|
+
# Update WA with new public key
|
|
1011
|
+
wa.pubkey = self._encode_public_key(public_key)
|
|
1012
|
+
|
|
1013
|
+
# Store the updated certificate
|
|
1014
|
+
await self.update_wa(wa_id, pubkey=wa.pubkey)
|
|
1015
|
+
|
|
1016
|
+
logger.info(f"Rotated keys for WA {wa_id}")
|
|
1017
|
+
return True
|
|
1018
|
+
|
|
1019
|
+
# Original verify_token implementation (renamed for internal use)
|
|
1020
|
+
async def _verify_token_internal(self, token: Optional[str]) -> Optional[AuthorizationContext]:
|
|
1021
|
+
"""Authenticate request and return auth context."""
|
|
1022
|
+
if not token:
|
|
1023
|
+
return None
|
|
1024
|
+
|
|
1025
|
+
# Check cache first
|
|
1026
|
+
if token in self._token_cache:
|
|
1027
|
+
return self._token_cache[token]
|
|
1028
|
+
|
|
1029
|
+
# Verify token
|
|
1030
|
+
result = await self._verify_jwt_and_get_context(token)
|
|
1031
|
+
|
|
1032
|
+
if result:
|
|
1033
|
+
context, _ = result # We don't need expiration here
|
|
1034
|
+
# Cache valid tokens
|
|
1035
|
+
self._token_cache[token] = context
|
|
1036
|
+
return context
|
|
1037
|
+
|
|
1038
|
+
return None
|
|
1039
|
+
|
|
1040
|
+
def _require_scope(self, scope: str) -> Callable[[F], F]:
|
|
1041
|
+
"""Decorator to require specific scope for endpoint."""
|
|
1042
|
+
|
|
1043
|
+
def decorator(func: F) -> F:
|
|
1044
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1045
|
+
# Extract auth context from kwargs
|
|
1046
|
+
auth_context = kwargs.get("auth_context")
|
|
1047
|
+
|
|
1048
|
+
if not auth_context:
|
|
1049
|
+
# Try to get token and verify it
|
|
1050
|
+
token = kwargs.get("token")
|
|
1051
|
+
if token:
|
|
1052
|
+
auth_context = await self._verify_token_internal(token)
|
|
1053
|
+
|
|
1054
|
+
if not auth_context:
|
|
1055
|
+
raise ValueError(f"Authentication required for scope '{scope}'")
|
|
1056
|
+
|
|
1057
|
+
# Check if the auth context has the required scope
|
|
1058
|
+
if not hasattr(auth_context, "scopes") or scope not in auth_context.scopes:
|
|
1059
|
+
raise ValueError(
|
|
1060
|
+
f"Insufficient permissions: Requires scope '{scope}', "
|
|
1061
|
+
f"but user has scopes: {getattr(auth_context, 'scopes', [])}"
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
# Add auth context to kwargs if not already present
|
|
1065
|
+
if "auth_context" not in kwargs:
|
|
1066
|
+
kwargs["auth_context"] = auth_context
|
|
1067
|
+
|
|
1068
|
+
return await func(*args, **kwargs)
|
|
1069
|
+
|
|
1070
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1071
|
+
# Extract auth context from kwargs
|
|
1072
|
+
auth_context = kwargs.get("auth_context")
|
|
1073
|
+
|
|
1074
|
+
if not auth_context:
|
|
1075
|
+
raise ValueError(f"Authentication required for scope '{scope}'")
|
|
1076
|
+
|
|
1077
|
+
# Check if the auth context has the required scope
|
|
1078
|
+
if not hasattr(auth_context, "scopes") or scope not in auth_context.scopes:
|
|
1079
|
+
raise ValueError(
|
|
1080
|
+
f"Insufficient permissions: Requires scope '{scope}', "
|
|
1081
|
+
f"but user has scopes: {getattr(auth_context, 'scopes', [])}"
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
return func(*args, **kwargs)
|
|
1085
|
+
|
|
1086
|
+
# Preserve function metadata and check if the function is async
|
|
1087
|
+
if asyncio.iscoroutinefunction(func):
|
|
1088
|
+
wrapper = functools.wraps(func)(async_wrapper)
|
|
1089
|
+
else:
|
|
1090
|
+
wrapper = functools.wraps(func)(sync_wrapper)
|
|
1091
|
+
|
|
1092
|
+
# Add metadata to indicate this function requires authentication
|
|
1093
|
+
setattr(wrapper, "_requires_scope", scope)
|
|
1094
|
+
|
|
1095
|
+
return wrapper # type: ignore
|
|
1096
|
+
|
|
1097
|
+
return decorator
|
|
1098
|
+
|
|
1099
|
+
def _require_wa_auth(self, scope: str) -> Callable[[F], F]:
|
|
1100
|
+
"""Decorator to require WA authentication with specific scope.
|
|
1101
|
+
|
|
1102
|
+
This decorator checks for authentication tokens in the following order:
|
|
1103
|
+
1. 'token' parameter in the function arguments
|
|
1104
|
+
2. 'auth_context' in the function arguments
|
|
1105
|
+
3. Token from the context (if available)
|
|
1106
|
+
|
|
1107
|
+
Args:
|
|
1108
|
+
scope: The required scope for accessing the decorated function
|
|
1109
|
+
|
|
1110
|
+
Returns:
|
|
1111
|
+
Decorated function that enforces authentication
|
|
1112
|
+
"""
|
|
1113
|
+
|
|
1114
|
+
def decorator(func: F) -> F:
|
|
1115
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1116
|
+
# Extract token from various sources
|
|
1117
|
+
token = None
|
|
1118
|
+
auth_context = None
|
|
1119
|
+
|
|
1120
|
+
# Check if 'token' is in kwargs
|
|
1121
|
+
if "token" in kwargs:
|
|
1122
|
+
token = kwargs.get("token")
|
|
1123
|
+
|
|
1124
|
+
# Check if 'auth_context' is already provided
|
|
1125
|
+
if "auth_context" in kwargs:
|
|
1126
|
+
auth_context = kwargs.get("auth_context")
|
|
1127
|
+
|
|
1128
|
+
# If no auth context yet, try to verify the token
|
|
1129
|
+
if not auth_context and token:
|
|
1130
|
+
auth_context = await self.verify_token(token)
|
|
1131
|
+
|
|
1132
|
+
# Check if authentication succeeded
|
|
1133
|
+
if not auth_context:
|
|
1134
|
+
raise ValueError("Authentication required: No valid token provided")
|
|
1135
|
+
|
|
1136
|
+
# Verify the required scope
|
|
1137
|
+
if not auth_context.has_scope(scope):
|
|
1138
|
+
raise ValueError(
|
|
1139
|
+
f"Insufficient permissions: Requires scope '{scope}', "
|
|
1140
|
+
f"but user has scopes: {auth_context.scopes}"
|
|
1141
|
+
)
|
|
1142
|
+
|
|
1143
|
+
# Check if the function accepts auth_context parameter
|
|
1144
|
+
sig = inspect.signature(func)
|
|
1145
|
+
|
|
1146
|
+
# If function has **kwargs or auth_context parameter, pass it
|
|
1147
|
+
if "auth_context" in sig.parameters or any(
|
|
1148
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
|
1149
|
+
):
|
|
1150
|
+
kwargs["auth_context"] = auth_context
|
|
1151
|
+
|
|
1152
|
+
# Call the original function
|
|
1153
|
+
return await func(*args, **kwargs)
|
|
1154
|
+
|
|
1155
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1156
|
+
# For synchronous functions, we need to handle auth differently
|
|
1157
|
+
# This is a simplified version that expects auth_context to be pre-verified
|
|
1158
|
+
auth_context = kwargs.get("auth_context")
|
|
1159
|
+
|
|
1160
|
+
if not auth_context:
|
|
1161
|
+
raise ValueError("Authentication required: No auth_context provided")
|
|
1162
|
+
|
|
1163
|
+
# Verify the required scope
|
|
1164
|
+
if not hasattr(auth_context, "has_scope") or not auth_context.has_scope(scope):
|
|
1165
|
+
raise ValueError(f"Insufficient permissions: Requires scope '{scope}'")
|
|
1166
|
+
|
|
1167
|
+
return func(*args, **kwargs)
|
|
1168
|
+
|
|
1169
|
+
# Preserve function metadata and check if the function is async
|
|
1170
|
+
if asyncio.iscoroutinefunction(func):
|
|
1171
|
+
wrapper = functools.wraps(func)(async_wrapper)
|
|
1172
|
+
else:
|
|
1173
|
+
wrapper = functools.wraps(func)(sync_wrapper)
|
|
1174
|
+
|
|
1175
|
+
# Add metadata to indicate this function requires authentication
|
|
1176
|
+
setattr(wrapper, "_requires_wa_auth", True)
|
|
1177
|
+
setattr(wrapper, "_required_scope", scope)
|
|
1178
|
+
|
|
1179
|
+
return wrapper # type: ignore
|
|
1180
|
+
|
|
1181
|
+
return decorator
|
|
1182
|
+
|
|
1183
|
+
def _get_adapter_token(self, adapter_id: str) -> Optional[str]:
|
|
1184
|
+
"""Get cached adapter token."""
|
|
1185
|
+
return self._channel_token_cache.get(adapter_id)
|
|
1186
|
+
|
|
1187
|
+
# Additional helper methods
|
|
1188
|
+
|
|
1189
|
+
async def _get_system_wa(self) -> Optional[WACertificate]:
|
|
1190
|
+
"""Get the system WA certificate if it exists."""
|
|
1191
|
+
was = await self._list_all_was()
|
|
1192
|
+
for wa in was:
|
|
1193
|
+
if wa.role == WARole.AUTHORITY and wa.name == "CIRIS System Authority":
|
|
1194
|
+
return wa
|
|
1195
|
+
return None
|
|
1196
|
+
|
|
1197
|
+
async def get_system_wa_id(self) -> Optional[str]:
|
|
1198
|
+
"""Get the system WA ID for signing system tasks."""
|
|
1199
|
+
system_wa = await self._get_system_wa()
|
|
1200
|
+
return system_wa.wa_id if system_wa else None
|
|
1201
|
+
|
|
1202
|
+
async def ensure_system_wa_exists(self) -> Optional[str]:
|
|
1203
|
+
"""Ensure the system WA exists, creating it if a ROOT WA is available.
|
|
1204
|
+
|
|
1205
|
+
This should be called after creating a ROOT WA during setup to ensure
|
|
1206
|
+
the system WA is immediately available for signing system tasks.
|
|
1207
|
+
|
|
1208
|
+
Returns:
|
|
1209
|
+
The system WA ID if it exists or was created, None if no ROOT WA exists.
|
|
1210
|
+
"""
|
|
1211
|
+
# Check if system WA already exists
|
|
1212
|
+
system_wa = await self._get_system_wa()
|
|
1213
|
+
if system_wa:
|
|
1214
|
+
return system_wa.wa_id
|
|
1215
|
+
|
|
1216
|
+
# Find a ROOT WA to use as parent
|
|
1217
|
+
for wa in await self._list_all_was():
|
|
1218
|
+
if wa.role == WARole.ROOT:
|
|
1219
|
+
# Create system WA as child of root
|
|
1220
|
+
new_system_wa = await self._create_system_wa_certificate(wa.wa_id)
|
|
1221
|
+
logger.info(f"✅ Created system WA {new_system_wa.wa_id} as child of ROOT {wa.wa_id}")
|
|
1222
|
+
return new_system_wa.wa_id
|
|
1223
|
+
|
|
1224
|
+
logger.warning("Cannot create system WA - no ROOT WA found")
|
|
1225
|
+
return None
|
|
1226
|
+
|
|
1227
|
+
async def _create_system_wa_certificate(self, parent_wa_id: str) -> WACertificate:
|
|
1228
|
+
"""Create the system WA certificate as a child of the root certificate.
|
|
1229
|
+
|
|
1230
|
+
This certificate is used to sign system-generated tasks like WAKEUP and DREAM.
|
|
1231
|
+
It respects the authority of the root certificate holder.
|
|
1232
|
+
"""
|
|
1233
|
+
# Generate keypair for system WA
|
|
1234
|
+
private_key, public_key = self.generate_keypair()
|
|
1235
|
+
|
|
1236
|
+
# Store the private key securely
|
|
1237
|
+
system_key_path = self.key_dir / "system_wa.key"
|
|
1238
|
+
system_key_path.write_bytes(private_key)
|
|
1239
|
+
system_key_path.chmod(0o600)
|
|
1240
|
+
|
|
1241
|
+
# Create the system WA certificate
|
|
1242
|
+
timestamp = self._time_service.now() if self._time_service else datetime.now(timezone.utc)
|
|
1243
|
+
wa_id = self._generate_wa_id(timestamp)
|
|
1244
|
+
jwt_kid = f"wa-jwt-{wa_id[-6:].lower()}"
|
|
1245
|
+
|
|
1246
|
+
# Create data to sign - the certificate attributes
|
|
1247
|
+
cert_data = {
|
|
1248
|
+
"wa_id": wa_id,
|
|
1249
|
+
"name": "CIRIS System Authority",
|
|
1250
|
+
"role": WARole.AUTHORITY.value,
|
|
1251
|
+
"pubkey": self._encode_public_key(public_key),
|
|
1252
|
+
"parent_wa_id": parent_wa_id,
|
|
1253
|
+
"created_at": timestamp.isoformat(),
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
# Sign with the system's private key (self-signed for now)
|
|
1257
|
+
# In production, this would be signed by the parent's private key
|
|
1258
|
+
signature_data = json.dumps(cert_data, sort_keys=True, separators=(",", ":"))
|
|
1259
|
+
parent_signature = self.sign_data(signature_data.encode("utf-8"), private_key)
|
|
1260
|
+
|
|
1261
|
+
# Create the certificate
|
|
1262
|
+
system_wa = WACertificate(
|
|
1263
|
+
wa_id=wa_id,
|
|
1264
|
+
name="CIRIS System Authority",
|
|
1265
|
+
role=WARole.AUTHORITY,
|
|
1266
|
+
pubkey=self._encode_public_key(public_key),
|
|
1267
|
+
jwt_kid=jwt_kid,
|
|
1268
|
+
parent_wa_id=parent_wa_id,
|
|
1269
|
+
parent_signature=parent_signature,
|
|
1270
|
+
scopes_json=json.dumps(
|
|
1271
|
+
[
|
|
1272
|
+
"system.task.create",
|
|
1273
|
+
"system.task.sign",
|
|
1274
|
+
"system.wakeup",
|
|
1275
|
+
"system.dream",
|
|
1276
|
+
"system.shutdown",
|
|
1277
|
+
"memory.read",
|
|
1278
|
+
"memory.write",
|
|
1279
|
+
]
|
|
1280
|
+
),
|
|
1281
|
+
created_at=timestamp,
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
# Store in database
|
|
1285
|
+
await self._store_wa_certificate(system_wa)
|
|
1286
|
+
logger.info(f"Created system WA certificate: {wa_id} (child of {parent_wa_id})")
|
|
1287
|
+
|
|
1288
|
+
return system_wa
|
|
1289
|
+
|
|
1290
|
+
async def sign_task(self, task: "Task", wa_id: str) -> Tuple[str, str]:
|
|
1291
|
+
"""Sign a task with a WA's private key.
|
|
1292
|
+
|
|
1293
|
+
Returns:
|
|
1294
|
+
Tuple of (signature, signed_at timestamp)
|
|
1295
|
+
"""
|
|
1296
|
+
# Get the WA certificate
|
|
1297
|
+
wa = await self.get_wa(wa_id)
|
|
1298
|
+
if not wa:
|
|
1299
|
+
raise ValueError(f"WA {wa_id} not found")
|
|
1300
|
+
|
|
1301
|
+
# Load the private key
|
|
1302
|
+
if wa.name == "CIRIS System Authority":
|
|
1303
|
+
# System WA key is stored locally
|
|
1304
|
+
key_path = self.key_dir / "system_wa.key"
|
|
1305
|
+
if not key_path.exists():
|
|
1306
|
+
raise ValueError("System WA private key not found")
|
|
1307
|
+
private_key = key_path.read_bytes()
|
|
1308
|
+
else:
|
|
1309
|
+
# Other WAs would have their keys managed differently
|
|
1310
|
+
raise ValueError(f"Private key management not implemented for WA {wa_id}")
|
|
1311
|
+
|
|
1312
|
+
# Create canonical representation of task for signing
|
|
1313
|
+
task_data = {
|
|
1314
|
+
"task_id": task.task_id,
|
|
1315
|
+
"description": task.description,
|
|
1316
|
+
"status": task.status.value if hasattr(task.status, "value") else str(task.status),
|
|
1317
|
+
"priority": task.priority,
|
|
1318
|
+
"created_at": task.created_at,
|
|
1319
|
+
"parent_task_id": task.parent_task_id,
|
|
1320
|
+
"context": task.context.model_dump() if task.context else None,
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
canonical_json = json.dumps(task_data, sort_keys=True, separators=(",", ":"))
|
|
1324
|
+
signature = self.sign_data(canonical_json.encode("utf-8"), private_key)
|
|
1325
|
+
signed_at = (self._time_service.now() if self._time_service else datetime.now(timezone.utc)).isoformat()
|
|
1326
|
+
|
|
1327
|
+
return signature, signed_at
|
|
1328
|
+
|
|
1329
|
+
async def verify_task_signature(self, task: "Task") -> bool:
|
|
1330
|
+
"""Verify a task's signature.
|
|
1331
|
+
|
|
1332
|
+
Returns:
|
|
1333
|
+
True if signature is valid, False otherwise
|
|
1334
|
+
"""
|
|
1335
|
+
if not task.signed_by or not task.signature or not task.signed_at:
|
|
1336
|
+
return False
|
|
1337
|
+
|
|
1338
|
+
# Get the WA that signed it
|
|
1339
|
+
wa = await self.get_wa(task.signed_by)
|
|
1340
|
+
if not wa:
|
|
1341
|
+
return False
|
|
1342
|
+
|
|
1343
|
+
# Recreate the canonical representation
|
|
1344
|
+
task_data = {
|
|
1345
|
+
"task_id": task.task_id,
|
|
1346
|
+
"description": task.description,
|
|
1347
|
+
"status": task.status.value if hasattr(task.status, "value") else str(task.status),
|
|
1348
|
+
"priority": task.priority,
|
|
1349
|
+
"created_at": task.created_at,
|
|
1350
|
+
"parent_task_id": task.parent_task_id,
|
|
1351
|
+
"context": task.context.model_dump() if task.context else None,
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
canonical_json = json.dumps(task_data, sort_keys=True, separators=(",", ":"))
|
|
1355
|
+
|
|
1356
|
+
# Verify the signature
|
|
1357
|
+
return self._verify_signature(canonical_json.encode("utf-8"), task.signature, wa.pubkey)
|
|
1358
|
+
|
|
1359
|
+
async def bootstrap_if_needed(self) -> None:
|
|
1360
|
+
"""Bootstrap the system if no WAs exist."""
|
|
1361
|
+
was = await self._list_all_was()
|
|
1362
|
+
|
|
1363
|
+
# Check if we have a root WA
|
|
1364
|
+
has_root = any(wa.role == WARole.ROOT for wa in was)
|
|
1365
|
+
|
|
1366
|
+
if not has_root:
|
|
1367
|
+
# Load and insert root certificate
|
|
1368
|
+
seed_path = Path(__file__).parent.parent.parent / "seed" / "root_pub.json"
|
|
1369
|
+
if seed_path.exists():
|
|
1370
|
+
async with aiofiles.open(seed_path) as f:
|
|
1371
|
+
content = await f.read()
|
|
1372
|
+
root_data = json.loads(content)
|
|
1373
|
+
|
|
1374
|
+
# Convert created timestamp - handle both 'Z' and '+00:00' formats
|
|
1375
|
+
created_str = root_data["created"]
|
|
1376
|
+
if created_str.endswith("Z"):
|
|
1377
|
+
created_str = created_str[:-1] + "+00:00"
|
|
1378
|
+
root_data["created"] = datetime.fromisoformat(created_str)
|
|
1379
|
+
|
|
1380
|
+
root_wa = WACertificate(**root_data)
|
|
1381
|
+
await self._store_wa_certificate(root_wa)
|
|
1382
|
+
|
|
1383
|
+
logger.info(f"Loaded root WA certificate: {root_wa.wa_id}")
|
|
1384
|
+
|
|
1385
|
+
# Check if system WA exists, create if not (whether we just loaded root or not)
|
|
1386
|
+
system_wa = await self._get_system_wa()
|
|
1387
|
+
if not system_wa:
|
|
1388
|
+
# Find the root certificate
|
|
1389
|
+
found_root_wa: Optional[WACertificate] = None
|
|
1390
|
+
for wa in await self._list_all_was():
|
|
1391
|
+
if wa.role == WARole.ROOT:
|
|
1392
|
+
found_root_wa = wa
|
|
1393
|
+
break
|
|
1394
|
+
|
|
1395
|
+
if found_root_wa:
|
|
1396
|
+
# Create system WA certificate as child of root
|
|
1397
|
+
await self._create_system_wa_certificate(found_root_wa.wa_id)
|
|
1398
|
+
else:
|
|
1399
|
+
logger.warning("No root WA certificate found - cannot create system WA")
|
|
1400
|
+
|
|
1401
|
+
async def _create_channel_token_for_adapter(self, adapter_type: str, adapter_info: JSONDict) -> str:
|
|
1402
|
+
"""Create a channel token for an adapter."""
|
|
1403
|
+
# Ensure adapter_info has proper structure
|
|
1404
|
+
if not adapter_info:
|
|
1405
|
+
adapter_info = {}
|
|
1406
|
+
|
|
1407
|
+
# Add default instance_id if not present
|
|
1408
|
+
if "instance_id" not in adapter_info:
|
|
1409
|
+
adapter_info["instance_id"] = "default"
|
|
1410
|
+
|
|
1411
|
+
# Create channel identity from adapter info
|
|
1412
|
+
channel_identity = ChannelIdentity(
|
|
1413
|
+
adapter_type=adapter_type,
|
|
1414
|
+
adapter_instance_id=adapter_info.get("instance_id", "default"),
|
|
1415
|
+
external_user_id=adapter_info.get("user_id", "system"),
|
|
1416
|
+
external_username=adapter_info.get("username", adapter_type),
|
|
1417
|
+
metadata=adapter_info,
|
|
1418
|
+
)
|
|
1419
|
+
|
|
1420
|
+
# Create or get adapter observer
|
|
1421
|
+
adapter_id = f"{adapter_type}_{channel_identity.adapter_instance_id}"
|
|
1422
|
+
observer = await self._create_adapter_observer(adapter_id, f"{adapter_type}_observer")
|
|
1423
|
+
|
|
1424
|
+
# Generate token - for observers, channel_id is not used
|
|
1425
|
+
token = await self.create_channel_token(observer.wa_id, adapter_id, ttl=0) # ttl=0 means no expiry
|
|
1426
|
+
|
|
1427
|
+
# Cache the token
|
|
1428
|
+
self._channel_token_cache[adapter_id] = token
|
|
1429
|
+
|
|
1430
|
+
return token
|
|
1431
|
+
|
|
1432
|
+
def verify_token_sync(self, token: str) -> Optional[JSONDict]:
|
|
1433
|
+
"""Synchronously verify a token (for non-async contexts)."""
|
|
1434
|
+
try:
|
|
1435
|
+
# For sync verification, we can only verify gateway-signed tokens
|
|
1436
|
+
# since authority tokens require async DB lookups for public keys
|
|
1437
|
+
|
|
1438
|
+
# Try gateway-signed token verification
|
|
1439
|
+
try:
|
|
1440
|
+
# Verify the token with gateway secret first
|
|
1441
|
+
decoded = jwt.decode(token, self.gateway_secret, algorithms=["HS256"])
|
|
1442
|
+
|
|
1443
|
+
# Now that the token is verified, we can trust its contents
|
|
1444
|
+
# Validate that this is indeed a gateway-signed token type
|
|
1445
|
+
sub_type = decoded.get("sub_type")
|
|
1446
|
+
if sub_type in [JWTSubType.ANON.value, JWTSubType.OAUTH.value, JWTSubType.USER.value]:
|
|
1447
|
+
# Valid gateway token
|
|
1448
|
+
return dict(decoded)
|
|
1449
|
+
else:
|
|
1450
|
+
# Invalid sub_type for gateway token
|
|
1451
|
+
return None
|
|
1452
|
+
|
|
1453
|
+
except jwt.InvalidTokenError:
|
|
1454
|
+
# Token failed verification with gateway secret
|
|
1455
|
+
pass
|
|
1456
|
+
|
|
1457
|
+
# Authority tokens require async DB access for public key retrieval
|
|
1458
|
+
# So we cannot verify them in sync mode
|
|
1459
|
+
return None
|
|
1460
|
+
|
|
1461
|
+
except Exception:
|
|
1462
|
+
return None
|
|
1463
|
+
|
|
1464
|
+
def get_capabilities(self) -> ServiceCapabilities:
|
|
1465
|
+
"""Get service capabilities."""
|
|
1466
|
+
from uuid import uuid4
|
|
1467
|
+
|
|
1468
|
+
from ciris_engine.schemas.services.core import ServiceCapabilities
|
|
1469
|
+
from ciris_engine.schemas.services.metadata import ServiceMetadata
|
|
1470
|
+
|
|
1471
|
+
return ServiceCapabilities(
|
|
1472
|
+
service_name="AuthenticationService",
|
|
1473
|
+
actions=[
|
|
1474
|
+
"authenticate",
|
|
1475
|
+
"create_token",
|
|
1476
|
+
"verify_token",
|
|
1477
|
+
"create_wa",
|
|
1478
|
+
"revoke_wa",
|
|
1479
|
+
"update_wa",
|
|
1480
|
+
"list_was",
|
|
1481
|
+
"get_wa",
|
|
1482
|
+
"rotate_keys",
|
|
1483
|
+
"bootstrap_if_needed",
|
|
1484
|
+
"create_channel_token",
|
|
1485
|
+
"verify_token_sync",
|
|
1486
|
+
"update_last_login",
|
|
1487
|
+
],
|
|
1488
|
+
version="1.0.0",
|
|
1489
|
+
dependencies=["TimeService"],
|
|
1490
|
+
metadata=ServiceMetadata(
|
|
1491
|
+
category="infrastructure",
|
|
1492
|
+
critical=True,
|
|
1493
|
+
description="Infrastructure service for WA authentication and identity management",
|
|
1494
|
+
),
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
def get_status(self) -> ServiceStatus:
|
|
1498
|
+
"""Get current service status."""
|
|
1499
|
+
from ciris_engine.schemas.services.core import ServiceStatus
|
|
1500
|
+
|
|
1501
|
+
# Count certificates by type
|
|
1502
|
+
try:
|
|
1503
|
+
counts = authentication_store.get_certificate_counts(self.db_path)
|
|
1504
|
+
cert_count = counts.get("active", 0)
|
|
1505
|
+
revoked_count = counts.get("revoked", 0)
|
|
1506
|
+
# Extract by_role dict with type assertion
|
|
1507
|
+
role_counts_raw: int | Dict[str, int] = counts.get("by_role", {})
|
|
1508
|
+
role_counts: Dict[str, int] = (
|
|
1509
|
+
role_counts_raw
|
|
1510
|
+
if isinstance(role_counts_raw, dict)
|
|
1511
|
+
else {"OBSERVER": 0, "USER": 0, "ADMIN": 0, "AUTHORITY": 0, "ROOT": 0}
|
|
1512
|
+
)
|
|
1513
|
+
except Exception as e:
|
|
1514
|
+
logger.warning(
|
|
1515
|
+
f"Authentication service health check failed: {type(e).__name__}: {str(e)} - Unable to access auth database"
|
|
1516
|
+
)
|
|
1517
|
+
cert_count = 0
|
|
1518
|
+
role_counts = {"OBSERVER": 0, "USER": 0, "ADMIN": 0, "AUTHORITY": 0, "ROOT": 0}
|
|
1519
|
+
revoked_count = 0
|
|
1520
|
+
|
|
1521
|
+
current_time = self._time_service.now() if self._time_service else datetime.now(timezone.utc)
|
|
1522
|
+
uptime_seconds = 0.0
|
|
1523
|
+
if self._start_time:
|
|
1524
|
+
uptime_seconds = (current_time - self._start_time).total_seconds()
|
|
1525
|
+
|
|
1526
|
+
# Calculate token cache stats
|
|
1527
|
+
auth_context_cached = len(self._token_cache)
|
|
1528
|
+
channel_tokens_cached = len(self._channel_token_cache)
|
|
1529
|
+
|
|
1530
|
+
# Build custom metrics
|
|
1531
|
+
custom_metrics = {
|
|
1532
|
+
"active_certificates": float(cert_count),
|
|
1533
|
+
"revoked_certificates": float(revoked_count),
|
|
1534
|
+
"observer_certificates": float(role_counts.get("OBSERVER", 0)),
|
|
1535
|
+
"user_certificates": float(role_counts.get("USER", 0)),
|
|
1536
|
+
"admin_certificates": float(role_counts.get("ADMIN", 0)),
|
|
1537
|
+
"authority_certificates": float(role_counts.get("AUTHORITY", 0)),
|
|
1538
|
+
"root_certificates": float(role_counts.get("ROOT", 0)),
|
|
1539
|
+
"auth_contexts_cached": float(auth_context_cached),
|
|
1540
|
+
"channel_tokens_cached": float(channel_tokens_cached),
|
|
1541
|
+
"total_tokens_cached": float(auth_context_cached + channel_tokens_cached),
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
return ServiceStatus(
|
|
1545
|
+
service_name="AuthenticationService",
|
|
1546
|
+
service_type="infrastructure_service",
|
|
1547
|
+
is_healthy=True, # Simple health check
|
|
1548
|
+
uptime_seconds=uptime_seconds,
|
|
1549
|
+
last_error=None,
|
|
1550
|
+
metrics={
|
|
1551
|
+
"certificate_count": float(cert_count),
|
|
1552
|
+
"cached_tokens": float(len(self._channel_token_cache)),
|
|
1553
|
+
"active_sessions": 0.0,
|
|
1554
|
+
},
|
|
1555
|
+
custom_metrics=custom_metrics,
|
|
1556
|
+
last_health_check=current_time,
|
|
1557
|
+
)
|
|
1558
|
+
|
|
1559
|
+
async def start(self) -> None:
|
|
1560
|
+
"""Start the service."""
|
|
1561
|
+
await super().start()
|
|
1562
|
+
self._started = True
|
|
1563
|
+
self._start_time = self._time_service.now() if self._time_service else datetime.now(timezone.utc)
|
|
1564
|
+
logger.info("AuthenticationService started")
|
|
1565
|
+
|
|
1566
|
+
async def stop(self) -> None:
|
|
1567
|
+
"""Stop the service."""
|
|
1568
|
+
await super().stop()
|
|
1569
|
+
self._started = False
|
|
1570
|
+
# Clear caches
|
|
1571
|
+
self._token_cache.clear()
|
|
1572
|
+
self._channel_token_cache.clear()
|
|
1573
|
+
logger.info("AuthenticationService stopped")
|
|
1574
|
+
|
|
1575
|
+
def _collect_custom_metrics(self) -> Dict[str, float]:
|
|
1576
|
+
"""Collect authentication-specific metrics."""
|
|
1577
|
+
metrics = super()._collect_custom_metrics()
|
|
1578
|
+
|
|
1579
|
+
# Calculate auth success rate
|
|
1580
|
+
auth_rate = 0.0
|
|
1581
|
+
if self._auth_attempts > 0:
|
|
1582
|
+
auth_rate = self._auth_successes / self._auth_attempts
|
|
1583
|
+
|
|
1584
|
+
metrics.update(
|
|
1585
|
+
{
|
|
1586
|
+
"auth_attempts": float(self._auth_attempts),
|
|
1587
|
+
"auth_successes": float(self._auth_successes),
|
|
1588
|
+
"auth_failures": float(self._auth_failures),
|
|
1589
|
+
"auth_success_rate": auth_rate,
|
|
1590
|
+
"token_validations": float(self._token_validations),
|
|
1591
|
+
"permission_checks": float(self._permission_checks),
|
|
1592
|
+
"role_assignments": float(self._role_assignments),
|
|
1593
|
+
"active_sessions": float(self._session_count),
|
|
1594
|
+
"expired_sessions": float(self._expired_sessions),
|
|
1595
|
+
"active_tokens": float(self._active_tokens),
|
|
1596
|
+
}
|
|
1597
|
+
)
|
|
1598
|
+
|
|
1599
|
+
return metrics
|
|
1600
|
+
|
|
1601
|
+
async def get_metrics(self) -> Dict[str, float]:
|
|
1602
|
+
"""Get all authentication service metrics including base, custom, and v1.4.3 specific.
|
|
1603
|
+
|
|
1604
|
+
Returns:
|
|
1605
|
+
Dict with all metrics including base, custom, and v1.4.3 metrics
|
|
1606
|
+
"""
|
|
1607
|
+
# Get all base + custom metrics
|
|
1608
|
+
metrics = self._collect_metrics()
|
|
1609
|
+
|
|
1610
|
+
current_time = self._time_service.now() if self._time_service else datetime.now(timezone.utc)
|
|
1611
|
+
uptime_seconds = 0.0
|
|
1612
|
+
if self._start_time:
|
|
1613
|
+
uptime_seconds = (current_time - self._start_time).total_seconds()
|
|
1614
|
+
|
|
1615
|
+
# Add v1.4.3 specific metrics
|
|
1616
|
+
metrics.update(
|
|
1617
|
+
{
|
|
1618
|
+
"auth_attempts_total": float(self._auth_attempts),
|
|
1619
|
+
"auth_successes_total": float(self._auth_successes),
|
|
1620
|
+
"auth_failures_total": float(self._auth_failures),
|
|
1621
|
+
"auth_active_sessions": float(self._session_count),
|
|
1622
|
+
"auth_uptime_seconds": uptime_seconds,
|
|
1623
|
+
}
|
|
1624
|
+
)
|
|
1625
|
+
|
|
1626
|
+
return metrics
|
|
1627
|
+
|
|
1628
|
+
async def is_healthy(self) -> bool:
|
|
1629
|
+
"""Check if service is healthy."""
|
|
1630
|
+
if not self._started:
|
|
1631
|
+
return False
|
|
1632
|
+
|
|
1633
|
+
# Check database connection via store
|
|
1634
|
+
return authentication_store.check_database_health(self.db_path)
|