@shadowob/cloud 1.1.7 → 1.1.9

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-35LJYCQF.js → chunk-3CT6RQNM.js} +745 -482
  29. package/dist/{chunk-KODMGZUC.js → chunk-4YO3NA26.js} +1 -1
  30. package/dist/{chunk-RECNVWMT.js → chunk-6V7MW4HU.js} +17 -3
  31. package/dist/{chunk-C6OI4ZNO.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-ZHVYNIHA.js → chunk-P5Y6F2NH.js} +745 -482
  36. package/dist/{chunk-EEFMJYKB.js → chunk-PSK2SYZ3.js} +2 -1
  37. package/dist/{chunk-6YAYCWGK.js → chunk-PYJRFKPN.js} +1 -1
  38. package/dist/{chunk-JY2HTT7Q.js → chunk-RMDY3W4V.js} +6 -0
  39. package/dist/{chunk-EWB7L7IW.js → chunk-X2SREECR.js} +6 -6
  40. package/dist/{chunk-LXJBQBGL.js → chunk-X5VOIA72.js} +6 -6
  41. package/dist/{chunk-CTNUKOQE.js → chunk-Y5BJ3EW2.js} +6 -0
  42. package/dist/{chunk-SAP2DBHO.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-ZMQFKLNQ.js → dashboard.command-BRPZCZER.js} +1 -1
  57. package/dist/{dashboard.command-2AM45SIT.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-YVG4X6II.js → init.command-C7UKPK2Y.js} +3 -3
  95. package/dist/{init.command-JKE3SXAS.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-XLBJUOV6.js → serve.command-G5RVQFUD.js} +3 -3
  135. package/dist/{serve.command-RD6I6MFD.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 +7 -0
  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/cli.js CHANGED
@@ -1,23 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- createConsoleCommand
4
- } from "./chunk-KODMGZUC.js";
5
- import {
6
- createInitCommand
7
- } from "./chunk-SAP2DBHO.js";
8
- import {
9
- formatProvisionState,
10
- loadProvisionState,
11
- mergeProvisionState,
12
- saveProvisionState
13
- } from "./chunk-RTPBU5HF.js";
14
2
  import {
15
3
  createServeCommand,
16
4
  detectInlineKey,
17
5
  resolveCloudPackageAssetDir,
18
6
  toProviderSecretEnvKey,
19
7
  withLegacyEnvAliases
20
- } from "./chunk-LXJBQBGL.js";
8
+ } from "./chunk-X5VOIA72.js";
9
+ import {
10
+ createConsoleCommand
11
+ } from "./chunk-4YO3NA26.js";
12
+ import {
13
+ createInitCommand
14
+ } from "./chunk-Y6BKVDG7.js";
21
15
  import {
22
16
  RUNNER_AGENTS_VOLUME_NAME,
23
17
  RUNNER_CONFIG_MOUNT_PATH,
@@ -29,6 +23,7 @@ import {
29
23
  SHADOW_SLASH_COMMANDS_PATH,
30
24
  buildOpenClawConfig,
31
25
  collectPluginBuildEnvVars,
26
+ collectPluginRuntimeEnvOmitKeys,
32
27
  collectPluginRuntimeExtensions,
33
28
  collectRuntimeEnvRefPolicy,
34
29
  collectTemplateRefs,
@@ -43,11 +38,17 @@ import {
43
38
  resolveConfig,
44
39
  runtimeContextEnv,
45
40
  runtimeStatePvcName
46
- } from "./chunk-35LJYCQF.js";
41
+ } from "./chunk-3CT6RQNM.js";
47
42
  import {
48
43
  deepMerge,
49
44
  getPluginRegistry
50
- } from "./chunk-6P2K6QZR.js";
45
+ } from "./chunk-ZGMWSSCC.js";
46
+ import {
47
+ formatProvisionState,
48
+ loadProvisionState,
49
+ mergeProvisionState,
50
+ saveProvisionState
51
+ } from "./chunk-RTPBU5HF.js";
51
52
  import {
52
53
  log
53
54
  } from "./chunk-BF6CV2Y4.js";
@@ -89,6 +90,45 @@ import { join, resolve as resolve2 } from "path";
89
90
  import { Command } from "commander";
90
91
 
91
92
  // src/infra/plugin-k8s.ts
