@shadowob/cloud 1.1.6 → 1.1.8

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 (198) hide show
  1. package/README.md +159 -11
  2. package/dist/{agent-browser-CERTMCDL.js → agent-browser-EI7FIK3X.js} +3 -3
  3. package/dist/{agent-browser-CIRZRIY4.js → agent-browser-YXE4ES6Q.js} +3 -3
  4. package/dist/{agent-pack-LF3O5TR4.js → agent-pack-35TFCZKP.js} +1 -1
  5. package/dist/{agent-pack-RQT27V7R.js → agent-pack-UOG6ZAUL.js} +1 -1
  6. package/dist/agentmemory-KP5O7GHB.js +101 -0
  7. package/dist/agentmemory-UV74POU5.js +100 -0
  8. package/dist/{airtable-BG2Q75G2.js → airtable-CXXS3YUN.js} +3 -3
  9. package/dist/{airtable-JCQXFM5D.js → airtable-YZ5JR5JC.js} +3 -3
  10. package/dist/{alipay-TZQI34RB.js → alipay-WED5P3XC.js} +3 -3
  11. package/dist/{alipay-MZX2XCDB.js → alipay-WJTVREMG.js} +3 -3
  12. package/dist/{amap-KPCLZYYL.js → amap-AIN23TQ7.js} +3 -3
  13. package/dist/{amap-5RQB3VGC.js → amap-F6GF7QKB.js} +3 -3
  14. package/dist/{atlassian-LGOEWYC7.js → atlassian-G6PM6UVM.js} +3 -3
  15. package/dist/{atlassian-TVS2A4IU.js → atlassian-IDR2NPJC.js} +3 -3
  16. package/dist/{baidu-appbuilder-QRRL3ETM.js → baidu-appbuilder-JKQIA5TG.js} +3 -3
  17. package/dist/{baidu-appbuilder-6UMESXHW.js → baidu-appbuilder-KVMCIFYH.js} +3 -3
  18. package/dist/{baidu-maps-HEPMVP5D.js → baidu-maps-GWMXV6YT.js} +3 -3
  19. package/dist/{baidu-maps-HXC4FBVP.js → baidu-maps-ZXGT6QZM.js} +3 -3
  20. package/dist/{baidu-netdisk-G5Q6B5NH.js → baidu-netdisk-J5SLQVWW.js} +3 -3
  21. package/dist/{baidu-netdisk-RS2K5W2M.js → baidu-netdisk-PONM3DY6.js} +3 -3
  22. package/dist/{baidu-smartprogram-JHD3XWF6.js → baidu-smartprogram-WJ5JMO6V.js} +3 -3
  23. package/dist/{baidu-smartprogram-EWTK5WKK.js → baidu-smartprogram-Y5WBR7RX.js} +3 -3
  24. package/dist/{browserbase-IUIYVYI7.js → browserbase-CGVQHRC4.js} +3 -3
  25. package/dist/{browserbase-JFO2PCIA.js → browserbase-PVWYTEZC.js} +3 -3
  26. package/dist/{canva-3YOFL7JS.js → canva-COCBQAT2.js} +3 -3
  27. package/dist/{canva-FMYN65SM.js → canva-RSTJSEX5.js} +3 -3
  28. package/dist/{chunk-ULOJLXQV.js → chunk-3CT6RQNM.js} +766 -482
  29. package/dist/{chunk-SIDBK5SZ.js → chunk-4YO3NA26.js} +1 -1
  30. package/dist/{chunk-RECNVWMT.js → chunk-6V7MW4HU.js} +17 -3
  31. package/dist/{chunk-2FWJSE6Z.js → chunk-EVV774KS.js} +1 -1
  32. package/dist/{chunk-SVMXSIMG.js → chunk-F6CQ6GAG.js} +2 -1
  33. package/dist/{chunk-JUPAE5IA.js → chunk-OL5VH6RN.js} +72 -69
  34. package/dist/{chunk-POSVEKIY.js → chunk-OYY64ZSX.js} +17 -3
  35. package/dist/{chunk-FSW4TNHN.js → chunk-P5Y6F2NH.js} +766 -482
  36. package/dist/{chunk-EEFMJYKB.js → chunk-PSK2SYZ3.js} +2 -1
  37. package/dist/{chunk-HRTBOZ7O.js → chunk-PYJRFKPN.js} +1 -1
  38. package/dist/{chunk-JY2HTT7Q.js → chunk-RMDY3W4V.js} +6 -0
  39. package/dist/{chunk-I3NRNCDR.js → chunk-X2SREECR.js} +6 -6
  40. package/dist/{chunk-XFJX4NWN.js → chunk-X5VOIA72.js} +6 -6
  41. package/dist/{chunk-CTNUKOQE.js → chunk-Y5BJ3EW2.js} +6 -0
  42. package/dist/{chunk-IXN3FU5J.js → chunk-Y6BKVDG7.js} +1 -1
  43. package/dist/{chunk-6P2K6QZR.js → chunk-ZGMWSSCC.js} +72 -69
  44. package/dist/{claude-plugin-577TAQVS.js → claude-plugin-FPN32WMT.js} +1 -1
  45. package/dist/{claude-plugin-L3MXJJ6J.js → claude-plugin-IYHOVTVL.js} +1 -1
  46. package/dist/cli.js +930 -149
  47. package/dist/{cloudflare-RDFPKMM5.js → cloudflare-U3RHJJKK.js} +3 -3
  48. package/dist/{cloudflare-HBBABPK6.js → cloudflare-ZHN7UGPX.js} +3 -3
  49. package/dist/{cnb-FLP3QX46.js → cnb-AMXC5I7D.js} +3 -3
  50. package/dist/{cnb-YAVVEYFB.js → cnb-P3IZ4JTD.js} +3 -3
  51. package/dist/console/index.html +1 -1
  52. package/dist/console/static/css/index.f4563d95.css +1 -0
  53. package/dist/console/static/js/index.020abc71.js +1 -0
  54. package/dist/{coze-E6VGRNLV.js → coze-66RYMKVB.js} +3 -3
  55. package/dist/{coze-C6PMDPBI.js → coze-YE3BINXP.js} +3 -3
  56. package/dist/{dashboard.command-SCAAQ23X.js → dashboard.command-BRPZCZER.js} +1 -1
  57. package/dist/{dashboard.command-H5DIGLUR.js → dashboard.command-GUHSJ2CN.js} +1 -1
  58. package/dist/{dingtalk-JNRNRN7E.js → dingtalk-4RFQG7N2.js} +3 -3
  59. package/dist/{dingtalk-WZGGIAHJ.js → dingtalk-VNFKXD2P.js} +3 -3
  60. package/dist/{douyin-miniprogram-AIJPPIZH.js → douyin-miniprogram-UEALAGOS.js} +3 -3
  61. package/dist/{douyin-miniprogram-HCYZ5NBW.js → douyin-miniprogram-UNB6UO2I.js} +3 -3
  62. package/dist/{figma-2YYNSCDX.js → figma-A264OWU5.js} +3 -3
  63. package/dist/{figma-RYOBMENP.js → figma-Y4TGSDZP.js} +3 -3
  64. package/dist/{firebase-OYSY6HPT.js → firebase-AI3MAGYG.js} +3 -3
  65. package/dist/{firebase-2IJDDBXX.js → firebase-ZGQARUIH.js} +3 -3
  66. package/dist/{firecrawl-2T3SBUW7.js → firecrawl-2JW7DMTH.js} +3 -3
  67. package/dist/{firecrawl-IYYXLAZM.js → firecrawl-UURQ5P5N.js} +3 -3
  68. package/dist/{flyai-QS5Q6FJR.js → flyai-EJGDMYFA.js} +3 -3
  69. package/dist/{flyai-7FJ4TRAG.js → flyai-ZFMZBBHJ.js} +3 -3
  70. package/dist/{gitagent-MWI75OIX.js → gitagent-5SDBYFNA.js} +1 -1
  71. package/dist/{gitagent-YBMWY7NZ.js → gitagent-ODXPCR4X.js} +1 -1
  72. package/dist/{gitee-3N7OFOM7.js → gitee-5UMJ4BC7.js} +3 -3
  73. package/dist/{gitee-KVNK6KLZ.js → gitee-D6NAZTCO.js} +3 -3
  74. package/dist/{github-LUEC2LID.js → github-JBLDKIA3.js} +3 -3
  75. package/dist/{github-XRO5Z3GC.js → github-PZQAVEZP.js} +3 -3
  76. package/dist/{google-ads-VPKWTX67.js → google-ads-BIFQOJ5M.js} +3 -3
  77. package/dist/{google-ads-A3QAJI4D.js → google-ads-QU3LJE4O.js} +3 -3
  78. package/dist/{google-analytics-C4UR5ZR2.js → google-analytics-7VZ6YZVA.js} +3 -3
  79. package/dist/{google-analytics-XDYZA2B7.js → google-analytics-HXMPCL5V.js} +3 -3
  80. package/dist/{google-workspace-YX35SHHX.js → google-workspace-6SEBJ4VA.js} +2 -2
  81. package/dist/{google-workspace-LL3EWVHH.js → google-workspace-L3AMJLCF.js} +2 -2
  82. package/dist/{huawei-xiaoyi-KPWLTSHB.js → huawei-xiaoyi-C6QIJMPM.js} +3 -3
  83. package/dist/{huawei-xiaoyi-6BSMGJHR.js → huawei-xiaoyi-JGLXWU5P.js} +3 -3
  84. package/dist/{hubspot-FTIEMNZO.js → hubspot-LACJGE6D.js} +3 -3
  85. package/dist/{hubspot-DIUHGEDI.js → hubspot-XWPRO4KZ.js} +3 -3
  86. package/dist/{huggingface-UUXK2RHK.js → huggingface-26QQZK4C.js} +3 -3
  87. package/dist/{huggingface-MJCOXA7E.js → huggingface-CQICNA2R.js} +3 -3
  88. package/dist/index.d.ts +1338 -1
  89. package/dist/index.js +1364 -226
  90. package/dist/{inference-ai-image-generation-PXV6IG4U.js → inference-ai-image-generation-5KYIUWT6.js} +3 -3
  91. package/dist/{inference-ai-image-generation-CMI6R5T3.js → inference-ai-image-generation-J2NYDCLZ.js} +3 -3
  92. package/dist/{inference-sh-7AZOLEFI.js → inference-sh-5SWQTK73.js} +3 -3
  93. package/dist/{inference-sh-ABQOD3YF.js → inference-sh-PTQF6T3R.js} +3 -3
  94. package/dist/{init.command-WGKN3GC6.js → init.command-C7UKPK2Y.js} +3 -3
  95. package/dist/{init.command-775GLXTC.js → init.command-UNL66BMR.js} +3 -3
  96. package/dist/{klaviyo-LDPBWBSS.js → klaviyo-4UNPMBFT.js} +3 -3
  97. package/dist/{klaviyo-6K5YEFNH.js → klaviyo-SLYNEULT.js} +3 -3
  98. package/dist/{kuaidi100-HGFM5VK2.js → kuaidi100-HZKV5AIS.js} +3 -3
  99. package/dist/{kuaidi100-UHPFCVXP.js → kuaidi100-ZQUW7GHH.js} +3 -3
  100. package/dist/lark-HQUZNHDI.js +382 -0
  101. package/dist/lark-PAV7XWJS.js +381 -0
  102. package/dist/{linear-T4ORUP7N.js → linear-PYGQ5SLK.js} +3 -3
  103. package/dist/{linear-7QFSFPOD.js → linear-VJLYNTUF.js} +3 -3
  104. package/dist/{lovart-PDUXRUHJ.js → lovart-KC6SVNAJ.js} +3 -3
  105. package/dist/{lovart-QO3SK55T.js → lovart-WVKY4RR4.js} +3 -3
  106. package/dist/{meta-ads-SCNFI45S.js → meta-ads-E6XT33GI.js} +3 -3
  107. package/dist/{meta-ads-V6XPZWX3.js → meta-ads-RJ6DWRYN.js} +3 -3
  108. package/dist/{miclaw-TPPPS2WN.js → miclaw-4BA3A2YN.js} +3 -3
  109. package/dist/{miclaw-5CNTW7VV.js → miclaw-LUV6DCHX.js} +3 -3
  110. package/dist/{model-provider-KFB76XV5.js → model-provider-SYXJZ3JD.js} +1 -1
  111. package/dist/{model-provider-AVSFJSZP.js → model-provider-U7NEYA3Y.js} +1 -1
  112. package/dist/nature-skills-G76ABIWZ.js +143 -0
  113. package/dist/nature-skills-SQHMFXKT.js +142 -0
  114. package/dist/{notion-WFA7KGZZ.js → notion-2JZAKOFP.js} +1 -1
  115. package/dist/{notion-FZK76MN2.js → notion-O3NO5TJH.js} +1 -1
  116. package/dist/{oceanengine-3JZUS3PP.js → oceanengine-D23UZGVB.js} +3 -3
  117. package/dist/{oceanengine-5BRIJVJE.js → oceanengine-WDK2OXX5.js} +3 -3
  118. package/dist/{opencli-PFXHGCS2.js → opencli-36P63YNU.js} +3 -3
  119. package/dist/{opencli-VIGRJTGH.js → opencli-SUHDFR33.js} +3 -3
  120. package/dist/{paypal-Z5JYHIWD.js → paypal-K27SUW3B.js} +3 -3
  121. package/dist/{paypal-33UADIPR.js → paypal-OPZ3KOV5.js} +3 -3
  122. package/dist/{playwright-SQAQ3DZG.js → playwright-2ULT3NIC.js} +3 -3
  123. package/dist/{playwright-MG5WHK47.js → playwright-3Q7LBILG.js} +3 -3
  124. package/dist/{plugins-HZBWK3WQ.js → plugins-2MITZ4ZD.js} +2 -2
  125. package/dist/{plugins-I4GD5SZX.js → plugins-UK2QWD6G.js} +2 -2
  126. package/dist/{posthog-MU5MAJOQ.js → posthog-E3EHXLAN.js} +3 -3
  127. package/dist/{posthog-RJRRKDWB.js → posthog-KPJVLGX6.js} +3 -3
  128. package/dist/{salesforce-34FVIJTG.js → salesforce-FGPNG7FB.js} +3 -3
  129. package/dist/{salesforce-3QZ6OFVO.js → salesforce-TVHISKBC.js} +3 -3
  130. package/dist/{sentry-PIWW46VA.js → sentry-BZ3J3MZM.js} +3 -3
  131. package/dist/{sentry-MCIRMACU.js → sentry-XC57YRAJ.js} +3 -3
  132. package/dist/{seo-suite-WJXMA3S4.js → seo-suite-2MDEDLAB.js} +3 -3
  133. package/dist/{seo-suite-4SQ3I67Q.js → seo-suite-U75O3QP6.js} +3 -3
  134. package/dist/{serve.command-2AHJI665.js → serve.command-G5RVQFUD.js} +3 -3
  135. package/dist/{serve.command-Q46LHQG6.js → serve.command-PYGDG7K3.js} +3 -3
  136. package/dist/{shadowob-PRSMI5MW.js → shadowob-3QZ7DLDW.js} +158 -31
  137. package/dist/{shadowob-JELOWHWX.js → shadowob-CJLOEKFP.js} +158 -31
  138. package/dist/{sherlock-2PKY2E2Y.js → sherlock-CQFUHKDH.js} +3 -3
  139. package/dist/{sherlock-C5ZWPPVT.js → sherlock-DONK2I6E.js} +3 -3
  140. package/dist/{shopify-GL3NFVGE.js → shopify-NO5GI3WD.js} +3 -3
  141. package/dist/{shopify-R4G3UXM6.js → shopify-VW2KLKH5.js} +3 -3
  142. package/dist/{skill-discovery-YPXXV622.js → skill-discovery-6JEPPKKM.js} +3 -3
  143. package/dist/{skill-discovery-7INAUP4D.js → skill-discovery-PWRAVGIS.js} +3 -3
  144. package/dist/skills/shadowob-cli/SKILL.md +33 -22
  145. package/dist/{stripe-LJNPQ3CQ.js → stripe-HCNCKG4C.js} +1 -1
  146. package/dist/{stripe-C22RR4ZS.js → stripe-IU3KTJ4H.js} +1 -1
  147. package/dist/{supabase-IRNQ54FJ.js → supabase-KRL7JW2D.js} +3 -3
  148. package/dist/{supabase-N4ONFJNQ.js → supabase-TYEBTZNO.js} +3 -3
  149. package/dist/{taobao-aipaas-LRR4GMO3.js → taobao-aipaas-PEUIDOYP.js} +3 -3
  150. package/dist/{taobao-aipaas-RVKORSF4.js → taobao-aipaas-SA5E4MZA.js} +3 -3
  151. package/dist/{tapd-TMQRSMFG.js → tapd-6DDIUPVQ.js} +3 -3
  152. package/dist/{tapd-3JPVJ7XH.js → tapd-OTYLSZGY.js} +3 -3
  153. package/dist/{tencent-ads-UHC6OPBV.js → tencent-ads-OW2TAMH5.js} +3 -3
  154. package/dist/{tencent-ads-IGD33LO7.js → tencent-ads-XTQZ27YT.js} +3 -3
  155. package/dist/{tencent-docs-C3A4J3CJ.js → tencent-docs-6D6A2VCO.js} +3 -3
  156. package/dist/{tencent-docs-O2SC4FHL.js → tencent-docs-K3TMUIWD.js} +3 -3
  157. package/dist/{tencent-maps-OQOKHVW2.js → tencent-maps-IZYWITJZ.js} +3 -3
  158. package/dist/{tencent-maps-HMMWMNF4.js → tencent-maps-SWI7CLQY.js} +3 -3
  159. package/dist/text-to-cad-B2UP6PKA.js +192 -0
  160. package/dist/text-to-cad-I4B6VBFV.js +193 -0
  161. package/dist/{vercel-KOXDDTHX.js → vercel-CCKRC76D.js} +3 -3
  162. package/dist/{vercel-OLNVDWMG.js → vercel-SBGEMIJJ.js} +3 -3
  163. package/dist/{webflow-OMJKZM54.js → webflow-C3EHNNSN.js} +3 -3
  164. package/dist/{webflow-FULU5Q2I.js → webflow-ZFBJH4CR.js} +3 -3
  165. package/dist/{wechat-miniprogram-skyline-KYCDMQNW.js → wechat-miniprogram-skyline-VNCRERHX.js} +3 -3
  166. package/dist/{wechat-miniprogram-skyline-VR4FVIQL.js → wechat-miniprogram-skyline-Z5JQUV5Q.js} +3 -3
  167. package/dist/{wechat-pay-BCMAJ6UW.js → wechat-pay-RPDKPUEB.js} +3 -3
  168. package/dist/{wechat-pay-YQQKXVUI.js → wechat-pay-XVDGJRF2.js} +3 -3
  169. package/dist/{wonda-NGWIORYN.js → wonda-EL2P44S7.js} +3 -3
  170. package/dist/{wonda-RBABXFNM.js → wonda-XK5JK4X3.js} +3 -3
  171. package/dist/{wordpress-woocommerce-RNA5HB3N.js → wordpress-woocommerce-4MEE5A2M.js} +3 -3
  172. package/dist/{wordpress-woocommerce-RDIUTHYT.js → wordpress-woocommerce-6ECNM2QU.js} +3 -3
  173. package/dist/{wps-LUWHMZQQ.js → wps-E4OZEMOF.js} +3 -3
  174. package/dist/{wps-DAEFQHDE.js → wps-JNQUC4JS.js} +3 -3
  175. package/dist/{yuque-HCHTJWNI.js → yuque-GLAAOS7X.js} +3 -3
  176. package/dist/{yuque-KRH5O74J.js → yuque-MEF6VFLJ.js} +3 -3
  177. package/images/RUNNERS.md +15 -0
  178. package/images/cc-connect-runner/entrypoint.mjs +228 -0
  179. package/images/claude-runner/RUNNER.md +5 -3
  180. package/images/codex-runner/RUNNER.md +5 -3
  181. package/images/gemini-runner/RUNNER.md +5 -2
  182. package/images/hermes-runner/RUNNER.md +4 -2
  183. package/images/hermes-runner/entrypoint.mjs +269 -1
  184. package/images/openclaw-runner/Dockerfile +1 -0
  185. package/images/openclaw-runner/RUNNER.md +3 -0
  186. package/images/openclaw-runner/entrypoint.mjs +249 -1
  187. package/images/openclaw-runner/warm-runtime-deps.mjs +1 -3
  188. package/images/opencode-runner/RUNNER.md +5 -3
  189. package/package.json +3 -3
  190. package/templates/agent-marketplace-buddy.template.json +4 -1
  191. package/templates/bmad-method-buddy.template.json +4 -1
  192. package/templates/code-trainer.template.json +331 -0
  193. package/templates/gstack-buddy.template.json +4 -1
  194. package/templates/little-match-girl.template.json +10 -3
  195. package/dist/console/static/css/index.7f91f806.css +0 -1
  196. package/dist/console/static/js/index.4487e1ff.js +0 -1
  197. package/dist/lark-6LNA3LUQ.js +0 -103
  198. package/dist/lark-URVBZNS4.js +0 -102
package/dist/index.js CHANGED
@@ -1,15 +1,3 @@
1
- import {
2
- createConsoleCommand
3
- } from "./chunk-HRTBOZ7O.js";
4
- import {
5
- createInitCommand
6
- } from "./chunk-2FWJSE6Z.js";
7
- import {
8
- formatProvisionState,
9
- loadProvisionState,
10
- mergeProvisionState,
11
- saveProvisionState
12
- } from "./chunk-7VMRQ7MG.js";
13
1
  import {
14
2
  CLOUD_SAAS_RUNTIME_KEY,
15
3
  attachCloudSaasProvisionState,
@@ -26,7 +14,13 @@ import {
26
14
  toProviderSecretEnvKey,
27
15
  validateCloudSaasConfigSnapshot,
28
16
  withLegacyEnvAliases
29
- } from "./chunk-I3NRNCDR.js";
17
+ } from "./chunk-X2SREECR.js";
18
+ import {
19
+ createConsoleCommand
20
+ } from "./chunk-PYJRFKPN.js";
21
+ import {
22
+ createInitCommand
23
+ } from "./chunk-EVV774KS.js";
30
24
  import {
31
25
  RUNNER_AGENTS_VOLUME_NAME,
32
26
  RUNNER_CONFIG_MOUNT_PATH,
@@ -39,6 +33,7 @@ import {
39
33
  applyRuntimeEnvRefPolicy,
40
34
  buildOpenClawConfig,
41
35
  collectPluginBuildEnvVars,
36
+ collectPluginRuntimeEnvOmitKeys,
42
37
  collectPluginRuntimeExtensions,
43
38
  collectRuntimeEnvFields,
44
39
  collectRuntimeEnvRefPolicy,
@@ -56,11 +51,17 @@ import {
56
51
  runtimeContextEnv,
57
52
  runtimeStatePvcName,
58
53
  validateCloudConfig
59
- } from "./chunk-FSW4TNHN.js";
54
+ } from "./chunk-P5Y6F2NH.js";
60
55
  import {
61
56
  deepMerge,
62
57
  getPluginRegistry
63
- } from "./chunk-JUPAE5IA.js";
58
+ } from "./chunk-OL5VH6RN.js";
59
+ import {
60
+ formatProvisionState,
61
+ loadProvisionState,
62
+ mergeProvisionState,
63
+ saveProvisionState
64
+ } from "./chunk-7VMRQ7MG.js";
64
65
  import {
65
66
  log
66
67
  } from "./chunk-HUICDC56.js";
