agentengine-sdk-python 0.8.2__py3-none-any.whl → 0.8.4__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.
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.4.dist-info}/METADATA +22 -28
- agentengine_sdk_python-0.8.4.dist-info/RECORD +1070 -0
- ksadk/_build_provenance.json +1 -0
- ksadk/agui/_agent_helpers.py +4 -0
- ksadk/api/client.py +108 -91
- ksadk/builders/code_builder.py +36 -9
- ksadk/builders/framework_requirements.py +2 -1
- ksadk/builders/ks3_uploader.py +36 -7
- ksadk/builders/managed_runtime_builder.py +4 -0
- ksadk/cli/__init__.py +38 -3
- ksadk/cli/cmd_managed_runtime.py +13 -1
- ksadk/cli/cmd_plugin.py +662 -0
- ksadk/cli/cmd_web.py +2 -5
- ksadk/cli/runtime_bootstrap.py +23 -1
- ksadk/codex/client.py +424 -96
- ksadk/codex/projection.py +24 -0
- ksadk/codex/runtime.py +83 -283
- ksadk/common/auth.py +16 -10
- ksadk/configs/env_registry.py +59 -3
- ksadk/configs/settings.py +9 -0
- ksadk/context_engine/contributors.py +73 -8
- ksadk/conversations/__init__.py +19 -0
- ksadk/conversations/context.py +104 -24
- ksadk/conversations/contracts.py +266 -0
- ksadk/conversations/message_projection.py +154 -15
- ksadk/conversations/projector.py +614 -0
- ksadk/conversations/reducer.py +98 -0
- ksadk/conversations/runtime_invocation.py +5 -0
- ksadk/conversations/runtime_payloads.py +1 -0
- ksadk/conversations/runtime_preparation.py +30 -27
- ksadk/conversations/runtime_resume.py +50 -2
- ksadk/conversations/runtime_stream_events.py +5 -0
- ksadk/conversations/runtime_streaming.py +25 -7
- ksadk/deployment/env_forward.py +2 -2
- ksadk/deployment/providers/serverless.py +1 -1
- ksadk/events/_v1_compat/projection.py +2 -0
- ksadk/events/adapters/_codex_validators.py +18 -7
- ksadk/events/adapters/codex.py +190 -0
- ksadk/events/adapters/langgraph.py +15 -1
- ksadk/events/canonical_store.py +3 -1
- ksadk/events/projections.py +1 -1
- ksadk/harness/__init__.py +45 -35
- ksadk/harness/config.py +2 -2
- ksadk/harness/runtime.py +75 -16
- ksadk/harness/tools.py +38 -13
- ksadk/interaction/provider.py +1 -0
- ksadk/interaction/providers/langgraph.py +1 -0
- ksadk/kernel/bootstrap.py +28 -4
- ksadk/kernel/contract_fingerprints.py +5 -4
- ksadk/kernel/contracts.py +8 -0
- ksadk/kernel/control.py +1 -0
- ksadk/kernel/ingress.py +36 -2
- ksadk/kernel/memory_store.py +1 -0
- ksadk/kernel/postgres_store.py +1 -0
- ksadk/kernel/sqlite_store.py +28 -0
- ksadk/kernel/store.py +3 -0
- ksadk/kernel/worker.py +76 -10
- ksadk/knowledge_base/client.py +57 -27
- ksadk/mcp_runtime/__init__.py +12 -4
- ksadk/memory/adk/backends/sdk_ltm_backend.py +161 -41
- ksadk/memory/adk/resilient_session_service.py +25 -17
- ksadk/memory/adk/short_term_memory.py +26 -9
- ksadk/memory/models.py +7 -4
- ksadk/memory/provider_resolver.py +3 -0
- ksadk/memory/providers/local_sqlite.py +7 -0
- ksadk/model_proxy/server.py +10 -5
- ksadk/plugins/__init__.py +171 -0
- ksadk/plugins/artifacts.py +275 -0
- ksadk/plugins/bridges/__init__.py +47 -0
- ksadk/plugins/bridges/codex.py +659 -0
- ksadk/plugins/bridges/dsh.py +1300 -0
- ksadk/plugins/builtins.py +746 -0
- ksadk/plugins/bundle.py +246 -0
- ksadk/plugins/bundle_security.py +191 -0
- ksadk/plugins/codex_manifest.py +983 -0
- ksadk/plugins/composition.py +537 -0
- ksadk/plugins/context_contributor.py +437 -0
- ksadk/plugins/contracts.py +569 -0
- ksadk/plugins/delivery.py +153 -0
- ksadk/plugins/dsh_installation_digest.py +71 -0
- ksadk/plugins/dsh_toolchain.py +1024 -0
- ksadk/plugins/ecosystem_bridge.py +1105 -0
- ksadk/plugins/ecosystem_probe.py +191 -0
- ksadk/plugins/host.py +787 -0
- ksadk/plugins/kernel_adapter.py +205 -0
- ksadk/plugins/providers/__init__.py +108 -0
- ksadk/plugins/providers/bundles/dsh-knowledge/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/dsh-knowledge/index.mjs +53 -0
- ksadk/plugins/providers/bundles/dsh-knowledge/package.json +16 -0
- ksadk/plugins/providers/bundles/dsh-memory/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/dsh-memory/index.mjs +175 -0
- ksadk/plugins/providers/bundles/dsh-memory/package.json +16 -0
- ksadk/plugins/providers/bundles/dsh-platform-resources/client.mjs +166 -0
- ksadk/plugins/providers/bundles/dsh-platform-resources/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/dsh-platform-resources/index.mjs +9 -0
- ksadk/plugins/providers/bundles/dsh-platform-resources/package.json +12 -0
- ksadk/plugins/providers/bundles/dsh-skill-center/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/dsh-skill-center/index.mjs +125 -0
- ksadk/plugins/providers/bundles/dsh-skill-center/package.json +16 -0
- ksadk/plugins/providers/bundles/ksadk-codex/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/ksadk-codex/index.mjs +18 -0
- ksadk/plugins/providers/bundles/ksadk-codex/package.json +19 -0
- ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/index.mjs +870 -0
- ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/package.json +27 -0
- ksadk/plugins/providers/bundles/ksadk-dsh-studio/client.js +92 -0
- ksadk/plugins/providers/bundles/ksadk-dsh-studio/index.mjs +15 -0
- ksadk/plugins/providers/bundles/ksadk-dsh-studio/package.json +7 -0
- ksadk/plugins/providers/bundles/ksadk-harness/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/ksadk-harness/index.mjs +19 -0
- ksadk/plugins/providers/bundles/ksadk-harness/package.json +19 -0
- ksadk/plugins/providers/codex.py +988 -0
- ksadk/plugins/providers/codex_dsh.py +83 -0
- ksadk/plugins/providers/codex_native.py +368 -0
- ksadk/plugins/providers/codex_turn.py +301 -0
- ksadk/plugins/providers/dsh.py +951 -0
- ksadk/plugins/providers/dsh_capabilities.py +1389 -0
- ksadk/plugins/providers/dsh_descriptor_host.py +226 -0
- ksadk/plugins/providers/dsh_mcp.py +467 -0
- ksadk/plugins/providers/harness.py +632 -0
- ksadk/plugins/providers/harness_dsh.py +91 -0
- ksadk/plugins/providers/legacy.py +215 -0
- ksadk/plugins/providers/legacy_catalog.py +100 -0
- ksadk/plugins/providers/mcp_projection.py +150 -0
- ksadk/plugins/providers/platform_resources.py +657 -0
- ksadk/plugins/providers/shipped_dsh.py +370 -0
- ksadk/plugins/resolver.py +338 -0
- ksadk/plugins/subagent_providers/__init__.py +12 -0
- ksadk/plugins/subagent_providers/codex.py +443 -0
- ksadk/plugins/subagents.py +333 -0
- ksadk/resource_runtime/__init__.py +4 -0
- ksadk/resource_runtime/broker.py +616 -0
- ksadk/resource_runtime/build_artifacts.py +165 -0
- ksadk/resource_runtime/contracts.py +138 -0
- ksadk/resource_runtime/discovery_receipts.py +100 -0
- ksadk/resource_runtime/discovery_skills.py +172 -0
- ksadk/resource_runtime/errors.py +6 -0
- ksadk/resource_runtime/ipc.py +142 -0
- ksadk/resource_runtime/kernel_approval.py +165 -0
- ksadk/resource_runtime/knowledge.py +114 -0
- ksadk/resource_runtime/langgraph.py +94 -0
- ksadk/resource_runtime/leases.py +191 -0
- ksadk/resource_runtime/managed_projection.py +186 -0
- ksadk/resource_runtime/mcp_server.py +138 -0
- ksadk/resource_runtime/memory_provider.py +318 -0
- ksadk/resource_runtime/memory_receipts.py +34 -0
- ksadk/resource_runtime/operation_ledger.py +211 -0
- ksadk/resource_runtime/plugin_config.py +48 -0
- ksadk/resource_runtime/policy_authorization.py +54 -0
- ksadk/resource_runtime/process.py +110 -0
- ksadk/resource_runtime/skill_artifacts.py +153 -0
- ksadk/resource_runtime/skill_runtime.py +42 -0
- ksadk/resource_runtime/skill_workspace.py +118 -0
- ksadk/resource_runtime/skills.py +215 -0
- ksadk/resource_runtime/snapshots.py +180 -0
- ksadk/resource_runtime/socket_server.py +103 -0
- ksadk/resource_runtime/supervisor.py +380 -0
- ksadk/resource_runtime/worker.py +504 -0
- ksadk/runners/_langgraph_runner_streams.py +74 -6
- ksadk/runners/adk_runner.py +149 -11
- ksadk/runners/base_runner.py +29 -1
- ksadk/runners/langgraph_runner.py +475 -145
- ksadk/runtime/_runner_adapter/stream_mapping.py +193 -60
- ksadk/runtime/adapter.py +3 -0
- ksadk/runtime/conversation_execution.py +92 -16
- ksadk/runtime/executor.py +78 -2
- ksadk/runtime/factory.py +30 -200
- ksadk/runtime/framework_adapters.py +41 -0
- ksadk/runtime/preprocessing.py +10 -2
- ksadk/runtime/runner_adapter.py +78 -14
- ksadk/runtime_context.py +40 -2
- ksadk/sandbox/__init__.py +2 -1
- ksadk/sandbox/backends/e2b.py +52 -11
- ksadk/sandbox/backends/local_process.py +9 -0
- ksadk/sandbox/base.py +2 -0
- ksadk/sandbox/e2b_connection.py +66 -0
- ksadk/sandbox/factory.py +72 -0
- ksadk/scheduler/__init__.py +18 -0
- ksadk/scheduler/calendar.py +102 -0
- ksadk/scheduler/contracts.py +254 -0
- ksadk/scheduler/dispatcher.py +129 -0
- ksadk/scheduler/engine.py +416 -0
- ksadk/scheduler/sqlite_store.py +581 -0
- ksadk/server/composition.py +2 -0
- ksadk/server/factory.py +5 -0
- ksadk/server/persistence_capability.py +516 -0
- ksadk/server/routes/control.py +210 -46
- ksadk/server/routes/dependencies.py +15 -0
- ksadk/server/routes/models.py +47 -4
- ksadk/server/routes/projection.py +252 -12
- ksadk/server/routes/sessions.py +284 -107
- ksadk/server/routes/streaming.py +43 -9
- ksadk/server/static/assets/{ArtifactsPanel-DSU1sWD_.js → ArtifactsPanel-CGbWlMNh.js} +1 -1
- ksadk/server/static/assets/{CodeBlock-DOH6MVcn.js → CodeBlock-b4C7upmO.js} +3 -3
- ksadk/server/static/assets/{MathMessageMarkdown-BL8my1R2.js → MathMessageMarkdown-DbdW5A4_.js} +4 -4
- ksadk/server/static/assets/{MermaidBlock-Dz4IP-Tx.js → MermaidBlock-BEBN4Ksz.js} +6 -6
- ksadk/server/static/assets/{NativeTerminalPanel-DUrK0JpZ.js → NativeTerminalPanel-zvL-WpAn.js} +1 -1
- ksadk/server/static/assets/{abnfDiagram-VCTEODGH-g20pFzNV.js → abnfDiagram-VCTEODGH-BYafW7Za.js} +1 -1
- ksadk/server/static/assets/{arc-C4FzinUA.js → arc-bTohInsS.js} +1 -1
- ksadk/server/static/assets/architecture-7GRP2DOG-BW5UtSnx.js +1 -0
- ksadk/server/static/assets/{architectureDiagram-5GKGNRK7-DpIqL5h4.js → architectureDiagram-5GKGNRK7-Ch5Lbzsi.js} +1 -1
- ksadk/server/static/assets/{blockDiagram-NRAW4CY4-BjkljTNz.js → blockDiagram-NRAW4CY4-kqF4MDOG.js} +1 -1
- ksadk/server/static/assets/{c4Diagram-UCG6FXSJ-Dk_ieq2X.js → c4Diagram-UCG6FXSJ-8uSRIODv.js} +1 -1
- ksadk/server/static/assets/channel-CF5Up3hQ.js +1 -0
- ksadk/server/static/assets/chunk-2Q5K7J3B-KkXX47WI.js +1 -0
- ksadk/server/static/assets/{chunk-5VM5RSS4-BVHAchFb.js → chunk-5VM5RSS4-DkhehZDr.js} +1 -1
- ksadk/server/static/assets/{chunk-F27PBJKO-C-ipQzuS.js → chunk-F27PBJKO-BdXhtPe9.js} +1 -1
- ksadk/server/static/assets/{chunk-G27WJ6UU-C2EyJbIK.js → chunk-G27WJ6UU-DPTpvpwi.js} +1 -1
- ksadk/server/static/assets/{chunk-JWPE2WC7-vYvVJb_M.js → chunk-JWPE2WC7-Pqoh38el.js} +1 -1
- ksadk/server/static/assets/{chunk-LCL6LL3I-BcI6ysPT.js → chunk-LCL6LL3I-DeU00qC-.js} +1 -1
- ksadk/server/static/assets/{chunk-POPQ4Y6H-C030x_Z1.js → chunk-POPQ4Y6H-CzxScFkr.js} +1 -1
- ksadk/server/static/assets/{chunk-RHFEMEQ7-yyGpsz4n.js → chunk-RHFEMEQ7-DpqMx-ux.js} +1 -1
- ksadk/server/static/assets/{chunk-SVP7TREG-BLTlmMU7.js → chunk-SVP7TREG-C6t0SypQ.js} +1 -1
- ksadk/server/static/assets/{chunk-XXDRQBXY-C_32ArgP.js → chunk-XXDRQBXY-DVaeafZk.js} +1 -1
- ksadk/server/static/assets/classDiagram-DTDB5LWJ-Cz81acRQ.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-Cz81acRQ.js +1 -0
- ksadk/server/static/assets/{cose-bilkent-JH36ORCC-9YQYwdl0.js → cose-bilkent-JH36ORCC-Bf9D3tzJ.js} +1 -1
- ksadk/server/static/assets/cynefin-OW5HDTMX-DVWs8a2t.js +1 -0
- ksadk/server/static/assets/{cynefinDiagram-5FMLGOSQ-ErLF5N13.js → cynefinDiagram-5FMLGOSQ-B9cY3iTz.js} +1 -1
- ksadk/server/static/assets/{dagre-3AP2YEHR-DFiQF-6f.js → dagre-3AP2YEHR-CSwlx3CB.js} +1 -1
- ksadk/server/static/assets/{dagre-BuhGnRI1.js → dagre-UBpgon5q.js} +1 -1
- ksadk/server/static/assets/{diagram-S7CK7UJ4-Bt1v8-GC.js → diagram-S7CK7UJ4-C9akhN9a.js} +1 -1
- ksadk/server/static/assets/{diagram-UQ7AKVKN-DSCxJdBK.js → diagram-UQ7AKVKN-CgedhTPI.js} +1 -1
- ksadk/server/static/assets/{diagram-VSXAHHWV-DHfYwp_9.js → diagram-VSXAHHWV-Cjucz5BY.js} +1 -1
- ksadk/server/static/assets/{diagram-VX7I27RA-DdzD-4Le.js → diagram-VX7I27RA-Dd4Vleyn.js} +1 -1
- ksadk/server/static/assets/{diagram-Z3DM3KII-Du-nAJ9F.js → diagram-Z3DM3KII-CZ2jGtYW.js} +1 -1
- ksadk/server/static/assets/{dist-B7seoj3d.js → dist-B2_CVgKv.js} +1 -1
- ksadk/server/static/assets/{dist-CSh_DE14.js → dist-B3FR1SKK.js} +1 -1
- ksadk/server/static/assets/{dist-DUX-id_F.js → dist-BIr4DSRq.js} +1 -1
- ksadk/server/static/assets/{dist-C_wsv-Qd.js → dist-BgV_owcB.js} +1 -1
- ksadk/server/static/assets/{dist-W-TIVntx.js → dist-Bj730Ib4.js} +1 -1
- ksadk/server/static/assets/{dist-BytlxIDT.js → dist-BnaTiyzN.js} +1 -1
- ksadk/server/static/assets/{dist-CXYYBw3_.js → dist-C1NMAY5a.js} +1 -1
- ksadk/server/static/assets/{dist-dDdADKFA.js → dist-C5_ZAZsG.js} +1 -1
- ksadk/server/static/assets/{dist-BVDffZ0U.js → dist-CB9iJrvg.js} +1 -1
- ksadk/server/static/assets/{dist-Bf2M8m3N.js → dist-CFzsn6BG.js} +1 -1
- ksadk/server/static/assets/{dist-ngtN0DJB.js → dist-Cf125P_m.js} +1 -1
- ksadk/server/static/assets/{dist-BE7bu-DL.js → dist-ClYUxfeK.js} +1 -1
- ksadk/server/static/assets/{dist-DNp6rLRA.js → dist-DFD6Qa6T.js} +1 -1
- ksadk/server/static/assets/{dist-BX8z72IC.js → dist-DVCOWtha.js} +1 -1
- ksadk/server/static/assets/{dist-dydu68Fl.js → dist-D_ctSS2i.js} +1 -1
- ksadk/server/static/assets/{dist-ClxGviKw.js → dist-DiixqL0J.js} +1 -1
- ksadk/server/static/assets/{dist-BjU6y-A2.js → dist-DoVSwoDb.js} +1 -1
- ksadk/server/static/assets/{dist-Dq9gLD7L.js → dist-DvFK7p5S.js} +1 -1
- ksadk/server/static/assets/{dist-8hqcx0sU.js → dist-QfkEHOlb.js} +1 -1
- ksadk/server/static/assets/{dist-CttYUqzS.js → dist-btbmmX5O.js} +1 -1
- ksadk/server/static/assets/{dist-DmldrHd_.js → dist-fBO2v5Kt.js} +1 -1
- ksadk/server/static/assets/{dist-DnfXs8Vn.js → dist-ufY3F5p6.js} +1 -1
- ksadk/server/static/assets/{ebnfDiagram-PWID7BFC-Da9C9NO1.js → ebnfDiagram-PWID7BFC-Ci8NqqZu.js} +1 -1
- ksadk/server/static/assets/{erDiagram-SSCWMZ5O-EAGqqR79.js → erDiagram-SSCWMZ5O-CCIOhLZG.js} +1 -1
- ksadk/server/static/assets/eventmodeling-NTZA5JFV-CQqR2hg3.js +1 -0
- ksadk/server/static/assets/flowDiagram-A5DVABFB-DDRRbGPm.js +1 -0
- ksadk/server/static/assets/{ganttDiagram-EL5Y4UJY-DqJsKb59.js → ganttDiagram-EL5Y4UJY-Cp_r9Hb6.js} +1 -1
- ksadk/server/static/assets/gitGraph-4MIJSDKK-fgWEf7UD.js +1 -0
- ksadk/server/static/assets/{gitGraphDiagram-WWUBYQGX-B6g4dtDi.js → gitGraphDiagram-WWUBYQGX-Dlhc8Wr8.js} +1 -1
- ksadk/server/static/assets/index-DcTML1Up.css +2 -0
- ksadk/server/static/assets/index-Dh2XHqPK.js +244 -0
- ksadk/server/static/assets/info-A6RAGUB7-BUecLTdt.js +1 -0
- ksadk/server/static/assets/{infoDiagram-RXCK75RN-Djm_nbd5.js → infoDiagram-RXCK75RN-CO0rRTRA.js} +1 -1
- ksadk/server/static/assets/{ishikawaDiagram-5VMMS53U-DWya9SG2.js → ishikawaDiagram-5VMMS53U-CHiaedSS.js} +1 -1
- ksadk/server/static/assets/{journeyDiagram-EYS64GPL-DutxhSNI.js → journeyDiagram-EYS64GPL-CHcCIB09.js} +1 -1
- ksadk/server/static/assets/{kanban-definition-3QL26DDD-DJdOF8Fm.js → kanban-definition-3QL26DDD-CJ_ebBZf.js} +1 -1
- ksadk/server/static/assets/{katex-vFWytM5c.js → katex-B4weu383.js} +1 -1
- ksadk/server/static/assets/{linear-BHjG8b-J.js → linear-V-MgYHW1.js} +1 -1
- ksadk/server/static/assets/{mermaid-parser.core-KGSy4jWT.js → mermaid-parser.core-CBijEX_c.js} +2 -2
- ksadk/server/static/assets/{mindmap-definition-FBJOCRG2-9-HC88D4.js → mindmap-definition-FBJOCRG2-Bc-pQvpp.js} +1 -1
- ksadk/server/static/assets/packet-AYTQ26CC-BeW-btFv.js +1 -0
- ksadk/server/static/assets/{pegDiagram-XKGWAZYB-DrEZd4fD.js → pegDiagram-XKGWAZYB-D7genGLl.js} +1 -1
- ksadk/server/static/assets/pie-WAS4IAKB-X0xh4SaT.js +1 -0
- ksadk/server/static/assets/{pieDiagram-E7YTZNPT-DpjqOFFp.js → pieDiagram-E7YTZNPT-Bxj32zwD.js} +1 -1
- ksadk/server/static/assets/{quadrantDiagram-AXDQQJYC-BiNhgOXX.js → quadrantDiagram-AXDQQJYC-BURv2mGh.js} +1 -1
- ksadk/server/static/assets/radar-RG4KPBEZ-DzRMSqjd.js +1 -0
- ksadk/server/static/assets/railroad-74A4TZTK-Cc44icX-.js +1 -0
- ksadk/server/static/assets/railroad-abnf-HS5TGJTU-D6S4BdV-.js +1 -0
- ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-vD3aVE2o.js +1 -0
- ksadk/server/static/assets/railroad-peg-WCYAUIDC-CYbW_b4T.js +1 -0
- ksadk/server/static/assets/{railroadDiagram-O6MQD6OU-_Y3Ojg6G.js → railroadDiagram-O6MQD6OU-CCsh3avW.js} +1 -1
- ksadk/server/static/assets/{requirementDiagram-EFPCY7ZU-DeCFdg9K.js → requirementDiagram-EFPCY7ZU-_FRRBKEd.js} +1 -1
- ksadk/server/static/assets/{sankeyDiagram-P5KCCOFB-CPH75rhw.js → sankeyDiagram-P5KCCOFB-C-gr2o9M.js} +1 -1
- ksadk/server/static/assets/{sequenceDiagram-WJ2MYXX4-OEpQQdr4.js → sequenceDiagram-WJ2MYXX4-D97NmfKg.js} +1 -1
- ksadk/server/static/assets/{sizeCapture-X5ZJPWSS-heUErzhI.js → sizeCapture-X5ZJPWSS-DjrPC5FX.js} +1 -1
- ksadk/server/static/assets/{stateDiagram-HBIQ2CUA-DvA3jSMB.js → stateDiagram-HBIQ2CUA-DzMNg1hh.js} +1 -1
- ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BIcjKFhF.js +1 -0
- ksadk/server/static/assets/{swimlanes-XN3QIQJK-CR-dfN_t.js → swimlanes-XN3QIQJK-Cssf_CsU.js} +1 -1
- ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-tq3K3Ybl.js +8 -0
- ksadk/server/static/assets/{timeline-definition-24CTP7MA-DAVJf1TX.js → timeline-definition-24CTP7MA-BWTHzyS1.js} +1 -1
- ksadk/server/static/assets/treeView-Q6P3EWNA-DBXt3GdU.js +1 -0
- ksadk/server/static/assets/treemap-WGGIJYW6-DrCy4E_l.js +1 -0
- ksadk/server/static/assets/{vennDiagram-4TSXK5OY-DwM8eDkh.js → vennDiagram-4TSXK5OY-BSKgJsmR.js} +1 -1
- ksadk/server/static/assets/wardley-WFR3VGLG-CefhOliM.js +1 -0
- ksadk/server/static/assets/{wardleyDiagram-VM6X3IG4-DZL-Zz2t.js → wardleyDiagram-VM6X3IG4-eflHNoaZ.js} +1 -1
- ksadk/server/static/assets/{xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js → xychartDiagram-S5SC5T6Z-eYqrQ-v4.js} +1 -1
- ksadk/server/static/index.html +2 -2
- ksadk/session_context.py +66 -0
- ksadk/sessions/__init__.py +11 -21
- ksadk/sessions/_local_service_sync.py +263 -1
- ksadk/sessions/base.py +176 -0
- ksadk/sessions/errors.py +4 -0
- ksadk/sessions/in_memory.py +309 -19
- ksadk/sessions/local_service.py +83 -1
- ksadk/sessions/persistence.py +275 -0
- ksadk/sessions/postgres_service.py +358 -0
- ksadk/sessions/resilience.py +8 -0
- ksadk/sessions/resilient.py +362 -4
- ksadk/sessions/topology.py +135 -0
- ksadk/skills/models.py +26 -4
- ksadk/skills/package_store.py +186 -38
- ksadk/skills/runtime/agent.py +31 -6
- ksadk/skills/runtime/artifact_delivery.py +139 -0
- ksadk/skills/runtime/backends/e2b.py +98 -6
- ksadk/skills/runtime/backends/local.py +73 -8
- ksadk/skills/runtime/base.py +3 -0
- ksadk/skills/runtime/executor.py +2 -0
- ksadk/skills/runtime/loader.py +10 -2
- ksadk/skills/runtime/pinned.py +114 -0
- ksadk/skills/runtime/request.py +31 -0
- ksadk/skills/service_client.py +211 -24
- ksadk/studio/agent_lifecycle.py +26 -20
- ksadk/studio/api.py +714 -90
- ksadk/studio/api_catalog_routes.py +23 -0
- ksadk/studio/api_contracts.py +34 -1
- ksadk/studio/api_plugin_routes.py +911 -0
- ksadk/studio/api_resource_connections.py +183 -0
- ksadk/studio/attachment_store.py +187 -0
- ksadk/studio/authoring.py +10 -4
- ksadk/studio/authoring_coordinator.py +31 -16
- ksadk/studio/builder.py +199 -43
- ksadk/studio/cloud.py +346 -104
- ksadk/studio/cloud_shared_web.py +565 -0
- ksadk/studio/codex_agent_service.py +140 -27
- ksadk/studio/codex_builder.py +294 -16
- ksadk/studio/codex_credentials.py +90 -0
- ksadk/studio/codex_manifest.py +51 -4
- ksadk/studio/codex_plugin_store.py +746 -0
- ksadk/studio/codex_provider_build.py +231 -0
- ksadk/studio/codex_run.py +200 -29
- ksadk/studio/compatibility_report.py +456 -0
- ksadk/studio/compiler.py +84 -15
- ksadk/studio/contracts.py +249 -4
- ksadk/studio/dsh_application.py +137 -0
- ksadk/studio/dsh_capability_service.py +1107 -0
- ksadk/studio/dsh_models.py +59 -0
- ksadk/studio/dsh_provider_registration.py +1075 -0
- ksadk/studio/event_store.py +54 -6
- ksadk/studio/framework_run.py +37 -4
- ksadk/studio/hosted_kernel.py +89 -14
- ksadk/studio/mcp_runtime.py +11 -3
- ksadk/studio/model_client.py +2 -2
- ksadk/studio/operations.py +12 -2
- ksadk/studio/plugin_composition.py +468 -0
- ksadk/studio/plugin_kernel_adapter.py +28 -0
- ksadk/studio/plugin_runtime.py +831 -0
- ksadk/studio/resource_authority.py +637 -0
- ksadk/studio/resource_binding_validation.py +77 -0
- ksadk/studio/resource_build_admission.py +196 -0
- ksadk/studio/resource_build_materializer.py +149 -0
- ksadk/studio/resource_catalog.py +149 -3
- ksadk/studio/resource_connections.py +220 -0
- ksadk/studio/run_service.py +1144 -96
- ksadk/studio/runtime_source.py +24 -8
- ksadk/studio/scheduler_assistant.py +317 -0
- ksadk/studio/scheduler_runtime.py +383 -0
- ksadk/studio/scheduler_service.py +214 -0
- ksadk/studio/service.py +1131 -95
- ksadk/studio/shared_web.py +610 -87
- ksadk/studio/soul.py +52 -0
- ksadk/studio/static/assets/CodeBlock-DCUUY0PL-BLjA7kbS.js +41 -0
- ksadk/studio/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- ksadk/studio/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- ksadk/studio/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- ksadk/studio/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- ksadk/studio/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- ksadk/studio/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- ksadk/studio/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- ksadk/studio/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- ksadk/studio/static/assets/MathMessageMarkdown-CBcDrAw--BEOyPC3w.css +1 -0
- ksadk/studio/static/assets/MathMessageMarkdown-CBcDrAw--CUYIXwgI.js +8 -0
- ksadk/studio/static/assets/MermaidBlock-DBFTztfQ-BTYKWEbD.js +44 -0
- ksadk/studio/static/assets/PrismRenderer-wiXRHURw.js +3 -0
- ksadk/studio/static/assets/abnfDiagram-VCTEODGH-CLETGjKq.js +1 -0
- ksadk/studio/static/assets/arc-Pry-7G1_.js +1 -0
- ksadk/studio/static/assets/architecture-7GRP2DOG-hXMiO_AU.js +1 -0
- ksadk/studio/static/assets/architectureDiagram-5GKGNRK7-B5EQysk6.js +36 -0
- ksadk/studio/static/assets/array-BifhSqXX.js +1 -0
- ksadk/studio/static/assets/blockDiagram-I7D4REHJ-CFmPl1de.js +129 -0
- ksadk/studio/static/assets/c4Diagram-7LVT6UL2-BRrGu8qt.js +38 -0
- ksadk/studio/static/assets/channel-BqO-gtEw.js +1 -0
- ksadk/studio/static/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- ksadk/studio/static/assets/chunk-4HAMMTFA-D220vUtq.js +62 -0
- ksadk/studio/static/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- ksadk/studio/static/assets/chunk-75Z2AOVW-C8lLv_qc.js +2 -0
- ksadk/studio/static/assets/chunk-DU6HZSFF-C4DtVpCu.js +128 -0
- ksadk/studio/static/assets/chunk-F27PBJKO-Cpw14SNG.js +1 -0
- ksadk/studio/static/assets/chunk-FOHPRMQF-DHwB1DNv.js +161 -0
- ksadk/studio/static/assets/chunk-GMAD6QVW-DiQA-u2N.js +72 -0
- ksadk/studio/static/assets/chunk-GVQU2GXP-DUdNBUCi.js +1 -0
- ksadk/studio/static/assets/chunk-IMKFNOWR-BSlbjT_b.js +231 -0
- ksadk/studio/static/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- ksadk/studio/static/assets/chunk-L3NEJ4N5-Db8Ab22W.js +1 -0
- ksadk/studio/static/assets/chunk-OSK3NFVY-M2m6G3j4.js +10 -0
- ksadk/studio/static/assets/chunk-P2QGCYS3-DezaQxiK.js +1 -0
- ksadk/studio/static/assets/chunk-POPQ4Y6H-DOpufJ3t.js +1 -0
- ksadk/studio/static/assets/chunk-PWAF6VOD-CfLb1gSc.js +1 -0
- ksadk/studio/static/assets/chunk-SHT3W25Y-DAyIVp5H.js +168 -0
- ksadk/studio/static/assets/chunk-SVP7TREG-DulomOrS.js +88 -0
- ksadk/studio/static/assets/chunk-TICWLB2K-bHJYoflj.js +206 -0
- ksadk/studio/static/assets/chunk-XXDRQBXY-7PcdR2B1.js +1 -0
- ksadk/studio/static/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- ksadk/studio/static/assets/classDiagram-ZZMXUADV-D_bW8JjA.js +1 -0
- ksadk/studio/static/assets/classDiagram-v2-VYDZK3BY-D_bW8JjA.js +1 -0
- ksadk/studio/static/assets/cose-bilkent-JH36ORCC-snL88wkX.js +1 -0
- ksadk/studio/static/assets/cynefin-OW5HDTMX-Bd1YhRo1.js +1 -0
- ksadk/studio/static/assets/cynefinDiagram-5FMLGOSQ-BBrIG_mI.js +62 -0
- ksadk/studio/static/assets/cytoscape.esm-CGd-uY3x.js +321 -0
- ksadk/studio/static/assets/dagre-GXQ25YYZ-DtF1nGdY.js +4 -0
- ksadk/studio/static/assets/dagre-PrKaheQc.js +1 -0
- ksadk/studio/static/assets/defaultLocale-BFoDCU3G.js +1 -0
- ksadk/studio/static/assets/diagram-S7CK7UJ4-0BZ98mya.js +30 -0
- ksadk/studio/static/assets/diagram-UQ7AKVKN-6mLKEo6L.js +41 -0
- ksadk/studio/static/assets/diagram-VSXAHHWV-CJBIaiZ2.js +3 -0
- ksadk/studio/static/assets/diagram-VX7I27RA-BohLEE1C.js +24 -0
- ksadk/studio/static/assets/diagram-Z3DM3KII-D8KIGonG.js +24 -0
- ksadk/studio/static/assets/dist-v5Q1xZ2K.js +1 -0
- ksadk/studio/static/assets/ebnfDiagram-PWID7BFC-C99HU61t.js +1 -0
- ksadk/studio/static/assets/erDiagram-RLTQ6QDP-DLGry2Ap.js +99 -0
- ksadk/studio/static/assets/eventmodeling-NTZA5JFV-B1_5Ac8j.js +1 -0
- ksadk/studio/static/assets/flowDiagram-HODETNUW-DD5hmicn.js +1 -0
- ksadk/studio/static/assets/ganttDiagram-EL5Y4UJY-BPJrVLSG.js +292 -0
- ksadk/studio/static/assets/gitGraph-4MIJSDKK-Cy_ZQBGi.js +1 -0
- ksadk/studio/static/assets/gitGraphDiagram-WWUBYQGX-CZCUz4MT.js +106 -0
- ksadk/studio/static/assets/graphlib-DS17s2tU.js +1 -0
- ksadk/studio/static/assets/index-CeHL9ARV.css +2 -0
- ksadk/studio/static/assets/index-D9QEcVpL.js +320 -0
- ksadk/studio/static/assets/info-A6RAGUB7-DBzDQB_I.js +1 -0
- ksadk/studio/static/assets/infoDiagram-27XIBGKW-B-sq07Hi.js +2 -0
- ksadk/studio/static/assets/init-C-OQMol4.js +1 -0
- ksadk/studio/static/assets/ishikawaDiagram-5VMMS53U-CljI2wyf.js +70 -0
- ksadk/studio/static/assets/journeyDiagram-3NMN7TZE-D50Fo2sL.js +139 -0
- ksadk/studio/static/assets/kanban-definition-UXKFOSKX-DimfOTam.js +89 -0
- ksadk/studio/static/assets/katex-DhON-x74.js +257 -0
- ksadk/studio/static/assets/line-CX5BOOUP.js +1 -0
- ksadk/studio/static/assets/linear-BCxN9JLv.js +1 -0
- ksadk/studio/static/assets/mermaid-parser.core-BdCz8fkS.js +7 -0
- ksadk/studio/static/assets/mindmap-definition-YA3MSWOX-Bizzqy5R.js +96 -0
- ksadk/studio/static/assets/ordinal-BDEzSJ7C.js +1 -0
- ksadk/studio/static/assets/packet-AYTQ26CC-gPln-_kG.js +1 -0
- ksadk/studio/static/assets/path-fybaL0A-.js +1 -0
- ksadk/studio/static/assets/pegDiagram-XKGWAZYB-Dy3fB-gW.js +1 -0
- ksadk/studio/static/assets/pie-WAS4IAKB-qZdHMr7B.js +1 -0
- ksadk/studio/static/assets/pieDiagram-E7YTZNPT-B9mrFSDF.js +39 -0
- ksadk/studio/static/assets/quadrantDiagram-AXDQQJYC-NzUfCbMR.js +7 -0
- ksadk/studio/static/assets/radar-RG4KPBEZ-BZlnaea9.js +1 -0
- ksadk/studio/static/assets/railroad-74A4TZTK-Ce3__H9c.js +1 -0
- ksadk/studio/static/assets/railroad-abnf-HS5TGJTU-DqQn7EzD.js +1 -0
- ksadk/studio/static/assets/railroad-ebnf-LZEXJU2U-B55kCA8F.js +1 -0
- ksadk/studio/static/assets/railroad-peg-WCYAUIDC-DfQnvkHd.js +1 -0
- ksadk/studio/static/assets/railroadDiagram-O6MQD6OU-DnhMjZCk.js +1 -0
- ksadk/studio/static/assets/requirementDiagram-BXWQKSXE-BF3d103U.js +84 -0
- ksadk/studio/static/assets/rolldown-runtime-Dy4uBu1J-DK3Fl9T5.js +1 -0
- ksadk/studio/static/assets/rough.esm-Dy-Kn_BL.js +1 -0
- ksadk/studio/static/assets/sankeyDiagram-P5KCCOFB-Bo-JTwt0.js +40 -0
- ksadk/studio/static/assets/sequenceDiagram-WJ2MYXX4-Dh4wZtUm.js +162 -0
- ksadk/studio/static/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
- ksadk/studio/static/assets/src-SFn9St9H.js +1 -0
- ksadk/studio/static/assets/stateDiagram-D77RDMKH-DH774G6d.js +1 -0
- ksadk/studio/static/assets/stateDiagram-v2-MP3YSRHH-DZvVWPBo.js +1 -0
- ksadk/studio/static/assets/swimlanes-42K2YHIH-Bl13OvTX.js +1 -0
- ksadk/studio/static/assets/swimlanesDiagram-VR7AAH4N-BnFOTALJ.js +8 -0
- ksadk/studio/static/assets/timeline-definition-24CTP7MA-DxmSSjRB.js +120 -0
- ksadk/studio/static/assets/treeView-Q6P3EWNA-D5Nn9Swz.js +1 -0
- ksadk/studio/static/assets/treemap-WGGIJYW6-CgHgnQhE.js +1 -0
- ksadk/studio/static/assets/vennDiagram-4TSXK5OY-B9QEXcJq.js +34 -0
- ksadk/studio/static/assets/wardley-WFR3VGLG-DXrK2RRY.js +1 -0
- ksadk/studio/static/assets/wardleyDiagram-VM6X3IG4-Ba1AOWoe.js +78 -0
- ksadk/studio/static/assets/xychartDiagram-S5SC5T6Z-Cr5pt5Y-.js +7 -0
- ksadk/studio/static/index.html +2 -2
- ksadk/studio/validator.py +1 -1
- ksadk/studio/workspace.py +1 -0
- ksadk/version.py +1 -1
- ksadk_runtime_common/workspace_files/path_utils.py +41 -0
- agentengine_sdk_python-0.8.2.dist-info/RECORD +0 -778
- ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +0 -1
- ksadk/server/static/assets/channel-4cQHKtx1.js +0 -1
- ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +0 -1
- ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +0 -1
- ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +0 -1
- ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +0 -1
- ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +0 -1
- ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +0 -1
- ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +0 -1
- ksadk/server/static/assets/index-8ipRcQ-M.js +0 -240
- ksadk/server/static/assets/index-ByFCqjlh.css +0 -2
- ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +0 -1
- ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +0 -1
- ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +0 -1
- ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +0 -1
- ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +0 -1
- ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +0 -1
- ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +0 -1
- ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +0 -1
- ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +0 -1
- ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +0 -8
- ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +0 -1
- ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +0 -1
- ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +0 -1
- ksadk/studio/static/assets/PrismRenderer-IYN-ffww.js +0 -3
- ksadk/studio/static/assets/index-BLpcdrgW.css +0 -1
- ksadk/studio/static/assets/index-CYekR2Xv.js +0 -260
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.4.dist-info}/WHEEL +0 -0
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.4.dist-info}/entry_points.txt +0 -0
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.4.dist-info}/licenses/LICENSE +0 -0
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1389 @@
|
|
|
1
|
+
"""Profile-level DSH tool capabilities exposed through a real MCP transport.
|
|
2
|
+
|
|
3
|
+
This module is deliberately orthogonal to ``dsh-agent-provider-host/v1``. It
|
|
4
|
+
boots one complete, immutable DSH Profile with a wheel-owned Cordis plugin,
|
|
5
|
+
reads a one-shot readiness record, and returns a process-scoped authenticated
|
|
6
|
+
Streamable HTTP MCP lease. Tool discovery and execution stay inside the real
|
|
7
|
+
``ctx.tools`` registry and its policy/cancellation pipeline.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import base64
|
|
14
|
+
import hashlib
|
|
15
|
+
import hmac
|
|
16
|
+
import json
|
|
17
|
+
import math
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import secrets
|
|
21
|
+
import shutil
|
|
22
|
+
import signal
|
|
23
|
+
import struct
|
|
24
|
+
import tempfile
|
|
25
|
+
import time
|
|
26
|
+
from collections import deque
|
|
27
|
+
from collections.abc import Mapping, Sequence
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, Literal, cast
|
|
31
|
+
from urllib.parse import urlsplit
|
|
32
|
+
|
|
33
|
+
import httpx
|
|
34
|
+
from pydantic import Field, field_validator, model_validator
|
|
35
|
+
|
|
36
|
+
from ksadk.plugins.bridges.dsh import DshProfileProjection, dsh_subprocess_environment
|
|
37
|
+
from ksadk.plugins.contracts import PluginContractModel
|
|
38
|
+
from ksadk.plugins.host import PluginHostError
|
|
39
|
+
from ksadk.plugins.providers.dsh import DshCircuitBreaker, DshCircuitSnapshot
|
|
40
|
+
|
|
41
|
+
DSH_CAPABILITY_HOST_PROTOCOL = "ksadk.dsh-capability-host/v1"
|
|
42
|
+
DSH_CAPABILITY_HOST_VERSION = "1.0.0"
|
|
43
|
+
DSH_CAPABILITY_BUNDLE_PACKAGE = "@kingsoftcloud/ksadk-dsh-capability-host"
|
|
44
|
+
DSH_CAPABILITY_DEFINITION = "mcp.connector/v1"
|
|
45
|
+
DSH_CAPABILITY_TRANSPORT = "streamable-http"
|
|
46
|
+
DSH_CAPABILITY_MCP_PROTOCOL = "2025-06-18"
|
|
47
|
+
DSH_CAPABILITY_MIN_NODE_VERSION = (22, 19, 0)
|
|
48
|
+
|
|
49
|
+
_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
50
|
+
_SEMVER = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$")
|
|
51
|
+
_PROFILE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
|
|
52
|
+
_TOOL_NAME = re.compile(r"^[A-Za-z0-9_.:-]+$")
|
|
53
|
+
_ENV_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
54
|
+
_NODE_VERSION = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$")
|
|
55
|
+
_SECRET_ENV = re.compile(
|
|
56
|
+
r"(?:^|_)(?:API_?KEY|ACCESS_?KEY|AUTH|BEARER|CREDENTIAL|PASSWORD|"
|
|
57
|
+
r"PRIVATE_?KEY|SECRET|TOKEN)(?:_|$)",
|
|
58
|
+
re.IGNORECASE,
|
|
59
|
+
)
|
|
60
|
+
_DIAGNOSTIC_SECRET = re.compile(
|
|
61
|
+
r"(?ix)(?:\bsk-[a-z0-9_-]{12,}\b|\bBearer\s+[a-z0-9._~+/=-]{8,}|"
|
|
62
|
+
r"\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|password|secret)"
|
|
63
|
+
r"\s*(?:=|:)\s*[^\s,;]+)"
|
|
64
|
+
)
|
|
65
|
+
_BLOCKED_ENV = frozenset(
|
|
66
|
+
{
|
|
67
|
+
"CORDIS_SHARED",
|
|
68
|
+
"DSH_HOME",
|
|
69
|
+
"KSADK_DSH_CAPABILITY_READY_FILE",
|
|
70
|
+
"KSADK_DSH_CAPABILITY_READY_STDOUT",
|
|
71
|
+
"KSADK_DSH_CAPABILITY_TOKEN",
|
|
72
|
+
"KSADK_DSH_PROFILE",
|
|
73
|
+
"KSADK_DSH_PROFILE_DIGEST",
|
|
74
|
+
"KSADK_DSH_VERSION",
|
|
75
|
+
"LD_PRELOAD",
|
|
76
|
+
"NODE_OPTIONS",
|
|
77
|
+
"PYTHONHOME",
|
|
78
|
+
"PYTHONPATH",
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
_MAX_READY_BYTES = 2 * 1024 * 1024
|
|
82
|
+
_MAX_CONFIG_BYTES = 8 * 1024 * 1024
|
|
83
|
+
_MAX_DIAGNOSTIC_CHARS = 4096
|
|
84
|
+
_NODE_MEMORY_LIMIT_MB = 512
|
|
85
|
+
_PROCESS_GROUP_ISOLATION_AVAILABLE = os.name == "posix"
|
|
86
|
+
_READY_PREFIX = "@@KSADK_DSH_CAPABILITY_READY@@"
|
|
87
|
+
_TOKEN_PREFIX = "@@KSADK_DSH_CAPABILITY_TOKEN@@"
|
|
88
|
+
_CORE_TOKEN_PREFIX = "@@KSADK_DSH_CORE_TOKEN@@"
|
|
89
|
+
_SCOPED_TOKEN_LIFETIME_SECONDS = 10 * 60
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _canonical_digest(value: Any) -> str:
|
|
93
|
+
try:
|
|
94
|
+
payload = json.dumps(
|
|
95
|
+
value,
|
|
96
|
+
ensure_ascii=False,
|
|
97
|
+
allow_nan=False,
|
|
98
|
+
sort_keys=True,
|
|
99
|
+
separators=(",", ":"),
|
|
100
|
+
).encode("utf-8")
|
|
101
|
+
except (TypeError, ValueError) as error:
|
|
102
|
+
raise ValueError("value must be strict JSON") from error
|
|
103
|
+
return f"sha256:{hashlib.sha256(payload).hexdigest()}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _canonical_json_value_bytes(value: Any) -> bytes:
|
|
107
|
+
"""Encode a JSON value identically in Python and the Node capability host.
|
|
108
|
+
|
|
109
|
+
JSON text is not a cross-language canonical form: CPython and ECMAScript
|
|
110
|
+
choose different spellings for valid binary64 values (for example
|
|
111
|
+
``1e-07`` versus ``1e-7``), and their native object-key ordering differs
|
|
112
|
+
for some Unicode keys. The inventory fence therefore uses a tiny tagged
|
|
113
|
+
value encoding with UTF-8 byte ordering and IEEE-754 bits for numbers.
|
|
114
|
+
It is not a new transport format; only the SHA-256 input uses these bytes.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
if value is None:
|
|
118
|
+
return b"n;"
|
|
119
|
+
if isinstance(value, bool):
|
|
120
|
+
return b"b1;" if value else b"b0;"
|
|
121
|
+
if isinstance(value, (int, float)):
|
|
122
|
+
try:
|
|
123
|
+
number = float(value)
|
|
124
|
+
except (OverflowError, ValueError) as error:
|
|
125
|
+
raise ValueError("JSON numbers must fit finite IEEE-754 binary64") from error
|
|
126
|
+
if not math.isfinite(number):
|
|
127
|
+
raise ValueError("JSON numbers must fit finite IEEE-754 binary64")
|
|
128
|
+
# JSON.stringify(-0) emits 0, so the digest also normalizes signed zero.
|
|
129
|
+
if number == 0:
|
|
130
|
+
number = 0.0
|
|
131
|
+
return b"d" + struct.pack(">d", number).hex().encode("ascii") + b";"
|
|
132
|
+
if isinstance(value, str):
|
|
133
|
+
try:
|
|
134
|
+
encoded = value.encode("utf-8")
|
|
135
|
+
except UnicodeEncodeError as error:
|
|
136
|
+
raise ValueError("JSON strings must contain Unicode scalar values") from error
|
|
137
|
+
return f"s{len(encoded)}:".encode("ascii") + encoded
|
|
138
|
+
if isinstance(value, (list, tuple)):
|
|
139
|
+
return f"a{len(value)}:".encode("ascii") + b"".join(
|
|
140
|
+
_canonical_json_value_bytes(item) for item in value
|
|
141
|
+
)
|
|
142
|
+
if isinstance(value, Mapping):
|
|
143
|
+
if any(not isinstance(key, str) for key in value):
|
|
144
|
+
raise ValueError("JSON object keys must be strings")
|
|
145
|
+
keys = sorted(value, key=lambda key: key.encode("utf-8"))
|
|
146
|
+
return f"o{len(keys)}:".encode("ascii") + b"".join(
|
|
147
|
+
_canonical_json_value_bytes(key) + _canonical_json_value_bytes(value[key])
|
|
148
|
+
for key in keys
|
|
149
|
+
)
|
|
150
|
+
raise ValueError("tool schemas must contain only JSON values")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class DshCapabilityTool(PluginContractModel):
|
|
154
|
+
"""One immutable MCP projection of a DSH ``ToolSchema``."""
|
|
155
|
+
|
|
156
|
+
name: str = Field(min_length=1, max_length=128)
|
|
157
|
+
# Normalize the upstream DSH limit to Studio/OpenAI's portable tool limit.
|
|
158
|
+
description: str = Field(max_length=1024)
|
|
159
|
+
input_schema: dict[str, Any]
|
|
160
|
+
|
|
161
|
+
@field_validator("name")
|
|
162
|
+
@classmethod
|
|
163
|
+
def validate_name(cls, value: str) -> str:
|
|
164
|
+
if not _TOOL_NAME.fullmatch(value):
|
|
165
|
+
raise ValueError("tool name contains unsupported characters")
|
|
166
|
+
return value
|
|
167
|
+
|
|
168
|
+
@model_validator(mode="after")
|
|
169
|
+
def validate_schema(self) -> "DshCapabilityTool":
|
|
170
|
+
payload = self.model_dump(by_alias=True, mode="json")
|
|
171
|
+
# This also rejects lone UTF-16 surrogates and non-finite/out-of-range
|
|
172
|
+
# numbers before they can make descriptor hashing or API serialization
|
|
173
|
+
# fail later in the lifecycle.
|
|
174
|
+
_canonical_json_value_bytes(payload)
|
|
175
|
+
encoded = json.dumps(
|
|
176
|
+
payload,
|
|
177
|
+
allow_nan=False,
|
|
178
|
+
ensure_ascii=False,
|
|
179
|
+
separators=(",", ":"),
|
|
180
|
+
).encode("utf-8")
|
|
181
|
+
if len(encoded) > _MAX_READY_BYTES:
|
|
182
|
+
raise ValueError("tool schema exceeds the capability descriptor limit")
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _inventory_digest(tools: Sequence[DshCapabilityTool]) -> str:
|
|
187
|
+
payload = [tool.model_dump(by_alias=True, mode="json") for tool in tools]
|
|
188
|
+
return f"sha256:{hashlib.sha256(_canonical_json_value_bytes(payload)).hexdigest()}"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _tool_sort_key(tool: DshCapabilityTool) -> bytes:
|
|
192
|
+
return tool.name.encode("utf-8")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class DshProfileCapabilityReady(PluginContractModel):
|
|
196
|
+
"""One-shot record written by the Cordis bridge after its listener binds."""
|
|
197
|
+
|
|
198
|
+
protocol_version: Literal["ksadk.dsh-capability-host/v1"]
|
|
199
|
+
host_version: str
|
|
200
|
+
dsh_version: str
|
|
201
|
+
profile: str = Field(min_length=1, max_length=64)
|
|
202
|
+
profile_digest: str
|
|
203
|
+
definition: Literal["mcp.connector/v1"]
|
|
204
|
+
transport: Literal["streamable-http"]
|
|
205
|
+
endpoint: str
|
|
206
|
+
inventory_digest: str
|
|
207
|
+
tools: tuple[DshCapabilityTool, ...] = Field(max_length=2048)
|
|
208
|
+
web_route_count: int = Field(ge=2)
|
|
209
|
+
|
|
210
|
+
@field_validator("host_version", "dsh_version")
|
|
211
|
+
@classmethod
|
|
212
|
+
def validate_version(cls, value: str) -> str:
|
|
213
|
+
if not _SEMVER.fullmatch(value):
|
|
214
|
+
raise ValueError("host versions must use exact semantic versioning")
|
|
215
|
+
return value
|
|
216
|
+
|
|
217
|
+
@field_validator("profile")
|
|
218
|
+
@classmethod
|
|
219
|
+
def validate_profile(cls, value: str) -> str:
|
|
220
|
+
if not _PROFILE.fullmatch(value):
|
|
221
|
+
raise ValueError("profile name is invalid")
|
|
222
|
+
return value
|
|
223
|
+
|
|
224
|
+
@field_validator("profile_digest", "inventory_digest")
|
|
225
|
+
@classmethod
|
|
226
|
+
def validate_digest(cls, value: str) -> str:
|
|
227
|
+
if not _DIGEST.fullmatch(value):
|
|
228
|
+
raise ValueError("capability digest is invalid")
|
|
229
|
+
return value
|
|
230
|
+
|
|
231
|
+
@field_validator("endpoint")
|
|
232
|
+
@classmethod
|
|
233
|
+
def validate_endpoint(cls, value: str) -> str:
|
|
234
|
+
parsed = urlsplit(value)
|
|
235
|
+
if (
|
|
236
|
+
parsed.scheme != "http"
|
|
237
|
+
or parsed.hostname != "127.0.0.1"
|
|
238
|
+
or parsed.port is None
|
|
239
|
+
or parsed.port < 1
|
|
240
|
+
or parsed.path != "/mcp"
|
|
241
|
+
or parsed.query
|
|
242
|
+
or parsed.fragment
|
|
243
|
+
or parsed.username is not None
|
|
244
|
+
or parsed.password is not None
|
|
245
|
+
):
|
|
246
|
+
raise ValueError("capability endpoint must be an unadorned loopback /mcp URL")
|
|
247
|
+
return value
|
|
248
|
+
|
|
249
|
+
@model_validator(mode="after")
|
|
250
|
+
def validate_inventory(self) -> "DshProfileCapabilityReady":
|
|
251
|
+
names = [tool.name for tool in self.tools]
|
|
252
|
+
if list(self.tools) != sorted(self.tools, key=_tool_sort_key) or len(names) != len(
|
|
253
|
+
set(names)
|
|
254
|
+
):
|
|
255
|
+
raise ValueError("capability tools must be sorted and unique")
|
|
256
|
+
if _inventory_digest(self.tools) != self.inventory_digest:
|
|
257
|
+
raise ValueError("capability inventory digest does not match its tools")
|
|
258
|
+
return self
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class DshProfileCapabilityDescriptor(PluginContractModel):
|
|
262
|
+
"""Stable profile capability facts; excludes the ephemeral URL and token."""
|
|
263
|
+
|
|
264
|
+
descriptor_format: Literal["dsh.profile-capability-descriptor/v1"] = (
|
|
265
|
+
"dsh.profile-capability-descriptor/v1"
|
|
266
|
+
)
|
|
267
|
+
ecosystem: Literal["dsh"] = "dsh"
|
|
268
|
+
bridge_package: Literal["@kingsoftcloud/ksadk-dsh-capability-host"] = (
|
|
269
|
+
"@kingsoftcloud/ksadk-dsh-capability-host"
|
|
270
|
+
)
|
|
271
|
+
bridge_version: Literal["1.0.0"] = "1.0.0"
|
|
272
|
+
dsh_version: str
|
|
273
|
+
profile: str = Field(min_length=1, max_length=64)
|
|
274
|
+
profile_digest: str
|
|
275
|
+
definition: Literal["mcp.connector/v1"] = "mcp.connector/v1"
|
|
276
|
+
transport: Literal["streamable-http"] = "streamable-http"
|
|
277
|
+
mcp_protocol: Literal["2025-06-18"] = "2025-06-18"
|
|
278
|
+
inventory_digest: str
|
|
279
|
+
tools: tuple[DshCapabilityTool, ...] = Field(max_length=2048)
|
|
280
|
+
|
|
281
|
+
@field_validator("dsh_version")
|
|
282
|
+
@classmethod
|
|
283
|
+
def validate_dsh_version(cls, value: str) -> str:
|
|
284
|
+
if not _SEMVER.fullmatch(value):
|
|
285
|
+
raise ValueError("dshVersion must use exact semantic versioning")
|
|
286
|
+
return value
|
|
287
|
+
|
|
288
|
+
@field_validator("profile")
|
|
289
|
+
@classmethod
|
|
290
|
+
def validate_descriptor_profile(cls, value: str) -> str:
|
|
291
|
+
if not _PROFILE.fullmatch(value):
|
|
292
|
+
raise ValueError("profile name is invalid")
|
|
293
|
+
return value
|
|
294
|
+
|
|
295
|
+
@field_validator("profile_digest", "inventory_digest")
|
|
296
|
+
@classmethod
|
|
297
|
+
def validate_descriptor_digest(cls, value: str) -> str:
|
|
298
|
+
if not _DIGEST.fullmatch(value):
|
|
299
|
+
raise ValueError("descriptor digest field is invalid")
|
|
300
|
+
return value
|
|
301
|
+
|
|
302
|
+
@model_validator(mode="after")
|
|
303
|
+
def validate_descriptor_inventory(self) -> "DshProfileCapabilityDescriptor":
|
|
304
|
+
names = [tool.name for tool in self.tools]
|
|
305
|
+
if list(self.tools) != sorted(self.tools, key=_tool_sort_key) or len(names) != len(
|
|
306
|
+
set(names)
|
|
307
|
+
):
|
|
308
|
+
raise ValueError("descriptor tools must be sorted and unique")
|
|
309
|
+
if _inventory_digest(self.tools) != self.inventory_digest:
|
|
310
|
+
raise ValueError("descriptor inventory digest does not match its tools")
|
|
311
|
+
return self
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def descriptor_digest(self) -> str:
|
|
315
|
+
return _canonical_digest(self.model_dump(by_alias=True, mode="json"))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@dataclass(frozen=True)
|
|
319
|
+
class DshMcpConnectorLease:
|
|
320
|
+
"""Ephemeral connection material for one running ``mcp.connector/v1``."""
|
|
321
|
+
|
|
322
|
+
endpoint: str = field(repr=False)
|
|
323
|
+
profile: str
|
|
324
|
+
profile_digest: str
|
|
325
|
+
descriptor_digest: str
|
|
326
|
+
_bearer_token: str = field(repr=False)
|
|
327
|
+
_browser_token: str = field(repr=False, default="")
|
|
328
|
+
web_route_count: int = 0
|
|
329
|
+
definition: Literal["mcp.connector/v1"] = "mcp.connector/v1"
|
|
330
|
+
transport: Literal["streamable-http"] = "streamable-http"
|
|
331
|
+
protocol_version: Literal["2025-06-18"] = "2025-06-18"
|
|
332
|
+
|
|
333
|
+
def headers(self) -> dict[str, str]:
|
|
334
|
+
"""Return fresh request headers without storing them in descriptors/logs."""
|
|
335
|
+
|
|
336
|
+
return {"Authorization": f"Bearer {self._bearer_token}"}
|
|
337
|
+
|
|
338
|
+
def resource_bearer_token(self, tool_aliases: Mapping[str, str], resource_leases) -> str:
|
|
339
|
+
"""Mint scope v2 from host-issued, activation-bound resource leases."""
|
|
340
|
+
from ksadk.resource_runtime.leases import ResourceLease
|
|
341
|
+
|
|
342
|
+
leases = tuple(resource_leases)
|
|
343
|
+
if not leases or any(not isinstance(lease, ResourceLease) for lease in leases):
|
|
344
|
+
raise ValueError("Resource scope requires host-issued leases")
|
|
345
|
+
first = leases[0].scope
|
|
346
|
+
if first.profile_digest != self.profile_digest:
|
|
347
|
+
raise ValueError("Resource scope belongs to another profile")
|
|
348
|
+
aliases = dict(tool_aliases)
|
|
349
|
+
if not 1 <= len(aliases) <= 32 or any(
|
|
350
|
+
not isinstance(alias, str)
|
|
351
|
+
or not isinstance(source, str)
|
|
352
|
+
or not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", alias)
|
|
353
|
+
or not _TOOL_NAME.fullmatch(source)
|
|
354
|
+
for alias, source in aliases.items()
|
|
355
|
+
):
|
|
356
|
+
raise ValueError("Resource scope requires explicit valid tool aliases")
|
|
357
|
+
resources = {}
|
|
358
|
+
lifetime = 600
|
|
359
|
+
for lease in leases:
|
|
360
|
+
scope = lease.scope
|
|
361
|
+
if (
|
|
362
|
+
scope.generation_id != first.generation_id
|
|
363
|
+
or scope.activation_id != first.activation_id
|
|
364
|
+
or scope.build_digest != first.build_digest
|
|
365
|
+
or scope.binding_snapshot_digest != first.binding_snapshot_digest
|
|
366
|
+
or scope.identity != first.identity
|
|
367
|
+
or scope.profile_digest != first.profile_digest
|
|
368
|
+
):
|
|
369
|
+
raise ValueError("Resource scope cannot mix activations")
|
|
370
|
+
lifetime = min(lifetime, math.floor(lease.expires_at - time.monotonic()))
|
|
371
|
+
for operation in scope.allowed_operations:
|
|
372
|
+
if operation not in aliases.values():
|
|
373
|
+
continue
|
|
374
|
+
if operation in resources:
|
|
375
|
+
raise ValueError("Resource operation has ambiguous bindings")
|
|
376
|
+
resources[operation] = {
|
|
377
|
+
"handle": lease.handle,
|
|
378
|
+
"bindingId": scope.binding_id,
|
|
379
|
+
"operation": operation,
|
|
380
|
+
}
|
|
381
|
+
if lifetime < 1 or not resources:
|
|
382
|
+
raise ValueError("Resource scope has no live operations")
|
|
383
|
+
issued = int(time.time())
|
|
384
|
+
payload = {
|
|
385
|
+
"v": 2,
|
|
386
|
+
"profileDigest": self.profile_digest,
|
|
387
|
+
"generationId": first.generation_id,
|
|
388
|
+
"activationId": first.activation_id,
|
|
389
|
+
"buildDigest": first.build_digest,
|
|
390
|
+
"bindingSnapshotDigest": first.binding_snapshot_digest,
|
|
391
|
+
"identity": first.identity.model_dump(by_alias=True, mode="json"),
|
|
392
|
+
"aliases": aliases,
|
|
393
|
+
"resources": resources,
|
|
394
|
+
"issuedAt": issued,
|
|
395
|
+
"expiresAt": issued + lifetime,
|
|
396
|
+
"jti": secrets.token_urlsafe(24),
|
|
397
|
+
}
|
|
398
|
+
raw = json.dumps(
|
|
399
|
+
payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False
|
|
400
|
+
).encode()
|
|
401
|
+
encoded = base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
|
|
402
|
+
message = f"ks2.{encoded}"
|
|
403
|
+
signature = (
|
|
404
|
+
base64.urlsafe_b64encode(
|
|
405
|
+
hmac.new(self._bearer_token.encode(), message.encode(), hashlib.sha256).digest()
|
|
406
|
+
)
|
|
407
|
+
.rstrip(b"=")
|
|
408
|
+
.decode()
|
|
409
|
+
)
|
|
410
|
+
token = f"{message}.{signature}"
|
|
411
|
+
if len(token) > 12 * 1024:
|
|
412
|
+
raise ValueError("Resource scope exceeds header size limit")
|
|
413
|
+
return token
|
|
414
|
+
|
|
415
|
+
def browser_url(self) -> str:
|
|
416
|
+
"""Return the one-time full Core DSH browser handoff URL."""
|
|
417
|
+
|
|
418
|
+
if self.web_route_count < 1 or not self._browser_token:
|
|
419
|
+
raise PluginHostError(
|
|
420
|
+
"dsh_core_web_unavailable",
|
|
421
|
+
"The selected DSH Profile does not expose the Core Web application",
|
|
422
|
+
)
|
|
423
|
+
parsed = urlsplit(self.endpoint)
|
|
424
|
+
return f"http://127.0.0.1:{parsed.port}/?token={self._browser_token}"
|
|
425
|
+
|
|
426
|
+
def bearer_token_for_runtime(self, tool_aliases: Mapping[str, str]) -> str:
|
|
427
|
+
"""Mint a generation-bound token limited to explicit tool names."""
|
|
428
|
+
|
|
429
|
+
aliases = {
|
|
430
|
+
str(alias).strip(): str(source).strip() for alias, source in tool_aliases.items()
|
|
431
|
+
}
|
|
432
|
+
if (
|
|
433
|
+
not aliases
|
|
434
|
+
or len(aliases) != len(tool_aliases)
|
|
435
|
+
or len(aliases) > 32
|
|
436
|
+
or any(
|
|
437
|
+
not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", alias) or not _TOOL_NAME.fullmatch(source)
|
|
438
|
+
for alias, source in aliases.items()
|
|
439
|
+
)
|
|
440
|
+
):
|
|
441
|
+
raise PluginHostError(
|
|
442
|
+
"dsh_capability_scope_invalid",
|
|
443
|
+
"DSH capability tokens require an explicit valid tool scope",
|
|
444
|
+
)
|
|
445
|
+
payload = json.dumps(
|
|
446
|
+
{
|
|
447
|
+
"v": 1,
|
|
448
|
+
"profileDigest": self.profile_digest,
|
|
449
|
+
"exp": int(time.time()) + _SCOPED_TOKEN_LIFETIME_SECONDS,
|
|
450
|
+
"jti": secrets.token_urlsafe(16),
|
|
451
|
+
"aliases": dict(sorted(aliases.items())),
|
|
452
|
+
},
|
|
453
|
+
allow_nan=False,
|
|
454
|
+
ensure_ascii=False,
|
|
455
|
+
sort_keys=True,
|
|
456
|
+
separators=(",", ":"),
|
|
457
|
+
).encode("utf-8")
|
|
458
|
+
encoded = base64.urlsafe_b64encode(payload).rstrip(b"=")
|
|
459
|
+
signature = hmac.new(self._bearer_token.encode("utf-8"), encoded, hashlib.sha256).digest()
|
|
460
|
+
encoded_signature = base64.urlsafe_b64encode(signature).rstrip(b"=")
|
|
461
|
+
token = f"ks1.{encoded.decode('ascii')}.{encoded_signature.decode('ascii')}"
|
|
462
|
+
if len(token) > 12 * 1024:
|
|
463
|
+
raise PluginHostError(
|
|
464
|
+
"dsh_capability_scope_too_large",
|
|
465
|
+
"DSH capability token scope exceeds the HTTP header budget",
|
|
466
|
+
)
|
|
467
|
+
return token
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
class DshProfileCapabilityInventory(PluginContractModel):
|
|
471
|
+
ecosystem: Literal["dsh"] = "dsh"
|
|
472
|
+
profile: str
|
|
473
|
+
profile_digest: str
|
|
474
|
+
descriptor_digest: str | None = None
|
|
475
|
+
inventory_digest: str | None = None
|
|
476
|
+
state: Literal["stopped", "starting", "ready", "failed", "disposed"]
|
|
477
|
+
pid: int | None = Field(default=None, ge=1)
|
|
478
|
+
tool_count: int = Field(ge=0)
|
|
479
|
+
circuit_state: Literal["closed", "open", "half-open"]
|
|
480
|
+
consecutive_failures: int = Field(ge=0)
|
|
481
|
+
retry_after_seconds: float = Field(ge=0)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@dataclass(frozen=True)
|
|
485
|
+
class DshCapabilityBundle:
|
|
486
|
+
package_name: str
|
|
487
|
+
version: str
|
|
488
|
+
root: Path
|
|
489
|
+
entrypoint: Path
|
|
490
|
+
patch: Path
|
|
491
|
+
digest: str
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def load_dsh_capability_bundle() -> DshCapabilityBundle:
|
|
495
|
+
"""Locate and validate the immutable bridge assets shipped in the wheel."""
|
|
496
|
+
|
|
497
|
+
root = Path(__file__).with_name("bundles") / "ksadk-dsh-capability-host"
|
|
498
|
+
manifest_path = root / "package.json"
|
|
499
|
+
try:
|
|
500
|
+
payload = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
501
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as error:
|
|
502
|
+
raise PluginHostError(
|
|
503
|
+
"dsh_capability_bundle_invalid", "DSH capability Bundle manifest is unavailable"
|
|
504
|
+
) from error
|
|
505
|
+
dsh = payload.get("dsh") if isinstance(payload, dict) else None
|
|
506
|
+
bundle = dsh.get("bundle") if isinstance(dsh, dict) else None
|
|
507
|
+
patch_value = bundle.get("patch") if isinstance(bundle, dict) else None
|
|
508
|
+
integrity = bundle.get("integrity") if isinstance(bundle, dict) else None
|
|
509
|
+
if (
|
|
510
|
+
not isinstance(payload, dict)
|
|
511
|
+
or payload.get("name") != DSH_CAPABILITY_BUNDLE_PACKAGE
|
|
512
|
+
or payload.get("version") != DSH_CAPABILITY_HOST_VERSION
|
|
513
|
+
or payload.get("type") != "module"
|
|
514
|
+
or patch_value != "./cordis.patch.yml"
|
|
515
|
+
or not isinstance(integrity, str)
|
|
516
|
+
or not _DIGEST.fullmatch(integrity)
|
|
517
|
+
):
|
|
518
|
+
raise PluginHostError(
|
|
519
|
+
"dsh_capability_bundle_invalid", "DSH capability Bundle identity is invalid"
|
|
520
|
+
)
|
|
521
|
+
entrypoint = (root / "index.mjs").resolve()
|
|
522
|
+
patch = (root / str(patch_value)).resolve()
|
|
523
|
+
resolved_root = root.resolve()
|
|
524
|
+
if (
|
|
525
|
+
not entrypoint.is_relative_to(resolved_root)
|
|
526
|
+
or not patch.is_relative_to(resolved_root)
|
|
527
|
+
or not entrypoint.is_file()
|
|
528
|
+
or not patch.is_file()
|
|
529
|
+
):
|
|
530
|
+
raise PluginHostError(
|
|
531
|
+
"dsh_capability_bundle_invalid", "DSH capability Bundle assets are missing"
|
|
532
|
+
)
|
|
533
|
+
hasher = hashlib.sha256()
|
|
534
|
+
for name, path in (("index.mjs", entrypoint), ("cordis.patch.yml", patch)):
|
|
535
|
+
hasher.update(name.encode("utf-8"))
|
|
536
|
+
hasher.update(b"\0")
|
|
537
|
+
hasher.update(path.read_bytes())
|
|
538
|
+
hasher.update(b"\0")
|
|
539
|
+
digest = f"sha256:{hasher.hexdigest()}"
|
|
540
|
+
if digest != integrity:
|
|
541
|
+
raise PluginHostError(
|
|
542
|
+
"dsh_capability_bundle_invalid",
|
|
543
|
+
"DSH capability Bundle integrity does not match its assets",
|
|
544
|
+
)
|
|
545
|
+
return DshCapabilityBundle(
|
|
546
|
+
package_name=DSH_CAPABILITY_BUNDLE_PACKAGE,
|
|
547
|
+
version=DSH_CAPABILITY_HOST_VERSION,
|
|
548
|
+
root=resolved_root,
|
|
549
|
+
entrypoint=entrypoint,
|
|
550
|
+
patch=patch,
|
|
551
|
+
digest=digest,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class DshProfileCapabilityHost:
|
|
556
|
+
"""Supervise one complete DSH Profile and its loopback MCP exporter."""
|
|
557
|
+
|
|
558
|
+
def __init__(
|
|
559
|
+
self,
|
|
560
|
+
dsh_command: Sequence[str],
|
|
561
|
+
*,
|
|
562
|
+
projection: DshProfileProjection,
|
|
563
|
+
dsh_home: Path,
|
|
564
|
+
cwd: Path | None = None,
|
|
565
|
+
environment: Mapping[str, str] | None = None,
|
|
566
|
+
node_command: str | Path | None = None,
|
|
567
|
+
studio_index: Path | None = None,
|
|
568
|
+
studio_models: Any = None,
|
|
569
|
+
startup_timeout: float = 15.0,
|
|
570
|
+
shutdown_timeout: float = 5.0,
|
|
571
|
+
health_timeout: float = 2.0,
|
|
572
|
+
call_timeout: float = 60.0,
|
|
573
|
+
max_argument_bytes: int = 256 * 1024,
|
|
574
|
+
max_result_bytes: int = 1024 * 1024,
|
|
575
|
+
max_request_bytes: int = 1024 * 1024,
|
|
576
|
+
max_in_flight: int = 64,
|
|
577
|
+
inventory_quiet: float = 2.0,
|
|
578
|
+
circuit_failure_threshold: int = 3,
|
|
579
|
+
circuit_recovery_timeout: float = 5.0,
|
|
580
|
+
) -> None:
|
|
581
|
+
self._command = self._validate_command(dsh_command)
|
|
582
|
+
if not _PROFILE.fullmatch(projection.profile) or not _DIGEST.fullmatch(
|
|
583
|
+
projection.config_digest
|
|
584
|
+
):
|
|
585
|
+
raise PluginHostError(
|
|
586
|
+
"dsh_capability_projection_invalid", "DSH Profile projection is invalid"
|
|
587
|
+
)
|
|
588
|
+
if projection.config_bytes < 0 or projection.config_bytes > _MAX_CONFIG_BYTES:
|
|
589
|
+
raise PluginHostError(
|
|
590
|
+
"dsh_capability_projection_invalid", "DSH Profile projection is too large"
|
|
591
|
+
)
|
|
592
|
+
if not _SEMVER.fullmatch(projection.host_version):
|
|
593
|
+
raise PluginHostError(
|
|
594
|
+
"dsh_capability_projection_invalid", "DSH Profile host version is invalid"
|
|
595
|
+
)
|
|
596
|
+
self._projection = projection
|
|
597
|
+
self._dsh_home = dsh_home.expanduser().resolve()
|
|
598
|
+
self._cwd = (cwd or Path.cwd()).expanduser().resolve()
|
|
599
|
+
self._explicit_environment = self._validate_environment(environment or {})
|
|
600
|
+
self._node_command = str(node_command).strip() if node_command is not None else None
|
|
601
|
+
self._startup_timeout = self._positive_float(startup_timeout, "startup_timeout")
|
|
602
|
+
self._shutdown_timeout = self._positive_float(shutdown_timeout, "shutdown_timeout")
|
|
603
|
+
self._health_timeout = self._positive_float(health_timeout, "health_timeout")
|
|
604
|
+
self._call_timeout_ms = self._milliseconds(call_timeout, "call_timeout", 3_600_000)
|
|
605
|
+
self._max_argument_bytes = self._positive_int(
|
|
606
|
+
max_argument_bytes, "max_argument_bytes", 8 * 1024 * 1024
|
|
607
|
+
)
|
|
608
|
+
self._max_result_bytes = self._positive_int(
|
|
609
|
+
max_result_bytes, "max_result_bytes", 8 * 1024 * 1024
|
|
610
|
+
)
|
|
611
|
+
self._max_request_bytes = self._positive_int(
|
|
612
|
+
max_request_bytes, "max_request_bytes", 8 * 1024 * 1024
|
|
613
|
+
)
|
|
614
|
+
self._max_in_flight = self._positive_int(max_in_flight, "max_in_flight", 1024)
|
|
615
|
+
self._inventory_quiet_ms = self._milliseconds(
|
|
616
|
+
inventory_quiet, "inventory_quiet", 5_000
|
|
617
|
+
)
|
|
618
|
+
self._bundle = load_dsh_capability_bundle()
|
|
619
|
+
self._studio_index = studio_index
|
|
620
|
+
self._studio_models = studio_models
|
|
621
|
+
self._circuit = DshCircuitBreaker(
|
|
622
|
+
failure_threshold=circuit_failure_threshold,
|
|
623
|
+
recovery_timeout=circuit_recovery_timeout,
|
|
624
|
+
)
|
|
625
|
+
self._lifecycle_lock = asyncio.Lock()
|
|
626
|
+
self._process: asyncio.subprocess.Process | None = None
|
|
627
|
+
self._stdout_task: asyncio.Task[None] | None = None
|
|
628
|
+
self._stderr_task: asyncio.Task[None] | None = None
|
|
629
|
+
self._monitor_task: asyncio.Task[None] | None = None
|
|
630
|
+
self._ready_future: asyncio.Future[DshProfileCapabilityReady] | None = None
|
|
631
|
+
self._token_future: asyncio.Future[str] | None = None
|
|
632
|
+
self._core_token_future: asyncio.Future[str] | None = None
|
|
633
|
+
self._runtime_dir: Path | None = None
|
|
634
|
+
self._resource_socket_path: Path | None = None
|
|
635
|
+
self._lease: DshMcpConnectorLease | None = None
|
|
636
|
+
self._descriptor: DshProfileCapabilityDescriptor | None = None
|
|
637
|
+
self._stdout_tail: deque[str] = deque(maxlen=64)
|
|
638
|
+
self._stderr_tail: deque[str] = deque(maxlen=64)
|
|
639
|
+
self._state: Literal["stopped", "starting", "ready", "failed", "disposed"] = "stopped"
|
|
640
|
+
self._disposed = False
|
|
641
|
+
self._stopping = False
|
|
642
|
+
self._process_failure_recorded = False
|
|
643
|
+
|
|
644
|
+
@property
|
|
645
|
+
def pid(self) -> int | None:
|
|
646
|
+
process = self._process
|
|
647
|
+
return process.pid if process is not None and process.returncode is None else None
|
|
648
|
+
|
|
649
|
+
@property
|
|
650
|
+
def descriptor(self) -> DshProfileCapabilityDescriptor:
|
|
651
|
+
if self._descriptor is None:
|
|
652
|
+
raise PluginHostError(
|
|
653
|
+
"dsh_capability_not_ready", "DSH capability descriptor is not ready"
|
|
654
|
+
)
|
|
655
|
+
return self._descriptor
|
|
656
|
+
|
|
657
|
+
@property
|
|
658
|
+
def circuit_snapshot(self) -> DshCircuitSnapshot:
|
|
659
|
+
return self._circuit.snapshot()
|
|
660
|
+
|
|
661
|
+
@property
|
|
662
|
+
def stdout_tail(self) -> tuple[str, ...]:
|
|
663
|
+
return tuple(self._stdout_tail)
|
|
664
|
+
|
|
665
|
+
@property
|
|
666
|
+
def stderr_tail(self) -> tuple[str, ...]:
|
|
667
|
+
return tuple(self._stderr_tail)
|
|
668
|
+
|
|
669
|
+
async def start(self) -> DshMcpConnectorLease:
|
|
670
|
+
async with self._lifecycle_lock:
|
|
671
|
+
if not _PROCESS_GROUP_ISOLATION_AVAILABLE:
|
|
672
|
+
raise PluginHostError(
|
|
673
|
+
"dsh_capability_platform_unsupported",
|
|
674
|
+
"DSH capability host requires POSIX process-group isolation",
|
|
675
|
+
)
|
|
676
|
+
if self._disposed:
|
|
677
|
+
raise PluginHostError(
|
|
678
|
+
"dsh_capability_host_disposed", "DSH capability host is disposed"
|
|
679
|
+
)
|
|
680
|
+
if self.pid is not None and self._state == "ready" and self._lease is not None:
|
|
681
|
+
return self._lease
|
|
682
|
+
if self._process is not None:
|
|
683
|
+
await self._terminate()
|
|
684
|
+
self._require_circuit_probe()
|
|
685
|
+
self._state = "starting"
|
|
686
|
+
self._process_failure_recorded = False
|
|
687
|
+
try:
|
|
688
|
+
environment = self._base_environment()
|
|
689
|
+
if self._studio_models is not None:
|
|
690
|
+
environment.update(self._studio_models.environment)
|
|
691
|
+
await self._require_node_version(environment)
|
|
692
|
+
await self._verify_projection(environment)
|
|
693
|
+
runtime_dir = Path(tempfile.mkdtemp(prefix="ksadk-dsh-capability-"))
|
|
694
|
+
runtime_dir.chmod(0o700)
|
|
695
|
+
self._runtime_dir = runtime_dir
|
|
696
|
+
overlay = runtime_dir / "cordis.patch.yml"
|
|
697
|
+
overlay.write_text(self._overlay_text(), encoding="utf-8")
|
|
698
|
+
overlay.chmod(0o600)
|
|
699
|
+
environment.update(
|
|
700
|
+
{
|
|
701
|
+
"DSH_TELEMETRY_DISABLED": "1",
|
|
702
|
+
"KSADK_DSH_CAPABILITY_READY_STDOUT": "1",
|
|
703
|
+
"KSADK_DSH_PROFILE": self._projection.profile,
|
|
704
|
+
"KSADK_DSH_PROFILE_DIGEST": self._projection.config_digest,
|
|
705
|
+
"KSADK_DSH_VERSION": self._projection.host_version,
|
|
706
|
+
}
|
|
707
|
+
)
|
|
708
|
+
process = await asyncio.create_subprocess_exec(
|
|
709
|
+
*self._command,
|
|
710
|
+
"--profile",
|
|
711
|
+
self._projection.profile,
|
|
712
|
+
"--patch",
|
|
713
|
+
str(overlay),
|
|
714
|
+
"--host",
|
|
715
|
+
"127.0.0.1",
|
|
716
|
+
"--port",
|
|
717
|
+
"0",
|
|
718
|
+
"--no-open",
|
|
719
|
+
cwd=str(self._cwd),
|
|
720
|
+
env=environment,
|
|
721
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
722
|
+
stdout=asyncio.subprocess.PIPE,
|
|
723
|
+
stderr=asyncio.subprocess.PIPE,
|
|
724
|
+
start_new_session=True,
|
|
725
|
+
)
|
|
726
|
+
self._process = process
|
|
727
|
+
ready_future = asyncio.get_running_loop().create_future()
|
|
728
|
+
token_future = asyncio.get_running_loop().create_future()
|
|
729
|
+
core_token_future = asyncio.get_running_loop().create_future()
|
|
730
|
+
self._ready_future = ready_future
|
|
731
|
+
self._token_future = token_future
|
|
732
|
+
self._core_token_future = core_token_future
|
|
733
|
+
self._stdout_task = asyncio.create_task(
|
|
734
|
+
self._capture_stream(
|
|
735
|
+
process.stdout,
|
|
736
|
+
self._stdout_tail,
|
|
737
|
+
ready_future=ready_future,
|
|
738
|
+
token_future=token_future,
|
|
739
|
+
core_token_future=core_token_future,
|
|
740
|
+
)
|
|
741
|
+
)
|
|
742
|
+
self._stderr_task = asyncio.create_task(
|
|
743
|
+
self._capture_stream(process.stderr, self._stderr_tail)
|
|
744
|
+
)
|
|
745
|
+
self._monitor_task = asyncio.create_task(self._monitor_process(process))
|
|
746
|
+
ready, token, core_token = await self._wait_ready(
|
|
747
|
+
ready_future,
|
|
748
|
+
token_future,
|
|
749
|
+
core_token_future,
|
|
750
|
+
process,
|
|
751
|
+
)
|
|
752
|
+
self._validate_ready_fences(ready)
|
|
753
|
+
descriptor = DshProfileCapabilityDescriptor(
|
|
754
|
+
dsh_version=ready.dsh_version,
|
|
755
|
+
profile=ready.profile,
|
|
756
|
+
profile_digest=ready.profile_digest,
|
|
757
|
+
inventory_digest=ready.inventory_digest,
|
|
758
|
+
tools=ready.tools,
|
|
759
|
+
)
|
|
760
|
+
lease = DshMcpConnectorLease(
|
|
761
|
+
endpoint=ready.endpoint,
|
|
762
|
+
profile=ready.profile,
|
|
763
|
+
profile_digest=ready.profile_digest,
|
|
764
|
+
descriptor_digest=descriptor.descriptor_digest,
|
|
765
|
+
_bearer_token=token,
|
|
766
|
+
_browser_token=core_token,
|
|
767
|
+
web_route_count=ready.web_route_count,
|
|
768
|
+
)
|
|
769
|
+
self._descriptor = descriptor
|
|
770
|
+
self._lease = lease
|
|
771
|
+
if not await self._probe_health(lease, descriptor):
|
|
772
|
+
raise PluginHostError(
|
|
773
|
+
"dsh_capability_health_failed",
|
|
774
|
+
"DSH capability MCP endpoint failed its startup health check",
|
|
775
|
+
)
|
|
776
|
+
self._state = "ready"
|
|
777
|
+
self._circuit.succeed()
|
|
778
|
+
return lease
|
|
779
|
+
except asyncio.CancelledError:
|
|
780
|
+
await self._terminate()
|
|
781
|
+
self._state = "stopped"
|
|
782
|
+
raise
|
|
783
|
+
except PluginHostError:
|
|
784
|
+
self._record_process_failure()
|
|
785
|
+
await self._terminate()
|
|
786
|
+
self._state = "failed"
|
|
787
|
+
raise
|
|
788
|
+
except Exception as error:
|
|
789
|
+
self._record_process_failure()
|
|
790
|
+
await self._terminate()
|
|
791
|
+
self._state = "failed"
|
|
792
|
+
raise PluginHostError(
|
|
793
|
+
"dsh_capability_start_failed",
|
|
794
|
+
f"DSH capability host failed to start: {type(error).__name__}",
|
|
795
|
+
) from error
|
|
796
|
+
|
|
797
|
+
async def lease(self) -> DshMcpConnectorLease:
|
|
798
|
+
return await self.start()
|
|
799
|
+
|
|
800
|
+
async def health(self) -> bool:
|
|
801
|
+
lease = self._lease
|
|
802
|
+
descriptor = self._descriptor
|
|
803
|
+
if self.pid is None or lease is None or descriptor is None or self._state != "ready":
|
|
804
|
+
return False
|
|
805
|
+
healthy = await self._probe_health(lease, descriptor)
|
|
806
|
+
if healthy:
|
|
807
|
+
self._circuit.succeed()
|
|
808
|
+
else:
|
|
809
|
+
self._circuit.fail()
|
|
810
|
+
self._state = "failed"
|
|
811
|
+
return healthy
|
|
812
|
+
|
|
813
|
+
async def inventory(self) -> DshProfileCapabilityInventory:
|
|
814
|
+
descriptor = self._descriptor
|
|
815
|
+
state = self._state
|
|
816
|
+
if state == "ready" and not await self.health():
|
|
817
|
+
state = "failed"
|
|
818
|
+
snapshot = self._circuit.snapshot()
|
|
819
|
+
return DshProfileCapabilityInventory(
|
|
820
|
+
profile=self._projection.profile,
|
|
821
|
+
profile_digest=self._projection.config_digest,
|
|
822
|
+
descriptor_digest=descriptor.descriptor_digest if descriptor is not None else None,
|
|
823
|
+
inventory_digest=descriptor.inventory_digest if descriptor is not None else None,
|
|
824
|
+
state=state,
|
|
825
|
+
pid=self.pid,
|
|
826
|
+
tool_count=len(descriptor.tools) if descriptor is not None else 0,
|
|
827
|
+
circuit_state=snapshot.state,
|
|
828
|
+
consecutive_failures=snapshot.consecutive_failures,
|
|
829
|
+
retry_after_seconds=snapshot.retry_after_seconds,
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
async def dispose(self) -> None:
|
|
833
|
+
async with self._lifecycle_lock:
|
|
834
|
+
if self._disposed:
|
|
835
|
+
return
|
|
836
|
+
self._disposed = True
|
|
837
|
+
await self._terminate()
|
|
838
|
+
self._state = "disposed"
|
|
839
|
+
|
|
840
|
+
async def configure_resource_socket(self, path: Path) -> None:
|
|
841
|
+
"""Stop any old Core and bind next-start IPC after owner revokes old resources."""
|
|
842
|
+
import stat
|
|
843
|
+
|
|
844
|
+
path = Path(path)
|
|
845
|
+
if (
|
|
846
|
+
not path.is_absolute() or path.is_symlink()
|
|
847
|
+
or not stat.S_ISSOCK(path.stat().st_mode)
|
|
848
|
+
or path.stat().st_mode & 0o077 or path.parent.stat().st_mode & 0o077
|
|
849
|
+
):
|
|
850
|
+
raise PluginHostError("dsh_resource_socket_invalid", "Resource IPC must be private")
|
|
851
|
+
async with self._lifecycle_lock:
|
|
852
|
+
if self._disposed:
|
|
853
|
+
raise PluginHostError("dsh_resource_socket_busy", "Resource host is disposed")
|
|
854
|
+
if self._process is not None:
|
|
855
|
+
await self._terminate()
|
|
856
|
+
self._state = "stopped"
|
|
857
|
+
self._resource_socket_path = path
|
|
858
|
+
|
|
859
|
+
def _overlay_text(self) -> str:
|
|
860
|
+
entrypoint = json.dumps(self._bundle.entrypoint.as_uri())
|
|
861
|
+
overlay = (
|
|
862
|
+
"- insert:\n"
|
|
863
|
+
" - id: ksadk-dsh-capability-host\n"
|
|
864
|
+
f" name: {entrypoint}\n"
|
|
865
|
+
" config:\n"
|
|
866
|
+
f" callTimeoutMs: {self._call_timeout_ms}\n"
|
|
867
|
+
f" maxArgumentBytes: {self._max_argument_bytes}\n"
|
|
868
|
+
f" maxResultBytes: {self._max_result_bytes}\n"
|
|
869
|
+
f" maxRequestBytes: {self._max_request_bytes}\n"
|
|
870
|
+
f" maxInFlight: {self._max_in_flight}\n"
|
|
871
|
+
f" inventoryQuietMs: {self._inventory_quiet_ms}\n"
|
|
872
|
+
)
|
|
873
|
+
if self._resource_socket_path is not None:
|
|
874
|
+
overlay += (
|
|
875
|
+
"- id: dsh-platform-resources\n"
|
|
876
|
+
" config:\n"
|
|
877
|
+
f" socketPath: {json.dumps(str(self._resource_socket_path))}\n"
|
|
878
|
+
)
|
|
879
|
+
if self._studio_index is not None:
|
|
880
|
+
app = self._bundle.root.parent / "ksadk-dsh-studio" / "index.mjs"
|
|
881
|
+
overlay += (
|
|
882
|
+
# Studio owns the settings outlet. The stock settings shell
|
|
883
|
+
# declares the same child slot and must not compete for it.
|
|
884
|
+
# Core's settings service, settingsScope and RPC stay enabled.
|
|
885
|
+
"- id: ui-settings-general\n"
|
|
886
|
+
" disabled: true\n"
|
|
887
|
+
"- insert:\n"
|
|
888
|
+
" - id: ksadk-studio-app\n"
|
|
889
|
+
f" name: {json.dumps(app.as_uri())}\n"
|
|
890
|
+
" config:\n"
|
|
891
|
+
f" indexPath: {json.dumps(str(self._studio_index))}\n"
|
|
892
|
+
)
|
|
893
|
+
if self._studio_models is not None and self._studio_models.providers:
|
|
894
|
+
overlay += (
|
|
895
|
+
"- id: llm-pi-ai\n"
|
|
896
|
+
" config:\n"
|
|
897
|
+
f" providers: {json.dumps(self._studio_models.providers)}\n"
|
|
898
|
+
"- id: agent-default-model\n"
|
|
899
|
+
" config:\n"
|
|
900
|
+
f" provider: {json.dumps(self._studio_models.default_provider)}\n"
|
|
901
|
+
f" model: {json.dumps(self._studio_models.default_model)}\n"
|
|
902
|
+
)
|
|
903
|
+
return overlay
|
|
904
|
+
|
|
905
|
+
def _base_environment(self) -> dict[str, str]:
|
|
906
|
+
environment = cast(dict[str, str], dsh_subprocess_environment(dsh_home=self._dsh_home))
|
|
907
|
+
environment.update(self._explicit_environment)
|
|
908
|
+
# The caller cannot inject NODE_OPTIONS, but the supervised sidecar gets
|
|
909
|
+
# a fixed heap ceiling. This also applies to Cordis plugin child Nodes.
|
|
910
|
+
environment["NODE_OPTIONS"] = f"--max-old-space-size={_NODE_MEMORY_LIMIT_MB}"
|
|
911
|
+
return environment
|
|
912
|
+
|
|
913
|
+
async def _require_node_version(self, environment: Mapping[str, str]) -> None:
|
|
914
|
+
executable = self._node_command
|
|
915
|
+
if executable is None:
|
|
916
|
+
executable = shutil.which("node", path=environment.get("PATH"))
|
|
917
|
+
if not executable or "\x00" in executable:
|
|
918
|
+
raise PluginHostError(
|
|
919
|
+
"dsh_capability_node_unavailable", "Node.js is required for DSH capabilities"
|
|
920
|
+
)
|
|
921
|
+
candidate = Path(executable).expanduser()
|
|
922
|
+
if candidate.is_absolute() or len(candidate.parts) > 1:
|
|
923
|
+
if not candidate.is_file():
|
|
924
|
+
raise PluginHostError(
|
|
925
|
+
"dsh_capability_node_unavailable", "Node.js executable is unavailable"
|
|
926
|
+
)
|
|
927
|
+
executable = str(candidate.resolve())
|
|
928
|
+
try:
|
|
929
|
+
process = await asyncio.create_subprocess_exec(
|
|
930
|
+
executable,
|
|
931
|
+
"--version",
|
|
932
|
+
cwd=str(self._cwd),
|
|
933
|
+
env=dict(environment),
|
|
934
|
+
stdout=asyncio.subprocess.PIPE,
|
|
935
|
+
stderr=asyncio.subprocess.PIPE,
|
|
936
|
+
)
|
|
937
|
+
except OSError as error:
|
|
938
|
+
raise PluginHostError(
|
|
939
|
+
"dsh_capability_node_unavailable", "Node.js version probe failed"
|
|
940
|
+
) from error
|
|
941
|
+
try:
|
|
942
|
+
stdout, _stderr = await asyncio.wait_for(
|
|
943
|
+
process.communicate(), timeout=self._health_timeout
|
|
944
|
+
)
|
|
945
|
+
except asyncio.CancelledError:
|
|
946
|
+
if process.returncode is None:
|
|
947
|
+
process.kill()
|
|
948
|
+
await process.wait()
|
|
949
|
+
raise
|
|
950
|
+
except asyncio.TimeoutError as error:
|
|
951
|
+
if process.returncode is None:
|
|
952
|
+
process.kill()
|
|
953
|
+
await process.wait()
|
|
954
|
+
raise PluginHostError(
|
|
955
|
+
"dsh_capability_node_unavailable", "Node.js version probe failed"
|
|
956
|
+
) from error
|
|
957
|
+
match = _NODE_VERSION.fullmatch(stdout.decode("utf-8", errors="replace").strip())
|
|
958
|
+
if process.returncode != 0 or match is None:
|
|
959
|
+
raise PluginHostError(
|
|
960
|
+
"dsh_capability_node_unavailable", "Node.js did not report a valid version"
|
|
961
|
+
)
|
|
962
|
+
version = tuple(int(value) for value in match.groups())
|
|
963
|
+
if version < DSH_CAPABILITY_MIN_NODE_VERSION:
|
|
964
|
+
minimum = ".".join(str(value) for value in DSH_CAPABILITY_MIN_NODE_VERSION)
|
|
965
|
+
raise PluginHostError(
|
|
966
|
+
"dsh_capability_node_version_unsupported",
|
|
967
|
+
f"DSH capabilities require Node.js >= {minimum}",
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
async def _verify_projection(self, environment: Mapping[str, str]) -> None:
|
|
971
|
+
try:
|
|
972
|
+
process = await asyncio.create_subprocess_exec(
|
|
973
|
+
*self._command,
|
|
974
|
+
"--profile",
|
|
975
|
+
self._projection.profile,
|
|
976
|
+
"--dump-config",
|
|
977
|
+
cwd=str(self._cwd),
|
|
978
|
+
env=dict(environment),
|
|
979
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
980
|
+
stdout=asyncio.subprocess.PIPE,
|
|
981
|
+
stderr=asyncio.subprocess.PIPE,
|
|
982
|
+
start_new_session=os.name == "posix",
|
|
983
|
+
)
|
|
984
|
+
except OSError as error:
|
|
985
|
+
raise PluginHostError(
|
|
986
|
+
"dsh_capability_profile_unavailable", "DSH Profile preflight did not complete"
|
|
987
|
+
) from error
|
|
988
|
+
try:
|
|
989
|
+
stdout, stderr = await asyncio.wait_for(
|
|
990
|
+
process.communicate(), timeout=self._startup_timeout
|
|
991
|
+
)
|
|
992
|
+
except asyncio.CancelledError:
|
|
993
|
+
await self._terminate_process_tree(process)
|
|
994
|
+
raise
|
|
995
|
+
except asyncio.TimeoutError as error:
|
|
996
|
+
await self._terminate_process_tree(process)
|
|
997
|
+
raise PluginHostError(
|
|
998
|
+
"dsh_capability_profile_unavailable", "DSH Profile preflight did not complete"
|
|
999
|
+
) from error
|
|
1000
|
+
self._append_diagnostic(stderr, self._stderr_tail)
|
|
1001
|
+
digest = f"sha256:{hashlib.sha256(stdout).hexdigest()}"
|
|
1002
|
+
if process.returncode != 0:
|
|
1003
|
+
raise PluginHostError(
|
|
1004
|
+
"dsh_capability_profile_unavailable", "DSH Profile preflight failed"
|
|
1005
|
+
)
|
|
1006
|
+
if len(stdout) != self._projection.config_bytes or digest != self._projection.config_digest:
|
|
1007
|
+
raise PluginHostError(
|
|
1008
|
+
"dsh_capability_profile_changed",
|
|
1009
|
+
"DSH Profile changed after its immutable projection was selected",
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
async def _wait_ready(
|
|
1013
|
+
self,
|
|
1014
|
+
ready_future: asyncio.Future[DshProfileCapabilityReady],
|
|
1015
|
+
token_future: asyncio.Future[str],
|
|
1016
|
+
core_token_future: asyncio.Future[str],
|
|
1017
|
+
process: asyncio.subprocess.Process,
|
|
1018
|
+
) -> tuple[DshProfileCapabilityReady, str, str]:
|
|
1019
|
+
deadline = asyncio.get_running_loop().time() + self._startup_timeout
|
|
1020
|
+
while asyncio.get_running_loop().time() < deadline:
|
|
1021
|
+
if ready_future.done() and token_future.done() and core_token_future.done():
|
|
1022
|
+
return (
|
|
1023
|
+
ready_future.result(),
|
|
1024
|
+
token_future.result(),
|
|
1025
|
+
core_token_future.result(),
|
|
1026
|
+
)
|
|
1027
|
+
if process.returncode is not None:
|
|
1028
|
+
raise PluginHostError(
|
|
1029
|
+
"dsh_capability_host_exited", "DSH capability host exited before readiness"
|
|
1030
|
+
)
|
|
1031
|
+
await asyncio.sleep(0.02)
|
|
1032
|
+
raise PluginHostError(
|
|
1033
|
+
"dsh_capability_start_timeout", "DSH capability host did not become ready in time"
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
def _validate_ready_fences(self, ready: DshProfileCapabilityReady) -> None:
|
|
1037
|
+
if (
|
|
1038
|
+
ready.host_version != DSH_CAPABILITY_HOST_VERSION
|
|
1039
|
+
or ready.dsh_version != self._projection.host_version
|
|
1040
|
+
or ready.profile != self._projection.profile
|
|
1041
|
+
or ready.profile_digest != self._projection.config_digest
|
|
1042
|
+
):
|
|
1043
|
+
raise PluginHostError(
|
|
1044
|
+
"dsh_capability_ready_mismatch",
|
|
1045
|
+
"DSH capability readiness crossed its Profile or version fence",
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
async def _probe_health(
|
|
1049
|
+
self,
|
|
1050
|
+
lease: DshMcpConnectorLease,
|
|
1051
|
+
descriptor: DshProfileCapabilityDescriptor,
|
|
1052
|
+
) -> bool:
|
|
1053
|
+
parsed = urlsplit(lease.endpoint)
|
|
1054
|
+
health_endpoint = f"http://127.0.0.1:{parsed.port}/health"
|
|
1055
|
+
try:
|
|
1056
|
+
async with httpx.AsyncClient(
|
|
1057
|
+
headers=lease.headers(), timeout=self._health_timeout, trust_env=False
|
|
1058
|
+
) as client:
|
|
1059
|
+
response = await client.get(health_endpoint)
|
|
1060
|
+
if len(response.content) > 64 * 1024:
|
|
1061
|
+
return False
|
|
1062
|
+
payload = response.json()
|
|
1063
|
+
except (httpx.HTTPError, ValueError, TypeError):
|
|
1064
|
+
return False
|
|
1065
|
+
return bool(
|
|
1066
|
+
response.status_code == 200
|
|
1067
|
+
and isinstance(payload, dict)
|
|
1068
|
+
and payload.get("protocolVersion") == DSH_CAPABILITY_HOST_PROTOCOL
|
|
1069
|
+
and payload.get("healthy") is True
|
|
1070
|
+
and payload.get("profile") == descriptor.profile
|
|
1071
|
+
and payload.get("profileDigest") == descriptor.profile_digest
|
|
1072
|
+
and payload.get("inventoryDigest") == descriptor.inventory_digest
|
|
1073
|
+
and payload.get("toolCount") == len(descriptor.tools)
|
|
1074
|
+
and payload.get("drifted") is False
|
|
1075
|
+
and payload.get("draining") is False
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
async def _capture_stream(
|
|
1079
|
+
self,
|
|
1080
|
+
stream: asyncio.StreamReader | None,
|
|
1081
|
+
target: deque[str],
|
|
1082
|
+
*,
|
|
1083
|
+
ready_future: asyncio.Future[DshProfileCapabilityReady] | None = None,
|
|
1084
|
+
token_future: asyncio.Future[str] | None = None,
|
|
1085
|
+
core_token_future: asyncio.Future[str] | None = None,
|
|
1086
|
+
) -> None:
|
|
1087
|
+
if stream is None:
|
|
1088
|
+
return
|
|
1089
|
+
ready_prefix = _READY_PREFIX.encode("ascii")
|
|
1090
|
+
token_prefix = _TOKEN_PREFIX.encode("ascii")
|
|
1091
|
+
core_token_prefix = _CORE_TOKEN_PREFIX.encode("ascii")
|
|
1092
|
+
remainder = bytearray()
|
|
1093
|
+
discard_oversized_ready = False
|
|
1094
|
+
while True:
|
|
1095
|
+
chunk = await stream.read(4096)
|
|
1096
|
+
if not chunk:
|
|
1097
|
+
break
|
|
1098
|
+
if discard_oversized_ready:
|
|
1099
|
+
separator = chunk.find(b"\n")
|
|
1100
|
+
if separator < 0:
|
|
1101
|
+
continue
|
|
1102
|
+
chunk = chunk[separator + 1 :]
|
|
1103
|
+
discard_oversized_ready = False
|
|
1104
|
+
remainder.extend(chunk)
|
|
1105
|
+
while True:
|
|
1106
|
+
separator = remainder.find(b"\n")
|
|
1107
|
+
if separator < 0:
|
|
1108
|
+
break
|
|
1109
|
+
line = bytes(remainder[:separator])
|
|
1110
|
+
del remainder[: separator + 1]
|
|
1111
|
+
if line.startswith(ready_prefix):
|
|
1112
|
+
self._capture_ready(line, ready_future)
|
|
1113
|
+
elif line.startswith(token_prefix):
|
|
1114
|
+
self._capture_token(line, token_future)
|
|
1115
|
+
elif line.startswith(core_token_prefix):
|
|
1116
|
+
self._capture_token(
|
|
1117
|
+
line, core_token_future, prefix=_CORE_TOKEN_PREFIX
|
|
1118
|
+
)
|
|
1119
|
+
else:
|
|
1120
|
+
self._append_diagnostic(line, target)
|
|
1121
|
+
if remainder.startswith(ready_prefix):
|
|
1122
|
+
if len(remainder) > len(ready_prefix) + _MAX_READY_BYTES:
|
|
1123
|
+
self._capture_ready(bytes(remainder), ready_future)
|
|
1124
|
+
remainder.clear()
|
|
1125
|
+
discard_oversized_ready = True
|
|
1126
|
+
elif not ready_prefix.startswith(remainder) and len(remainder) > _MAX_DIAGNOSTIC_CHARS:
|
|
1127
|
+
self._append_diagnostic(bytes(remainder), target)
|
|
1128
|
+
remainder.clear()
|
|
1129
|
+
if remainder:
|
|
1130
|
+
line = bytes(remainder)
|
|
1131
|
+
if line.startswith(ready_prefix):
|
|
1132
|
+
self._capture_ready(line, ready_future)
|
|
1133
|
+
elif line.startswith(token_prefix):
|
|
1134
|
+
self._capture_token(line, token_future)
|
|
1135
|
+
elif line.startswith(core_token_prefix):
|
|
1136
|
+
self._capture_token(line, core_token_future, prefix=_CORE_TOKEN_PREFIX)
|
|
1137
|
+
else:
|
|
1138
|
+
self._append_diagnostic(line, target)
|
|
1139
|
+
|
|
1140
|
+
@staticmethod
|
|
1141
|
+
def _capture_ready(
|
|
1142
|
+
line: bytes,
|
|
1143
|
+
future: asyncio.Future[DshProfileCapabilityReady] | None,
|
|
1144
|
+
) -> None:
|
|
1145
|
+
if future is None or future.done():
|
|
1146
|
+
return
|
|
1147
|
+
encoded = line[len(_READY_PREFIX.encode("ascii")) :]
|
|
1148
|
+
try:
|
|
1149
|
+
if not encoded or len(encoded) > _MAX_READY_BYTES:
|
|
1150
|
+
raise ValueError("readiness payload is empty or oversized")
|
|
1151
|
+
payload = json.loads(encoded.decode("utf-8", errors="strict"))
|
|
1152
|
+
ready = DshProfileCapabilityReady.model_validate(payload)
|
|
1153
|
+
except (UnicodeError, json.JSONDecodeError, ValueError):
|
|
1154
|
+
future.set_exception(
|
|
1155
|
+
PluginHostError(
|
|
1156
|
+
"dsh_capability_ready_invalid",
|
|
1157
|
+
"DSH capability readiness record is invalid",
|
|
1158
|
+
)
|
|
1159
|
+
)
|
|
1160
|
+
return
|
|
1161
|
+
future.set_result(ready)
|
|
1162
|
+
|
|
1163
|
+
@staticmethod
|
|
1164
|
+
def _capture_token(
|
|
1165
|
+
line: bytes,
|
|
1166
|
+
future: asyncio.Future[str] | None,
|
|
1167
|
+
*,
|
|
1168
|
+
prefix: str = _TOKEN_PREFIX,
|
|
1169
|
+
) -> None:
|
|
1170
|
+
if future is None or future.done():
|
|
1171
|
+
return
|
|
1172
|
+
try:
|
|
1173
|
+
token = line[len(prefix.encode("ascii")) :].decode("ascii", errors="strict")
|
|
1174
|
+
if re.fullmatch(r"[A-Za-z0-9_-]{32,128}", token) is None:
|
|
1175
|
+
raise ValueError("invalid runtime token")
|
|
1176
|
+
except (UnicodeError, ValueError):
|
|
1177
|
+
future.set_exception(
|
|
1178
|
+
PluginHostError(
|
|
1179
|
+
"dsh_capability_ready_invalid",
|
|
1180
|
+
"DSH capability readiness record is invalid",
|
|
1181
|
+
)
|
|
1182
|
+
)
|
|
1183
|
+
return
|
|
1184
|
+
future.set_result(token)
|
|
1185
|
+
|
|
1186
|
+
async def _monitor_process(self, process: asyncio.subprocess.Process) -> None:
|
|
1187
|
+
await process.wait()
|
|
1188
|
+
async with self._lifecycle_lock:
|
|
1189
|
+
if process is self._process and not self._stopping and not self._disposed:
|
|
1190
|
+
self._state = "failed"
|
|
1191
|
+
self._record_process_failure()
|
|
1192
|
+
# The group leader may exit while Cordis/plugin children keep
|
|
1193
|
+
# running. Reap the process group immediately so a stale PGID
|
|
1194
|
+
# is neither leaked nor reused before the next lifecycle call.
|
|
1195
|
+
await self._terminate()
|
|
1196
|
+
|
|
1197
|
+
async def _terminate(self) -> None:
|
|
1198
|
+
process = self._process
|
|
1199
|
+
self._stopping = True
|
|
1200
|
+
try:
|
|
1201
|
+
if process is not None:
|
|
1202
|
+
await self._terminate_process_tree(process)
|
|
1203
|
+
monitor = self._monitor_task
|
|
1204
|
+
if monitor is not None and monitor is not asyncio.current_task() and not monitor.done():
|
|
1205
|
+
# Lifecycle methods hold _lifecycle_lock. A monitor that has
|
|
1206
|
+
# observed process exit may be waiting for that same lock, so
|
|
1207
|
+
# waiting for it here would deadlock until the shutdown timeout.
|
|
1208
|
+
monitor.cancel()
|
|
1209
|
+
await asyncio.gather(monitor, return_exceptions=True)
|
|
1210
|
+
tasks = [self._stdout_task, self._stderr_task]
|
|
1211
|
+
for task in tasks:
|
|
1212
|
+
if task is None or task is asyncio.current_task():
|
|
1213
|
+
continue
|
|
1214
|
+
try:
|
|
1215
|
+
await asyncio.wait_for(task, timeout=self._shutdown_timeout)
|
|
1216
|
+
except asyncio.TimeoutError:
|
|
1217
|
+
task.cancel()
|
|
1218
|
+
await asyncio.gather(task, return_exceptions=True)
|
|
1219
|
+
finally:
|
|
1220
|
+
self._process = None
|
|
1221
|
+
self._stdout_task = None
|
|
1222
|
+
self._stderr_task = None
|
|
1223
|
+
self._monitor_task = None
|
|
1224
|
+
ready_future = self._ready_future
|
|
1225
|
+
self._ready_future = None
|
|
1226
|
+
if ready_future is not None and not ready_future.done():
|
|
1227
|
+
ready_future.cancel()
|
|
1228
|
+
token_future = self._token_future
|
|
1229
|
+
self._token_future = None
|
|
1230
|
+
if token_future is not None and not token_future.done():
|
|
1231
|
+
token_future.cancel()
|
|
1232
|
+
core_token_future = self._core_token_future
|
|
1233
|
+
self._core_token_future = None
|
|
1234
|
+
if core_token_future is not None and not core_token_future.done():
|
|
1235
|
+
core_token_future.cancel()
|
|
1236
|
+
self._lease = None
|
|
1237
|
+
self._descriptor = None
|
|
1238
|
+
runtime_dir = self._runtime_dir
|
|
1239
|
+
self._runtime_dir = None
|
|
1240
|
+
if runtime_dir is not None and runtime_dir.name.startswith("ksadk-dsh-capability-"):
|
|
1241
|
+
shutil.rmtree(runtime_dir, ignore_errors=True)
|
|
1242
|
+
self._stopping = False
|
|
1243
|
+
|
|
1244
|
+
async def _terminate_process_tree(self, process: asyncio.subprocess.Process) -> None:
|
|
1245
|
+
"""Stop the isolated sidecar session, including plugin child processes."""
|
|
1246
|
+
|
|
1247
|
+
deadline = asyncio.get_running_loop().time() + self._shutdown_timeout
|
|
1248
|
+
if os.name == "posix":
|
|
1249
|
+
self._signal_process_group(process.pid, signal.SIGTERM)
|
|
1250
|
+
elif process.returncode is None:
|
|
1251
|
+
process.terminate()
|
|
1252
|
+
|
|
1253
|
+
if process.returncode is None:
|
|
1254
|
+
try:
|
|
1255
|
+
remaining = max(0.001, deadline - asyncio.get_running_loop().time())
|
|
1256
|
+
await asyncio.wait_for(process.wait(), timeout=remaining)
|
|
1257
|
+
except asyncio.TimeoutError:
|
|
1258
|
+
pass
|
|
1259
|
+
|
|
1260
|
+
if os.name == "posix":
|
|
1261
|
+
while self._process_group_exists(process.pid):
|
|
1262
|
+
remaining = deadline - asyncio.get_running_loop().time()
|
|
1263
|
+
if remaining <= 0:
|
|
1264
|
+
break
|
|
1265
|
+
await asyncio.sleep(min(0.05, remaining))
|
|
1266
|
+
if self._process_group_exists(process.pid):
|
|
1267
|
+
self._signal_process_group(process.pid, signal.SIGKILL)
|
|
1268
|
+
elif process.returncode is None:
|
|
1269
|
+
process.kill()
|
|
1270
|
+
|
|
1271
|
+
if process.returncode is None:
|
|
1272
|
+
await process.wait()
|
|
1273
|
+
|
|
1274
|
+
@staticmethod
|
|
1275
|
+
def _signal_process_group(process_group: int, signum: int) -> None:
|
|
1276
|
+
try:
|
|
1277
|
+
os.killpg(process_group, signum)
|
|
1278
|
+
except ProcessLookupError:
|
|
1279
|
+
return
|
|
1280
|
+
|
|
1281
|
+
@staticmethod
|
|
1282
|
+
def _process_group_exists(process_group: int) -> bool:
|
|
1283
|
+
try:
|
|
1284
|
+
os.killpg(process_group, 0)
|
|
1285
|
+
except ProcessLookupError:
|
|
1286
|
+
return False
|
|
1287
|
+
except PermissionError:
|
|
1288
|
+
return True
|
|
1289
|
+
return True
|
|
1290
|
+
|
|
1291
|
+
def _record_process_failure(self) -> None:
|
|
1292
|
+
if self._process_failure_recorded:
|
|
1293
|
+
return
|
|
1294
|
+
self._process_failure_recorded = True
|
|
1295
|
+
self._circuit.fail()
|
|
1296
|
+
|
|
1297
|
+
def _require_circuit_probe(self) -> None:
|
|
1298
|
+
if self._circuit.acquire():
|
|
1299
|
+
return
|
|
1300
|
+
snapshot = self._circuit.snapshot()
|
|
1301
|
+
raise PluginHostError(
|
|
1302
|
+
"dsh_capability_circuit_open",
|
|
1303
|
+
"DSH capability host circuit is open; wait "
|
|
1304
|
+
f"{snapshot.retry_after_seconds:.1f}s before another probe",
|
|
1305
|
+
)
|
|
1306
|
+
|
|
1307
|
+
@staticmethod
|
|
1308
|
+
def _append_diagnostic(value: bytes, target: deque[str]) -> None:
|
|
1309
|
+
text = value.decode("utf-8", errors="replace").strip()[:_MAX_DIAGNOSTIC_CHARS]
|
|
1310
|
+
if text:
|
|
1311
|
+
target.append(_DIAGNOSTIC_SECRET.sub("[REDACTED]", text))
|
|
1312
|
+
|
|
1313
|
+
@staticmethod
|
|
1314
|
+
def _validate_command(command: Sequence[str]) -> tuple[str, ...]:
|
|
1315
|
+
if isinstance(command, (str, bytes)):
|
|
1316
|
+
raise PluginHostError(
|
|
1317
|
+
"dsh_capability_command_invalid", "DSH command must be an argv sequence"
|
|
1318
|
+
)
|
|
1319
|
+
normalized = tuple(str(value) for value in command)
|
|
1320
|
+
if not normalized or any(not value or "\x00" in value for value in normalized):
|
|
1321
|
+
raise PluginHostError(
|
|
1322
|
+
"dsh_capability_command_invalid", "DSH command contains an invalid argv item"
|
|
1323
|
+
)
|
|
1324
|
+
return normalized
|
|
1325
|
+
|
|
1326
|
+
@staticmethod
|
|
1327
|
+
def _validate_environment(environment: Mapping[str, str]) -> dict[str, str]:
|
|
1328
|
+
normalized: dict[str, str] = {}
|
|
1329
|
+
for key, value in environment.items():
|
|
1330
|
+
if (
|
|
1331
|
+
not _ENV_NAME.fullmatch(key)
|
|
1332
|
+
or key in _BLOCKED_ENV
|
|
1333
|
+
or key.startswith("DYLD_")
|
|
1334
|
+
or _SECRET_ENV.search(key)
|
|
1335
|
+
):
|
|
1336
|
+
raise PluginHostError(
|
|
1337
|
+
"dsh_capability_environment_denied",
|
|
1338
|
+
f"DSH capability environment key {key!r} is not allowed",
|
|
1339
|
+
)
|
|
1340
|
+
text = str(value)
|
|
1341
|
+
if "\x00" in text:
|
|
1342
|
+
raise PluginHostError(
|
|
1343
|
+
"dsh_capability_environment_denied", "DSH capability environment is invalid"
|
|
1344
|
+
)
|
|
1345
|
+
normalized[key] = text
|
|
1346
|
+
return normalized
|
|
1347
|
+
|
|
1348
|
+
@staticmethod
|
|
1349
|
+
def _positive_float(value: float, field_name: str) -> float:
|
|
1350
|
+
normalized = float(value)
|
|
1351
|
+
if normalized <= 0:
|
|
1352
|
+
raise PluginHostError("dsh_capability_limit_invalid", f"{field_name} must be positive")
|
|
1353
|
+
return normalized
|
|
1354
|
+
|
|
1355
|
+
@classmethod
|
|
1356
|
+
def _milliseconds(cls, value: float, field_name: str, maximum: int) -> int:
|
|
1357
|
+
milliseconds = round(cls._positive_float(value, field_name) * 1000)
|
|
1358
|
+
if milliseconds < 1 or milliseconds > maximum:
|
|
1359
|
+
raise PluginHostError(
|
|
1360
|
+
"dsh_capability_limit_invalid", f"{field_name} is outside the supported range"
|
|
1361
|
+
)
|
|
1362
|
+
return milliseconds
|
|
1363
|
+
|
|
1364
|
+
@staticmethod
|
|
1365
|
+
def _positive_int(value: int, field_name: str, maximum: int) -> int:
|
|
1366
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 1 or value > maximum:
|
|
1367
|
+
raise PluginHostError(
|
|
1368
|
+
"dsh_capability_limit_invalid", f"{field_name} is outside the supported range"
|
|
1369
|
+
)
|
|
1370
|
+
return value
|
|
1371
|
+
|
|
1372
|
+
|
|
1373
|
+
__all__ = [
|
|
1374
|
+
"DSH_CAPABILITY_BUNDLE_PACKAGE",
|
|
1375
|
+
"DSH_CAPABILITY_DEFINITION",
|
|
1376
|
+
"DSH_CAPABILITY_HOST_PROTOCOL",
|
|
1377
|
+
"DSH_CAPABILITY_HOST_VERSION",
|
|
1378
|
+
"DSH_CAPABILITY_MCP_PROTOCOL",
|
|
1379
|
+
"DSH_CAPABILITY_MIN_NODE_VERSION",
|
|
1380
|
+
"DSH_CAPABILITY_TRANSPORT",
|
|
1381
|
+
"DshCapabilityBundle",
|
|
1382
|
+
"DshCapabilityTool",
|
|
1383
|
+
"DshMcpConnectorLease",
|
|
1384
|
+
"DshProfileCapabilityDescriptor",
|
|
1385
|
+
"DshProfileCapabilityHost",
|
|
1386
|
+
"DshProfileCapabilityInventory",
|
|
1387
|
+
"DshProfileCapabilityReady",
|
|
1388
|
+
"load_dsh_capability_bundle",
|
|
1389
|
+
]
|