93
+ var COLON_SEPARATED_ENV_KEYS = /* @__PURE__ */ new Set(["PATH", "PYTHONPATH", "NODE_PATH"]);
94
+ var DEFAULT_CONTAINER_PATH_PARTS = [
95
+ "/usr/local/sbin",
96
+ "/usr/local/bin",
97
+ "/usr/sbin",
98
+ "/usr/bin",
99
+ "/sbin",
100
+ "/bin"
101
+ ];
102
+ function uniquePathParts(parts) {
103
+ const out = [];
104
+ for (const part of parts) {
105
+ if (!part || out.includes(part)) continue;
106
+ out.push(part);
107
+ }
108
+ return out;
109
+ }
110
+ function mergeColonSeparatedEnvValue(key, current, next) {
111
+ const parts = [...current.split(":"), ...next.split(":")];
112
+ if (key !== "PATH") return uniquePathParts(parts).join(":");
113
+ const defaults = new Set(DEFAULT_CONTAINER_PATH_PARTS);
114
+ const pluginParts = parts.filter((part) => part && !defaults.has(part));
115
+ const defaultParts = parts.filter((part) => defaults.has(part));
116
+ return uniquePathParts([...pluginParts, ...defaultParts]).join(":");
117
+ }
118
+ function mergePluginEnvVars(target, incoming) {
119
+ for (const envVar of incoming) {
120
+ if (typeof envVar.name === "string" && typeof envVar.value === "string" && COLON_SEPARATED_ENV_KEYS.has(envVar.name)) {
121
+ const existing = target.find(
122
+ (candidate) => candidate.name === envVar.name && typeof candidate.value === "string"
123
+ );
124
+ if (existing && typeof existing.value === "string") {
125
+ existing.value = mergeColonSeparatedEnvValue(envVar.name, existing.value, envVar.value);
126
+ continue;
127
+ }
128
+ }
129
+ target.push(envVar);
130
+ }
131
+ }
92
132
  function collectPluginK8sArtifacts(agent, config, namespace) {
93
133
  const result = {
94
134
  initContainers: [],
@@ -126,9 +166,7 @@ function collectPluginK8sArtifacts(agent, config, namespace) {
126
166
  if (artifacts.volumeMounts?.length) {
127
167
  result.volumeMounts.push(...artifacts.volumeMounts);
128
168
  }
129
- if (artifacts.envVars?.length) {
130
- result.envVars.push(...artifacts.envVars);
131
- }
169
+ if (artifacts.envVars?.length) mergePluginEnvVars(result.envVars, artifacts.envVars);
132
170
  if (artifacts.labels) {
133
171
  Object.assign(result.labels, artifacts.labels);
134
172
  }
@@ -259,7 +297,7 @@ function getKubeconfigPath(clusterName) {
259
297
  function getMetaPath(clusterName) {
260
298
  return join2(getClustersDir(), `${clusterName}.json`);
261
299
  }
262
- function storeKubeconfig(clusterName, rawKubeconfig, masterPublicIp, nodeCount) {
300
+ function storeKubeconfig(clusterName, rawKubeconfig, masterPublicIp, nodeCount, options) {
263
301
  const dir = getClustersDir();
264
302
  mkdirSync(dir, { recursive: true });
265
303
  const kubeconfig = rawKubeconfig.replace(/https?:\/\/127\.0\.0\.1/g, `https://${masterPublicIp}`);
@@ -270,7 +308,9 @@ function storeKubeconfig(clusterName, rawKubeconfig, masterPublicIp, nodeCount)
270
308
  masterHost: masterPublicIp,
271
309
  nodeCount,
272
310
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
273
- kubeconfigPath
311
+ kubeconfigPath,
312
+ ...options?.configHash ? { configHash: options.configHash } : {},
313
+ ...options?.features ? { features: options.features } : {}
274
314
  };
275
315
  writeFileSync2(getMetaPath(clusterName), JSON.stringify(meta, null, 2), { mode: 384 });
276
316
  return meta;
@@ -285,6 +325,15 @@ Run: shadowob-cloud cluster init --config cluster.json`
285
325
  }
286
326
  return path;
287
327
  }
328
+ function loadClusterMeta(clusterName) {
329
+ const path = getMetaPath(clusterName);
330
+ if (!existsSync3(path)) return null;
331
+ try {
332
+ return JSON.parse(readFileSync(path, "utf8"));
333
+ } catch {
334
+ return null;
335
+ }
336
+ }
288
337
  function listRegisteredClusters() {
289
338
  const dir = getClustersDir();
290
339
  if (!existsSync3(dir)) return [];
@@ -330,22 +379,41 @@ import { resolve as resolve4 } from "path";
330
379
 
331
380
  // src/cluster/schema.ts
332
381
  import { z } from "zod";
333
- var NodeRoleSchema = z.enum(["master", "worker"]);
334
- var NodeConfigSchema = z.object({
335
- /** Node role in the cluster */
336
- role: NodeRoleSchema,
337
- /** Public IP or hostname */
338
- host: z.string().min(1),
339
- /** SSH port (default: 22) */
340
- port: z.number().int().min(1).max(65535).default(22),
341
- /** SSH username */
342
- user: z.string().min(1),
343
- /** Path to SSH private key (supports ~) — mutually inclusive with or exclusive of password */
344
- sshKeyPath: z.string().optional(),
345
- /** SSH password — use ${env:VAR} to avoid storing plaintext */
346
- password: z.string().optional()
347
- }).refine((n) => n.sshKeyPath !== void 0 || n.password !== void 0, {
348
- message: "Each node must have either sshKeyPath or password"
382
+ 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])?$/;
383
+ var K8S_LABEL_VALUE_RE = /^(([A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9])?)$/;
384
+ var KubernetesLabelMapSchema = z.record(
385
+ z.string().min(1).regex(K8S_LABEL_KEY_RE, "Invalid Kubernetes label key"),
386
+ z.string().max(63).regex(K8S_LABEL_VALUE_RE, "Invalid Kubernetes label value")
387
+ );
388
+ var ImageReferenceSchema = z.string().min(1).regex(/^\S+$/, "image must not contain whitespace");
389
+ var ClusterContainerdRegistriesSchema = z.object({
390
+ /**
391
+ * k3s containerd mirrors. Written to /etc/rancher/k3s/registries.yaml as JSON/YAML.
392
+ * Example: { "docker.io": { "endpoint": ["https://registry-1.docker.io"] } }
393
+ */
394
+ mirrors: z.record(
395
+ z.string().min(1),
396
+ z.object({
397
+ endpoint: z.array(z.string().url()).min(1)
398
+ })
399
+ ).optional(),
400
+ configs: z.record(
401
+ z.string().min(1),
402
+ z.object({
403
+ auth: z.object({
404
+ username: z.string().optional(),
405
+ password: z.string().optional(),
406
+ auth: z.string().optional(),
407
+ identityToken: z.string().optional()
408
+ }).optional(),
409
+ tls: z.object({
410
+ caFile: z.string().optional(),
411
+ certFile: z.string().optional(),
412
+ keyFile: z.string().optional(),
413
+ insecureSkipVerify: z.boolean().optional()
414
+ }).optional()
415
+ })
416
+ ).optional()
349
417
  });
350
418
  var ClusterInstallConfigSchema = z.object({
351
419
  /** k3s release version. Example: v1.35.4+k3s1 */
@@ -361,7 +429,86 @@ var ClusterInstallConfigSchema = z.object({
361
429
  /** Registry prefix used by k3s for bundled system images. */
362
430
  systemDefaultRegistry: z.string().min(1).regex(/^\S+$/, "systemDefaultRegistry must not contain whitespace").optional(),
363
431
  /** Sandbox pause image used by k3s/containerd. Useful when Docker Hub is unreachable. */
364
- pauseImage: z.string().min(1).regex(/^\S+$/, "pauseImage must not contain whitespace").optional()
432
+ pauseImage: z.string().min(1).regex(/^\S+$/, "pauseImage must not contain whitespace").optional(),
433
+ /** Optional k3s containerd registry mirrors/auth config for workload images. */
434
+ registries: ClusterContainerdRegistriesSchema.optional()
435
+ });
436
+ var AGENT_SANDBOX_DEFAULT_VERSION = "v0.4.5";
437
+ var AGENT_SANDBOX_DEFAULT_RUNTIME_CLASS = "shadow-runc";
438
+ var AGENT_SANDBOX_DEFAULT_RUNTIME_HANDLER = "runc";
439
+ var RuntimeClassNameSchema = z.string().min(1).regex(/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/, "runtimeClassName must be a Kubernetes DNS label");
440
+ var ClusterSandboxFeatureConfigSchema = z.object({
441
+ /** Enable agent-sandbox as a cluster capability. */
442
+ enabled: z.boolean().default(true),
443
+ /** Install/upgrade the upstream CRDs and controller during cluster init/apply. */
444
+ install: z.boolean().default(true),
445
+ /** Pinned upstream agent-sandbox release used to build default manifest URLs. */
446
+ 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),
447
+ /**
448
+ * Optional manifest URLs. Defaults to the upstream release manifest.yaml and extensions.yaml.
449
+ * Use mirrored URLs for restricted networks.
450
+ */
451
+ manifestUrls: z.array(z.string().url()).min(1).optional(),
452
+ /** Optional controller image override, useful for domestic/private registries. */
453
+ controllerImage: ImageReferenceSchema.optional(),
454
+ /** RuntimeClass injected into generated Cloud SaaS sandbox configs. */
455
+ runtimeClassName: RuntimeClassNameSchema.default(AGENT_SANDBOX_DEFAULT_RUNTIME_CLASS),
456
+ /** Create RuntimeClass automatically. Disable when the cluster already provides gvisor/runsc. */
457
+ createRuntimeClass: z.boolean().default(true),
458
+ /** RuntimeClass handler used when createRuntimeClass is true. */
459
+ runtimeClassHandler: z.string().min(1).regex(/^\S+$/, "runtimeClassHandler must not contain whitespace").default(AGENT_SANDBOX_DEFAULT_RUNTIME_HANDLER),
460
+ /** Wait timeout for CRDs/controller readiness. */
461
+ waitTimeoutSeconds: z.number().int().min(30).max(1200).default(300),
462
+ /** Fail cluster init/apply when sandbox cannot be verified. */
463
+ required: z.boolean().default(true),
464
+ /** Label selector injected into sandbox workloads unless templates override it. */
465
+ nodeSelector: KubernetesLabelMapSchema.default({ "shadowob.com/sandbox-ready": "true" }),
466
+ /** Run a real SandboxTemplate/SandboxClaim smoke after install/verify. */
467
+ smokeTest: z.boolean().default(false),
468
+ /** Image used by the optional smoke test. Mirror this for restricted networks. */
469
+ smokeImage: ImageReferenceSchema.default("busybox:1.36")
470
+ }).refine((sandbox) => !sandbox.createRuntimeClass || Boolean(sandbox.runtimeClassHandler), {
471
+ path: ["runtimeClassHandler"],
472
+ message: "runtimeClassHandler is required when createRuntimeClass is true"
473
+ });
474
+ var ClusterSandboxFeatureSchema = z.union([z.boolean(), ClusterSandboxFeatureConfigSchema]);
475
+ var ClusterFeaturesSchema = z.object({
476
+ /** agent-sandbox CRDs/controller/runtime-class management. */
477
+ sandbox: ClusterSandboxFeatureSchema.optional()
478
+ });
479
+ var NodeRoleSchema = z.enum(["master", "worker"]);
480
+ var NodeConfigSchema = z.object({
481
+ /** Node role in the cluster */
482
+ role: NodeRoleSchema,
483
+ /** Public IP or hostname */
484
+ host: z.string().min(1),
485
+ /** SSH port (default: 22) */
486
+ port: z.number().int().min(1).max(65535).default(22),
487
+ /** SSH username */
488
+ user: z.string().min(1),
489
+ /** Path to SSH private key (supports ~) — mutually inclusive with or exclusive of password */
490
+ sshKeyPath: z.string().optional(),
491
+ /** SSH private key passphrase — use ${env:VAR} to avoid storing plaintext */
492
+ sshKeyPassphrase: z.string().optional(),
493
+ /**
494
+ * SSH agent socket. Use true for SSH_AUTH_SOCK, or a socket path/template.
495
+ * Useful for encrypted private keys already loaded into an agent.
496
+ */
497
+ sshAgent: z.union([z.boolean(), z.string().min(1)]).optional(),
498
+ /** SSH password — use ${env:VAR} to avoid storing plaintext */
499
+ password: z.string().optional(),
500
+ /** Optional per-node k3s installer overrides for mixed-region clusters. */
501
+ install: ClusterInstallConfigSchema.optional(),
502
+ /** Region label applied during cluster init/apply, e.g. cn or us. */
503
+ region: z.string().min(1).max(63).regex(K8S_LABEL_VALUE_RE).optional(),
504
+ /** Extra Kubernetes node labels applied during cluster init/apply. */
505
+ labels: KubernetesLabelMapSchema.optional(),
506
+ /** Per-node feature flags used for mixed-capability clusters. */
507
+ features: z.object({
508
+ sandbox: z.boolean().optional()
509
+ }).optional()
510
+ }).refine((n) => n.sshKeyPath !== void 0 || n.password !== void 0 || n.sshAgent, {
511
+ message: "Each node must have either sshKeyPath, password, or sshAgent"
365
512
  });
366
513
  var ClusterProviderSchema = z.enum(["ssh"]);
367
514
  var ClusterConfigSchema = z.object({
@@ -373,7 +520,9 @@ var ClusterConfigSchema = z.object({
373
520
  /** List of nodes */
374
521
  nodes: z.array(NodeConfigSchema).min(1),
375
522
  /** Optional k3s installer settings for restricted networks or pinned versions. */
376
- install: ClusterInstallConfigSchema.optional()
523
+ install: ClusterInstallConfigSchema.optional(),
524
+ /** Optional cluster capabilities managed by cluster init/apply. */
525
+ features: ClusterFeaturesSchema.optional()
377
526
  }).refine((c) => c.nodes.filter((n) => n.role === "master").length === 1, {
378
527
  message: "Cluster must have exactly one master node"
379
528
  });
@@ -409,14 +558,57 @@ ${issues.join("\n")}`);
409
558
  return result.data;
410
559
  }
411
560
  function resolveNodeCredentials(node) {
561
+ let sshAgent;
562
+ if (node.sshAgent === true) {
563
+ sshAgent = process.env.SSH_AUTH_SOCK;
564
+ if (!sshAgent) {
565
+ throw new Error("SSH_AUTH_SOCK is not set (required by cluster.json sshAgent=true)");
566
+ }
567
+ } else if (typeof node.sshAgent === "string") {
568
+ sshAgent = resolveEnvTemplate(node.sshAgent);
569
+ }
412
570
  return {
413
571
  host: node.host,
414
572
  port: node.port,
415
573
  user: node.user,
416
574
  sshKeyPath: node.sshKeyPath ? expandHome(node.sshKeyPath) : void 0,
575
+ sshKeyPassphrase: node.sshKeyPassphrase ? resolveEnvTemplate(node.sshKeyPassphrase) : void 0,
576
+ sshAgent,
417
577
  password: node.password ? resolveEnvTemplate(node.password) : void 0
418
578
  };
419
579
  }
580
+ function resolveNodeInstallConfig(clusterInstall, node) {
581
+ const merged = { ...clusterInstall, ...node.install };
582
+ return Object.keys(merged).length > 0 ? merged : void 0;
583
+ }
584
+ function defaultAgentSandboxManifestUrls(version = AGENT_SANDBOX_DEFAULT_VERSION) {
585
+ return [
586
+ `https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${version}/manifest.yaml`,
587
+ `https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${version}/extensions.yaml`
588
+ ];
589
+ }
590
+ function resolveClusterSandboxConfig(config) {
591
+ const sandbox = config.features?.sandbox;
592
+ if (sandbox === void 0 || sandbox === false) return null;
593
+ const normalized = sandbox === true ? {
594
+ enabled: true,
595
+ install: true,
596
+ version: AGENT_SANDBOX_DEFAULT_VERSION,
597
+ runtimeClassName: AGENT_SANDBOX_DEFAULT_RUNTIME_CLASS,
598
+ createRuntimeClass: true,
599
+ runtimeClassHandler: AGENT_SANDBOX_DEFAULT_RUNTIME_HANDLER,
600
+ waitTimeoutSeconds: 300,
601
+ required: true,
602
+ nodeSelector: { "shadowob.com/sandbox-ready": "true" },
603
+ smokeTest: false,
604
+ smokeImage: "busybox:1.36"
605
+ } : sandbox;
606
+ if (!normalized.enabled) return null;
607
+ return {
608
+ ...normalized,
609
+ manifestUrls: normalized.manifestUrls && normalized.manifestUrls.length > 0 ? normalized.manifestUrls : defaultAgentSandboxManifestUrls(normalized.version)
610
+ };
611
+ }
420
612
  function getMasterNode(config) {
421
613
  const master = config.nodes.find((n) => n.role === "master");
422
614
  if (!master) throw new Error("No master node found in cluster config");
@@ -429,21 +621,35 @@ function getWorkerNodes(config) {
429
621
  // src/interfaces/cli/cluster.command.ts
430
622
  function createClusterCommand(container2) {
431
623
  const cluster = new Command2("cluster").description(
432
- "Manage bare-server k3s clusters (init, status, list, destroy)"
624
+ "Manage bare-server k3s clusters (init, apply, status, list, destroy)"
433
625
  );
434
- 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) => {
626
+ async function applyClusterConfig(options) {
627
+ const config = readClusterConfig(options.config);
628
+ container2.logger.info(`Applying cluster "${config.name}" from ${options.config}...`);
629
+ const meta = await container2.cluster.init(
630
+ config,
631
+ (msg) => {
632
+ container2.logger.dim(msg);
633
+ },
634
+ options.force
635
+ );
636
+ container2.logger.success(`Cluster "${meta.name}" ready! Kubeconfig: ${meta.kubeconfigPath}`);
637
+ container2.logger.info(
638
+ `Edit ${options.config} and run this command again to add newly listed nodes.`
639
+ );
640
+ container2.logger.info(`Deploy agents: shadowob-cloud up --cluster ${meta.name}`);
641
+ }
642
+ 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) => {
435
643
  try {
436
- const config = readClusterConfig(options.config);
437
- container2.logger.info(`Initializing cluster "${config.name}"...`);
438
- const meta = await container2.cluster.init(
439
- config,
440
- (msg) => {
441
- container2.logger.dim(msg);
442
- },
443
- options.force
444
- );
445
- container2.logger.success(`Cluster "${meta.name}" ready! Kubeconfig: ${meta.kubeconfigPath}`);
446
- container2.logger.info(`Deploy agents: shadowob-cloud up --cluster ${meta.name}`);
644
+ await applyClusterConfig(options);
645
+ } catch (err) {
646
+ container2.logger.error(err.message);
647
+ process.exit(1);
648
+ }
649
+ });
650
+ 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) => {
651
+ try {
652
+ await applyClusterConfig(options);
447
653
  } catch (err) {
448
654
  container2.logger.error(err.message);
449
655
  process.exit(1);
@@ -973,7 +1179,7 @@ function createGenerateCommand(container2) {
973
1179
  container2.logger.error(`Config file not found: ${filePath}`);
974
1180
  process.exit(1);
975
1181
  }
976
- const { loadAllPlugins, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-I4GD5SZX.js");
1182
+ const { loadAllPlugins, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-UK2QWD6G.js");
977
1183
  await loadAllPlugins(getPluginRegistry2());
978
1184
  const outputPath = options.output ? resolve7(options.output) : void 0;
979
1185
  const configCwd = dirname(filePath);
@@ -1381,7 +1587,7 @@ function createOnboardCommand(container2) {
1381
1587
  } else {
1382
1588
  const answer = await ask(` No ${configPath} found. Create one from template? [Y/n] `);
1383
1589
  if (!answer || answer.toLowerCase() !== "n") {
1384
- const { createInitCommand: createInitCommand2 } = await import("./init.command-YVG4X6II.js");
1590
+ const { createInitCommand: createInitCommand2 } = await import("./init.command-C7UKPK2Y.js");
1385
1591
  const init = createInitCommand2(container2);
1386
1592
  await init.parseAsync(["--quick"], { from: "user" });
1387
1593
  } else {
@@ -1398,7 +1604,7 @@ function createOnboardCommand(container2) {
1398
1604
  }
1399
1605
  container2.logger.success("Onboarding complete! Starting console...");
1400
1606
  console.log();
1401
- const { createConsoleCommand: createConsoleCommand2 } = await import("./dashboard.command-ZMQFKLNQ.js");
1607
+ const { createConsoleCommand: createConsoleCommand2 } = await import("./dashboard.command-BRPZCZER.js");
1402
1608
  const console_ = createConsoleCommand2(container2);
1403
1609
  await console_.parseAsync([], { from: "user" });
1404
1610
  });
@@ -1436,14 +1642,14 @@ function createProvisionCommand(container2) {
1436
1642
  process.exit(1);
1437
1643
  }
1438
1644
  try {
1439
- const { executePluginProvisions, loadAllPlugins, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-I4GD5SZX.js");
1645
+ const { executePluginProvisions, loadAllPlugins, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-UK2QWD6G.js");
1440
1646
  try {
1441
1647
  await loadAllPlugins(getPluginRegistry2());
1442
1648
  } catch {
1443
1649
  }
1444
1650
  const agents = resolved.deployments?.agents ?? [];
1445
1651
  const namespace = resolved.deployments?.namespace ?? "shadowob-cloud";
1446
- const existing = loadProvisionState(filePath, options.stateDir);
1652
+ const existing = options.force ? null : loadProvisionState(filePath, options.stateDir);
1447
1653
  const extraSecrets = {
1448
1654
  SHADOW_SERVER_URL: shadowUrl,
1449
1655
  SHADOW_USER_TOKEN: shadowToken
@@ -2150,7 +2356,7 @@ var SSHClient = class {
2150
2356
  host: opts.host,
2151
2357
  port: opts.port,
2152
2358
  username: opts.user,
2153
- ...opts.sshKeyPath ? { privateKey: readFileSync5(opts.sshKeyPath, "utf8") } : { password: opts.password },
2359
+ ...opts.sshAgent ? { agent: opts.sshAgent } : opts.sshKeyPath ? { privateKey: readFileSync5(opts.sshKeyPath, "utf8"), passphrase: opts.sshKeyPassphrase } : { password: opts.password },
2154
2360
  // Reasonable timeout for WAN SSH
2155
2361
  readyTimeout: 3e4
2156
2362
  });
@@ -2236,9 +2442,14 @@ async function destroyCluster(options) {
2236
2442
  }
2237
2443
 
2238
2444
  // src/cluster/init.ts
2239
- var K3S_INSTALL_URL = "https://get.k3s.io";
2240
- var CN_PAUSE_IMAGE = "registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6";
2241
- var CN_SYSTEM_DEFAULT_REGISTRY = "registry.cn-hangzhou.aliyuncs.com";
2445
+ import { createHash } from "crypto";
2446
+
2447
+ // src/cluster/sandbox.ts
2448
+ var REQUIRED_AGENT_SANDBOX_CRDS = [
2449
+ "sandboxes.agents.x-k8s.io",
2450
+ "sandboxtemplates.extensions.agents.x-k8s.io",
2451
+ "sandboxclaims.extensions.agents.x-k8s.io"
2452
+ ];
2242
2453
  function log2(onLog, msg) {
2243
2454
  onLog?.(msg);
2244
2455
  }
@@ -2249,6 +2460,291 @@ function asRoot2(shellCommand) {
2249
2460
  const quoted = shellQuote2(shellCommand);
2250
2461
  return `if [ "$(id -u)" -eq 0 ]; then sh -c ${quoted}; else sudo -n sh -c ${quoted}; fi`;
2251
2462
  }
2463
+ async function hasAgentSandboxCrds(client) {
2464
+ const result = await client.exec(
2465
+ asRoot2(
2466
+ `k3s kubectl get crd ${REQUIRED_AGENT_SANDBOX_CRDS.map(shellQuote2).join(" ")} >/dev/null`
2467
+ )
2468
+ );
2469
+ return result.code === 0;
2470
+ }
2471
+ async function applyManifestUrl(client, url, onLog) {
2472
+ log2(onLog, `[sandbox] Applying ${url}`);
2473
+ await client.execOrThrow(asRoot2(`k3s kubectl apply -f ${shellQuote2(url)}`), {
2474
+ onStdout: (chunk) => log2(onLog, `[sandbox] ${chunk.trimEnd()}`),
2475
+ onStderr: (chunk) => log2(onLog, `[sandbox] ${chunk.trimEnd()}`),
2476
+ errorMessage: `Failed to apply agent-sandbox manifest ${url}. If this node cannot reach GitHub, set features.sandbox.manifestUrls to mirrored URLs.`
2477
+ });
2478
+ }
2479
+ async function createOrVerifyRuntimeClass(client, options) {
2480
+ if (options.create) {
2481
+ const yaml = [
2482
+ "apiVersion: node.k8s.io/v1",
2483
+ "kind: RuntimeClass",
2484
+ "metadata:",
2485
+ ` name: ${options.name}`,
2486
+ `handler: ${options.handler}`,
2487
+ ""
2488
+ ].join("\n");
2489
+ log2(
2490
+ options.onLog,
2491
+ `[sandbox] Ensuring RuntimeClass ${options.name} uses handler ${options.handler}`
2492
+ );
2493
+ await client.execOrThrow(asRoot2(`cat <<'EOF' | k3s kubectl apply -f -
2494
+ ${yaml}EOF`), {
2495
+ errorMessage: `Failed to create RuntimeClass ${options.name}`
2496
+ });
2497
+ return;
2498
+ }
2499
+ log2(options.onLog, `[sandbox] Verifying RuntimeClass ${options.name}`);
2500
+ await client.execOrThrow(asRoot2(`k3s kubectl get runtimeclass ${shellQuote2(options.name)}`), {
2501
+ errorMessage: `RuntimeClass ${options.name} was not found. Install the runtime handler first, or set features.sandbox.createRuntimeClass=true with an explicit runtimeClassHandler.`
2502
+ });
2503
+ }
2504
+ async function waitForAgentSandboxReady(client, timeoutSeconds, onLog) {
2505
+ const crdNames = REQUIRED_AGENT_SANDBOX_CRDS.map((name) => `crd/${name}`).join(" ");
2506
+ log2(onLog, "[sandbox] Waiting for agent-sandbox CRDs to become Established");
2507
+ await client.execOrThrow(
2508
+ asRoot2(`k3s kubectl wait --for=condition=Established ${crdNames} --timeout=${timeoutSeconds}s`),
2509
+ { errorMessage: "agent-sandbox CRDs did not become Established" }
2510
+ );
2511
+ log2(onLog, "[sandbox] Waiting for agent-sandbox controller rollout");
2512
+ await client.execOrThrow(
2513
+ asRoot2(
2514
+ `k3s kubectl -n agent-sandbox-system rollout status deployment/agent-sandbox-controller --timeout=${timeoutSeconds}s`
2515
+ ),
2516
+ { errorMessage: "agent-sandbox controller did not become Ready" }
2517
+ );
2518
+ }
2519
+ function nodeLabelArgs(labels) {
2520
+ return Object.entries(labels).map(([key, value]) => `${key}=${value}`).map(shellQuote2).join(" ");
2521
+ }
2522
+ function kubeNodeMatchesConfigNode(kubeNode, node) {
2523
+ const metadata = kubeNode.metadata ?? {};
2524
+ const status = kubeNode.status ?? {};
2525
+ const addresses = Array.isArray(status.addresses) ? status.addresses : [];
2526
+ const candidates = /* @__PURE__ */ new Set([
2527
+ typeof metadata.name === "string" ? metadata.name : "",
2528
+ ...addresses.map((address) => address.address).filter((address) => typeof address === "string")
2529
+ ]);
2530
+ return candidates.has(node.host);
2531
+ }
2532
+ async function labelConfiguredNodes(client, config, sandboxReady, onLog) {
2533
+ const output2 = await client.execOrThrow(asRoot2("k3s kubectl get nodes -o json"), {
2534
+ errorMessage: "Failed to list Kubernetes nodes for labeling"
2535
+ });
2536
+ const kubeNodes = JSON.parse(output2.stdout).items ?? [];
2537
+ for (const node of config.nodes) {
2538
+ const kubeNode = kubeNodes.find((candidate) => kubeNodeMatchesConfigNode(candidate, node));
2539
+ if (!kubeNode) {
2540
+ log2(onLog, `[sandbox] Could not match cluster.json node ${node.host} to a Kubernetes node`);
2541
+ continue;
2542
+ }
2543
+ const metadata = kubeNode.metadata;
2544
+ const nodeName = metadata.name;
2545
+ if (typeof nodeName !== "string" || !nodeName) continue;
2546
+ const nodeSandboxReady = sandboxReady && (node.features?.sandbox ?? true);
2547
+ const labels = {
2548
+ "shadowob.com/sandbox-ready": nodeSandboxReady ? "true" : "false",
2549
+ ...node.region ? { "shadowob.com/region": node.region } : {},
2550
+ ...node.labels ?? {}
2551
+ };
2552
+ log2(onLog, `[sandbox] Labeling node ${nodeName}`);
2553
+ await client.execOrThrow(
2554
+ asRoot2(`k3s kubectl label node ${shellQuote2(nodeName)} ${nodeLabelArgs(labels)} --overwrite`),
2555
+ { errorMessage: `Failed to label Kubernetes node ${nodeName}` }
2556
+ );
2557
+ }
2558
+ }
2559
+ async function runAgentSandboxSmoke(client, options) {
2560
+ const namespace = `shadow-sandbox-smoke-${Date.now().toString(36)}`;
2561
+ const manifest = [
2562
+ {
2563
+ apiVersion: "v1",
2564
+ kind: "Namespace",
2565
+ metadata: { name: namespace }
2566
+ },
2567
+ {
2568
+ apiVersion: "extensions.agents.x-k8s.io/v1alpha1",
2569
+ kind: "SandboxTemplate",
2570
+ metadata: { name: "smoke-template", namespace },
2571
+ spec: {
2572
+ networkPolicyManagement: "Unmanaged",
2573
+ envVarsInjectionPolicy: "Disallowed",
2574
+ podTemplate: {
2575
+ metadata: { labels: { app: "shadow-sandbox-smoke" } },
2576
+ spec: {
2577
+ runtimeClassName: options.runtimeClassName,
2578
+ containers: [
2579
+ {
2580
+ name: "smoke",
2581
+ image: options.image,
2582
+ command: ["sh", "-c", "echo shadow-sandbox-smoke && sleep 30"]
2583
+ }
2584
+ ],
2585
+ restartPolicy: "Always"
2586
+ }
2587
+ },
2588
+ volumeClaimTemplates: []
2589
+ }
2590
+ },
2591
+ {
2592
+ apiVersion: "extensions.agents.x-k8s.io/v1alpha1",
2593
+ kind: "SandboxClaim",
2594
+ metadata: { name: "smoke", namespace },
2595
+ spec: {
2596
+ sandboxTemplateRef: { name: "smoke-template" },
2597
+ warmpool: "none",
2598
+ lifecycle: { shutdownPolicy: "Delete" }
2599
+ }
2600
+ }
2601
+ ];
2602
+ const yaml = JSON.stringify({ apiVersion: "v1", kind: "List", items: manifest }, null, 2);
2603
+ log2(options.onLog, `[sandbox] Running smoke test in namespace ${namespace}`);
2604
+ try {
2605
+ await client.execOrThrow(asRoot2(`cat <<'EOF' | k3s kubectl apply -f -
2606
+ ${yaml}
2607
+ EOF`), {
2608
+ errorMessage: "Failed to create agent-sandbox smoke resources"
2609
+ });
2610
+ await client.execOrThrow(
2611
+ asRoot2(
2612
+ `timeout ${options.timeoutSeconds} sh -c ` + shellQuote2(
2613
+ 'until [ "$(k3s kubectl -n ' + namespace + ' get sandboxclaim smoke -o jsonpath="{.status.conditions[?(@.type==\\"Ready\\")].status}" 2>/dev/null)" = "True" ]; do sleep 3; done'
2614
+ )
2615
+ ),
2616
+ { errorMessage: "agent-sandbox smoke SandboxClaim did not become Ready" }
2617
+ );
2618
+ log2(options.onLog, "[sandbox] Smoke test passed");
2619
+ } finally {
2620
+ await client.exec(
2621
+ asRoot2(`k3s kubectl delete namespace ${shellQuote2(namespace)} --ignore-not-found=true`)
2622
+ );
2623
+ }
2624
+ }
2625
+ async function installClusterSandbox(options) {
2626
+ const sandbox = resolveClusterSandboxConfig(options.config);
2627
+ if (!sandbox) return false;
2628
+ const master = getMasterNode(options.config);
2629
+ const creds = resolveNodeCredentials(master);
2630
+ const client = new SSHClient();
2631
+ log2(options.onLog, `[sandbox ${creds.host}] Connecting via SSH...`);
2632
+ await client.connect(creds);
2633
+ try {
2634
+ const alreadyInstalled = await hasAgentSandboxCrds(client);
2635
+ if (!sandbox.install && !alreadyInstalled) {
2636
+ 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.";
2637
+ if (sandbox.required) throw new Error(message);
2638
+ log2(options.onLog, `[sandbox] ${message}`);
2639
+ return false;
2640
+ }
2641
+ if (sandbox.install) {
2642
+ log2(options.onLog, `[sandbox] Installing agent-sandbox ${sandbox.version}`);
2643
+ for (const url of sandbox.manifestUrls) {
2644
+ await applyManifestUrl(client, url, options.onLog);
2645
+ }
2646
+ if (sandbox.controllerImage) {
2647
+ log2(options.onLog, `[sandbox] Setting controller image ${sandbox.controllerImage}`);
2648
+ await client.execOrThrow(
2649
+ asRoot2(
2650
+ `k3s kubectl -n agent-sandbox-system set image deployment/agent-sandbox-controller agent-sandbox-controller=${shellQuote2(sandbox.controllerImage)}`
2651
+ ),
2652
+ { errorMessage: "Failed to set agent-sandbox controller image" }
2653
+ );
2654
+ }
2655
+ }
2656
+ await createOrVerifyRuntimeClass(client, {
2657
+ name: sandbox.runtimeClassName,
2658
+ handler: sandbox.runtimeClassHandler,
2659
+ create: sandbox.createRuntimeClass,
2660
+ onLog: options.onLog
2661
+ });
2662
+ await waitForAgentSandboxReady(client, sandbox.waitTimeoutSeconds, options.onLog);
2663
+ await labelConfiguredNodes(client, options.config, true, options.onLog);
2664
+ if (sandbox.smokeTest) {
2665
+ await runAgentSandboxSmoke(client, {
2666
+ runtimeClassName: sandbox.runtimeClassName,
2667
+ image: sandbox.smokeImage,
2668
+ timeoutSeconds: sandbox.waitTimeoutSeconds,
2669
+ onLog: options.onLog
2670
+ });
2671
+ }
2672
+ log2(
2673
+ options.onLog,
2674
+ `[sandbox] Ready with RuntimeClass ${sandbox.runtimeClassName}; new deployments can use agent-sandbox`
2675
+ );
2676
+ return true;
2677
+ } catch (err) {
2678
+ if (sandbox.required) throw err;
2679
+ log2(
2680
+ options.onLog,
2681
+ `[sandbox] Not ready; deployment fallback remains available: ${err.message}`
2682
+ );
2683
+ return false;
2684
+ } finally {
2685
+ await client.dispose();
2686
+ }
2687
+ }
2688
+
2689
+ // src/cluster/init.ts
2690
+ var K3S_INSTALL_URL = "https://get.k3s.io";
2691
+ var CN_PAUSE_IMAGE = "registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6";
2692
+ var CN_SYSTEM_DEFAULT_REGISTRY = "registry.cn-hangzhou.aliyuncs.com";
2693
+ function log3(onLog, msg) {
2694
+ onLog?.(msg);
2695
+ }
2696
+ function shellQuote3(value) {
2697
+ return `'${value.replace(/'/g, "'\\''")}'`;
2698
+ }
2699
+ function asRoot3(shellCommand) {
2700
+ const quoted = shellQuote3(shellCommand);
2701
+ return `if [ "$(id -u)" -eq 0 ]; then sh -c ${quoted}; else sudo -n sh -c ${quoted}; fi`;
2702
+ }
2703
+ function resolveEnvTemplates(value) {
2704
+ if (typeof value === "string") {
2705
+ return value.replace(/\$\{env:([^}]+)\}/g, (_match, envKey) => {
2706
+ const envVal = process.env[envKey];
2707
+ if (envVal === void 0) {
2708
+ throw new Error(`Environment variable "${envKey}" is not set (required by cluster.json)`);
2709
+ }
2710
+ return envVal;
2711
+ });
2712
+ }
2713
+ if (Array.isArray(value)) return value.map(resolveEnvTemplates);
2714
+ if (value && typeof value === "object") {
2715
+ return Object.fromEntries(
2716
+ Object.entries(value).map(([key, child]) => [key, resolveEnvTemplates(child)])
2717
+ );
2718
+ }
2719
+ return value;
2720
+ }
2721
+ function clusterConfigHash(config) {
2722
+ return createHash("sha256").update(stableStringify(config)).digest("hex");
2723
+ }
2724
+ function stableStringify(value) {
2725
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
2726
+ if (value && typeof value === "object") {
2727
+ return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => `${JSON.stringify(key)}:${stableStringify(child)}`).join(",")}}`;
2728
+ }
2729
+ return JSON.stringify(value);
2730
+ }
2731
+ async function ensureK3sRegistriesConfig(client, install2, serviceName, onLog) {
2732
+ if (!install2?.registries) return;
2733
+ const registries = JSON.stringify(resolveEnvTemplates(install2.registries), null, 2);
2734
+ log3(onLog, `[${serviceName}] Writing k3s containerd registries.yaml`);
2735
+ await client.execOrThrow(
2736
+ asRoot3(
2737
+ [
2738
+ "mkdir -p /etc/rancher/k3s",
2739
+ `cat > /etc/rancher/k3s/registries.yaml <<'EOF'
2740
+ ${registries}
2741
+ EOF`,
2742
+ `if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet ${serviceName}; then systemctl restart ${serviceName}; fi`
2743
+ ].join("\n")
2744
+ ),
2745
+ { errorMessage: `Failed to write k3s registries.yaml for ${serviceName}` }
2746
+ );
2747
+ }
2252
2748
  function resolveK3sMirror(install2) {
2253
2749
  return process.env.INSTALL_K3S_MIRROR ?? process.env.K3S_INSTALL_MIRROR ?? install2?.k3sMirror;
2254
2750
  }
@@ -2317,7 +2813,7 @@ function k3sInstallEnv(extra, install2) {
2317
2813
  INSTALL_K3S_VERSION: version,
2318
2814
  ...extra
2319
2815
  };
2320
- return Object.entries(env).filter((entry) => Boolean(entry[1])).map(([key, value]) => `${key}=${shellQuote2(value)}`).join(" ");
2816
+ return Object.entries(env).filter((entry) => Boolean(entry[1])).map(([key, value]) => `${key}=${shellQuote3(value)}`).join(" ");
2321
2817
  }
2322
2818
  async function isK3sInstalled(client) {
2323
2819
  const result = await client.exec("which k3s");
@@ -2326,25 +2822,26 @@ async function isK3sInstalled(client) {
2326
2822
  async function installMaster(master, install2, force, onLog) {
2327
2823
  const creds = resolveNodeCredentials(master);
2328
2824
  const client = new SSHClient();
2329
- log2(onLog, `[master ${creds.host}] Connecting via SSH...`);
2825
+ log3(onLog, `[master ${creds.host}] Connecting via SSH...`);
2330
2826
  await client.connect(creds);
2331
2827
  try {
2828
+ await ensureK3sRegistriesConfig(client, install2, "k3s", onLog);
2332
2829
  const alreadyInstalled = await isK3sInstalled(client);
2333
2830
  if (alreadyInstalled && !force) {
2334
- log2(
2831
+ log3(
2335
2832
  onLog,
2336
2833
  `[master ${creds.host}] k3s already installed \u2014 skipping install (use --force to reinstall)`
2337
2834
  );
2338
- log2(onLog, `[master ${creds.host}] Reading existing token and kubeconfig...`);
2835
+ log3(onLog, `[master ${creds.host}] Reading existing token and kubeconfig...`);
2339
2836
  } else {
2340
2837
  if (alreadyInstalled && force) {
2341
- log2(onLog, `[master ${creds.host}] k3s already installed \u2014 reinstalling (--force)`);
2342
- await client.exec(asRoot2("/usr/local/bin/k3s-uninstall.sh 2>/dev/null || true"));
2838
+ log3(onLog, `[master ${creds.host}] k3s already installed \u2014 reinstalling (--force)`);
2839
+ await client.exec(asRoot3("/usr/local/bin/k3s-uninstall.sh 2>/dev/null || true"));
2343
2840
  }
2344
- log2(onLog, `[master ${creds.host}] Installing k3s server...`);
2841
+ log3(onLog, `[master ${creds.host}] Installing k3s server...`);
2345
2842
  await client.execOrThrow(
2346
- asRoot2(
2347
- `curl -sfL ${shellQuote2(K3S_INSTALL_URL)} | ${k3sInstallEnv(
2843
+ asRoot3(
2844
+ `curl -sfL ${shellQuote3(K3S_INSTALL_URL)} | ${k3sInstallEnv(
2348
2845
  {
2349
2846
  INSTALL_K3S_EXEC: k3sExec(["server", "--tls-san", creds.host], install2)
2350
2847
  },
@@ -2352,28 +2849,28 @@ async function installMaster(master, install2, force, onLog) {
2352
2849
  )} sh -`
2353
2850
  ),
2354
2851
  {
2355
- onStdout: (c) => log2(onLog, `[master] ${c.trimEnd()}`),
2356
- onStderr: (c) => log2(onLog, `[master] ${c.trimEnd()}`),
2852
+ onStdout: (c) => log3(onLog, `[master] ${c.trimEnd()}`),
2853
+ onStderr: (c) => log3(onLog, `[master] ${c.trimEnd()}`),
2357
2854
  errorMessage: `Failed to install k3s on master ${creds.host}`
2358
2855
  }
2359
2856
  );
2360
2857
  }
2361
- log2(onLog, `[master ${creds.host}] Waiting for k3s to be ready...`);
2858
+ log3(onLog, `[master ${creds.host}] Waiting for k3s to be ready...`);
2362
2859
  await client.execOrThrow(
2363
- asRoot2(`timeout 120 sh -c 'until k3s kubectl get nodes > /dev/null 2>&1; do sleep 3; done'`),
2860
+ asRoot3(`timeout 120 sh -c 'until k3s kubectl get nodes > /dev/null 2>&1; do sleep 3; done'`),
2364
2861
  { errorMessage: "k3s master did not become ready within 120s" }
2365
2862
  );
2366
- log2(onLog, `[master ${creds.host}] Reading node token...`);
2863
+ log3(onLog, `[master ${creds.host}] Reading node token...`);
2367
2864
  const tokenResult = await client.execOrThrow(
2368
- asRoot2("cat /var/lib/rancher/k3s/server/node-token"),
2865
+ asRoot3("cat /var/lib/rancher/k3s/server/node-token"),
2369
2866
  {
2370
2867
  errorMessage: "Failed to read k3s node token"
2371
2868
  }
2372
2869
  );
2373
2870
  const token = tokenResult.stdout.trim();
2374
2871
  if (!token) throw new Error("k3s node token is empty");
2375
- log2(onLog, `[master ${creds.host}] Reading kubeconfig...`);
2376
- const kubeconfigResult = await client.execOrThrow(asRoot2("cat /etc/rancher/k3s/k3s.yaml"), {
2872
+ log3(onLog, `[master ${creds.host}] Reading kubeconfig...`);
2873
+ const kubeconfigResult = await client.execOrThrow(asRoot3("cat /etc/rancher/k3s/k3s.yaml"), {
2377
2874
  errorMessage: "Failed to read k3s kubeconfig"
2378
2875
  });
2379
2876
  return { token, kubeconfig: kubeconfigResult.stdout };
@@ -2384,25 +2881,26 @@ async function installMaster(master, install2, force, onLog) {
2384
2881
  async function installWorker(worker, masterHost, token, install2, force, onLog) {
2385
2882
  const creds = resolveNodeCredentials(worker);
2386
2883
  const client = new SSHClient();
2387
- log2(onLog, `[worker ${creds.host}] Connecting via SSH...`);
2884
+ log3(onLog, `[worker ${creds.host}] Connecting via SSH...`);
2388
2885
  await client.connect(creds);
2389
2886
  try {
2887
+ await ensureK3sRegistriesConfig(client, install2, "k3s-agent", onLog);
2390
2888
  const alreadyInstalled = await isK3sInstalled(client);
2391
2889
  if (alreadyInstalled && !force) {
2392
- log2(
2890
+ log3(
2393
2891
  onLog,
2394
2892
  `[worker ${creds.host}] k3s already installed \u2014 skipping install (use --force to reinstall)`
2395
2893
  );
2396
2894
  return;
2397
2895
  }
2398
2896
  if (alreadyInstalled && force) {
2399
- log2(onLog, `[worker ${creds.host}] k3s already installed \u2014 reinstalling (--force)`);
2400
- await client.exec(asRoot2("/usr/local/bin/k3s-agent-uninstall.sh 2>/dev/null || true"));
2897
+ log3(onLog, `[worker ${creds.host}] k3s already installed \u2014 reinstalling (--force)`);
2898
+ await client.exec(asRoot3("/usr/local/bin/k3s-agent-uninstall.sh 2>/dev/null || true"));
2401
2899
  }
2402
- log2(onLog, `[worker ${creds.host}] Installing k3s agent and joining cluster...`);
2900
+ log3(onLog, `[worker ${creds.host}] Installing k3s agent and joining cluster...`);
2403
2901
  await client.execOrThrow(
2404
- asRoot2(
2405
- `curl -sfL ${shellQuote2(K3S_INSTALL_URL)} | ${k3sInstallEnv(
2902
+ asRoot3(
2903
+ `curl -sfL ${shellQuote3(K3S_INSTALL_URL)} | ${k3sInstallEnv(
2406
2904
  {
2407
2905
  K3S_URL: `https://${masterHost}:6443`,
2408
2906
  K3S_TOKEN: token,
@@ -2412,12 +2910,12 @@ async function installWorker(worker, masterHost, token, install2, force, onLog)
2412
2910
  )} sh -`
2413
2911
  ),
2414
2912
  {
2415
- onStdout: (c) => log2(onLog, `[worker ${creds.host}] ${c.trimEnd()}`),
2416
- onStderr: (c) => log2(onLog, `[worker ${creds.host}] ${c.trimEnd()}`),
2913
+ onStdout: (c) => log3(onLog, `[worker ${creds.host}] ${c.trimEnd()}`),
2914
+ onStderr: (c) => log3(onLog, `[worker ${creds.host}] ${c.trimEnd()}`),
2417
2915
  errorMessage: `Failed to install k3s agent on worker ${creds.host}`
2418
2916
  }
2419
2917
  );
2420
- log2(onLog, `[worker ${creds.host}] Agent joined cluster \u2713`);
2918
+ log3(onLog, `[worker ${creds.host}] Agent joined cluster \u2713`);
2421
2919
  } finally {
2422
2920
  await client.dispose();
2423
2921
  }
@@ -2426,17 +2924,39 @@ async function initCluster(options) {
2426
2924
  const { config, force = false, onLog } = options;
2427
2925
  const master = getMasterNode(config);
2428
2926
  const workers = getWorkerNodes(config);
2429
- log2(onLog, `Initializing cluster "${config.name}" with ${config.nodes.length} nodes...`);
2430
- const { token, kubeconfig } = await installMaster(master, config.install, force, onLog);
2927
+ log3(onLog, `Initializing cluster "${config.name}" with ${config.nodes.length} nodes...`);
2928
+ const masterInstall = resolveNodeInstallConfig(config.install, master);
2929
+ const { token, kubeconfig } = await installMaster(master, masterInstall, force, onLog);
2431
2930
  if (workers.length > 0) {
2432
- log2(onLog, `Installing ${workers.length} worker(s) in parallel...`);
2931
+ log3(onLog, `Installing ${workers.length} worker(s) in parallel...`);
2433
2932
  await Promise.all(
2434
- workers.map((w) => installWorker(w, master.host, token, config.install, force, onLog))
2933
+ workers.map(
2934
+ (worker) => installWorker(
2935
+ worker,
2936
+ master.host,
2937
+ token,
2938
+ resolveNodeInstallConfig(config.install, worker),
2939
+ force,
2940
+ onLog
2941
+ )
2942
+ )
2435
2943
  );
2436
2944
  }
2437
- const meta = storeKubeconfig(config.name, kubeconfig, master.host, config.nodes.length);
2438
- log2(onLog, `Kubeconfig stored at ${meta.kubeconfigPath}`);
2439
- log2(onLog, `Cluster "${config.name}" is ready. Use: shadowob-cloud up --cluster ${config.name}`);
2945
+ const sandboxConfig = resolveClusterSandboxConfig(config);
2946
+ const sandboxEnabled = await installClusterSandbox({ config, onLog });
2947
+ const meta = storeKubeconfig(config.name, kubeconfig, master.host, config.nodes.length, {
2948
+ configHash: clusterConfigHash(config),
2949
+ features: {
2950
+ sandbox: sandboxConfig ? {
2951
+ enabled: sandboxEnabled,
2952
+ version: sandboxConfig.version,
2953
+ runtimeClassName: sandboxConfig.runtimeClassName,
2954
+ nodeSelector: sandboxConfig.nodeSelector
2955
+ } : { enabled: false }
2956
+ }
2957
+ });
2958
+ log3(onLog, `Kubeconfig stored at ${meta.kubeconfigPath}`);
2959
+ log3(onLog, `Cluster "${config.name}" is ready. Use: shadowob-cloud up --cluster ${config.name}`);
2440
2960
  return meta;
2441
2961
  }
2442
2962
 
@@ -2587,8 +3107,68 @@ var ConfigService = class {
2587
3107
  };
2588
3108
 
2589
3109
  // src/services/deploy.service.ts
2590
- import { existsSync as existsSync16, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
3110
+ import { existsSync as existsSync17, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
2591
3111
  import { dirname as dirname3, resolve as resolve16 } from "path";
3112
+
3113
+ // src/utils/kubeconfig-file.ts
3114
+ import { existsSync as existsSync16, readFileSync as readFileSync6, statSync } from "fs";
3115
+ import { homedir as homedir3 } from "os";
3116
+ import { join as join4 } from "path";
3117
+ function defaultKubeconfigPath() {
3118
+ return join4(homedir3(), ".kube", "config");
3119
+ }
3120
+ function kubeconfigSetupHint() {
3121
+ 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.";
3122
+ }
3123
+ function assertReadableKubeconfigFile(kubeconfigPath, label = "Kubernetes kubeconfig") {
3124
+ if (!existsSync16(kubeconfigPath)) {
3125
+ throw new Error(`${label} not found at ${kubeconfigPath}. ${kubeconfigSetupHint()}`);
3126
+ }
3127
+ let stat3;
3128
+ try {
3129
+ stat3 = statSync(kubeconfigPath);
3130
+ } catch (err) {
3131
+ throw new Error(
3132
+ `Failed to inspect ${label} at ${kubeconfigPath}: ${err.message}. ` + kubeconfigSetupHint()
3133
+ );
3134
+ }
3135
+ if (stat3.isDirectory()) {
3136
+ throw new Error(
3137
+ `${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()
3138
+ );
3139
+ }
3140
+ if (!stat3.isFile()) {
3141
+ throw new Error(
3142
+ `${label} path ${kubeconfigPath} is not a regular file. ${kubeconfigSetupHint()}`
3143
+ );
3144
+ }
3145
+ if (stat3.size === 0) {
3146
+ throw new Error(`${label} at ${kubeconfigPath} is empty. ${kubeconfigSetupHint()}`);
3147
+ }
3148
+ }
3149
+ function readKubeconfigFile(kubeconfigPath, label = "Kubernetes kubeconfig") {
3150
+ assertReadableKubeconfigFile(kubeconfigPath, label);
3151
+ try {
3152
+ return readFileSync6(kubeconfigPath, "utf8");
3153
+ } catch (err) {
3154
+ throw new Error(
3155
+ `Failed to read ${label} at ${kubeconfigPath}: ${err.message}. ` + kubeconfigSetupHint()
3156
+ );
3157
+ }
3158
+ }
3159
+ function findReadableKubeconfigPath(candidates, label = "Kubernetes kubeconfig") {
3160
+ const uniqueCandidates = [
3161
+ ...new Set(candidates.filter((candidate) => Boolean(candidate)))
3162
+ ];
3163
+ for (const candidate of uniqueCandidates) {
3164
+ if (!existsSync16(candidate)) continue;
3165
+ assertReadableKubeconfigFile(candidate, label);
3166
+ return candidate;
3167
+ }
3168
+ return void 0;
3169
+ }
3170
+
3171
+ // src/services/deploy.service.ts
2592
3172
  function deploymentReadyTimeoutMs() {
2593
3173
  const raw = Number(process.env.CLOUD_DEPLOYMENT_READY_TIMEOUT_MS);
2594
3174
  return Number.isFinite(raw) && raw > 0 ? raw : 20 * 6e4;
@@ -2596,13 +3176,22 @@ function deploymentReadyTimeoutMs() {
2596
3176
  function isAgentSandboxBackend(config) {
2597
3177
  return (config.deployments?.backend ?? "agent-sandbox") === "agent-sandbox";
2598
3178
  }
3179
+ function workloadBackendPolicy(config) {
3180
+ if (config.deployments?.backendPolicy) return config.deployments.backendPolicy;
3181
+ return isAgentSandboxBackend(config) ? "sandbox-required" : "deployment-only";
3182
+ }
3183
+ function sandboxRuntimeClassNames(config) {
3184
+ const deployments = config.deployments;
3185
+ if (!deployments) return [];
3186
+ const defaultRuntimeClassName = deployments.sandbox?.runtimeClassName ?? "gvisor";
3187
+ const names = deployments.agents.map(
3188
+ (agent) => agent.sandbox?.runtimeClassName ?? defaultRuntimeClassName
3189
+ );
3190
+ return [...new Set(names)];
3191
+ }
2599
3192
  function readKubeconfigForRuntimeWait(kubeConfigPath) {
2600
3193
  if (!kubeConfigPath) return void 0;
2601
- try {
2602
- return readFileSync6(kubeConfigPath, "utf8");
2603
- } catch {
2604
- return void 0;
2605
- }
3194
+ return readKubeconfigFile(kubeConfigPath);
2606
3195
  }
2607
3196
  function resolveStackName(namespace, stack) {
2608
3197
  return stack ?? `dev-${namespace}`;
@@ -2644,7 +3233,7 @@ function configUsesPlugins(value, depth = 0) {
2644
3233
  }
2645
3234
  async function ensureBuiltInPluginsLoaded() {
2646
3235
  try {
2647
- const { loadAllPlugins, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-I4GD5SZX.js");
3236
+ const { loadAllPlugins, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-UK2QWD6G.js");
2648
3237
  const registry = getPluginRegistry2();
2649
3238
  if (registry.size === 0) await loadAllPlugins(registry);
2650
3239
  } catch {
@@ -2653,7 +3242,7 @@ async function ensureBuiltInPluginsLoaded() {
2653
3242
  function readKubeconfigCurrentContext(kubeConfigPath) {
2654
3243
  if (!kubeConfigPath) return void 0;
2655
3244
  try {
2656
- return readFileSync6(kubeConfigPath, "utf8").match(/current-context:\s*(\S+)/)?.[1];
3245
+ return readKubeconfigFile(kubeConfigPath).match(/current-context:\s*(\S+)/)?.[1];
2657
3246
  } catch {
2658
3247
  return void 0;
2659
3248
  }
@@ -2666,6 +3255,39 @@ function summarizeK8sTarget(options, kubeConfigPath) {
2666
3255
  const kubeconfig = kubeConfigPath ?? process.env.KUBECONFIG ?? "~/.kube/config";
2667
3256
  return `Kubernetes target: cluster=${cluster} context=${context} kubeconfig=${kubeconfig}`;
2668
3257
  }
3258
+ function applyManagedClusterDefaults(config, clusterName) {
3259
+ if (!clusterName || !config.deployments) return;
3260
+ const sandbox = loadClusterMeta(clusterName)?.features?.sandbox;
3261
+ if (!config.deployments.backendPolicy) {
3262
+ config.deployments.backendPolicy = sandbox?.enabled ? "sandbox-preferred" : config.deployments.backend === "agent-sandbox" ? "sandbox-required" : "deployment-only";
3263
+ }
3264
+ if (!config.deployments.backend) {
3265
+ config.deployments.backend = config.deployments.backendPolicy === "deployment-only" ? "deployment" : sandbox?.enabled ? "agent-sandbox" : "deployment";
3266
+ }
3267
+ if (config.deployments.backend === "agent-sandbox" && sandbox?.enabled && sandbox.runtimeClassName) {
3268
+ config.deployments.sandbox = {
3269
+ ...config.deployments.sandbox ?? {},
3270
+ runtimeClassName: config.deployments.sandbox?.runtimeClassName ?? sandbox.runtimeClassName
3271
+ };
3272
+ }
3273
+ if (config.deployments.backend === "agent-sandbox" && sandbox?.enabled && sandbox.nodeSelector) {
3274
+ config.deployments.scheduling = {
3275
+ ...config.deployments.scheduling ?? {},
3276
+ nodeSelector: {
3277
+ ...sandbox.nodeSelector,
3278
+ ...config.deployments.scheduling?.nodeSelector ?? {}
3279
+ }
3280
+ };
3281
+ }
3282
+ }
3283
+ function describeSandboxPreflightFailure(missing, warnings) {
3284
+ return [
3285
+ "agent-sandbox preflight failed.",
3286
+ missing.length > 0 ? `Missing: ${missing.join(", ")}.` : "",
3287
+ warnings.length > 0 ? `Warnings: ${warnings.join(", ")}.` : "",
3288
+ 'Run "shadowob-cloud cluster apply --config cluster.json" to install/verify sandbox, or set deployments.backendPolicy="deployment-only" for fallback.'
3289
+ ].filter(Boolean).join(" ");
3290
+ }
2669
3291
  var DeployService = class {
2670
3292
  constructor(configService, manifestService, k8s6, logger) {
2671
3293
  this.configService = configService;
@@ -2685,10 +3307,13 @@ var DeployService = class {
2685
3307
  });
2686
3308
  const runtimeContext = normalizeDeploymentRuntimeContext(options.runtimeContext);
2687
3309
  const effectiveEnv = buildEffectiveEnv(options.runtimeEnvVars, runtimeContext);
2688
- if (!existsSync16(filePath)) {
3310
+ if (!existsSync17(filePath)) {
2689
3311
  throw new Error(`Config file not found: ${filePath}`);
2690
3312
  }
2691
3313
  const kubeConfigPath = options.kubeConfigPath ?? (options.cluster ? loadKubeconfigPath(options.cluster) : void 0);
3314
+ if (kubeConfigPath) {
3315
+ assertReadableKubeconfigFile(kubeConfigPath);
3316
+ }
2692
3317
  const k8sTargetSummary = summarizeK8sTarget(options, kubeConfigPath);
2693
3318
  this.logger.info(k8sTargetSummary);
2694
3319
  emit(`${k8sTargetSummary}
@@ -2701,6 +3326,7 @@ var DeployService = class {
2701
3326
  await this.configService.parseFile(filePath),
2702
3327
  runtimeContext
2703
3328
  );
3329
+ applyManagedClusterDefaults(config, options.cluster);
2704
3330
  const namespace = options.namespace ?? config.deployments?.namespace ?? "shadowob-cloud";
2705
3331
  const stackName = resolveStackName(namespace, options.stack);
2706
3332
  const agents = config.deployments?.agents ?? [];
@@ -2745,10 +3371,31 @@ var DeployService = class {
2745
3371
  const usesPlugins = configUsesPlugins(config);
2746
3372
  if (usesPlugins) await ensureBuiltInPluginsLoaded();
2747
3373
  const resolved = await this.configService.resolve(config, configCwd, { env: effectiveEnv });
3374
+ if (resolved.deployments && isAgentSandboxBackend(resolved)) {
3375
+ const policy = workloadBackendPolicy(resolved);
3376
+ const preflight = this.k8s.checkAgentSandboxPreflight({
3377
+ kubeconfig: readKubeconfigForRuntimeWait(kubeConfigPath),
3378
+ runtimeClassNames: sandboxRuntimeClassNames(resolved)
3379
+ });
3380
+ if (!preflight.ok) {
3381
+ const message = describeSandboxPreflightFailure(preflight.missing, preflight.warnings);
3382
+ if (policy === "sandbox-preferred") {
3383
+ this.logger.warn(`${message} Falling back to Kubernetes Deployment.`);
3384
+ emit(`${message} Falling back to Kubernetes Deployment.
3385
+ `);
3386
+ resolved.deployments.backend = "deployment";
3387
+ resolved.deployments.backendPolicy = "deployment-only";
3388
+ } else {
3389
+ throw new Error(message);
3390
+ }
3391
+ } else if (preflight.warnings.length > 0) {
3392
+ this.logger.warn(`agent-sandbox preflight warnings: ${preflight.warnings.join(", ")}`);
3393
+ }
3394
+ }
2748
3395
  if (usesPlugins) await ensureBuiltInPluginsLoaded();
2749
3396
  if (!options.skipProvision) {
2750
3397
  try {
2751
- const { executePluginProvisions, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-I4GD5SZX.js");
3398
+ const { executePluginProvisions, getPluginRegistry: getPluginRegistry2 } = await import("./plugins-UK2QWD6G.js");
2752
3399
  for (const agent of agents) {
2753
3400
  const provisionResults = await executePluginProvisions(
2754
3401
  agent,
@@ -2763,6 +3410,9 @@ var DeployService = class {
2763
3410
  for (const e of provisionResults.errors) {
2764
3411
  this.logger.warn(`Plugin provision error (${e.pluginId}): ${e.error}`);
2765
3412
  }
3413
+ throw new Error(
3414
+ `Plugin provisioning failed: ${provisionResults.errors.map((e) => `${e.pluginId}: ${e.error}`).join("; ")}`
3415
+ );
2766
3416
  }
2767
3417
  if (Object.keys(provisionResults.secrets).length > 0) {
2768
3418
  agent.env = { ...agent.env ?? {}, ...provisionResults.secrets };
@@ -2786,7 +3436,10 @@ var DeployService = class {
2786
3436
  await options.onProvisionState?.(merged);
2787
3437
  }
2788
3438
  }
2789
- } catch {
3439
+ } catch (err) {
3440
+ const message = err instanceof Error ? err.message : String(err);
3441
+ this.logger.warn(`Plugin provisioning failed; aborting deploy: ${message}`);
3442
+ throw err;
2790
3443
  }
2791
3444
  }
2792
3445
  const k8sShadowUrl = options.k8sShadowUrl ?? effectiveEnv.SHADOW_AGENT_SERVER_URL ?? options.shadowUrl ?? effectiveEnv.K8S_SHADOW_URL ?? effectiveEnv.SHADOW_SERVER_URL;
@@ -2859,11 +3512,11 @@ var DeployService = class {
2859
3512
  emit("Lock canceled, retrying...\n");
2860
3513
  }
2861
3514
  } catch {
2862
- const { join: join7 } = await import("path");
3515
+ const { join: join8 } = await import("path");
2863
3516
  const { homedir: homedir6 } = await import("os");
2864
- const { rmSync: rmSync4, existsSync: existsSync19 } = await import("fs");
2865
- const lockDir = join7(homedir6(), ".shadowob", "pulumi", ".pulumi", "locks");
2866
- if (existsSync19(lockDir)) {
3517
+ const { rmSync: rmSync4, existsSync: existsSync20 } = await import("fs");
3518
+ const lockDir = join8(homedir6(), ".shadowob", "pulumi", ".pulumi", "locks");
3519
+ if (existsSync20(lockDir)) {
2867
3520
  try {
2868
3521
  rmSync4(lockDir, { recursive: true });
2869
3522
  this.logger.info("Lock files removed, retrying...");
@@ -2963,6 +3616,9 @@ var DeployService = class {
2963
3616
  `Cannot destroy namespace "${namespace}" without a Pulumi config snapshot. Destroy must run through the deployment stack state.`
2964
3617
  );
2965
3618
  }
3619
+ if (options.kubeConfigPath) {
3620
+ assertReadableKubeconfigFile(options.kubeConfigPath);
3621
+ }
2966
3622
  const stack = await this.k8s.getOrCreateStack({
2967
3623
  stackName,
2968
3624
  config: options.config,
@@ -2990,10 +3646,10 @@ var DeployService = class {
2990
3646
  };
2991
3647
 
2992
3648
  // src/services/deployment-runtime.service.ts
2993
- import { createHash } from "crypto";
2994
- import { existsSync as existsSync17, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
2995
- import { homedir as homedir3, tmpdir as tmpdir2 } from "os";
2996
- import { delimiter, join as join4 } from "path";
3649
+ import { createHash as createHash2 } from "crypto";
3650
+ import { existsSync as existsSync18, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
3651
+ import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
3652
+ import { delimiter, join as join5 } from "path";
2997
3653
  function extractKubeContext(kubeconfigYaml) {
2998
3654
  const match = kubeconfigYaml.match(/current-context:\s*(\S+)/);
2999
3655
  return match?.[1];
@@ -3030,24 +3686,24 @@ function normalizeRuntimeEnvVars2(envVars) {
3030
3686
  return normalized;
3031
3687
  }
3032
3688
  function getStableRuntimeKubeconfigPath(kubeconfigYaml) {
3033
- const runtimeDir = join4(homedir3(), ".shadowob", "kubeconfigs");
3689
+ const runtimeDir = join5(homedir4(), ".shadowob", "kubeconfigs");
3034
3690
  mkdirSync4(runtimeDir, { recursive: true });
3035
- const hash = createHash("sha256").update(kubeconfigYaml).digest("hex");
3036
- const kubeconfigPath = join4(runtimeDir, `${hash}.yaml`);
3037
- if (!existsSync17(kubeconfigPath)) {
3691
+ const hash = createHash2("sha256").update(kubeconfigYaml).digest("hex");
3692
+ const kubeconfigPath = join5(runtimeDir, `${hash}.yaml`);
3693
+ if (!existsSync18(kubeconfigPath)) {
3038
3694
  writeFileSync5(kubeconfigPath, kubeconfigYaml, { mode: 384 });
3039
3695
  }
3040
3696
  return kubeconfigPath;
3041
3697
  }
3042
3698
  function isContainerizedRuntime() {
3043
- return process.env.SHADOW_CONTAINERIZED === "1" || existsSync17("/.dockerenv");
3699
+ return process.env.SHADOW_CONTAINERIZED === "1" || existsSync18("/.dockerenv");
3044
3700
  }
3045
3701
  function getHostLocalRuntimeKubeconfigPaths() {
3046
3702
  const candidates = [process.env.KUBECONFIG_HOST_PATH?.trim()];
3047
3703
  if (!isContainerizedRuntime()) {
3048
3704
  candidates.push(
3049
3705
  ...process.env.KUBECONFIG?.split(delimiter).map((candidate) => candidate.trim()).filter((candidate) => candidate.length > 0) ?? [],
3050
- join4(homedir3(), ".kube", "config")
3706
+ defaultKubeconfigPath()
3051
3707
  );
3052
3708
  }
3053
3709
  return [...new Set(candidates.filter((candidate) => Boolean(candidate)))];
@@ -3060,17 +3716,17 @@ function resolveAmbientRuntimeKubeconfigPath() {
3060
3716
  const candidates = [
3061
3717
  ...process.env.KUBECONFIG?.split(delimiter).map((candidate) => candidate.trim()).filter((candidate) => candidate.length > 0) ?? [],
3062
3718
  process.env.KUBECONFIG_HOST_PATH?.trim(),
3063
- join4(homedir3(), ".kube", "config")
3719
+ defaultKubeconfigPath()
3064
3720
  ].filter((candidate) => Boolean(candidate));
3065
- return candidates.find((candidate) => existsSync17(candidate));
3721
+ return findReadableKubeconfigPath(candidates, "Cloud SaaS Kubernetes kubeconfig");
3066
3722
  }
3067
3723
  var DeploymentRuntimeService = class {
3068
3724
  constructor(deployService) {
3069
3725
  this.deployService = deployService;
3070
3726
  }
3071
3727
  async deployFromSnapshot(options) {
3072
- const configDir = mkdtempSync2(join4(tmpdir2(), "sc-cfg-"));
3073
- const configPath = join4(configDir, "shadowob-cloud.json");
3728
+ const configDir = mkdtempSync2(join5(tmpdir2(), "sc-cfg-"));
3729
+ const configPath = join5(configDir, "shadowob-cloud.json");
3074
3730
  writeFileSync5(configPath, JSON.stringify(options.configSnapshot, null, 2), "utf-8");
3075
3731
  const {
3076
3732
  configSnapshot: _configSnapshot,
@@ -3123,7 +3779,7 @@ var DeploymentRuntimeService = class {
3123
3779
  let k8sContext;
3124
3780
  let kubeConfigPath;
3125
3781
  const activeKubeconfigPath = resolveAmbientRuntimeKubeconfigPath();
3126
- const activeKubeconfig = cluster?.kubeconfig ? cluster.kubeconfig : activeKubeconfigPath ? readFileSync7(activeKubeconfigPath, "utf8") : void 0;
3782
+ const activeKubeconfig = cluster?.kubeconfig ? cluster.kubeconfig : activeKubeconfigPath ? readKubeconfigFile(activeKubeconfigPath, "Cloud SaaS Kubernetes kubeconfig") : void 0;
3127
3783
  if (activeKubeconfig) {
3128
3784
  const shouldRewriteLoopback = Boolean(
3129
3785
  cluster?.kubeconfig || activeKubeconfigPath && !isHostLocalRuntimeKubeconfigPath(activeKubeconfigPath)
@@ -3454,23 +4110,23 @@ function rolloutUndoAll(namespace) {
3454
4110
 
3455
4111
  // src/clients/kubectl-runtime.ts
3456
4112
  import { execFileSync as execFileSync2, spawn as spawn4, spawnSync as spawnSync2 } from "child_process";
3457
- import { existsSync as existsSync18, mkdtempSync as mkdtempSync3, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
3458
- import { homedir as homedir4, tmpdir as tmpdir3 } from "os";
3459
- import { delimiter as delimiter2, join as join5 } from "path";
4113
+ import { existsSync as existsSync19, mkdtempSync as mkdtempSync3, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
4114
+ import { tmpdir as tmpdir3 } from "os";
4115
+ import { delimiter as delimiter2, join as join6 } from "path";
3460
4116
  function volumeSnapshotApiAvailableFromOutput2(output2) {
3461
4117
  return output2.split(/\s+/).map((item) => item.trim()).some(
3462
4118
  (resource) => resource === "volumesnapshots" || resource === "volumesnapshots.snapshot.storage.k8s.io"
3463
4119
  );
3464
4120
  }
3465
4121
  function isContainerizedRuntime2() {
3466
- return process.env.SHADOW_CONTAINERIZED === "1" || existsSync18("/.dockerenv");
4122
+ return process.env.SHADOW_CONTAINERIZED === "1" || existsSync19("/.dockerenv");
3467
4123
  }
3468
4124
  function getHostLocalKubeconfigPaths() {
3469
4125
  const candidates = [process.env.KUBECONFIG_HOST_PATH?.trim()];
3470
4126
  if (!isContainerizedRuntime2()) {
3471
4127
  candidates.push(
3472
4128
  ...process.env.KUBECONFIG?.split(delimiter2).map((candidate) => candidate.trim()).filter((candidate) => candidate.length > 0) ?? [],
3473
- join5(homedir4(), ".kube", "config")
4129
+ defaultKubeconfigPath()
3474
4130
  );
3475
4131
  }
3476
4132
  return [...new Set(candidates.filter((candidate) => Boolean(candidate)))];
@@ -3484,22 +4140,23 @@ function extractCurrentContext(kubeconfigYaml) {
3484
4140
  }
3485
4141
  function resolveAmbientKubeconfig() {
3486
4142
  const envCandidates = process.env.KUBECONFIG?.split(delimiter2).map((candidate) => candidate.trim()).filter((candidate) => candidate.length > 0) ?? [];
3487
- const kubeconfigPath = [
4143
+ const candidates = [
3488
4144
  ...envCandidates,
3489
4145
  process.env.KUBECONFIG_HOST_PATH?.trim(),
3490
- join5(homedir4(), ".kube", "config")
3491
- ].filter((candidate) => Boolean(candidate)).find((candidate) => existsSync18(candidate));
4146
+ defaultKubeconfigPath()
4147
+ ].filter((candidate) => Boolean(candidate));
4148
+ const kubeconfigPath = findReadableKubeconfigPath(candidates, "Kubernetes kubectl kubeconfig");
3492
4149
  if (!kubeconfigPath) {
3493
4150
  return void 0;
3494
4151
  }
3495
4152
  return {
3496
- kubeconfig: readFileSync8(kubeconfigPath, "utf-8"),
4153
+ kubeconfig: readKubeconfigFile(kubeconfigPath, "Kubernetes kubectl kubeconfig"),
3497
4154
  shouldRewriteLoopback: !isHostLocalKubeconfigPath(kubeconfigPath)
3498
4155
  };
3499
4156
  }
3500
4157
  function createTempKubeconfig(kubeconfig, includeAmbientContext = false, rewriteLoopback = true) {
3501
- const dir = mkdtempSync3(join5(tmpdir3(), "sc-saas-kube-"));
3502
- const path = join5(dir, "kubeconfig");
4158
+ const dir = mkdtempSync3(join6(tmpdir3(), "sc-saas-kube-"));
4159
+ const path = join6(dir, "kubeconfig");
3503
4160
  const rewritten = rewriteLoopback ? rewriteLoopbackKubeconfig(kubeconfig, process.env.KUBECONFIG_LOOPBACK_HOST) : kubeconfig;
3504
4161
  writeFileSync6(path, rewritten, { mode: 384 });
3505
4162
  const args = ["--kubeconfig", path];
@@ -3552,6 +4209,93 @@ async function withKubeconfigAsync(kubeconfig, fn) {
3552
4209
  cleanup();
3553
4210
  }
3554
4211
  }
4212
+ function execKubectl(args, kubeconfig, timeout = 3e3) {
4213
+ return withKubeconfig(
4214
+ kubeconfig,
4215
+ (kubeArgs) => execFileSync2("kubectl", [...kubeArgs, ...args], {
4216
+ encoding: "utf-8",
4217
+ timeout,
4218
+ stdio: ["ignore", "pipe", "pipe"]
4219
+ })
4220
+ );
4221
+ }
4222
+ function tryExecKubectl(args, kubeconfig, timeout = 5e3) {
4223
+ try {
4224
+ return execKubectl(args, kubeconfig, timeout);
4225
+ } catch {
4226
+ return null;
4227
+ }
4228
+ }
4229
+ function resourceOutputHas(output2, resourceName) {
4230
+ return Boolean(
4231
+ output2?.split(/\s+/).map((item) => item.trim()).some((item) => item === resourceName)
4232
+ );
4233
+ }
4234
+ function checkAgentSandboxPreflight(options) {
4235
+ const missing = [];
4236
+ const warnings = [];
4237
+ const kubeconfig = options?.kubeconfig;
4238
+ const extensionResources = tryExecKubectl(
4239
+ ["api-resources", "--api-group", "extensions.agents.x-k8s.io", "-o", "name"],
4240
+ kubeconfig
4241
+ );
4242
+ if (!resourceOutputHas(extensionResources, "sandboxtemplates")) {
4243
+ missing.push("CRD sandboxtemplates.extensions.agents.x-k8s.io");
4244
+ }
4245
+ if (!resourceOutputHas(extensionResources, "sandboxclaims")) {
4246
+ missing.push("CRD sandboxclaims.extensions.agents.x-k8s.io");
4247
+ }
4248
+ const coreResources = tryExecKubectl(
4249
+ ["api-resources", "--api-group", "agents.x-k8s.io", "-o", "name"],
4250
+ kubeconfig
4251
+ );
4252
+ if (!resourceOutputHas(coreResources, "sandboxes")) {
4253
+ missing.push("CRD sandboxes.agents.x-k8s.io");
4254
+ }
4255
+ const controllerOutput = tryExecKubectl(
4256
+ ["-n", "agent-sandbox-system", "get", "deployment", "agent-sandbox-controller", "-o", "json"],
4257
+ kubeconfig,
4258
+ 1e4
4259
+ );
4260
+ if (!controllerOutput) {
4261
+ missing.push("deployment/agent-sandbox-controller in namespace agent-sandbox-system");
4262
+ } else {
4263
+ try {
4264
+ const controller = JSON.parse(controllerOutput);
4265
+ const status = controller.status ?? {};
4266
+ if ((status.availableReplicas ?? 0) < 1) {
4267
+ missing.push("Ready agent-sandbox controller");
4268
+ }
4269
+ } catch {
4270
+ missing.push("Readable agent-sandbox controller status");
4271
+ }
4272
+ }
4273
+ const runtimeClassNames = [
4274
+ ...new Set(
4275
+ [options?.runtimeClassName, ...options?.runtimeClassNames ?? []].map((name) => name?.trim()).filter((name) => Boolean(name))
4276
+ )
4277
+ ];
4278
+ for (const runtimeClassName of runtimeClassNames) {
4279
+ const runtimeClass = tryExecKubectl(["get", "runtimeclass", runtimeClassName], kubeconfig);
4280
+ if (!runtimeClass) {
4281
+ missing.push(`RuntimeClass ${runtimeClassName}`);
4282
+ }
4283
+ }
4284
+ const sandboxNodes = tryExecKubectl(
4285
+ ["get", "nodes", "-l", "shadowob.com/sandbox-ready=true", "-o", "name"],
4286
+ kubeconfig
4287
+ );
4288
+ if (!sandboxNodes?.trim()) {
4289
+ warnings.push("No nodes are labeled shadowob.com/sandbox-ready=true");
4290
+ }
4291
+ return {
4292
+ ok: missing.length === 0,
4293
+ missing,
4294
+ warnings,
4295
+ runtimeClassName: runtimeClassNames[0],
4296
+ runtimeClassNames
4297
+ };
4298
+ }
3555
4299
  function execKubectlAsync(args, kubeconfig, timeout = 3e3) {
3556
4300
  return withKubeconfigAsync(
3557
4301
  kubeconfig,
@@ -3669,7 +4413,7 @@ async function getPodReadyState(namespace, podName, kubeconfig) {
3669
4413
  throw error;
3670
4414
  }
3671
4415
  }
3672
- async function waitForAgentSandboxReady(options) {
4416
+ async function waitForAgentSandboxReady2(options) {
3673
4417
  const timeoutMs = options.timeoutMs ?? 18e4;
3674
4418
  const intervalMs = options.intervalMs ?? 2e3;
3675
4419
  const startedAt = Date.now();
@@ -3875,7 +4619,7 @@ async function restorePvcFromVolumeSnapshot(options) {
3875
4619
  import { execFileSync as execFileSync3 } from "child_process";
3876
4620
  import { mkdir as mkdir3 } from "fs/promises";
3877
4621
  import { homedir as homedir5 } from "os";
3878
- import { delimiter as delimiter3, join as join6 } from "path";
4622
+ import { delimiter as delimiter3, join as join7 } from "path";
3879
4623
  import * as automation from "@pulumi/pulumi/automation/index.js";
3880
4624
  import { PulumiCommand } from "@pulumi/pulumi/automation/index.js";
3881
4625
 
@@ -4143,6 +4887,29 @@ function baseVolumes(configMapName) {
4143
4887
  { name: RUNNER_AGENTS_VOLUME_NAME, emptyDir: {} }
4144
4888
  ];
4145
4889
  }
4890
+ function isAgentSandboxBackend2(config) {
4891
+ return (config.deployments?.backend ?? "agent-sandbox") === "agent-sandbox";
4892
+ }
4893
+ function hasKeys(value) {
4894
+ return Boolean(value && typeof value === "object" && Object.keys(value).length > 0);
4895
+ }
4896
+ function resolveSchedulingConfig(config, agent) {
4897
+ const defaults = isAgentSandboxBackend2(config) ? { nodeSelector: { "shadowob.com/sandbox-ready": "true" } } : {};
4898
+ const globalScheduling = config.deployments?.scheduling ?? {};
4899
+ const agentScheduling = agent.scheduling ?? {};
4900
+ const nodeSelector = {
4901
+ ...defaults.nodeSelector ?? {},
4902
+ ...globalScheduling.nodeSelector ?? {},
4903
+ ...agentScheduling.nodeSelector ?? {}
4904
+ };
4905
+ const affinity = agentScheduling.affinity ?? globalScheduling.affinity;
4906
+ const tolerations = agentScheduling.tolerations ?? globalScheduling.tolerations;
4907
+ return {
4908
+ ...Object.keys(nodeSelector).length > 0 ? { nodeSelector } : {},
4909
+ ...hasKeys(affinity) ? { affinity } : {},
4910
+ ...tolerations && tolerations.length > 0 ? { tolerations } : {}
4911
+ };
4912
+ }
4146
4913
  function buildAgentPodSpec(options) {
4147
4914
  const runtime = getRuntime(options.agent.runtime);
4148
4915
  const image = options.agent.image ?? runtime.defaultImage;
@@ -4237,6 +5004,7 @@ function buildAgentPodSpec(options) {
4237
5004
  initContainers,
4238
5005
  containers,
4239
5006
  volumes,
5007
+ scheduling: resolveSchedulingConfig(options.config, options.agent),
4240
5008
  pluginArtifacts
4241
5009
  };
4242
5010
  }
@@ -4334,6 +5102,9 @@ function createAgentDeployment(options) {
4334
5102
  securityContext: buildSecurityContext(),
4335
5103
  containers: pod.containers,
4336
5104
  volumes: pod.volumes,
5105
+ nodeSelector: pod.scheduling.nodeSelector,
5106
+ affinity: pod.scheduling.affinity,
5107
+ tolerations: pod.scheduling.tolerations,
4337
5108
  restartPolicy: "Always"
4338
5109
  }
4339
5110
  }
@@ -4539,6 +5310,9 @@ function buildAgentSandboxTemplateManifest(options) {
4539
5310
  securityContext: buildSecurityContext(),
4540
5311
  containers: options.pod.containers,
4541
5312
  volumes: options.pod.volumes,
5313
+ nodeSelector: options.pod.scheduling.nodeSelector,
5314
+ affinity: options.pod.scheduling.affinity,
5315
+ tolerations: options.pod.scheduling.tolerations,
4542
5316
  restartPolicy: "Always"
4543
5317
  }
4544
5318
  },
@@ -4626,18 +5400,18 @@ function createConfigResources(options) {
4626
5400
  }
4627
5401
 
4628
5402
  // src/infra/hash.ts
4629
- import { createHash as createHash2 } from "crypto";
4630
- function stableStringify(value) {
5403
+ import { createHash as createHash3 } from "crypto";
5404
+ function stableStringify2(value) {
4631
5405
  if (Array.isArray(value)) {
4632
- return `[${value.map((item) => stableStringify(item)).join(",")}]`;
5406
+ return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
4633
5407
  }
4634
5408
  if (value && typeof value === "object") {
4635
- return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(",")}}`;
5409
+ return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${stableStringify2(item)}`).join(",")}}`;
4636
5410
  }
4637
5411
  return JSON.stringify(value);
4638
5412
  }
4639
5413
  function stableHash(value) {
4640
- return createHash2("sha256").update(stableStringify(value)).digest("hex");
5414
+ return createHash3("sha256").update(stableStringify2(value)).digest("hex");
4641
5415
  }
4642
5416
 
4643
5417
  // src/infra/networking.ts
@@ -4737,6 +5511,9 @@ function classifyEnv(registrySecretEnv, mergedEnv) {
4737
5511
  }
4738
5512
  return { plainEnv, secretData };
4739
5513
  }
5514
+ function omitEnvKeys(env, keys) {
5515
+ for (const key of keys) delete env[key];
5516
+ }
4740
5517
  function runtimePackageEnvDefaults(options) {
4741
5518
  const env = {};
4742
5519
  if (!options.currentEnv.SHADOW_SLASH_COMMANDS_PATH) {
@@ -4759,6 +5536,7 @@ function buildAgentRuntimePackage(options) {
4759
5536
  ...agent.env ?? {},
4760
5537
  ...extraEnv ?? {}
4761
5538
  };
5539
+ const runtimeEnvOmitKeys = collectPluginRuntimeEnvOmitKeys(agent, config, cwd, runtimeEnv);
4762
5540
  const runtimeExtensions = collectPluginRuntimeExtensions(agent, config, cwd, runtimeEnv);
4763
5541
  const mergedEnv = {
4764
5542
  ...collectPluginBuildEnvVars(agent, config, cwd, runtimeEnv),
@@ -4784,6 +5562,7 @@ function buildAgentRuntimePackage(options) {
4784
5562
  if (runtimeArtifacts.provisionSecrets) {
4785
5563
  Object.assign(mergedEnv, runtimeArtifacts.provisionSecrets);
4786
5564
  }
5565
+ omitEnvKeys(mergedEnv, runtimeEnvOmitKeys);
4787
5566
  const { plainEnv, secretData } = classifyEnv(registrySecretEnv, mergedEnv);
4788
5567
  return {
4789
5568
  runtimeKind: runtime.runtimeKind,
@@ -4796,10 +5575,9 @@ function buildAgentRuntimePackage(options) {
4796
5575
  }
4797
5576
 
4798
5577
  // src/infra/shared.ts
4799
- import { readFileSync as readFileSync9 } from "fs";
4800
5578
  import * as k8s5 from "@pulumi/kubernetes";
4801
5579
  function createSharedResources(options) {
4802
- const providerConfig = options.kubeConfigPath ? { kubeconfig: readFileSync9(options.kubeConfigPath, "utf8") } : {
5580
+ const providerConfig = options.kubeConfigPath ? { kubeconfig: readKubeconfigFile(options.kubeConfigPath) } : {
4803
5581
  context: options.kubeContext ?? process.env.KUBECONFIG_CONTEXT ?? process.env.K8S_CONTEXT ?? "rancher-desktop"
4804
5582
  };
4805
5583
  const provider = new k8s5.Provider("k8s-provider", providerConfig);
@@ -5191,13 +5969,13 @@ function getNonEmptyEnv(name) {
5191
5969
  return trimmed.length > 0 ? trimmed : void 0;
5192
5970
  }
5193
5971
  function getDefaultStateDir() {
5194
- return getNonEmptyEnv("PULUMI_BACKEND_URL") ? "" : join6(homedir5(), ".shadowob", "pulumi");
5972
+ return getNonEmptyEnv("PULUMI_BACKEND_URL") ? "" : join7(homedir5(), ".shadowob", "pulumi");
5195
5973
  }
5196
5974
  function resolvePulumiBackendUrl(stateDir) {
5197
5975
  return getNonEmptyEnv("PULUMI_BACKEND_URL") ?? (stateDir ? `file://${stateDir}` : void 0);
5198
5976
  }
5199
5977
  function ensurePulumiCliOnPath(cliRoot) {
5200
- const binDir = join6(cliRoot, "bin");
5978
+ const binDir = join7(cliRoot, "bin");
5201
5979
  const currentPath = process.env.PATH ?? "";
5202
5980
  const parts = currentPath.split(delimiter3).filter(Boolean);
5203
5981
  if (!parts.includes(binDir)) {
@@ -5227,8 +6005,8 @@ ${stderr}` : ""}`
5227
6005
  }
5228
6006
  }
5229
6007
  async function getOrCreateStack(options) {
5230
- const cliRoot = join6(homedir5(), ".shadowob", "pulumi", "cli");
5231
- const pulumiHome = join6(homedir5(), ".shadowob", "pulumi", "home");
6008
+ const cliRoot = join7(homedir5(), ".shadowob", "pulumi", "cli");
6009
+ const pulumiHome = join7(homedir5(), ".shadowob", "pulumi", "home");
5232
6010
  const infraOpts = {
5233
6011
  config: options.config,
5234
6012
  namespace: options.namespace,
@@ -5419,11 +6197,14 @@ var K8sService = class {
5419
6197
  });
5420
6198
  }
5421
6199
  async waitForAgentSandboxReady(options) {
5422
- return waitForAgentSandboxReady(options);
6200
+ return waitForAgentSandboxReady2(options);
5423
6201
  }
5424
6202
  async waitForAgentSandboxPaused(options) {
5425
6203
  return waitForAgentSandboxPaused(options);
5426
6204
  }
6205
+ checkAgentSandboxPreflight(options) {
6206
+ return checkAgentSandboxPreflight(options);
6207
+ }
5427
6208
  async restorePvcFromVolumeSnapshot(options) {
5428
6209
  await restorePvcFromVolumeSnapshot(options);
5429
6210
  }