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.
Files changed (563) hide show
  1. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.4.dist-info}/METADATA +22 -28
  2. agentengine_sdk_python-0.8.4.dist-info/RECORD +1070 -0
  3. ksadk/_build_provenance.json +1 -0
  4. ksadk/agui/_agent_helpers.py +4 -0
  5. ksadk/api/client.py +108 -91
  6. ksadk/builders/code_builder.py +36 -9
  7. ksadk/builders/framework_requirements.py +2 -1
  8. ksadk/builders/ks3_uploader.py +36 -7
  9. ksadk/builders/managed_runtime_builder.py +4 -0
  10. ksadk/cli/__init__.py +38 -3
  11. ksadk/cli/cmd_managed_runtime.py +13 -1
  12. ksadk/cli/cmd_plugin.py +662 -0
  13. ksadk/cli/cmd_web.py +2 -5
  14. ksadk/cli/runtime_bootstrap.py +23 -1
  15. ksadk/codex/client.py +424 -96
  16. ksadk/codex/projection.py +24 -0
  17. ksadk/codex/runtime.py +83 -283
  18. ksadk/common/auth.py +16 -10
  19. ksadk/configs/env_registry.py +59 -3
  20. ksadk/configs/settings.py +9 -0
  21. ksadk/context_engine/contributors.py +73 -8
  22. ksadk/conversations/__init__.py +19 -0
  23. ksadk/conversations/context.py +104 -24
  24. ksadk/conversations/contracts.py +266 -0
  25. ksadk/conversations/message_projection.py +154 -15
  26. ksadk/conversations/projector.py +614 -0
  27. ksadk/conversations/reducer.py +98 -0
  28. ksadk/conversations/runtime_invocation.py +5 -0
  29. ksadk/conversations/runtime_payloads.py +1 -0
  30. ksadk/conversations/runtime_preparation.py +30 -27
  31. ksadk/conversations/runtime_resume.py +50 -2
  32. ksadk/conversations/runtime_stream_events.py +5 -0
  33. ksadk/conversations/runtime_streaming.py +25 -7
  34. ksadk/deployment/env_forward.py +2 -2
  35. ksadk/deployment/providers/serverless.py +1 -1
  36. ksadk/events/_v1_compat/projection.py +2 -0
  37. ksadk/events/adapters/_codex_validators.py +18 -7
  38. ksadk/events/adapters/codex.py +190 -0
  39. ksadk/events/adapters/langgraph.py +15 -1
  40. ksadk/events/canonical_store.py +3 -1
  41. ksadk/events/projections.py +1 -1
  42. ksadk/harness/__init__.py +45 -35
  43. ksadk/harness/config.py +2 -2
  44. ksadk/harness/runtime.py +75 -16
  45. ksadk/harness/tools.py +38 -13
  46. ksadk/interaction/provider.py +1 -0
  47. ksadk/interaction/providers/langgraph.py +1 -0
  48. ksadk/kernel/bootstrap.py +28 -4
  49. ksadk/kernel/contract_fingerprints.py +5 -4
  50. ksadk/kernel/contracts.py +8 -0
  51. ksadk/kernel/control.py +1 -0
  52. ksadk/kernel/ingress.py +36 -2
  53. ksadk/kernel/memory_store.py +1 -0
  54. ksadk/kernel/postgres_store.py +1 -0
  55. ksadk/kernel/sqlite_store.py +28 -0
  56. ksadk/kernel/store.py +3 -0
  57. ksadk/kernel/worker.py +76 -10
  58. ksadk/knowledge_base/client.py +57 -27
  59. ksadk/mcp_runtime/__init__.py +12 -4
  60. ksadk/memory/adk/backends/sdk_ltm_backend.py +161 -41
  61. ksadk/memory/adk/resilient_session_service.py +25 -17
  62. ksadk/memory/adk/short_term_memory.py +26 -9
  63. ksadk/memory/models.py +7 -4
  64. ksadk/memory/provider_resolver.py +3 -0
  65. ksadk/memory/providers/local_sqlite.py +7 -0
  66. ksadk/model_proxy/server.py +10 -5
  67. ksadk/plugins/__init__.py +171 -0
  68. ksadk/plugins/artifacts.py +275 -0
  69. ksadk/plugins/bridges/__init__.py +47 -0
  70. ksadk/plugins/bridges/codex.py +659 -0
  71. ksadk/plugins/bridges/dsh.py +1300 -0
  72. ksadk/plugins/builtins.py +746 -0
  73. ksadk/plugins/bundle.py +246 -0
  74. ksadk/plugins/bundle_security.py +191 -0
  75. ksadk/plugins/codex_manifest.py +983 -0
  76. ksadk/plugins/composition.py +537 -0
  77. ksadk/plugins/context_contributor.py +437 -0
  78. ksadk/plugins/contracts.py +569 -0
  79. ksadk/plugins/delivery.py +153 -0
  80. ksadk/plugins/dsh_installation_digest.py +71 -0
  81. ksadk/plugins/dsh_toolchain.py +1024 -0
  82. ksadk/plugins/ecosystem_bridge.py +1105 -0
  83. ksadk/plugins/ecosystem_probe.py +191 -0
  84. ksadk/plugins/host.py +787 -0
  85. ksadk/plugins/kernel_adapter.py +205 -0
  86. ksadk/plugins/providers/__init__.py +108 -0
  87. ksadk/plugins/providers/bundles/dsh-knowledge/cordis.patch.yml +3 -0
  88. ksadk/plugins/providers/bundles/dsh-knowledge/index.mjs +53 -0
  89. ksadk/plugins/providers/bundles/dsh-knowledge/package.json +16 -0
  90. ksadk/plugins/providers/bundles/dsh-memory/cordis.patch.yml +3 -0
  91. ksadk/plugins/providers/bundles/dsh-memory/index.mjs +175 -0
  92. ksadk/plugins/providers/bundles/dsh-memory/package.json +16 -0
  93. ksadk/plugins/providers/bundles/dsh-platform-resources/client.mjs +166 -0
  94. ksadk/plugins/providers/bundles/dsh-platform-resources/cordis.patch.yml +3 -0
  95. ksadk/plugins/providers/bundles/dsh-platform-resources/index.mjs +9 -0
  96. ksadk/plugins/providers/bundles/dsh-platform-resources/package.json +12 -0
  97. ksadk/plugins/providers/bundles/dsh-skill-center/cordis.patch.yml +3 -0
  98. ksadk/plugins/providers/bundles/dsh-skill-center/index.mjs +125 -0
  99. ksadk/plugins/providers/bundles/dsh-skill-center/package.json +16 -0
  100. ksadk/plugins/providers/bundles/ksadk-codex/cordis.patch.yml +3 -0
  101. ksadk/plugins/providers/bundles/ksadk-codex/index.mjs +18 -0
  102. ksadk/plugins/providers/bundles/ksadk-codex/package.json +19 -0
  103. ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/cordis.patch.yml +3 -0
  104. ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/index.mjs +870 -0
  105. ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/package.json +27 -0
  106. ksadk/plugins/providers/bundles/ksadk-dsh-studio/client.js +92 -0
  107. ksadk/plugins/providers/bundles/ksadk-dsh-studio/index.mjs +15 -0
  108. ksadk/plugins/providers/bundles/ksadk-dsh-studio/package.json +7 -0
  109. ksadk/plugins/providers/bundles/ksadk-harness/cordis.patch.yml +3 -0
  110. ksadk/plugins/providers/bundles/ksadk-harness/index.mjs +19 -0
  111. ksadk/plugins/providers/bundles/ksadk-harness/package.json +19 -0
  112. ksadk/plugins/providers/codex.py +988 -0
  113. ksadk/plugins/providers/codex_dsh.py +83 -0
  114. ksadk/plugins/providers/codex_native.py +368 -0
  115. ksadk/plugins/providers/codex_turn.py +301 -0
  116. ksadk/plugins/providers/dsh.py +951 -0
  117. ksadk/plugins/providers/dsh_capabilities.py +1389 -0
  118. ksadk/plugins/providers/dsh_descriptor_host.py +226 -0
  119. ksadk/plugins/providers/dsh_mcp.py +467 -0
  120. ksadk/plugins/providers/harness.py +632 -0
  121. ksadk/plugins/providers/harness_dsh.py +91 -0
  122. ksadk/plugins/providers/legacy.py +215 -0
  123. ksadk/plugins/providers/legacy_catalog.py +100 -0
  124. ksadk/plugins/providers/mcp_projection.py +150 -0
  125. ksadk/plugins/providers/platform_resources.py +657 -0
  126. ksadk/plugins/providers/shipped_dsh.py +370 -0
  127. ksadk/plugins/resolver.py +338 -0
  128. ksadk/plugins/subagent_providers/__init__.py +12 -0
  129. ksadk/plugins/subagent_providers/codex.py +443 -0
  130. ksadk/plugins/subagents.py +333 -0
  131. ksadk/resource_runtime/__init__.py +4 -0
  132. ksadk/resource_runtime/broker.py +616 -0
  133. ksadk/resource_runtime/build_artifacts.py +165 -0
  134. ksadk/resource_runtime/contracts.py +138 -0
  135. ksadk/resource_runtime/discovery_receipts.py +100 -0
  136. ksadk/resource_runtime/discovery_skills.py +172 -0
  137. ksadk/resource_runtime/errors.py +6 -0
  138. ksadk/resource_runtime/ipc.py +142 -0
  139. ksadk/resource_runtime/kernel_approval.py +165 -0
  140. ksadk/resource_runtime/knowledge.py +114 -0
  141. ksadk/resource_runtime/langgraph.py +94 -0
  142. ksadk/resource_runtime/leases.py +191 -0
  143. ksadk/resource_runtime/managed_projection.py +186 -0
  144. ksadk/resource_runtime/mcp_server.py +138 -0
  145. ksadk/resource_runtime/memory_provider.py +318 -0
  146. ksadk/resource_runtime/memory_receipts.py +34 -0
  147. ksadk/resource_runtime/operation_ledger.py +211 -0
  148. ksadk/resource_runtime/plugin_config.py +48 -0
  149. ksadk/resource_runtime/policy_authorization.py +54 -0
  150. ksadk/resource_runtime/process.py +110 -0
  151. ksadk/resource_runtime/skill_artifacts.py +153 -0
  152. ksadk/resource_runtime/skill_runtime.py +42 -0
  153. ksadk/resource_runtime/skill_workspace.py +118 -0
  154. ksadk/resource_runtime/skills.py +215 -0
  155. ksadk/resource_runtime/snapshots.py +180 -0
  156. ksadk/resource_runtime/socket_server.py +103 -0
  157. ksadk/resource_runtime/supervisor.py +380 -0
  158. ksadk/resource_runtime/worker.py +504 -0
  159. ksadk/runners/_langgraph_runner_streams.py +74 -6
  160. ksadk/runners/adk_runner.py +149 -11
  161. ksadk/runners/base_runner.py +29 -1
  162. ksadk/runners/langgraph_runner.py +475 -145
  163. ksadk/runtime/_runner_adapter/stream_mapping.py +193 -60
  164. ksadk/runtime/adapter.py +3 -0
  165. ksadk/runtime/conversation_execution.py +92 -16
  166. ksadk/runtime/executor.py +78 -2
  167. ksadk/runtime/factory.py +30 -200
  168. ksadk/runtime/framework_adapters.py +41 -0
  169. ksadk/runtime/preprocessing.py +10 -2
  170. ksadk/runtime/runner_adapter.py +78 -14
  171. ksadk/runtime_context.py +40 -2
  172. ksadk/sandbox/__init__.py +2 -1
  173. ksadk/sandbox/backends/e2b.py +52 -11
  174. ksadk/sandbox/backends/local_process.py +9 -0
  175. ksadk/sandbox/base.py +2 -0
  176. ksadk/sandbox/e2b_connection.py +66 -0
  177. ksadk/sandbox/factory.py +72 -0
  178. ksadk/scheduler/__init__.py +18 -0
  179. ksadk/scheduler/calendar.py +102 -0
  180. ksadk/scheduler/contracts.py +254 -0
  181. ksadk/scheduler/dispatcher.py +129 -0
  182. ksadk/scheduler/engine.py +416 -0
  183. ksadk/scheduler/sqlite_store.py +581 -0
  184. ksadk/server/composition.py +2 -0
  185. ksadk/server/factory.py +5 -0
  186. ksadk/server/persistence_capability.py +516 -0
  187. ksadk/server/routes/control.py +210 -46
  188. ksadk/server/routes/dependencies.py +15 -0
  189. ksadk/server/routes/models.py +47 -4
  190. ksadk/server/routes/projection.py +252 -12
  191. ksadk/server/routes/sessions.py +284 -107
  192. ksadk/server/routes/streaming.py +43 -9
  193. ksadk/server/static/assets/{ArtifactsPanel-DSU1sWD_.js → ArtifactsPanel-CGbWlMNh.js} +1 -1
  194. ksadk/server/static/assets/{CodeBlock-DOH6MVcn.js → CodeBlock-b4C7upmO.js} +3 -3
  195. ksadk/server/static/assets/{MathMessageMarkdown-BL8my1R2.js → MathMessageMarkdown-DbdW5A4_.js} +4 -4
  196. ksadk/server/static/assets/{MermaidBlock-Dz4IP-Tx.js → MermaidBlock-BEBN4Ksz.js} +6 -6
  197. ksadk/server/static/assets/{NativeTerminalPanel-DUrK0JpZ.js → NativeTerminalPanel-zvL-WpAn.js} +1 -1
  198. ksadk/server/static/assets/{abnfDiagram-VCTEODGH-g20pFzNV.js → abnfDiagram-VCTEODGH-BYafW7Za.js} +1 -1
  199. ksadk/server/static/assets/{arc-C4FzinUA.js → arc-bTohInsS.js} +1 -1
  200. ksadk/server/static/assets/architecture-7GRP2DOG-BW5UtSnx.js +1 -0
  201. ksadk/server/static/assets/{architectureDiagram-5GKGNRK7-DpIqL5h4.js → architectureDiagram-5GKGNRK7-Ch5Lbzsi.js} +1 -1
  202. ksadk/server/static/assets/{blockDiagram-NRAW4CY4-BjkljTNz.js → blockDiagram-NRAW4CY4-kqF4MDOG.js} +1 -1
  203. ksadk/server/static/assets/{c4Diagram-UCG6FXSJ-Dk_ieq2X.js → c4Diagram-UCG6FXSJ-8uSRIODv.js} +1 -1
  204. ksadk/server/static/assets/channel-CF5Up3hQ.js +1 -0
  205. ksadk/server/static/assets/chunk-2Q5K7J3B-KkXX47WI.js +1 -0
  206. ksadk/server/static/assets/{chunk-5VM5RSS4-BVHAchFb.js → chunk-5VM5RSS4-DkhehZDr.js} +1 -1
  207. ksadk/server/static/assets/{chunk-F27PBJKO-C-ipQzuS.js → chunk-F27PBJKO-BdXhtPe9.js} +1 -1
  208. ksadk/server/static/assets/{chunk-G27WJ6UU-C2EyJbIK.js → chunk-G27WJ6UU-DPTpvpwi.js} +1 -1
  209. ksadk/server/static/assets/{chunk-JWPE2WC7-vYvVJb_M.js → chunk-JWPE2WC7-Pqoh38el.js} +1 -1
  210. ksadk/server/static/assets/{chunk-LCL6LL3I-BcI6ysPT.js → chunk-LCL6LL3I-DeU00qC-.js} +1 -1
  211. ksadk/server/static/assets/{chunk-POPQ4Y6H-C030x_Z1.js → chunk-POPQ4Y6H-CzxScFkr.js} +1 -1
  212. ksadk/server/static/assets/{chunk-RHFEMEQ7-yyGpsz4n.js → chunk-RHFEMEQ7-DpqMx-ux.js} +1 -1
  213. ksadk/server/static/assets/{chunk-SVP7TREG-BLTlmMU7.js → chunk-SVP7TREG-C6t0SypQ.js} +1 -1
  214. ksadk/server/static/assets/{chunk-XXDRQBXY-C_32ArgP.js → chunk-XXDRQBXY-DVaeafZk.js} +1 -1
  215. ksadk/server/static/assets/classDiagram-DTDB5LWJ-Cz81acRQ.js +1 -0
  216. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-Cz81acRQ.js +1 -0
  217. ksadk/server/static/assets/{cose-bilkent-JH36ORCC-9YQYwdl0.js → cose-bilkent-JH36ORCC-Bf9D3tzJ.js} +1 -1
  218. ksadk/server/static/assets/cynefin-OW5HDTMX-DVWs8a2t.js +1 -0
  219. ksadk/server/static/assets/{cynefinDiagram-5FMLGOSQ-ErLF5N13.js → cynefinDiagram-5FMLGOSQ-B9cY3iTz.js} +1 -1
  220. ksadk/server/static/assets/{dagre-3AP2YEHR-DFiQF-6f.js → dagre-3AP2YEHR-CSwlx3CB.js} +1 -1
  221. ksadk/server/static/assets/{dagre-BuhGnRI1.js → dagre-UBpgon5q.js} +1 -1
  222. ksadk/server/static/assets/{diagram-S7CK7UJ4-Bt1v8-GC.js → diagram-S7CK7UJ4-C9akhN9a.js} +1 -1
  223. ksadk/server/static/assets/{diagram-UQ7AKVKN-DSCxJdBK.js → diagram-UQ7AKVKN-CgedhTPI.js} +1 -1
  224. ksadk/server/static/assets/{diagram-VSXAHHWV-DHfYwp_9.js → diagram-VSXAHHWV-Cjucz5BY.js} +1 -1
  225. ksadk/server/static/assets/{diagram-VX7I27RA-DdzD-4Le.js → diagram-VX7I27RA-Dd4Vleyn.js} +1 -1
  226. ksadk/server/static/assets/{diagram-Z3DM3KII-Du-nAJ9F.js → diagram-Z3DM3KII-CZ2jGtYW.js} +1 -1
  227. ksadk/server/static/assets/{dist-B7seoj3d.js → dist-B2_CVgKv.js} +1 -1
  228. ksadk/server/static/assets/{dist-CSh_DE14.js → dist-B3FR1SKK.js} +1 -1
  229. ksadk/server/static/assets/{dist-DUX-id_F.js → dist-BIr4DSRq.js} +1 -1
  230. ksadk/server/static/assets/{dist-C_wsv-Qd.js → dist-BgV_owcB.js} +1 -1
  231. ksadk/server/static/assets/{dist-W-TIVntx.js → dist-Bj730Ib4.js} +1 -1
  232. ksadk/server/static/assets/{dist-BytlxIDT.js → dist-BnaTiyzN.js} +1 -1
  233. ksadk/server/static/assets/{dist-CXYYBw3_.js → dist-C1NMAY5a.js} +1 -1
  234. ksadk/server/static/assets/{dist-dDdADKFA.js → dist-C5_ZAZsG.js} +1 -1
  235. ksadk/server/static/assets/{dist-BVDffZ0U.js → dist-CB9iJrvg.js} +1 -1
  236. ksadk/server/static/assets/{dist-Bf2M8m3N.js → dist-CFzsn6BG.js} +1 -1
  237. ksadk/server/static/assets/{dist-ngtN0DJB.js → dist-Cf125P_m.js} +1 -1
  238. ksadk/server/static/assets/{dist-BE7bu-DL.js → dist-ClYUxfeK.js} +1 -1
  239. ksadk/server/static/assets/{dist-DNp6rLRA.js → dist-DFD6Qa6T.js} +1 -1
  240. ksadk/server/static/assets/{dist-BX8z72IC.js → dist-DVCOWtha.js} +1 -1
  241. ksadk/server/static/assets/{dist-dydu68Fl.js → dist-D_ctSS2i.js} +1 -1
  242. ksadk/server/static/assets/{dist-ClxGviKw.js → dist-DiixqL0J.js} +1 -1
  243. ksadk/server/static/assets/{dist-BjU6y-A2.js → dist-DoVSwoDb.js} +1 -1
  244. ksadk/server/static/assets/{dist-Dq9gLD7L.js → dist-DvFK7p5S.js} +1 -1
  245. ksadk/server/static/assets/{dist-8hqcx0sU.js → dist-QfkEHOlb.js} +1 -1
  246. ksadk/server/static/assets/{dist-CttYUqzS.js → dist-btbmmX5O.js} +1 -1
  247. ksadk/server/static/assets/{dist-DmldrHd_.js → dist-fBO2v5Kt.js} +1 -1
  248. ksadk/server/static/assets/{dist-DnfXs8Vn.js → dist-ufY3F5p6.js} +1 -1
  249. ksadk/server/static/assets/{ebnfDiagram-PWID7BFC-Da9C9NO1.js → ebnfDiagram-PWID7BFC-Ci8NqqZu.js} +1 -1
  250. ksadk/server/static/assets/{erDiagram-SSCWMZ5O-EAGqqR79.js → erDiagram-SSCWMZ5O-CCIOhLZG.js} +1 -1
  251. ksadk/server/static/assets/eventmodeling-NTZA5JFV-CQqR2hg3.js +1 -0
  252. ksadk/server/static/assets/flowDiagram-A5DVABFB-DDRRbGPm.js +1 -0
  253. ksadk/server/static/assets/{ganttDiagram-EL5Y4UJY-DqJsKb59.js → ganttDiagram-EL5Y4UJY-Cp_r9Hb6.js} +1 -1
  254. ksadk/server/static/assets/gitGraph-4MIJSDKK-fgWEf7UD.js +1 -0
  255. ksadk/server/static/assets/{gitGraphDiagram-WWUBYQGX-B6g4dtDi.js → gitGraphDiagram-WWUBYQGX-Dlhc8Wr8.js} +1 -1
  256. ksadk/server/static/assets/index-DcTML1Up.css +2 -0
  257. ksadk/server/static/assets/index-Dh2XHqPK.js +244 -0
  258. ksadk/server/static/assets/info-A6RAGUB7-BUecLTdt.js +1 -0
  259. ksadk/server/static/assets/{infoDiagram-RXCK75RN-Djm_nbd5.js → infoDiagram-RXCK75RN-CO0rRTRA.js} +1 -1
  260. ksadk/server/static/assets/{ishikawaDiagram-5VMMS53U-DWya9SG2.js → ishikawaDiagram-5VMMS53U-CHiaedSS.js} +1 -1
  261. ksadk/server/static/assets/{journeyDiagram-EYS64GPL-DutxhSNI.js → journeyDiagram-EYS64GPL-CHcCIB09.js} +1 -1
  262. ksadk/server/static/assets/{kanban-definition-3QL26DDD-DJdOF8Fm.js → kanban-definition-3QL26DDD-CJ_ebBZf.js} +1 -1
  263. ksadk/server/static/assets/{katex-vFWytM5c.js → katex-B4weu383.js} +1 -1
  264. ksadk/server/static/assets/{linear-BHjG8b-J.js → linear-V-MgYHW1.js} +1 -1
  265. ksadk/server/static/assets/{mermaid-parser.core-KGSy4jWT.js → mermaid-parser.core-CBijEX_c.js} +2 -2
  266. ksadk/server/static/assets/{mindmap-definition-FBJOCRG2-9-HC88D4.js → mindmap-definition-FBJOCRG2-Bc-pQvpp.js} +1 -1
  267. ksadk/server/static/assets/packet-AYTQ26CC-BeW-btFv.js +1 -0
  268. ksadk/server/static/assets/{pegDiagram-XKGWAZYB-DrEZd4fD.js → pegDiagram-XKGWAZYB-D7genGLl.js} +1 -1
  269. ksadk/server/static/assets/pie-WAS4IAKB-X0xh4SaT.js +1 -0
  270. ksadk/server/static/assets/{pieDiagram-E7YTZNPT-DpjqOFFp.js → pieDiagram-E7YTZNPT-Bxj32zwD.js} +1 -1
  271. ksadk/server/static/assets/{quadrantDiagram-AXDQQJYC-BiNhgOXX.js → quadrantDiagram-AXDQQJYC-BURv2mGh.js} +1 -1
  272. ksadk/server/static/assets/radar-RG4KPBEZ-DzRMSqjd.js +1 -0
  273. ksadk/server/static/assets/railroad-74A4TZTK-Cc44icX-.js +1 -0
  274. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-D6S4BdV-.js +1 -0
  275. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-vD3aVE2o.js +1 -0
  276. ksadk/server/static/assets/railroad-peg-WCYAUIDC-CYbW_b4T.js +1 -0
  277. ksadk/server/static/assets/{railroadDiagram-O6MQD6OU-_Y3Ojg6G.js → railroadDiagram-O6MQD6OU-CCsh3avW.js} +1 -1
  278. ksadk/server/static/assets/{requirementDiagram-EFPCY7ZU-DeCFdg9K.js → requirementDiagram-EFPCY7ZU-_FRRBKEd.js} +1 -1
  279. ksadk/server/static/assets/{sankeyDiagram-P5KCCOFB-CPH75rhw.js → sankeyDiagram-P5KCCOFB-C-gr2o9M.js} +1 -1
  280. ksadk/server/static/assets/{sequenceDiagram-WJ2MYXX4-OEpQQdr4.js → sequenceDiagram-WJ2MYXX4-D97NmfKg.js} +1 -1
  281. ksadk/server/static/assets/{sizeCapture-X5ZJPWSS-heUErzhI.js → sizeCapture-X5ZJPWSS-DjrPC5FX.js} +1 -1
  282. ksadk/server/static/assets/{stateDiagram-HBIQ2CUA-DvA3jSMB.js → stateDiagram-HBIQ2CUA-DzMNg1hh.js} +1 -1
  283. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BIcjKFhF.js +1 -0
  284. ksadk/server/static/assets/{swimlanes-XN3QIQJK-CR-dfN_t.js → swimlanes-XN3QIQJK-Cssf_CsU.js} +1 -1
  285. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-tq3K3Ybl.js +8 -0
  286. ksadk/server/static/assets/{timeline-definition-24CTP7MA-DAVJf1TX.js → timeline-definition-24CTP7MA-BWTHzyS1.js} +1 -1
  287. ksadk/server/static/assets/treeView-Q6P3EWNA-DBXt3GdU.js +1 -0
  288. ksadk/server/static/assets/treemap-WGGIJYW6-DrCy4E_l.js +1 -0
  289. ksadk/server/static/assets/{vennDiagram-4TSXK5OY-DwM8eDkh.js → vennDiagram-4TSXK5OY-BSKgJsmR.js} +1 -1
  290. ksadk/server/static/assets/wardley-WFR3VGLG-CefhOliM.js +1 -0
  291. ksadk/server/static/assets/{wardleyDiagram-VM6X3IG4-DZL-Zz2t.js → wardleyDiagram-VM6X3IG4-eflHNoaZ.js} +1 -1
  292. ksadk/server/static/assets/{xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js → xychartDiagram-S5SC5T6Z-eYqrQ-v4.js} +1 -1
  293. ksadk/server/static/index.html +2 -2
  294. ksadk/session_context.py +66 -0
  295. ksadk/sessions/__init__.py +11 -21
  296. ksadk/sessions/_local_service_sync.py +263 -1
  297. ksadk/sessions/base.py +176 -0
  298. ksadk/sessions/errors.py +4 -0
  299. ksadk/sessions/in_memory.py +309 -19
  300. ksadk/sessions/local_service.py +83 -1
  301. ksadk/sessions/persistence.py +275 -0
  302. ksadk/sessions/postgres_service.py +358 -0
  303. ksadk/sessions/resilience.py +8 -0
  304. ksadk/sessions/resilient.py +362 -4
  305. ksadk/sessions/topology.py +135 -0
  306. ksadk/skills/models.py +26 -4
  307. ksadk/skills/package_store.py +186 -38
  308. ksadk/skills/runtime/agent.py +31 -6
  309. ksadk/skills/runtime/artifact_delivery.py +139 -0
  310. ksadk/skills/runtime/backends/e2b.py +98 -6
  311. ksadk/skills/runtime/backends/local.py +73 -8
  312. ksadk/skills/runtime/base.py +3 -0
  313. ksadk/skills/runtime/executor.py +2 -0
  314. ksadk/skills/runtime/loader.py +10 -2
  315. ksadk/skills/runtime/pinned.py +114 -0
  316. ksadk/skills/runtime/request.py +31 -0
  317. ksadk/skills/service_client.py +211 -24
  318. ksadk/studio/agent_lifecycle.py +26 -20
  319. ksadk/studio/api.py +714 -90
  320. ksadk/studio/api_catalog_routes.py +23 -0
  321. ksadk/studio/api_contracts.py +34 -1
  322. ksadk/studio/api_plugin_routes.py +911 -0
  323. ksadk/studio/api_resource_connections.py +183 -0
  324. ksadk/studio/attachment_store.py +187 -0
  325. ksadk/studio/authoring.py +10 -4
  326. ksadk/studio/authoring_coordinator.py +31 -16
  327. ksadk/studio/builder.py +199 -43
  328. ksadk/studio/cloud.py +346 -104
  329. ksadk/studio/cloud_shared_web.py +565 -0
  330. ksadk/studio/codex_agent_service.py +140 -27
  331. ksadk/studio/codex_builder.py +294 -16
  332. ksadk/studio/codex_credentials.py +90 -0
  333. ksadk/studio/codex_manifest.py +51 -4
  334. ksadk/studio/codex_plugin_store.py +746 -0
  335. ksadk/studio/codex_provider_build.py +231 -0
  336. ksadk/studio/codex_run.py +200 -29
  337. ksadk/studio/compatibility_report.py +456 -0
  338. ksadk/studio/compiler.py +84 -15
  339. ksadk/studio/contracts.py +249 -4
  340. ksadk/studio/dsh_application.py +137 -0
  341. ksadk/studio/dsh_capability_service.py +1107 -0
  342. ksadk/studio/dsh_models.py +59 -0
  343. ksadk/studio/dsh_provider_registration.py +1075 -0
  344. ksadk/studio/event_store.py +54 -6
  345. ksadk/studio/framework_run.py +37 -4
  346. ksadk/studio/hosted_kernel.py +89 -14
  347. ksadk/studio/mcp_runtime.py +11 -3
  348. ksadk/studio/model_client.py +2 -2
  349. ksadk/studio/operations.py +12 -2
  350. ksadk/studio/plugin_composition.py +468 -0
  351. ksadk/studio/plugin_kernel_adapter.py +28 -0
  352. ksadk/studio/plugin_runtime.py +831 -0
  353. ksadk/studio/resource_authority.py +637 -0
  354. ksadk/studio/resource_binding_validation.py +77 -0
  355. ksadk/studio/resource_build_admission.py +196 -0
  356. ksadk/studio/resource_build_materializer.py +149 -0
  357. ksadk/studio/resource_catalog.py +149 -3
  358. ksadk/studio/resource_connections.py +220 -0
  359. ksadk/studio/run_service.py +1144 -96
  360. ksadk/studio/runtime_source.py +24 -8
  361. ksadk/studio/scheduler_assistant.py +317 -0
  362. ksadk/studio/scheduler_runtime.py +383 -0
  363. ksadk/studio/scheduler_service.py +214 -0
  364. ksadk/studio/service.py +1131 -95
  365. ksadk/studio/shared_web.py +610 -87
  366. ksadk/studio/soul.py +52 -0
  367. ksadk/studio/static/assets/CodeBlock-DCUUY0PL-BLjA7kbS.js +41 -0
  368. ksadk/studio/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  369. ksadk/studio/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  370. ksadk/studio/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  371. ksadk/studio/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  372. ksadk/studio/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  373. ksadk/studio/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  374. ksadk/studio/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  375. ksadk/studio/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  376. ksadk/studio/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  377. ksadk/studio/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  378. ksadk/studio/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  379. ksadk/studio/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  380. ksadk/studio/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  381. ksadk/studio/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  382. ksadk/studio/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  383. ksadk/studio/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  384. ksadk/studio/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  385. ksadk/studio/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  386. ksadk/studio/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  387. ksadk/studio/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  388. ksadk/studio/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  389. ksadk/studio/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  390. ksadk/studio/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  391. ksadk/studio/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  392. ksadk/studio/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  393. ksadk/studio/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  394. ksadk/studio/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  395. ksadk/studio/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  396. ksadk/studio/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  397. ksadk/studio/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  398. ksadk/studio/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  399. ksadk/studio/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  400. ksadk/studio/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  401. ksadk/studio/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  402. ksadk/studio/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  403. ksadk/studio/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  404. ksadk/studio/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  405. ksadk/studio/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  406. ksadk/studio/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  407. ksadk/studio/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  408. ksadk/studio/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  409. ksadk/studio/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  410. ksadk/studio/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  411. ksadk/studio/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  412. ksadk/studio/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  413. ksadk/studio/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  414. ksadk/studio/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  415. ksadk/studio/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  416. ksadk/studio/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  417. ksadk/studio/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  418. ksadk/studio/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  419. ksadk/studio/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  420. ksadk/studio/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  421. ksadk/studio/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  422. ksadk/studio/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  423. ksadk/studio/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  424. ksadk/studio/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  425. ksadk/studio/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  426. ksadk/studio/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  427. ksadk/studio/static/assets/MathMessageMarkdown-CBcDrAw--BEOyPC3w.css +1 -0
  428. ksadk/studio/static/assets/MathMessageMarkdown-CBcDrAw--CUYIXwgI.js +8 -0
  429. ksadk/studio/static/assets/MermaidBlock-DBFTztfQ-BTYKWEbD.js +44 -0
  430. ksadk/studio/static/assets/PrismRenderer-wiXRHURw.js +3 -0
  431. ksadk/studio/static/assets/abnfDiagram-VCTEODGH-CLETGjKq.js +1 -0
  432. ksadk/studio/static/assets/arc-Pry-7G1_.js +1 -0
  433. ksadk/studio/static/assets/architecture-7GRP2DOG-hXMiO_AU.js +1 -0
  434. ksadk/studio/static/assets/architectureDiagram-5GKGNRK7-B5EQysk6.js +36 -0
  435. ksadk/studio/static/assets/array-BifhSqXX.js +1 -0
  436. ksadk/studio/static/assets/blockDiagram-I7D4REHJ-CFmPl1de.js +129 -0
  437. ksadk/studio/static/assets/c4Diagram-7LVT6UL2-BRrGu8qt.js +38 -0
  438. ksadk/studio/static/assets/channel-BqO-gtEw.js +1 -0
  439. ksadk/studio/static/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  440. ksadk/studio/static/assets/chunk-4HAMMTFA-D220vUtq.js +62 -0
  441. ksadk/studio/static/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  442. ksadk/studio/static/assets/chunk-75Z2AOVW-C8lLv_qc.js +2 -0
  443. ksadk/studio/static/assets/chunk-DU6HZSFF-C4DtVpCu.js +128 -0
  444. ksadk/studio/static/assets/chunk-F27PBJKO-Cpw14SNG.js +1 -0
  445. ksadk/studio/static/assets/chunk-FOHPRMQF-DHwB1DNv.js +161 -0
  446. ksadk/studio/static/assets/chunk-GMAD6QVW-DiQA-u2N.js +72 -0
  447. ksadk/studio/static/assets/chunk-GVQU2GXP-DUdNBUCi.js +1 -0
  448. ksadk/studio/static/assets/chunk-IMKFNOWR-BSlbjT_b.js +231 -0
  449. ksadk/studio/static/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  450. ksadk/studio/static/assets/chunk-L3NEJ4N5-Db8Ab22W.js +1 -0
  451. ksadk/studio/static/assets/chunk-OSK3NFVY-M2m6G3j4.js +10 -0
  452. ksadk/studio/static/assets/chunk-P2QGCYS3-DezaQxiK.js +1 -0
  453. ksadk/studio/static/assets/chunk-POPQ4Y6H-DOpufJ3t.js +1 -0
  454. ksadk/studio/static/assets/chunk-PWAF6VOD-CfLb1gSc.js +1 -0
  455. ksadk/studio/static/assets/chunk-SHT3W25Y-DAyIVp5H.js +168 -0
  456. ksadk/studio/static/assets/chunk-SVP7TREG-DulomOrS.js +88 -0
  457. ksadk/studio/static/assets/chunk-TICWLB2K-bHJYoflj.js +206 -0
  458. ksadk/studio/static/assets/chunk-XXDRQBXY-7PcdR2B1.js +1 -0
  459. ksadk/studio/static/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  460. ksadk/studio/static/assets/classDiagram-ZZMXUADV-D_bW8JjA.js +1 -0
  461. ksadk/studio/static/assets/classDiagram-v2-VYDZK3BY-D_bW8JjA.js +1 -0
  462. ksadk/studio/static/assets/cose-bilkent-JH36ORCC-snL88wkX.js +1 -0
  463. ksadk/studio/static/assets/cynefin-OW5HDTMX-Bd1YhRo1.js +1 -0
  464. ksadk/studio/static/assets/cynefinDiagram-5FMLGOSQ-BBrIG_mI.js +62 -0
  465. ksadk/studio/static/assets/cytoscape.esm-CGd-uY3x.js +321 -0
  466. ksadk/studio/static/assets/dagre-GXQ25YYZ-DtF1nGdY.js +4 -0
  467. ksadk/studio/static/assets/dagre-PrKaheQc.js +1 -0
  468. ksadk/studio/static/assets/defaultLocale-BFoDCU3G.js +1 -0
  469. ksadk/studio/static/assets/diagram-S7CK7UJ4-0BZ98mya.js +30 -0
  470. ksadk/studio/static/assets/diagram-UQ7AKVKN-6mLKEo6L.js +41 -0
  471. ksadk/studio/static/assets/diagram-VSXAHHWV-CJBIaiZ2.js +3 -0
  472. ksadk/studio/static/assets/diagram-VX7I27RA-BohLEE1C.js +24 -0
  473. ksadk/studio/static/assets/diagram-Z3DM3KII-D8KIGonG.js +24 -0
  474. ksadk/studio/static/assets/dist-v5Q1xZ2K.js +1 -0
  475. ksadk/studio/static/assets/ebnfDiagram-PWID7BFC-C99HU61t.js +1 -0
  476. ksadk/studio/static/assets/erDiagram-RLTQ6QDP-DLGry2Ap.js +99 -0
  477. ksadk/studio/static/assets/eventmodeling-NTZA5JFV-B1_5Ac8j.js +1 -0
  478. ksadk/studio/static/assets/flowDiagram-HODETNUW-DD5hmicn.js +1 -0
  479. ksadk/studio/static/assets/ganttDiagram-EL5Y4UJY-BPJrVLSG.js +292 -0
  480. ksadk/studio/static/assets/gitGraph-4MIJSDKK-Cy_ZQBGi.js +1 -0
  481. ksadk/studio/static/assets/gitGraphDiagram-WWUBYQGX-CZCUz4MT.js +106 -0
  482. ksadk/studio/static/assets/graphlib-DS17s2tU.js +1 -0
  483. ksadk/studio/static/assets/index-CeHL9ARV.css +2 -0
  484. ksadk/studio/static/assets/index-D9QEcVpL.js +320 -0
  485. ksadk/studio/static/assets/info-A6RAGUB7-DBzDQB_I.js +1 -0
  486. ksadk/studio/static/assets/infoDiagram-27XIBGKW-B-sq07Hi.js +2 -0
  487. ksadk/studio/static/assets/init-C-OQMol4.js +1 -0
  488. ksadk/studio/static/assets/ishikawaDiagram-5VMMS53U-CljI2wyf.js +70 -0
  489. ksadk/studio/static/assets/journeyDiagram-3NMN7TZE-D50Fo2sL.js +139 -0
  490. ksadk/studio/static/assets/kanban-definition-UXKFOSKX-DimfOTam.js +89 -0
  491. ksadk/studio/static/assets/katex-DhON-x74.js +257 -0
  492. ksadk/studio/static/assets/line-CX5BOOUP.js +1 -0
  493. ksadk/studio/static/assets/linear-BCxN9JLv.js +1 -0
  494. ksadk/studio/static/assets/mermaid-parser.core-BdCz8fkS.js +7 -0
  495. ksadk/studio/static/assets/mindmap-definition-YA3MSWOX-Bizzqy5R.js +96 -0
  496. ksadk/studio/static/assets/ordinal-BDEzSJ7C.js +1 -0
  497. ksadk/studio/static/assets/packet-AYTQ26CC-gPln-_kG.js +1 -0
  498. ksadk/studio/static/assets/path-fybaL0A-.js +1 -0
  499. ksadk/studio/static/assets/pegDiagram-XKGWAZYB-Dy3fB-gW.js +1 -0
  500. ksadk/studio/static/assets/pie-WAS4IAKB-qZdHMr7B.js +1 -0
  501. ksadk/studio/static/assets/pieDiagram-E7YTZNPT-B9mrFSDF.js +39 -0
  502. ksadk/studio/static/assets/quadrantDiagram-AXDQQJYC-NzUfCbMR.js +7 -0
  503. ksadk/studio/static/assets/radar-RG4KPBEZ-BZlnaea9.js +1 -0
  504. ksadk/studio/static/assets/railroad-74A4TZTK-Ce3__H9c.js +1 -0
  505. ksadk/studio/static/assets/railroad-abnf-HS5TGJTU-DqQn7EzD.js +1 -0
  506. ksadk/studio/static/assets/railroad-ebnf-LZEXJU2U-B55kCA8F.js +1 -0
  507. ksadk/studio/static/assets/railroad-peg-WCYAUIDC-DfQnvkHd.js +1 -0
  508. ksadk/studio/static/assets/railroadDiagram-O6MQD6OU-DnhMjZCk.js +1 -0
  509. ksadk/studio/static/assets/requirementDiagram-BXWQKSXE-BF3d103U.js +84 -0
  510. ksadk/studio/static/assets/rolldown-runtime-Dy4uBu1J-DK3Fl9T5.js +1 -0
  511. ksadk/studio/static/assets/rough.esm-Dy-Kn_BL.js +1 -0
  512. ksadk/studio/static/assets/sankeyDiagram-P5KCCOFB-Bo-JTwt0.js +40 -0
  513. ksadk/studio/static/assets/sequenceDiagram-WJ2MYXX4-Dh4wZtUm.js +162 -0
  514. ksadk/studio/static/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
  515. ksadk/studio/static/assets/src-SFn9St9H.js +1 -0
  516. ksadk/studio/static/assets/stateDiagram-D77RDMKH-DH774G6d.js +1 -0
  517. ksadk/studio/static/assets/stateDiagram-v2-MP3YSRHH-DZvVWPBo.js +1 -0
  518. ksadk/studio/static/assets/swimlanes-42K2YHIH-Bl13OvTX.js +1 -0
  519. ksadk/studio/static/assets/swimlanesDiagram-VR7AAH4N-BnFOTALJ.js +8 -0
  520. ksadk/studio/static/assets/timeline-definition-24CTP7MA-DxmSSjRB.js +120 -0
  521. ksadk/studio/static/assets/treeView-Q6P3EWNA-D5Nn9Swz.js +1 -0
  522. ksadk/studio/static/assets/treemap-WGGIJYW6-CgHgnQhE.js +1 -0
  523. ksadk/studio/static/assets/vennDiagram-4TSXK5OY-B9QEXcJq.js +34 -0
  524. ksadk/studio/static/assets/wardley-WFR3VGLG-DXrK2RRY.js +1 -0
  525. ksadk/studio/static/assets/wardleyDiagram-VM6X3IG4-Ba1AOWoe.js +78 -0
  526. ksadk/studio/static/assets/xychartDiagram-S5SC5T6Z-Cr5pt5Y-.js +7 -0
  527. ksadk/studio/static/index.html +2 -2
  528. ksadk/studio/validator.py +1 -1
  529. ksadk/studio/workspace.py +1 -0
  530. ksadk/version.py +1 -1
  531. ksadk_runtime_common/workspace_files/path_utils.py +41 -0
  532. agentengine_sdk_python-0.8.2.dist-info/RECORD +0 -778
  533. ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +0 -1
  534. ksadk/server/static/assets/channel-4cQHKtx1.js +0 -1
  535. ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +0 -1
  536. ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +0 -1
  537. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +0 -1
  538. ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +0 -1
  539. ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +0 -1
  540. ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +0 -1
  541. ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +0 -1
  542. ksadk/server/static/assets/index-8ipRcQ-M.js +0 -240
  543. ksadk/server/static/assets/index-ByFCqjlh.css +0 -2
  544. ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +0 -1
  545. ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +0 -1
  546. ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +0 -1
  547. ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +0 -1
  548. ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +0 -1
  549. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +0 -1
  550. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +0 -1
  551. ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +0 -1
  552. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +0 -1
  553. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +0 -8
  554. ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +0 -1
  555. ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +0 -1
  556. ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +0 -1
  557. ksadk/studio/static/assets/PrismRenderer-IYN-ffww.js +0 -3
  558. ksadk/studio/static/assets/index-BLpcdrgW.css +0 -1
  559. ksadk/studio/static/assets/index-CYekR2Xv.js +0 -260
  560. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.4.dist-info}/WHEEL +0 -0
  561. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.4.dist-info}/entry_points.txt +0 -0
  562. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.4.dist-info}/licenses/LICENSE +0 -0
  563. {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
+ ]