@shadowob/cloud 1.1.6 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +159 -11
  2. package/dist/{agent-browser-CERTMCDL.js → agent-browser-EI7FIK3X.js} +3 -3
  3. package/dist/{agent-browser-CIRZRIY4.js → agent-browser-YXE4ES6Q.js} +3 -3
  4. package/dist/{agent-pack-LF3O5TR4.js → agent-pack-35TFCZKP.js} +1 -1
  5. package/dist/{agent-pack-RQT27V7R.js → agent-pack-UOG6ZAUL.js} +1 -1
  6. package/dist/agentmemory-KP5O7GHB.js +101 -0
  7. package/dist/agentmemory-UV74POU5.js +100 -0
  8. package/dist/{airtable-BG2Q75G2.js → airtable-CXXS3YUN.js} +3 -3
  9. package/dist/{airtable-JCQXFM5D.js → airtable-YZ5JR5JC.js} +3 -3
  10. package/dist/{alipay-TZQI34RB.js → alipay-WED5P3XC.js} +3 -3
  11. package/dist/{alipay-MZX2XCDB.js → alipay-WJTVREMG.js} +3 -3
  12. package/dist/{amap-KPCLZYYL.js → amap-AIN23TQ7.js} +3 -3
  13. package/dist/{amap-5RQB3VGC.js → amap-F6GF7QKB.js} +3 -3
  14. package/dist/{atlassian-LGOEWYC7.js → atlassian-G6PM6UVM.js} +3 -3
  15. package/dist/{atlassian-TVS2A4IU.js → atlassian-IDR2NPJC.js} +3 -3
  16. package/dist/{baidu-appbuilder-QRRL3ETM.js → baidu-appbuilder-JKQIA5TG.js} +3 -3
  17. package/dist/{baidu-appbuilder-6UMESXHW.js → baidu-appbuilder-KVMCIFYH.js} +3 -3
  18. package/dist/{baidu-maps-HEPMVP5D.js → baidu-maps-GWMXV6YT.js} +3 -3
  19. package/dist/{baidu-maps-HXC4FBVP.js → baidu-maps-ZXGT6QZM.js} +3 -3
  20. package/dist/{baidu-netdisk-G5Q6B5NH.js → baidu-netdisk-J5SLQVWW.js} +3 -3
  21. package/dist/{baidu-netdisk-RS2K5W2M.js → baidu-netdisk-PONM3DY6.js} +3 -3
  22. package/dist/{baidu-smartprogram-JHD3XWF6.js → baidu-smartprogram-WJ5JMO6V.js} +3 -3
  23. package/dist/{baidu-smartprogram-EWTK5WKK.js → baidu-smartprogram-Y5WBR7RX.js} +3 -3
  24. package/dist/{browserbase-IUIYVYI7.js → browserbase-CGVQHRC4.js} +3 -3
  25. package/dist/{browserbase-JFO2PCIA.js → browserbase-PVWYTEZC.js} +3 -3
  26. package/dist/{canva-3YOFL7JS.js → canva-COCBQAT2.js} +3 -3
  27. package/dist/{canva-FMYN65SM.js → canva-RSTJSEX5.js} +3 -3
  28. package/dist/{chunk-ULOJLXQV.js → chunk-3CT6RQNM.js} +766 -482
  29. package/dist/{chunk-SIDBK5SZ.js → chunk-4YO3NA26.js} +1 -1
  30. package/dist/{chunk-RECNVWMT.js → chunk-6V7MW4HU.js} +17 -3
  31. package/dist/{chunk-2FWJSE6Z.js → chunk-EVV774KS.js} +1 -1
  32. package/dist/{chunk-SVMXSIMG.js → chunk-F6CQ6GAG.js} +2 -1
  33. package/dist/{chunk-JUPAE5IA.js → chunk-OL5VH6RN.js} +72 -69
  34. package/dist/{chunk-POSVEKIY.js → chunk-OYY64ZSX.js} +17 -3
  35. package/dist/{chunk-FSW4TNHN.js → chunk-P5Y6F2NH.js} +766 -482
  36. package/dist/{chunk-EEFMJYKB.js → chunk-PSK2SYZ3.js} +2 -1
  37. package/dist/{chunk-HRTBOZ7O.js → chunk-PYJRFKPN.js} +1 -1
  38. package/dist/{chunk-JY2HTT7Q.js → chunk-RMDY3W4V.js} +6 -0
  39. package/dist/{chunk-I3NRNCDR.js → chunk-X2SREECR.js} +6 -6
  40. package/dist/{chunk-XFJX4NWN.js → chunk-X5VOIA72.js} +6 -6
  41. package/dist/{chunk-CTNUKOQE.js → chunk-Y5BJ3EW2.js} +6 -0
  42. package/dist/{chunk-IXN3FU5J.js → chunk-Y6BKVDG7.js} +1 -1
  43. package/dist/{chunk-6P2K6QZR.js → chunk-ZGMWSSCC.js} +72 -69
  44. package/dist/{claude-plugin-577TAQVS.js → claude-plugin-FPN32WMT.js} +1 -1
  45. package/dist/{claude-plugin-L3MXJJ6J.js → claude-plugin-IYHOVTVL.js} +1 -1
  46. package/dist/cli.js +930 -149
  47. package/dist/{cloudflare-RDFPKMM5.js → cloudflare-U3RHJJKK.js} +3 -3
  48. package/dist/{cloudflare-HBBABPK6.js → cloudflare-ZHN7UGPX.js} +3 -3
  49. package/dist/{cnb-FLP3QX46.js → cnb-AMXC5I7D.js} +3 -3
  50. package/dist/{cnb-YAVVEYFB.js → cnb-P3IZ4JTD.js} +3 -3
  51. package/dist/console/index.html +1 -1
  52. package/dist/console/static/css/index.f4563d95.css +1 -0
  53. package/dist/console/static/js/index.020abc71.js +1 -0
  54. package/dist/{coze-E6VGRNLV.js → coze-66RYMKVB.js} +3 -3
  55. package/dist/{coze-C6PMDPBI.js → coze-YE3BINXP.js} +3 -3
  56. package/dist/{dashboard.command-SCAAQ23X.js → dashboard.command-BRPZCZER.js} +1 -1
  57. package/dist/{dashboard.command-H5DIGLUR.js → dashboard.command-GUHSJ2CN.js} +1 -1
  58. package/dist/{dingtalk-JNRNRN7E.js → dingtalk-4RFQG7N2.js} +3 -3
  59. package/dist/{dingtalk-WZGGIAHJ.js → dingtalk-VNFKXD2P.js} +3 -3
  60. package/dist/{douyin-miniprogram-AIJPPIZH.js → douyin-miniprogram-UEALAGOS.js} +3 -3
  61. package/dist/{douyin-miniprogram-HCYZ5NBW.js → douyin-miniprogram-UNB6UO2I.js} +3 -3
  62. package/dist/{figma-2YYNSCDX.js → figma-A264OWU5.js} +3 -3
  63. package/dist/{figma-RYOBMENP.js → figma-Y4TGSDZP.js} +3 -3
  64. package/dist/{firebase-OYSY6HPT.js → firebase-AI3MAGYG.js} +3 -3
  65. package/dist/{firebase-2IJDDBXX.js → firebase-ZGQARUIH.js} +3 -3
  66. package/dist/{firecrawl-2T3SBUW7.js → firecrawl-2JW7DMTH.js} +3 -3
  67. package/dist/{firecrawl-IYYXLAZM.js → firecrawl-UURQ5P5N.js} +3 -3
  68. package/dist/{flyai-QS5Q6FJR.js → flyai-EJGDMYFA.js} +3 -3
  69. package/dist/{flyai-7FJ4TRAG.js → flyai-ZFMZBBHJ.js} +3 -3
  70. package/dist/{gitagent-MWI75OIX.js → gitagent-5SDBYFNA.js} +1 -1
  71. package/dist/{gitagent-YBMWY7NZ.js → gitagent-ODXPCR4X.js} +1 -1
  72. package/dist/{gitee-3N7OFOM7.js → gitee-5UMJ4BC7.js} +3 -3
  73. package/dist/{gitee-KVNK6KLZ.js → gitee-D6NAZTCO.js} +3 -3
  74. package/dist/{github-LUEC2LID.js → github-JBLDKIA3.js} +3 -3
  75. package/dist/{github-XRO5Z3GC.js → github-PZQAVEZP.js} +3 -3
  76. package/dist/{google-ads-VPKWTX67.js → google-ads-BIFQOJ5M.js} +3 -3
  77. package/dist/{google-ads-A3QAJI4D.js → google-ads-QU3LJE4O.js} +3 -3
  78. package/dist/{google-analytics-C4UR5ZR2.js → google-analytics-7VZ6YZVA.js} +3 -3
  79. package/dist/{google-analytics-XDYZA2B7.js → google-analytics-HXMPCL5V.js} +3 -3
  80. package/dist/{google-workspace-YX35SHHX.js → google-workspace-6SEBJ4VA.js} +2 -2
  81. package/dist/{google-workspace-LL3EWVHH.js → google-workspace-L3AMJLCF.js} +2 -2
  82. package/dist/{huawei-xiaoyi-KPWLTSHB.js → huawei-xiaoyi-C6QIJMPM.js} +3 -3
  83. package/dist/{huawei-xiaoyi-6BSMGJHR.js → huawei-xiaoyi-JGLXWU5P.js} +3 -3
  84. package/dist/{hubspot-FTIEMNZO.js → hubspot-LACJGE6D.js} +3 -3
  85. package/dist/{hubspot-DIUHGEDI.js → hubspot-XWPRO4KZ.js} +3 -3
  86. package/dist/{huggingface-UUXK2RHK.js → huggingface-26QQZK4C.js} +3 -3
  87. package/dist/{huggingface-MJCOXA7E.js → huggingface-CQICNA2R.js} +3 -3
  88. package/dist/index.d.ts +1338 -1
  89. package/dist/index.js +1364 -226
  90. package/dist/{inference-ai-image-generation-PXV6IG4U.js → inference-ai-image-generation-5KYIUWT6.js} +3 -3
  91. package/dist/{inference-ai-image-generation-CMI6R5T3.js → inference-ai-image-generation-J2NYDCLZ.js} +3 -3
  92. package/dist/{inference-sh-7AZOLEFI.js → inference-sh-5SWQTK73.js} +3 -3
  93. package/dist/{inference-sh-ABQOD3YF.js → inference-sh-PTQF6T3R.js} +3 -3
  94. package/dist/{init.command-WGKN3GC6.js → init.command-C7UKPK2Y.js} +3 -3
  95. package/dist/{init.command-775GLXTC.js → init.command-UNL66BMR.js} +3 -3
  96. package/dist/{klaviyo-LDPBWBSS.js → klaviyo-4UNPMBFT.js} +3 -3
  97. package/dist/{klaviyo-6K5YEFNH.js → klaviyo-SLYNEULT.js} +3 -3
  98. package/dist/{kuaidi100-HGFM5VK2.js → kuaidi100-HZKV5AIS.js} +3 -3
  99. package/dist/{kuaidi100-UHPFCVXP.js → kuaidi100-ZQUW7GHH.js} +3 -3
  100. package/dist/lark-HQUZNHDI.js +382 -0
  101. package/dist/lark-PAV7XWJS.js +381 -0
  102. package/dist/{linear-T4ORUP7N.js → linear-PYGQ5SLK.js} +3 -3
  103. package/dist/{linear-7QFSFPOD.js → linear-VJLYNTUF.js} +3 -3
  104. package/dist/{lovart-PDUXRUHJ.js → lovart-KC6SVNAJ.js} +3 -3
  105. package/dist/{lovart-QO3SK55T.js → lovart-WVKY4RR4.js} +3 -3
  106. package/dist/{meta-ads-SCNFI45S.js → meta-ads-E6XT33GI.js} +3 -3
  107. package/dist/{meta-ads-V6XPZWX3.js → meta-ads-RJ6DWRYN.js} +3 -3
  108. package/dist/{miclaw-TPPPS2WN.js → miclaw-4BA3A2YN.js} +3 -3
  109. package/dist/{miclaw-5CNTW7VV.js → miclaw-LUV6DCHX.js} +3 -3
  110. package/dist/{model-provider-KFB76XV5.js → model-provider-SYXJZ3JD.js} +1 -1
  111. package/dist/{model-provider-AVSFJSZP.js → model-provider-U7NEYA3Y.js} +1 -1
  112. package/dist/nature-skills-G76ABIWZ.js +143 -0
  113. package/dist/nature-skills-SQHMFXKT.js +142 -0
  114. package/dist/{notion-WFA7KGZZ.js → notion-2JZAKOFP.js} +1 -1
  115. package/dist/{notion-FZK76MN2.js → notion-O3NO5TJH.js} +1 -1
  116. package/dist/{oceanengine-3JZUS3PP.js → oceanengine-D23UZGVB.js} +3 -3
  117. package/dist/{oceanengine-5BRIJVJE.js → oceanengine-WDK2OXX5.js} +3 -3
  118. package/dist/{opencli-PFXHGCS2.js → opencli-36P63YNU.js} +3 -3
  119. package/dist/{opencli-VIGRJTGH.js → opencli-SUHDFR33.js} +3 -3
  120. package/dist/{paypal-Z5JYHIWD.js → paypal-K27SUW3B.js} +3 -3
  121. package/dist/{paypal-33UADIPR.js → paypal-OPZ3KOV5.js} +3 -3
  122. package/dist/{playwright-SQAQ3DZG.js → playwright-2ULT3NIC.js} +3 -3
  123. package/dist/{playwright-MG5WHK47.js → playwright-3Q7LBILG.js} +3 -3
  124. package/dist/{plugins-HZBWK3WQ.js → plugins-2MITZ4ZD.js} +2 -2
  125. package/dist/{plugins-I4GD5SZX.js → plugins-UK2QWD6G.js} +2 -2
  126. package/dist/{posthog-MU5MAJOQ.js → posthog-E3EHXLAN.js} +3 -3
  127. package/dist/{posthog-RJRRKDWB.js → posthog-KPJVLGX6.js} +3 -3
  128. package/dist/{salesforce-34FVIJTG.js → salesforce-FGPNG7FB.js} +3 -3
  129. package/dist/{salesforce-3QZ6OFVO.js → salesforce-TVHISKBC.js} +3 -3
  130. package/dist/{sentry-PIWW46VA.js → sentry-BZ3J3MZM.js} +3 -3
  131. package/dist/{sentry-MCIRMACU.js → sentry-XC57YRAJ.js} +3 -3
  132. package/dist/{seo-suite-WJXMA3S4.js → seo-suite-2MDEDLAB.js} +3 -3
  133. package/dist/{seo-suite-4SQ3I67Q.js → seo-suite-U75O3QP6.js} +3 -3
  134. package/dist/{serve.command-2AHJI665.js → serve.command-G5RVQFUD.js} +3 -3
  135. package/dist/{serve.command-Q46LHQG6.js → serve.command-PYGDG7K3.js} +3 -3
  136. package/dist/{shadowob-PRSMI5MW.js → shadowob-3QZ7DLDW.js} +158 -31
  137. package/dist/{shadowob-JELOWHWX.js → shadowob-CJLOEKFP.js} +158 -31
  138. package/dist/{sherlock-2PKY2E2Y.js → sherlock-CQFUHKDH.js} +3 -3
  139. package/dist/{sherlock-C5ZWPPVT.js → sherlock-DONK2I6E.js} +3 -3
  140. package/dist/{shopify-GL3NFVGE.js → shopify-NO5GI3WD.js} +3 -3
  141. package/dist/{shopify-R4G3UXM6.js → shopify-VW2KLKH5.js} +3 -3
  142. package/dist/{skill-discovery-YPXXV622.js → skill-discovery-6JEPPKKM.js} +3 -3
  143. package/dist/{skill-discovery-7INAUP4D.js → skill-discovery-PWRAVGIS.js} +3 -3
  144. package/dist/skills/shadowob-cli/SKILL.md +33 -22
  145. package/dist/{stripe-LJNPQ3CQ.js → stripe-HCNCKG4C.js} +1 -1
  146. package/dist/{stripe-C22RR4ZS.js → stripe-IU3KTJ4H.js} +1 -1
  147. package/dist/{supabase-IRNQ54FJ.js → supabase-KRL7JW2D.js} +3 -3
  148. package/dist/{supabase-N4ONFJNQ.js → supabase-TYEBTZNO.js} +3 -3
  149. package/dist/{taobao-aipaas-LRR4GMO3.js → taobao-aipaas-PEUIDOYP.js} +3 -3
  150. package/dist/{taobao-aipaas-RVKORSF4.js → taobao-aipaas-SA5E4MZA.js} +3 -3
  151. package/dist/{tapd-TMQRSMFG.js → tapd-6DDIUPVQ.js} +3 -3
  152. package/dist/{tapd-3JPVJ7XH.js → tapd-OTYLSZGY.js} +3 -3
  153. package/dist/{tencent-ads-UHC6OPBV.js → tencent-ads-OW2TAMH5.js} +3 -3
  154. package/dist/{tencent-ads-IGD33LO7.js → tencent-ads-XTQZ27YT.js} +3 -3
  155. package/dist/{tencent-docs-C3A4J3CJ.js → tencent-docs-6D6A2VCO.js} +3 -3
  156. package/dist/{tencent-docs-O2SC4FHL.js → tencent-docs-K3TMUIWD.js} +3 -3
  157. package/dist/{tencent-maps-OQOKHVW2.js → tencent-maps-IZYWITJZ.js} +3 -3
  158. package/dist/{tencent-maps-HMMWMNF4.js → tencent-maps-SWI7CLQY.js} +3 -3
  159. package/dist/text-to-cad-B2UP6PKA.js +192 -0
  160. package/dist/text-to-cad-I4B6VBFV.js +193 -0
  161. package/dist/{vercel-KOXDDTHX.js → vercel-CCKRC76D.js} +3 -3
  162. package/dist/{vercel-OLNVDWMG.js → vercel-SBGEMIJJ.js} +3 -3
  163. package/dist/{webflow-OMJKZM54.js → webflow-C3EHNNSN.js} +3 -3
  164. package/dist/{webflow-FULU5Q2I.js → webflow-ZFBJH4CR.js} +3 -3
  165. package/dist/{wechat-miniprogram-skyline-KYCDMQNW.js → wechat-miniprogram-skyline-VNCRERHX.js} +3 -3
  166. package/dist/{wechat-miniprogram-skyline-VR4FVIQL.js → wechat-miniprogram-skyline-Z5JQUV5Q.js} +3 -3
  167. package/dist/{wechat-pay-BCMAJ6UW.js → wechat-pay-RPDKPUEB.js} +3 -3
  168. package/dist/{wechat-pay-YQQKXVUI.js → wechat-pay-XVDGJRF2.js} +3 -3
  169. package/dist/{wonda-NGWIORYN.js → wonda-EL2P44S7.js} +3 -3
  170. package/dist/{wonda-RBABXFNM.js → wonda-XK5JK4X3.js} +3 -3
  171. package/dist/{wordpress-woocommerce-RNA5HB3N.js → wordpress-woocommerce-4MEE5A2M.js} +3 -3
  172. package/dist/{wordpress-woocommerce-RDIUTHYT.js → wordpress-woocommerce-6ECNM2QU.js} +3 -3
  173. package/dist/{wps-LUWHMZQQ.js → wps-E4OZEMOF.js} +3 -3
  174. package/dist/{wps-DAEFQHDE.js → wps-JNQUC4JS.js} +3 -3
  175. package/dist/{yuque-HCHTJWNI.js → yuque-GLAAOS7X.js} +3 -3
  176. package/dist/{yuque-KRH5O74J.js → yuque-MEF6VFLJ.js} +3 -3
  177. package/images/RUNNERS.md +15 -0
  178. package/images/cc-connect-runner/entrypoint.mjs +228 -0
  179. package/images/claude-runner/RUNNER.md +5 -3
  180. package/images/codex-runner/RUNNER.md +5 -3
  181. package/images/gemini-runner/RUNNER.md +5 -2
  182. package/images/hermes-runner/RUNNER.md +4 -2
  183. package/images/hermes-runner/entrypoint.mjs +269 -1
  184. package/images/openclaw-runner/Dockerfile +1 -0
  185. package/images/openclaw-runner/RUNNER.md +3 -0
  186. package/images/openclaw-runner/entrypoint.mjs +249 -1
  187. package/images/openclaw-runner/warm-runtime-deps.mjs +1 -3
  188. package/images/opencode-runner/RUNNER.md +5 -3
  189. package/package.json +3 -3
  190. package/templates/agent-marketplace-buddy.template.json +4 -1
  191. package/templates/bmad-method-buddy.template.json +4 -1
  192. package/templates/code-trainer.template.json +331 -0
  193. package/templates/gstack-buddy.template.json +4 -1
  194. package/templates/little-match-girl.template.json +10 -3
  195. package/dist/console/static/css/index.7f91f806.css +0 -1
  196. package/dist/console/static/js/index.4487e1ff.js +0 -1
  197. package/dist/lark-6LNA3LUQ.js +0 -103
  198. package/dist/lark-URVBZNS4.js +0 -102
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { spawn, spawnSync } from 'node:child_process'
12
+ import { createHash } from 'node:crypto'
12
13
  import {
13
14
  chmodSync,
14
15
  cpSync,
@@ -27,8 +28,13 @@ const CONFIG_MOUNT = process.env.SHADOW_RUNNER_CONFIG_MOUNT ?? '/etc/openclaw'
27
28
  const RUNTIME_FILES_PATH = join(CONFIG_MOUNT, 'runtime-files.json')
28
29
  const RUNTIME_EXTENSIONS_PATH = join(CONFIG_MOUNT, 'runtime-extensions.json')
29
30
  const RUNNER_HOME = process.env.HOME ?? '/home/shadow'
31
+ const TEMPLATE_ROUTINES_PATH =
32
+ process.env.SHADOW_TEMPLATE_ROUTINES_PATH ?? '/etc/shadowob/template-routines.json'
30
33
  const CC_CONNECT_CONFIG_PATH =
31
34
  process.env.CC_CONNECT_CONFIG_PATH ?? join(RUNNER_HOME, '.cc-connect/config.toml')
35
+ const CC_CONNECT_DATA_DIR = process.env.CC_CONNECT_DATA_DIR ?? join(RUNNER_HOME, '.cc-connect')
36
+ const CC_CONNECT_CRON_STORE_PATH =
37
+ process.env.CC_CONNECT_CRON_STORE_PATH ?? join(CC_CONNECT_DATA_DIR, 'crons', 'jobs.json')
32
38
  const HEALTH_PORT = Number.parseInt(
33
39
  process.env.SHADOW_RUNNER_HEALTH_PORT ??
34
40
  process.env.OPENCLAW_GATEWAY_PORT ??
@@ -85,6 +91,227 @@ function modeForPath(path) {
85
91
  return 0o644
86
92
  }
87
93
 
94
+ function isPlainObject(value) {
95
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
96
+ }
97
+
98
+ function stableValue(value) {
99
+ if (Array.isArray(value)) return value.map(stableValue)
100
+ if (!isPlainObject(value)) return value
101
+ return Object.fromEntries(
102
+ Object.entries(value)
103
+ .sort(([left], [right]) => left.localeCompare(right))
104
+ .map(([key, item]) => [key, stableValue(item)]),
105
+ )
106
+ }
107
+
108
+ function stableHash(value) {
109
+ return createHash('sha256')
110
+ .update(JSON.stringify(stableValue(value)))
111
+ .digest('hex')
112
+ }
113
+
114
+ function readJsonFile(path, fallback) {
115
+ if (!existsSync(path)) return fallback
116
+ try {
117
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'))
118
+ return parsed && typeof parsed === 'object' ? parsed : fallback
119
+ } catch (err) {
120
+ console.warn(`[entrypoint] Failed to parse ${path}: ${err.message}`)
121
+ return fallback
122
+ }
123
+ }
124
+
125
+ function parseRoutineEveryCron(interval) {
126
+ if (typeof interval !== 'string') return null
127
+ const match = interval.trim().match(/^(\d+)\s*(m|h|d)$/i)
128
+ if (!match) return null
129
+ const amount = Number.parseInt(match[1], 10)
130
+ if (!Number.isFinite(amount) || amount <= 0) return null
131
+ const unit = match[2].toLowerCase()
132
+ if (unit === 'm') return amount < 60 ? `*/${amount} * * * *` : null
133
+ if (unit === 'h') return amount < 24 ? `0 */${amount} * * *` : null
134
+ return `0 0 */${amount} * *`
135
+ }
136
+
137
+ function buildCcConnectRoutineCronExpr(routine) {
138
+ const schedule = isPlainObject(routine.schedule) ? routine.schedule : {}
139
+ let expr = null
140
+ if (typeof schedule.cron === 'string' && schedule.cron.trim()) {
141
+ expr = schedule.cron.trim()
142
+ } else {
143
+ expr = parseRoutineEveryCron(schedule.interval)
144
+ }
145
+ if (!expr) return null
146
+ if (typeof schedule.timezone === 'string' && schedule.timezone.trim()) {
147
+ return `CRON_TZ=${schedule.timezone.trim()} ${expr}`
148
+ }
149
+ return expr
150
+ }
151
+
152
+ function resolveShadowobRoutineDelivery(routine) {
153
+ const deliveries = Array.isArray(routine.deliveries) ? routine.deliveries : []
154
+ for (const delivery of deliveries) {
155
+ if (
156
+ !isPlainObject(delivery) ||
157
+ delivery.pluginId !== 'shadowob' ||
158
+ delivery.kind !== 'channel'
159
+ ) {
160
+ continue
161
+ }
162
+ const target = isPlainObject(delivery.target) ? delivery.target : {}
163
+ const channelEnvKey =
164
+ typeof target.channelEnvKey === 'string' && target.channelEnvKey.trim()
165
+ ? target.channelEnvKey.trim()
166
+ : null
167
+ const channelId =
168
+ (channelEnvKey ? process.env[channelEnvKey] : undefined) ??
169
+ (typeof target.channelId === 'string' ? target.channelId : undefined)
170
+ if (!channelId) continue
171
+ return {
172
+ channelId,
173
+ threadId:
174
+ typeof target.threadId === 'string' && target.threadId.trim()
175
+ ? target.threadId.trim()
176
+ : null,
177
+ }
178
+ }
179
+ return null
180
+ }
181
+
182
+ function managedRoutineJobId(routine) {
183
+ const raw = `shadow-template-${routine.agentId ?? 'agent'}-${routine.id ?? 'routine'}`
184
+ .toLowerCase()
185
+ .replace(/[^a-z0-9._:-]+/g, '-')
186
+ .replace(/^-+|-+$/g, '')
187
+ if (raw.length >= 8 && raw.length <= 64) return raw
188
+ return `shadow-template-${stableHash({ agentId: routine.agentId, id: routine.id }).slice(0, 20)}`
189
+ }
190
+
191
+ function ccConnectRoutineSessionKey(delivery) {
192
+ if (delivery.threadId) return `shadowob:channel:${delivery.channelId}:thread:${delivery.threadId}`
193
+ return `shadowob:channel:${delivery.channelId}`
194
+ }
195
+
196
+ function ccConnectManagedJobShape(job) {
197
+ return {
198
+ id: job.id,
199
+ project: job.project,
200
+ session_key: job.session_key,
201
+ cron_expr: job.cron_expr,
202
+ prompt: job.prompt,
203
+ exec: job.exec,
204
+ work_dir: job.work_dir,
205
+ description: job.description,
206
+ enabled: job.enabled,
207
+ silent: job.silent,
208
+ mute: job.mute,
209
+ session_mode: job.session_mode,
210
+ mode: job.mode,
211
+ timeout_mins: job.timeout_mins,
212
+ }
213
+ }
214
+
215
+ function buildCcConnectRoutineJob(routine, now) {
216
+ if (
217
+ !isPlainObject(routine) ||
218
+ typeof routine.id !== 'string' ||
219
+ typeof routine.agentId !== 'string'
220
+ ) {
221
+ return null
222
+ }
223
+ const cronExpr = buildCcConnectRoutineCronExpr(routine)
224
+ const delivery = resolveShadowobRoutineDelivery(routine)
225
+ if (!cronExpr || !delivery) return null
226
+ const job = {
227
+ id: managedRoutineJobId(routine),
228
+ project: routine.agentId,
229
+ session_key: ccConnectRoutineSessionKey(delivery),
230
+ cron_expr: cronExpr,
231
+ prompt: String(routine.prompt ?? ''),
232
+ description:
233
+ typeof routine.title === 'string' && routine.title.trim() ? routine.title.trim() : routine.id,
234
+ enabled: routine.enabled !== false,
235
+ session_mode: 'new_per_run',
236
+ created_at: now,
237
+ last_run: '0001-01-01T00:00:00Z',
238
+ }
239
+ const managedSpecHash = stableHash(ccConnectManagedJobShape(job))
240
+ return {
241
+ ...job,
242
+ shadowTemplateRoutine: {
243
+ version: 1,
244
+ routineId: routine.id,
245
+ agentId: routine.agentId,
246
+ sourceHash: typeof routine.sourceHash === 'string' ? routine.sourceHash : null,
247
+ managedSpecHash,
248
+ },
249
+ }
250
+ }
251
+
252
+ function syncTemplateRoutinesToCcConnectCron() {
253
+ if (!existsSync(TEMPLATE_ROUTINES_PATH)) return
254
+ const seed = readJsonFile(TEMPLATE_ROUTINES_PATH, null)
255
+ const routines = Array.isArray(seed?.routines) ? seed.routines : []
256
+ if (routines.length === 0) return
257
+
258
+ const now = new Date().toISOString()
259
+ const existing = readJsonFile(CC_CONNECT_CRON_STORE_PATH, [])
260
+ const jobs = Array.isArray(existing) ? existing.filter(Boolean) : []
261
+ let changed = false
262
+
263
+ for (const routine of routines) {
264
+ const desired = buildCcConnectRoutineJob(routine, now)
265
+ if (!desired) {
266
+ console.warn(
267
+ `[entrypoint] Skipping invalid template routine: ${JSON.stringify(routine?.id ?? null)}`,
268
+ )
269
+ continue
270
+ }
271
+ const existingIndex = jobs.findIndex((job) => {
272
+ const marker = isPlainObject(job?.shadowTemplateRoutine) ? job.shadowTemplateRoutine : null
273
+ return marker?.routineId === desired.shadowTemplateRoutine.routineId || job?.id === desired.id
274
+ })
275
+ if (existingIndex < 0) {
276
+ jobs.push(desired)
277
+ changed = true
278
+ console.log(`[entrypoint] Seeded cc-connect cron routine: ${desired.id}`)
279
+ continue
280
+ }
281
+ const current = jobs[existingIndex]
282
+ const marker = isPlainObject(current.shadowTemplateRoutine)
283
+ ? current.shadowTemplateRoutine
284
+ : null
285
+ const currentManagedSpecHash = stableHash(ccConnectManagedJobShape(current))
286
+ if (!marker || marker.managedSpecHash !== currentManagedSpecHash) {
287
+ console.log(
288
+ `[entrypoint] Preserved user-edited cc-connect cron routine: ${current.id ?? desired.id}`,
289
+ )
290
+ continue
291
+ }
292
+ if (marker.managedSpecHash === desired.shadowTemplateRoutine.managedSpecHash) continue
293
+ jobs[existingIndex] = {
294
+ ...desired,
295
+ id: current.id ?? desired.id,
296
+ created_at: current.created_at ?? desired.created_at,
297
+ last_run: current.last_run ?? desired.last_run,
298
+ last_error: current.last_error,
299
+ }
300
+ changed = true
301
+ console.log(
302
+ `[entrypoint] Updated cc-connect cron routine from template: ${jobs[existingIndex].id}`,
303
+ )
304
+ }
305
+
306
+ if (!changed) return
307
+ mkdirSync(dirname(CC_CONNECT_CRON_STORE_PATH), { recursive: true })
308
+ writeFileSync(CC_CONNECT_CRON_STORE_PATH, `${JSON.stringify(jobs, null, 2)}\n`, {
309
+ encoding: 'utf-8',
310
+ mode: 0o600,
311
+ })
312
+ chmodSync(CC_CONNECT_CRON_STORE_PATH, 0o600)
313
+ }
314
+
88
315
  function materializeRuntimeFiles() {
89
316
  const files = loadJson(RUNTIME_FILES_PATH, {})
90
317
  for (const [path, content] of Object.entries(files)) {
@@ -290,6 +517,7 @@ async function main() {
290
517
  materializeRuntimeFiles()
291
518
  materializeCredentialFiles()
292
519
  materializePluginRuntimeAssets()
520
+ syncTemplateRoutinesToCcConnectCron()
293
521
 
294
522
  if (process.env.SHADOW_RUNNER_VALIDATE_ONLY === '1') {
295
523
  verifyBinary('cc-connect', ['--help'])
@@ -167,9 +167,11 @@ not through OpenClaw `models.providers`.
167
167
  `.claude/commands` only for compatibility.
168
168
  - MCP: write `.mcp.json` for project-scoped MCP and avoid relying on
169
169
  `~/.claude.json` in immutable images.
170
- - Cron/routine: Claude Code has scheduled prompt support in its automation
171
- docs, but Cloud phase 1 should treat scheduling as a Cloud/Shadow concern
172
- unless explicitly mounting a Claude-native schedule store.
170
+ - Cron/routine: Cloud template routines are materialized by the shared
171
+ cc-connect runner from `/etc/shadowob/template-routines.json` into
172
+ `~/.cc-connect/crons/jobs.json`. Managed jobs use deterministic ids and a
173
+ spec hash so user-edited schedules are preserved. ShadowOB delivery uses
174
+ `session_key = "shadowob:channel:<channel_id>"` or a thread-qualified variant.
173
175
  - Hooks: write Claude settings `hooks`, not OpenClaw `hooks`.
174
176
  - Subagents: materialize `.claude/agents` and any preloaded skill references.
175
177
  - Logs: collect both cc-connect daemon logs and Claude Code native telemetry or
@@ -165,9 +165,11 @@ type = "shadowob"
165
165
  - Skills: materialize `.agents/skills` for repo-scoped workflows and
166
166
  `$CODEX_HOME/skills` only if the runner owns the whole home directory.
167
167
  - MCP: generate `[mcp_servers.*]` TOML tables.
168
- - Cron/routine: Codex app automations are not the same as CLI-local cron; Cloud
169
- should own phase-1 schedules unless later integrating the Codex app
170
- automation APIs.
168
+ - Cron/routine: Cloud template routines are materialized by the shared
169
+ cc-connect runner from `/etc/shadowob/template-routines.json` into
170
+ `~/.cc-connect/crons/jobs.json`. Managed jobs use deterministic ids and a
171
+ spec hash so user-edited schedules are preserved. ShadowOB delivery uses
172
+ `session_key = "shadowob:channel:<channel_id>"` or a thread-qualified variant.
171
173
  - Hooks: write Codex hook config in trusted project or user config.
172
174
  - Subagents: generate Codex agent roles and instruction files under `.codex`
173
175
  only when `features.multi_agent` or equivalent config is enabled.
@@ -165,8 +165,11 @@ type = "shadowob"
165
165
  - Skills: no native `SKILL.md` surface was found; use `GEMINI.md`, custom
166
166
  commands, and extensions for phase 1.
167
167
  - MCP: write `mcpServers` in `.gemini/settings.json`.
168
- - Cron/routine: no native CLI cron surface found in the researched config docs;
169
- Cloud should own scheduling for phase 1.
168
+ - Cron/routine: Cloud template routines are materialized by the shared
169
+ cc-connect runner from `/etc/shadowob/template-routines.json` into
170
+ `~/.cc-connect/crons/jobs.json`. Managed jobs use deterministic ids and a
171
+ spec hash so user-edited schedules are preserved. ShadowOB delivery uses
172
+ `session_key = "shadowob:channel:<channel_id>"` or a thread-qualified variant.
170
173
  - Hooks: write `hooksConfig` and `hooks.*` in Gemini settings.
171
174
  - Subagents: Gemini settings expose agent override and hook points around agent
172
175
  execution, but cc-connect currently drives the main Gemini CLI agent only.
@@ -176,8 +176,10 @@ SHADOW_TOKEN=...
176
176
  runner profile.
177
177
  - MCP: write Hermes MCP config rather than OpenClaw or Codex MCP formats.
178
178
  - Cron/routine: Hermes has the strongest native cron surface among the target
179
- runners; keep cron jobs in Hermes native storage when the agent runtime is
180
- Hermes.
179
+ runners. Cloud template routines are seeded from
180
+ `/etc/shadowob/template-routines.json` into `~/.hermes/cron/jobs.json`, using
181
+ Hermes native `deliver`/`origin` ShadowOB delivery. Managed jobs use
182
+ deterministic ids and a spec hash so user-edited schedules are preserved.
181
183
  - Hooks: expose plugin hooks through `plugins.enabled` and plugin files, not a
182
184
  central OpenClaw hook adapter.
183
185
  - Subagents: support Hermes delegation later as native Hermes multi-agent
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { spawn, spawnSync } from 'node:child_process'
9
+ import { createHash } from 'node:crypto'
9
10
  import {
10
11
  chmodSync,
11
12
  cpSync,
@@ -24,6 +25,11 @@ const CONFIG_MOUNT = process.env.SHADOW_RUNNER_CONFIG_MOUNT ?? '/etc/openclaw'
24
25
  const RUNTIME_FILES_PATH = join(CONFIG_MOUNT, 'runtime-files.json')
25
26
  const RUNTIME_EXTENSIONS_PATH = join(CONFIG_MOUNT, 'runtime-extensions.json')
26
27
  const RUNNER_HOME = process.env.HOME ?? '/home/shadow'
28
+ const HERMES_HOME = process.env.HERMES_HOME ?? join(RUNNER_HOME, '.hermes')
29
+ const TEMPLATE_ROUTINES_PATH =
30
+ process.env.SHADOW_TEMPLATE_ROUTINES_PATH ?? '/etc/shadowob/template-routines.json'
31
+ const HERMES_CRON_STORE_PATH =
32
+ process.env.HERMES_CRON_STORE_PATH ?? join(HERMES_HOME, 'cron', 'jobs.json')
27
33
  const HEALTH_PORT = Number.parseInt(
28
34
  process.env.SHADOW_RUNNER_HEALTH_PORT ?? process.env.OPENCLAW_GATEWAY_PORT ?? '3100',
29
35
  10,
@@ -76,6 +82,267 @@ function modeForPath(path) {
76
82
  return 0o644
77
83
  }
78
84
 
85
+ function isPlainObject(value) {
86
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
87
+ }
88
+
89
+ function stableValue(value) {
90
+ if (Array.isArray(value)) return value.map(stableValue)
91
+ if (!isPlainObject(value)) return value
92
+ return Object.fromEntries(
93
+ Object.entries(value)
94
+ .sort(([left], [right]) => left.localeCompare(right))
95
+ .map(([key, item]) => [key, stableValue(item)]),
96
+ )
97
+ }
98
+
99
+ function stableHash(value) {
100
+ return createHash('sha256')
101
+ .update(JSON.stringify(stableValue(value)))
102
+ .digest('hex')
103
+ }
104
+
105
+ function readJsonFile(path, fallback) {
106
+ if (!existsSync(path)) return fallback
107
+ try {
108
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'))
109
+ return parsed && typeof parsed === 'object' ? parsed : fallback
110
+ } catch (err) {
111
+ console.warn(`[entrypoint] Failed to parse ${path}: ${err.message}`)
112
+ return fallback
113
+ }
114
+ }
115
+
116
+ function parseRoutineEveryMinutes(interval) {
117
+ if (typeof interval !== 'string') return null
118
+ const match = interval.trim().match(/^(\d+)\s*(m|h|d)$/i)
119
+ if (!match) return null
120
+ const amount = Number.parseInt(match[1], 10)
121
+ if (!Number.isFinite(amount) || amount <= 0) return null
122
+ const unit = match[2].toLowerCase()
123
+ return amount * (unit === 'm' ? 1 : unit === 'h' ? 60 : 1440)
124
+ }
125
+
126
+ function isoNow() {
127
+ return new Date().toISOString()
128
+ }
129
+
130
+ function addMinutesIso(minutes) {
131
+ return new Date(Date.now() + minutes * 60_000).toISOString()
132
+ }
133
+
134
+ function buildHermesRoutineSchedule(routine) {
135
+ const schedule = isPlainObject(routine.schedule) ? routine.schedule : {}
136
+ if (typeof schedule.cron === 'string' && schedule.cron.trim()) {
137
+ const expr = schedule.cron.trim()
138
+ return {
139
+ schedule: { kind: 'cron', expr, display: expr },
140
+ scheduleDisplay: expr,
141
+ nextRunAt: null,
142
+ }
143
+ }
144
+ const minutes = parseRoutineEveryMinutes(schedule.interval)
145
+ if (minutes) {
146
+ const display = `every ${minutes}m`
147
+ return {
148
+ schedule: { kind: 'interval', minutes, display },
149
+ scheduleDisplay: display,
150
+ nextRunAt: addMinutesIso(minutes),
151
+ }
152
+ }
153
+ return null
154
+ }
155
+
156
+ function resolveShadowobRoutineDelivery(routine) {
157
+ const deliveries = Array.isArray(routine.deliveries) ? routine.deliveries : []
158
+ for (const delivery of deliveries) {
159
+ if (
160
+ !isPlainObject(delivery) ||
161
+ delivery.pluginId !== 'shadowob' ||
162
+ delivery.kind !== 'channel'
163
+ ) {
164
+ continue
165
+ }
166
+ const target = isPlainObject(delivery.target) ? delivery.target : {}
167
+ const channelEnvKey =
168
+ typeof target.channelEnvKey === 'string' && target.channelEnvKey.trim()
169
+ ? target.channelEnvKey.trim()
170
+ : null
171
+ const channelId =
172
+ (channelEnvKey ? process.env[channelEnvKey] : undefined) ??
173
+ (typeof target.channelId === 'string' ? target.channelId : undefined)
174
+ if (!channelId) continue
175
+ return {
176
+ channelId,
177
+ threadId:
178
+ typeof target.threadId === 'string' && target.threadId.trim()
179
+ ? target.threadId.trim()
180
+ : null,
181
+ }
182
+ }
183
+ return null
184
+ }
185
+
186
+ function managedRoutineJobId(routine) {
187
+ const raw = `shadow-template-${routine.agentId ?? 'agent'}-${routine.id ?? 'routine'}`
188
+ .toLowerCase()
189
+ .replace(/[^a-z0-9._:-]+/g, '-')
190
+ .replace(/^-+|-+$/g, '')
191
+ if (raw.length >= 8 && raw.length <= 64) return raw
192
+ return `shadow-template-${stableHash({ agentId: routine.agentId, id: routine.id }).slice(0, 20)}`
193
+ }
194
+
195
+ function hermesManagedJobShape(job) {
196
+ return {
197
+ id: job.id,
198
+ name: job.name,
199
+ prompt: job.prompt,
200
+ skills: job.skills,
201
+ skill: job.skill,
202
+ model: job.model,
203
+ provider: job.provider,
204
+ base_url: job.base_url,
205
+ script: job.script,
206
+ no_agent: job.no_agent,
207
+ context_from: job.context_from,
208
+ schedule: job.schedule,
209
+ schedule_display: job.schedule_display,
210
+ repeat: job.repeat,
211
+ enabled: job.enabled,
212
+ state: job.state,
213
+ deliver: job.deliver,
214
+ origin: job.origin,
215
+ enabled_toolsets: job.enabled_toolsets,
216
+ workdir: job.workdir,
217
+ profile: job.profile,
218
+ }
219
+ }
220
+
221
+ function buildHermesRoutineJob(routine, now) {
222
+ if (
223
+ !isPlainObject(routine) ||
224
+ typeof routine.id !== 'string' ||
225
+ typeof routine.agentId !== 'string'
226
+ ) {
227
+ return null
228
+ }
229
+ const schedule = buildHermesRoutineSchedule(routine)
230
+ const delivery = resolveShadowobRoutineDelivery(routine)
231
+ if (!schedule || !delivery) return null
232
+
233
+ const origin = {
234
+ platform: 'shadowob',
235
+ chat_id: delivery.channelId,
236
+ ...(delivery.threadId ? { thread_id: delivery.threadId } : {}),
237
+ }
238
+ const job = {
239
+ id: managedRoutineJobId(routine),
240
+ name:
241
+ typeof routine.title === 'string' && routine.title.trim() ? routine.title.trim() : routine.id,
242
+ prompt: String(routine.prompt ?? ''),
243
+ skills: [],
244
+ skill: null,
245
+ model: null,
246
+ provider: null,
247
+ base_url: null,
248
+ script: null,
249
+ no_agent: false,
250
+ context_from: null,
251
+ schedule: schedule.schedule,
252
+ schedule_display: schedule.scheduleDisplay,
253
+ repeat: { times: null, completed: 0 },
254
+ enabled: routine.enabled !== false,
255
+ state: routine.enabled === false ? 'paused' : 'scheduled',
256
+ paused_at: null,
257
+ paused_reason: null,
258
+ created_at: now,
259
+ next_run_at: schedule.nextRunAt,
260
+ last_run_at: null,
261
+ last_status: null,
262
+ last_error: null,
263
+ last_delivery_error: null,
264
+ deliver: delivery.threadId ? 'origin' : `shadowob:${delivery.channelId}`,
265
+ origin,
266
+ enabled_toolsets: null,
267
+ workdir: '/workspace',
268
+ profile: null,
269
+ }
270
+ const managedSpecHash = stableHash(hermesManagedJobShape(job))
271
+ return {
272
+ ...job,
273
+ shadowTemplateRoutine: {
274
+ version: 1,
275
+ routineId: routine.id,
276
+ agentId: routine.agentId,
277
+ sourceHash: typeof routine.sourceHash === 'string' ? routine.sourceHash : null,
278
+ managedSpecHash,
279
+ },
280
+ }
281
+ }
282
+
283
+ function syncTemplateRoutinesToHermesCron() {
284
+ if (!existsSync(TEMPLATE_ROUTINES_PATH)) return
285
+ const seed = readJsonFile(TEMPLATE_ROUTINES_PATH, null)
286
+ const routines = Array.isArray(seed?.routines) ? seed.routines : []
287
+ if (routines.length === 0) return
288
+
289
+ const now = isoNow()
290
+ const store = readJsonFile(HERMES_CRON_STORE_PATH, { jobs: [] })
291
+ const jobs = Array.isArray(store.jobs) ? store.jobs.filter(Boolean) : []
292
+ let changed = false
293
+
294
+ for (const routine of routines) {
295
+ const desired = buildHermesRoutineJob(routine, now)
296
+ if (!desired) {
297
+ console.warn(
298
+ `[entrypoint] Skipping invalid template routine: ${JSON.stringify(routine?.id ?? null)}`,
299
+ )
300
+ continue
301
+ }
302
+ const existingIndex = jobs.findIndex((job) => {
303
+ const marker = isPlainObject(job?.shadowTemplateRoutine) ? job.shadowTemplateRoutine : null
304
+ return marker?.routineId === desired.shadowTemplateRoutine.routineId || job?.id === desired.id
305
+ })
306
+ if (existingIndex < 0) {
307
+ jobs.push(desired)
308
+ changed = true
309
+ console.log(`[entrypoint] Seeded Hermes cron routine: ${desired.id}`)
310
+ continue
311
+ }
312
+ const existing = jobs[existingIndex]
313
+ const marker = isPlainObject(existing.shadowTemplateRoutine)
314
+ ? existing.shadowTemplateRoutine
315
+ : null
316
+ const currentManagedSpecHash = stableHash(hermesManagedJobShape(existing))
317
+ if (!marker || marker.managedSpecHash !== currentManagedSpecHash) {
318
+ console.log(
319
+ `[entrypoint] Preserved user-edited Hermes cron routine: ${existing.id ?? desired.id}`,
320
+ )
321
+ continue
322
+ }
323
+ if (marker.managedSpecHash === desired.shadowTemplateRoutine.managedSpecHash) continue
324
+ jobs[existingIndex] = {
325
+ ...desired,
326
+ id: existing.id ?? desired.id,
327
+ created_at: existing.created_at ?? desired.created_at,
328
+ last_run_at: existing.last_run_at ?? null,
329
+ last_status: existing.last_status ?? null,
330
+ last_error: existing.last_error ?? null,
331
+ last_delivery_error: existing.last_delivery_error ?? null,
332
+ }
333
+ changed = true
334
+ console.log(`[entrypoint] Updated Hermes cron routine from template: ${jobs[existingIndex].id}`)
335
+ }
336
+
337
+ if (!changed) return
338
+ mkdirSync(dirname(HERMES_CRON_STORE_PATH), { recursive: true })
339
+ writeFileSync(HERMES_CRON_STORE_PATH, `${JSON.stringify({ jobs, updated_at: now }, null, 2)}\n`, {
340
+ encoding: 'utf-8',
341
+ mode: 0o600,
342
+ })
343
+ chmodSync(HERMES_CRON_STORE_PATH, 0o600)
344
+ }
345
+
79
346
  function materializeRuntimeFiles() {
80
347
  const files = loadJson(RUNTIME_FILES_PATH, {})
81
348
  for (const [path, content] of Object.entries(files)) {
@@ -226,7 +493,7 @@ function startHermes() {
226
493
  const proc = spawn('hermes', ['gateway'], {
227
494
  env: {
228
495
  ...process.env,
229
- HERMES_HOME: process.env.HERMES_HOME ?? join(RUNNER_HOME, '.hermes'),
496
+ HERMES_HOME,
230
497
  },
231
498
  stdio: ['ignore', 'pipe', 'pipe'],
232
499
  cwd: '/workspace',
@@ -262,6 +529,7 @@ async function main() {
262
529
  materializeRuntimeFiles()
263
530
  materializeCredentialFiles()
264
531
  materializePluginRuntimeAssets()
532
+ syncTemplateRoutinesToHermesCron()
265
533
 
266
534
  if (process.env.SHADOW_RUNNER_VALIDATE_ONLY === '1') {
267
535
  verifyBinary('hermes', ['--version'])
@@ -61,6 +61,7 @@ RUN find node_modules -type d \( -name ".github" -o -name "test" -o -name "tests
61
61
  # TypeScript source changes.
62
62
  WORKDIR /workspace
63
63
  COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json .npmrc ./
64
+ COPY patches patches
64
65
  COPY packages/shared/package.json packages/shared/package.json
65
66
  COPY packages/sdk/package.json packages/sdk/package.json
66
67
  COPY packages/cli/package.json packages/cli/package.json
@@ -17,6 +17,9 @@ The current image already follows this model more closely than the ACP runners:
17
17
  ShadowOB connector CLI.
18
18
  - `entrypoint.mjs` reads `/etc/openclaw/config.json`.
19
19
  - Runtime extensions are read from `/etc/openclaw/runtime-extensions.json`.
20
+ - Template routines are read from `/etc/shadowob/template-routines.json` and
21
+ synced into the OpenClaw cron store before the gateway starts. The sync keeps a
22
+ managed spec hash so runtime-edited jobs are preserved on redeploy.
20
23
  - The generated OpenClaw config is written outside mutable state by default.
21
24
  - File logging goes to `/var/log/openclaw/entrypoint.log`.
22
25
  - Shadow slash command artifacts are surfaced through