@@ -206,9 +207,9 @@ var RAW_PLUGIN_LIBRARY = String.raw`[
206
207
  "type": "array",
207
208
  "description": "Binding rules connecting buddies to agents and channels"
208
209
  },
209
- "playLaunch": {
210
+ "greeting": {
210
211
  "type": "object",
211
- "description": "Landing behavior for play launch flows, including default channel and greeting"
212
+ "description": "Greeting and entry-channel behavior for provisioned Shadow spaces"
212
213
  }
213
214
  }
214
215
  },
@@ -247,7 +248,7 @@ var RAW_PLUGIN_LIBRARY = String.raw`[
247
248
  {
248
249
  "id": "lark",
249
250
  "name": "Lark / Feishu",
250
- "description": "Lark and Feishu workspace operations for messages, docs, Base, sheets, calendar, mail, tasks, meetings, approvals, and weekly execution workflows.",
251
+ "description": "Lark, Feishu, and Meegle workspace operations for messages, docs, Base, sheets, calendar, mail, tasks, meetings, approvals, projects, and weekly execution workflows.",
251
252
  "version": "1.0.0",
252
253
  "category": "communication",
253
254
  "capabilities": [
@@ -255,18 +256,19 @@ var RAW_PLUGIN_LIBRARY = String.raw`[
255
256
  "data-source",
256
257
  "action",
257
258
  "cli",
258
- "mcp",
259
259
  "skill"
260
260
  ],
261
261
  "tags": [
262
262
  "lark",
263
263
  "feishu",
264
+ "meegle",
264
265
  "docs",
265
266
  "base",
266
267
  "calendar",
267
268
  "messenger",
269
+ "projects",
268
270
  "skills",
269
- "mcp"
271
+ "cli"
270
272
  ],
271
273
  "authType": "api-key",
272
274
  "website": "https://open.feishu.cn",
@@ -275,7 +277,7 @@ var RAW_PLUGIN_LIBRARY = String.raw`[
275
277
  "manifest": {
276
278
  "id": "lark",
277
279
  "name": "Lark / Feishu",
278
- "description": "Lark and Feishu workspace operations for messages, docs, Base, sheets, calendar, mail, tasks, meetings, approvals, and weekly execution workflows.",
280
+ "description": "Lark, Feishu, and Meegle workspace operations for messages, docs, Base, sheets, calendar, mail, tasks, meetings, approvals, projects, and weekly execution workflows.",
279
281
  "version": "1.0.0",
280
282
  "category": "communication",
281
283
  "icon": "messages-square",
@@ -287,20 +289,20 @@ var RAW_PLUGIN_LIBRARY = String.raw`[
287
289
  {
288
290
  "key": "LARKSUITE_CLI_APP_ID",
289
291
  "label": "App ID",
290
- "description": "Feishu or Lark app ID used by lark-cli and Lark MCP.",
292
+ "description": "Feishu or Lark app ID used by lark-cli. Get it from the app console.",
291
293
  "required": true,
292
294
  "sensitive": false,
293
295
  "placeholder": "cli_xxx",
294
- "helpUrl": "https://open.feishu.cn/document/home/introduction-to-custom-app-development/self-built-application-development-process"
296
+ "helpUrl": "https://open.feishu.cn/app"
295
297
  },
296
298
  {
297
299
  "key": "LARKSUITE_CLI_APP_SECRET",
298
300
  "label": "App secret",
299
- "description": "App secret from the Feishu or Lark developer console.",
301
+ "description": "App secret from the Feishu or Lark app console. Cloud writes this into lark-cli config.json instead of raw app env vars so bot CLI calls can mint tenant tokens correctly.",
300
302
  "required": true,
301
303
  "sensitive": true,
302
304
  "placeholder": "App secret",
303
- "helpUrl": "https://open.feishu.cn/document/home/introduction-to-custom-app-development/self-built-application-development-process"
305
+ "helpUrl": "https://open.feishu.cn/app"
304
306
  },
305
307
  {
306
308
  "key": "LARKSUITE_CLI_BRAND",
@@ -308,7 +310,60 @@ var RAW_PLUGIN_LIBRARY = String.raw`[
308
310
  "description": "Use feishu for China tenants or lark for global tenants.",
309
311
  "required": false,
310
312
  "sensitive": false,
311
- "placeholder": "feishu"
313
+ "placeholder": "feishu",
314
+ "helpUrl": "https://open.feishu.cn/app"
315
+ },
316
+ {
317
+ "key": "LARKSUITE_CLI_DEFAULT_AS",
318
+ "label": "Default identity",
319
+ "description": "Default lark-cli identity: bot, user, or auto. Defaults to bot.",
320
+ "required": false,
321
+ "sensitive": false,
322
+ "placeholder": "bot"
323
+ },
324
+ {
325
+ "key": "LARKSUITE_CLI_STRICT_MODE",
326
+ "label": "Strict mode",
327
+ "description": "Restrict lark-cli to bot, user, or off. Defaults to bot.",
328
+ "required": false,
329
+ "sensitive": false,
330
+ "placeholder": "bot"
331
+ },
332
+ {
333
+ "key": "MEEGLE_HOST",
334
+ "label": "Meegle host",
335
+ "description": "Meegle site domain, such as project.feishu.cn, meegle.com, or a tenant host.",
336
+ "required": false,
337
+ "sensitive": false,
338
+ "placeholder": "project.feishu.cn",
339
+ "helpUrl": "https://github.com/larksuite/meegle-cli#configuration"
340
+ },
341
+ {
342
+ "key": "MEEGLE_USER_ACCESS_TOKEN",
343
+ "label": "Meegle user access token",
344
+ "description": "Optional Meegle user access token for direct CLI auth. Rotate this token when Meegle returns 401.",
345
+ "required": false,
346
+ "sensitive": true,
347
+ "placeholder": "u-...",
348
+ "helpUrl": "https://github.com/larksuite/meegle-cli#configuration"
349
+ },
350
+ {
351
+ "key": "MEEGLE_ACCESS_TOKEN_HEADER",
352
+ "label": "Meegle token header",
353
+ "description": "Optional custom Meegle token header. Empty uses Authorization: Bearer <token>.",
354
+ "required": false,
355
+ "sensitive": false,
356
+ "placeholder": "x-meegle-auth",
357
+ "helpUrl": "https://github.com/larksuite/meegle-cli#configuration"
358
+ },
359
+ {
360
+ "key": "MEEGLE_USER_AGENT",
361
+ "label": "Meegle user agent suffix",
362
+ "description": "Optional caller suffix appended to the Meegle CLI User-Agent.",
363
+ "required": false,
364
+ "sensitive": false,
365
+ "placeholder": "shadow-cloud",
366
+ "helpUrl": "https://github.com/larksuite/meegle-cli#configuration"
312
367
  }
313
368
  ]
314
369
  },
@@ -317,18 +372,19 @@ var RAW_PLUGIN_LIBRARY = String.raw`[
317
372
  "data-source",
318
373
  "action",
319
374
  "cli",
320
- "mcp",
321
375
  "skill"
322
376
  ],
323
377
  "tags": [
324
378
  "lark",
325
379
  "feishu",
380
+ "meegle",
326
381
  "docs",
327
382
  "base",
328
383
  "calendar",
329
384
  "messenger",
385
+ "projects",
330
386
  "skills",
331
- "mcp"
387
+ "cli"
332
388
  ],
333
389
  "popularity": 99
334
390
  },
@@ -336,28 +392,32 @@ var RAW_PLUGIN_LIBRARY = String.raw`[
336
392
  {
337
393
  "key": "LARKSUITE_CLI_APP_ID",
338
394
  "label": "App ID",
339
- "description": "Feishu or Lark app ID used by lark-cli and Lark MCP.",
395
+ "description": "Feishu or Lark app ID used by lark-cli. Get it from the app console.",
340
396
  "sensitive": false
341
397
  },
342
398
  {
343
399
  "key": "LARKSUITE_CLI_APP_SECRET",
344
400
  "label": "App secret",
345
- "description": "App secret from the Feishu or Lark developer console.",
401
+ "description": "App secret from the Feishu or Lark app console. Cloud writes this into lark-cli config.json instead of raw app env vars so bot CLI calls can mint tenant tokens correctly.",
346
402
  "sensitive": true
347
403
  }
348
404
  ],
349
405
  "readme": {
350
406
  "title": "Lark / Feishu Plugin",
351
- "excerpt": "Lark / Feishu Plugin\n\nLark / Feishu connects a Buddy to workspace operations across messages, Docs, Base, Sheets, Calendar, Mail, Tasks, Meetings, approvals, and weekly execution workflows.\n\nConfiguration Keys\n\n Key Required Sensitive Description \n \n LARKSUITE CLI APP ID Yes No Feishu or Lark app ID used by lark cli and Lark MCP. \n LARKSUITE CLI APP SECRET Yes Yes App secret from the Feishu or Lark developer console. \n LARKSUITE CLI BRAND No No Use feishu for China tenants or lark for global tenants. Defaults to feishu. \n\nSetup\n\n1. Open the Feishu or Lark developer console.\n2. Create a self built application.\n3. Grant the APIs needed by the Buddy, such as Messenger, Docs, Base, Sheets, Calendar, Mail, Tasks, Meetings, or approvals.\n4. Copy the app ID into LARKSUITE CLI APP ID.\n5. Copy the app secret into LARKSUITE CLI APP SECRET.\n6. Set LARKSUITE CLI BRAND to match the tenant.\n7. Deploy the Buddy and verify with a read only document, Base, or calendar lookup.\n\nRuntime Assets\n\n Installs the official @larksuite/cli package.\n Registers the official @larksuiteoapi/lark mcp MCP server through npx.\n Mounts official Lark CLI agent skills under /workspace/.agents/plugin skills/lark.\n Adds verification checks for the CLI and mounted skills.\n\nReferences\n\n Lark CLI\n Lark custom application development",
407
+ "excerpt": "Lark / Feishu Plugin\n\nLark / Feishu connects a Buddy to workspace operations across messages, Docs, Base, Sheets, Calendar, Mail, Tasks, Meetings, approvals, Meegle work items, and weekly execution workflows.\n\nConfiguration Keys\n\n Key Required Sensitive Description \n \n LARKSUITE CLI APP ID Yes No Feishu or Lark app ID used by lark cli. \n LARKSUITE CLI APP SECRET Yes Yes App secret from the Feishu or Lark developer console. \n LARKSUITE CLI BRAND No No Use feishu for China tenants or lark for global tenants. Defaults to feishu. \n LARKSUITE CLI DEFAULT AS No No Default lark cli identity: bot, user, or auto. Defaults to bot. \n LARKSUITE CLI STRICT MODE No No Restrict lark cli to bot, user, or off. Defaults to bot. \n MEEGLE HOST No No Meegle site domain, such as project.feishu.cn, meegle.com, or a tenant host. \n MEEGLE USER ACCESS TOKEN No Yes Optional Meegle user access token for direct CLI auth. \n MEEGLE ACCESS TOKEN HEADER No No Optional custom Meegle token header. Empty uses Authorization: Bearer <token . \n MEEGLE USER AGENT No No Optional caller suffix appended to the Meegle CLI User Agent. \n\nCredential Source URLs\n\n Environment variable Where to get it \n \n LARKSUITE CLI APP ID Feishu China app console: <https://open.feishu.cn/app \n LARKSUITE CLI APP SECRET Feishu China app console: <https://open.feishu.cn/app \n LARKSUITE CLI BRAND Use feishu for apps from <https://open.feishu.cn/app ; use lark for global Lark tenants. \n MEEGLE HOST The Meegle/Lark Project tenant host, for example <https://project.feishu.cn . \n MEEGLE USER ACCESS TOKEN Meegle CLI direct env token configurat",
352
408
  "headings": [
353
409
  "Lark / Feishu Plugin",
354
410
  "Configuration Keys",
411
+ "Credential Source URLs",
355
412
  "Setup",
413
+ "Lark CLI Credential Model",
414
+ "Knowledge Base Access",
415
+ "Local Plugin Test",
356
416
  "Runtime Assets",
357
417
  "References"
358
418
  ]
359
419
  },
360
- "searchText": "lark\nlark / feishu\nlark and feishu workspace operations for messages, docs, base, sheets, calendar, mail, tasks, meetings, approvals, and weekly execution workflows.\ncommunication\ntool data-source action cli mcp skill\nlark feishu docs base calendar messenger skills mcp\nlarksuite_cli_app_id app id feishu or lark app id used by lark-cli and lark mcp. larksuite_cli_app_secret app secret app secret from the feishu or lark developer console. larksuite_cli_brand workspace brand use feishu for china tenants or lark for global tenants.\nlark / feishu plugin\nlark / feishu plugin configuration keys setup runtime assets references\nlark / feishu plugin\n\nlark / feishu connects a buddy to workspace operations across messages, docs, base, sheets, calendar, mail, tasks, meetings, approvals, and weekly execution workflows.\n\nconfiguration keys\n\n key required sensitive description \n \n larksuite cli app id yes no feishu or lark app id used by lark cli and lark mcp. \n larksuite cli app secret yes yes app secret from the feishu or lark developer console. \n larksuite cli brand no no use feishu for china tenants or lark for global tenants. defaults to feishu. \n\nsetup\n\n1. open the feishu or lark developer console.\n2. create a self built application.\n3. grant the apis needed by the buddy, such as messenger, docs, base, sheets, calendar, mail, tasks, meetings, or approvals.\n4. copy the app id into larksuite cli app id.\n5. copy the app secret into larksuite cli app secret.\n6. set larksuite cli brand to match the tenant.\n7. deploy the buddy and verify with a read only document, base, or calendar lookup.\n\nruntime assets\n\n installs the official @larksuite/cli package.\n registers the official @larksuiteoapi/lark mcp mcp server through npx.\n mounts official lark cli agent skills under /workspace/.agents/plugin skills/lark.\n adds verification checks for the cli and mounted skills.\n\nreferences\n\n lark cli\n lark custom application development"
420
+ "searchText": "lark\nlark / feishu\nlark, feishu, and meegle workspace operations for messages, docs, base, sheets, calendar, mail, tasks, meetings, approvals, projects, and weekly execution workflows.\ncommunication\ntool data-source action cli skill\nlark feishu meegle docs base calendar messenger projects skills cli\nlarksuite_cli_app_id app id feishu or lark app id used by lark-cli. get it from the app console. larksuite_cli_app_secret app secret app secret from the feishu or lark app console. cloud writes this into lark-cli config.json instead of raw app env vars so bot cli calls can mint tenant tokens correctly. larksuite_cli_brand workspace brand use feishu for china tenants or lark for global tenants. larksuite_cli_default_as default identity default lark-cli identity: bot, user, or auto. defaults to bot. larksuite_cli_strict_mode strict mode restrict lark-cli to bot, user, or off. defaults to bot. meegle_host meegle host meegle site domain, such as project.feishu.cn, meegle.com, or a tenant host. meegle_user_access_token meegle user access token optional meegle user access token for direct cli auth. rotate this token when meegle returns 401. meegle_access_token_header meegle token header optional custom meegle token header. empty uses authorization: bearer <token>. meegle_user_agent meegle user agent suffix optional caller suffix appended to the meegle cli user-agent.\nlark / feishu plugin\nlark / feishu plugin configuration keys credential source urls setup lark cli credential model knowledge base access local plugin test runtime assets references\nlark / feishu plugin\n\nlark / feishu connects a buddy to workspace operations across messages, docs, base, sheets, calendar, mail, tasks, meetings, approvals, meegle work items, and weekly execution workflows.\n\nconfiguration keys\n\n key required sensitive description \n \n larksuite cli app id yes no feishu or lark app id used by lark cli. \n larksuite cli app secret yes yes app secret from the feishu or lark developer console. \n larksuite cli brand no no use feishu for china tenants or lark for global tenants. defaults to feishu. \n larksuite cli default as no no default lark cli identity: bot, user, or auto. defaults to bot. \n larksuite cli strict mode no no restrict lark cli to bot, user, or off. defaults to bot. \n meegle host no no meegle site domain, such as project.feishu.cn, meegle.com, or a tenant host. \n meegle user access token no yes optional meegle user access token for direct cli auth. \n meegle access token header no no optional custom meegle token header. empty uses authorization: bearer <token . \n meegle user agent no no optional caller suffix appended to the meegle cli user agent. \n\ncredential source urls\n\n environment variable where to get it \n \n larksuite cli app id feishu china app console: <https://open.feishu.cn/app \n larksuite cli app secret feishu china app console: <https://open.feishu.cn/app \n larksuite cli brand use feishu for apps from <https://open.feishu.cn/app ; use lark for global lark tenants. \n meegle host the meegle/lark project tenant host, for example <https://project.feishu.cn . \n meegle user access token meegle cli direct env token configurat"
361
421
  },
362
422
  {
363
423
  "id": "figma",
@@ -4206,6 +4266,79 @@ var RAW_PLUGIN_LIBRARY = String.raw`[
4206
4266
  },
4207
4267
  "searchText": "inference-sh\ninference.sh\ninference.sh lets buddies run cloud ai apps for images, videos, llms, search, audio, 3d, and automation through the belt cli.\nautomation\ntool data-source action cli skill\ninference-sh belt ai-apps image video llm search skills\ninfsh_api_key inference.sh api key api key used by the belt cli.\ninference.sh plugin\ninference.sh plugin configuration keys setup runtime assets references\ninference.sh plugin\n\ninference.sh lets a buddy run cloud ai apps for images, videos, llms, search, audio, 3d, and automation through the belt cli.\n\nconfiguration keys\n\n key required sensitive description \n \n infsh api key yes yes api key used by the belt cli. \n\nsetup\n\n1. sign in to inference.sh.\n2. create or copy an api key from your inference.sh account settings.\n3. add the key as infsh api key in the deployment form or secret group.\n4. deploy the buddy.\n5. run the verification check to confirm belt help works inside the runtime.\n\nruntime assets\n\n installs system prerequisites for the official installer.\n installs the belt cli with https://cli.inference.sh.\n mounts the infsh cli skill from infsh skills/skills.\n\nreferences\n\n inference.sh skills\n inference.sh cli setup\n inference.sh authentication\n infsh skills repository"
4208
4268
  },
4269
+ {
4270
+ "id": "text-to-cad",
4271
+ "name": "CAD Skills",
4272
+ "description": "CAD Skills mounts text-to-cad workflows for STEP-first CAD generation, inspection, rendering, robot descriptions, standard parts, and fabrication preflight.",
4273
+ "version": "1.0.0",
4274
+ "category": "automation",
4275
+ "capabilities": [
4276
+ "tool",
4277
+ "data-source",
4278
+ "action",
4279
+ "cli",
4280
+ "skill"
4281
+ ],
4282
+ "tags": [
4283
+ "cad",
4284
+ "step",
4285
+ "stl",
4286
+ "dxf",
4287
+ "robotics",
4288
+ "urdf",
4289
+ "sdf",
4290
+ "srdf",
4291
+ "hardware"
4292
+ ],
4293
+ "authType": "none",
4294
+ "website": "https://www.cadskills.xyz",
4295
+ "docs": "https://github.com/earthtojake/text-to-cad",
4296
+ "popularity": 88,
4297
+ "manifest": {
4298
+ "id": "text-to-cad",
4299
+ "name": "CAD Skills",
4300
+ "description": "CAD Skills mounts text-to-cad workflows for STEP-first CAD generation, inspection, rendering, robot descriptions, standard parts, and fabrication preflight.",
4301
+ "version": "1.0.0",
4302
+ "category": "automation",
4303
+ "icon": "boxes",
4304
+ "website": "https://www.cadskills.xyz",
4305
+ "docs": "https://github.com/earthtojake/text-to-cad",
4306
+ "auth": {
4307
+ "type": "none",
4308
+ "fields": []
4309
+ },
4310
+ "capabilities": [
4311
+ "tool",
4312
+ "data-source",
4313
+ "action",
4314
+ "cli",
4315
+ "skill"
4316
+ ],
4317
+ "tags": [
4318
+ "cad",
4319
+ "step",
4320
+ "stl",
4321
+ "dxf",
4322
+ "robotics",
4323
+ "urdf",
4324
+ "sdf",
4325
+ "srdf",
4326
+ "hardware"
4327
+ ],
4328
+ "popularity": 88
4329
+ },
4330
+ "requiredFields": [],
4331
+ "readme": {
4332
+ "title": "CAD Skills",
4333
+ "excerpt": "CAD Skills\n\nMounts the earthtojake/text to cad agent skills for CAD, robotics, and\nhardware design workflows.\n\nRuntime\n\n Pulls the repository skills/ tree into\n /workspace/.agents/plugin skills/text to cad.\n Installs Python CAD dependencies into the plugin runtime dependency volume\n from a Debian based Node init image so binary Python wheels are available.\n Installs the CAD Explorer viewer dependencies after the skills have been\n copied into the init container staging volume.\n Patches the bundled step.parts downloader to use the currently live\n https://www.step.parts/v1 API origin.\n Exposes cad step, cad inspect, and cad dxf wrappers on PATH.\n\nNotes\n\nThe upstream repository excludes LFS heavy benchmark assets from normal use.\nThis plugin mounts only the skill tree and keeps CAD outputs in the active\nworkspace as ordinary derived artifacts.",
4334
+ "headings": [
4335
+ "CAD Skills",
4336
+ "Runtime",
4337
+ "Notes"
4338
+ ]
4339
+ },
4340
+ "searchText": "text-to-cad\ncad skills\ncad skills mounts text-to-cad workflows for step-first cad generation, inspection, rendering, robot descriptions, standard parts, and fabrication preflight.\nautomation\ntool data-source action cli skill\ncad step stl dxf robotics urdf sdf srdf hardware\n\ncad skills\ncad skills runtime notes\ncad skills\n\nmounts the earthtojake/text to cad agent skills for cad, robotics, and\nhardware design workflows.\n\nruntime\n\n pulls the repository skills/ tree into\n /workspace/.agents/plugin skills/text to cad.\n installs python cad dependencies into the plugin runtime dependency volume\n from a debian based node init image so binary python wheels are available.\n installs the cad explorer viewer dependencies after the skills have been\n copied into the init container staging volume.\n patches the bundled step.parts downloader to use the currently live\n https://www.step.parts/v1 api origin.\n exposes cad step, cad inspect, and cad dxf wrappers on path.\n\nnotes\n\nthe upstream repository excludes lfs heavy benchmark assets from normal use.\nthis plugin mounts only the skill tree and keeps cad outputs in the active\nworkspace as ordinary derived artifacts."
4341
+ },
4209
4342
  {
4210
4343
  "id": "claude-plugin",
4211
4344
  "name": "Claude Plugin Importer",
@@ -6387,6 +6520,107 @@ var RAW_PLUGIN_LIBRARY = String.raw`[
6387
6520
  },
6388
6521
  "searchText": "vercel\nvercel\nvercel operations for deployments, domains, logs, project settings, environment variables, and next.js production diagnostics.\ndevops\ntool data-source action cli mcp skill\nvercel deployments nextjs logs domains mcp\nvercel_token vercel token vercel access token. vercel_team_id team id default vercel team id. vercel_project_id project id default vercel project id.\nvercel plugin\nvercel plugin configuration keys setup runtime assets references\nvercel plugin\n\nvercel launchops supports preview deploys, deployment logs, domain configuration, project settings, environment variables, and next.js production diagnostics.\n\nconfiguration keys\n\n key required sensitive description \n \n vercel token yes yes vercel access token for project and deployment operations. \n vercel team id no no optional default vercel team id. \n vercel project id no no optional default vercel project id. \n\nsetup\n\n1. open vercel account settings.\n2. create an access token.\n3. add the token as vercel token.\n4. copy vercel team id from team settings if the buddy should operate under a team.\n5. copy vercel project id from project settings if the buddy should default to one project.\n6. deploy the buddy and verify vercel version.\n\nruntime assets\n\n installs the vercel cli.\n registers hosted vercel mcp metadata.\n\nreferences\n\n vercel agent resources\n vercel mcp\n vercel cli\n creating a vercel access token"
6389
6522
  },
6523
+ {
6524
+ "id": "nature-skills",
6525
+ "name": "Nature Skills",
6526
+ "description": "Nature Skills mounts academic writing, polishing, citation, paper reading, figure, reviewer response, paper-to-PPT, data availability, and literature search workflows.",
6527
+ "version": "1.0.0",
6528
+ "category": "productivity",
6529
+ "capabilities": [
6530
+ "tool",
6531
+ "data-source",
6532
+ "action",
6533
+ "cli",
6534
+ "mcp",
6535
+ "skill"
6536
+ ],
6537
+ "tags": [
6538
+ "nature",
6539
+ "academic-writing",
6540
+ "paper-reading",
6541
+ "citation",
6542
+ "figures",
6543
+ "literature-search",
6544
+ "review-response",
6545
+ "ppt"
6546
+ ],
6547
+ "authType": "none",
6548
+ "website": "https://github.com/Yuan1z0825/nature-skills",
6549
+ "docs": "https://github.com/Yuan1z0825/nature-skills",
6550
+ "popularity": 84,
6551
+ "manifest": {
6552
+ "id": "nature-skills",
6553
+ "name": "Nature Skills",
6554
+ "description": "Nature Skills mounts academic writing, polishing, citation, paper reading, figure, reviewer response, paper-to-PPT, data availability, and literature search workflows.",
6555
+ "version": "1.0.0",
6556
+ "category": "productivity",
6557
+ "icon": "microscope",
6558
+ "website": "https://github.com/Yuan1z0825/nature-skills",
6559
+ "docs": "https://github.com/Yuan1z0825/nature-skills",
6560
+ "auth": {
6561
+ "type": "none",
6562
+ "fields": [
6563
+ {
6564
+ "key": "PUBMED_EMAIL",
6565
+ "label": "PubMed email",
6566
+ "description": "Optional email used by NCBI E-utilities for academic search workflows.",
6567
+ "required": false,
6568
+ "sensitive": false,
6569
+ "placeholder": "researcher@example.com",
6570
+ "helpUrl": "https://www.ncbi.nlm.nih.gov/books/NBK25497/"
6571
+ },
6572
+ {
6573
+ "key": "NCBI_API_KEY",
6574
+ "label": "NCBI API key",
6575
+ "description": "Optional NCBI API key for higher PubMed rate limits.",
6576
+ "required": false,
6577
+ "sensitive": true,
6578
+ "placeholder": "ncbi_...",
6579
+ "helpUrl": "https://www.ncbi.nlm.nih.gov/account/settings/"
6580
+ },
6581
+ {
6582
+ "key": "SEMANTIC_SCHOLAR_API_KEY",
6583
+ "label": "Semantic Scholar API key",
6584
+ "description": "Optional Semantic Scholar API key for literature search workflows.",
6585
+ "required": false,
6586
+ "sensitive": true,
6587
+ "placeholder": "sk-...",
6588
+ "helpUrl": "https://api.semanticscholar.org/"
6589
+ }
6590
+ ]
6591
+ },
6592
+ "capabilities": [
6593
+ "tool",
6594
+ "data-source",
6595
+ "action",
6596
+ "cli",
6597
+ "mcp",
6598
+ "skill"
6599
+ ],
6600
+ "tags": [
6601
+ "nature",
6602
+ "academic-writing",
6603
+ "paper-reading",
6604
+ "citation",
6605
+ "figures",
6606
+ "literature-search",
6607
+ "review-response",
6608
+ "ppt"
6609
+ ],
6610
+ "popularity": 84
6611
+ },
6612
+ "requiredFields": [],
6613
+ "readme": {
6614
+ "title": "Nature Skills",
6615
+ "excerpt": "Nature Skills\n\nMounts Yuan1z0825/nature skills into Cloud agents as a bundled academic\nresearch and publication workflow plugin.\n\nRuntime\n\n Pulls all skills/nature folders into\n /workspace/.agents/plugin skills/nature skills.\n Installs the Python dependencies needed by the bundled academic search MCP\n server into the plugin runtime dependency volume from a Debian based Node init\n image.\n Registers the nature academic search stdio MCP server.\n\nOptional Credentials\n\n PUBMED EMAIL improves compliance with NCBI E utilities guidance.\n NCBI API KEY raises PubMed rate limits.\n SEMANTIC SCHOLAR API KEY can improve Semantic Scholar search throughput.\n\nThe non search skills can be used without credentials.",
6616
+ "headings": [
6617
+ "Nature Skills",
6618
+ "Runtime",
6619
+ "Optional Credentials"
6620
+ ]
6621
+ },
6622
+ "searchText": "nature-skills\nnature skills\nnature skills mounts academic writing, polishing, citation, paper reading, figure, reviewer response, paper-to-ppt, data availability, and literature search workflows.\nproductivity\ntool data-source action cli mcp skill\nnature academic-writing paper-reading citation figures literature-search review-response ppt\npubmed_email pubmed email optional email used by ncbi e-utilities for academic search workflows. ncbi_api_key ncbi api key optional ncbi api key for higher pubmed rate limits. semantic_scholar_api_key semantic scholar api key optional semantic scholar api key for literature search workflows.\nnature skills\nnature skills runtime optional credentials\nnature skills\n\nmounts yuan1z0825/nature skills into cloud agents as a bundled academic\nresearch and publication workflow plugin.\n\nruntime\n\n pulls all skills/nature folders into\n /workspace/.agents/plugin skills/nature skills.\n installs the python dependencies needed by the bundled academic search mcp\n server into the plugin runtime dependency volume from a debian based node init\n image.\n registers the nature academic search stdio mcp server.\n\noptional credentials\n\n pubmed email improves compliance with ncbi e utilities guidance.\n ncbi api key raises pubmed rate limits.\n semantic scholar api key can improve semantic scholar search throughput.\n\nthe non search skills can be used without credentials."
6623
+ },
6390
6624
  {
6391
6625
  "id": "atlassian",
6392
6626
  "name": "Atlassian",
@@ -6698,6 +6932,103 @@ var RAW_PLUGIN_LIBRARY = String.raw`[
6698
6932
  },
6699
6933
  "searchText": "posthog\nposthog\nposthog productops covers funnels, retention, feature flags, experiments, session replay, hogql, logs, and product-growth diagnostics.\nanalytics\ntool data-source action cli mcp skill\nposthog analytics feature-flags funnels hogql experiments\nposthog_api_key posthog api key personal or project api key for posthog. posthog_project_id project id optional default project. posthog_host posthog host optional posthog host.\nposthog plugin\nposthog plugin configuration keys setup runtime assets references\nposthog plugin\n\nposthog productops covers funnels, retention, feature flags, experiments, session replay, hogql, logs, and product growth diagnostics.\n\nconfiguration keys\n\n key required sensitive description \n \n posthog api key yes yes posthog personal or project api key. \n posthog project id no no optional default posthog project id. \n posthog host no no optional posthog host, for example https://app.posthog.com or a self hosted url. \n\nsetup\n\n1. open posthog project or personal api settings.\n2. create an api key with the minimum scopes needed by the buddy.\n3. add the key as posthog api key.\n4. add posthog project id if the buddy should default to one project.\n5. add posthog host for eu cloud or self hosted installations.\n6. deploy the buddy and run the cli verification check.\n\nruntime assets\n\n installs the experimental posthog cli package.\n registers hosted posthog mcp metadata.\n\nreferences\n\n posthog mcp\n posthog api overview\n posthog endpoints cli"
6700
6934
  },
6935
+ {
6936
+ "id": "agentmemory",
6937
+ "name": "AgentMemory",
6938
+ "description": "AgentMemory adds persistent, searchable memory for coding agents through its MCP server, CLI, and optional local/remote memory service.",
6939
+ "version": "1.0.0",
6940
+ "category": "automation",
6941
+ "capabilities": [
6942
+ "tool",
6943
+ "data-source",
6944
+ "action",
6945
+ "cli",
6946
+ "mcp",
6947
+ "skill"
6948
+ ],
6949
+ "tags": [
6950
+ "memory",
6951
+ "mcp",
6952
+ "coding-agent",
6953
+ "context",
6954
+ "search",
6955
+ "persistence"
6956
+ ],
6957
+ "authType": "none",
6958
+ "website": "https://agent-memory.dev",
6959
+ "docs": "https://github.com/rohitg00/agentmemory",
6960
+ "popularity": 82,
6961
+ "manifest": {
6962
+ "id": "agentmemory",
6963
+ "name": "AgentMemory",
6964
+ "description": "AgentMemory adds persistent, searchable memory for coding agents through its MCP server, CLI, and optional local/remote memory service.",
6965
+ "version": "1.0.0",
6966
+ "category": "automation",
6967
+ "icon": "brain",
6968
+ "website": "https://agent-memory.dev",
6969
+ "docs": "https://github.com/rohitg00/agentmemory",
6970
+ "auth": {
6971
+ "type": "none",
6972
+ "fields": [
6973
+ {
6974
+ "key": "AGENTMEMORY_URL",
6975
+ "label": "AgentMemory service URL",
6976
+ "description": "Optional remote AgentMemory service URL. Omit to use the local runtime store.",
6977
+ "required": false,
6978
+ "sensitive": false,
6979
+ "placeholder": "http://127.0.0.1:7331",
6980
+ "helpUrl": "https://github.com/rohitg00/agentmemory"
6981
+ },
6982
+ {
6983
+ "key": "AGENTMEMORY_API_KEY",
6984
+ "label": "AgentMemory API key",
6985
+ "description": "Optional API key for a protected AgentMemory service.",
6986
+ "required": false,
6987
+ "sensitive": true,
6988
+ "placeholder": "am_...",
6989
+ "helpUrl": "https://github.com/rohitg00/agentmemory"
6990
+ },
6991
+ {
6992
+ "key": "AGENTMEMORY_PROJECT_ID",
6993
+ "label": "AgentMemory project id",
6994
+ "description": "Optional project/workspace id used to partition memories.",
6995
+ "required": false,
6996
+ "sensitive": false,
6997
+ "placeholder": "shadow-cloud"
6998
+ }
6999
+ ]
7000
+ },
7001
+ "capabilities": [
7002
+ "tool",
7003
+ "data-source",
7004
+ "action",
7005
+ "cli",
7006
+ "mcp",
7007
+ "skill"
7008
+ ],
7009
+ "tags": [
7010
+ "memory",
7011
+ "mcp",
7012
+ "coding-agent",
7013
+ "context",
7014
+ "search",
7015
+ "persistence"
7016
+ ],
7017
+ "popularity": 82
7018
+ },
7019
+ "requiredFields": [],
7020
+ "readme": {
7021
+ "title": "AgentMemory",
7022
+ "excerpt": "AgentMemory\n\nAgentMemory adds persistent, searchable memory to Shadow Cloud agents through\nthe @agentmemory/mcp MCP server and the agentmemory CLI.\n\nCapabilities\n\n Registers the AgentMemory MCP server for every supported runtime.\n Installs the AgentMemory CLI and MCP package into the runtime dependency\n volume.\n Supports an optional remote AgentMemory service via AGENTMEMORY URL and\n AGENTMEMORY API KEY.\n\nConfiguration\n\n \n\nOptional environment fields:\n\n AGENTMEMORY URL: remote service URL. Omit to use the local runtime store.\n AGENTMEMORY API KEY: key for a protected remote service.\n AGENTMEMORY PROJECT ID: project/workspace partition id.\n\nSafety\n\nAgents should only store durable project context, decisions, and user approved\nfacts. Do not store secrets, tokens, credentials, or private personal data.",
7023
+ "headings": [
7024
+ "AgentMemory",
7025
+ "Capabilities",
7026
+ "Configuration",
7027
+ "Safety"
7028
+ ]
7029
+ },
7030
+ "searchText": "agentmemory\nagentmemory\nagentmemory adds persistent, searchable memory for coding agents through its mcp server, cli, and optional local/remote memory service.\nautomation\ntool data-source action cli mcp skill\nmemory mcp coding-agent context search persistence\nagentmemory_url agentmemory service url optional remote agentmemory service url. omit to use the local runtime store. agentmemory_api_key agentmemory api key optional api key for a protected agentmemory service. agentmemory_project_id agentmemory project id optional project/workspace id used to partition memories.\nagentmemory\nagentmemory capabilities configuration safety\nagentmemory\n\nagentmemory adds persistent, searchable memory to shadow cloud agents through\nthe @agentmemory/mcp mcp server and the agentmemory cli.\n\ncapabilities\n\n registers the agentmemory mcp server for every supported runtime.\n installs the agentmemory cli and mcp package into the runtime dependency\n volume.\n supports an optional remote agentmemory service via agentmemory url and\n agentmemory api key.\n\nconfiguration\n\n \n\noptional environment fields:\n\n agentmemory url: remote service url. omit to use the local runtime store.\n agentmemory api key: key for a protected remote service.\n agentmemory project id: project/workspace partition id.\n\nsafety\n\nagents should only store durable project context, decisions, and user approved\nfacts. do not store secrets, tokens, credentials, or private personal data."
7031
+ },
6701
7032
  {
6702
7033
  "id": "baidu-smartprogram",
6703
7034
  "name": "Baidu Smart Program",
@@ -7583,6 +7914,43 @@ var RAW_TEMPLATE_LIBRARY = String.raw`[
7583
7914
  },
7584
7915
  "searchText": "code-arena\n代码擂台\n用于出题、限时挑战、提示和代码复盘的编程对战 buddy 模板。\ngeneral\nmodel-provider shadowob\nproblems arena review\narena buddy\ngenerate coding challenges, set constraints, track attempts, give hints, and review solutions. prefer concise test cases and actionable feedback."
7585
7916
  },
7917
+ {
7918
+ "slug": "code-trainer",
7919
+ "title": "Code Trainer 算法训练",
7920
+ "description": "基于 Code Trainer Server App 授权的单 Buddy 算法训练模板,用多个频道承载自动复盘、题目推荐、学习计划、技巧推送、错题回炉和进度报告。",
7921
+ "category": "general",
7922
+ "plugins": [
7923
+ "model-provider",
7924
+ "shadowob"
7925
+ ],
7926
+ "channels": [
7927
+ "助教资讯",
7928
+ "题目推荐",
7929
+ "学习规划",
7930
+ "代码复盘",
7931
+ "错题回炉",
7932
+ "算法小技巧"
7933
+ ],
7934
+ "buddyNames": [
7935
+ "算法教练"
7936
+ ],
7937
+ "agentCount": 1,
7938
+ "systemPromptExcerpt": "You are the single Code Trainer Buddy. Use the mounted shadowob CLI skill as the source of truth: run \u0060shadowob app discover --server \"$SHADOW_SERVER_APP_SERVER_CODE_TRAINER_APP\" --json\u0060, then call \u0060shadowob app call \"$SHADOW_SERVER_APP_KEY_CODE_TRAINER_APP\" <command> --server \"$SHADOW_SERVER_APP_SERVER_CODE_TRAINER_APP\" --json-input '<raw-command-input-json>' --json\u0060.\n\nUse channels as modes, not separate identities:\n- #代码复盘: claim Inbox tasks immediately, acknowledge within 60 seconds, run visible examples plus hidden/edge cases, then write submissions.analyze with verdict, score, complexity, diagnosis, and next steps. If review takes more than 3 minutes, post one brief progress note.\n- #题目推荐: recommend the next problem from learning.overview using reinforcement, diversity, review, and popular coverage. Include predicted ACK rate when available and offer a lighter fallback.\n- #学习规划: maintain daily/weekly lists, deadline target, and first concrete action.\n- #算法小技巧: post compact templates, pitfalls, complexity reminders, and short checks tied to current weak or active tags.\n- #错题回炉: schedule spaced review for wrong or nearly forgotten problems and suggest one similar variation.\n- #助",
7939
+ "valid": true,
7940
+ "validation": {
7941
+ "valid": true,
7942
+ "agents": 1,
7943
+ "configurations": 0,
7944
+ "violations": [],
7945
+ "extendsErrors": [],
7946
+ "templateRefs": {
7947
+ "env": 1,
7948
+ "secret": 0,
7949
+ "file": 0
7950
+ }
7951
+ },
7952
+ "searchText": "code-trainer\ncode trainer 算法训练\n基于 code trainer server app 授权的单 buddy 算法训练模板,用多个频道承载自动复盘、题目推荐、学习计划、技巧推送、错题回炉和进度报告。\ngeneral\nmodel-provider shadowob\n助教资讯 题目推荐 学习规划 代码复盘 错题回炉 算法小技巧\n算法教练\nyou are the single code trainer buddy. use the mounted shadowob cli skill as the source of truth: run \u0060shadowob app discover --server \"$shadow_server_app_server_code_trainer_app\" --json\u0060, then call \u0060shadowob app call \"$shadow_server_app_key_code_trainer_app\" <command> --server \"$shadow_server_app_server_code_trainer_app\" --json-input '<raw-command-input-json>' --json\u0060.\n\nuse channels as modes, not separate identities:\n- #代码复盘: claim inbox tasks immediately, acknowledge within 60 seconds, run visible examples plus hidden/edge cases, then write submissions.analyze with verdict, score, complexity, diagnosis, and next steps. if review takes more than 3 minutes, post one brief progress note.\n- #题目推荐: recommend the next problem from learning.overview using reinforcement, diversity, review, and popular coverage. include predicted ack rate when available and offer a lighter fallback.\n- #学习规划: maintain daily/weekly lists, deadline target, and first concrete action.\n- #算法小技巧: post compact templates, pitfalls, complexity reminders, and short checks tied to current weak or active tags.\n- #错题回炉: schedule spaced review for wrong or nearly forgotten problems and suggest one similar variation.\n- #助"
7953
+ },
7586
7954
  {
7587
7955
  "slug": "daily-brief",
7588
7956
  "title": "晨间简报",
@@ -8682,15 +9050,75 @@ function summarizeCostOverview(summaries, billingUnit) {
8682
9050
 
8683
9051
  // src/clients/kubectl-runtime.ts
8684
9052
  import { execFileSync, spawn, spawnSync } from "child_process";
8685
- import { existsSync as existsSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
8686
- import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
8687
- import { delimiter as delimiter2, join as join2 } from "path";
9053
+ import { existsSync as existsSync3, mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
9054
+ import { tmpdir as tmpdir2 } from "os";
9055
+ import { delimiter as delimiter2, join as join3 } from "path";
8688
9056
 
8689
9057
  // src/services/deployment-runtime.service.ts
8690
9058
  import { createHash } from "crypto";
8691
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
8692
- import { homedir, tmpdir } from "os";
8693
- import { delimiter, join } from "path";
9059
+ import { existsSync as existsSync2, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
9060
+ import { homedir as homedir2, tmpdir } from "os";
9061
+ import { delimiter, join as join2 } from "path";
9062
+
9063
+ // src/utils/kubeconfig-file.ts
9064
+ import { existsSync, readFileSync, statSync } from "fs";
9065
+ import { homedir } from "os";
9066
+ import { join } from "path";
9067
+ function defaultKubeconfigPath() {
9068
+ return join(homedir(), ".kube", "config");
9069
+ }
9070
+ function kubeconfigSetupHint() {
9071
+ return "Configure KUBECONFIG_HOST_PATH or CLOUD_SAAS_CLUSTER_KUBECONFIG_HOST_PATH to an existing host kubeconfig file, or initialize/import a cluster before deploying.";
9072
+ }
9073
+ function assertReadableKubeconfigFile(kubeconfigPath, label = "Kubernetes kubeconfig") {
9074
+ if (!existsSync(kubeconfigPath)) {
9075
+ throw new Error(`${label} not found at ${kubeconfigPath}. ${kubeconfigSetupHint()}`);
9076
+ }
9077
+ let stat3;
9078
+ try {
9079
+ stat3 = statSync(kubeconfigPath);
9080
+ } catch (err) {
9081
+ throw new Error(
9082
+ `Failed to inspect ${label} at ${kubeconfigPath}: ${err.message}. ` + kubeconfigSetupHint()
9083
+ );
9084
+ }
9085
+ if (stat3.isDirectory()) {
9086
+ throw new Error(
9087
+ `${label} path ${kubeconfigPath} is a directory, not a file. This usually means Docker created the bind-mount target because the host kubeconfig file is missing. ` + kubeconfigSetupHint()
9088
+ );
9089
+ }
9090
+ if (!stat3.isFile()) {
9091
+ throw new Error(
9092
+ `${label} path ${kubeconfigPath} is not a regular file. ${kubeconfigSetupHint()}`
9093
+ );
9094
+ }
9095
+ if (stat3.size === 0) {
9096
+ throw new Error(`${label} at ${kubeconfigPath} is empty. ${kubeconfigSetupHint()}`);
9097
+ }
9098
+ }
9099
+ function readKubeconfigFile(kubeconfigPath, label = "Kubernetes kubeconfig") {
9100
+ assertReadableKubeconfigFile(kubeconfigPath, label);
9101
+ try {
9102
+ return readFileSync(kubeconfigPath, "utf8");
9103
+ } catch (err) {
9104
+ throw new Error(
9105
+ `Failed to read ${label} at ${kubeconfigPath}: ${err.message}. ` + kubeconfigSetupHint()
9106
+ );
9107
+ }
9108
+ }
9109
+ function findReadableKubeconfigPath(candidates, label = "Kubernetes kubeconfig") {
9110
+ const uniqueCandidates = [
9111
+ ...new Set(candidates.filter((candidate) => Boolean(candidate)))
9112
+ ];
9113
+ for (const candidate of uniqueCandidates) {
9114
+ if (!existsSync(candidate)) continue;
9115
+ assertReadableKubeconfigFile(candidate, label);
9116
+ return candidate;
9117
+ }
9118
+ return void 0;
9119
+ }
9120
+
9121
+ // src/services/deployment-runtime.service.ts
8694
9122
  function extractKubeContext(kubeconfigYaml) {
8695
9123
  const match = kubeconfigYaml.match(/current-context:\s*(\S+)/);
8696
9124
  return match?.[1];
@@ -8727,24 +9155,24 @@ function normalizeRuntimeEnvVars(envVars) {
8727
9155
  return normalized;
8728
9156
  }
8729
9157
  function getStableRuntimeKubeconfigPath(kubeconfigYaml) {
8730
- const runtimeDir = join(homedir(), ".shadowob", "kubeconfigs");
9158
+ const runtimeDir = join2(homedir2(), ".shadowob", "kubeconfigs");
8731
9159
  mkdirSync(runtimeDir, { recursive: true });
8732
9160
  const hash = createHash("sha256").update(kubeconfigYaml).digest("hex");
8733
- const kubeconfigPath = join(runtimeDir, `${hash}.yaml`);
8734
- if (!existsSync(kubeconfigPath)) {
9161
+ const kubeconfigPath = join2(runtimeDir, `${hash}.yaml`);
9162
+ if (!existsSync2(kubeconfigPath)) {
8735
9163
  writeFileSync(kubeconfigPath, kubeconfigYaml, { mode: 384 });
8736
9164
  }
8737
9165
  return kubeconfigPath;
8738
9166
  }
8739
9167
  function isContainerizedRuntime() {
8740
- return process.env.SHADOW_CONTAINERIZED === "1" || existsSync("/.dockerenv");
9168
+ return process.env.SHADOW_CONTAINERIZED === "1" || existsSync2("/.dockerenv");
8741
9169
  }
8742
9170
  function getHostLocalRuntimeKubeconfigPaths() {
8743
9171
  const candidates = [process.env.KUBECONFIG_HOST_PATH?.trim()];
8744
9172
  if (!isContainerizedRuntime()) {
8745
9173
  candidates.push(
8746
9174
  ...process.env.KUBECONFIG?.split(delimiter).map((candidate) => candidate.trim()).filter((candidate) => candidate.length > 0) ?? [],
8747
- join(homedir(), ".kube", "config")
9175
+ defaultKubeconfigPath()
8748
9176
  );
8749
9177
  }
8750
9178
  return [...new Set(candidates.filter((candidate) => Boolean(candidate)))];
@@ -8757,17 +9185,17 @@ function resolveAmbientRuntimeKubeconfigPath() {
8757
9185
  const candidates = [
8758
9186
  ...process.env.KUBECONFIG?.split(delimiter).map((candidate) => candidate.trim()).filter((candidate) => candidate.length > 0) ?? [],
8759
9187
  process.env.KUBECONFIG_HOST_PATH?.trim(),
8760
- join(homedir(), ".kube", "config")
9188
+ defaultKubeconfigPath()
8761
9189
  ].filter((candidate) => Boolean(candidate));
8762
- return candidates.find((candidate) => existsSync(candidate));
9190
+ return findReadableKubeconfigPath(candidates, "Cloud SaaS Kubernetes kubeconfig");
8763
9191
  }
8764
9192
  var DeploymentRuntimeService = class {
8765
9193
  constructor(deployService) {
8766
9194
  this.deployService = deployService;
8767
9195
  }
8768
9196
  async deployFromSnapshot(options) {
8769
- const configDir = mkdtempSync(join(tmpdir(), "sc-cfg-"));
8770
- const configPath = join(configDir, "shadowob-cloud.json");
9197
+ const configDir = mkdtempSync(join2(tmpdir(), "sc-cfg-"));
9198
+ const configPath = join2(configDir, "shadowob-cloud.json");
8771
9199
  writeFileSync(configPath, JSON.stringify(options.configSnapshot, null, 2), "utf-8");
8772
9200
  const {
8773
9201
  configSnapshot: _configSnapshot,
@@ -8820,7 +9248,7 @@ var DeploymentRuntimeService = class {
8820
9248
  let k8sContext;
8821
9249
  let kubeConfigPath;
8822
9250
  const activeKubeconfigPath = resolveAmbientRuntimeKubeconfigPath();
8823
- const activeKubeconfig = cluster?.kubeconfig ? cluster.kubeconfig : activeKubeconfigPath ? readFileSync(activeKubeconfigPath, "utf8") : void 0;
9251
+ const activeKubeconfig = cluster?.kubeconfig ? cluster.kubeconfig : activeKubeconfigPath ? readKubeconfigFile(activeKubeconfigPath, "Cloud SaaS Kubernetes kubeconfig") : void 0;
8824
9252
  if (activeKubeconfig) {
8825
9253
  const shouldRewriteLoopback = Boolean(
8826
9254
  cluster?.kubeconfig || activeKubeconfigPath && !isHostLocalRuntimeKubeconfigPath(activeKubeconfigPath)
@@ -8845,14 +9273,14 @@ function volumeSnapshotApiAvailableFromOutput(output2) {
8845
9273
  );
8846
9274
  }
8847
9275
  function isContainerizedRuntime2() {
8848
- return process.env.SHADOW_CONTAINERIZED === "1" || existsSync2("/.dockerenv");
9276
+ return process.env.SHADOW_CONTAINERIZED === "1" || existsSync3("/.dockerenv");
8849
9277
  }
8850
9278
  function getHostLocalKubeconfigPaths() {
8851
9279
  const candidates = [process.env.KUBECONFIG_HOST_PATH?.trim()];
8852
9280
  if (!isContainerizedRuntime2()) {
8853
9281
  candidates.push(
8854
9282
  ...process.env.KUBECONFIG?.split(delimiter2).map((candidate) => candidate.trim()).filter((candidate) => candidate.length > 0) ?? [],
8855
- join2(homedir2(), ".kube", "config")
9283
+ defaultKubeconfigPath()
8856
9284
  );
8857
9285
  }
8858
9286
  return [...new Set(candidates.filter((candidate) => Boolean(candidate)))];
@@ -8866,22 +9294,23 @@ function extractCurrentContext(kubeconfigYaml) {
8866
9294
  }
8867
9295
  function resolveAmbientKubeconfig() {
8868
9296
  const envCandidates = process.env.KUBECONFIG?.split(delimiter2).map((candidate) => candidate.trim()).filter((candidate) => candidate.length > 0) ?? [];
8869
- const kubeconfigPath = [
9297
+ const candidates = [
8870
9298
  ...envCandidates,
8871
9299
  process.env.KUBECONFIG_HOST_PATH?.trim(),
8872
- join2(homedir2(), ".kube", "config")
8873
- ].filter((candidate) => Boolean(candidate)).find((candidate) => existsSync2(candidate));
9300
+ defaultKubeconfigPath()
9301
+ ].filter((candidate) => Boolean(candidate));
9302
+ const kubeconfigPath = findReadableKubeconfigPath(candidates, "Kubernetes kubectl kubeconfig");
8874
9303
  if (!kubeconfigPath) {
8875
9304
  return void 0;
8876
9305
  }
8877
9306
  return {
8878
- kubeconfig: readFileSync2(kubeconfigPath, "utf-8"),
9307
+ kubeconfig: readKubeconfigFile(kubeconfigPath, "Kubernetes kubectl kubeconfig"),
8879
9308
  shouldRewriteLoopback: !isHostLocalKubeconfigPath(kubeconfigPath)
8880
9309
  };
8881
9310
  }
8882
9311
  function createTempKubeconfig(kubeconfig, includeAmbientContext = false, rewriteLoopback = true) {
8883
- const dir = mkdtempSync2(join2(tmpdir2(), "sc-saas-kube-"));
8884
- const path = join2(dir, "kubeconfig");
9312
+ const dir = mkdtempSync2(join3(tmpdir2(), "sc-saas-kube-"));
9313
+ const path = join3(dir, "kubeconfig");
8885
9314
  const rewritten = rewriteLoopback ? rewriteLoopbackKubeconfig(kubeconfig, process.env.KUBECONFIG_LOOPBACK_HOST) : kubeconfig;
8886
9315
  writeFileSync2(path, rewritten, { mode: 384 });
8887
9316
  const args = ["--kubeconfig", path];
@@ -8944,6 +9373,83 @@ function execKubectl(args, kubeconfig, timeout = 3e3) {
8944
9373
  })
8945
9374
  );
8946
9375
  }
9376
+ function tryExecKubectl(args, kubeconfig, timeout = 5e3) {
9377
+ try {
9378
+ return execKubectl(args, kubeconfig, timeout);
9379
+ } catch {
9380
+ return null;
9381
+ }
9382
+ }
9383
+ function resourceOutputHas(output2, resourceName) {
9384
+ return Boolean(
9385
+ output2?.split(/\s+/).map((item) => item.trim()).some((item) => item === resourceName)
9386
+ );
9387
+ }
9388
+ function checkAgentSandboxPreflight(options) {
9389
+ const missing = [];
9390
+ const warnings = [];
9391
+ const kubeconfig = options?.kubeconfig;
9392
+ const extensionResources = tryExecKubectl(
9393
+ ["api-resources", "--api-group", "extensions.agents.x-k8s.io", "-o", "name"],
9394
+ kubeconfig
9395
+ );
9396
+ if (!resourceOutputHas(extensionResources, "sandboxtemplates")) {
9397
+ missing.push("CRD sandboxtemplates.extensions.agents.x-k8s.io");
9398
+ }
9399
+ if (!resourceOutputHas(extensionResources, "sandboxclaims")) {
9400
+ missing.push("CRD sandboxclaims.extensions.agents.x-k8s.io");
9401
+ }
9402
+ const coreResources = tryExecKubectl(
9403
+ ["api-resources", "--api-group", "agents.x-k8s.io", "-o", "name"],
9404
+ kubeconfig
9405
+ );
9406
+ if (!resourceOutputHas(coreResources, "sandboxes")) {
9407
+ missing.push("CRD sandboxes.agents.x-k8s.io");
9408
+ }
9409
+ const controllerOutput = tryExecKubectl(
9410
+ ["-n", "agent-sandbox-system", "get", "deployment", "agent-sandbox-controller", "-o", "json"],
9411
+ kubeconfig,
9412
+ 1e4
9413
+ );
9414
+ if (!controllerOutput) {
9415
+ missing.push("deployment/agent-sandbox-controller in namespace agent-sandbox-system");
9416
+ } else {
9417
+ try {
9418
+ const controller = JSON.parse(controllerOutput);
9419
+ const status = controller.status ?? {};
9420
+ if ((status.availableReplicas ?? 0) < 1) {
9421
+ missing.push("Ready agent-sandbox controller");
9422
+ }
9423
+ } catch {
9424
+ missing.push("Readable agent-sandbox controller status");
9425
+ }
9426
+ }
9427
+ const runtimeClassNames = [
9428
+ ...new Set(
9429
+ [options?.runtimeClassName, ...options?.runtimeClassNames ?? []].map((name) => name?.trim()).filter((name) => Boolean(name))
9430
+ )
9431
+ ];
9432
+ for (const runtimeClassName of runtimeClassNames) {
9433
+ const runtimeClass = tryExecKubectl(["get", "runtimeclass", runtimeClassName], kubeconfig);
9434
+ if (!runtimeClass) {
9435
+ missing.push(`RuntimeClass ${runtimeClassName}`);
9436
+ }
9437
+ }
9438
+ const sandboxNodes = tryExecKubectl(
9439
+ ["get", "nodes", "-l", "shadowob.com/sandbox-ready=true", "-o", "name"],
9440
+ kubeconfig
9441
+ );
9442
+ if (!sandboxNodes?.trim()) {
9443
+ warnings.push("No nodes are labeled shadowob.com/sandbox-ready=true");
9444
+ }
9445
+ return {
9446
+ ok: missing.length === 0,
9447
+ missing,
9448
+ warnings,
9449
+ runtimeClassName: runtimeClassNames[0],
9450
+ runtimeClassNames
9451
+ };
9452
+ }
8947
9453
  function execKubectlAsync(args, kubeconfig, timeout = 3e3) {
8948
9454
  return withKubeconfigAsync(
8949
9455
  kubeconfig,
@@ -9594,25 +10100,25 @@ function deleteNamespace(namespace, kubeconfig) {
9594
10100
 
9595
10101
  // src/cluster/kubeconfig.ts
9596
10102
  import {
9597
- existsSync as existsSync3,
10103
+ existsSync as existsSync4,
9598
10104
  mkdirSync as mkdirSync2,
9599
10105
  readdirSync,
9600
- readFileSync as readFileSync3,
10106
+ readFileSync as readFileSync2,
9601
10107
  unlinkSync,
9602
10108
  writeFileSync as writeFileSync3
9603
10109
  } from "fs";
9604
10110
  import { homedir as homedir3 } from "os";
9605
- import { join as join3, resolve } from "path";
10111
+ import { join as join4, resolve } from "path";
9606
10112
  function getClustersDir() {
9607
10113
  return resolve(homedir3(), ".shadow-cloud", "clusters");
9608
10114
  }
9609
10115
  function getKubeconfigPath(clusterName) {
9610
- return join3(getClustersDir(), `${clusterName}.yaml`);
10116
+ return join4(getClustersDir(), `${clusterName}.yaml`);
9611
10117
  }
9612
10118
  function getMetaPath(clusterName) {
9613
- return join3(getClustersDir(), `${clusterName}.json`);
10119
+ return join4(getClustersDir(), `${clusterName}.json`);
9614
10120
  }
9615
- function storeKubeconfig(clusterName, rawKubeconfig, masterPublicIp, nodeCount) {
10121
+ function storeKubeconfig(clusterName, rawKubeconfig, masterPublicIp, nodeCount, options) {
9616
10122
  const dir = getClustersDir();
9617
10123
  mkdirSync2(dir, { recursive: true });
9618
10124
  const kubeconfig = rawKubeconfig.replace(/https?:\/\/127\.0\.0\.1/g, `https://${masterPublicIp}`);
@@ -9623,14 +10129,16 @@ function storeKubeconfig(clusterName, rawKubeconfig, masterPublicIp, nodeCount)
9623
10129
  masterHost: masterPublicIp,
9624
10130
  nodeCount,
9625
10131
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
9626
- kubeconfigPath
10132
+ kubeconfigPath,
10133
+ ...options?.configHash ? { configHash: options.configHash } : {},
10134
+ ...options?.features ? { features: options.features } : {}
9627
10135
  };
9628
10136
  writeFileSync3(getMetaPath(clusterName), JSON.stringify(meta, null, 2), { mode: 384 });
9629
10137
  return meta;
9630
10138
  }
9631
10139
  function loadKubeconfigPath(clusterName) {
9632
10140
  const path = getKubeconfigPath(clusterName);
9633
- if (!existsSync3(path)) {
10141
+ if (!existsSync4(path)) {
9634
10142
  throw new Error(
9635
10143
  `Cluster "${clusterName}" not found.
9636
10144
  Run: shadowob-cloud cluster init --config cluster.json`
@@ -9638,12 +10146,21 @@ Run: shadowob-cloud cluster init --config cluster.json`
9638
10146
  }
9639
10147
  return path;
9640
10148
  }
10149
+ function loadClusterMeta(clusterName) {
10150
+ const path = getMetaPath(clusterName);
10151
+ if (!existsSync4(path)) return null;
10152
+ try {
10153
+ return JSON.parse(readFileSync2(path, "utf8"));
10154
+ } catch {
10155
+ return null;
10156
+ }
10157
+ }
9641
10158
  function listRegisteredClusters() {
9642
10159
  const dir = getClustersDir();
9643
- if (!existsSync3(dir)) return [];
10160
+ if (!existsSync4(dir)) return [];
9644
10161
  return readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => {
9645
10162
  try {
9646
- return JSON.parse(readFileSync3(join3(dir, f), "utf8"));
10163
+ return JSON.parse(readFileSync2(join4(dir, f), "utf8"));
9647
10164
  } catch {
9648
10165
  return null;
9649
10166
  }
@@ -9652,7 +10169,7 @@ function listRegisteredClusters() {
9652
10169
  function importKubeconfig(clusterName, kubeconfigFilePath) {
9653
10170
  const dir = getClustersDir();
9654
10171
  mkdirSync2(dir, { recursive: true });
9655
- const raw = readFileSync3(kubeconfigFilePath, "utf8");
10172
+ const raw = readFileSync2(kubeconfigFilePath, "utf8");
9656
10173
  const serverMatch = raw.match(/server:\s*https?:\/\/([^/\s]+)/);
9657
10174
  const rawHost = serverMatch?.[1] ?? "unknown";
9658
10175
  const masterHost = rawHost.replace(/:\d+$/, "");
@@ -9672,33 +10189,52 @@ function importKubeconfig(clusterName, kubeconfigFilePath) {
9672
10189
  function removeClusterFiles(clusterName) {
9673
10190
  const kubeconfig = getKubeconfigPath(clusterName);
9674
10191
  const meta = getMetaPath(clusterName);
9675
- if (existsSync3(kubeconfig)) unlinkSync(kubeconfig);
9676
- if (existsSync3(meta)) unlinkSync(meta);
10192
+ if (existsSync4(kubeconfig)) unlinkSync(kubeconfig);
10193
+ if (existsSync4(meta)) unlinkSync(meta);
9677
10194
  }
9678
10195
 
9679
10196
  // src/cluster/parser.ts
9680
- import { readFileSync as readFileSync4 } from "fs";
10197
+ import { readFileSync as readFileSync3 } from "fs";
9681
10198
  import { homedir as homedir4 } from "os";
9682
10199
  import { resolve as resolve2 } from "path";
9683
10200
 
9684
10201
  // src/cluster/schema.ts
9685
10202
  import { z } from "zod";
9686
- var NodeRoleSchema = z.enum(["master", "worker"]);
9687
- var NodeConfigSchema = z.object({
9688
- /** Node role in the cluster */
9689
- role: NodeRoleSchema,
9690
- /** Public IP or hostname */
9691
- host: z.string().min(1),
9692
- /** SSH port (default: 22) */
9693
- port: z.number().int().min(1).max(65535).default(22),
9694
- /** SSH username */
9695
- user: z.string().min(1),
9696
- /** Path to SSH private key (supports ~) — mutually inclusive with or exclusive of password */
9697
- sshKeyPath: z.string().optional(),
9698
- /** SSH password — use ${env:VAR} to avoid storing plaintext */
9699
- password: z.string().optional()
9700
- }).refine((n) => n.sshKeyPath !== void 0 || n.password !== void 0, {
9701
- message: "Each node must have either sshKeyPath or password"
10203
+ var K8S_LABEL_KEY_RE = /^([A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9]\/)?[A-Za-z0-9]([-A-Za-z0-9_.]*[A-Za-z0-9])?$/;
10204
+ var K8S_LABEL_VALUE_RE = /^(([A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9])?)$/;
10205
+ var KubernetesLabelMapSchema = z.record(
10206
+ z.string().min(1).regex(K8S_LABEL_KEY_RE, "Invalid Kubernetes label key"),
10207
+ z.string().max(63).regex(K8S_LABEL_VALUE_RE, "Invalid Kubernetes label value")
10208
+ );
10209
+ var ImageReferenceSchema = z.string().min(1).regex(/^\S+$/, "image must not contain whitespace");
10210
+ var ClusterContainerdRegistriesSchema = z.object({
10211
+ /**
10212
+ * k3s containerd mirrors. Written to /etc/rancher/k3s/registries.yaml as JSON/YAML.
10213
+ * Example: { "docker.io": { "endpoint": ["https://registry-1.docker.io"] } }
10214
+ */
10215
+ mirrors: z.record(
10216
+ z.string().min(1),
10217
+ z.object({
10218
+ endpoint: z.array(z.string().url()).min(1)
10219
+ })
10220
+ ).optional(),
10221
+ configs: z.record(
10222
+ z.string().min(1),
10223
+ z.object({
10224
+ auth: z.object({
10225
+ username: z.string().optional(),
10226
+ password: z.string().optional(),
10227
+ auth: z.string().optional(),
10228
+ identityToken: z.string().optional()
10229
+ }).optional(),
10230
+ tls: z.object({
10231
+ caFile: z.string().optional(),
10232
+ certFile: z.string().optional(),
10233
+ keyFile: z.string().optional(),
10234
+ insecureSkipVerify: z.boolean().optional()
10235
+ }).optional()
10236
+ })
10237
+ ).optional()
9702
10238
  });
9703
10239
  var ClusterInstallConfigSchema = z.object({
9704
10240
  /** k3s release version. Example: v1.35.4+k3s1 */
@@ -9714,7 +10250,86 @@ var ClusterInstallConfigSchema = z.object({
9714
10250
  /** Registry prefix used by k3s for bundled system images. */
9715
10251
  systemDefaultRegistry: z.string().min(1).regex(/^\S+$/, "systemDefaultRegistry must not contain whitespace").optional(),
9716
10252
  /** Sandbox pause image used by k3s/containerd. Useful when Docker Hub is unreachable. */
9717
- pauseImage: z.string().min(1).regex(/^\S+$/, "pauseImage must not contain whitespace").optional()
10253
+ pauseImage: z.string().min(1).regex(/^\S+$/, "pauseImage must not contain whitespace").optional(),
10254
+ /** Optional k3s containerd registry mirrors/auth config for workload images. */
10255
+ registries: ClusterContainerdRegistriesSchema.optional()
10256
+ });
10257
+ var AGENT_SANDBOX_DEFAULT_VERSION = "v0.4.5";
10258
+ var AGENT_SANDBOX_DEFAULT_RUNTIME_CLASS = "shadow-runc";
10259
+ var AGENT_SANDBOX_DEFAULT_RUNTIME_HANDLER = "runc";
10260
+ var RuntimeClassNameSchema = z.string().min(1).regex(/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/, "runtimeClassName must be a Kubernetes DNS label");
10261
+ var ClusterSandboxFeatureConfigSchema = z.object({
10262
+ /** Enable agent-sandbox as a cluster capability. */
10263
+ enabled: z.boolean().default(true),
10264
+ /** Install/upgrade the upstream CRDs and controller during cluster init/apply. */
10265
+ install: z.boolean().default(true),
10266
+ /** Pinned upstream agent-sandbox release used to build default manifest URLs. */
10267
+ version: z.string().min(1).regex(/^v\d+\.\d+\.\d+(?:[-+][A-Za-z0-9._-]+)?$/, "version must look like v0.4.5").default(AGENT_SANDBOX_DEFAULT_VERSION),
10268
+ /**
10269
+ * Optional manifest URLs. Defaults to the upstream release manifest.yaml and extensions.yaml.
10270
+ * Use mirrored URLs for restricted networks.
10271
+ */
10272
+ manifestUrls: z.array(z.string().url()).min(1).optional(),
10273
+ /** Optional controller image override, useful for domestic/private registries. */
10274
+ controllerImage: ImageReferenceSchema.optional(),
10275
+ /** RuntimeClass injected into generated Cloud SaaS sandbox configs. */
10276
+ runtimeClassName: RuntimeClassNameSchema.default(AGENT_SANDBOX_DEFAULT_RUNTIME_CLASS),
10277
+ /** Create RuntimeClass automatically. Disable when the cluster already provides gvisor/runsc. */
10278
+ createRuntimeClass: z.boolean().default(true),
10279
+ /** RuntimeClass handler used when createRuntimeClass is true. */
10280
+ runtimeClassHandler: z.string().min(1).regex(/^\S+$/, "runtimeClassHandler must not contain whitespace").default(AGENT_SANDBOX_DEFAULT_RUNTIME_HANDLER),
10281
+ /** Wait timeout for CRDs/controller readiness. */
10282
+ waitTimeoutSeconds: z.number().int().min(30).max(1200).default(300),
10283
+ /** Fail cluster init/apply when sandbox cannot be verified. */
10284
+ required: z.boolean().default(true),
10285
+ /** Label selector injected into sandbox workloads unless templates override it. */
10286
+ nodeSelector: KubernetesLabelMapSchema.default({ "shadowob.com/sandbox-ready": "true" }),
10287
+ /** Run a real SandboxTemplate/SandboxClaim smoke after install/verify. */
10288
+ smokeTest: z.boolean().default(false),
10289
+ /** Image used by the optional smoke test. Mirror this for restricted networks. */
10290
+ smokeImage: ImageReferenceSchema.default("busybox:1.36")
10291
+ }).refine((sandbox) => !sandbox.createRuntimeClass || Boolean(sandbox.runtimeClassHandler), {
10292
+ path: ["runtimeClassHandler"],
10293
+ message: "runtimeClassHandler is required when createRuntimeClass is true"
10294
+ });
10295
+ var ClusterSandboxFeatureSchema = z.union([z.boolean(), ClusterSandboxFeatureConfigSchema]);
10296
+ var ClusterFeaturesSchema = z.object({
10297
+ /** agent-sandbox CRDs/controller/runtime-class management. */
10298
+ sandbox: ClusterSandboxFeatureSchema.optional()
10299
+ });
10300
+ var NodeRoleSchema = z.enum(["master", "worker"]);
10301
+ var NodeConfigSchema = z.object({
10302
+ /** Node role in the cluster */
10303
+ role: NodeRoleSchema,
10304
+ /** Public IP or hostname */
10305
+ host: z.string().min(1),
10306
+ /** SSH port (default: 22) */
10307
+ port: z.number().int().min(1).max(65535).default(22),
10308
+ /** SSH username */
10309
+ user: z.string().min(1),
10310
+ /** Path to SSH private key (supports ~) — mutually inclusive with or exclusive of password */
10311
+ sshKeyPath: z.string().optional(),
10312
+ /** SSH private key passphrase — use ${env:VAR} to avoid storing plaintext */
10313
+ sshKeyPassphrase: z.string().optional(),
10314
+ /**
10315
+ * SSH agent socket. Use true for SSH_AUTH_SOCK, or a socket path/template.
10316
+ * Useful for encrypted private keys already loaded into an agent.
10317
+ */
10318
+ sshAgent: z.union([z.boolean(), z.string().min(1)]).optional(),
10319
+ /** SSH password — use ${env:VAR} to avoid storing plaintext */
10320
+ password: z.string().optional(),
10321
+ /** Optional per-node k3s installer overrides for mixed-region clusters. */
10322
+ install: ClusterInstallConfigSchema.optional(),
10323
+ /** Region label applied during cluster init/apply, e.g. cn or us. */
10324
+ region: z.string().min(1).max(63).regex(K8S_LABEL_VALUE_RE).optional(),
10325
+ /** Extra Kubernetes node labels applied during cluster init/apply. */
10326
+ labels: KubernetesLabelMapSchema.optional(),
10327
+ /** Per-node feature flags used for mixed-capability clusters. */
10328
+ features: z.object({
10329
+ sandbox: z.boolean().optional()
10330
+ }).optional()
10331
+ }).refine((n) => n.sshKeyPath !== void 0 || n.password !== void 0 || n.sshAgent, {
10332
+ message: "Each node must have either sshKeyPath, password, or sshAgent"
9718
10333
  });
9719
10334
  var ClusterProviderSchema = z.enum(["ssh"]);
9720
10335
  var ClusterConfigSchema = z.object({
@@ -9726,7 +10341,9 @@ var ClusterConfigSchema = z.object({
9726
10341
  /** List of nodes */
9727
10342
  nodes: z.array(NodeConfigSchema).min(1),
9728
10343
  /** Optional k3s installer settings for restricted networks or pinned versions. */
9729
- install: ClusterInstallConfigSchema.optional()
10344
+ install: ClusterInstallConfigSchema.optional(),
10345
+ /** Optional cluster capabilities managed by cluster init/apply. */
10346
+ features: ClusterFeaturesSchema.optional()
9730
10347
  }).refine((c) => c.nodes.filter((n) => n.role === "master").length === 1, {
9731
10348
  message: "Cluster must have exactly one master node"
9732
10349
  });
@@ -9749,7 +10366,7 @@ function readClusterConfig(filePath) {
9749
10366
  const abs = resolve2(filePath);
9750
10367
  let raw;
9751
10368
  try {
9752
- raw = JSON.parse(readFileSync4(abs, "utf8"));
10369
+ raw = JSON.parse(readFileSync3(abs, "utf8"));
9753
10370
  } catch (err) {
9754
10371
  throw new Error(`Failed to read cluster config at ${abs}: ${err.message}`);
9755
10372
  }
@@ -9762,14 +10379,57 @@ ${issues.join("\n")}`);
9762
10379
  return result.data;
9763
10380
  }
9764
10381
  function resolveNodeCredentials(node) {
10382
+ let sshAgent;
10383
+ if (node.sshAgent === true) {
10384
+ sshAgent = process.env.SSH_AUTH_SOCK;
10385
+ if (!sshAgent) {
10386
+ throw new Error("SSH_AUTH_SOCK is not set (required by cluster.json sshAgent=true)");
10387
+ }
10388
+ } else if (typeof node.sshAgent === "string") {
10389
+ sshAgent = resolveEnvTemplate(node.sshAgent);
10390
+ }
9765
10391
  return {
9766
10392
  host: node.host,
9767
10393
  port: node.port,
9768
10394
  user: node.user,
9769
10395
  sshKeyPath: node.sshKeyPath ? expandHome(node.sshKeyPath) : void 0,
10396
+ sshKeyPassphrase: node.sshKeyPassphrase ? resolveEnvTemplate(node.sshKeyPassphrase) : void 0,
10397
+ sshAgent,
9770
10398
  password: node.password ? resolveEnvTemplate(node.password) : void 0
9771
10399
  };
9772
10400
  }
10401
+ function resolveNodeInstallConfig(clusterInstall, node) {
10402
+ const merged = { ...clusterInstall, ...node.install };
10403
+ return Object.keys(merged).length > 0 ? merged : void 0;
10404
+ }
10405
+ function defaultAgentSandboxManifestUrls(version = AGENT_SANDBOX_DEFAULT_VERSION) {
10406
+ return [
10407
+ `https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${version}/manifest.yaml`,
10408
+ `https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${version}/extensions.yaml`
10409
+ ];
10410
+ }
10411
+ function resolveClusterSandboxConfig(config) {
10412
+ const sandbox = config.features?.sandbox;
10413
+ if (sandbox === void 0 || sandbox === false) return null;
10414
+ const normalized = sandbox === true ? {
10415
+ enabled: true,
10416
+ install: true,
10417
+ version: AGENT_SANDBOX_DEFAULT_VERSION,
10418
+ runtimeClassName: AGENT_SANDBOX_DEFAULT_RUNTIME_CLASS,
10419
+ createRuntimeClass: true,
10420
+ runtimeClassHandler: AGENT_SANDBOX_DEFAULT_RUNTIME_HANDLER,
10421
+ waitTimeoutSeconds: 300,
10422
+ required: true,
10423
+ nodeSelector: { "shadowob.com/sandbox-ready": "true" },
10424
+ smokeTest: false,
10425
+ smokeImage: "busybox:1.36"
10426
+ } : sandbox;
10427
+ if (!normalized.enabled) return null;
10428
+ return {
10429
+ ...normalized,
10430
+ manifestUrls: normalized.manifestUrls && normalized.manifestUrls.length > 0 ? normalized.manifestUrls : defaultAgentSandboxManifestUrls(normalized.version)
10431
+ };
10432
+ }
9773
10433
  function getMasterNode(config) {
9774
10434
  const master = config.nodes.find((n) => n.role === "master");
9775
10435
  if (!master) throw new Error("No master node found in cluster config");
@@ -9831,14 +10491,14 @@ import chalk2 from "chalk";
9831
10491
  import { Command as Command17 } from "commander";
9832
10492
 
9833
10493
  // src/utils/env.ts
9834
- import { existsSync as existsSync4 } from "fs";
10494
+ import { existsSync as existsSync5 } from "fs";
9835
10495
  import { resolve as resolve3 } from "path";
9836
10496
  function loadEnvFiles(paths) {
9837
10497
  const loaded = [];
9838
10498
  if (paths && paths.length > 0) {
9839
10499
  for (const p of paths) {
9840
10500
  const abs = resolve3(p);
9841
- if (!existsSync4(abs)) {
10501
+ if (!existsSync5(abs)) {
9842
10502
  throw new Error(`Env file not found: ${abs}`);
9843
10503
  }
9844
10504
  process.loadEnvFile(abs);
@@ -9846,7 +10506,7 @@ function loadEnvFiles(paths) {
9846
10506
  }
9847
10507
  } else {
9848
10508
  const defaultPath = resolve3(".env");
9849
- if (existsSync4(defaultPath)) {
10509
+ if (existsSync5(defaultPath)) {
9850
10510
  process.loadEnvFile(defaultPath);
9851
10511
  loaded.push(defaultPath);
9852
10512
  }
@@ -9856,12 +10516,51 @@ function loadEnvFiles(paths) {
9856
10516
 
9857
10517
  // src/interfaces/cli/build.command.ts
9858
10518
  import { spawn as spawn2 } from "child_process";
9859
- import { existsSync as existsSync5, mkdtempSync as mkdtempSync3, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
10519
+ import { existsSync as existsSync6, mkdtempSync as mkdtempSync3, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
9860
10520
  import { tmpdir as tmpdir3 } from "os";
9861
- import { join as join4, resolve as resolve4 } from "path";
10521
+ import { join as join5, resolve as resolve4 } from "path";
9862
10522
  import { Command } from "commander";
9863
10523
 
9864
10524
  // src/infra/plugin-k8s.ts
10525
+ var COLON_SEPARATED_ENV_KEYS = /* @__PURE__ */ new Set(["PATH", "PYTHONPATH", "NODE_PATH"]);
10526
+ var DEFAULT_CONTAINER_PATH_PARTS = [
10527
+ "/usr/local/sbin",
10528
+ "/usr/local/bin",
10529
+ "/usr/sbin",
10530
+ "/usr/bin",
10531
+ "/sbin",
10532
+ "/bin"
10533
+ ];
10534
+ function uniquePathParts(parts) {
10535
+ const out = [];
10536
+ for (const part of parts) {
10537
+ if (!part || out.includes(part)) continue;
10538
+ out.push(part);
10539
+ }
10540
+ return out;
10541
+ }
10542
+ function mergeColonSeparatedEnvValue(key, current, next) {
10543
+ const parts = [...current.split(":"), ...next.split(":")];
10544
+ if (key !== "PATH") return uniquePathParts(parts).join(":");
10545
+ const defaults = new Set(DEFAULT_CONTAINER_PATH_PARTS);
10546
+ const pluginParts = parts.filter((part) => part && !defaults.has(part));
10547
+ const defaultParts = parts.filter((part) => defaults.has(part));
10548
+ return uniquePathParts([...pluginParts, ...defaultParts]).join(":");
10549
+ }
10550
+ function mergePluginEnvVars(target, incoming) {
10551
+ for (const envVar of incoming) {
10552
+ if (typeof envVar.name === "string" && typeof envVar.value === "string" && COLON_SEPARATED_ENV_KEYS.has(envVar.name)) {
10553
+ const existing = target.find(
10554
+ (candidate) => candidate.name === envVar.name && typeof candidate.value === "string"
10555
+ );
10556
+ if (existing && typeof existing.value === "string") {
10557
+ existing.value = mergeColonSeparatedEnvValue(envVar.name, existing.value, envVar.value);
10558
+ continue;
10559
+ }
10560
+ }
10561
+ target.push(envVar);
10562
+ }
10563
+ }
9865
10564
  function collectPluginK8sArtifacts(agent, config, namespace) {
9866
10565
  const result = {
9867
10566
  initContainers: [],
@@ -9899,9 +10598,7 @@ function collectPluginK8sArtifacts(agent, config, namespace) {
9899
10598
  if (artifacts.volumeMounts?.length) {
9900
10599
  result.volumeMounts.push(...artifacts.volumeMounts);
9901
10600
  }
9902
- if (artifacts.envVars?.length) {
9903
- result.envVars.push(...artifacts.envVars);
9904
- }
10601
+ if (artifacts.envVars?.length) mergePluginEnvVars(result.envVars, artifacts.envVars);
9905
10602
  if (artifacts.labels) {
9906
10603
  Object.assign(result.labels, artifacts.labels);
9907
10604
  }
@@ -9932,7 +10629,7 @@ function createBuildCommand(container) {
9932
10629
  return new Command("build").description("Build Docker images for agents using build-image git source strategy").option("-f, --file <path>", "Config file path", "shadowob-cloud.json").option("-a, --agent <id>", "Build a specific agent (default: all build-image agents)").option("--push", "Push image(s) after building").option("--no-cache", "Pass --no-cache to docker build").option("--platform <platform>", "Target platform(s) e.g. linux/amd64,linux/arm64").option("--tag <tag>", "Override image tag (only valid with --agent)").option("--output-dockerfile", "Print generated Dockerfile(s) to stdout, skip building").action(
9933
10630
  async (options) => {
9934
10631
  const filePath = resolve4(options.file);
9935
- if (!existsSync5(filePath)) {
10632
+ if (!existsSync6(filePath)) {
9936
10633
  container.logger.error(`Config file not found: ${filePath}`);
9937
10634
  process.exit(1);
9938
10635
  }
@@ -9972,8 +10669,8 @@ function createBuildCommand(container) {
9972
10669
  continue;
9973
10670
  }
9974
10671
  container.logger.step(`Building image for ${agent.id}: ${imageTag2}`);
9975
- const tmpDir = mkdtempSync3(join4(tmpdir3(), `shadowob-cloud-build-${agent.id}-`));
9976
- const dockerfilePath = join4(tmpDir, "Dockerfile");
10672
+ const tmpDir = mkdtempSync3(join5(tmpdir3(), `shadowob-cloud-build-${agent.id}-`));
10673
+ const dockerfilePath = join5(tmpDir, "Dockerfile");
9977
10674
  writeFileSync4(dockerfilePath, dockerfile, "utf-8");
9978
10675
  try {
9979
10676
  const buildArgs = ["build", "-t", imageTag2, "-f", dockerfilePath, tmpDir];
@@ -10013,21 +10710,35 @@ import chalk from "chalk";
10013
10710
  import { Command as Command2 } from "commander";
10014
10711
  function createClusterCommand(container) {
10015
10712
  const cluster = new Command2("cluster").description(
10016
- "Manage bare-server k3s clusters (init, status, list, destroy)"
10713
+ "Manage bare-server k3s clusters (init, apply, status, list, destroy)"
10017
10714
  );
10018
- cluster.command("init").description("Bootstrap a k3s cluster on bare servers").option("-c, --config <path>", "Path to cluster.json", "cluster.json").option("--force", "Reinstall k3s even if already installed on nodes").action(async (options) => {
10715
+ async function applyClusterConfig(options) {
10716
+ const config = readClusterConfig(options.config);
10717
+ container.logger.info(`Applying cluster "${config.name}" from ${options.config}...`);
10718
+ const meta = await container.cluster.init(
10719
+ config,
10720
+ (msg) => {
10721
+ container.logger.dim(msg);
10722
+ },
10723
+ options.force
10724
+ );
10725
+ container.logger.success(`Cluster "${meta.name}" ready! Kubeconfig: ${meta.kubeconfigPath}`);
10726
+ container.logger.info(
10727
+ `Edit ${options.config} and run this command again to add newly listed nodes.`
10728
+ );
10729
+ container.logger.info(`Deploy agents: shadowob-cloud up --cluster ${meta.name}`);
10730
+ }
10731
+ cluster.command("init").description("Bootstrap or update a k3s cluster on bare servers").option("-c, --config <path>", "Path to cluster.json", "cluster.json").option("--force", "Reinstall k3s even if already installed on nodes").action(async (options) => {
10019
10732
  try {
10020
- const config = readClusterConfig(options.config);
10021
- container.logger.info(`Initializing cluster "${config.name}"...`);
10022
- const meta = await container.cluster.init(
10023
- config,
10024
- (msg) => {
10025
- container.logger.dim(msg);
10026
- },
10027
- options.force
10028
- );
10029
- container.logger.success(`Cluster "${meta.name}" ready! Kubeconfig: ${meta.kubeconfigPath}`);
10030
- container.logger.info(`Deploy agents: shadowob-cloud up --cluster ${meta.name}`);
10733
+ await applyClusterConfig(options);
10734
+ } catch (err) {
10735
+ container.logger.error(err.message);
10736
+ process.exit(1);
10737
+ }
10738
+ });
10739
+ cluster.command("apply").description("Apply cluster.json idempotently and add newly listed nodes").option("-c, --config <path>", "Path to cluster.json", "cluster.json").option("--force", "Reinstall k3s even if already installed on nodes").action(async (options) => {
10740
+ try {
10741
+ await applyClusterConfig(options);
10031
10742
  } catch (err) {
10032
10743
  container.logger.error(err.message);
10033
10744
  process.exit(1);
@@ -10144,13 +10855,13 @@ function createClusterCommand(container) {
10144
10855
  }
10145
10856
 
10146
10857
  // src/interfaces/cli/costs.command.ts
10147
- import { existsSync as existsSync6 } from "fs";
10858
+ import { existsSync as existsSync7 } from "fs";
10148
10859
  import { resolve as resolve5 } from "path";
10149
10860
  import { Command as Command3 } from "commander";
10150
10861
  async function resolveNamespace(container, file, namespace) {
10151
10862
  if (namespace) return namespace;
10152
10863
  const filePath = resolve5(file);
10153
- if (!existsSync6(filePath)) return "shadowob-cloud";
10864
+ if (!existsSync7(filePath)) return "shadowob-cloud";
10154
10865
  try {
10155
10866
  const config = await container.config.parseFile(filePath);
10156
10867
  return config.deployments?.namespace ?? "shadowob-cloud";
@@ -10220,7 +10931,7 @@ function createCostsCommand(container) {
10220
10931
 
10221
10932
  // src/interfaces/cli/doctor.command.ts
10222
10933
  import { execSync } from "child_process";
10223
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
10934
+ import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
10224
10935
  import { platform } from "os";
10225
10936
  import { Command as Command4 } from "commander";
10226
10937
  function getVersion(cmd, versionFlag = "--version") {
@@ -10374,10 +11085,10 @@ function createDoctorCommand(container) {
10374
11085
  });
10375
11086
  }
10376
11087
  }
10377
- if (existsSync7(".env")) {
11088
+ if (existsSync8(".env")) {
10378
11089
  let inGitignore = false;
10379
- if (existsSync7(".gitignore")) {
10380
- const gitignore = readFileSync5(".gitignore", "utf-8");
11090
+ if (existsSync8(".gitignore")) {
11091
+ const gitignore = readFileSync4(".gitignore", "utf-8");
10381
11092
  inGitignore = gitignore.split("\n").some((line) => line.trim() === ".env");
10382
11093
  }
10383
11094
  secResults.push({
@@ -10432,7 +11143,7 @@ function createDoctorCommand(container) {
10432
11143
  }
10433
11144
 
10434
11145
  // src/interfaces/cli/down.command.ts
10435
- import { existsSync as existsSync8 } from "fs";
11146
+ import { existsSync as existsSync9 } from "fs";
10436
11147
  import { resolve as resolve6 } from "path";
10437
11148
  import { Command as Command5 } from "commander";
10438
11149
  function createDownCommand(container) {
@@ -10441,7 +11152,7 @@ function createDownCommand(container) {
10441
11152
  const filePath = resolve6(options.file);
10442
11153
  let namespace = options.namespace;
10443
11154
  let config;
10444
- if (existsSync8(filePath)) {
11155
+ if (existsSync9(filePath)) {
10445
11156
  try {
10446
11157
  config = await container.config.parseFile(filePath);
10447
11158
  namespace = namespace ?? config.deployments?.namespace;
@@ -10510,7 +11221,7 @@ function createDownCommand(container) {
10510
11221
  }
10511
11222
 
10512
11223
  // src/interfaces/cli/generate.command.ts
10513
- import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
11224
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
10514
11225
  import { dirname, resolve as resolve7 } from "path";
10515
11226
  import { fileURLToPath } from "url";
10516
11227
  import { Command as Command6 } from "commander";
@@ -10520,7 +11231,7 @@ function createGenerateCommand(container) {
10520
11231
  new Command6("manifests").description("Generate K8s YAML/JSON manifests").option("-f, --file <path>", "Config file path", "shadowob-cloud.json").option("-o, --output <dir>", "Output directory", ".shadowob-manifests").option("-n, --namespace <ns>", "Kubernetes namespace").option("--provision-url <url>", "Shadow server URL (for NetworkPolicy egress)").action(
10521
11232
  async (options) => {
10522
11233
  const filePath = resolve7(options.file);
10523
- if (!existsSync9(filePath)) {
11234
+ if (!existsSync10(filePath)) {
10524
11235
  container.logger.error(`Config file not found: ${filePath}`);
10525
11236
  process.exit(1);
10526
11237
  }
@@ -10553,11 +11264,11 @@ function createGenerateCommand(container) {
10553
11264
  cmd.addCommand(
10554
11265
  new Command6("openclaw-config").description("Generate OpenClaw config.json for a specific agent").argument("<agent>", "Agent ID").option("-f, --file <path>", "Config file path", "shadowob-cloud.json").option("-o, --output <path>", "Output file path").action(async (agent, options) => {
10555
11266
  const filePath = resolve7(options.file);
10556
- if (!existsSync9(filePath)) {
11267
+ if (!existsSync10(filePath)) {
10557
11268
  container.logger.error(`Config file not found: ${filePath}`);
10558
11269
  process.exit(1);
10559
11270
  }
10560
- const { loadAllPlugins, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-HZBWK3WQ.js");
11271
+ const { loadAllPlugins, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-2MITZ4ZD.js");
10561
11272
  await loadAllPlugins(getPluginRegistry2());
10562
11273
  const outputPath = options.output ? resolve7(options.output) : void 0;
10563
11274
  const configCwd = dirname(filePath);
@@ -10589,13 +11300,13 @@ function createGenerateCommand(container) {
10589
11300
  "schemas",
10590
11301
  "config.schema.json"
10591
11302
  );
10592
- if (!existsSync9(schemaPath)) {
11303
+ if (!existsSync10(schemaPath)) {
10593
11304
  container.logger.error(
10594
11305
  "Schema file not found. Run `pnpm generate:schema` in the cloud package first."
10595
11306
  );
10596
11307
  process.exit(1);
10597
11308
  }
10598
- const schema = readFileSync6(schemaPath, "utf-8");
11309
+ const schema = readFileSync5(schemaPath, "utf-8");
10599
11310
  const outPath = resolve7(options.output);
10600
11311
  mkdirSync3(dirname(outPath), { recursive: true });
10601
11312
  writeFileSync5(outPath, schema, "utf-8");
@@ -10613,7 +11324,7 @@ import { Command as Command7 } from "commander";
10613
11324
 
10614
11325
  // src/services/image.service.ts
10615
11326
  import { execFileSync as execFileSync2, spawn as spawn3 } from "child_process";
10616
- import { existsSync as existsSync10 } from "fs";
11327
+ import { existsSync as existsSync11 } from "fs";
10617
11328
  import { resolve as resolve8 } from "path";
10618
11329
  var IMAGES = [
10619
11330
  "openclaw-runner",
@@ -10650,7 +11361,7 @@ var ImageService = class {
10650
11361
  const { name, tag = "latest", noCache, intoK8s, push, platform: platform3 } = options;
10651
11362
  const dockerfilePath = resolve8(this.imagesDir, name, "Dockerfile");
10652
11363
  const buildContext = this.getBuildContext(name);
10653
- if (!existsSync10(dockerfilePath)) {
11364
+ if (!existsSync11(dockerfilePath)) {
10654
11365
  throw new Error(`Dockerfile not found: ${dockerfilePath}`);
10655
11366
  }
10656
11367
  if (!IMAGES.includes(name)) {
@@ -10691,14 +11402,14 @@ var ImageService = class {
10691
11402
  list() {
10692
11403
  return IMAGES.map((name) => ({
10693
11404
  name,
10694
- hasDockerfile: existsSync10(resolve8(this.imagesDir, name, "Dockerfile"))
11405
+ hasDockerfile: existsSync11(resolve8(this.imagesDir, name, "Dockerfile"))
10695
11406
  }));
10696
11407
  }
10697
11408
  getBuildContext(name) {
10698
11409
  if (name !== "openclaw-runner") return resolve8(this.imagesDir, name);
10699
11410
  const repoRoot = resolve8(this.imagesDir, "../../..");
10700
11411
  const localShadowobPlugin = resolve8(repoRoot, "packages", "openclaw-shadowob", "package.json");
10701
- return existsSync10(localShadowobPlugin) ? repoRoot : resolve8(this.imagesDir, name);
11412
+ return existsSync11(localShadowobPlugin) ? repoRoot : resolve8(this.imagesDir, name);
10702
11413
  }
10703
11414
  dockerBuild(args) {
10704
11415
  return this.spawnProcess("docker", ["build", ...args]);
@@ -10780,7 +11491,7 @@ function createImagesCommand(container) {
10780
11491
  }
10781
11492
 
10782
11493
  // src/interfaces/cli/logs.command.ts
10783
- import { existsSync as existsSync11 } from "fs";
11494
+ import { existsSync as existsSync12 } from "fs";
10784
11495
  import { resolve as resolve9 } from "path";
10785
11496
  import { Command as Command8 } from "commander";
10786
11497
  function createLogsCommand(container) {
@@ -10789,7 +11500,7 @@ function createLogsCommand(container) {
10789
11500
  let namespace = options.namespace;
10790
11501
  if (!namespace) {
10791
11502
  const filePath = resolve9(options.file);
10792
- if (existsSync11(filePath)) {
11503
+ if (existsSync12(filePath)) {
10793
11504
  try {
10794
11505
  const config = await container.config.parseFile(filePath);
10795
11506
  namespace = config.deployments?.namespace;
@@ -10835,7 +11546,7 @@ function createLogsCommand(container) {
10835
11546
 
10836
11547
  // src/interfaces/cli/onboard.command.ts
10837
11548
  import { execSync as execSync2 } from "child_process";
10838
- import { existsSync as existsSync12 } from "fs";
11549
+ import { existsSync as existsSync13 } from "fs";
10839
11550
  import { platform as platform2 } from "os";
10840
11551
  import { createInterface } from "readline";
10841
11552
  import { Command as Command9 } from "commander";
@@ -10960,12 +11671,12 @@ function createOnboardCommand(container) {
10960
11671
  container.logger.step("Step 3/4 \u2014 Configuration...");
10961
11672
  console.log();
10962
11673
  const configPath = "shadowob-cloud.json";
10963
- if (existsSync12(configPath)) {
11674
+ if (existsSync13(configPath)) {
10964
11675
  container.logger.success(`Config file found: ${configPath}`);
10965
11676
  } else {
10966
11677
  const answer = await ask(` No ${configPath} found. Create one from template? [Y/n] `);
10967
11678
  if (!answer || answer.toLowerCase() !== "n") {
10968
- const { createInitCommand: createInitCommand2 } = await import("./init.command-775GLXTC.js");
11679
+ const { createInitCommand: createInitCommand2 } = await import("./init.command-UNL66BMR.js");
10969
11680
  const init = createInitCommand2(container);
10970
11681
  await init.parseAsync(["--quick"], { from: "user" });
10971
11682
  } else {
@@ -10982,14 +11693,14 @@ function createOnboardCommand(container) {
10982
11693
  }
10983
11694
  container.logger.success("Onboarding complete! Starting console...");
10984
11695
  console.log();
10985
- const { createConsoleCommand: createConsoleCommand2 } = await import("./dashboard.command-H5DIGLUR.js");
11696
+ const { createConsoleCommand: createConsoleCommand2 } = await import("./dashboard.command-GUHSJ2CN.js");
10986
11697
  const console_ = createConsoleCommand2(container);
10987
11698
  await console_.parseAsync([], { from: "user" });
10988
11699
  });
10989
11700
  }
10990
11701
 
10991
11702
  // src/interfaces/cli/provision.command.ts
10992
- import { existsSync as existsSync13 } from "fs";
11703
+ import { existsSync as existsSync14 } from "fs";
10993
11704
  import { resolve as resolve10 } from "path";
10994
11705
  import { Command as Command10 } from "commander";
10995
11706
  function shadowBuddyTokenEnvKey(id) {
@@ -10999,7 +11710,7 @@ function createProvisionCommand(container) {
10999
11710
  return new Command10("provision").description("Provision Shadow resources (servers, channels, buddies) without deploying").option("-f, --file <path>", "Config file path", "shadowob-cloud.json").option("--provision-url <url>", "Shadow server URL").option("--provision-token <token>", "Shadow user token").option("--dry-run", "Preview what would be provisioned").option("--output <path>", "Write provisioned tokens to file").option("--force", "Force re-provisioning even if state shows resources exist").option("--state-dir <dir>", "Subdirectory for provision state (default: .shadowob)").action(
11000
11711
  async (options) => {
11001
11712
  const filePath = resolve10(options.file);
11002
- if (!existsSync13(filePath)) {
11713
+ if (!existsSync14(filePath)) {
11003
11714
  container.logger.error(`Config file not found: ${filePath}`);
11004
11715
  process.exit(1);
11005
11716
  }
@@ -11020,14 +11731,14 @@ function createProvisionCommand(container) {
11020
11731
  process.exit(1);
11021
11732
  }
11022
11733
  try {
11023
- const { executePluginProvisions, loadAllPlugins, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-HZBWK3WQ.js");
11734
+ const { executePluginProvisions, loadAllPlugins, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-2MITZ4ZD.js");
11024
11735
  try {
11025
11736
  await loadAllPlugins(getPluginRegistry2());
11026
11737
  } catch {
11027
11738
  }
11028
11739
  const agents = resolved.deployments?.agents ?? [];
11029
11740
  const namespace = resolved.deployments?.namespace ?? "shadowob-cloud";
11030
- const existing = loadProvisionState(filePath, options.stateDir);
11741
+ const existing = options.force ? null : loadProvisionState(filePath, options.stateDir);
11031
11742
  const extraSecrets = {
11032
11743
  SHADOW_SERVER_URL: shadowUrl,
11033
11744
  SHADOW_USER_TOKEN: shadowToken
@@ -11121,13 +11832,13 @@ function createProvisionCommand(container) {
11121
11832
  }
11122
11833
 
11123
11834
  // src/interfaces/cli/sandbox.command.ts
11124
- import { existsSync as existsSync14 } from "fs";
11835
+ import { existsSync as existsSync15 } from "fs";
11125
11836
  import { resolve as resolve11 } from "path";
11126
11837
  import { Command as Command11 } from "commander";
11127
11838
  async function resolveNamespace2(container, options) {
11128
11839
  if (options.namespace) return options.namespace;
11129
11840
  const filePath = resolve11(options.file ?? "shadowob-cloud.json");
11130
- if (existsSync14(filePath)) {
11841
+ if (existsSync15(filePath)) {
11131
11842
  try {
11132
11843
  const config = await container.config.parseFile(filePath);
11133
11844
  return config.deployments?.namespace ?? "shadowob-cloud";
@@ -11228,7 +11939,7 @@ function createSandboxCommand(container) {
11228
11939
  }
11229
11940
 
11230
11941
  // src/interfaces/cli/scale.command.ts
11231
- import { existsSync as existsSync15 } from "fs";
11942
+ import { existsSync as existsSync16 } from "fs";
11232
11943
  import { resolve as resolve12 } from "path";
11233
11944
  import { Command as Command12 } from "commander";
11234
11945
  function createScaleCommand(container) {
@@ -11237,7 +11948,7 @@ function createScaleCommand(container) {
11237
11948
  let namespace = options.namespace;
11238
11949
  if (!namespace) {
11239
11950
  const filePath = resolve12(options.file);
11240
- if (existsSync15(filePath)) {
11951
+ if (existsSync16(filePath)) {
11241
11952
  try {
11242
11953
  const config = await container.config.parseFile(filePath);
11243
11954
  namespace = config.deployments?.namespace;
@@ -11264,7 +11975,7 @@ function createScaleCommand(container) {
11264
11975
  }
11265
11976
 
11266
11977
  // src/interfaces/cli/status.command.ts
11267
- import { existsSync as existsSync16 } from "fs";
11978
+ import { existsSync as existsSync17 } from "fs";
11268
11979
  import { resolve as resolve13 } from "path";
11269
11980
  import { Command as Command13 } from "commander";
11270
11981
  function createStatusCommand(container) {
@@ -11273,7 +11984,7 @@ function createStatusCommand(container) {
11273
11984
  let namespace = options.namespace;
11274
11985
  let config;
11275
11986
  const filePath = resolve13(options.file);
11276
- if (existsSync16(filePath)) {
11987
+ if (existsSync17(filePath)) {
11277
11988
  try {
11278
11989
  config = await container.config.parseFile(filePath);
11279
11990
  namespace = namespace ?? config.deployments?.namespace;
@@ -11486,13 +12197,13 @@ function createUpCommand(container) {
11486
12197
  }
11487
12198
 
11488
12199
  // src/interfaces/cli/validate.command.ts
11489
- import { existsSync as existsSync17 } from "fs";
12200
+ import { existsSync as existsSync18 } from "fs";
11490
12201
  import { resolve as resolve15 } from "path";
11491
12202
  import { Command as Command16 } from "commander";
11492
12203
  function createValidateCommand(container) {
11493
12204
  return new Command16("validate").description("Validate an shadowob-cloud.json config file").option("-f, --file <path>", "Config file path", "shadowob-cloud.json").option("--strict", "Fail on unresolvable env vars").option("--dry-run", "Validate structure without requiring env vars to be set").action(async (options) => {
11494
12205
  const filePath = resolve15(options.file);
11495
- if (!existsSync17(filePath)) {
12206
+ if (!existsSync18(filePath)) {
11496
12207
  container.logger.error(`Config file not found: ${filePath}`);
11497
12208
  process.exit(1);
11498
12209
  }
@@ -11606,7 +12317,7 @@ function createCLI(container) {
11606
12317
 
11607
12318
  // src/dao/template.dao.ts
11608
12319
  import { cp, mkdir as mkdir2, readdir, readFile, stat } from "fs/promises";
11609
- import { join as join5 } from "path";
12320
+ import { join as join6 } from "path";
11610
12321
  var TemplateDao = class {
11611
12322
  constructor(templatesDir) {
11612
12323
  this.templatesDir = templatesDir;
@@ -11622,7 +12333,7 @@ var TemplateDao = class {
11622
12333
  const seenSlugs = /* @__PURE__ */ new Set();
11623
12334
  const records = [];
11624
12335
  for (const name of dirEntries) {
11625
- const fullPath = join5(this.templatesDir, name);
12336
+ const fullPath = join6(this.templatesDir, name);
11626
12337
  let isDir = false;
11627
12338
  try {
11628
12339
  isDir = (await stat(fullPath)).isDirectory();
@@ -11631,24 +12342,24 @@ var TemplateDao = class {
11631
12342
  }
11632
12343
  if (!isDir) continue;
11633
12344
  const slug = name;
11634
- const configPath = join5(this.templatesDir, slug, "shadowob-cloud.json");
12345
+ const configPath = join6(this.templatesDir, slug, "shadowob-cloud.json");
11635
12346
  if (!await this.exists(configPath)) continue;
11636
12347
  seenSlugs.add(slug);
11637
- const indexPath = join5(this.templatesDir, slug, "index.json");
12348
+ const indexPath = join6(this.templatesDir, slug, "index.json");
11638
12349
  const hasIndex = await this.exists(indexPath);
11639
12350
  records.push({
11640
12351
  slug,
11641
12352
  isFolder: true,
11642
12353
  configPath,
11643
12354
  indexPath: hasIndex ? indexPath : null,
11644
- dir: join5(this.templatesDir, slug)
12355
+ dir: join6(this.templatesDir, slug)
11645
12356
  });
11646
12357
  }
11647
12358
  for (const name of dirEntries) {
11648
12359
  if (!name.endsWith(".template.json")) continue;
11649
12360
  const slug = name.replace(/\.template\.json$/, "");
11650
12361
  if (seenSlugs.has(slug)) continue;
11651
- const configPath = join5(this.templatesDir, name);
12362
+ const configPath = join6(this.templatesDir, name);
11652
12363
  records.push({
11653
12364
  slug,
11654
12365
  isFolder: false,
@@ -11661,19 +12372,19 @@ var TemplateDao = class {
11661
12372
  }
11662
12373
  /** Find a single template by slug. Returns null if not found. */
11663
12374
  async findBySlug(slug) {
11664
- const folderConfig = join5(this.templatesDir, slug, "shadowob-cloud.json");
12375
+ const folderConfig = join6(this.templatesDir, slug, "shadowob-cloud.json");
11665
12376
  if (await this.exists(folderConfig)) {
11666
- const indexPath = join5(this.templatesDir, slug, "index.json");
12377
+ const indexPath = join6(this.templatesDir, slug, "index.json");
11667
12378
  const hasIndex = await this.exists(indexPath);
11668
12379
  return {
11669
12380
  slug,
11670
12381
  isFolder: true,
11671
12382
  configPath: folderConfig,
11672
12383
  indexPath: hasIndex ? indexPath : null,
11673
- dir: join5(this.templatesDir, slug)
12384
+ dir: join6(this.templatesDir, slug)
11674
12385
  };
11675
12386
  }
11676
- const flatConfig = join5(this.templatesDir, `${slug}.template.json`);
12387
+ const flatConfig = join6(this.templatesDir, `${slug}.template.json`);
11677
12388
  if (await this.exists(flatConfig)) {
11678
12389
  return {
11679
12390
  slug,
@@ -11710,7 +12421,7 @@ var TemplateDao = class {
11710
12421
  if (record.isFolder) {
11711
12422
  await cp(record.dir, destDir, { recursive: true });
11712
12423
  } else {
11713
- await cp(record.configPath, join5(destDir, "shadowob-cloud.json"));
12424
+ await cp(record.configPath, join6(destDir, "shadowob-cloud.json"));
11714
12425
  }
11715
12426
  }
11716
12427
  async exists(path) {
@@ -11724,7 +12435,7 @@ var TemplateDao = class {
11724
12435
  };
11725
12436
 
11726
12437
  // src/cluster/ssh.ts
11727
- import { readFileSync as readFileSync7 } from "fs";
12438
+ import { readFileSync as readFileSync6 } from "fs";
11728
12439
  import { NodeSSH } from "node-ssh";
11729
12440
  var SSHClient = class {
11730
12441
  ssh = new NodeSSH();
@@ -11734,7 +12445,7 @@ var SSHClient = class {
11734
12445
  host: opts.host,
11735
12446
  port: opts.port,
11736
12447
  username: opts.user,
11737
- ...opts.sshKeyPath ? { privateKey: readFileSync7(opts.sshKeyPath, "utf8") } : { password: opts.password },
12448
+ ...opts.sshAgent ? { agent: opts.sshAgent } : opts.sshKeyPath ? { privateKey: readFileSync6(opts.sshKeyPath, "utf8"), passphrase: opts.sshKeyPassphrase } : { password: opts.password },
11738
12449
  // Reasonable timeout for WAN SSH
11739
12450
  readyTimeout: 3e4
11740
12451
  });
@@ -11820,9 +12531,14 @@ async function destroyCluster(options) {
11820
12531
  }
11821
12532
 
11822
12533
  // src/cluster/init.ts
11823
- var K3S_INSTALL_URL = "https://get.k3s.io";
11824
- var CN_PAUSE_IMAGE = "registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6";
11825
- var CN_SYSTEM_DEFAULT_REGISTRY = "registry.cn-hangzhou.aliyuncs.com";
12534
+ import { createHash as createHash2 } from "crypto";
12535
+
12536
+ // src/cluster/sandbox.ts
12537
+ var REQUIRED_AGENT_SANDBOX_CRDS = [
12538
+ "sandboxes.agents.x-k8s.io",
12539
+ "sandboxtemplates.extensions.agents.x-k8s.io",
12540
+ "sandboxclaims.extensions.agents.x-k8s.io"
12541
+ ];
11826
12542
  function log2(onLog, msg) {
11827
12543
  onLog?.(msg);
11828
12544
  }
@@ -11833,6 +12549,291 @@ function asRoot2(shellCommand) {
11833
12549
  const quoted = shellQuote2(shellCommand);
11834
12550
  return `if [ "$(id -u)" -eq 0 ]; then sh -c ${quoted}; else sudo -n sh -c ${quoted}; fi`;
11835
12551
  }
12552
+ async function hasAgentSandboxCrds(client) {
12553
+ const result = await client.exec(
12554
+ asRoot2(
12555
+ `k3s kubectl get crd ${REQUIRED_AGENT_SANDBOX_CRDS.map(shellQuote2).join(" ")} >/dev/null`
12556
+ )
12557
+ );
12558
+ return result.code === 0;
12559
+ }
12560
+ async function applyManifestUrl(client, url, onLog) {
12561
+ log2(onLog, `[sandbox] Applying ${url}`);
12562
+ await client.execOrThrow(asRoot2(`k3s kubectl apply -f ${shellQuote2(url)}`), {
12563
+ onStdout: (chunk) => log2(onLog, `[sandbox] ${chunk.trimEnd()}`),
12564
+ onStderr: (chunk) => log2(onLog, `[sandbox] ${chunk.trimEnd()}`),
12565
+ errorMessage: `Failed to apply agent-sandbox manifest ${url}. If this node cannot reach GitHub, set features.sandbox.manifestUrls to mirrored URLs.`
12566
+ });
12567
+ }
12568
+ async function createOrVerifyRuntimeClass(client, options) {
12569
+ if (options.create) {
12570
+ const yaml = [
12571
+ "apiVersion: node.k8s.io/v1",
12572
+ "kind: RuntimeClass",
12573
+ "metadata:",
12574
+ ` name: ${options.name}`,
12575
+ `handler: ${options.handler}`,
12576
+ ""
12577
+ ].join("\n");
12578
+ log2(
12579
+ options.onLog,
12580
+ `[sandbox] Ensuring RuntimeClass ${options.name} uses handler ${options.handler}`
12581
+ );
12582
+ await client.execOrThrow(asRoot2(`cat <<'EOF' | k3s kubectl apply -f -
12583
+ ${yaml}EOF`), {
12584
+ errorMessage: `Failed to create RuntimeClass ${options.name}`
12585
+ });
12586
+ return;
12587
+ }
12588
+ log2(options.onLog, `[sandbox] Verifying RuntimeClass ${options.name}`);
12589
+ await client.execOrThrow(asRoot2(`k3s kubectl get runtimeclass ${shellQuote2(options.name)}`), {
12590
+ errorMessage: `RuntimeClass ${options.name} was not found. Install the runtime handler first, or set features.sandbox.createRuntimeClass=true with an explicit runtimeClassHandler.`
12591
+ });
12592
+ }
12593
+ async function waitForAgentSandboxReady2(client, timeoutSeconds, onLog) {
12594
+ const crdNames = REQUIRED_AGENT_SANDBOX_CRDS.map((name) => `crd/${name}`).join(" ");
12595
+ log2(onLog, "[sandbox] Waiting for agent-sandbox CRDs to become Established");
12596
+ await client.execOrThrow(
12597
+ asRoot2(`k3s kubectl wait --for=condition=Established ${crdNames} --timeout=${timeoutSeconds}s`),
12598
+ { errorMessage: "agent-sandbox CRDs did not become Established" }
12599
+ );
12600
+ log2(onLog, "[sandbox] Waiting for agent-sandbox controller rollout");
12601
+ await client.execOrThrow(
12602
+ asRoot2(
12603
+ `k3s kubectl -n agent-sandbox-system rollout status deployment/agent-sandbox-controller --timeout=${timeoutSeconds}s`
12604
+ ),
12605
+ { errorMessage: "agent-sandbox controller did not become Ready" }
12606
+ );
12607
+ }
12608
+ function nodeLabelArgs(labels) {
12609
+ return Object.entries(labels).map(([key, value]) => `${key}=${value}`).map(shellQuote2).join(" ");
12610
+ }
12611
+ function kubeNodeMatchesConfigNode(kubeNode, node) {
12612
+ const metadata = kubeNode.metadata ?? {};
12613
+ const status = kubeNode.status ?? {};
12614
+ const addresses = Array.isArray(status.addresses) ? status.addresses : [];
12615
+ const candidates = /* @__PURE__ */ new Set([
12616
+ typeof metadata.name === "string" ? metadata.name : "",
12617
+ ...addresses.map((address) => address.address).filter((address) => typeof address === "string")
12618
+ ]);
12619
+ return candidates.has(node.host);
12620
+ }
12621
+ async function labelConfiguredNodes(client, config, sandboxReady, onLog) {
12622
+ const output2 = await client.execOrThrow(asRoot2("k3s kubectl get nodes -o json"), {
12623
+ errorMessage: "Failed to list Kubernetes nodes for labeling"
12624
+ });
12625
+ const kubeNodes = JSON.parse(output2.stdout).items ?? [];
12626
+ for (const node of config.nodes) {
12627
+ const kubeNode = kubeNodes.find((candidate) => kubeNodeMatchesConfigNode(candidate, node));
12628
+ if (!kubeNode) {
12629
+ log2(onLog, `[sandbox] Could not match cluster.json node ${node.host} to a Kubernetes node`);
12630
+ continue;
12631
+ }
12632
+ const metadata = kubeNode.metadata;
12633
+ const nodeName = metadata.name;
12634
+ if (typeof nodeName !== "string" || !nodeName) continue;
12635
+ const nodeSandboxReady = sandboxReady && (node.features?.sandbox ?? true);
12636
+ const labels = {
12637
+ "shadowob.com/sandbox-ready": nodeSandboxReady ? "true" : "false",
12638
+ ...node.region ? { "shadowob.com/region": node.region } : {},
12639
+ ...node.labels ?? {}
12640
+ };
12641
+ log2(onLog, `[sandbox] Labeling node ${nodeName}`);
12642
+ await client.execOrThrow(
12643
+ asRoot2(`k3s kubectl label node ${shellQuote2(nodeName)} ${nodeLabelArgs(labels)} --overwrite`),
12644
+ { errorMessage: `Failed to label Kubernetes node ${nodeName}` }
12645
+ );
12646
+ }
12647
+ }
12648
+ async function runAgentSandboxSmoke(client, options) {
12649
+ const namespace = `shadow-sandbox-smoke-${Date.now().toString(36)}`;
12650
+ const manifest = [
12651
+ {
12652
+ apiVersion: "v1",
12653
+ kind: "Namespace",
12654
+ metadata: { name: namespace }
12655
+ },
12656
+ {
12657
+ apiVersion: "extensions.agents.x-k8s.io/v1alpha1",
12658
+ kind: "SandboxTemplate",
12659
+ metadata: { name: "smoke-template", namespace },
12660
+ spec: {
12661
+ networkPolicyManagement: "Unmanaged",
12662
+ envVarsInjectionPolicy: "Disallowed",
12663
+ podTemplate: {
12664
+ metadata: { labels: { app: "shadow-sandbox-smoke" } },
12665
+ spec: {
12666
+ runtimeClassName: options.runtimeClassName,
12667
+ containers: [
12668
+ {
12669
+ name: "smoke",
12670
+ image: options.image,
12671
+ command: ["sh", "-c", "echo shadow-sandbox-smoke && sleep 30"]
12672
+ }
12673
+ ],
12674
+ restartPolicy: "Always"
12675
+ }
12676
+ },
12677
+ volumeClaimTemplates: []
12678
+ }
12679
+ },
12680
+ {
12681
+ apiVersion: "extensions.agents.x-k8s.io/v1alpha1",
12682
+ kind: "SandboxClaim",
12683
+ metadata: { name: "smoke", namespace },
12684
+ spec: {
12685
+ sandboxTemplateRef: { name: "smoke-template" },
12686
+ warmpool: "none",
12687
+ lifecycle: { shutdownPolicy: "Delete" }
12688
+ }
12689
+ }
12690
+ ];
12691
+ const yaml = JSON.stringify({ apiVersion: "v1", kind: "List", items: manifest }, null, 2);
12692
+ log2(options.onLog, `[sandbox] Running smoke test in namespace ${namespace}`);
12693
+ try {
12694
+ await client.execOrThrow(asRoot2(`cat <<'EOF' | k3s kubectl apply -f -
12695
+ ${yaml}
12696
+ EOF`), {
12697
+ errorMessage: "Failed to create agent-sandbox smoke resources"
12698
+ });
12699
+ await client.execOrThrow(
12700
+ asRoot2(
12701
+ `timeout ${options.timeoutSeconds} sh -c ` + shellQuote2(
12702
+ 'until [ "$(k3s kubectl -n ' + namespace + ' get sandboxclaim smoke -o jsonpath="{.status.conditions[?(@.type==\\"Ready\\")].status}" 2>/dev/null)" = "True" ]; do sleep 3; done'
12703
+ )
12704
+ ),
12705
+ { errorMessage: "agent-sandbox smoke SandboxClaim did not become Ready" }
12706
+ );
12707
+ log2(options.onLog, "[sandbox] Smoke test passed");
12708
+ } finally {
12709
+ await client.exec(
12710
+ asRoot2(`k3s kubectl delete namespace ${shellQuote2(namespace)} --ignore-not-found=true`)
12711
+ );
12712
+ }
12713
+ }
12714
+ async function installClusterSandbox(options) {
12715
+ const sandbox = resolveClusterSandboxConfig(options.config);
12716
+ if (!sandbox) return false;
12717
+ const master = getMasterNode(options.config);
12718
+ const creds = resolveNodeCredentials(master);
12719
+ const client = new SSHClient();
12720
+ log2(options.onLog, `[sandbox ${creds.host}] Connecting via SSH...`);
12721
+ await client.connect(creds);
12722
+ try {
12723
+ const alreadyInstalled = await hasAgentSandboxCrds(client);
12724
+ if (!sandbox.install && !alreadyInstalled) {
12725
+ const message = "features.sandbox.enabled=true but the required agent-sandbox CRDs are not installed. Set features.sandbox.install=true or apply the CRDs/controller before using sandbox.";
12726
+ if (sandbox.required) throw new Error(message);
12727
+ log2(options.onLog, `[sandbox] ${message}`);
12728
+ return false;
12729
+ }
12730
+ if (sandbox.install) {
12731
+ log2(options.onLog, `[sandbox] Installing agent-sandbox ${sandbox.version}`);
12732
+ for (const url of sandbox.manifestUrls) {
12733
+ await applyManifestUrl(client, url, options.onLog);
12734
+ }
12735
+ if (sandbox.controllerImage) {
12736
+ log2(options.onLog, `[sandbox] Setting controller image ${sandbox.controllerImage}`);
12737
+ await client.execOrThrow(
12738
+ asRoot2(
12739
+ `k3s kubectl -n agent-sandbox-system set image deployment/agent-sandbox-controller agent-sandbox-controller=${shellQuote2(sandbox.controllerImage)}`
12740
+ ),
12741
+ { errorMessage: "Failed to set agent-sandbox controller image" }
12742
+ );
12743
+ }
12744
+ }
12745
+ await createOrVerifyRuntimeClass(client, {
12746
+ name: sandbox.runtimeClassName,
12747
+ handler: sandbox.runtimeClassHandler,
12748
+ create: sandbox.createRuntimeClass,
12749
+ onLog: options.onLog
12750
+ });
12751
+ await waitForAgentSandboxReady2(client, sandbox.waitTimeoutSeconds, options.onLog);
12752
+ await labelConfiguredNodes(client, options.config, true, options.onLog);
12753
+ if (sandbox.smokeTest) {
12754
+ await runAgentSandboxSmoke(client, {
12755
+ runtimeClassName: sandbox.runtimeClassName,
12756
+ image: sandbox.smokeImage,
12757
+ timeoutSeconds: sandbox.waitTimeoutSeconds,
12758
+ onLog: options.onLog
12759
+ });
12760
+ }
12761
+ log2(
12762
+ options.onLog,
12763
+ `[sandbox] Ready with RuntimeClass ${sandbox.runtimeClassName}; new deployments can use agent-sandbox`
12764
+ );
12765
+ return true;
12766
+ } catch (err) {
12767
+ if (sandbox.required) throw err;
12768
+ log2(
12769
+ options.onLog,
12770
+ `[sandbox] Not ready; deployment fallback remains available: ${err.message}`
12771
+ );
12772
+ return false;
12773
+ } finally {
12774
+ await client.dispose();
12775
+ }
12776
+ }
12777
+
12778
+ // src/cluster/init.ts
12779
+ var K3S_INSTALL_URL = "https://get.k3s.io";
12780
+ var CN_PAUSE_IMAGE = "registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6";
12781
+ var CN_SYSTEM_DEFAULT_REGISTRY = "registry.cn-hangzhou.aliyuncs.com";
12782
+ function log3(onLog, msg) {
12783
+ onLog?.(msg);
12784
+ }
12785
+ function shellQuote3(value) {
12786
+ return `'${value.replace(/'/g, "'\\''")}'`;
12787
+ }
12788
+ function asRoot3(shellCommand) {
12789
+ const quoted = shellQuote3(shellCommand);
12790
+ return `if [ "$(id -u)" -eq 0 ]; then sh -c ${quoted}; else sudo -n sh -c ${quoted}; fi`;
12791
+ }
12792
+ function resolveEnvTemplates(value) {
12793
+ if (typeof value === "string") {
12794
+ return value.replace(/\$\{env:([^}]+)\}/g, (_match, envKey) => {
12795
+ const envVal = process.env[envKey];
12796
+ if (envVal === void 0) {
12797
+ throw new Error(`Environment variable "${envKey}" is not set (required by cluster.json)`);
12798
+ }
12799
+ return envVal;
12800
+ });
12801
+ }
12802
+ if (Array.isArray(value)) return value.map(resolveEnvTemplates);
12803
+ if (value && typeof value === "object") {
12804
+ return Object.fromEntries(
12805
+ Object.entries(value).map(([key, child]) => [key, resolveEnvTemplates(child)])
12806
+ );
12807
+ }
12808
+ return value;
12809
+ }
12810
+ function clusterConfigHash(config) {
12811
+ return createHash2("sha256").update(stableStringify(config)).digest("hex");
12812
+ }
12813
+ function stableStringify(value) {
12814
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
12815
+ if (value && typeof value === "object") {
12816
+ return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => `${JSON.stringify(key)}:${stableStringify(child)}`).join(",")}}`;
12817
+ }
12818
+ return JSON.stringify(value);
12819
+ }
12820
+ async function ensureK3sRegistriesConfig(client, install2, serviceName, onLog) {
12821
+ if (!install2?.registries) return;
12822
+ const registries = JSON.stringify(resolveEnvTemplates(install2.registries), null, 2);
12823
+ log3(onLog, `[${serviceName}] Writing k3s containerd registries.yaml`);
12824
+ await client.execOrThrow(
12825
+ asRoot3(
12826
+ [
12827
+ "mkdir -p /etc/rancher/k3s",
12828
+ `cat > /etc/rancher/k3s/registries.yaml <<'EOF'
12829
+ ${registries}
12830
+ EOF`,
12831
+ `if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet ${serviceName}; then systemctl restart ${serviceName}; fi`
12832
+ ].join("\n")
12833
+ ),
12834
+ { errorMessage: `Failed to write k3s registries.yaml for ${serviceName}` }
12835
+ );
12836
+ }
11836
12837
  function resolveK3sMirror(install2) {
11837
12838
  return process.env.INSTALL_K3S_MIRROR ?? process.env.K3S_INSTALL_MIRROR ?? install2?.k3sMirror;
11838
12839
  }
@@ -11901,7 +12902,7 @@ function k3sInstallEnv(extra, install2) {
11901
12902
  INSTALL_K3S_VERSION: version,
11902
12903
  ...extra
11903
12904
  };
11904
- return Object.entries(env).filter((entry) => Boolean(entry[1])).map(([key, value]) => `${key}=${shellQuote2(value)}`).join(" ");
12905
+ return Object.entries(env).filter((entry) => Boolean(entry[1])).map(([key, value]) => `${key}=${shellQuote3(value)}`).join(" ");
11905
12906
  }
11906
12907
  async function isK3sInstalled(client) {
11907
12908
  const result = await client.exec("which k3s");
@@ -11910,25 +12911,26 @@ async function isK3sInstalled(client) {
11910
12911
  async function installMaster(master, install2, force, onLog) {
11911
12912
  const creds = resolveNodeCredentials(master);
11912
12913
  const client = new SSHClient();
11913
- log2(onLog, `[master ${creds.host}] Connecting via SSH...`);
12914
+ log3(onLog, `[master ${creds.host}] Connecting via SSH...`);
11914
12915
  await client.connect(creds);
11915
12916
  try {
12917
+ await ensureK3sRegistriesConfig(client, install2, "k3s", onLog);
11916
12918
  const alreadyInstalled = await isK3sInstalled(client);
11917
12919
  if (alreadyInstalled && !force) {
11918
- log2(
12920
+ log3(
11919
12921
  onLog,
11920
12922
  `[master ${creds.host}] k3s already installed \u2014 skipping install (use --force to reinstall)`
11921
12923
  );
11922
- log2(onLog, `[master ${creds.host}] Reading existing token and kubeconfig...`);
12924
+ log3(onLog, `[master ${creds.host}] Reading existing token and kubeconfig...`);
11923
12925
  } else {
11924
12926
  if (alreadyInstalled && force) {
11925
- log2(onLog, `[master ${creds.host}] k3s already installed \u2014 reinstalling (--force)`);
11926
- await client.exec(asRoot2("/usr/local/bin/k3s-uninstall.sh 2>/dev/null || true"));
12927
+ log3(onLog, `[master ${creds.host}] k3s already installed \u2014 reinstalling (--force)`);
12928
+ await client.exec(asRoot3("/usr/local/bin/k3s-uninstall.sh 2>/dev/null || true"));
11927
12929
  }
11928
- log2(onLog, `[master ${creds.host}] Installing k3s server...`);
12930
+ log3(onLog, `[master ${creds.host}] Installing k3s server...`);
11929
12931
  await client.execOrThrow(
11930
- asRoot2(
11931
- `curl -sfL ${shellQuote2(K3S_INSTALL_URL)} | ${k3sInstallEnv(
12932
+ asRoot3(
12933
+ `curl -sfL ${shellQuote3(K3S_INSTALL_URL)} | ${k3sInstallEnv(
11932
12934
  {
11933
12935
  INSTALL_K3S_EXEC: k3sExec(["server", "--tls-san", creds.host], install2)
11934
12936
  },
@@ -11936,28 +12938,28 @@ async function installMaster(master, install2, force, onLog) {
11936
12938
  )} sh -`
11937
12939
  ),
11938
12940
  {
11939
- onStdout: (c) => log2(onLog, `[master] ${c.trimEnd()}`),
11940
- onStderr: (c) => log2(onLog, `[master] ${c.trimEnd()}`),
12941
+ onStdout: (c) => log3(onLog, `[master] ${c.trimEnd()}`),
12942
+ onStderr: (c) => log3(onLog, `[master] ${c.trimEnd()}`),
11941
12943
  errorMessage: `Failed to install k3s on master ${creds.host}`
11942
12944
  }
11943
12945
  );
11944
12946
  }
11945
- log2(onLog, `[master ${creds.host}] Waiting for k3s to be ready...`);
12947
+ log3(onLog, `[master ${creds.host}] Waiting for k3s to be ready...`);
11946
12948
  await client.execOrThrow(
11947
- asRoot2(`timeout 120 sh -c 'until k3s kubectl get nodes > /dev/null 2>&1; do sleep 3; done'`),
12949
+ asRoot3(`timeout 120 sh -c 'until k3s kubectl get nodes > /dev/null 2>&1; do sleep 3; done'`),
11948
12950
  { errorMessage: "k3s master did not become ready within 120s" }
11949
12951
  );
11950
- log2(onLog, `[master ${creds.host}] Reading node token...`);
12952
+ log3(onLog, `[master ${creds.host}] Reading node token...`);
11951
12953
  const tokenResult = await client.execOrThrow(
11952
- asRoot2("cat /var/lib/rancher/k3s/server/node-token"),
12954
+ asRoot3("cat /var/lib/rancher/k3s/server/node-token"),
11953
12955
  {
11954
12956
  errorMessage: "Failed to read k3s node token"
11955
12957
  }
11956
12958
  );
11957
12959
  const token = tokenResult.stdout.trim();
11958
12960
  if (!token) throw new Error("k3s node token is empty");
11959
- log2(onLog, `[master ${creds.host}] Reading kubeconfig...`);
11960
- const kubeconfigResult = await client.execOrThrow(asRoot2("cat /etc/rancher/k3s/k3s.yaml"), {
12961
+ log3(onLog, `[master ${creds.host}] Reading kubeconfig...`);
12962
+ const kubeconfigResult = await client.execOrThrow(asRoot3("cat /etc/rancher/k3s/k3s.yaml"), {
11961
12963
  errorMessage: "Failed to read k3s kubeconfig"
11962
12964
  });
11963
12965
  return { token, kubeconfig: kubeconfigResult.stdout };
@@ -11968,25 +12970,26 @@ async function installMaster(master, install2, force, onLog) {
11968
12970
  async function installWorker(worker, masterHost, token, install2, force, onLog) {
11969
12971
  const creds = resolveNodeCredentials(worker);
11970
12972
  const client = new SSHClient();
11971
- log2(onLog, `[worker ${creds.host}] Connecting via SSH...`);
12973
+ log3(onLog, `[worker ${creds.host}] Connecting via SSH...`);
11972
12974
  await client.connect(creds);
11973
12975
  try {
12976
+ await ensureK3sRegistriesConfig(client, install2, "k3s-agent", onLog);
11974
12977
  const alreadyInstalled = await isK3sInstalled(client);
11975
12978
  if (alreadyInstalled && !force) {
11976
- log2(
12979
+ log3(
11977
12980
  onLog,
11978
12981
  `[worker ${creds.host}] k3s already installed \u2014 skipping install (use --force to reinstall)`
11979
12982
  );
11980
12983
  return;
11981
12984
  }
11982
12985
  if (alreadyInstalled && force) {
11983
- log2(onLog, `[worker ${creds.host}] k3s already installed \u2014 reinstalling (--force)`);
11984
- await client.exec(asRoot2("/usr/local/bin/k3s-agent-uninstall.sh 2>/dev/null || true"));
12986
+ log3(onLog, `[worker ${creds.host}] k3s already installed \u2014 reinstalling (--force)`);
12987
+ await client.exec(asRoot3("/usr/local/bin/k3s-agent-uninstall.sh 2>/dev/null || true"));
11985
12988
  }
11986
- log2(onLog, `[worker ${creds.host}] Installing k3s agent and joining cluster...`);
12989
+ log3(onLog, `[worker ${creds.host}] Installing k3s agent and joining cluster...`);
11987
12990
  await client.execOrThrow(
11988
- asRoot2(
11989
- `curl -sfL ${shellQuote2(K3S_INSTALL_URL)} | ${k3sInstallEnv(
12991
+ asRoot3(
12992
+ `curl -sfL ${shellQuote3(K3S_INSTALL_URL)} | ${k3sInstallEnv(
11990
12993
  {
11991
12994
  K3S_URL: `https://${masterHost}:6443`,
11992
12995
  K3S_TOKEN: token,
@@ -11996,12 +12999,12 @@ async function installWorker(worker, masterHost, token, install2, force, onLog)
11996
12999
  )} sh -`
11997
13000
  ),
11998
13001
  {
11999
- onStdout: (c) => log2(onLog, `[worker ${creds.host}] ${c.trimEnd()}`),
12000
- onStderr: (c) => log2(onLog, `[worker ${creds.host}] ${c.trimEnd()}`),
13002
+ onStdout: (c) => log3(onLog, `[worker ${creds.host}] ${c.trimEnd()}`),
13003
+ onStderr: (c) => log3(onLog, `[worker ${creds.host}] ${c.trimEnd()}`),
12001
13004
  errorMessage: `Failed to install k3s agent on worker ${creds.host}`
12002
13005
  }
12003
13006
  );
12004
- log2(onLog, `[worker ${creds.host}] Agent joined cluster \u2713`);
13007
+ log3(onLog, `[worker ${creds.host}] Agent joined cluster \u2713`);
12005
13008
  } finally {
12006
13009
  await client.dispose();
12007
13010
  }
@@ -12010,17 +13013,39 @@ async function initCluster(options) {
12010
13013
  const { config, force = false, onLog } = options;
12011
13014
  const master = getMasterNode(config);
12012
13015
  const workers = getWorkerNodes(config);
12013
- log2(onLog, `Initializing cluster "${config.name}" with ${config.nodes.length} nodes...`);
12014
- const { token, kubeconfig } = await installMaster(master, config.install, force, onLog);
13016
+ log3(onLog, `Initializing cluster "${config.name}" with ${config.nodes.length} nodes...`);
13017
+ const masterInstall = resolveNodeInstallConfig(config.install, master);
13018
+ const { token, kubeconfig } = await installMaster(master, masterInstall, force, onLog);
12015
13019
  if (workers.length > 0) {
12016
- log2(onLog, `Installing ${workers.length} worker(s) in parallel...`);
13020
+ log3(onLog, `Installing ${workers.length} worker(s) in parallel...`);
12017
13021
  await Promise.all(
12018
- workers.map((w) => installWorker(w, master.host, token, config.install, force, onLog))
13022
+ workers.map(
13023
+ (worker) => installWorker(
13024
+ worker,
13025
+ master.host,
13026
+ token,
13027
+ resolveNodeInstallConfig(config.install, worker),
13028
+ force,
13029
+ onLog
13030
+ )
13031
+ )
12019
13032
  );
12020
13033
  }
12021
- const meta = storeKubeconfig(config.name, kubeconfig, master.host, config.nodes.length);
12022
- log2(onLog, `Kubeconfig stored at ${meta.kubeconfigPath}`);
12023
- log2(onLog, `Cluster "${config.name}" is ready. Use: shadowob-cloud up --cluster ${config.name}`);
13034
+ const sandboxConfig = resolveClusterSandboxConfig(config);
13035
+ const sandboxEnabled = await installClusterSandbox({ config, onLog });
13036
+ const meta = storeKubeconfig(config.name, kubeconfig, master.host, config.nodes.length, {
13037
+ configHash: clusterConfigHash(config),
13038
+ features: {
13039
+ sandbox: sandboxConfig ? {
13040
+ enabled: sandboxEnabled,
13041
+ version: sandboxConfig.version,
13042
+ runtimeClassName: sandboxConfig.runtimeClassName,
13043
+ nodeSelector: sandboxConfig.nodeSelector
13044
+ } : { enabled: false }
13045
+ }
13046
+ });
13047
+ log3(onLog, `Kubeconfig stored at ${meta.kubeconfigPath}`);
13048
+ log3(onLog, `Cluster "${config.name}" is ready. Use: shadowob-cloud up --cluster ${config.name}`);
12024
13049
  return meta;
12025
13050
  }
12026
13051
 
@@ -12139,7 +13164,7 @@ var ConfigService = class {
12139
13164
  };
12140
13165
 
12141
13166
  // src/services/deploy.service.ts
12142
- import { existsSync as existsSync18, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
13167
+ import { existsSync as existsSync19, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
12143
13168
  import { dirname as dirname3, resolve as resolve16 } from "path";
12144
13169
  function deploymentReadyTimeoutMs() {
12145
13170
  const raw = Number(process.env.CLOUD_DEPLOYMENT_READY_TIMEOUT_MS);
@@ -12148,13 +13173,22 @@ function deploymentReadyTimeoutMs() {
12148
13173
  function isAgentSandboxBackend(config) {
12149
13174
  return (config.deployments?.backend ?? "agent-sandbox") === "agent-sandbox";
12150
13175
  }
13176
+ function workloadBackendPolicy(config) {
13177
+ if (config.deployments?.backendPolicy) return config.deployments.backendPolicy;
13178
+ return isAgentSandboxBackend(config) ? "sandbox-required" : "deployment-only";
13179
+ }
13180
+ function sandboxRuntimeClassNames(config) {
13181
+ const deployments = config.deployments;
13182
+ if (!deployments) return [];
13183
+ const defaultRuntimeClassName = deployments.sandbox?.runtimeClassName ?? "gvisor";
13184
+ const names = deployments.agents.map(
13185
+ (agent) => agent.sandbox?.runtimeClassName ?? defaultRuntimeClassName
13186
+ );
13187
+ return [...new Set(names)];
13188
+ }
12151
13189
  function readKubeconfigForRuntimeWait(kubeConfigPath) {
12152
13190
  if (!kubeConfigPath) return void 0;
12153
- try {
12154
- return readFileSync8(kubeConfigPath, "utf8");
12155
- } catch {
12156
- return void 0;
12157
- }
13191
+ return readKubeconfigFile(kubeConfigPath);
12158
13192
  }
12159
13193
  function resolveStackName(namespace, stack) {
12160
13194
  return stack ?? `dev-${namespace}`;
@@ -12196,7 +13230,7 @@ function configUsesPlugins(value, depth = 0) {
12196
13230
  }
12197
13231
  async function ensureBuiltInPluginsLoaded() {
12198
13232
  try {
12199
- const { loadAllPlugins, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-HZBWK3WQ.js");
13233
+ const { loadAllPlugins, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-2MITZ4ZD.js");
12200
13234
  const registry = getPluginRegistry2();
12201
13235
  if (registry.size === 0) await loadAllPlugins(registry);
12202
13236
  } catch {
@@ -12205,7 +13239,7 @@ async function ensureBuiltInPluginsLoaded() {
12205
13239
  function readKubeconfigCurrentContext(kubeConfigPath) {
12206
13240
  if (!kubeConfigPath) return void 0;
12207
13241
  try {
12208
- return readFileSync8(kubeConfigPath, "utf8").match(/current-context:\s*(\S+)/)?.[1];
13242
+ return readKubeconfigFile(kubeConfigPath).match(/current-context:\s*(\S+)/)?.[1];
12209
13243
  } catch {
12210
13244
  return void 0;
12211
13245
  }
@@ -12218,6 +13252,39 @@ function summarizeK8sTarget(options, kubeConfigPath) {
12218
13252
  const kubeconfig = kubeConfigPath ?? process.env.KUBECONFIG ?? "~/.kube/config";
12219
13253
  return `Kubernetes target: cluster=${cluster} context=${context} kubeconfig=${kubeconfig}`;
12220
13254
  }
13255
+ function applyManagedClusterDefaults(config, clusterName) {
13256
+ if (!clusterName || !config.deployments) return;
13257
+ const sandbox = loadClusterMeta(clusterName)?.features?.sandbox;
13258
+ if (!config.deployments.backendPolicy) {
13259
+ config.deployments.backendPolicy = sandbox?.enabled ? "sandbox-preferred" : config.deployments.backend === "agent-sandbox" ? "sandbox-required" : "deployment-only";
13260
+ }
13261
+ if (!config.deployments.backend) {
13262
+ config.deployments.backend = config.deployments.backendPolicy === "deployment-only" ? "deployment" : sandbox?.enabled ? "agent-sandbox" : "deployment";
13263
+ }
13264
+ if (config.deployments.backend === "agent-sandbox" && sandbox?.enabled && sandbox.runtimeClassName) {
13265
+ config.deployments.sandbox = {
13266
+ ...config.deployments.sandbox ?? {},
13267
+ runtimeClassName: config.deployments.sandbox?.runtimeClassName ?? sandbox.runtimeClassName
13268
+ };
13269
+ }
13270
+ if (config.deployments.backend === "agent-sandbox" && sandbox?.enabled && sandbox.nodeSelector) {
13271
+ config.deployments.scheduling = {
13272
+ ...config.deployments.scheduling ?? {},
13273
+ nodeSelector: {
13274
+ ...sandbox.nodeSelector,
13275
+ ...config.deployments.scheduling?.nodeSelector ?? {}
13276
+ }
13277
+ };
13278
+ }
13279
+ }
13280
+ function describeSandboxPreflightFailure(missing, warnings) {
13281
+ return [
13282
+ "agent-sandbox preflight failed.",
13283
+ missing.length > 0 ? `Missing: ${missing.join(", ")}.` : "",
13284
+ warnings.length > 0 ? `Warnings: ${warnings.join(", ")}.` : "",
13285
+ 'Run "shadowob-cloud cluster apply --config cluster.json" to install/verify sandbox, or set deployments.backendPolicy="deployment-only" for fallback.'
13286
+ ].filter(Boolean).join(" ");
13287
+ }
12221
13288
  var DeployService = class {
12222
13289
  constructor(configService, manifestService, k8s6, logger) {
12223
13290
  this.configService = configService;
@@ -12237,10 +13304,13 @@ var DeployService = class {
12237
13304
  });
12238
13305
  const runtimeContext = normalizeDeploymentRuntimeContext(options.runtimeContext);
12239
13306
  const effectiveEnv = buildEffectiveEnv(options.runtimeEnvVars, runtimeContext);
12240
- if (!existsSync18(filePath)) {
13307
+ if (!existsSync19(filePath)) {
12241
13308
  throw new Error(`Config file not found: ${filePath}`);
12242
13309
  }
12243
13310
  const kubeConfigPath = options.kubeConfigPath ?? (options.cluster ? loadKubeconfigPath(options.cluster) : void 0);
13311
+ if (kubeConfigPath) {
13312
+ assertReadableKubeconfigFile(kubeConfigPath);
13313
+ }
12244
13314
  const k8sTargetSummary = summarizeK8sTarget(options, kubeConfigPath);
12245
13315
  this.logger.info(k8sTargetSummary);
12246
13316
  emit(`${k8sTargetSummary}
@@ -12253,6 +13323,7 @@ var DeployService = class {
12253
13323
  await this.configService.parseFile(filePath),
12254
13324
  runtimeContext
12255
13325
  );
13326
+ applyManagedClusterDefaults(config, options.cluster);
12256
13327
  const namespace = options.namespace ?? config.deployments?.namespace ?? "shadowob-cloud";
12257
13328
  const stackName = resolveStackName(namespace, options.stack);
12258
13329
  const agents = config.deployments?.agents ?? [];
@@ -12297,10 +13368,31 @@ var DeployService = class {
12297
13368
  const usesPlugins = configUsesPlugins(config);
12298
13369
  if (usesPlugins) await ensureBuiltInPluginsLoaded();
12299
13370
  const resolved = await this.configService.resolve(config, configCwd, { env: effectiveEnv });
13371
+ if (resolved.deployments && isAgentSandboxBackend(resolved)) {
13372
+ const policy = workloadBackendPolicy(resolved);
13373
+ const preflight = this.k8s.checkAgentSandboxPreflight({
13374
+ kubeconfig: readKubeconfigForRuntimeWait(kubeConfigPath),
13375
+ runtimeClassNames: sandboxRuntimeClassNames(resolved)
13376
+ });
13377
+ if (!preflight.ok) {
13378
+ const message = describeSandboxPreflightFailure(preflight.missing, preflight.warnings);
13379
+ if (policy === "sandbox-preferred") {
13380
+ this.logger.warn(`${message} Falling back to Kubernetes Deployment.`);
13381
+ emit(`${message} Falling back to Kubernetes Deployment.
13382
+ `);
13383
+ resolved.deployments.backend = "deployment";
13384
+ resolved.deployments.backendPolicy = "deployment-only";
13385
+ } else {
13386
+ throw new Error(message);
13387
+ }
13388
+ } else if (preflight.warnings.length > 0) {
13389
+ this.logger.warn(`agent-sandbox preflight warnings: ${preflight.warnings.join(", ")}`);
13390
+ }
13391
+ }
12300
13392
  if (usesPlugins) await ensureBuiltInPluginsLoaded();
12301
13393
  if (!options.skipProvision) {
12302
13394
  try {
12303
- const { executePluginProvisions, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-HZBWK3WQ.js");
13395
+ const { executePluginProvisions, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-2MITZ4ZD.js");
12304
13396
  for (const agent of agents) {
12305
13397
  const provisionResults = await executePluginProvisions(
12306
13398
  agent,
@@ -12315,6 +13407,9 @@ var DeployService = class {
12315
13407
  for (const e of provisionResults.errors) {
12316
13408
  this.logger.warn(`Plugin provision error (${e.pluginId}): ${e.error}`);
12317
13409
  }
13410
+ throw new Error(
13411
+ `Plugin provisioning failed: ${provisionResults.errors.map((e) => `${e.pluginId}: ${e.error}`).join("; ")}`
13412
+ );
12318
13413
  }
12319
13414
  if (Object.keys(provisionResults.secrets).length > 0) {
12320
13415
  agent.env = { ...agent.env ?? {}, ...provisionResults.secrets };
@@ -12338,7 +13433,10 @@ var DeployService = class {
12338
13433
  await options.onProvisionState?.(merged);
12339
13434
  }
12340
13435
  }
12341
- } catch {
13436
+ } catch (err) {
13437
+ const message = err instanceof Error ? err.message : String(err);
13438
+ this.logger.warn(`Plugin provisioning failed; aborting deploy: ${message}`);
13439
+ throw err;
12342
13440
  }
12343
13441
  }
12344
13442
  const k8sShadowUrl = options.k8sShadowUrl ?? effectiveEnv.SHADOW_AGENT_SERVER_URL ?? options.shadowUrl ?? effectiveEnv.K8S_SHADOW_URL ?? effectiveEnv.SHADOW_SERVER_URL;
@@ -12411,11 +13509,11 @@ var DeployService = class {
12411
13509
  emit("Lock canceled, retrying...\n");
12412
13510
  }
12413
13511
  } catch {
12414
- const { join: join7 } = await import("path");
13512
+ const { join: join8 } = await import("path");
12415
13513
  const { homedir: homedir6 } = await import("os");
12416
- const { rmSync: rmSync4, existsSync: existsSync19 } = await import("fs");
12417
- const lockDir = join7(homedir6(), ".shadowob", "pulumi", ".pulumi", "locks");
12418
- if (existsSync19(lockDir)) {
13514
+ const { rmSync: rmSync4, existsSync: existsSync20 } = await import("fs");
13515
+ const lockDir = join8(homedir6(), ".shadowob", "pulumi", ".pulumi", "locks");
13516
+ if (existsSync20(lockDir)) {
12419
13517
  try {
12420
13518
  rmSync4(lockDir, { recursive: true });
12421
13519
  this.logger.info("Lock files removed, retrying...");
@@ -12515,6 +13613,9 @@ var DeployService = class {
12515
13613
  `Cannot destroy namespace "${namespace}" without a Pulumi config snapshot. Destroy must run through the deployment stack state.`
12516
13614
  );
12517
13615
  }
13616
+ if (options.kubeConfigPath) {
13617
+ assertReadableKubeconfigFile(options.kubeConfigPath);
13618
+ }
12518
13619
  const stack = await this.k8s.getOrCreateStack({
12519
13620
  stackName,
12520
13621
  config: options.config,
@@ -12856,7 +13957,7 @@ function rolloutUndoAll(namespace) {
12856
13957
  import { execFileSync as execFileSync3 } from "child_process";
12857
13958
  import { mkdir as mkdir3 } from "fs/promises";
12858
13959
  import { homedir as homedir5 } from "os";
12859
- import { delimiter as delimiter3, join as join6 } from "path";
13960
+ import { delimiter as delimiter3, join as join7 } from "path";
12860
13961
  import * as automation from "@pulumi/pulumi/automation/index.js";
12861
13962
  import { PulumiCommand } from "@pulumi/pulumi/automation/index.js";
12862
13963
 
@@ -13077,6 +14178,29 @@ function baseVolumes(configMapName) {
13077
14178
  { name: RUNNER_AGENTS_VOLUME_NAME, emptyDir: {} }
13078
14179
  ];
13079
14180
  }
14181
+ function isAgentSandboxBackend2(config) {
14182
+ return (config.deployments?.backend ?? "agent-sandbox") === "agent-sandbox";
14183
+ }
14184
+ function hasKeys(value) {
14185
+ return Boolean(value && typeof value === "object" && Object.keys(value).length > 0);
14186
+ }
14187
+ function resolveSchedulingConfig(config, agent) {
14188
+ const defaults = isAgentSandboxBackend2(config) ? { nodeSelector: { "shadowob.com/sandbox-ready": "true" } } : {};
14189
+ const globalScheduling = config.deployments?.scheduling ?? {};
14190
+ const agentScheduling = agent.scheduling ?? {};
14191
+ const nodeSelector = {
14192
+ ...defaults.nodeSelector ?? {},
14193
+ ...globalScheduling.nodeSelector ?? {},
14194
+ ...agentScheduling.nodeSelector ?? {}
14195
+ };
14196
+ const affinity = agentScheduling.affinity ?? globalScheduling.affinity;
14197
+ const tolerations = agentScheduling.tolerations ?? globalScheduling.tolerations;
14198
+ return {
14199
+ ...Object.keys(nodeSelector).length > 0 ? { nodeSelector } : {},
14200
+ ...hasKeys(affinity) ? { affinity } : {},
14201
+ ...tolerations && tolerations.length > 0 ? { tolerations } : {}
14202
+ };
14203
+ }
13080
14204
  function buildAgentPodSpec(options) {
13081
14205
  const runtime = getRuntime(options.agent.runtime);
13082
14206
  const image = options.agent.image ?? runtime.defaultImage;
@@ -13171,6 +14295,7 @@ function buildAgentPodSpec(options) {
13171
14295
  initContainers,
13172
14296
  containers,
13173
14297
  volumes,
14298
+ scheduling: resolveSchedulingConfig(options.config, options.agent),
13174
14299
  pluginArtifacts
13175
14300
  };
13176
14301
  }
@@ -13268,6 +14393,9 @@ function createAgentDeployment(options) {
13268
14393
  securityContext: buildSecurityContext(),
13269
14394
  containers: pod.containers,
13270
14395
  volumes: pod.volumes,
14396
+ nodeSelector: pod.scheduling.nodeSelector,
14397
+ affinity: pod.scheduling.affinity,
14398
+ tolerations: pod.scheduling.tolerations,
13271
14399
  restartPolicy: "Always"
13272
14400
  }
13273
14401
  }
@@ -13473,6 +14601,9 @@ function buildAgentSandboxTemplateManifest(options) {
13473
14601
  securityContext: buildSecurityContext(),
13474
14602
  containers: options.pod.containers,
13475
14603
  volumes: options.pod.volumes,
14604
+ nodeSelector: options.pod.scheduling.nodeSelector,
14605
+ affinity: options.pod.scheduling.affinity,
14606
+ tolerations: options.pod.scheduling.tolerations,
13476
14607
  restartPolicy: "Always"
13477
14608
  }
13478
14609
  },
@@ -13560,18 +14691,18 @@ function createConfigResources(options) {
13560
14691
  }
13561
14692
 
13562
14693
  // src/infra/hash.ts
13563
- import { createHash as createHash2 } from "crypto";
13564
- function stableStringify(value) {
14694
+ import { createHash as createHash3 } from "crypto";
14695
+ function stableStringify2(value) {
13565
14696
  if (Array.isArray(value)) {
13566
- return `[${value.map((item) => stableStringify(item)).join(",")}]`;
14697
+ return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
13567
14698
  }
13568
14699
  if (value && typeof value === "object") {
13569
- return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(",")}}`;
14700
+ return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${stableStringify2(item)}`).join(",")}}`;
13570
14701
  }
13571
14702
  return JSON.stringify(value);
13572
14703
  }
13573
14704
  function stableHash(value) {
13574
- return createHash2("sha256").update(stableStringify(value)).digest("hex");
14705
+ return createHash3("sha256").update(stableStringify2(value)).digest("hex");
13575
14706
  }
13576
14707
 
13577
14708
  // src/infra/networking.ts
@@ -13671,6 +14802,9 @@ function classifyEnv(registrySecretEnv, mergedEnv) {
13671
14802
  }
13672
14803
  return { plainEnv, secretData };
13673
14804
  }
14805
+ function omitEnvKeys(env, keys) {
14806
+ for (const key of keys) delete env[key];
14807
+ }
13674
14808
  function runtimePackageEnvDefaults(options) {
13675
14809
  const env = {};
13676
14810
  if (!options.currentEnv.SHADOW_SLASH_COMMANDS_PATH) {
@@ -13693,6 +14827,7 @@ function buildAgentRuntimePackage(options) {
13693
14827
  ...agent.env ?? {},
13694
14828
  ...extraEnv ?? {}
13695
14829
  };
14830
+ const runtimeEnvOmitKeys = collectPluginRuntimeEnvOmitKeys(agent, config, cwd, runtimeEnv);
13696
14831
  const runtimeExtensions = collectPluginRuntimeExtensions(agent, config, cwd, runtimeEnv);
13697
14832
  const mergedEnv = {
13698
14833
  ...collectPluginBuildEnvVars(agent, config, cwd, runtimeEnv),
@@ -13718,6 +14853,7 @@ function buildAgentRuntimePackage(options) {
13718
14853
  if (runtimeArtifacts.provisionSecrets) {
13719
14854
  Object.assign(mergedEnv, runtimeArtifacts.provisionSecrets);
13720
14855
  }
14856
+ omitEnvKeys(mergedEnv, runtimeEnvOmitKeys);
13721
14857
  const { plainEnv, secretData } = classifyEnv(registrySecretEnv, mergedEnv);
13722
14858
  return {
13723
14859
  runtimeKind: runtime.runtimeKind,
@@ -13730,10 +14866,9 @@ function buildAgentRuntimePackage(options) {
13730
14866
  }
13731
14867
 
13732
14868
  // src/infra/shared.ts
13733
- import { readFileSync as readFileSync9 } from "fs";
13734
14869
  import * as k8s5 from "@pulumi/kubernetes";
13735
14870
  function createSharedResources(options) {
13736
- const providerConfig = options.kubeConfigPath ? { kubeconfig: readFileSync9(options.kubeConfigPath, "utf8") } : {
14871
+ const providerConfig = options.kubeConfigPath ? { kubeconfig: readKubeconfigFile(options.kubeConfigPath) } : {
13737
14872
  context: options.kubeContext ?? process.env.KUBECONFIG_CONTEXT ?? process.env.K8S_CONTEXT ?? "rancher-desktop"
13738
14873
  };
13739
14874
  const provider = new k8s5.Provider("k8s-provider", providerConfig);
@@ -14125,13 +15260,13 @@ function getNonEmptyEnv(name) {
14125
15260
  return trimmed.length > 0 ? trimmed : void 0;
14126
15261
  }
14127
15262
  function getDefaultStateDir() {
14128
- return getNonEmptyEnv("PULUMI_BACKEND_URL") ? "" : join6(homedir5(), ".shadowob", "pulumi");
15263
+ return getNonEmptyEnv("PULUMI_BACKEND_URL") ? "" : join7(homedir5(), ".shadowob", "pulumi");
14129
15264
  }
14130
15265
  function resolvePulumiBackendUrl(stateDir) {
14131
15266
  return getNonEmptyEnv("PULUMI_BACKEND_URL") ?? (stateDir ? `file://${stateDir}` : void 0);
14132
15267
  }
14133
15268
  function ensurePulumiCliOnPath(cliRoot) {
14134
- const binDir = join6(cliRoot, "bin");
15269
+ const binDir = join7(cliRoot, "bin");
14135
15270
  const currentPath = process.env.PATH ?? "";
14136
15271
  const parts = currentPath.split(delimiter3).filter(Boolean);
14137
15272
  if (!parts.includes(binDir)) {
@@ -14161,8 +15296,8 @@ ${stderr}` : ""}`
14161
15296
  }
14162
15297
  }
14163
15298
  async function getOrCreateStack(options) {
14164
- const cliRoot = join6(homedir5(), ".shadowob", "pulumi", "cli");
14165
- const pulumiHome = join6(homedir5(), ".shadowob", "pulumi", "home");
15299
+ const cliRoot = join7(homedir5(), ".shadowob", "pulumi", "cli");
15300
+ const pulumiHome = join7(homedir5(), ".shadowob", "pulumi", "home");
14166
15301
  const infraOpts = {
14167
15302
  config: options.config,
14168
15303
  namespace: options.namespace,
@@ -14358,6 +15493,9 @@ var K8sService = class {
14358
15493
  async waitForAgentSandboxPaused(options) {
14359
15494
  return waitForAgentSandboxPaused(options);
14360
15495
  }
15496
+ checkAgentSandboxPreflight(options) {
15497
+ return checkAgentSandboxPreflight(options);
15498
+ }
14361
15499
  async restorePvcFromVolumeSnapshot(options) {
14362
15500
  await restorePvcFromVolumeSnapshot(options);
14363
15501
  }