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,1300 @@
1
+ """Transactional DeepSeek Harness profile plugin bridge.
2
+
3
+ DSH remains the native owner of its plugin packages and composed runtime. This
4
+ module only manages one isolated Profile through the public ``dsh plugin`` and
5
+ ``--dump-config`` commands, then projects non-secret inventory into KsADK.
6
+ It deliberately does not import Cordis or execute DSH plugin code in PluginHost.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import os
14
+ import re
15
+ import shutil
16
+ import signal
17
+ import stat
18
+ import subprocess
19
+ import tempfile
20
+ import threading
21
+ from collections.abc import Callable, Mapping, Sequence
22
+ from contextlib import contextmanager
23
+ from dataclasses import dataclass
24
+ from pathlib import Path
25
+ from typing import Iterator, Literal, TypedDict
26
+
27
+ try:
28
+ import fcntl
29
+ except ImportError: # pragma: no cover - DSH production hosts are Unix
30
+ fcntl = None # type: ignore[assignment]
31
+
32
+ import yaml
33
+ from pydantic import BaseModel, ConfigDict
34
+
35
+ from ksadk.plugins.dsh_installation_digest import (
36
+ DshInstallationDigestError,
37
+ installation_digest,
38
+ )
39
+
40
+ _COMMAND_TIMEOUT_SECONDS = 120
41
+
42
+
43
+ def _to_camel(value: str) -> str:
44
+ head, *tail = value.split("_")
45
+ return head + "".join(part.capitalize() for part in tail)
46
+
47
+
48
+ class _DshModel(BaseModel):
49
+ model_config = ConfigDict(
50
+ alias_generator=_to_camel,
51
+ populate_by_name=True,
52
+ extra="forbid",
53
+ frozen=True,
54
+ )
55
+
56
+
57
+ class DshBridgeHost(_DshModel):
58
+ host_id: Literal["deepseek-harness"] = "deepseek-harness"
59
+ version: str
60
+ protocol: Literal["dsh.profile/v1"] = "dsh.profile/v1"
61
+ available: Literal[True] = True
62
+
63
+
64
+ class DshPluginInventory(_DshModel):
65
+ ecosystem: Literal["dsh"] = "dsh"
66
+ integration_mode: Literal["bridged"] = "bridged"
67
+ profile: str
68
+ name: str
69
+ display_name: str
70
+ description: str = ""
71
+ version: str
72
+ requested_spec: str
73
+ source_digest: str | None = None
74
+ source_kind: Literal["directory", "tgz"] | None = None
75
+ client_extension: bool = False
76
+ settings_integration: bool = False
77
+ installed: Literal[True] = True
78
+ enabled: bool
79
+ permissions_declared: Literal[False] = False
80
+ risk_disclosures: tuple[str, ...] = (
81
+ "DSH packages and install scripts run with the native host user privileges.",
82
+ "DSH bundle manifests do not declare a complete runtime permission set.",
83
+ )
84
+
85
+
86
+ class DshProfileProjection(_DshModel):
87
+ profile: str
88
+ bundles: tuple[str, ...]
89
+ config_digest: str
90
+ config_bytes: int
91
+ host_version: str
92
+
93
+
94
+ class DshProfileBuildSnapshot(_DshModel):
95
+ """Credential-free identity of the actual local profile execution inputs."""
96
+
97
+ projection: DshProfileProjection
98
+ dependency_lock_digest: str
99
+ installation_digest: str
100
+
101
+ @property
102
+ def digest(self) -> str:
103
+ payload = json.dumps(self.model_dump(mode="json"), sort_keys=True, separators=(",", ":"))
104
+ return "sha256:" + hashlib.sha256(payload.encode()).hexdigest()
105
+
106
+
107
+ class DshBridgeError(RuntimeError):
108
+ """Base failure for one bounded DSH profile operation."""
109
+
110
+
111
+ class DshHostUnavailableError(DshBridgeError):
112
+ pass
113
+
114
+
115
+ class DshPluginNotFoundError(DshBridgeError):
116
+ pass
117
+
118
+
119
+ class DshPluginApprovalRequired(DshBridgeError):
120
+ pass
121
+
122
+
123
+ class DshPluginMutationError(DshBridgeError):
124
+ pass
125
+
126
+
127
+ class DshProfileRecoveryError(DshPluginMutationError):
128
+ """The owner must keep run admission suspended until profile recovery."""
129
+
130
+
131
+ class _CommandResult(_DshModel):
132
+ stdout: str = ""
133
+ stderr: str = ""
134
+
135
+
136
+ CommandRunner = Callable[[Sequence[str], Path, Mapping[str, str]], _CommandResult]
137
+
138
+ _PROFILE_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
139
+ _PACKAGE_NAME = re.compile(r"^(?:@[A-Za-z0-9._-]+/)?[A-Za-z0-9._-]+$")
140
+ _PINNED_REGISTRY_SOURCE = re.compile(
141
+ r"^(?:@[A-Za-z0-9._-]+/)?[A-Za-z0-9._-]+@"
142
+ r"\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$"
143
+ )
144
+ _HOST_VERSION = re.compile(r"\b(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b")
145
+ _STATE_FILE = ".ksadk-dsh-plugins.json"
146
+ _IMMUTABLE_SOURCE_DIR = "immutable-plugin-sources"
147
+ _SNAPSHOT_FILES = ("package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", _STATE_FILE)
148
+ _WEB_PROFILE_BUILTINS = ("@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app")
149
+ _MAX_JSON_BYTES = 2 * 1024 * 1024
150
+ _PROFILE_LOCK_DIR = "profile-locks"
151
+ _DSH_SUBPROCESS_ENV_KEYS = (
152
+ "PATH",
153
+ "HOME",
154
+ "USER",
155
+ "LOGNAME",
156
+ "SHELL",
157
+ "TMPDIR",
158
+ "TMP",
159
+ "TEMP",
160
+ "LANG",
161
+ "LANGUAGE",
162
+ "LC_ALL",
163
+ "LC_CTYPE",
164
+ "HTTP_PROXY",
165
+ "HTTPS_PROXY",
166
+ "ALL_PROXY",
167
+ "NO_PROXY",
168
+ "http_proxy",
169
+ "https_proxy",
170
+ "all_proxy",
171
+ "no_proxy",
172
+ "SSL_CERT_FILE",
173
+ "SSL_CERT_DIR",
174
+ "NODE_EXTRA_CA_CERTS",
175
+ "REQUESTS_CA_BUNDLE",
176
+ "CURL_CA_BUNDLE",
177
+ "NPM_CONFIG_REGISTRY",
178
+ "npm_config_registry",
179
+ "SYSTEMROOT",
180
+ "WINDIR",
181
+ "COMSPEC",
182
+ "PATHEXT",
183
+ )
184
+
185
+
186
+ def validate_dsh_registry_source(source: str) -> str:
187
+ """Return one exact, credential-free npm registry package coordinate."""
188
+
189
+ value = source.strip()
190
+ if not _PINNED_REGISTRY_SOURCE.fullmatch(value):
191
+ raise ValueError(
192
+ "DSH registry source must be <package>@<exact-semver>; "
193
+ "Git URLs, tags, and ranges are not allowed"
194
+ )
195
+ return value
196
+
197
+
198
+ def validate_dsh_registry_request(source: str) -> str:
199
+ """Accept a package name for latest resolution, or an exact coordinate."""
200
+ value = source.strip()
201
+ if len(value) <= 214 and not value.startswith("-") and _PACKAGE_NAME.fullmatch(value):
202
+ return value
203
+ return validate_dsh_registry_source(value)
204
+
205
+
206
+ def dsh_subprocess_environment(*, dsh_home: Path | None = None) -> dict[str, str]:
207
+ """Build the explicit environment inherited by DSH and pnpm children.
208
+
209
+ DSH bundles may execute package lifecycle scripts. They need process
210
+ discovery, a home/temp directory, locale, proxies and CA configuration,
211
+ but never receive arbitrary model, cloud, npm, SSH or host application
212
+ credentials from the parent process.
213
+ """
214
+
215
+ environment = {
216
+ name: os.environ[name] for name in _DSH_SUBPROCESS_ENV_KEYS if os.environ.get(name)
217
+ }
218
+ if "PATH" not in environment:
219
+ environment["PATH"] = os.defpath
220
+ if dsh_home is not None:
221
+ environment["DSH_HOME"] = str(dsh_home)
222
+ return environment
223
+
224
+
225
+ class _SourceReceipt(TypedDict):
226
+ digest: str
227
+ kind: Literal["directory", "tgz"]
228
+ artifact: str
229
+ dependency_spec: str
230
+
231
+
232
+ class _ProfileState(TypedDict):
233
+ order: list[str]
234
+ disabled: list[str]
235
+ sources: dict[str, _SourceReceipt]
236
+
237
+
238
+ @dataclass(frozen=True)
239
+ class _PreparedSource:
240
+ command_source: str
241
+ digest: str
242
+ kind: Literal["directory", "tgz"]
243
+ artifact: str
244
+
245
+
246
+ @dataclass(frozen=True)
247
+ class _ProfileSnapshot:
248
+ files: Mapping[str, bytes | None]
249
+ node_modules_existed: bool
250
+
251
+
252
+ class DshProfilePluginBridge:
253
+ """Manage DSH bundles in one isolated Profile with rollback and preflight."""
254
+
255
+ def __init__(
256
+ self,
257
+ *,
258
+ dsh_home: Path,
259
+ profile: str = "web",
260
+ dsh_command: Sequence[str] | None = None,
261
+ command_runner: CommandRunner | None = None,
262
+ cwd: Path | None = None,
263
+ ) -> None:
264
+ if not _PROFILE_NAME.fullmatch(profile):
265
+ raise ValueError("DSH profile must be a simple name without path separators")
266
+ if dsh_command is not None and not dsh_command:
267
+ raise ValueError("DSH command cannot be empty")
268
+ self._dsh_home = dsh_home.expanduser().resolve()
269
+ self._profile = profile
270
+ self._profile_root = self._dsh_home / "profiles" / profile
271
+ self._command = tuple(dsh_command) if dsh_command is not None else None
272
+ self._runner = command_runner or self._run_command
273
+ self._cwd = (cwd or Path.cwd()).resolve()
274
+ self._host: DshBridgeHost | None = None
275
+ self._lock = threading.RLock()
276
+ self._transaction_local = threading.local()
277
+
278
+ @property
279
+ def host(self) -> DshBridgeHost:
280
+ if self._host is None:
281
+ raise DshHostUnavailableError("DSH bridge is not started")
282
+ return self._host
283
+
284
+ def start(self) -> DshBridgeHost:
285
+ if self._host is not None:
286
+ return self._host
287
+ command = self._resolve_command()
288
+ result = self._invoke((*command, "--version"), cwd=self._cwd)
289
+ match = _HOST_VERSION.search(result.stdout or result.stderr)
290
+ if match is None:
291
+ raise DshHostUnavailableError("DSH host did not report a parseable version")
292
+ self._command = command
293
+ self._host = DshBridgeHost(version=match.group(1))
294
+ return self._host
295
+
296
+ def close(self) -> None:
297
+ self._host = None
298
+
299
+ def __enter__(self) -> "DshProfilePluginBridge":
300
+ self.start()
301
+ return self
302
+
303
+ def __exit__(self, _exc_type: object, _exc: object, _tb: object) -> None:
304
+ self.close()
305
+
306
+ def list_plugins(self) -> tuple[DshPluginInventory, ...]:
307
+ with self._profile_transaction(exclusive=False):
308
+ return self._list_plugins_locked()
309
+
310
+ def _list_plugins_locked(self) -> tuple[DshPluginInventory, ...]:
311
+ self._ensure_started()
312
+ if not self._manifest_path().is_file():
313
+ return ()
314
+ manifest = self._read_manifest()
315
+ state = self._read_state(manifest)
316
+ self._verify_source_receipts(manifest, state)
317
+ active = set(self._bundles(manifest))
318
+ items: list[DshPluginInventory] = []
319
+ for name, requested_spec in self._dependencies(manifest).items():
320
+ package = self._read_package(name)
321
+ if package is None or self._bundle_patch(package) is None:
322
+ continue
323
+ dsh = package.get("dsh")
324
+ client = dsh.get("client") if isinstance(dsh, dict) else None
325
+ inject = client.get("inject") if isinstance(client, dict) else None
326
+ client_inject = (
327
+ tuple(item for item in inject if isinstance(item, str))
328
+ if isinstance(inject, list)
329
+ else ()
330
+ )
331
+ receipt = state["sources"].get(name)
332
+ items.append(
333
+ DshPluginInventory(
334
+ profile=self._profile,
335
+ name=name,
336
+ display_name=self._string(package.get("displayName"))
337
+ or self._string(package.get("name"))
338
+ or name,
339
+ description=self._string(package.get("description")),
340
+ version=self._string(package.get("version")),
341
+ requested_spec=requested_spec,
342
+ source_digest=receipt["digest"] if receipt is not None else None,
343
+ source_kind=receipt["kind"] if receipt is not None else None,
344
+ client_extension=isinstance(client, dict),
345
+ settings_integration=any(
346
+ "client-ui-settings" in item or "client-ui-slots" in item
347
+ for item in client_inject
348
+ ),
349
+ enabled=name in active and name not in set(state["disabled"]),
350
+ )
351
+ )
352
+ return tuple(sorted(items, key=lambda item: (item.display_name.casefold(), item.name)))
353
+
354
+ def get_plugin(self, name: str) -> DshPluginInventory:
355
+ self._validate_package_name(name)
356
+ matches = [item for item in self.list_plugins() if item.name == name]
357
+ if not matches:
358
+ raise DshPluginNotFoundError(f"DSH plugin {name!r} is not installed")
359
+ return matches[0]
360
+
361
+ def install_plugin(
362
+ self,
363
+ source: str,
364
+ *,
365
+ accept_host_permissions: bool = False,
366
+ ) -> DshPluginInventory:
367
+ if not accept_host_permissions:
368
+ raise DshPluginApprovalRequired(
369
+ "DSH packages can run install scripts and runtime code with host privileges; "
370
+ "explicit approval is required"
371
+ )
372
+ self._validate_source(source)
373
+ source = self._resolve_install_source(source.strip())
374
+ with self._profile_transaction(exclusive=True):
375
+ self._require_package_mutation_rollback(new_profile_allowed=True)
376
+ snapshot = self._snapshot()
377
+ before = (
378
+ self._dependencies(self._read_manifest()) if self._manifest_path().is_file() else {}
379
+ )
380
+ try:
381
+ if self._manifest_path().is_file():
382
+ existing_manifest = self._read_manifest()
383
+ self._verify_source_receipts(
384
+ existing_manifest, self._read_state(existing_manifest)
385
+ )
386
+ prepared = self._prepare_source(source)
387
+ self._plugin_command("add", prepared.command_source)
388
+ manifest = self._read_manifest()
389
+ added = [name for name in self._dependencies(manifest) if name not in before]
390
+ if len(added) != 1:
391
+ raise DshPluginMutationError(
392
+ "installing one DSH plugin must add exactly one direct dependency"
393
+ )
394
+ name = added[0]
395
+ self._require_bundle(name)
396
+ state = self._read_state(manifest)
397
+ state["order"] = [item for item in state["order"] if item != name] + [name]
398
+ if name not in state["disabled"]:
399
+ state["disabled"].append(name)
400
+ if prepared.digest:
401
+ state["sources"][name] = {
402
+ "digest": prepared.digest,
403
+ "kind": prepared.kind,
404
+ "artifact": prepared.artifact,
405
+ "dependency_spec": self._dependencies(manifest)[name],
406
+ }
407
+ self._write_state(state)
408
+ self._write_active_bundles(manifest, state)
409
+ self._preflight()
410
+ return self.get_plugin(name)
411
+ except BaseException as error:
412
+ self._rollback(snapshot, error)
413
+ raise
414
+
415
+ def set_enabled(self, name: str, *, enabled: bool) -> DshPluginInventory:
416
+ self._validate_package_name(name)
417
+ with self._profile_transaction(exclusive=True):
418
+ snapshot = self._snapshot()
419
+ try:
420
+ manifest = self._read_manifest()
421
+ if name not in self._dependencies(manifest):
422
+ raise DshPluginNotFoundError(f"DSH plugin {name!r} is not installed")
423
+ self._require_bundle(name)
424
+ state = self._read_state(manifest)
425
+ self._verify_source_receipts(manifest, state, names=(name,))
426
+ if name not in state["order"]:
427
+ state["order"].append(name)
428
+ if enabled:
429
+ state["disabled"] = [item for item in state["disabled"] if item != name]
430
+ elif name not in state["disabled"]:
431
+ state["disabled"].append(name)
432
+ self._write_state(state)
433
+ self._write_active_bundles(manifest, state)
434
+ self._preflight()
435
+ return self.get_plugin(name)
436
+ except BaseException as error:
437
+ self._rollback(snapshot, error)
438
+ raise
439
+
440
+ def update_plugin(
441
+ self,
442
+ name: str,
443
+ *,
444
+ source: str | None = None,
445
+ accept_host_permissions: bool = False,
446
+ ) -> DshPluginInventory:
447
+ self._validate_package_name(name)
448
+ if not accept_host_permissions:
449
+ raise DshPluginApprovalRequired(
450
+ "updating a DSH package requires host permission approval"
451
+ )
452
+ with self._profile_transaction(exclusive=True):
453
+ self._require_package_mutation_rollback(new_profile_allowed=False)
454
+ snapshot = self._snapshot()
455
+ try:
456
+ self.get_plugin(name)
457
+ before_manifest = self._read_manifest()
458
+ before_state = self._read_state(before_manifest)
459
+ self._verify_source_receipts(before_manifest, before_state, names=(name,))
460
+ existing_source = before_state["sources"].get(name)
461
+ if existing_source is not None and source is None:
462
+ raise DshPluginMutationError(
463
+ "updating an immutable local DSH plugin requires an explicit source"
464
+ )
465
+ prepared = None
466
+ if source is not None:
467
+ self._validate_source(source)
468
+ prepared = self._prepare_source(source)
469
+ if not prepared.digest:
470
+ raise DshPluginMutationError(
471
+ "an explicit update source must be a local directory or tgz"
472
+ )
473
+ self._plugin_command("add", prepared.command_source)
474
+ else:
475
+ self._plugin_command("update", name)
476
+ manifest = self._read_manifest()
477
+ if prepared is not None:
478
+ changed = {
479
+ dependency
480
+ for dependency in (
481
+ set(self._dependencies(before_manifest))
482
+ | set(self._dependencies(manifest))
483
+ )
484
+ if self._dependencies(before_manifest).get(dependency)
485
+ != self._dependencies(manifest).get(dependency)
486
+ }
487
+ if changed != {name}:
488
+ raise DshPluginMutationError(
489
+ "updated local source must replace exactly the selected plugin"
490
+ )
491
+ self._require_bundle(name)
492
+ state = self._read_state(manifest)
493
+ if prepared is not None:
494
+ if name not in self._dependencies(manifest):
495
+ raise DshPluginMutationError(
496
+ "updated local source did not preserve the plugin package name"
497
+ )
498
+ state["sources"][name] = {
499
+ "digest": prepared.digest,
500
+ "kind": prepared.kind,
501
+ "artifact": prepared.artifact,
502
+ "dependency_spec": self._dependencies(manifest)[name],
503
+ }
504
+ self._write_state(state)
505
+ self._write_active_bundles(manifest, state)
506
+ self._verify_source_receipts(manifest, state, names=(name,))
507
+ self._preflight()
508
+ return self.get_plugin(name)
509
+ except BaseException as error:
510
+ self._rollback(snapshot, error)
511
+ raise
512
+
513
+ def uninstall_plugin(self, name: str) -> None:
514
+ self._validate_package_name(name)
515
+ with self._profile_transaction(exclusive=True):
516
+ self._require_package_mutation_rollback(new_profile_allowed=False)
517
+ snapshot = self._snapshot()
518
+ try:
519
+ self.get_plugin(name)
520
+ self._plugin_command("remove", name)
521
+ manifest = self._read_manifest()
522
+ state = self._read_state(manifest)
523
+ state["order"] = [item for item in state["order"] if item != name]
524
+ state["disabled"] = [item for item in state["disabled"] if item != name]
525
+ state["sources"].pop(name, None)
526
+ self._write_state(state)
527
+ self._write_active_bundles(manifest, state)
528
+ self._preflight()
529
+ if any(item.name == name for item in self.list_plugins()):
530
+ raise DshPluginMutationError("DSH host still reports the removed plugin")
531
+ except BaseException as error:
532
+ self._rollback(snapshot, error)
533
+ raise
534
+
535
+ def project_profile(self) -> DshProfileProjection:
536
+ """Validate the native profile and expose only its digest, never raw config."""
537
+
538
+ with self._profile_transaction(exclusive=False):
539
+ self._ensure_started()
540
+ manifest = self._read_manifest()
541
+ self._verify_source_receipts(manifest, self._read_state(manifest))
542
+ result = self._preflight()
543
+ payload = result.stdout.encode("utf-8")
544
+ return DshProfileProjection(
545
+ profile=self._profile,
546
+ bundles=tuple(self._bundles(manifest)),
547
+ config_digest=f"sha256:{hashlib.sha256(payload).hexdigest()}",
548
+ config_bytes=len(payload),
549
+ host_version=self.host.version,
550
+ )
551
+
552
+ def migrate_to_isolated_layout(
553
+ self,
554
+ *,
555
+ accept_host_permissions: bool = False,
556
+ recover_external_dependency_links: bool = False,
557
+ ) -> None:
558
+ """Migrate a stopped profile; the caller owns Core/admission suspension.
559
+
560
+ Keep an independent byte copy for rollback: a frozen reinstall of the
561
+ old layout can itself hit the pinned pnpm shutdown defect. Studio-owned
562
+ profiles may also repair an isolated tree polluted by a legacy Core
563
+ module-fallback cache.
564
+ """
565
+ if not accept_host_permissions:
566
+ raise DshPluginApprovalRequired("Profile migration may run package install scripts")
567
+ with self._profile_transaction(exclusive=True):
568
+ self._require_package_mutation_rollback(new_profile_allowed=False)
569
+ manifest = self._read_manifest()
570
+ self._verify_source_receipts(manifest, self._read_state(manifest))
571
+ settings_path = self._profile_root / "pnpm-workspace.yaml"
572
+ settings = yaml.safe_load(settings_path.read_text(encoding="utf-8"))
573
+ if not isinstance(settings, dict):
574
+ raise DshPluginMutationError("Invalid package-manager settings")
575
+ node_linker = settings.get("nodeLinker")
576
+ if node_linker not in {"hoisted", "isolated"}:
577
+ raise DshPluginMutationError(
578
+ "Only hoisted or isolated profiles can use this migration"
579
+ )
580
+ modules = self._profile_root / "node_modules"
581
+ if modules.is_symlink() or not modules.is_dir():
582
+ raise DshPluginMutationError("Profile installation is not a real directory")
583
+ # A normal migration only accepts a closed tree. Studio may repair
584
+ # its own legacy hoisted Profile when pnpm linked a dependency to
585
+ # an ambient store: move that tree atomically instead of reading or
586
+ # following the external target, then reinstall from the frozen lock.
587
+ moved_unsafe_tree = False
588
+ try:
589
+ original_digest = installation_digest(modules)
590
+ except DshInstallationDigestError as error:
591
+ if (
592
+ not recover_external_dependency_links
593
+ or str(error) != "DSH dependency link escapes the installed tree"
594
+ ):
595
+ raise
596
+ original_digest = None
597
+ moved_unsafe_tree = True
598
+ if node_linker == "isolated" and not moved_unsafe_tree:
599
+ self._preflight()
600
+ return
601
+ fallback = self._profile_root / ".dsh-module-fallback"
602
+ if fallback.exists() and (fallback.is_symlink() or not fallback.is_dir()):
603
+ raise DshPluginMutationError("DSH module fallback is not a real directory")
604
+ snapshot = self._snapshot()
605
+ root = tempfile.mkdtemp(prefix=".layout-backup-", dir=self._profile_root)
606
+ preserve_backup = False
607
+ try:
608
+ backup = Path(root) / "node_modules"
609
+ fallback_backup = Path(root) / "module-fallback"
610
+ if moved_unsafe_tree:
611
+ os.replace(modules, backup)
612
+ else:
613
+ shutil.copytree(modules, backup, symlinks=True)
614
+ if installation_digest(backup) != original_digest:
615
+ raise DshPluginMutationError("Profile installation backup did not verify")
616
+ fallback_moved = fallback.is_dir()
617
+ if fallback_moved:
618
+ os.replace(fallback, fallback_backup)
619
+ original_files = Path(root) / "profile-files"
620
+ original_files.mkdir(mode=0o700)
621
+ for name, content in snapshot.files.items():
622
+ if content is not None:
623
+ original_path = original_files / name
624
+ original_path.write_bytes(content)
625
+ original_path.chmod(0o600)
626
+ try:
627
+ if not moved_unsafe_tree:
628
+ shutil.rmtree(modules)
629
+ settings["nodeLinker"] = "isolated"
630
+ settings_path.write_text(yaml.safe_dump(settings), encoding="utf-8")
631
+ settings_path.chmod(0o600)
632
+ self._plugin_command("install", "--frozen-lockfile")
633
+ current_lock = (self._profile_root / "pnpm-lock.yaml").read_bytes()
634
+ if current_lock != snapshot.files["pnpm-lock.yaml"]:
635
+ raise DshPluginMutationError("Layout migration changed the dependency lock")
636
+ # Native reconcile can re-enable previously disabled bundles.
637
+ for name in ("package.json", _STATE_FILE):
638
+ content = snapshot.files[name]
639
+ path = self._profile_root / name
640
+ if content is None:
641
+ path.unlink(missing_ok=True)
642
+ else:
643
+ path.write_bytes(content)
644
+ path.chmod(0o600)
645
+ self._verify_source_receipts(manifest, self._read_state(manifest))
646
+ self._preflight()
647
+ except BaseException as error:
648
+ try:
649
+ if modules.is_symlink():
650
+ modules.unlink()
651
+ elif modules.exists():
652
+ shutil.rmtree(modules)
653
+ os.replace(backup, modules)
654
+ if fallback.is_symlink():
655
+ fallback.unlink()
656
+ elif fallback.exists():
657
+ shutil.rmtree(fallback)
658
+ if fallback_moved:
659
+ os.replace(fallback_backup, fallback)
660
+ self._rollback(snapshot, error, reinstall=False)
661
+ except BaseException as recovery_error:
662
+ preserve_backup = True
663
+ raise DshProfileRecoveryError(
664
+ "Profile layout recovery failed; retain the layout backup for recovery"
665
+ ) from recovery_error
666
+ raise
667
+ finally:
668
+ if not preserve_backup:
669
+ shutil.rmtree(root)
670
+
671
+ def snapshot_for_build(self) -> DshProfileBuildSnapshot:
672
+ """Capture exact local inputs under the same profile mutation lock."""
673
+ with self._profile_transaction(exclusive=False):
674
+ projection = self.project_profile()
675
+ lock = self._profile_root / "pnpm-lock.yaml"
676
+ if lock.is_symlink() or not lock.is_file() or lock.stat().st_size > 16 * 1024 * 1024:
677
+ raise DshPluginMutationError("DSH Build requires a bounded dependency lockfile")
678
+ return DshProfileBuildSnapshot(
679
+ projection=projection,
680
+ dependency_lock_digest="sha256:" + hashlib.sha256(lock.read_bytes()).hexdigest(),
681
+ installation_digest=installation_digest(self._profile_root / "node_modules"),
682
+ )
683
+
684
+ def verify_build_snapshot(self, expected: DshProfileBuildSnapshot) -> None:
685
+ if self.snapshot_for_build() != expected:
686
+ raise DshPluginMutationError("DSH installed profile no longer matches the Build")
687
+
688
+ def _resolve_command(self) -> tuple[str, ...]:
689
+ if self._command is not None:
690
+ return self._command
691
+ executable = shutil.which("dsh")
692
+ if executable is None:
693
+ raise DshHostUnavailableError(
694
+ "DSH host is not installed; configure an exact DSH executable "
695
+ "before using the bridge"
696
+ )
697
+ return (executable,)
698
+
699
+ def _ensure_started(self) -> None:
700
+ if self._host is None or self._command is None:
701
+ raise DshHostUnavailableError("DSH bridge is not started")
702
+
703
+ @contextmanager
704
+ def _profile_transaction(self, *, exclusive: bool) -> Iterator[None]:
705
+ """Serialize one Profile snapshot across bridge objects and processes."""
706
+
707
+ with self._lock:
708
+ depth = int(getattr(self._transaction_local, "depth", 0))
709
+ held_exclusive = bool(getattr(self._transaction_local, "exclusive", False))
710
+ if depth:
711
+ if exclusive and not held_exclusive:
712
+ raise DshPluginMutationError(
713
+ "cannot upgrade a DSH profile read transaction to a mutation"
714
+ )
715
+ self._transaction_local.depth = depth + 1
716
+ try:
717
+ yield
718
+ finally:
719
+ self._transaction_local.depth = depth
720
+ return
721
+
722
+ if fcntl is None:
723
+ raise DshHostUnavailableError("DSH profile transactions require Unix file locking")
724
+ lock_root = self._dsh_home / _PROFILE_LOCK_DIR
725
+ lock_root.mkdir(parents=True, exist_ok=True, mode=0o700)
726
+ if lock_root.resolve() != lock_root:
727
+ raise DshPluginMutationError("DSH profile lock directory is not trusted")
728
+ lock_path = lock_root / f"{self._profile}.lock"
729
+ flags = os.O_CREAT | os.O_RDWR
730
+ if hasattr(os, "O_NOFOLLOW"):
731
+ flags |= os.O_NOFOLLOW
732
+ try:
733
+ descriptor = os.open(lock_path, flags, 0o600)
734
+ except OSError as error:
735
+ raise DshPluginMutationError(
736
+ "DSH profile transaction lock is unavailable"
737
+ ) from error
738
+ try:
739
+ metadata = os.fstat(descriptor)
740
+ if not stat.S_ISREG(metadata.st_mode):
741
+ raise DshPluginMutationError(
742
+ "DSH profile transaction lock is not a regular file"
743
+ )
744
+ os.fchmod(descriptor, 0o600)
745
+ fcntl.flock(
746
+ descriptor,
747
+ fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH,
748
+ )
749
+ self._transaction_local.depth = 1
750
+ self._transaction_local.exclusive = exclusive
751
+ try:
752
+ yield
753
+ finally:
754
+ self._transaction_local.depth = 0
755
+ self._transaction_local.exclusive = False
756
+ fcntl.flock(descriptor, fcntl.LOCK_UN)
757
+ finally:
758
+ os.close(descriptor)
759
+
760
+ def _invoke(self, command: Sequence[str], *, cwd: Path) -> _CommandResult:
761
+ environment = dsh_subprocess_environment(dsh_home=self._dsh_home)
762
+ try:
763
+ return self._runner(tuple(command), cwd, environment)
764
+ except DshBridgeError:
765
+ raise
766
+ except Exception as error:
767
+ raise DshHostUnavailableError("DSH host command could not be executed") from error
768
+
769
+ def _plugin_command(self, verb: str, value: str) -> _CommandResult:
770
+ self._ensure_started()
771
+ assert self._command is not None
772
+ initializing = verb == "add" and not self._manifest_path().exists()
773
+ # The pinned pnpm hoisted linker can start fetch workers after its
774
+ # shutdown pass during removal. Use its supported isolated layout for
775
+ # newly owned profiles, without changing an existing profile's policy.
776
+ options = ("--config.node-linker=isolated",) if initializing else ()
777
+ result = self._invoke(
778
+ (*self._command, "plugin", "--profile", self._profile, verb, value, *options),
779
+ cwd=self._cwd,
780
+ )
781
+ if initializing:
782
+ settings_path = self._profile_root / "pnpm-workspace.yaml"
783
+ settings = yaml.safe_load(settings_path.read_text(encoding="utf-8"))
784
+ if not isinstance(settings, dict):
785
+ raise DshPluginMutationError("DSH initialized invalid package-manager settings")
786
+ settings["nodeLinker"] = "isolated"
787
+ # This is a newly created profile inside the install transaction;
788
+ # any subsequent failure removes it through the existing rollback.
789
+ settings_path.write_text(yaml.safe_dump(settings), encoding="utf-8")
790
+ settings_path.chmod(0o600)
791
+ return result
792
+
793
+ def _preflight(self) -> _CommandResult:
794
+ self._ensure_started()
795
+ assert self._command is not None
796
+ return self._invoke(
797
+ (*self._command, "--profile", self._profile, "--dump-config"),
798
+ cwd=self._cwd,
799
+ )
800
+
801
+ def _manifest_path(self) -> Path:
802
+ return self._profile_root / "package.json"
803
+
804
+ def _read_manifest(self) -> dict[str, object]:
805
+ return self._read_json(self._manifest_path(), required=True)
806
+
807
+ def _read_package(self, name: str) -> dict[str, object] | None:
808
+ return self._read_json(
809
+ self._profile_root / "node_modules" / Path(*name.split("/")) / "package.json",
810
+ required=False,
811
+ )
812
+
813
+ @staticmethod
814
+ def _read_json(path: Path, *, required: bool) -> dict[str, object] | None:
815
+ try:
816
+ if path.stat().st_size > _MAX_JSON_BYTES:
817
+ raise DshBridgeError("DSH manifest exceeds the supported size limit")
818
+ payload = json.loads(path.read_text(encoding="utf-8"))
819
+ except FileNotFoundError:
820
+ if required:
821
+ raise DshBridgeError("required DSH profile manifest is unavailable") from None
822
+ return None
823
+ except (OSError, UnicodeError, json.JSONDecodeError) as error:
824
+ raise DshBridgeError("DSH profile contains an unreadable JSON manifest") from error
825
+ if not isinstance(payload, dict):
826
+ raise DshBridgeError("DSH JSON manifest must be an object")
827
+ return payload
828
+
829
+ @staticmethod
830
+ def _dependencies(manifest: Mapping[str, object]) -> dict[str, str]:
831
+ raw = manifest.get("dependencies")
832
+ if raw is None:
833
+ return {}
834
+ if not isinstance(raw, dict) or any(
835
+ not isinstance(name, str) or not isinstance(value, str) for name, value in raw.items()
836
+ ):
837
+ raise DshBridgeError("DSH profile dependencies are invalid")
838
+ return dict(raw)
839
+
840
+ @staticmethod
841
+ def _bundles(manifest: Mapping[str, object]) -> list[str]:
842
+ dsh = manifest.get("dsh")
843
+ profile = dsh.get("profile") if isinstance(dsh, dict) else None
844
+ bundles = profile.get("bundles") if isinstance(profile, dict) else None
845
+ if bundles is None:
846
+ return []
847
+ if not isinstance(bundles, list) or any(not isinstance(item, str) for item in bundles):
848
+ raise DshBridgeError("DSH profile bundle order is invalid")
849
+ return list(bundles)
850
+
851
+ @staticmethod
852
+ def _bundle_patch(package: Mapping[str, object]) -> str | None:
853
+ dsh = package.get("dsh")
854
+ bundle = dsh.get("bundle") if isinstance(dsh, dict) else None
855
+ patch = bundle.get("patch") if isinstance(bundle, dict) else None
856
+ return patch.strip() if isinstance(patch, str) and patch.strip() else None
857
+
858
+ def _require_bundle(self, name: str) -> None:
859
+ package = self._read_package(name)
860
+ if package is None:
861
+ raise DshPluginMutationError(f"{name} does not declare dsh.bundle.patch")
862
+ patch = self._bundle_patch(package)
863
+ root = (self._profile_root / "node_modules" / Path(*name.split("/"))).resolve()
864
+ if patch is None:
865
+ # Real upstream plugins may ship without a dsh.bundle.patch yet
866
+ # still be valid Cordis tool plugins (inject: ['tools']). Generate
867
+ # a minimal bundle patch so the dsh profile loader activates them.
868
+ # This is a compatibility shim, not a source modification: the
869
+ # plugin's own code is untouched.
870
+ patch_path = root / "cordis.patch.yml"
871
+ patch_path.write_text(
872
+ "- insert:\n - id: "
873
+ + name.rsplit("/", 1)[-1].replace("-", "_")
874
+ + "\n name: '" + name + "'\n",
875
+ encoding="utf-8",
876
+ )
877
+ package.setdefault("dsh", {}).setdefault("bundle", {})["patch"] = "./cordis.patch.yml"
878
+ self._write_json(root / "package.json", package)
879
+ patch = "./cordis.patch.yml"
880
+ relative = Path(patch)
881
+ if relative.is_absolute() or ".." in relative.parts:
882
+ raise DshPluginMutationError(f"{name} declares an unsafe bundle patch path")
883
+ target = (root / relative).resolve()
884
+ if not target.is_relative_to(root) or not target.is_file():
885
+ raise DshPluginMutationError(f"{name} bundle patch is unavailable")
886
+
887
+ def _read_state(self, manifest: Mapping[str, object]) -> _ProfileState:
888
+ dependencies = set(self._dependencies(manifest))
889
+ bundle_dependencies = [
890
+ name
891
+ for name in dependencies
892
+ if self._bundle_patch(self._read_package(name) or {}) is not None
893
+ ]
894
+ active = [name for name in self._bundles(manifest) if name in dependencies]
895
+ stored = self._read_json(self._profile_root / _STATE_FILE, required=False) or {}
896
+ order_raw = stored.get("order")
897
+ disabled_raw = stored.get("disabled")
898
+ sources_raw = stored.get("sources")
899
+ order = (
900
+ [item for item in order_raw if isinstance(item, str) and item in bundle_dependencies]
901
+ if isinstance(order_raw, list)
902
+ else []
903
+ )
904
+ for name in [*active, *bundle_dependencies]:
905
+ if name not in order:
906
+ order.append(name)
907
+ disabled = (
908
+ [item for item in disabled_raw if isinstance(item, str) and item in bundle_dependencies]
909
+ if isinstance(disabled_raw, list)
910
+ else [name for name in bundle_dependencies if name not in active]
911
+ )
912
+ sources: dict[str, _SourceReceipt] = {}
913
+ if isinstance(sources_raw, dict):
914
+ for name, raw in sources_raw.items():
915
+ if name not in bundle_dependencies:
916
+ continue
917
+ if not isinstance(raw, dict):
918
+ raise DshPluginMutationError("DSH local source receipt is invalid")
919
+ digest = raw.get("digest")
920
+ kind = raw.get("kind")
921
+ artifact = raw.get("artifact")
922
+ dependency_spec = raw.get("dependencySpec") or raw.get("dependency_spec")
923
+ if (
924
+ isinstance(digest, str)
925
+ and re.fullmatch(r"sha256:[0-9a-f]{64}", digest)
926
+ and kind in {"directory", "tgz"}
927
+ and isinstance(artifact, str)
928
+ and isinstance(dependency_spec, str)
929
+ ):
930
+ sources[name] = {
931
+ "digest": digest,
932
+ "kind": kind,
933
+ "artifact": artifact,
934
+ "dependency_spec": dependency_spec,
935
+ }
936
+ else:
937
+ raise DshPluginMutationError("DSH local source receipt is invalid")
938
+ return {"order": order, "disabled": disabled, "sources": sources}
939
+
940
+ def _write_state(self, state: _ProfileState) -> None:
941
+ self._write_json(
942
+ self._profile_root / _STATE_FILE,
943
+ {
944
+ "version": 2,
945
+ "order": list(state["order"]),
946
+ "disabled": list(state["disabled"]),
947
+ "sources": {
948
+ name: {
949
+ "digest": receipt["digest"],
950
+ "kind": receipt["kind"],
951
+ "artifact": receipt["artifact"],
952
+ "dependencySpec": receipt["dependency_spec"],
953
+ }
954
+ for name, receipt in sorted(state["sources"].items())
955
+ },
956
+ },
957
+ )
958
+
959
+ def _write_active_bundles(
960
+ self,
961
+ manifest: dict[str, object],
962
+ state: _ProfileState,
963
+ ) -> None:
964
+ dependencies = set(self._dependencies(manifest))
965
+ existing = self._bundles(manifest)
966
+ builtins = [name for name in existing if name not in dependencies]
967
+ disabled = set(state["disabled"])
968
+ enabled = [name for name in state["order"] if name in dependencies and name not in disabled]
969
+ dsh = manifest.get("dsh") if isinstance(manifest.get("dsh"), dict) else {}
970
+ assert isinstance(dsh, dict)
971
+ profile = dsh.get("profile") if isinstance(dsh.get("profile"), dict) else {}
972
+ assert isinstance(profile, dict)
973
+ profile["bundles"] = [*builtins, *enabled]
974
+ dsh["profile"] = profile
975
+ manifest["dsh"] = dsh
976
+ self._write_json(self._manifest_path(), manifest)
977
+
978
+ def _resolve_install_source(self, source: str) -> str:
979
+ if not _PACKAGE_NAME.fullmatch(source):
980
+ return source
981
+ # Resolve metadata before running package scripts. The mutable latest
982
+ # selector never reaches `dsh plugin add`; the exact version does.
983
+ result = self._invoke(
984
+ ("npm", "view", f"{source}@latest", "version", "--json"), cwd=self._cwd
985
+ )
986
+ try:
987
+ if len(result.stdout) > _MAX_JSON_BYTES:
988
+ raise ValueError("oversized registry response")
989
+ version = json.loads(result.stdout)
990
+ if not isinstance(version, str):
991
+ raise ValueError("expected one version")
992
+ return validate_dsh_registry_source(f"{source}@{version}")
993
+ except (ValueError, TypeError) as error:
994
+ raise DshPluginMutationError(
995
+ "npm latest did not resolve to one exact version"
996
+ ) from error
997
+
998
+ def _prepare_source(self, source: str) -> _PreparedSource:
999
+ value = source.strip()
1000
+ candidate = Path(value).expanduser()
1001
+ if not candidate.is_absolute() or not candidate.exists():
1002
+ return _PreparedSource(value, "", "tgz", "")
1003
+ local = candidate.resolve()
1004
+ with tempfile.TemporaryDirectory(prefix="agentengine-dsh-source-") as directory:
1005
+ if local.is_dir():
1006
+ # Import lazily: dsh_toolchain owns the pinned pnpm workflow and
1007
+ # imports this bridge for validation.
1008
+ from ksadk.plugins.dsh_toolchain import ( # noqa: PLC0415
1009
+ DshPluginDeveloper,
1010
+ DshToolchainManager,
1011
+ )
1012
+
1013
+ output = Path(directory)
1014
+ packed = DshPluginDeveloper(toolchain=DshToolchainManager()).pack(
1015
+ local, output_dir=output
1016
+ )
1017
+ archive = Path(packed.artifact)
1018
+ kind: Literal["directory", "tgz"] = "directory"
1019
+ elif local.is_file() and local.name.endswith(".tgz"):
1020
+ archive = local
1021
+ kind = "tgz"
1022
+ else:
1023
+ raise DshPluginMutationError(
1024
+ "local DSH plugin source must be a directory or .tgz archive"
1025
+ )
1026
+ try:
1027
+ content = archive.read_bytes()
1028
+ except OSError as error:
1029
+ raise DshPluginMutationError("local DSH plugin source is unreadable") from error
1030
+ digest_hex = hashlib.sha256(content).hexdigest()
1031
+ digest = f"sha256:{digest_hex}"
1032
+ root = (self._dsh_home / _IMMUTABLE_SOURCE_DIR / digest_hex).resolve()
1033
+ expected_parent = (self._dsh_home / _IMMUTABLE_SOURCE_DIR).resolve()
1034
+ if root.parent != expected_parent:
1035
+ raise DshPluginMutationError("immutable DSH source path escaped its store")
1036
+ target = root / "package.tgz"
1037
+ root.mkdir(parents=True, exist_ok=True, mode=0o700)
1038
+ if target.exists():
1039
+ if hashlib.sha256(target.read_bytes()).hexdigest() != digest_hex:
1040
+ raise DshPluginMutationError(
1041
+ "immutable DSH source store contains a digest collision"
1042
+ )
1043
+ else:
1044
+ temporary = root / f".package.{os.getpid()}.tmp"
1045
+ try:
1046
+ temporary.write_bytes(content)
1047
+ temporary.chmod(0o400)
1048
+ os.replace(temporary, target)
1049
+ finally:
1050
+ temporary.unlink(missing_ok=True)
1051
+ target.chmod(0o400)
1052
+ relative = target.relative_to(self._dsh_home).as_posix()
1053
+ return _PreparedSource(str(target), digest, kind, relative)
1054
+
1055
+ def _verify_source_receipts(
1056
+ self,
1057
+ manifest: Mapping[str, object],
1058
+ state: _ProfileState,
1059
+ *,
1060
+ names: Sequence[str] | None = None,
1061
+ ) -> None:
1062
+ dependencies = self._dependencies(manifest)
1063
+ for name, spec in dependencies.items():
1064
+ if _IMMUTABLE_SOURCE_DIR in spec and name not in state["sources"]:
1065
+ raise DshPluginMutationError(
1066
+ f"DSH plugin {name!r} is missing its immutable source receipt"
1067
+ )
1068
+ selected = names if names is not None else tuple(state["sources"])
1069
+ store = (self._dsh_home / _IMMUTABLE_SOURCE_DIR).resolve()
1070
+ for name in selected:
1071
+ receipt = state["sources"].get(name)
1072
+ if receipt is None:
1073
+ continue
1074
+ if dependencies.get(name) != receipt["dependency_spec"]:
1075
+ raise DshPluginMutationError(
1076
+ f"DSH plugin {name!r} dependency no longer matches its source receipt"
1077
+ )
1078
+ artifact = (self._dsh_home / receipt["artifact"]).resolve()
1079
+ if not artifact.is_relative_to(store) or not artifact.is_file():
1080
+ raise DshPluginMutationError(f"DSH plugin {name!r} immutable source is unavailable")
1081
+ try:
1082
+ actual = f"sha256:{hashlib.sha256(artifact.read_bytes()).hexdigest()}"
1083
+ except OSError as error:
1084
+ raise DshPluginMutationError(
1085
+ f"DSH plugin {name!r} immutable source is unreadable"
1086
+ ) from error
1087
+ if actual != receipt["digest"]:
1088
+ raise DshPluginMutationError(f"DSH plugin {name!r} immutable source digest changed")
1089
+
1090
+ @staticmethod
1091
+ def _write_json(path: Path, payload: object) -> None:
1092
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
1093
+ descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
1094
+ try:
1095
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
1096
+ json.dump(payload, handle, ensure_ascii=False, indent=2)
1097
+ handle.write("\n")
1098
+ handle.flush()
1099
+ os.fsync(handle.fileno())
1100
+ os.chmod(temporary, 0o600)
1101
+ os.replace(temporary, path)
1102
+ finally:
1103
+ try:
1104
+ os.unlink(temporary)
1105
+ except FileNotFoundError:
1106
+ pass
1107
+
1108
+ def _snapshot(self) -> _ProfileSnapshot:
1109
+ files: dict[str, bytes | None] = {}
1110
+ for name in _SNAPSHOT_FILES:
1111
+ path = self._profile_root / name
1112
+ try:
1113
+ files[name] = path.read_bytes()
1114
+ except FileNotFoundError:
1115
+ files[name] = None
1116
+ return _ProfileSnapshot(
1117
+ files=files,
1118
+ node_modules_existed=(self._profile_root / "node_modules").exists(),
1119
+ )
1120
+
1121
+ def _require_package_mutation_rollback(self, *, new_profile_allowed: bool) -> None:
1122
+ """Reject package mutations unless their filesystem effects are reversible."""
1123
+
1124
+ if not self._profile_root.exists():
1125
+ if new_profile_allowed:
1126
+ return
1127
+ raise DshPluginNotFoundError(f"DSH profile {self._profile!r} is not initialized")
1128
+ if not self._manifest_path().is_file():
1129
+ raise DshPluginMutationError(
1130
+ "existing DSH profile directory has no package manifest; refusing to mutate it"
1131
+ )
1132
+ if not (self._profile_root / "pnpm-lock.yaml").is_file():
1133
+ if new_profile_allowed and self._is_pristine_official_web_profile():
1134
+ return
1135
+ raise DshPluginMutationError(
1136
+ "existing DSH profile has no pnpm lockfile, so package rollback is unavailable"
1137
+ )
1138
+
1139
+ def _is_pristine_official_web_profile(self) -> bool:
1140
+ """Recognize only the untouched profile generated by ``dsh --profile web``."""
1141
+
1142
+ if self._profile != "web" or (self._profile_root / "node_modules").exists():
1143
+ return False
1144
+ if (self._profile_root / _STATE_FILE).exists():
1145
+ return False
1146
+ manifest = self._read_manifest()
1147
+ return (
1148
+ self._dependencies(manifest) == {}
1149
+ and tuple(self._bundles(manifest)) == _WEB_PROFILE_BUILTINS
1150
+ and (self._profile_root / "pnpm-workspace.yaml").is_file()
1151
+ )
1152
+
1153
+ def _rollback(
1154
+ self, snapshot: _ProfileSnapshot, original: BaseException, *, reinstall: bool = True,
1155
+ ) -> None:
1156
+ try:
1157
+ if all(content is None for content in snapshot.files.values()):
1158
+ profiles_root = (self._dsh_home / "profiles").resolve()
1159
+ profile_root = self._profile_root.resolve()
1160
+ if profile_root.parent != profiles_root:
1161
+ raise DshPluginMutationError("refusing to clean an untrusted DSH profile path")
1162
+ if profile_root.exists():
1163
+ shutil.rmtree(profile_root, ignore_errors=False)
1164
+ return
1165
+ node_modules = self._profile_root / "node_modules"
1166
+ if not snapshot.node_modules_existed and node_modules.exists():
1167
+ profile_root = self._profile_root.resolve()
1168
+ if node_modules.is_symlink() or node_modules.resolve().parent != profile_root:
1169
+ raise DshPluginMutationError(
1170
+ "refusing to clean an untrusted DSH node_modules path"
1171
+ )
1172
+ shutil.rmtree(node_modules, ignore_errors=False)
1173
+ self._profile_root.mkdir(parents=True, exist_ok=True, mode=0o700)
1174
+ for name, content in snapshot.files.items():
1175
+ path = self._profile_root / name
1176
+ if content is None:
1177
+ path.unlink(missing_ok=True)
1178
+ else:
1179
+ path.write_bytes(content)
1180
+ path.chmod(0o600)
1181
+ if (
1182
+ reinstall and snapshot.files.get("pnpm-lock.yaml") is not None
1183
+ and self._manifest_path().is_file()
1184
+ ):
1185
+ self._plugin_command("install", "--frozen-lockfile")
1186
+ for name in ("package.json", _STATE_FILE):
1187
+ content = snapshot.files.get(name)
1188
+ path = self._profile_root / name
1189
+ if content is None:
1190
+ path.unlink(missing_ok=True)
1191
+ else:
1192
+ path.write_bytes(content)
1193
+ path.chmod(0o600)
1194
+ except BaseException as rollback_error:
1195
+ raise DshPluginMutationError(
1196
+ "DSH plugin mutation failed and profile rollback also failed"
1197
+ ) from rollback_error
1198
+ if isinstance(original, DshBridgeError):
1199
+ return
1200
+
1201
+ @staticmethod
1202
+ def _validate_source(source: str) -> None:
1203
+ value = source.strip()
1204
+ if (
1205
+ not value
1206
+ or len(value) > 2048
1207
+ or value.startswith("-")
1208
+ or any(character in source for character in ("\r", "\n", "\0"))
1209
+ or value in {".", ".."}
1210
+ or value.startswith(("file:", "link:"))
1211
+ or value.startswith(("./", "../", "file:./", "file:../", "link:./", "link:../"))
1212
+ ):
1213
+ raise ValueError(
1214
+ "DSH plugin source must be an exact registry version or absolute local path"
1215
+ )
1216
+ candidate = Path(value).expanduser()
1217
+ if candidate.is_absolute():
1218
+ if not candidate.exists():
1219
+ raise ValueError("absolute local DSH plugin source does not exist")
1220
+ return
1221
+ # Bare names resolve latest to an exact version before package mutation.
1222
+ # Git, URL, explicit tags and ranges remain unsupported. Local sources
1223
+ # are packed and content-addressed by _prepare_source.
1224
+ validate_dsh_registry_request(value)
1225
+
1226
+ @staticmethod
1227
+ def _validate_package_name(name: str) -> None:
1228
+ if not _PACKAGE_NAME.fullmatch(name):
1229
+ raise ValueError("invalid DSH plugin package name")
1230
+
1231
+ @staticmethod
1232
+ def _string(value: object) -> str:
1233
+ return value if isinstance(value, str) else ""
1234
+
1235
+ @staticmethod
1236
+ def _run_command(
1237
+ command: Sequence[str],
1238
+ cwd: Path,
1239
+ environment: Mapping[str, str],
1240
+ ) -> _CommandResult:
1241
+ try:
1242
+ process = subprocess.Popen(
1243
+ list(command),
1244
+ cwd=cwd,
1245
+ env=dict(environment),
1246
+ stdout=subprocess.PIPE,
1247
+ stderr=subprocess.PIPE,
1248
+ text=True,
1249
+ start_new_session=os.name == "posix",
1250
+ )
1251
+ try:
1252
+ stdout, stderr = process.communicate(timeout=_COMMAND_TIMEOUT_SECONDS)
1253
+ except BaseException:
1254
+ # DSH synchronously waits on pnpm. Stop both before profile
1255
+ # rollback, including when a child still owns the output pipes.
1256
+ if os.name == "posix":
1257
+ try:
1258
+ os.killpg(process.pid, signal.SIGKILL)
1259
+ except ProcessLookupError:
1260
+ pass
1261
+ else: # pragma: no cover - supported production hosts are Unix
1262
+ process.kill()
1263
+ process.communicate()
1264
+ raise
1265
+ completed = subprocess.CompletedProcess(command, process.returncode, stdout, stderr)
1266
+ except (OSError, subprocess.TimeoutExpired) as error:
1267
+ raise DshHostUnavailableError("DSH host command did not complete") from error
1268
+ if completed.returncode != 0:
1269
+ diagnostic = _redact_diagnostic(completed.stderr or completed.stdout)
1270
+ raise DshPluginMutationError(
1271
+ f"DSH host command failed with exit code {completed.returncode}"
1272
+ + (f": {diagnostic[-2000:]}" if diagnostic else "")
1273
+ )
1274
+ return _CommandResult(stdout=completed.stdout, stderr=completed.stderr)
1275
+
1276
+
1277
+ def _redact_diagnostic(value: str) -> str:
1278
+ value = re.sub(r"(https?://)[^/\s:@]+:[^/\s@]+@", r"\1[redacted]@", value, flags=re.I)
1279
+ return re.sub(
1280
+ r"((?:token|password|authorization|_authToken)\s*[:=]\s*)[^\s]+",
1281
+ r"\1[redacted]",
1282
+ value,
1283
+ flags=re.I,
1284
+ ).strip()
1285
+
1286
+
1287
+ __all__ = [
1288
+ "DshBridgeError",
1289
+ "DshBridgeHost",
1290
+ "DshHostUnavailableError",
1291
+ "DshPluginApprovalRequired",
1292
+ "DshPluginInventory",
1293
+ "DshPluginMutationError",
1294
+ "DshPluginNotFoundError",
1295
+ "DshProfilePluginBridge",
1296
+ "DshProfileProjection",
1297
+ "DshProfileRecoveryError",
1298
+ "dsh_subprocess_environment",
1299
+ "validate_dsh_registry_source",
1300
+ ]