agentengine-sdk-python 0.8.2__py3-none-any.whl → 0.8.3__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.3.dist-info}/METADATA +18 -25
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/RECORD +224 -169
- ksadk/_build_provenance.json +1 -0
- ksadk/agui/_agent_helpers.py +4 -0
- ksadk/api/client.py +3 -0
- ksadk/builders/managed_runtime_builder.py +3 -0
- ksadk/cli/__init__.py +6 -0
- ksadk/cli/cmd_plugin.py +657 -0
- ksadk/codex/runtime.py +50 -8
- ksadk/configs/env_registry.py +31 -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 +19 -1
- ksadk/conversations/projector.py +616 -0
- ksadk/conversations/reducer.py +98 -0
- ksadk/events/_v1_compat/projection.py +2 -0
- ksadk/events/adapters/codex.py +190 -0
- ksadk/harness/__init__.py +45 -35
- ksadk/harness/runtime.py +60 -1
- ksadk/kernel/contract_fingerprints.py +5 -4
- ksadk/kernel/contracts.py +8 -0
- ksadk/kernel/control.py +1 -0
- ksadk/kernel/ingress.py +21 -0
- ksadk/kernel/memory_store.py +1 -0
- ksadk/kernel/postgres_store.py +1 -0
- ksadk/kernel/sqlite_store.py +1 -0
- ksadk/kernel/worker.py +11 -0
- ksadk/memory/providers/local_sqlite.py +7 -0
- ksadk/plugins/__init__.py +167 -0
- ksadk/plugins/bridges/__init__.py +45 -0
- ksadk/plugins/bridges/codex.py +552 -0
- ksadk/plugins/bridges/dsh.py +1116 -0
- ksadk/plugins/builtins.py +747 -0
- ksadk/plugins/bundle.py +246 -0
- ksadk/plugins/bundle_security.py +167 -0
- ksadk/plugins/composition.py +529 -0
- ksadk/plugins/context_contributor.py +437 -0
- ksadk/plugins/contracts.py +451 -0
- ksadk/plugins/dsh_toolchain.py +935 -0
- ksadk/plugins/ecosystem_bridge.py +1105 -0
- ksadk/plugins/ecosystem_probe.py +191 -0
- ksadk/plugins/host.py +724 -0
- ksadk/plugins/providers/__init__.py +85 -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-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 +722 -0
- ksadk/plugins/providers/codex_dsh.py +83 -0
- ksadk/plugins/providers/dsh.py +951 -0
- ksadk/plugins/providers/dsh_descriptor_host.py +226 -0
- ksadk/plugins/providers/harness.py +627 -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/shipped_dsh.py +370 -0
- ksadk/plugins/resolver.py +331 -0
- ksadk/plugins/subagent_providers/__init__.py +12 -0
- ksadk/plugins/subagent_providers/codex.py +443 -0
- ksadk/plugins/subagents.py +333 -0
- ksadk/runtime/_runner_adapter/stream_mapping.py +51 -42
- ksadk/runtime/adapter.py +1 -0
- ksadk/runtime/factory.py +76 -2
- ksadk/runtime/framework_adapters.py +18 -0
- ksadk/runtime/runner_adapter.py +5 -0
- ksadk/sandbox/__init__.py +2 -1
- 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 +563 -0
- ksadk/server/static/assets/{ArtifactsPanel-DSU1sWD_.js → ArtifactsPanel-10GPXIJ6.js} +1 -1
- ksadk/server/static/assets/{CodeBlock-DOH6MVcn.js → CodeBlock-BXk1l-G1.js} +1 -1
- ksadk/server/static/assets/{MathMessageMarkdown-BL8my1R2.js → MathMessageMarkdown-D9uB55Oh.js} +1 -1
- ksadk/server/static/assets/{MermaidBlock-Dz4IP-Tx.js → MermaidBlock--OEYoXIJ.js} +6 -6
- ksadk/server/static/assets/{NativeTerminalPanel-DUrK0JpZ.js → NativeTerminalPanel-DrEe1syq.js} +1 -1
- ksadk/server/static/assets/{abnfDiagram-VCTEODGH-g20pFzNV.js → abnfDiagram-VCTEODGH-eeTFJLvo.js} +1 -1
- ksadk/server/static/assets/{arc-C4FzinUA.js → arc-S3ZNbIAd.js} +1 -1
- ksadk/server/static/assets/architecture-7GRP2DOG-Dk9nRQ6-.js +1 -0
- ksadk/server/static/assets/{architectureDiagram-5GKGNRK7-DpIqL5h4.js → architectureDiagram-5GKGNRK7-CRjgEJDX.js} +1 -1
- ksadk/server/static/assets/{blockDiagram-NRAW4CY4-BjkljTNz.js → blockDiagram-NRAW4CY4-C7bsBoNH.js} +1 -1
- ksadk/server/static/assets/{c4Diagram-UCG6FXSJ-Dk_ieq2X.js → c4Diagram-UCG6FXSJ-BX3c33Ob.js} +1 -1
- ksadk/server/static/assets/channel-s354Yo6o.js +1 -0
- ksadk/server/static/assets/chunk-2Q5K7J3B-CDpPpKR5.js +1 -0
- ksadk/server/static/assets/{chunk-5VM5RSS4-BVHAchFb.js → chunk-5VM5RSS4-bZ7ws-8m.js} +1 -1
- ksadk/server/static/assets/{chunk-F27PBJKO-C-ipQzuS.js → chunk-F27PBJKO-D2r0pvhY.js} +1 -1
- ksadk/server/static/assets/{chunk-G27WJ6UU-C2EyJbIK.js → chunk-G27WJ6UU-CZwF3oUP.js} +1 -1
- ksadk/server/static/assets/{chunk-JWPE2WC7-vYvVJb_M.js → chunk-JWPE2WC7-DigFYCML.js} +1 -1
- ksadk/server/static/assets/{chunk-LCL6LL3I-BcI6ysPT.js → chunk-LCL6LL3I-CnorGV5q.js} +1 -1
- ksadk/server/static/assets/{chunk-POPQ4Y6H-C030x_Z1.js → chunk-POPQ4Y6H-CexntQA-.js} +1 -1
- ksadk/server/static/assets/{chunk-RHFEMEQ7-yyGpsz4n.js → chunk-RHFEMEQ7-CHTC7UxA.js} +1 -1
- ksadk/server/static/assets/{chunk-SVP7TREG-BLTlmMU7.js → chunk-SVP7TREG-BC1EWt_-.js} +1 -1
- ksadk/server/static/assets/{chunk-XXDRQBXY-C_32ArgP.js → chunk-XXDRQBXY-BTG24xN0.js} +1 -1
- ksadk/server/static/assets/classDiagram-DTDB5LWJ-Dd0nucl9.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-Dd0nucl9.js +1 -0
- ksadk/server/static/assets/{cose-bilkent-JH36ORCC-9YQYwdl0.js → cose-bilkent-JH36ORCC-CkonRdAS.js} +1 -1
- ksadk/server/static/assets/cynefin-OW5HDTMX-BBlT4uMf.js +1 -0
- ksadk/server/static/assets/{cynefinDiagram-5FMLGOSQ-ErLF5N13.js → cynefinDiagram-5FMLGOSQ-DEOCBe15.js} +1 -1
- ksadk/server/static/assets/{dagre-3AP2YEHR-DFiQF-6f.js → dagre-3AP2YEHR-DBThPuLa.js} +1 -1
- ksadk/server/static/assets/{dagre-BuhGnRI1.js → dagre-ClTPrTht.js} +1 -1
- ksadk/server/static/assets/{diagram-S7CK7UJ4-Bt1v8-GC.js → diagram-S7CK7UJ4-DsZHrnpJ.js} +1 -1
- ksadk/server/static/assets/{diagram-UQ7AKVKN-DSCxJdBK.js → diagram-UQ7AKVKN-BJR4nhVr.js} +1 -1
- ksadk/server/static/assets/{diagram-VSXAHHWV-DHfYwp_9.js → diagram-VSXAHHWV-xzcERX0a.js} +1 -1
- ksadk/server/static/assets/{diagram-VX7I27RA-DdzD-4Le.js → diagram-VX7I27RA-tn2xM1xu.js} +1 -1
- ksadk/server/static/assets/{diagram-Z3DM3KII-Du-nAJ9F.js → diagram-Z3DM3KII-BZPGBkw5.js} +1 -1
- ksadk/server/static/assets/{dist-ClxGviKw.js → dist-4zN1_tq5.js} +1 -1
- ksadk/server/static/assets/{dist-BE7bu-DL.js → dist-8VWI-2bZ.js} +1 -1
- ksadk/server/static/assets/{dist-BX8z72IC.js → dist-B-46q3Hk.js} +1 -1
- ksadk/server/static/assets/{dist-BjU6y-A2.js → dist-B0gdk4rl.js} +1 -1
- ksadk/server/static/assets/{dist-C_wsv-Qd.js → dist-B1oWRmrH.js} +1 -1
- ksadk/server/static/assets/{dist-ngtN0DJB.js → dist-BI1qQH4n.js} +1 -1
- ksadk/server/static/assets/{dist-dydu68Fl.js → dist-BJLUwH6W.js} +1 -1
- ksadk/server/static/assets/{dist-Dq9gLD7L.js → dist-Bbwqa8TG.js} +1 -1
- ksadk/server/static/assets/{dist-CttYUqzS.js → dist-Btyo4vX4.js} +1 -1
- ksadk/server/static/assets/{dist-CXYYBw3_.js → dist-C71CsujO.js} +1 -1
- ksadk/server/static/assets/{dist-dDdADKFA.js → dist-CXpH_Rdd.js} +1 -1
- ksadk/server/static/assets/{dist-CSh_DE14.js → dist-Cq4OL6nO.js} +1 -1
- ksadk/server/static/assets/{dist-8hqcx0sU.js → dist-D1V3f1B4.js} +1 -1
- ksadk/server/static/assets/{dist-BVDffZ0U.js → dist-DJmCPRVB.js} +1 -1
- ksadk/server/static/assets/{dist-DnfXs8Vn.js → dist-DQAB0qn_.js} +1 -1
- ksadk/server/static/assets/{dist-DNp6rLRA.js → dist-DUwKa_l6.js} +1 -1
- ksadk/server/static/assets/{dist-B7seoj3d.js → dist-DdRBIceA.js} +1 -1
- ksadk/server/static/assets/{dist-BytlxIDT.js → dist-DfGFrb0v.js} +1 -1
- ksadk/server/static/assets/{dist-Bf2M8m3N.js → dist-DjhwGUgZ.js} +1 -1
- ksadk/server/static/assets/{dist-W-TIVntx.js → dist-DyqV4fL4.js} +1 -1
- ksadk/server/static/assets/{dist-DUX-id_F.js → dist-M67Hwu0n.js} +1 -1
- ksadk/server/static/assets/{dist-DmldrHd_.js → dist-u8tVH5Ak.js} +1 -1
- ksadk/server/static/assets/{ebnfDiagram-PWID7BFC-Da9C9NO1.js → ebnfDiagram-PWID7BFC-DVfMsLge.js} +1 -1
- ksadk/server/static/assets/{erDiagram-SSCWMZ5O-EAGqqR79.js → erDiagram-SSCWMZ5O-CWEZSYaw.js} +1 -1
- ksadk/server/static/assets/eventmodeling-NTZA5JFV-Dr5okd8V.js +1 -0
- ksadk/server/static/assets/flowDiagram-A5DVABFB-VEO-cVcO.js +1 -0
- ksadk/server/static/assets/{ganttDiagram-EL5Y4UJY-DqJsKb59.js → ganttDiagram-EL5Y4UJY-TWIxoyYL.js} +1 -1
- ksadk/server/static/assets/gitGraph-4MIJSDKK-CKiKY9Cf.js +1 -0
- ksadk/server/static/assets/{gitGraphDiagram-WWUBYQGX-B6g4dtDi.js → gitGraphDiagram-WWUBYQGX-DDh2hd2S.js} +1 -1
- ksadk/server/static/assets/index-B2k_urY8.js +241 -0
- ksadk/server/static/assets/index-ILPt6IWF.css +2 -0
- ksadk/server/static/assets/info-A6RAGUB7-ubA4Nm_p.js +1 -0
- ksadk/server/static/assets/{infoDiagram-RXCK75RN-Djm_nbd5.js → infoDiagram-RXCK75RN-CgoyWiDR.js} +1 -1
- ksadk/server/static/assets/{ishikawaDiagram-5VMMS53U-DWya9SG2.js → ishikawaDiagram-5VMMS53U-CyRIu_om.js} +1 -1
- ksadk/server/static/assets/{journeyDiagram-EYS64GPL-DutxhSNI.js → journeyDiagram-EYS64GPL-DN5JXHzN.js} +1 -1
- ksadk/server/static/assets/{kanban-definition-3QL26DDD-DJdOF8Fm.js → kanban-definition-3QL26DDD-CNP3ui66.js} +1 -1
- ksadk/server/static/assets/{katex-vFWytM5c.js → katex-DWEzQy1d.js} +1 -1
- ksadk/server/static/assets/{linear-BHjG8b-J.js → linear-C-qHp7d2.js} +1 -1
- ksadk/server/static/assets/{mermaid-parser.core-KGSy4jWT.js → mermaid-parser.core-Cl-K943T.js} +2 -2
- ksadk/server/static/assets/{mindmap-definition-FBJOCRG2-9-HC88D4.js → mindmap-definition-FBJOCRG2-D5DB5mYm.js} +1 -1
- ksadk/server/static/assets/packet-AYTQ26CC-C7R3NUlK.js +1 -0
- ksadk/server/static/assets/{pegDiagram-XKGWAZYB-DrEZd4fD.js → pegDiagram-XKGWAZYB-BQVc9ia_.js} +1 -1
- ksadk/server/static/assets/pie-WAS4IAKB-BIT6qjr3.js +1 -0
- ksadk/server/static/assets/{pieDiagram-E7YTZNPT-DpjqOFFp.js → pieDiagram-E7YTZNPT-B6UCAvtY.js} +1 -1
- ksadk/server/static/assets/{quadrantDiagram-AXDQQJYC-BiNhgOXX.js → quadrantDiagram-AXDQQJYC-mIr2d3eb.js} +1 -1
- ksadk/server/static/assets/radar-RG4KPBEZ-l5noef9X.js +1 -0
- ksadk/server/static/assets/railroad-74A4TZTK-Baea6ZvM.js +1 -0
- ksadk/server/static/assets/railroad-abnf-HS5TGJTU-OKn8zZnU.js +1 -0
- ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-DdKAKC4l.js +1 -0
- ksadk/server/static/assets/railroad-peg-WCYAUIDC-Bds8udlG.js +1 -0
- ksadk/server/static/assets/{railroadDiagram-O6MQD6OU-_Y3Ojg6G.js → railroadDiagram-O6MQD6OU-Dt3FFDmK.js} +1 -1
- ksadk/server/static/assets/{requirementDiagram-EFPCY7ZU-DeCFdg9K.js → requirementDiagram-EFPCY7ZU-Cz_vQtgF.js} +1 -1
- ksadk/server/static/assets/{sankeyDiagram-P5KCCOFB-CPH75rhw.js → sankeyDiagram-P5KCCOFB-wlO1TsH5.js} +1 -1
- ksadk/server/static/assets/{sequenceDiagram-WJ2MYXX4-OEpQQdr4.js → sequenceDiagram-WJ2MYXX4-CHRbElYq.js} +1 -1
- ksadk/server/static/assets/{sizeCapture-X5ZJPWSS-heUErzhI.js → sizeCapture-X5ZJPWSS-DXX54iyO.js} +1 -1
- ksadk/server/static/assets/{stateDiagram-HBIQ2CUA-DvA3jSMB.js → stateDiagram-HBIQ2CUA-Dp_lw9II.js} +1 -1
- ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-5b-8o2iS.js +1 -0
- ksadk/server/static/assets/{swimlanes-XN3QIQJK-CR-dfN_t.js → swimlanes-XN3QIQJK-D8O5UlLG.js} +1 -1
- ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-uf6WPRA4.js +8 -0
- ksadk/server/static/assets/{timeline-definition-24CTP7MA-DAVJf1TX.js → timeline-definition-24CTP7MA-8mfKcxvG.js} +1 -1
- ksadk/server/static/assets/treeView-Q6P3EWNA-CK5CSrSO.js +1 -0
- ksadk/server/static/assets/treemap-WGGIJYW6-Ch2BKgAM.js +1 -0
- ksadk/server/static/assets/{vennDiagram-4TSXK5OY-DwM8eDkh.js → vennDiagram-4TSXK5OY-EnNl0DE7.js} +1 -1
- ksadk/server/static/assets/wardley-WFR3VGLG-YW3Q0ml_.js +1 -0
- ksadk/server/static/assets/{wardleyDiagram-VM6X3IG4-DZL-Zz2t.js → wardleyDiagram-VM6X3IG4-DBu5fbYr.js} +1 -1
- ksadk/server/static/assets/{xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js → xychartDiagram-S5SC5T6Z-BSVXs4IY.js} +1 -1
- ksadk/server/static/index.html +2 -2
- ksadk/skills/service_client.py +48 -0
- ksadk/studio/agent_lifecycle.py +26 -20
- ksadk/studio/api.py +513 -71
- ksadk/studio/api_catalog_routes.py +5 -0
- ksadk/studio/api_contracts.py +34 -1
- ksadk/studio/api_plugin_routes.py +480 -0
- ksadk/studio/attachment_store.py +187 -0
- ksadk/studio/authoring.py +10 -4
- ksadk/studio/authoring_coordinator.py +12 -2
- ksadk/studio/builder.py +123 -43
- ksadk/studio/cloud.py +4 -0
- ksadk/studio/codex_agent_service.py +12 -1
- ksadk/studio/codex_builder.py +54 -15
- ksadk/studio/codex_manifest.py +21 -1
- ksadk/studio/codex_run.py +16 -4
- ksadk/studio/compatibility_report.py +445 -0
- ksadk/studio/compiler.py +6 -1
- ksadk/studio/contracts.py +123 -4
- ksadk/studio/dsh_provider_registration.py +768 -0
- ksadk/studio/event_store.py +54 -6
- ksadk/studio/framework_run.py +37 -4
- ksadk/studio/hosted_kernel.py +1 -1
- ksadk/studio/operations.py +12 -2
- ksadk/studio/plugin_composition.py +301 -0
- ksadk/studio/plugin_kernel_adapter.py +128 -0
- ksadk/studio/plugin_runtime.py +652 -0
- ksadk/studio/resource_catalog.py +6 -1
- ksadk/studio/run_service.py +713 -75
- ksadk/studio/runtime_source.py +9 -1
- ksadk/studio/scheduler_runtime.py +374 -0
- ksadk/studio/scheduler_service.py +211 -0
- ksadk/studio/service.py +672 -82
- ksadk/studio/shared_web.py +167 -28
- ksadk/studio/soul.py +52 -0
- ksadk/studio/static/assets/index-DXs1Eq4M.css +1 -0
- ksadk/studio/static/assets/index-DeDqwTsD.js +283 -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
- 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/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.3.dist-info}/WHEEL +0 -0
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/entry_points.txt +0 -0
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/licenses/LICENSE +0 -0
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
"""DeepSeek Harness Profile -> AgentProvider sidecar vertical.
|
|
2
|
+
|
|
3
|
+
DSH owns its Bundle/Profile ABI and executes all Cordis plugin code. KsADK
|
|
4
|
+
only starts a trusted, fixed host command and consumes the host's typed
|
|
5
|
+
``agent.provider/v1`` descriptor over a bounded JSONL RPC protocol. The
|
|
6
|
+
descriptor is projected into an internal :class:`PluginManifest` solely so the
|
|
7
|
+
existing PluginHost can keep ownership of admission, activation fencing, and
|
|
8
|
+
cleanup; DSH package authors do not need to publish a KsADK manifest.
|
|
9
|
+
|
|
10
|
+
The sidecar is language-neutral: any executable that implements the frozen
|
|
11
|
+
JSONL protocol can host the Cordis composition. Python is not part of the ABI.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import time
|
|
22
|
+
from collections import deque
|
|
23
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Literal
|
|
27
|
+
|
|
28
|
+
from pydantic import ConfigDict, Field, field_validator
|
|
29
|
+
|
|
30
|
+
from ksadk.plugins.bridges.dsh import DshProfileProjection
|
|
31
|
+
from ksadk.plugins.bundle import ResolvedPluginBundle
|
|
32
|
+
from ksadk.plugins.contracts import PluginContractModel, PluginManifest
|
|
33
|
+
from ksadk.plugins.host import PluginExecutionContext, PluginHostError
|
|
34
|
+
|
|
35
|
+
DSH_AGENT_PROVIDER_HOST_PROTOCOL = "ksadk.dsh-agent-provider-host/v1"
|
|
36
|
+
DSH_AGENT_PROVIDER_HOST_METHODS = frozenset(
|
|
37
|
+
{
|
|
38
|
+
"handshake",
|
|
39
|
+
"describe",
|
|
40
|
+
"preflight",
|
|
41
|
+
"activate",
|
|
42
|
+
"inventory",
|
|
43
|
+
"execute",
|
|
44
|
+
"cancel",
|
|
45
|
+
"health",
|
|
46
|
+
"drain",
|
|
47
|
+
"dispose",
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
DSH_HOST_USER_PERMISSION = "process:host-user"
|
|
51
|
+
|
|
52
|
+
_MAX_LINE_BYTES = 1024 * 1024
|
|
53
|
+
_ID = re.compile(r"^[a-z0-9]+(?:[._-][a-z0-9]+)*$")
|
|
54
|
+
_SEMVER = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$")
|
|
55
|
+
_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
56
|
+
_PACKAGE = re.compile(r"^(?:@[A-Za-z0-9._-]+/)?[A-Za-z0-9._-]+$")
|
|
57
|
+
_ENV_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
58
|
+
_SECRET_ENV = re.compile(
|
|
59
|
+
r"(?:^|_)(?:API_?KEY|ACCESS_?KEY|AUTH|BEARER|CREDENTIAL|PASSWORD|"
|
|
60
|
+
r"PRIVATE_?KEY|SECRET|TOKEN)(?:_|$)",
|
|
61
|
+
re.IGNORECASE,
|
|
62
|
+
)
|
|
63
|
+
_DIAGNOSTIC_SECRET = re.compile(
|
|
64
|
+
r"(?ix)(?:\bsk-[a-z0-9_-]{12,}\b|\bBearer\s+[a-z0-9._~+/=-]{8,}|"
|
|
65
|
+
r"\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|password|secret)"
|
|
66
|
+
r"\s*(?:=|:)\s*[^\s,;]+)"
|
|
67
|
+
)
|
|
68
|
+
_SAFE_INHERITED_ENV = frozenset(
|
|
69
|
+
{"LANG", "LC_ALL", "PATH", "SYSTEMROOT", "TEMP", "TMP", "TMPDIR", "TZ", "WINDIR"}
|
|
70
|
+
)
|
|
71
|
+
_BLOCKED_ENV = frozenset({"PYTHONHOME", "PYTHONPATH", "LD_PRELOAD"})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class DshCircuitSnapshot:
|
|
76
|
+
"""Observable host health state; it contains no command or credential data."""
|
|
77
|
+
|
|
78
|
+
state: Literal["closed", "open", "half-open"]
|
|
79
|
+
consecutive_failures: int
|
|
80
|
+
retry_after_seconds: float
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DshCircuitBreaker:
|
|
84
|
+
"""Single-probe circuit breaker for DSH host transport and protocol faults.
|
|
85
|
+
|
|
86
|
+
It deliberately does not retry RPC calls. An ``execute`` frame can create
|
|
87
|
+
external side effects, so replay remains the responsibility of the
|
|
88
|
+
caller's idempotent run/session protocol.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
*,
|
|
94
|
+
failure_threshold: int = 3,
|
|
95
|
+
recovery_timeout: float = 5.0,
|
|
96
|
+
clock: Callable[[], float] = time.monotonic,
|
|
97
|
+
) -> None:
|
|
98
|
+
if failure_threshold < 1:
|
|
99
|
+
raise ValueError("failure_threshold must be positive")
|
|
100
|
+
if recovery_timeout < 0:
|
|
101
|
+
raise ValueError("recovery_timeout cannot be negative")
|
|
102
|
+
self._failure_threshold = failure_threshold
|
|
103
|
+
self._recovery_timeout = recovery_timeout
|
|
104
|
+
self._clock = clock
|
|
105
|
+
self._failures = 0
|
|
106
|
+
self._opened_at: float | None = None
|
|
107
|
+
self._half_open_claimed = False
|
|
108
|
+
|
|
109
|
+
def acquire(self) -> bool:
|
|
110
|
+
if self._opened_at is None:
|
|
111
|
+
return True
|
|
112
|
+
if self._clock() - self._opened_at < self._recovery_timeout:
|
|
113
|
+
return False
|
|
114
|
+
if self._half_open_claimed:
|
|
115
|
+
return False
|
|
116
|
+
self._half_open_claimed = True
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
def succeed(self) -> None:
|
|
120
|
+
self._failures = 0
|
|
121
|
+
self._opened_at = None
|
|
122
|
+
self._half_open_claimed = False
|
|
123
|
+
|
|
124
|
+
def fail(self) -> None:
|
|
125
|
+
self._failures += 1
|
|
126
|
+
self._half_open_claimed = False
|
|
127
|
+
if self._failures >= self._failure_threshold:
|
|
128
|
+
self._opened_at = self._clock()
|
|
129
|
+
|
|
130
|
+
def snapshot(self) -> DshCircuitSnapshot:
|
|
131
|
+
if self._opened_at is None:
|
|
132
|
+
return DshCircuitSnapshot("closed", self._failures, 0.0)
|
|
133
|
+
retry_after = max(0.0, self._recovery_timeout - (self._clock() - self._opened_at))
|
|
134
|
+
return DshCircuitSnapshot(
|
|
135
|
+
"half-open" if retry_after == 0 else "open",
|
|
136
|
+
self._failures,
|
|
137
|
+
retry_after,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class _DshProviderModel(PluginContractModel):
|
|
142
|
+
model_config = ConfigDict(
|
|
143
|
+
alias_generator=lambda value: (
|
|
144
|
+
value.split("_")[0] + "".join(part.capitalize() for part in value.split("_")[1:])
|
|
145
|
+
),
|
|
146
|
+
populate_by_name=True,
|
|
147
|
+
extra="forbid",
|
|
148
|
+
frozen=True,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class DshAgentProviderDescriptor(_DshProviderModel):
|
|
153
|
+
"""One AgentProvider contribution exposed by the composed DSH host."""
|
|
154
|
+
|
|
155
|
+
descriptor_format: Literal["dsh.agent-provider-descriptor/v1"] = (
|
|
156
|
+
"dsh.agent-provider-descriptor/v1"
|
|
157
|
+
)
|
|
158
|
+
ecosystem: Literal["dsh"] = "dsh"
|
|
159
|
+
provider_id: str = Field(min_length=3, max_length=128)
|
|
160
|
+
provider_version: str
|
|
161
|
+
display_name: str = Field(min_length=1, max_length=256)
|
|
162
|
+
plugin_name: str = Field(min_length=1, max_length=256)
|
|
163
|
+
profile: str = Field(min_length=1, max_length=64)
|
|
164
|
+
profile_digest: str
|
|
165
|
+
definition: Literal["agent.provider/v1"] = "agent.provider/v1"
|
|
166
|
+
slot: Literal["agent.execution"] = "agent.execution"
|
|
167
|
+
runtime_protocols: tuple[str, ...] = ()
|
|
168
|
+
|
|
169
|
+
@field_validator("provider_id")
|
|
170
|
+
@classmethod
|
|
171
|
+
def validate_provider_id(cls, value: str) -> str:
|
|
172
|
+
if not _ID.fullmatch(value):
|
|
173
|
+
raise ValueError("providerId must be a lowercase qualified id")
|
|
174
|
+
return value
|
|
175
|
+
|
|
176
|
+
@field_validator("provider_version")
|
|
177
|
+
@classmethod
|
|
178
|
+
def validate_provider_version(cls, value: str) -> str:
|
|
179
|
+
if not _SEMVER.fullmatch(value):
|
|
180
|
+
raise ValueError("providerVersion must use exact semantic versioning")
|
|
181
|
+
return value
|
|
182
|
+
|
|
183
|
+
@field_validator("plugin_name")
|
|
184
|
+
@classmethod
|
|
185
|
+
def validate_plugin_name(cls, value: str) -> str:
|
|
186
|
+
if not _PACKAGE.fullmatch(value):
|
|
187
|
+
raise ValueError("pluginName must be a valid DSH package name")
|
|
188
|
+
return value
|
|
189
|
+
|
|
190
|
+
@field_validator("profile_digest")
|
|
191
|
+
@classmethod
|
|
192
|
+
def validate_profile_digest(cls, value: str) -> str:
|
|
193
|
+
if not _DIGEST.fullmatch(value):
|
|
194
|
+
raise ValueError("profileDigest must be a lowercase sha256 digest")
|
|
195
|
+
return value
|
|
196
|
+
|
|
197
|
+
@field_validator("runtime_protocols")
|
|
198
|
+
@classmethod
|
|
199
|
+
def validate_runtime_protocols(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
200
|
+
if any(not item or len(item) > 128 for item in value):
|
|
201
|
+
raise ValueError("runtimeProtocols entries must be non-empty and bounded")
|
|
202
|
+
if len(value) != len(set(value)):
|
|
203
|
+
raise ValueError("runtimeProtocols entries must be unique")
|
|
204
|
+
return value
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def descriptor_digest(self) -> str:
|
|
208
|
+
payload = json.dumps(
|
|
209
|
+
self.model_dump(by_alias=True, exclude_none=True, mode="json"),
|
|
210
|
+
ensure_ascii=False,
|
|
211
|
+
sort_keys=True,
|
|
212
|
+
separators=(",", ":"),
|
|
213
|
+
).encode("utf-8")
|
|
214
|
+
return f"sha256:{hashlib.sha256(payload).hexdigest()}"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class DshAgentProviderPreflight(_DshProviderModel):
|
|
218
|
+
ready: bool
|
|
219
|
+
descriptor_digest: str
|
|
220
|
+
profile_digest: str
|
|
221
|
+
|
|
222
|
+
@field_validator("descriptor_digest", "profile_digest")
|
|
223
|
+
@classmethod
|
|
224
|
+
def validate_digest(cls, value: str) -> str:
|
|
225
|
+
if not _DIGEST.fullmatch(value):
|
|
226
|
+
raise ValueError("preflight digests must be lowercase sha256 values")
|
|
227
|
+
return value
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class DshAgentProviderInventory(_DshProviderModel):
|
|
231
|
+
provider_id: str = Field(min_length=3, max_length=128)
|
|
232
|
+
provider_version: str
|
|
233
|
+
profile: str = Field(min_length=1, max_length=64)
|
|
234
|
+
profile_digest: str
|
|
235
|
+
descriptor_digest: str
|
|
236
|
+
state: Literal["ready", "draining", "disposed", "failed"]
|
|
237
|
+
activation_count: int = Field(ge=0)
|
|
238
|
+
|
|
239
|
+
@field_validator("profile_digest", "descriptor_digest")
|
|
240
|
+
@classmethod
|
|
241
|
+
def validate_digest(cls, value: str) -> str:
|
|
242
|
+
if not _DIGEST.fullmatch(value):
|
|
243
|
+
raise ValueError("inventory digests must be lowercase sha256 values")
|
|
244
|
+
return value
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@dataclass(frozen=True)
|
|
248
|
+
class DshAgentProviderRegistration:
|
|
249
|
+
"""Selector-safe registration emitted only after successful preflight."""
|
|
250
|
+
|
|
251
|
+
descriptor: DshAgentProviderDescriptor
|
|
252
|
+
preflight: DshAgentProviderPreflight
|
|
253
|
+
manifest: PluginManifest
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class DshAgentProviderHost:
|
|
257
|
+
"""Supervise one fixed DSH provider host for an immutable Profile projection."""
|
|
258
|
+
|
|
259
|
+
def __init__(
|
|
260
|
+
self,
|
|
261
|
+
command: Sequence[str],
|
|
262
|
+
*,
|
|
263
|
+
projection: DshProfileProjection,
|
|
264
|
+
cwd: str | Path | None = None,
|
|
265
|
+
environment: Mapping[str, str] | None = None,
|
|
266
|
+
startup_timeout: float = 5.0,
|
|
267
|
+
request_timeout: float = 30.0,
|
|
268
|
+
shutdown_timeout: float = 2.0,
|
|
269
|
+
circuit_failure_threshold: int = 3,
|
|
270
|
+
circuit_recovery_timeout: float = 5.0,
|
|
271
|
+
) -> None:
|
|
272
|
+
self._command = _validate_command(command)
|
|
273
|
+
self._projection = projection
|
|
274
|
+
self._cwd = Path(cwd).resolve() if cwd is not None else None
|
|
275
|
+
self._environment = _minimal_environment(environment or {})
|
|
276
|
+
self._startup_timeout = _positive_timeout(startup_timeout, "startup_timeout")
|
|
277
|
+
self._request_timeout = _positive_timeout(request_timeout, "request_timeout")
|
|
278
|
+
self._shutdown_timeout = _positive_timeout(shutdown_timeout, "shutdown_timeout")
|
|
279
|
+
self._circuit = DshCircuitBreaker(
|
|
280
|
+
failure_threshold=circuit_failure_threshold,
|
|
281
|
+
recovery_timeout=circuit_recovery_timeout,
|
|
282
|
+
)
|
|
283
|
+
self._process: asyncio.subprocess.Process | None = None
|
|
284
|
+
self._request_lock = asyncio.Lock()
|
|
285
|
+
self._lifecycle_lock = asyncio.Lock()
|
|
286
|
+
self._stderr_task: asyncio.Task[None] | None = None
|
|
287
|
+
self._stderr_tail: deque[str] = deque(maxlen=64)
|
|
288
|
+
self._next_request_id = 0
|
|
289
|
+
self._descriptor: DshAgentProviderDescriptor | None = None
|
|
290
|
+
self._preflight: DshAgentProviderPreflight | None = None
|
|
291
|
+
self._disposed = False
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def pid(self) -> int | None:
|
|
295
|
+
process = self._process
|
|
296
|
+
return process.pid if process is not None and process.returncode is None else None
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def stderr_tail(self) -> tuple[str, ...]:
|
|
300
|
+
return tuple(self._stderr_tail)
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def circuit_snapshot(self) -> DshCircuitSnapshot:
|
|
304
|
+
return self._circuit.snapshot()
|
|
305
|
+
|
|
306
|
+
async def start(self) -> None:
|
|
307
|
+
async with self._lifecycle_lock:
|
|
308
|
+
if self.pid is not None:
|
|
309
|
+
return
|
|
310
|
+
if self._disposed:
|
|
311
|
+
raise PluginHostError("dsh_provider_host_disposed", "DSH provider host is disposed")
|
|
312
|
+
self._require_circuit_probe()
|
|
313
|
+
try:
|
|
314
|
+
process = await asyncio.create_subprocess_exec(
|
|
315
|
+
*self._command,
|
|
316
|
+
cwd=str(self._cwd) if self._cwd is not None else None,
|
|
317
|
+
env=self._environment,
|
|
318
|
+
stdin=asyncio.subprocess.PIPE,
|
|
319
|
+
stdout=asyncio.subprocess.PIPE,
|
|
320
|
+
stderr=asyncio.subprocess.PIPE,
|
|
321
|
+
limit=_MAX_LINE_BYTES + 1,
|
|
322
|
+
start_new_session=os.name == "posix",
|
|
323
|
+
)
|
|
324
|
+
except (OSError, ValueError) as error:
|
|
325
|
+
self._circuit.fail()
|
|
326
|
+
raise PluginHostError(
|
|
327
|
+
"dsh_provider_host_start_failed",
|
|
328
|
+
f"cannot start fixed DSH provider host: {type(error).__name__}",
|
|
329
|
+
) from error
|
|
330
|
+
self._process = process
|
|
331
|
+
self._stderr_task = asyncio.create_task(self._capture_stderr(process))
|
|
332
|
+
try:
|
|
333
|
+
handshake = await self._request(
|
|
334
|
+
"handshake",
|
|
335
|
+
{"profile": self._projection_payload()},
|
|
336
|
+
timeout=self._startup_timeout,
|
|
337
|
+
circuit_probe_acquired=True,
|
|
338
|
+
)
|
|
339
|
+
try:
|
|
340
|
+
self._validate_handshake(handshake)
|
|
341
|
+
except PluginHostError:
|
|
342
|
+
self._circuit.fail()
|
|
343
|
+
raise
|
|
344
|
+
except BaseException:
|
|
345
|
+
await self._terminate()
|
|
346
|
+
raise
|
|
347
|
+
|
|
348
|
+
async def describe(self) -> DshAgentProviderDescriptor:
|
|
349
|
+
await self.start()
|
|
350
|
+
if self._descriptor is not None:
|
|
351
|
+
return self._descriptor
|
|
352
|
+
payload = await self._request("describe", {"profile": self._projection_payload()})
|
|
353
|
+
try:
|
|
354
|
+
descriptor = DshAgentProviderDescriptor.model_validate(payload)
|
|
355
|
+
except Exception as error: # noqa: BLE001 - external protocol boundary
|
|
356
|
+
raise PluginHostError(
|
|
357
|
+
"dsh_provider_descriptor_invalid",
|
|
358
|
+
"DSH host returned an invalid provider descriptor",
|
|
359
|
+
) from error
|
|
360
|
+
if descriptor.profile != self._projection.profile:
|
|
361
|
+
raise PluginHostError(
|
|
362
|
+
"dsh_provider_descriptor_mismatch", "DSH provider descriptor profile does not match"
|
|
363
|
+
)
|
|
364
|
+
if descriptor.profile_digest != self._projection.config_digest:
|
|
365
|
+
raise PluginHostError(
|
|
366
|
+
"dsh_provider_descriptor_mismatch",
|
|
367
|
+
"DSH provider descriptor is not fenced to the selected Profile digest",
|
|
368
|
+
)
|
|
369
|
+
if descriptor.plugin_name not in self._projection.bundles:
|
|
370
|
+
raise PluginHostError(
|
|
371
|
+
"dsh_provider_descriptor_mismatch",
|
|
372
|
+
"DSH provider descriptor does not belong to an active Profile bundle",
|
|
373
|
+
)
|
|
374
|
+
self._descriptor = descriptor
|
|
375
|
+
return descriptor
|
|
376
|
+
|
|
377
|
+
async def preflight(self) -> DshAgentProviderPreflight:
|
|
378
|
+
descriptor = await self.describe()
|
|
379
|
+
payload = await self._request(
|
|
380
|
+
"preflight",
|
|
381
|
+
{
|
|
382
|
+
"profileDigest": self._projection.config_digest,
|
|
383
|
+
"descriptorDigest": descriptor.descriptor_digest,
|
|
384
|
+
},
|
|
385
|
+
)
|
|
386
|
+
try:
|
|
387
|
+
result = DshAgentProviderPreflight.model_validate(payload)
|
|
388
|
+
except Exception as error: # noqa: BLE001 - external protocol boundary
|
|
389
|
+
raise PluginHostError(
|
|
390
|
+
"dsh_provider_preflight_invalid", "DSH host returned an invalid preflight result"
|
|
391
|
+
) from error
|
|
392
|
+
if (
|
|
393
|
+
result.descriptor_digest != descriptor.descriptor_digest
|
|
394
|
+
or result.profile_digest != self._projection.config_digest
|
|
395
|
+
):
|
|
396
|
+
raise PluginHostError(
|
|
397
|
+
"dsh_provider_preflight_mismatch", "DSH preflight result crossed a Profile fence"
|
|
398
|
+
)
|
|
399
|
+
if not result.ready:
|
|
400
|
+
raise PluginHostError(
|
|
401
|
+
"dsh_provider_preflight_failed", "DSH AgentProvider is not ready for activation"
|
|
402
|
+
)
|
|
403
|
+
self._preflight = result
|
|
404
|
+
return result
|
|
405
|
+
|
|
406
|
+
async def registration(self) -> DshAgentProviderRegistration:
|
|
407
|
+
"""Return a selector-visible projection only after the provider is ready."""
|
|
408
|
+
|
|
409
|
+
descriptor = await self.describe()
|
|
410
|
+
preflight = await self.preflight()
|
|
411
|
+
return DshAgentProviderRegistration(
|
|
412
|
+
descriptor=descriptor,
|
|
413
|
+
preflight=preflight,
|
|
414
|
+
manifest=dsh_agent_provider_manifest(descriptor, preflight=preflight),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
async def activate(
|
|
418
|
+
self,
|
|
419
|
+
bundle: ResolvedPluginBundle,
|
|
420
|
+
capabilities: PluginExecutionContext,
|
|
421
|
+
) -> str:
|
|
422
|
+
descriptor = await self.describe()
|
|
423
|
+
await self.preflight()
|
|
424
|
+
payload = await self._request(
|
|
425
|
+
"activate",
|
|
426
|
+
{
|
|
427
|
+
"descriptorDigest": descriptor.descriptor_digest,
|
|
428
|
+
"bundle": _bundle_payload(bundle),
|
|
429
|
+
"capabilities": _capability_payload(capabilities),
|
|
430
|
+
},
|
|
431
|
+
)
|
|
432
|
+
if not isinstance(payload, dict):
|
|
433
|
+
raise PluginHostError(
|
|
434
|
+
"dsh_provider_activation_invalid", "DSH host activation result must be an object"
|
|
435
|
+
)
|
|
436
|
+
activation_id = str(payload.get("activationId") or "").strip()
|
|
437
|
+
if not activation_id or len(activation_id) > 256:
|
|
438
|
+
raise PluginHostError(
|
|
439
|
+
"dsh_provider_activation_invalid", "DSH host returned no valid activationId"
|
|
440
|
+
)
|
|
441
|
+
return activation_id
|
|
442
|
+
|
|
443
|
+
async def inventory(self) -> DshAgentProviderInventory:
|
|
444
|
+
descriptor = await self.describe()
|
|
445
|
+
payload = await self._request(
|
|
446
|
+
"inventory", {"descriptorDigest": descriptor.descriptor_digest}
|
|
447
|
+
)
|
|
448
|
+
try:
|
|
449
|
+
result = DshAgentProviderInventory.model_validate(payload)
|
|
450
|
+
except Exception as error: # noqa: BLE001 - external protocol boundary
|
|
451
|
+
raise PluginHostError(
|
|
452
|
+
"dsh_provider_inventory_invalid", "DSH host returned invalid provider inventory"
|
|
453
|
+
) from error
|
|
454
|
+
if (
|
|
455
|
+
result.provider_id != descriptor.provider_id
|
|
456
|
+
or result.provider_version != descriptor.provider_version
|
|
457
|
+
or result.profile != descriptor.profile
|
|
458
|
+
or result.profile_digest != descriptor.profile_digest
|
|
459
|
+
or result.descriptor_digest != descriptor.descriptor_digest
|
|
460
|
+
):
|
|
461
|
+
raise PluginHostError(
|
|
462
|
+
"dsh_provider_inventory_mismatch",
|
|
463
|
+
"DSH provider inventory crossed a descriptor fence",
|
|
464
|
+
)
|
|
465
|
+
return result
|
|
466
|
+
|
|
467
|
+
async def health(self, *, activation_id: str | None = None) -> bool:
|
|
468
|
+
params: dict[str, Any] = {"scope": "activation" if activation_id else "provider"}
|
|
469
|
+
if activation_id:
|
|
470
|
+
params["activationId"] = activation_id
|
|
471
|
+
payload = await self._request("health", params)
|
|
472
|
+
if not isinstance(payload, dict) or not isinstance(payload.get("healthy"), bool):
|
|
473
|
+
raise PluginHostError(
|
|
474
|
+
"dsh_provider_health_invalid", "DSH provider health must contain boolean healthy"
|
|
475
|
+
)
|
|
476
|
+
return bool(payload["healthy"])
|
|
477
|
+
|
|
478
|
+
async def execute(self, activation_id: str, request: Any) -> Any:
|
|
479
|
+
return await self._request(
|
|
480
|
+
"execute", {"activationId": activation_id, "request": _json_value(request)}
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
async def cancel_activation(self, activation_id: str) -> None:
|
|
484
|
+
await self._request("cancel", {"activationId": activation_id})
|
|
485
|
+
|
|
486
|
+
async def drain(self, *, activation_id: str | None = None) -> None:
|
|
487
|
+
params: dict[str, Any] = {"scope": "activation" if activation_id else "provider"}
|
|
488
|
+
if activation_id:
|
|
489
|
+
params["activationId"] = activation_id
|
|
490
|
+
await self._request("drain", params)
|
|
491
|
+
|
|
492
|
+
async def dispose_activation(self, activation_id: str) -> None:
|
|
493
|
+
await self._request("dispose", {"scope": "activation", "activationId": activation_id})
|
|
494
|
+
|
|
495
|
+
async def dispose(self) -> None:
|
|
496
|
+
async with self._lifecycle_lock:
|
|
497
|
+
if self._disposed:
|
|
498
|
+
return
|
|
499
|
+
self._disposed = True
|
|
500
|
+
failure: Exception | None = None
|
|
501
|
+
if self.pid is not None:
|
|
502
|
+
try:
|
|
503
|
+
await self._request("dispose", {"scope": "host"})
|
|
504
|
+
except Exception as error: # cleanup must still terminate the sidecar
|
|
505
|
+
failure = error
|
|
506
|
+
await self._terminate()
|
|
507
|
+
if failure is not None:
|
|
508
|
+
raise failure
|
|
509
|
+
|
|
510
|
+
async def _request(
|
|
511
|
+
self,
|
|
512
|
+
method: str,
|
|
513
|
+
params: Mapping[str, Any],
|
|
514
|
+
*,
|
|
515
|
+
timeout: float | None = None,
|
|
516
|
+
circuit_probe_acquired: bool = False,
|
|
517
|
+
) -> Any:
|
|
518
|
+
if method not in DSH_AGENT_PROVIDER_HOST_METHODS:
|
|
519
|
+
raise PluginHostError(
|
|
520
|
+
"dsh_provider_method_denied", f"DSH provider host method {method!r} is denied"
|
|
521
|
+
)
|
|
522
|
+
if not circuit_probe_acquired:
|
|
523
|
+
self._require_circuit_probe()
|
|
524
|
+
self._next_request_id += 1
|
|
525
|
+
request_id = f"dsh-{self._next_request_id}"
|
|
526
|
+
try:
|
|
527
|
+
encoded = json.dumps(
|
|
528
|
+
{"id": request_id, "method": method, "params": dict(params)},
|
|
529
|
+
ensure_ascii=False,
|
|
530
|
+
allow_nan=False,
|
|
531
|
+
separators=(",", ":"),
|
|
532
|
+
).encode("utf-8")
|
|
533
|
+
except (TypeError, ValueError) as error:
|
|
534
|
+
raise PluginHostError(
|
|
535
|
+
"dsh_provider_request_invalid", "DSH provider request is not strict JSON"
|
|
536
|
+
) from error
|
|
537
|
+
if len(encoded) > _MAX_LINE_BYTES:
|
|
538
|
+
raise PluginHostError(
|
|
539
|
+
"dsh_provider_request_too_large", "DSH provider request exceeds the protocol limit"
|
|
540
|
+
)
|
|
541
|
+
async with self._request_lock:
|
|
542
|
+
process = self._process
|
|
543
|
+
if (
|
|
544
|
+
process is None
|
|
545
|
+
or process.returncode is not None
|
|
546
|
+
or process.stdin is None
|
|
547
|
+
or process.stdout is None
|
|
548
|
+
):
|
|
549
|
+
self._circuit.fail()
|
|
550
|
+
raise PluginHostError(
|
|
551
|
+
"dsh_provider_host_unavailable", "DSH provider host is not running"
|
|
552
|
+
)
|
|
553
|
+
try:
|
|
554
|
+
process.stdin.write(encoded + b"\n")
|
|
555
|
+
await process.stdin.drain()
|
|
556
|
+
line = await asyncio.wait_for(
|
|
557
|
+
process.stdout.readline(),
|
|
558
|
+
timeout=timeout if timeout is not None else self._request_timeout,
|
|
559
|
+
)
|
|
560
|
+
except asyncio.TimeoutError as error:
|
|
561
|
+
await self._terminate()
|
|
562
|
+
self._circuit.fail()
|
|
563
|
+
raise PluginHostError(
|
|
564
|
+
"dsh_provider_request_timeout", f"DSH provider host timed out during {method}"
|
|
565
|
+
) from error
|
|
566
|
+
except (BrokenPipeError, ConnectionError, OSError, ValueError) as error:
|
|
567
|
+
await self._terminate()
|
|
568
|
+
self._circuit.fail()
|
|
569
|
+
raise PluginHostError(
|
|
570
|
+
"dsh_provider_transport_failed", f"DSH provider host failed during {method}"
|
|
571
|
+
) from error
|
|
572
|
+
if not line or len(line) > _MAX_LINE_BYTES:
|
|
573
|
+
await self._terminate()
|
|
574
|
+
self._circuit.fail()
|
|
575
|
+
raise PluginHostError(
|
|
576
|
+
"dsh_provider_protocol_invalid", "DSH provider host emitted no bounded response"
|
|
577
|
+
)
|
|
578
|
+
try:
|
|
579
|
+
response = json.loads(line)
|
|
580
|
+
except (UnicodeDecodeError, ValueError) as error:
|
|
581
|
+
await self._terminate()
|
|
582
|
+
self._circuit.fail()
|
|
583
|
+
raise PluginHostError(
|
|
584
|
+
"dsh_provider_protocol_invalid", "DSH provider host emitted invalid JSONL"
|
|
585
|
+
) from error
|
|
586
|
+
if not isinstance(response, dict) or response.get("id") != request_id:
|
|
587
|
+
await self._terminate()
|
|
588
|
+
self._circuit.fail()
|
|
589
|
+
raise PluginHostError(
|
|
590
|
+
"dsh_provider_response_mismatch", "DSH provider response id does not match"
|
|
591
|
+
)
|
|
592
|
+
if response.get("error") is not None:
|
|
593
|
+
raise PluginHostError(
|
|
594
|
+
"dsh_provider_remote_error", "DSH provider host rejected the request"
|
|
595
|
+
)
|
|
596
|
+
if "result" not in response:
|
|
597
|
+
self._circuit.fail()
|
|
598
|
+
raise PluginHostError(
|
|
599
|
+
"dsh_provider_protocol_invalid", "DSH provider response has no result"
|
|
600
|
+
)
|
|
601
|
+
self._circuit.succeed()
|
|
602
|
+
return response["result"]
|
|
603
|
+
|
|
604
|
+
def _require_circuit_probe(self) -> None:
|
|
605
|
+
if self._circuit.acquire():
|
|
606
|
+
return
|
|
607
|
+
snapshot = self._circuit.snapshot()
|
|
608
|
+
raise PluginHostError(
|
|
609
|
+
"dsh_provider_circuit_open",
|
|
610
|
+
"DSH provider host circuit is open; wait "
|
|
611
|
+
f"{snapshot.retry_after_seconds:.1f}s before another probe",
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
def _validate_handshake(self, payload: Any) -> None:
|
|
615
|
+
if not isinstance(payload, dict):
|
|
616
|
+
raise PluginHostError(
|
|
617
|
+
"dsh_provider_handshake_invalid", "DSH provider handshake must be an object"
|
|
618
|
+
)
|
|
619
|
+
methods = payload.get("methods")
|
|
620
|
+
host_version = payload.get("hostVersion")
|
|
621
|
+
if (
|
|
622
|
+
payload.get("protocolVersion") != DSH_AGENT_PROVIDER_HOST_PROTOCOL
|
|
623
|
+
or not isinstance(methods, list)
|
|
624
|
+
or set(methods) != DSH_AGENT_PROVIDER_HOST_METHODS
|
|
625
|
+
or len(methods) != len(DSH_AGENT_PROVIDER_HOST_METHODS)
|
|
626
|
+
or not isinstance(host_version, str)
|
|
627
|
+
or not _SEMVER.fullmatch(host_version)
|
|
628
|
+
):
|
|
629
|
+
raise PluginHostError(
|
|
630
|
+
"dsh_provider_handshake_invalid",
|
|
631
|
+
"DSH provider host protocol, methods, or version is incompatible",
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
def _projection_payload(self) -> dict[str, Any]:
|
|
635
|
+
return self._projection.model_dump(by_alias=True, mode="json")
|
|
636
|
+
|
|
637
|
+
async def _capture_stderr(self, process: asyncio.subprocess.Process) -> None:
|
|
638
|
+
if process.stderr is None:
|
|
639
|
+
return
|
|
640
|
+
while True:
|
|
641
|
+
line = await process.stderr.readline()
|
|
642
|
+
if not line:
|
|
643
|
+
return
|
|
644
|
+
diagnostic = line.decode("utf-8", errors="replace").rstrip()[:4096]
|
|
645
|
+
self._stderr_tail.append(_DIAGNOSTIC_SECRET.sub("[REDACTED]", diagnostic))
|
|
646
|
+
|
|
647
|
+
async def _terminate(self) -> None:
|
|
648
|
+
process = self._process
|
|
649
|
+
self._process = None
|
|
650
|
+
if process is not None and process.returncode is None:
|
|
651
|
+
process.terminate()
|
|
652
|
+
try:
|
|
653
|
+
await asyncio.wait_for(process.wait(), timeout=self._shutdown_timeout)
|
|
654
|
+
except asyncio.TimeoutError:
|
|
655
|
+
process.kill()
|
|
656
|
+
await process.wait()
|
|
657
|
+
task = self._stderr_task
|
|
658
|
+
self._stderr_task = None
|
|
659
|
+
if task is not None and task is not asyncio.current_task():
|
|
660
|
+
try:
|
|
661
|
+
await asyncio.wait_for(task, timeout=self._shutdown_timeout)
|
|
662
|
+
except asyncio.TimeoutError:
|
|
663
|
+
task.cancel()
|
|
664
|
+
await asyncio.gather(task, return_exceptions=True)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
class DshAgentProviderRuntime:
|
|
668
|
+
"""Existing PluginHost AgentProvider SPI backed by one DSH host."""
|
|
669
|
+
|
|
670
|
+
def __init__(self, host: DshAgentProviderHost) -> None:
|
|
671
|
+
self._host = host
|
|
672
|
+
self._ready = False
|
|
673
|
+
self._disposed = False
|
|
674
|
+
|
|
675
|
+
async def start(self) -> None:
|
|
676
|
+
if self._disposed:
|
|
677
|
+
raise PluginHostError("dsh_provider_disposed", "DSH AgentProvider is disposed")
|
|
678
|
+
await self._host.preflight()
|
|
679
|
+
self._ready = True
|
|
680
|
+
|
|
681
|
+
async def health(self) -> bool:
|
|
682
|
+
return (
|
|
683
|
+
self._ready
|
|
684
|
+
and not self._disposed
|
|
685
|
+
and self._host.pid is not None
|
|
686
|
+
and await self._host.health()
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
async def prepare(
|
|
690
|
+
self,
|
|
691
|
+
bundle: ResolvedPluginBundle,
|
|
692
|
+
*,
|
|
693
|
+
capabilities: PluginExecutionContext,
|
|
694
|
+
) -> "DshPreparedAgent":
|
|
695
|
+
if not await self.health():
|
|
696
|
+
raise PluginHostError("dsh_provider_unavailable", "DSH AgentProvider is not healthy")
|
|
697
|
+
activation_id = await self._host.activate(bundle, capabilities)
|
|
698
|
+
return DshPreparedAgent(self._host, activation_id)
|
|
699
|
+
|
|
700
|
+
async def inventory(self) -> DshAgentProviderInventory:
|
|
701
|
+
return await self._host.inventory()
|
|
702
|
+
|
|
703
|
+
async def drain(self) -> None:
|
|
704
|
+
if self._ready and not self._disposed and self._host.pid is not None:
|
|
705
|
+
await self._host.drain()
|
|
706
|
+
self._ready = False
|
|
707
|
+
|
|
708
|
+
async def dispose(self) -> None:
|
|
709
|
+
if self._disposed:
|
|
710
|
+
return
|
|
711
|
+
self._disposed = True
|
|
712
|
+
self._ready = False
|
|
713
|
+
await self._host.dispose()
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
class DshPreparedAgent:
|
|
717
|
+
def __init__(self, host: DshAgentProviderHost, activation_id: str) -> None:
|
|
718
|
+
self._host = host
|
|
719
|
+
self._activation_id = activation_id
|
|
720
|
+
self._started = False
|
|
721
|
+
self._drained = False
|
|
722
|
+
self._disposed = False
|
|
723
|
+
|
|
724
|
+
async def start(self) -> None:
|
|
725
|
+
if self._disposed:
|
|
726
|
+
raise PluginHostError("dsh_activation_disposed", "DSH activation is disposed")
|
|
727
|
+
self._started = True
|
|
728
|
+
|
|
729
|
+
async def health(self) -> bool:
|
|
730
|
+
return (
|
|
731
|
+
self._started
|
|
732
|
+
and not self._drained
|
|
733
|
+
and not self._disposed
|
|
734
|
+
and await self._host.health(activation_id=self._activation_id)
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
async def execute(self, request: Any) -> Any:
|
|
738
|
+
if not await self.health():
|
|
739
|
+
raise PluginHostError("dsh_activation_unavailable", "DSH activation is not healthy")
|
|
740
|
+
return await self._host.execute(self._activation_id, request)
|
|
741
|
+
|
|
742
|
+
async def cancel(self) -> None:
|
|
743
|
+
"""Request bounded cancellation without losing the disposable handle."""
|
|
744
|
+
|
|
745
|
+
if self._disposed:
|
|
746
|
+
return
|
|
747
|
+
await self._host.cancel_activation(self._activation_id)
|
|
748
|
+
|
|
749
|
+
async def drain(self) -> None:
|
|
750
|
+
if self._disposed or self._drained:
|
|
751
|
+
return
|
|
752
|
+
await self._host.drain(activation_id=self._activation_id)
|
|
753
|
+
self._drained = True
|
|
754
|
+
|
|
755
|
+
async def dispose(self) -> None:
|
|
756
|
+
if self._disposed:
|
|
757
|
+
return
|
|
758
|
+
self._disposed = True
|
|
759
|
+
if self._host.pid is not None:
|
|
760
|
+
await self._host.dispose_activation(self._activation_id)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
class DshAgentProviderFactory:
|
|
764
|
+
"""Project one discovered DSH provider into the internal PluginFactory seam."""
|
|
765
|
+
|
|
766
|
+
def __init__(
|
|
767
|
+
self,
|
|
768
|
+
host: DshAgentProviderHost,
|
|
769
|
+
registration: DshAgentProviderRegistration,
|
|
770
|
+
) -> None:
|
|
771
|
+
self._host = host
|
|
772
|
+
self._registration = registration
|
|
773
|
+
self.runtime: DshAgentProviderRuntime | None = None
|
|
774
|
+
|
|
775
|
+
async def stage(
|
|
776
|
+
self,
|
|
777
|
+
manifest: PluginManifest,
|
|
778
|
+
*,
|
|
779
|
+
profile: Any,
|
|
780
|
+
services: Mapping[str, Any],
|
|
781
|
+
) -> DshAgentProviderRuntime:
|
|
782
|
+
del profile, services
|
|
783
|
+
expected = dsh_agent_provider_manifest(
|
|
784
|
+
self._registration.descriptor,
|
|
785
|
+
preflight=self._registration.preflight,
|
|
786
|
+
)
|
|
787
|
+
if manifest != expected:
|
|
788
|
+
raise PluginHostError(
|
|
789
|
+
"dsh_provider_manifest_mismatch",
|
|
790
|
+
"internal DSH provider projection does not match the discovered descriptor",
|
|
791
|
+
)
|
|
792
|
+
self.runtime = DshAgentProviderRuntime(self._host)
|
|
793
|
+
return self.runtime
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def dsh_agent_provider_manifest(
|
|
797
|
+
descriptor: DshAgentProviderDescriptor,
|
|
798
|
+
*,
|
|
799
|
+
preflight: DshAgentProviderPreflight,
|
|
800
|
+
) -> PluginManifest:
|
|
801
|
+
"""Create an internal admission projection; this is not a DSH package format."""
|
|
802
|
+
|
|
803
|
+
if (
|
|
804
|
+
not preflight.ready
|
|
805
|
+
or preflight.descriptor_digest != descriptor.descriptor_digest
|
|
806
|
+
or preflight.profile_digest != descriptor.profile_digest
|
|
807
|
+
):
|
|
808
|
+
raise PluginHostError(
|
|
809
|
+
"dsh_provider_not_ready",
|
|
810
|
+
"DSH AgentProvider cannot enter the selector before matching preflight",
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
return PluginManifest.model_validate(
|
|
814
|
+
{
|
|
815
|
+
"metadata": {
|
|
816
|
+
"id": descriptor.provider_id,
|
|
817
|
+
"version": descriptor.provider_version,
|
|
818
|
+
},
|
|
819
|
+
"spec": {
|
|
820
|
+
"domain": "runtime-native",
|
|
821
|
+
"runtime": "process",
|
|
822
|
+
"entrypoint": "deepseek-harness:profile-agent-provider",
|
|
823
|
+
"provides": [
|
|
824
|
+
{
|
|
825
|
+
"definition": descriptor.definition,
|
|
826
|
+
"slot": descriptor.slot,
|
|
827
|
+
"mode": "unique",
|
|
828
|
+
}
|
|
829
|
+
],
|
|
830
|
+
"permissions": [DSH_HOST_USER_PERMISSION],
|
|
831
|
+
"isolation": "sidecar",
|
|
832
|
+
"compatibility": {
|
|
833
|
+
"kernelApi": ">=1,<2",
|
|
834
|
+
"runtimeProtocols": list(descriptor.runtime_protocols),
|
|
835
|
+
},
|
|
836
|
+
"healthContract": "plugin.health/v1",
|
|
837
|
+
"provenance": {
|
|
838
|
+
"source": "runtime-native",
|
|
839
|
+
"digest": descriptor.descriptor_digest,
|
|
840
|
+
},
|
|
841
|
+
},
|
|
842
|
+
}
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _validate_command(command: Sequence[str]) -> tuple[str, ...]:
|
|
847
|
+
if isinstance(command, (str, bytes)):
|
|
848
|
+
raise PluginHostError(
|
|
849
|
+
"dsh_provider_command_invalid", "DSH host command must be an argv sequence"
|
|
850
|
+
)
|
|
851
|
+
normalized = tuple(str(item) for item in command)
|
|
852
|
+
if not normalized or any(not item or "\x00" in item for item in normalized):
|
|
853
|
+
raise PluginHostError(
|
|
854
|
+
"dsh_provider_command_invalid", "DSH host command contains an invalid argv item"
|
|
855
|
+
)
|
|
856
|
+
return normalized
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _minimal_environment(explicit: Mapping[str, str]) -> dict[str, str]:
|
|
860
|
+
environment = {
|
|
861
|
+
key: value
|
|
862
|
+
for key, value in os.environ.items()
|
|
863
|
+
if key in _SAFE_INHERITED_ENV and not _SECRET_ENV.search(key)
|
|
864
|
+
}
|
|
865
|
+
for key, value in explicit.items():
|
|
866
|
+
if (
|
|
867
|
+
not _ENV_NAME.fullmatch(key)
|
|
868
|
+
or key in _BLOCKED_ENV
|
|
869
|
+
or key.startswith("DYLD_")
|
|
870
|
+
or _SECRET_ENV.search(key)
|
|
871
|
+
):
|
|
872
|
+
raise PluginHostError(
|
|
873
|
+
"dsh_provider_environment_denied",
|
|
874
|
+
f"DSH host environment key {key!r} is not allowed",
|
|
875
|
+
)
|
|
876
|
+
environment[key] = str(value)
|
|
877
|
+
return environment
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def _positive_timeout(value: float, field: str) -> float:
|
|
881
|
+
normalized = float(value)
|
|
882
|
+
if normalized <= 0:
|
|
883
|
+
raise PluginHostError("dsh_provider_timeout_invalid", f"{field} must be positive")
|
|
884
|
+
return normalized
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _bundle_payload(bundle: ResolvedPluginBundle) -> dict[str, Any]:
|
|
888
|
+
return {
|
|
889
|
+
"root": str(bundle.root),
|
|
890
|
+
"manifest": bundle.manifest.model_dump(by_alias=True, exclude_none=True, mode="json"),
|
|
891
|
+
"resolvedAgentSpec": _json_value(bundle.resolved_agent_spec),
|
|
892
|
+
"composition": {
|
|
893
|
+
"profileDigest": bundle.composition.profile_digest,
|
|
894
|
+
"pluginLockDigest": bundle.composition.plugin_lock_digest,
|
|
895
|
+
},
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _capability_payload(context: PluginExecutionContext) -> dict[str, Any]:
|
|
900
|
+
return {
|
|
901
|
+
"profileDigest": context.profile_digest,
|
|
902
|
+
"pluginLockDigest": context.plugin_lock_digest,
|
|
903
|
+
"bindings": [
|
|
904
|
+
{
|
|
905
|
+
"pluginId": binding.plugin_id,
|
|
906
|
+
"pluginVersion": binding.plugin_version,
|
|
907
|
+
"definition": binding.definition,
|
|
908
|
+
"slot": binding.slot,
|
|
909
|
+
}
|
|
910
|
+
for binding in context.bindings
|
|
911
|
+
],
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _json_value(value: Any) -> Any:
|
|
916
|
+
if isinstance(value, Mapping):
|
|
917
|
+
normalized: Any = {str(key): _json_value(child) for key, child in value.items()}
|
|
918
|
+
elif isinstance(value, (list, tuple)):
|
|
919
|
+
normalized = [_json_value(child) for child in value]
|
|
920
|
+
elif value is None or isinstance(value, (str, int, float, bool)):
|
|
921
|
+
normalized = value
|
|
922
|
+
else:
|
|
923
|
+
raise PluginHostError(
|
|
924
|
+
"dsh_provider_request_invalid",
|
|
925
|
+
f"DSH provider value {type(value).__name__} is not JSON compatible",
|
|
926
|
+
)
|
|
927
|
+
try:
|
|
928
|
+
json.dumps(normalized, ensure_ascii=False, allow_nan=False)
|
|
929
|
+
except (TypeError, ValueError) as error:
|
|
930
|
+
raise PluginHostError(
|
|
931
|
+
"dsh_provider_request_invalid", "DSH provider value is not strict JSON"
|
|
932
|
+
) from error
|
|
933
|
+
return normalized
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
__all__ = [
|
|
937
|
+
"DSH_AGENT_PROVIDER_HOST_METHODS",
|
|
938
|
+
"DSH_AGENT_PROVIDER_HOST_PROTOCOL",
|
|
939
|
+
"DSH_HOST_USER_PERMISSION",
|
|
940
|
+
"DshAgentProviderDescriptor",
|
|
941
|
+
"DshAgentProviderFactory",
|
|
942
|
+
"DshAgentProviderHost",
|
|
943
|
+
"DshAgentProviderInventory",
|
|
944
|
+
"DshAgentProviderPreflight",
|
|
945
|
+
"DshAgentProviderRegistration",
|
|
946
|
+
"DshAgentProviderRuntime",
|
|
947
|
+
"DshCircuitBreaker",
|
|
948
|
+
"DshCircuitSnapshot",
|
|
949
|
+
"DshPreparedAgent",
|
|
950
|
+
"dsh_agent_provider_manifest",
|
|
951
|
+
]
|