agentengine-sdk-python 0.8.2__py3-none-any.whl → 0.8.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/METADATA +18 -25
  2. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/RECORD +224 -169
  3. ksadk/_build_provenance.json +1 -0
  4. ksadk/agui/_agent_helpers.py +4 -0
  5. ksadk/api/client.py +3 -0
  6. ksadk/builders/managed_runtime_builder.py +3 -0
  7. ksadk/cli/__init__.py +6 -0
  8. ksadk/cli/cmd_plugin.py +657 -0
  9. ksadk/codex/runtime.py +50 -8
  10. ksadk/configs/env_registry.py +31 -3
  11. ksadk/configs/settings.py +9 -0
  12. ksadk/context_engine/contributors.py +73 -8
  13. ksadk/conversations/__init__.py +19 -0
  14. ksadk/conversations/context.py +104 -24
  15. ksadk/conversations/contracts.py +266 -0
  16. ksadk/conversations/message_projection.py +19 -1
  17. ksadk/conversations/projector.py +616 -0
  18. ksadk/conversations/reducer.py +98 -0
  19. ksadk/events/_v1_compat/projection.py +2 -0
  20. ksadk/events/adapters/codex.py +190 -0
  21. ksadk/harness/__init__.py +45 -35
  22. ksadk/harness/runtime.py +60 -1
  23. ksadk/kernel/contract_fingerprints.py +5 -4
  24. ksadk/kernel/contracts.py +8 -0
  25. ksadk/kernel/control.py +1 -0
  26. ksadk/kernel/ingress.py +21 -0
  27. ksadk/kernel/memory_store.py +1 -0
  28. ksadk/kernel/postgres_store.py +1 -0
  29. ksadk/kernel/sqlite_store.py +1 -0
  30. ksadk/kernel/worker.py +11 -0
  31. ksadk/memory/providers/local_sqlite.py +7 -0
  32. ksadk/plugins/__init__.py +167 -0
  33. ksadk/plugins/bridges/__init__.py +45 -0
  34. ksadk/plugins/bridges/codex.py +552 -0
  35. ksadk/plugins/bridges/dsh.py +1116 -0
  36. ksadk/plugins/builtins.py +747 -0
  37. ksadk/plugins/bundle.py +246 -0
  38. ksadk/plugins/bundle_security.py +167 -0
  39. ksadk/plugins/composition.py +529 -0
  40. ksadk/plugins/context_contributor.py +437 -0
  41. ksadk/plugins/contracts.py +451 -0
  42. ksadk/plugins/dsh_toolchain.py +935 -0
  43. ksadk/plugins/ecosystem_bridge.py +1105 -0
  44. ksadk/plugins/ecosystem_probe.py +191 -0
  45. ksadk/plugins/host.py +724 -0
  46. ksadk/plugins/providers/__init__.py +85 -0
  47. ksadk/plugins/providers/bundles/ksadk-codex/cordis.patch.yml +3 -0
  48. ksadk/plugins/providers/bundles/ksadk-codex/index.mjs +18 -0
  49. ksadk/plugins/providers/bundles/ksadk-codex/package.json +19 -0
  50. ksadk/plugins/providers/bundles/ksadk-harness/cordis.patch.yml +3 -0
  51. ksadk/plugins/providers/bundles/ksadk-harness/index.mjs +19 -0
  52. ksadk/plugins/providers/bundles/ksadk-harness/package.json +19 -0
  53. ksadk/plugins/providers/codex.py +722 -0
  54. ksadk/plugins/providers/codex_dsh.py +83 -0
  55. ksadk/plugins/providers/dsh.py +951 -0
  56. ksadk/plugins/providers/dsh_descriptor_host.py +226 -0
  57. ksadk/plugins/providers/harness.py +627 -0
  58. ksadk/plugins/providers/harness_dsh.py +91 -0
  59. ksadk/plugins/providers/legacy.py +215 -0
  60. ksadk/plugins/providers/legacy_catalog.py +100 -0
  61. ksadk/plugins/providers/shipped_dsh.py +370 -0
  62. ksadk/plugins/resolver.py +331 -0
  63. ksadk/plugins/subagent_providers/__init__.py +12 -0
  64. ksadk/plugins/subagent_providers/codex.py +443 -0
  65. ksadk/plugins/subagents.py +333 -0
  66. ksadk/runtime/_runner_adapter/stream_mapping.py +51 -42
  67. ksadk/runtime/adapter.py +1 -0
  68. ksadk/runtime/factory.py +76 -2
  69. ksadk/runtime/framework_adapters.py +18 -0
  70. ksadk/runtime/runner_adapter.py +5 -0
  71. ksadk/sandbox/__init__.py +2 -1
  72. ksadk/sandbox/factory.py +72 -0
  73. ksadk/scheduler/__init__.py +18 -0
  74. ksadk/scheduler/calendar.py +102 -0
  75. ksadk/scheduler/contracts.py +254 -0
  76. ksadk/scheduler/dispatcher.py +129 -0
  77. ksadk/scheduler/engine.py +416 -0
  78. ksadk/scheduler/sqlite_store.py +563 -0
  79. ksadk/server/static/assets/{ArtifactsPanel-DSU1sWD_.js → ArtifactsPanel-10GPXIJ6.js} +1 -1
  80. ksadk/server/static/assets/{CodeBlock-DOH6MVcn.js → CodeBlock-BXk1l-G1.js} +1 -1
  81. ksadk/server/static/assets/{MathMessageMarkdown-BL8my1R2.js → MathMessageMarkdown-D9uB55Oh.js} +1 -1
  82. ksadk/server/static/assets/{MermaidBlock-Dz4IP-Tx.js → MermaidBlock--OEYoXIJ.js} +6 -6
  83. ksadk/server/static/assets/{NativeTerminalPanel-DUrK0JpZ.js → NativeTerminalPanel-DrEe1syq.js} +1 -1
  84. ksadk/server/static/assets/{abnfDiagram-VCTEODGH-g20pFzNV.js → abnfDiagram-VCTEODGH-eeTFJLvo.js} +1 -1
  85. ksadk/server/static/assets/{arc-C4FzinUA.js → arc-S3ZNbIAd.js} +1 -1
  86. ksadk/server/static/assets/architecture-7GRP2DOG-Dk9nRQ6-.js +1 -0
  87. ksadk/server/static/assets/{architectureDiagram-5GKGNRK7-DpIqL5h4.js → architectureDiagram-5GKGNRK7-CRjgEJDX.js} +1 -1
  88. ksadk/server/static/assets/{blockDiagram-NRAW4CY4-BjkljTNz.js → blockDiagram-NRAW4CY4-C7bsBoNH.js} +1 -1
  89. ksadk/server/static/assets/{c4Diagram-UCG6FXSJ-Dk_ieq2X.js → c4Diagram-UCG6FXSJ-BX3c33Ob.js} +1 -1
  90. ksadk/server/static/assets/channel-s354Yo6o.js +1 -0
  91. ksadk/server/static/assets/chunk-2Q5K7J3B-CDpPpKR5.js +1 -0
  92. ksadk/server/static/assets/{chunk-5VM5RSS4-BVHAchFb.js → chunk-5VM5RSS4-bZ7ws-8m.js} +1 -1
  93. ksadk/server/static/assets/{chunk-F27PBJKO-C-ipQzuS.js → chunk-F27PBJKO-D2r0pvhY.js} +1 -1
  94. ksadk/server/static/assets/{chunk-G27WJ6UU-C2EyJbIK.js → chunk-G27WJ6UU-CZwF3oUP.js} +1 -1
  95. ksadk/server/static/assets/{chunk-JWPE2WC7-vYvVJb_M.js → chunk-JWPE2WC7-DigFYCML.js} +1 -1
  96. ksadk/server/static/assets/{chunk-LCL6LL3I-BcI6ysPT.js → chunk-LCL6LL3I-CnorGV5q.js} +1 -1
  97. ksadk/server/static/assets/{chunk-POPQ4Y6H-C030x_Z1.js → chunk-POPQ4Y6H-CexntQA-.js} +1 -1
  98. ksadk/server/static/assets/{chunk-RHFEMEQ7-yyGpsz4n.js → chunk-RHFEMEQ7-CHTC7UxA.js} +1 -1
  99. ksadk/server/static/assets/{chunk-SVP7TREG-BLTlmMU7.js → chunk-SVP7TREG-BC1EWt_-.js} +1 -1
  100. ksadk/server/static/assets/{chunk-XXDRQBXY-C_32ArgP.js → chunk-XXDRQBXY-BTG24xN0.js} +1 -1
  101. ksadk/server/static/assets/classDiagram-DTDB5LWJ-Dd0nucl9.js +1 -0
  102. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-Dd0nucl9.js +1 -0
  103. ksadk/server/static/assets/{cose-bilkent-JH36ORCC-9YQYwdl0.js → cose-bilkent-JH36ORCC-CkonRdAS.js} +1 -1
  104. ksadk/server/static/assets/cynefin-OW5HDTMX-BBlT4uMf.js +1 -0
  105. ksadk/server/static/assets/{cynefinDiagram-5FMLGOSQ-ErLF5N13.js → cynefinDiagram-5FMLGOSQ-DEOCBe15.js} +1 -1
  106. ksadk/server/static/assets/{dagre-3AP2YEHR-DFiQF-6f.js → dagre-3AP2YEHR-DBThPuLa.js} +1 -1
  107. ksadk/server/static/assets/{dagre-BuhGnRI1.js → dagre-ClTPrTht.js} +1 -1
  108. ksadk/server/static/assets/{diagram-S7CK7UJ4-Bt1v8-GC.js → diagram-S7CK7UJ4-DsZHrnpJ.js} +1 -1
  109. ksadk/server/static/assets/{diagram-UQ7AKVKN-DSCxJdBK.js → diagram-UQ7AKVKN-BJR4nhVr.js} +1 -1
  110. ksadk/server/static/assets/{diagram-VSXAHHWV-DHfYwp_9.js → diagram-VSXAHHWV-xzcERX0a.js} +1 -1
  111. ksadk/server/static/assets/{diagram-VX7I27RA-DdzD-4Le.js → diagram-VX7I27RA-tn2xM1xu.js} +1 -1
  112. ksadk/server/static/assets/{diagram-Z3DM3KII-Du-nAJ9F.js → diagram-Z3DM3KII-BZPGBkw5.js} +1 -1
  113. ksadk/server/static/assets/{dist-ClxGviKw.js → dist-4zN1_tq5.js} +1 -1
  114. ksadk/server/static/assets/{dist-BE7bu-DL.js → dist-8VWI-2bZ.js} +1 -1
  115. ksadk/server/static/assets/{dist-BX8z72IC.js → dist-B-46q3Hk.js} +1 -1
  116. ksadk/server/static/assets/{dist-BjU6y-A2.js → dist-B0gdk4rl.js} +1 -1
  117. ksadk/server/static/assets/{dist-C_wsv-Qd.js → dist-B1oWRmrH.js} +1 -1
  118. ksadk/server/static/assets/{dist-ngtN0DJB.js → dist-BI1qQH4n.js} +1 -1
  119. ksadk/server/static/assets/{dist-dydu68Fl.js → dist-BJLUwH6W.js} +1 -1
  120. ksadk/server/static/assets/{dist-Dq9gLD7L.js → dist-Bbwqa8TG.js} +1 -1
  121. ksadk/server/static/assets/{dist-CttYUqzS.js → dist-Btyo4vX4.js} +1 -1
  122. ksadk/server/static/assets/{dist-CXYYBw3_.js → dist-C71CsujO.js} +1 -1
  123. ksadk/server/static/assets/{dist-dDdADKFA.js → dist-CXpH_Rdd.js} +1 -1
  124. ksadk/server/static/assets/{dist-CSh_DE14.js → dist-Cq4OL6nO.js} +1 -1
  125. ksadk/server/static/assets/{dist-8hqcx0sU.js → dist-D1V3f1B4.js} +1 -1
  126. ksadk/server/static/assets/{dist-BVDffZ0U.js → dist-DJmCPRVB.js} +1 -1
  127. ksadk/server/static/assets/{dist-DnfXs8Vn.js → dist-DQAB0qn_.js} +1 -1
  128. ksadk/server/static/assets/{dist-DNp6rLRA.js → dist-DUwKa_l6.js} +1 -1
  129. ksadk/server/static/assets/{dist-B7seoj3d.js → dist-DdRBIceA.js} +1 -1
  130. ksadk/server/static/assets/{dist-BytlxIDT.js → dist-DfGFrb0v.js} +1 -1
  131. ksadk/server/static/assets/{dist-Bf2M8m3N.js → dist-DjhwGUgZ.js} +1 -1
  132. ksadk/server/static/assets/{dist-W-TIVntx.js → dist-DyqV4fL4.js} +1 -1
  133. ksadk/server/static/assets/{dist-DUX-id_F.js → dist-M67Hwu0n.js} +1 -1
  134. ksadk/server/static/assets/{dist-DmldrHd_.js → dist-u8tVH5Ak.js} +1 -1
  135. ksadk/server/static/assets/{ebnfDiagram-PWID7BFC-Da9C9NO1.js → ebnfDiagram-PWID7BFC-DVfMsLge.js} +1 -1
  136. ksadk/server/static/assets/{erDiagram-SSCWMZ5O-EAGqqR79.js → erDiagram-SSCWMZ5O-CWEZSYaw.js} +1 -1
  137. ksadk/server/static/assets/eventmodeling-NTZA5JFV-Dr5okd8V.js +1 -0
  138. ksadk/server/static/assets/flowDiagram-A5DVABFB-VEO-cVcO.js +1 -0
  139. ksadk/server/static/assets/{ganttDiagram-EL5Y4UJY-DqJsKb59.js → ganttDiagram-EL5Y4UJY-TWIxoyYL.js} +1 -1
  140. ksadk/server/static/assets/gitGraph-4MIJSDKK-CKiKY9Cf.js +1 -0
  141. ksadk/server/static/assets/{gitGraphDiagram-WWUBYQGX-B6g4dtDi.js → gitGraphDiagram-WWUBYQGX-DDh2hd2S.js} +1 -1
  142. ksadk/server/static/assets/index-B2k_urY8.js +241 -0
  143. ksadk/server/static/assets/index-ILPt6IWF.css +2 -0
  144. ksadk/server/static/assets/info-A6RAGUB7-ubA4Nm_p.js +1 -0
  145. ksadk/server/static/assets/{infoDiagram-RXCK75RN-Djm_nbd5.js → infoDiagram-RXCK75RN-CgoyWiDR.js} +1 -1
  146. ksadk/server/static/assets/{ishikawaDiagram-5VMMS53U-DWya9SG2.js → ishikawaDiagram-5VMMS53U-CyRIu_om.js} +1 -1
  147. ksadk/server/static/assets/{journeyDiagram-EYS64GPL-DutxhSNI.js → journeyDiagram-EYS64GPL-DN5JXHzN.js} +1 -1
  148. ksadk/server/static/assets/{kanban-definition-3QL26DDD-DJdOF8Fm.js → kanban-definition-3QL26DDD-CNP3ui66.js} +1 -1
  149. ksadk/server/static/assets/{katex-vFWytM5c.js → katex-DWEzQy1d.js} +1 -1
  150. ksadk/server/static/assets/{linear-BHjG8b-J.js → linear-C-qHp7d2.js} +1 -1
  151. ksadk/server/static/assets/{mermaid-parser.core-KGSy4jWT.js → mermaid-parser.core-Cl-K943T.js} +2 -2
  152. ksadk/server/static/assets/{mindmap-definition-FBJOCRG2-9-HC88D4.js → mindmap-definition-FBJOCRG2-D5DB5mYm.js} +1 -1
  153. ksadk/server/static/assets/packet-AYTQ26CC-C7R3NUlK.js +1 -0
  154. ksadk/server/static/assets/{pegDiagram-XKGWAZYB-DrEZd4fD.js → pegDiagram-XKGWAZYB-BQVc9ia_.js} +1 -1
  155. ksadk/server/static/assets/pie-WAS4IAKB-BIT6qjr3.js +1 -0
  156. ksadk/server/static/assets/{pieDiagram-E7YTZNPT-DpjqOFFp.js → pieDiagram-E7YTZNPT-B6UCAvtY.js} +1 -1
  157. ksadk/server/static/assets/{quadrantDiagram-AXDQQJYC-BiNhgOXX.js → quadrantDiagram-AXDQQJYC-mIr2d3eb.js} +1 -1
  158. ksadk/server/static/assets/radar-RG4KPBEZ-l5noef9X.js +1 -0
  159. ksadk/server/static/assets/railroad-74A4TZTK-Baea6ZvM.js +1 -0
  160. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-OKn8zZnU.js +1 -0
  161. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-DdKAKC4l.js +1 -0
  162. ksadk/server/static/assets/railroad-peg-WCYAUIDC-Bds8udlG.js +1 -0
  163. ksadk/server/static/assets/{railroadDiagram-O6MQD6OU-_Y3Ojg6G.js → railroadDiagram-O6MQD6OU-Dt3FFDmK.js} +1 -1
  164. ksadk/server/static/assets/{requirementDiagram-EFPCY7ZU-DeCFdg9K.js → requirementDiagram-EFPCY7ZU-Cz_vQtgF.js} +1 -1
  165. ksadk/server/static/assets/{sankeyDiagram-P5KCCOFB-CPH75rhw.js → sankeyDiagram-P5KCCOFB-wlO1TsH5.js} +1 -1
  166. ksadk/server/static/assets/{sequenceDiagram-WJ2MYXX4-OEpQQdr4.js → sequenceDiagram-WJ2MYXX4-CHRbElYq.js} +1 -1
  167. ksadk/server/static/assets/{sizeCapture-X5ZJPWSS-heUErzhI.js → sizeCapture-X5ZJPWSS-DXX54iyO.js} +1 -1
  168. ksadk/server/static/assets/{stateDiagram-HBIQ2CUA-DvA3jSMB.js → stateDiagram-HBIQ2CUA-Dp_lw9II.js} +1 -1
  169. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-5b-8o2iS.js +1 -0
  170. ksadk/server/static/assets/{swimlanes-XN3QIQJK-CR-dfN_t.js → swimlanes-XN3QIQJK-D8O5UlLG.js} +1 -1
  171. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-uf6WPRA4.js +8 -0
  172. ksadk/server/static/assets/{timeline-definition-24CTP7MA-DAVJf1TX.js → timeline-definition-24CTP7MA-8mfKcxvG.js} +1 -1
  173. ksadk/server/static/assets/treeView-Q6P3EWNA-CK5CSrSO.js +1 -0
  174. ksadk/server/static/assets/treemap-WGGIJYW6-Ch2BKgAM.js +1 -0
  175. ksadk/server/static/assets/{vennDiagram-4TSXK5OY-DwM8eDkh.js → vennDiagram-4TSXK5OY-EnNl0DE7.js} +1 -1
  176. ksadk/server/static/assets/wardley-WFR3VGLG-YW3Q0ml_.js +1 -0
  177. ksadk/server/static/assets/{wardleyDiagram-VM6X3IG4-DZL-Zz2t.js → wardleyDiagram-VM6X3IG4-DBu5fbYr.js} +1 -1
  178. ksadk/server/static/assets/{xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js → xychartDiagram-S5SC5T6Z-BSVXs4IY.js} +1 -1
  179. ksadk/server/static/index.html +2 -2
  180. ksadk/skills/service_client.py +48 -0
  181. ksadk/studio/agent_lifecycle.py +26 -20
  182. ksadk/studio/api.py +513 -71
  183. ksadk/studio/api_catalog_routes.py +5 -0
  184. ksadk/studio/api_contracts.py +34 -1
  185. ksadk/studio/api_plugin_routes.py +480 -0
  186. ksadk/studio/attachment_store.py +187 -0
  187. ksadk/studio/authoring.py +10 -4
  188. ksadk/studio/authoring_coordinator.py +12 -2
  189. ksadk/studio/builder.py +123 -43
  190. ksadk/studio/cloud.py +4 -0
  191. ksadk/studio/codex_agent_service.py +12 -1
  192. ksadk/studio/codex_builder.py +54 -15
  193. ksadk/studio/codex_manifest.py +21 -1
  194. ksadk/studio/codex_run.py +16 -4
  195. ksadk/studio/compatibility_report.py +445 -0
  196. ksadk/studio/compiler.py +6 -1
  197. ksadk/studio/contracts.py +123 -4
  198. ksadk/studio/dsh_provider_registration.py +768 -0
  199. ksadk/studio/event_store.py +54 -6
  200. ksadk/studio/framework_run.py +37 -4
  201. ksadk/studio/hosted_kernel.py +1 -1
  202. ksadk/studio/operations.py +12 -2
  203. ksadk/studio/plugin_composition.py +301 -0
  204. ksadk/studio/plugin_kernel_adapter.py +128 -0
  205. ksadk/studio/plugin_runtime.py +652 -0
  206. ksadk/studio/resource_catalog.py +6 -1
  207. ksadk/studio/run_service.py +713 -75
  208. ksadk/studio/runtime_source.py +9 -1
  209. ksadk/studio/scheduler_runtime.py +374 -0
  210. ksadk/studio/scheduler_service.py +211 -0
  211. ksadk/studio/service.py +672 -82
  212. ksadk/studio/shared_web.py +167 -28
  213. ksadk/studio/soul.py +52 -0
  214. ksadk/studio/static/assets/index-DXs1Eq4M.css +1 -0
  215. ksadk/studio/static/assets/index-DeDqwTsD.js +283 -0
  216. ksadk/studio/static/index.html +2 -2
  217. ksadk/studio/validator.py +1 -1
  218. ksadk/studio/workspace.py +1 -0
  219. ksadk/version.py +1 -1
  220. ksadk_runtime_common/workspace_files/path_utils.py +41 -0
  221. ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +0 -1
  222. ksadk/server/static/assets/channel-4cQHKtx1.js +0 -1
  223. ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +0 -1
  224. ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +0 -1
  225. ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +0 -1
  226. ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +0 -1
  227. ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +0 -1
  228. ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +0 -1
  229. ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +0 -1
  230. ksadk/server/static/assets/index-8ipRcQ-M.js +0 -240
  231. ksadk/server/static/assets/index-ByFCqjlh.css +0 -2
  232. ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +0 -1
  233. ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +0 -1
  234. ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +0 -1
  235. ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +0 -1
  236. ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +0 -1
  237. ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +0 -1
  238. ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +0 -1
  239. ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +0 -1
  240. ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +0 -1
  241. ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +0 -8
  242. ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +0 -1
  243. ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +0 -1
  244. ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +0 -1
  245. ksadk/studio/static/assets/index-BLpcdrgW.css +0 -1
  246. ksadk/studio/static/assets/index-CYekR2Xv.js +0 -260
  247. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/WHEEL +0 -0
  248. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/entry_points.txt +0 -0
  249. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/licenses/LICENSE +0 -0
  250. {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1116 @@
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 stat
17
+ import subprocess
18
+ import tempfile
19
+ import threading
20
+ from collections.abc import Callable, Mapping, Sequence
21
+ from contextlib import contextmanager
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+ from typing import Iterator, Literal, TypedDict
25
+
26
+ try:
27
+ import fcntl
28
+ except ImportError: # pragma: no cover - DSH production hosts are Unix
29
+ fcntl = None # type: ignore[assignment]
30
+
31
+ from pydantic import BaseModel, ConfigDict
32
+
33
+
34
+ def _to_camel(value: str) -> str:
35
+ head, *tail = value.split("_")
36
+ return head + "".join(part.capitalize() for part in tail)
37
+
38
+
39
+ class _DshModel(BaseModel):
40
+ model_config = ConfigDict(
41
+ alias_generator=_to_camel,
42
+ populate_by_name=True,
43
+ extra="forbid",
44
+ frozen=True,
45
+ )
46
+
47
+
48
+ class DshBridgeHost(_DshModel):
49
+ host_id: Literal["deepseek-harness"] = "deepseek-harness"
50
+ version: str
51
+ protocol: Literal["dsh.profile/v1"] = "dsh.profile/v1"
52
+ available: Literal[True] = True
53
+
54
+
55
+ class DshClientBundle(_DshModel):
56
+ """One validated browser half from an installed DSH package."""
57
+
58
+ platform: Literal["web"] = "web"
59
+ digest: str
60
+ content_bytes: int
61
+ external: tuple[str, ...] = ()
62
+ inject: tuple[str, ...] = ()
63
+ compatible: bool
64
+ incompatibility_reason: str = ""
65
+
66
+
67
+ class DshPluginInventory(_DshModel):
68
+ ecosystem: Literal["dsh"] = "dsh"
69
+ integration_mode: Literal["bridged"] = "bridged"
70
+ profile: str
71
+ name: str
72
+ display_name: str
73
+ description: str = ""
74
+ version: str
75
+ requested_spec: str
76
+ source_digest: str | None = None
77
+ source_kind: Literal["directory", "tgz"] | None = None
78
+ installed: Literal[True] = True
79
+ enabled: bool
80
+ permissions_declared: Literal[False] = False
81
+ client_bundle: DshClientBundle | None = None
82
+ risk_disclosures: tuple[str, ...] = (
83
+ "DSH packages and install scripts run with the native host user privileges.",
84
+ "DSH bundle manifests do not declare a complete runtime permission set.",
85
+ )
86
+
87
+
88
+ class DshProfileProjection(_DshModel):
89
+ profile: str
90
+ bundles: tuple[str, ...]
91
+ config_digest: str
92
+ config_bytes: int
93
+ host_version: str
94
+
95
+
96
+ class DshBridgeError(RuntimeError):
97
+ """Base failure for one bounded DSH profile operation."""
98
+
99
+
100
+ class DshHostUnavailableError(DshBridgeError):
101
+ pass
102
+
103
+
104
+ class DshPluginNotFoundError(DshBridgeError):
105
+ pass
106
+
107
+
108
+ class DshPluginApprovalRequired(DshBridgeError):
109
+ pass
110
+
111
+
112
+ class DshPluginMutationError(DshBridgeError):
113
+ pass
114
+
115
+
116
+ class _CommandResult(_DshModel):
117
+ stdout: str = ""
118
+ stderr: str = ""
119
+
120
+
121
+ CommandRunner = Callable[[Sequence[str], Path, Mapping[str, str]], _CommandResult]
122
+
123
+ _PROFILE_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
124
+ _PACKAGE_NAME = re.compile(r"^(?:@[A-Za-z0-9._-]+/)?[A-Za-z0-9._-]+$")
125
+ _HOST_VERSION = re.compile(r"\b(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b")
126
+ _STATE_FILE = ".ksadk-dsh-plugins.json"
127
+ _IMMUTABLE_SOURCE_DIR = "immutable-plugin-sources"
128
+ _SNAPSHOT_FILES = ("package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", _STATE_FILE)
129
+ _MAX_JSON_BYTES = 2 * 1024 * 1024
130
+ _MAX_CLIENT_BUNDLE_BYTES = 8 * 1024 * 1024
131
+ _STUDIO_CLIENT_EXTERNALS = frozenset({"react"})
132
+ _PROFILE_LOCK_DIR = "profile-locks"
133
+ _DSH_SUBPROCESS_ENV_KEYS = (
134
+ "PATH",
135
+ "HOME",
136
+ "USER",
137
+ "LOGNAME",
138
+ "SHELL",
139
+ "TMPDIR",
140
+ "TMP",
141
+ "TEMP",
142
+ "LANG",
143
+ "LANGUAGE",
144
+ "LC_ALL",
145
+ "LC_CTYPE",
146
+ "HTTP_PROXY",
147
+ "HTTPS_PROXY",
148
+ "ALL_PROXY",
149
+ "NO_PROXY",
150
+ "http_proxy",
151
+ "https_proxy",
152
+ "all_proxy",
153
+ "no_proxy",
154
+ "SSL_CERT_FILE",
155
+ "SSL_CERT_DIR",
156
+ "NODE_EXTRA_CA_CERTS",
157
+ "REQUESTS_CA_BUNDLE",
158
+ "CURL_CA_BUNDLE",
159
+ "NPM_CONFIG_REGISTRY",
160
+ "npm_config_registry",
161
+ "SYSTEMROOT",
162
+ "WINDIR",
163
+ "COMSPEC",
164
+ "PATHEXT",
165
+ )
166
+
167
+
168
+ def dsh_subprocess_environment(*, dsh_home: Path | None = None) -> dict[str, str]:
169
+ """Build the explicit environment inherited by DSH and pnpm children.
170
+
171
+ DSH bundles may execute package lifecycle scripts. They need process
172
+ discovery, a home/temp directory, locale, proxies and CA configuration,
173
+ but never receive arbitrary model, cloud, npm, SSH or host application
174
+ credentials from the parent process.
175
+ """
176
+
177
+ environment = {
178
+ name: os.environ[name] for name in _DSH_SUBPROCESS_ENV_KEYS if os.environ.get(name)
179
+ }
180
+ if "PATH" not in environment:
181
+ environment["PATH"] = os.defpath
182
+ if dsh_home is not None:
183
+ environment["DSH_HOME"] = str(dsh_home)
184
+ return environment
185
+
186
+
187
+ class _SourceReceipt(TypedDict):
188
+ digest: str
189
+ kind: Literal["directory", "tgz"]
190
+ artifact: str
191
+ dependency_spec: str
192
+
193
+
194
+ class _ProfileState(TypedDict):
195
+ order: list[str]
196
+ disabled: list[str]
197
+ sources: dict[str, _SourceReceipt]
198
+
199
+
200
+ @dataclass(frozen=True)
201
+ class _PreparedSource:
202
+ command_source: str
203
+ digest: str
204
+ kind: Literal["directory", "tgz"]
205
+ artifact: str
206
+
207
+
208
+ class DshProfilePluginBridge:
209
+ """Manage DSH bundles in one isolated Profile with rollback and preflight."""
210
+
211
+ def __init__(
212
+ self,
213
+ *,
214
+ dsh_home: Path,
215
+ profile: str = "ksadk",
216
+ dsh_command: Sequence[str] | None = None,
217
+ command_runner: CommandRunner | None = None,
218
+ cwd: Path | None = None,
219
+ ) -> None:
220
+ if not _PROFILE_NAME.fullmatch(profile):
221
+ raise ValueError("DSH profile must be a simple name without path separators")
222
+ if dsh_command is not None and not dsh_command:
223
+ raise ValueError("DSH command cannot be empty")
224
+ self._dsh_home = dsh_home.expanduser().resolve()
225
+ self._profile = profile
226
+ self._profile_root = self._dsh_home / "profiles" / profile
227
+ self._command = tuple(dsh_command) if dsh_command is not None else None
228
+ self._runner = command_runner or self._run_command
229
+ self._cwd = (cwd or Path.cwd()).resolve()
230
+ self._host: DshBridgeHost | None = None
231
+ self._lock = threading.RLock()
232
+ self._transaction_local = threading.local()
233
+
234
+ @property
235
+ def host(self) -> DshBridgeHost:
236
+ if self._host is None:
237
+ raise DshHostUnavailableError("DSH bridge is not started")
238
+ return self._host
239
+
240
+ def start(self) -> DshBridgeHost:
241
+ if self._host is not None:
242
+ return self._host
243
+ command = self._resolve_command()
244
+ result = self._invoke((*command, "--version"), cwd=self._cwd)
245
+ match = _HOST_VERSION.search(result.stdout or result.stderr)
246
+ if match is None:
247
+ raise DshHostUnavailableError("DSH host did not report a parseable version")
248
+ self._command = command
249
+ self._host = DshBridgeHost(version=match.group(1))
250
+ return self._host
251
+
252
+ def close(self) -> None:
253
+ self._host = None
254
+
255
+ def __enter__(self) -> "DshProfilePluginBridge":
256
+ self.start()
257
+ return self
258
+
259
+ def __exit__(self, _exc_type: object, _exc: object, _tb: object) -> None:
260
+ self.close()
261
+
262
+ def list_plugins(self) -> tuple[DshPluginInventory, ...]:
263
+ with self._profile_transaction(exclusive=False):
264
+ return self._list_plugins_locked()
265
+
266
+ def _list_plugins_locked(self) -> tuple[DshPluginInventory, ...]:
267
+ self._ensure_started()
268
+ if not self._manifest_path().is_file():
269
+ return ()
270
+ manifest = self._read_manifest()
271
+ state = self._read_state(manifest)
272
+ self._verify_source_receipts(manifest, state)
273
+ active = set(self._bundles(manifest))
274
+ items: list[DshPluginInventory] = []
275
+ for name, requested_spec in self._dependencies(manifest).items():
276
+ package = self._read_package(name)
277
+ if package is None or self._bundle_patch(package) is None:
278
+ continue
279
+ receipt = state["sources"].get(name)
280
+ items.append(
281
+ DshPluginInventory(
282
+ profile=self._profile,
283
+ name=name,
284
+ display_name=self._string(package.get("displayName"))
285
+ or self._string(package.get("name"))
286
+ or name,
287
+ description=self._string(package.get("description")),
288
+ version=self._string(package.get("version")),
289
+ requested_spec=requested_spec,
290
+ source_digest=receipt["digest"] if receipt is not None else None,
291
+ source_kind=receipt["kind"] if receipt is not None else None,
292
+ enabled=name in active and name not in set(state["disabled"]),
293
+ client_bundle=self._client_bundle_metadata(name, package),
294
+ )
295
+ )
296
+ return tuple(sorted(items, key=lambda item: (item.display_name.casefold(), item.name)))
297
+
298
+ def get_plugin(self, name: str) -> DshPluginInventory:
299
+ self._validate_package_name(name)
300
+ matches = [item for item in self.list_plugins() if item.name == name]
301
+ if not matches:
302
+ raise DshPluginNotFoundError(f"DSH plugin {name!r} is not installed")
303
+ return matches[0]
304
+
305
+ def install_plugin(
306
+ self,
307
+ source: str,
308
+ *,
309
+ accept_host_permissions: bool = False,
310
+ ) -> DshPluginInventory:
311
+ if not accept_host_permissions:
312
+ raise DshPluginApprovalRequired(
313
+ "DSH packages can run install scripts and runtime code with host privileges; "
314
+ "explicit approval is required"
315
+ )
316
+ self._validate_source(source)
317
+ with self._profile_transaction(exclusive=True):
318
+ self._require_package_mutation_rollback(new_profile_allowed=True)
319
+ snapshot = self._snapshot()
320
+ before = (
321
+ self._dependencies(self._read_manifest()) if self._manifest_path().is_file() else {}
322
+ )
323
+ try:
324
+ if self._manifest_path().is_file():
325
+ existing_manifest = self._read_manifest()
326
+ self._verify_source_receipts(
327
+ existing_manifest, self._read_state(existing_manifest)
328
+ )
329
+ prepared = self._prepare_source(source)
330
+ self._plugin_command("add", prepared.command_source)
331
+ manifest = self._read_manifest()
332
+ added = [name for name in self._dependencies(manifest) if name not in before]
333
+ if len(added) != 1:
334
+ raise DshPluginMutationError(
335
+ "installing one DSH plugin must add exactly one direct dependency"
336
+ )
337
+ name = added[0]
338
+ self._require_bundle(name)
339
+ state = self._read_state(manifest)
340
+ state["order"] = [item for item in state["order"] if item != name] + [name]
341
+ if name not in state["disabled"]:
342
+ state["disabled"].append(name)
343
+ if prepared.digest:
344
+ state["sources"][name] = {
345
+ "digest": prepared.digest,
346
+ "kind": prepared.kind,
347
+ "artifact": prepared.artifact,
348
+ "dependency_spec": self._dependencies(manifest)[name],
349
+ }
350
+ self._write_state(state)
351
+ self._write_active_bundles(manifest, state)
352
+ self._preflight()
353
+ return self.get_plugin(name)
354
+ except BaseException as error:
355
+ self._rollback(snapshot, error)
356
+ raise
357
+
358
+ def set_enabled(self, name: str, *, enabled: bool) -> DshPluginInventory:
359
+ self._validate_package_name(name)
360
+ with self._profile_transaction(exclusive=True):
361
+ snapshot = self._snapshot()
362
+ try:
363
+ manifest = self._read_manifest()
364
+ if name not in self._dependencies(manifest):
365
+ raise DshPluginNotFoundError(f"DSH plugin {name!r} is not installed")
366
+ self._require_bundle(name)
367
+ state = self._read_state(manifest)
368
+ self._verify_source_receipts(manifest, state, names=(name,))
369
+ if name not in state["order"]:
370
+ state["order"].append(name)
371
+ if enabled:
372
+ state["disabled"] = [item for item in state["disabled"] if item != name]
373
+ elif name not in state["disabled"]:
374
+ state["disabled"].append(name)
375
+ self._write_state(state)
376
+ self._write_active_bundles(manifest, state)
377
+ self._preflight()
378
+ return self.get_plugin(name)
379
+ except BaseException as error:
380
+ self._rollback(snapshot, error)
381
+ raise
382
+
383
+ def update_plugin(
384
+ self,
385
+ name: str,
386
+ *,
387
+ source: str | None = None,
388
+ accept_host_permissions: bool = False,
389
+ ) -> DshPluginInventory:
390
+ self._validate_package_name(name)
391
+ if not accept_host_permissions:
392
+ raise DshPluginApprovalRequired(
393
+ "updating a DSH package requires host permission approval"
394
+ )
395
+ with self._profile_transaction(exclusive=True):
396
+ self._require_package_mutation_rollback(new_profile_allowed=False)
397
+ snapshot = self._snapshot()
398
+ try:
399
+ self.get_plugin(name)
400
+ before_manifest = self._read_manifest()
401
+ before_state = self._read_state(before_manifest)
402
+ self._verify_source_receipts(before_manifest, before_state, names=(name,))
403
+ existing_source = before_state["sources"].get(name)
404
+ if existing_source is not None and source is None:
405
+ raise DshPluginMutationError(
406
+ "updating an immutable local DSH plugin requires an explicit source"
407
+ )
408
+ prepared = None
409
+ if source is not None:
410
+ self._validate_source(source)
411
+ prepared = self._prepare_source(source)
412
+ if not prepared.digest:
413
+ raise DshPluginMutationError(
414
+ "an explicit update source must be a local directory or tgz"
415
+ )
416
+ self._plugin_command("add", prepared.command_source)
417
+ else:
418
+ self._plugin_command("update", name)
419
+ manifest = self._read_manifest()
420
+ if prepared is not None:
421
+ changed = {
422
+ dependency
423
+ for dependency in (
424
+ set(self._dependencies(before_manifest))
425
+ | set(self._dependencies(manifest))
426
+ )
427
+ if self._dependencies(before_manifest).get(dependency)
428
+ != self._dependencies(manifest).get(dependency)
429
+ }
430
+ if changed != {name}:
431
+ raise DshPluginMutationError(
432
+ "updated local source must replace exactly the selected plugin"
433
+ )
434
+ self._require_bundle(name)
435
+ state = self._read_state(manifest)
436
+ if prepared is not None:
437
+ if name not in self._dependencies(manifest):
438
+ raise DshPluginMutationError(
439
+ "updated local source did not preserve the plugin package name"
440
+ )
441
+ state["sources"][name] = {
442
+ "digest": prepared.digest,
443
+ "kind": prepared.kind,
444
+ "artifact": prepared.artifact,
445
+ "dependency_spec": self._dependencies(manifest)[name],
446
+ }
447
+ self._write_state(state)
448
+ self._write_active_bundles(manifest, state)
449
+ self._verify_source_receipts(manifest, state, names=(name,))
450
+ self._preflight()
451
+ return self.get_plugin(name)
452
+ except BaseException as error:
453
+ self._rollback(snapshot, error)
454
+ raise
455
+
456
+ def uninstall_plugin(self, name: str) -> None:
457
+ self._validate_package_name(name)
458
+ with self._profile_transaction(exclusive=True):
459
+ self._require_package_mutation_rollback(new_profile_allowed=False)
460
+ snapshot = self._snapshot()
461
+ try:
462
+ self.get_plugin(name)
463
+ self._plugin_command("remove", name)
464
+ manifest = self._read_manifest()
465
+ state = self._read_state(manifest)
466
+ state["order"] = [item for item in state["order"] if item != name]
467
+ state["disabled"] = [item for item in state["disabled"] if item != name]
468
+ state["sources"].pop(name, None)
469
+ self._write_state(state)
470
+ self._write_active_bundles(manifest, state)
471
+ self._preflight()
472
+ if any(item.name == name for item in self.list_plugins()):
473
+ raise DshPluginMutationError("DSH host still reports the removed plugin")
474
+ except BaseException as error:
475
+ self._rollback(snapshot, error)
476
+ raise
477
+
478
+ def project_profile(self) -> DshProfileProjection:
479
+ """Validate the native profile and expose only its digest, never raw config."""
480
+
481
+ with self._profile_transaction(exclusive=False):
482
+ self._ensure_started()
483
+ manifest = self._read_manifest()
484
+ self._verify_source_receipts(manifest, self._read_state(manifest))
485
+ result = self._preflight()
486
+ payload = result.stdout.encode("utf-8")
487
+ return DshProfileProjection(
488
+ profile=self._profile,
489
+ bundles=tuple(self._bundles(manifest)),
490
+ config_digest=f"sha256:{hashlib.sha256(payload).hexdigest()}",
491
+ config_bytes=len(payload),
492
+ host_version=self.host.version,
493
+ )
494
+
495
+ def read_client_bundle(self, name: str, *, expected_digest: str) -> bytes:
496
+ """Read one immutable browser artifact after revalidating Profile inventory."""
497
+
498
+ with self._profile_transaction(exclusive=False):
499
+ return self._read_client_bundle_locked(name, expected_digest=expected_digest)
500
+
501
+ def _read_client_bundle_locked(self, name: str, *, expected_digest: str) -> bytes:
502
+ item = self.get_plugin(name)
503
+ if not item.enabled:
504
+ raise DshPluginNotFoundError(f"DSH plugin {name!r} is disabled")
505
+ client = item.client_bundle
506
+ if client is None or not client.compatible:
507
+ raise DshPluginNotFoundError(
508
+ f"DSH plugin {name!r} has no Studio-compatible client bundle"
509
+ )
510
+ if client.digest != expected_digest:
511
+ raise DshPluginMutationError("DSH client bundle digest fence does not match")
512
+ package = self._read_package(name)
513
+ assert package is not None
514
+ path = self._client_bundle_path(name, package)
515
+ if path is None:
516
+ raise DshPluginNotFoundError(f"DSH plugin {name!r} client bundle is unavailable")
517
+ try:
518
+ content = path.read_bytes()
519
+ except OSError as error:
520
+ raise DshPluginMutationError("DSH client bundle became unreadable") from error
521
+ actual_digest = f"sha256:{hashlib.sha256(content).hexdigest()}"
522
+ if actual_digest != expected_digest:
523
+ raise DshPluginMutationError("DSH client bundle changed after inventory projection")
524
+ return content
525
+
526
+ def _resolve_command(self) -> tuple[str, ...]:
527
+ if self._command is not None:
528
+ return self._command
529
+ executable = shutil.which("dsh")
530
+ if executable is None:
531
+ raise DshHostUnavailableError(
532
+ "DSH host is not installed; configure an exact DSH executable "
533
+ "before using the bridge"
534
+ )
535
+ return (executable,)
536
+
537
+ def _ensure_started(self) -> None:
538
+ if self._host is None or self._command is None:
539
+ raise DshHostUnavailableError("DSH bridge is not started")
540
+
541
+ @contextmanager
542
+ def _profile_transaction(self, *, exclusive: bool) -> Iterator[None]:
543
+ """Serialize one Profile snapshot across bridge objects and processes."""
544
+
545
+ with self._lock:
546
+ depth = int(getattr(self._transaction_local, "depth", 0))
547
+ held_exclusive = bool(getattr(self._transaction_local, "exclusive", False))
548
+ if depth:
549
+ if exclusive and not held_exclusive:
550
+ raise DshPluginMutationError(
551
+ "cannot upgrade a DSH profile read transaction to a mutation"
552
+ )
553
+ self._transaction_local.depth = depth + 1
554
+ try:
555
+ yield
556
+ finally:
557
+ self._transaction_local.depth = depth
558
+ return
559
+
560
+ if fcntl is None:
561
+ raise DshHostUnavailableError("DSH profile transactions require Unix file locking")
562
+ lock_root = self._dsh_home / _PROFILE_LOCK_DIR
563
+ lock_root.mkdir(parents=True, exist_ok=True, mode=0o700)
564
+ if lock_root.resolve() != lock_root:
565
+ raise DshPluginMutationError("DSH profile lock directory is not trusted")
566
+ lock_path = lock_root / f"{self._profile}.lock"
567
+ flags = os.O_CREAT | os.O_RDWR
568
+ if hasattr(os, "O_NOFOLLOW"):
569
+ flags |= os.O_NOFOLLOW
570
+ try:
571
+ descriptor = os.open(lock_path, flags, 0o600)
572
+ except OSError as error:
573
+ raise DshPluginMutationError(
574
+ "DSH profile transaction lock is unavailable"
575
+ ) from error
576
+ try:
577
+ metadata = os.fstat(descriptor)
578
+ if not stat.S_ISREG(metadata.st_mode):
579
+ raise DshPluginMutationError(
580
+ "DSH profile transaction lock is not a regular file"
581
+ )
582
+ os.fchmod(descriptor, 0o600)
583
+ fcntl.flock(
584
+ descriptor,
585
+ fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH,
586
+ )
587
+ self._transaction_local.depth = 1
588
+ self._transaction_local.exclusive = exclusive
589
+ try:
590
+ yield
591
+ finally:
592
+ self._transaction_local.depth = 0
593
+ self._transaction_local.exclusive = False
594
+ fcntl.flock(descriptor, fcntl.LOCK_UN)
595
+ finally:
596
+ os.close(descriptor)
597
+
598
+ def _invoke(self, command: Sequence[str], *, cwd: Path) -> _CommandResult:
599
+ environment = dsh_subprocess_environment(dsh_home=self._dsh_home)
600
+ try:
601
+ return self._runner(tuple(command), cwd, environment)
602
+ except DshBridgeError:
603
+ raise
604
+ except Exception as error:
605
+ raise DshHostUnavailableError("DSH host command could not be executed") from error
606
+
607
+ def _plugin_command(self, verb: str, value: str) -> _CommandResult:
608
+ self._ensure_started()
609
+ assert self._command is not None
610
+ return self._invoke(
611
+ (*self._command, "plugin", "--profile", self._profile, verb, value),
612
+ cwd=self._cwd,
613
+ )
614
+
615
+ def _preflight(self) -> _CommandResult:
616
+ self._ensure_started()
617
+ assert self._command is not None
618
+ return self._invoke(
619
+ (*self._command, "--profile", self._profile, "--dump-config"),
620
+ cwd=self._cwd,
621
+ )
622
+
623
+ def _manifest_path(self) -> Path:
624
+ return self._profile_root / "package.json"
625
+
626
+ def _read_manifest(self) -> dict[str, object]:
627
+ return self._read_json(self._manifest_path(), required=True)
628
+
629
+ def _read_package(self, name: str) -> dict[str, object] | None:
630
+ return self._read_json(
631
+ self._profile_root / "node_modules" / Path(*name.split("/")) / "package.json",
632
+ required=False,
633
+ )
634
+
635
+ @staticmethod
636
+ def _read_json(path: Path, *, required: bool) -> dict[str, object] | None:
637
+ try:
638
+ if path.stat().st_size > _MAX_JSON_BYTES:
639
+ raise DshBridgeError("DSH manifest exceeds the supported size limit")
640
+ payload = json.loads(path.read_text(encoding="utf-8"))
641
+ except FileNotFoundError:
642
+ if required:
643
+ raise DshBridgeError("required DSH profile manifest is unavailable") from None
644
+ return None
645
+ except (OSError, UnicodeError, json.JSONDecodeError) as error:
646
+ raise DshBridgeError("DSH profile contains an unreadable JSON manifest") from error
647
+ if not isinstance(payload, dict):
648
+ raise DshBridgeError("DSH JSON manifest must be an object")
649
+ return payload
650
+
651
+ @staticmethod
652
+ def _dependencies(manifest: Mapping[str, object]) -> dict[str, str]:
653
+ raw = manifest.get("dependencies")
654
+ if raw is None:
655
+ return {}
656
+ if not isinstance(raw, dict) or any(
657
+ not isinstance(name, str) or not isinstance(value, str) for name, value in raw.items()
658
+ ):
659
+ raise DshBridgeError("DSH profile dependencies are invalid")
660
+ return dict(raw)
661
+
662
+ @staticmethod
663
+ def _bundles(manifest: Mapping[str, object]) -> list[str]:
664
+ dsh = manifest.get("dsh")
665
+ profile = dsh.get("profile") if isinstance(dsh, dict) else None
666
+ bundles = profile.get("bundles") if isinstance(profile, dict) else None
667
+ if bundles is None:
668
+ return []
669
+ if not isinstance(bundles, list) or any(not isinstance(item, str) for item in bundles):
670
+ raise DshBridgeError("DSH profile bundle order is invalid")
671
+ return list(bundles)
672
+
673
+ @staticmethod
674
+ def _bundle_patch(package: Mapping[str, object]) -> str | None:
675
+ dsh = package.get("dsh")
676
+ bundle = dsh.get("bundle") if isinstance(dsh, dict) else None
677
+ patch = bundle.get("patch") if isinstance(bundle, dict) else None
678
+ return patch.strip() if isinstance(patch, str) and patch.strip() else None
679
+
680
+ @staticmethod
681
+ def _client_declaration(package: Mapping[str, object]) -> Mapping[str, object] | None:
682
+ dsh = package.get("dsh")
683
+ client = dsh.get("client") if isinstance(dsh, dict) else None
684
+ return client if isinstance(client, dict) else None
685
+
686
+ @staticmethod
687
+ def _string_tuple(value: object) -> tuple[str, ...] | None:
688
+ if value is None:
689
+ return ()
690
+ if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
691
+ return None
692
+ return tuple(value)
693
+
694
+ @staticmethod
695
+ def _client_export(package: Mapping[str, object]) -> str | None:
696
+ exports = package.get("exports")
697
+ client = exports.get("./client") if isinstance(exports, dict) else None
698
+ if isinstance(client, str):
699
+ return client
700
+ default = client.get("default") if isinstance(client, dict) else None
701
+ return default if isinstance(default, str) else None
702
+
703
+ def _client_bundle_path(
704
+ self, name: str, package: Mapping[str, object]
705
+ ) -> Path | None:
706
+ declared = self._client_export(package)
707
+ if not declared:
708
+ return None
709
+ relative = Path(declared)
710
+ if relative.is_absolute() or ".." in relative.parts:
711
+ return None
712
+ root = (self._profile_root / "node_modules" / Path(*name.split("/"))).resolve()
713
+ target = (root / relative).resolve()
714
+ if not target.is_relative_to(root) or not target.is_file():
715
+ return None
716
+ return target
717
+
718
+ def _client_bundle_metadata(
719
+ self, name: str, package: Mapping[str, object]
720
+ ) -> DshClientBundle | None:
721
+ declaration = self._client_declaration(package)
722
+ if declaration is None or declaration.get("platform") != "web":
723
+ return None
724
+ inject = self._string_tuple(declaration.get("inject"))
725
+ external = self._string_tuple(declaration.get("external"))
726
+ target = self._client_bundle_path(name, package)
727
+ reason = ""
728
+ if inject is None or external is None:
729
+ reason = "dsh.client inject/external must be string arrays"
730
+ elif target is None:
731
+ reason = "exports[./client] does not resolve to a built bundle"
732
+ elif inject:
733
+ reason = "client bundle dependencies are not present in the Studio graph"
734
+ elif any(item not in _STUDIO_CLIENT_EXTERNALS for item in external):
735
+ reason = "client bundle requests unsupported external modules"
736
+ if target is None:
737
+ return DshClientBundle(
738
+ digest="",
739
+ content_bytes=0,
740
+ inject=inject or (),
741
+ external=external or (),
742
+ compatible=False,
743
+ incompatibility_reason=reason,
744
+ )
745
+ try:
746
+ size = target.stat().st_size
747
+ if size > _MAX_CLIENT_BUNDLE_BYTES:
748
+ reason = "client bundle exceeds the supported size limit"
749
+ content = b""
750
+ else:
751
+ content = target.read_bytes()
752
+ except OSError:
753
+ size = 0
754
+ content = b""
755
+ reason = "client bundle is unreadable"
756
+ return DshClientBundle(
757
+ digest=f"sha256:{hashlib.sha256(content).hexdigest()}" if content else "",
758
+ content_bytes=size,
759
+ inject=inject or (),
760
+ external=external or (),
761
+ compatible=not reason,
762
+ incompatibility_reason=reason,
763
+ )
764
+
765
+ def _require_bundle(self, name: str) -> None:
766
+ package = self._read_package(name)
767
+ patch = self._bundle_patch(package or {})
768
+ if package is None or patch is None:
769
+ raise DshPluginMutationError(f"{name} does not declare dsh.bundle.patch")
770
+ relative = Path(patch)
771
+ if relative.is_absolute() or ".." in relative.parts:
772
+ raise DshPluginMutationError(f"{name} declares an unsafe bundle patch path")
773
+ root = (self._profile_root / "node_modules" / Path(*name.split("/"))).resolve()
774
+ target = (root / relative).resolve()
775
+ if not target.is_relative_to(root) or not target.is_file():
776
+ raise DshPluginMutationError(f"{name} bundle patch is unavailable")
777
+
778
+ def _read_state(self, manifest: Mapping[str, object]) -> _ProfileState:
779
+ dependencies = set(self._dependencies(manifest))
780
+ bundle_dependencies = [
781
+ name
782
+ for name in dependencies
783
+ if self._bundle_patch(self._read_package(name) or {}) is not None
784
+ ]
785
+ active = [name for name in self._bundles(manifest) if name in dependencies]
786
+ stored = self._read_json(self._profile_root / _STATE_FILE, required=False) or {}
787
+ order_raw = stored.get("order")
788
+ disabled_raw = stored.get("disabled")
789
+ sources_raw = stored.get("sources")
790
+ order = (
791
+ [item for item in order_raw if isinstance(item, str) and item in bundle_dependencies]
792
+ if isinstance(order_raw, list)
793
+ else []
794
+ )
795
+ for name in [*active, *bundle_dependencies]:
796
+ if name not in order:
797
+ order.append(name)
798
+ disabled = (
799
+ [item for item in disabled_raw if isinstance(item, str) and item in bundle_dependencies]
800
+ if isinstance(disabled_raw, list)
801
+ else [name for name in bundle_dependencies if name not in active]
802
+ )
803
+ sources: dict[str, _SourceReceipt] = {}
804
+ if isinstance(sources_raw, dict):
805
+ for name, raw in sources_raw.items():
806
+ if name not in bundle_dependencies:
807
+ continue
808
+ if not isinstance(raw, dict):
809
+ raise DshPluginMutationError("DSH local source receipt is invalid")
810
+ digest = raw.get("digest")
811
+ kind = raw.get("kind")
812
+ artifact = raw.get("artifact")
813
+ dependency_spec = raw.get("dependencySpec") or raw.get("dependency_spec")
814
+ if (
815
+ isinstance(digest, str)
816
+ and re.fullmatch(r"sha256:[0-9a-f]{64}", digest)
817
+ and kind in {"directory", "tgz"}
818
+ and isinstance(artifact, str)
819
+ and isinstance(dependency_spec, str)
820
+ ):
821
+ sources[name] = {
822
+ "digest": digest,
823
+ "kind": kind,
824
+ "artifact": artifact,
825
+ "dependency_spec": dependency_spec,
826
+ }
827
+ else:
828
+ raise DshPluginMutationError("DSH local source receipt is invalid")
829
+ return {"order": order, "disabled": disabled, "sources": sources}
830
+
831
+ def _write_state(self, state: _ProfileState) -> None:
832
+ self._write_json(
833
+ self._profile_root / _STATE_FILE,
834
+ {
835
+ "version": 2,
836
+ "order": list(state["order"]),
837
+ "disabled": list(state["disabled"]),
838
+ "sources": {
839
+ name: {
840
+ "digest": receipt["digest"],
841
+ "kind": receipt["kind"],
842
+ "artifact": receipt["artifact"],
843
+ "dependencySpec": receipt["dependency_spec"],
844
+ }
845
+ for name, receipt in sorted(state["sources"].items())
846
+ },
847
+ },
848
+ )
849
+
850
+ def _write_active_bundles(
851
+ self,
852
+ manifest: dict[str, object],
853
+ state: _ProfileState,
854
+ ) -> None:
855
+ dependencies = set(self._dependencies(manifest))
856
+ existing = self._bundles(manifest)
857
+ builtins = [name for name in existing if name not in dependencies]
858
+ disabled = set(state["disabled"])
859
+ enabled = [name for name in state["order"] if name in dependencies and name not in disabled]
860
+ dsh = manifest.get("dsh") if isinstance(manifest.get("dsh"), dict) else {}
861
+ assert isinstance(dsh, dict)
862
+ profile = dsh.get("profile") if isinstance(dsh.get("profile"), dict) else {}
863
+ assert isinstance(profile, dict)
864
+ profile["bundles"] = [*builtins, *enabled]
865
+ dsh["profile"] = profile
866
+ manifest["dsh"] = dsh
867
+ self._write_json(self._manifest_path(), manifest)
868
+
869
+ def _prepare_source(self, source: str) -> _PreparedSource:
870
+ value = source.strip()
871
+ candidate = Path(value).expanduser()
872
+ if not candidate.is_absolute() or not candidate.exists():
873
+ return _PreparedSource(value, "", "tgz", "")
874
+ local = candidate.resolve()
875
+ with tempfile.TemporaryDirectory(prefix="agentengine-dsh-source-") as directory:
876
+ if local.is_dir():
877
+ # Import lazily: dsh_toolchain owns the pinned pnpm workflow and
878
+ # imports this bridge for validation.
879
+ from ksadk.plugins.dsh_toolchain import ( # noqa: PLC0415
880
+ DshPluginDeveloper,
881
+ DshToolchainManager,
882
+ )
883
+
884
+ output = Path(directory)
885
+ packed = DshPluginDeveloper(toolchain=DshToolchainManager()).pack(
886
+ local, output_dir=output
887
+ )
888
+ archive = Path(packed.artifact)
889
+ kind: Literal["directory", "tgz"] = "directory"
890
+ elif local.is_file() and local.name.endswith(".tgz"):
891
+ archive = local
892
+ kind = "tgz"
893
+ else:
894
+ raise DshPluginMutationError(
895
+ "local DSH plugin source must be a directory or .tgz archive"
896
+ )
897
+ try:
898
+ content = archive.read_bytes()
899
+ except OSError as error:
900
+ raise DshPluginMutationError("local DSH plugin source is unreadable") from error
901
+ digest_hex = hashlib.sha256(content).hexdigest()
902
+ digest = f"sha256:{digest_hex}"
903
+ root = (self._dsh_home / _IMMUTABLE_SOURCE_DIR / digest_hex).resolve()
904
+ expected_parent = (self._dsh_home / _IMMUTABLE_SOURCE_DIR).resolve()
905
+ if root.parent != expected_parent:
906
+ raise DshPluginMutationError("immutable DSH source path escaped its store")
907
+ target = root / "package.tgz"
908
+ root.mkdir(parents=True, exist_ok=True, mode=0o700)
909
+ if target.exists():
910
+ if hashlib.sha256(target.read_bytes()).hexdigest() != digest_hex:
911
+ raise DshPluginMutationError(
912
+ "immutable DSH source store contains a digest collision"
913
+ )
914
+ else:
915
+ temporary = root / f".package.{os.getpid()}.tmp"
916
+ try:
917
+ temporary.write_bytes(content)
918
+ temporary.chmod(0o400)
919
+ os.replace(temporary, target)
920
+ finally:
921
+ temporary.unlink(missing_ok=True)
922
+ target.chmod(0o400)
923
+ relative = target.relative_to(self._dsh_home).as_posix()
924
+ return _PreparedSource(str(target), digest, kind, relative)
925
+
926
+ def _verify_source_receipts(
927
+ self,
928
+ manifest: Mapping[str, object],
929
+ state: _ProfileState,
930
+ *,
931
+ names: Sequence[str] | None = None,
932
+ ) -> None:
933
+ dependencies = self._dependencies(manifest)
934
+ for name, spec in dependencies.items():
935
+ if _IMMUTABLE_SOURCE_DIR in spec and name not in state["sources"]:
936
+ raise DshPluginMutationError(
937
+ f"DSH plugin {name!r} is missing its immutable source receipt"
938
+ )
939
+ selected = names if names is not None else tuple(state["sources"])
940
+ store = (self._dsh_home / _IMMUTABLE_SOURCE_DIR).resolve()
941
+ for name in selected:
942
+ receipt = state["sources"].get(name)
943
+ if receipt is None:
944
+ continue
945
+ if dependencies.get(name) != receipt["dependency_spec"]:
946
+ raise DshPluginMutationError(
947
+ f"DSH plugin {name!r} dependency no longer matches its source receipt"
948
+ )
949
+ artifact = (self._dsh_home / receipt["artifact"]).resolve()
950
+ if not artifact.is_relative_to(store) or not artifact.is_file():
951
+ raise DshPluginMutationError(
952
+ f"DSH plugin {name!r} immutable source is unavailable"
953
+ )
954
+ try:
955
+ actual = f"sha256:{hashlib.sha256(artifact.read_bytes()).hexdigest()}"
956
+ except OSError as error:
957
+ raise DshPluginMutationError(
958
+ f"DSH plugin {name!r} immutable source is unreadable"
959
+ ) from error
960
+ if actual != receipt["digest"]:
961
+ raise DshPluginMutationError(
962
+ f"DSH plugin {name!r} immutable source digest changed"
963
+ )
964
+
965
+ @staticmethod
966
+ def _write_json(path: Path, payload: object) -> None:
967
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
968
+ descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
969
+ try:
970
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
971
+ json.dump(payload, handle, ensure_ascii=False, indent=2)
972
+ handle.write("\n")
973
+ handle.flush()
974
+ os.fsync(handle.fileno())
975
+ os.chmod(temporary, 0o600)
976
+ os.replace(temporary, path)
977
+ finally:
978
+ try:
979
+ os.unlink(temporary)
980
+ except FileNotFoundError:
981
+ pass
982
+
983
+ def _snapshot(self) -> dict[str, bytes | None]:
984
+ snapshot: dict[str, bytes | None] = {}
985
+ for name in _SNAPSHOT_FILES:
986
+ path = self._profile_root / name
987
+ try:
988
+ snapshot[name] = path.read_bytes()
989
+ except FileNotFoundError:
990
+ snapshot[name] = None
991
+ return snapshot
992
+
993
+ def _require_package_mutation_rollback(self, *, new_profile_allowed: bool) -> None:
994
+ """Reject package mutations unless their filesystem effects are reversible."""
995
+
996
+ if not self._profile_root.exists():
997
+ if new_profile_allowed:
998
+ return
999
+ raise DshPluginNotFoundError(f"DSH profile {self._profile!r} is not initialized")
1000
+ if not self._manifest_path().is_file():
1001
+ raise DshPluginMutationError(
1002
+ "existing DSH profile directory has no package manifest; refusing to mutate it"
1003
+ )
1004
+ if not (self._profile_root / "pnpm-lock.yaml").is_file():
1005
+ raise DshPluginMutationError(
1006
+ "existing DSH profile has no pnpm lockfile, so package rollback is unavailable"
1007
+ )
1008
+
1009
+ def _rollback(self, snapshot: Mapping[str, bytes | None], original: BaseException) -> None:
1010
+ try:
1011
+ if all(content is None for content in snapshot.values()):
1012
+ profiles_root = (self._dsh_home / "profiles").resolve()
1013
+ profile_root = self._profile_root.resolve()
1014
+ if profile_root.parent != profiles_root:
1015
+ raise DshPluginMutationError("refusing to clean an untrusted DSH profile path")
1016
+ if profile_root.exists():
1017
+ shutil.rmtree(profile_root, ignore_errors=False)
1018
+ return
1019
+ self._profile_root.mkdir(parents=True, exist_ok=True, mode=0o700)
1020
+ for name, content in snapshot.items():
1021
+ path = self._profile_root / name
1022
+ if content is None:
1023
+ path.unlink(missing_ok=True)
1024
+ else:
1025
+ path.write_bytes(content)
1026
+ path.chmod(0o600)
1027
+ if snapshot.get("pnpm-lock.yaml") is not None and self._manifest_path().is_file():
1028
+ self._plugin_command("install", "--frozen-lockfile")
1029
+ for name in ("package.json", _STATE_FILE):
1030
+ content = snapshot.get(name)
1031
+ path = self._profile_root / name
1032
+ if content is None:
1033
+ path.unlink(missing_ok=True)
1034
+ else:
1035
+ path.write_bytes(content)
1036
+ path.chmod(0o600)
1037
+ except BaseException as rollback_error:
1038
+ raise DshPluginMutationError(
1039
+ "DSH plugin mutation failed and profile rollback also failed"
1040
+ ) from rollback_error
1041
+ if isinstance(original, DshBridgeError):
1042
+ return
1043
+
1044
+ @staticmethod
1045
+ def _validate_source(source: str) -> None:
1046
+ value = source.strip()
1047
+ if (
1048
+ not value
1049
+ or len(value) > 2048
1050
+ or value.startswith("-")
1051
+ or any(character in source for character in ("\r", "\n", "\0"))
1052
+ or value in {".", ".."}
1053
+ or value.startswith(("file:", "link:"))
1054
+ or value.startswith(("./", "../", "file:./", "file:../", "link:./", "link:../"))
1055
+ ):
1056
+ raise ValueError("DSH plugin source must be a package, Git URL, or absolute local path")
1057
+
1058
+ @staticmethod
1059
+ def _validate_package_name(name: str) -> None:
1060
+ if not _PACKAGE_NAME.fullmatch(name):
1061
+ raise ValueError("invalid DSH plugin package name")
1062
+
1063
+ @staticmethod
1064
+ def _string(value: object) -> str:
1065
+ return value if isinstance(value, str) else ""
1066
+
1067
+ @staticmethod
1068
+ def _run_command(
1069
+ command: Sequence[str],
1070
+ cwd: Path,
1071
+ environment: Mapping[str, str],
1072
+ ) -> _CommandResult:
1073
+ try:
1074
+ completed = subprocess.run(
1075
+ list(command),
1076
+ cwd=cwd,
1077
+ env=dict(environment),
1078
+ check=False,
1079
+ capture_output=True,
1080
+ text=True,
1081
+ timeout=120,
1082
+ )
1083
+ except (OSError, subprocess.TimeoutExpired) as error:
1084
+ raise DshHostUnavailableError("DSH host command did not complete") from error
1085
+ if completed.returncode != 0:
1086
+ diagnostic = _redact_diagnostic(completed.stderr or completed.stdout)
1087
+ raise DshPluginMutationError(
1088
+ f"DSH host command failed with exit code {completed.returncode}"
1089
+ + (f": {diagnostic[-2000:]}" if diagnostic else "")
1090
+ )
1091
+ return _CommandResult(stdout=completed.stdout, stderr=completed.stderr)
1092
+
1093
+
1094
+ def _redact_diagnostic(value: str) -> str:
1095
+ value = re.sub(r"(https?://)[^/\s:@]+:[^/\s@]+@", r"\1[redacted]@", value, flags=re.I)
1096
+ return re.sub(
1097
+ r"((?:token|password|authorization|_authToken)\s*[:=]\s*)[^\s]+",
1098
+ r"\1[redacted]",
1099
+ value,
1100
+ flags=re.I,
1101
+ ).strip()
1102
+
1103
+
1104
+ __all__ = [
1105
+ "DshBridgeError",
1106
+ "DshBridgeHost",
1107
+ "DshClientBundle",
1108
+ "DshHostUnavailableError",
1109
+ "DshPluginApprovalRequired",
1110
+ "DshPluginInventory",
1111
+ "DshPluginMutationError",
1112
+ "DshPluginNotFoundError",
1113
+ "DshProfilePluginBridge",
1114
+ "DshProfileProjection",
1115
+ "dsh_subprocess_environment",
1116
+ ]