@askexenow/exe-os 0.9.302 → 0.9.310

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 (311) hide show
  1. package/deploy/compose/.env.example +8 -2
  2. package/deploy/compose/README.md +7 -3
  3. package/deploy/compose/add-routes.sh +107 -0
  4. package/deploy/compose/docker-compose.yml +91 -18
  5. package/deploy/compose/gateway-watchdog.sh +223 -0
  6. package/deploy/compose/generate-env.ts +16 -4
  7. package/deploy/compose/setup.sh +7 -8
  8. package/deploy/stack-manifests/v0.9.json +125 -4
  9. package/dist/{active-agent-ODI6GGVD.js → active-agent-ORX47ZKW.js} +4 -3
  10. package/dist/{active-agent-GGQAP4QE.js → active-agent-S5T2YMOR.js} +4 -3
  11. package/dist/{agentic-ontology-M7T72M6V.js → agentic-ontology-6KBAXLSJ.js} +1 -1
  12. package/dist/{backfill-metadata-GSZP2BEW.js → backfill-metadata-FV7GX6AS.js} +5 -4
  13. package/dist/{behaviors-JM6KFSXL.js → behaviors-47CPEKJ7.js} +6 -5
  14. package/dist/bin/agentic-ontology-backfill.js +6 -5
  15. package/dist/bin/agentic-reflection-backfill.js +7 -6
  16. package/dist/bin/agentic-semantic-label.js +6 -5
  17. package/dist/bin/backfill-conversations.js +8 -7
  18. package/dist/bin/backfill-responses.js +8 -7
  19. package/dist/bin/backfill-vectors.js +10 -9
  20. package/dist/bin/bulk-sync-postgres.js +14 -13
  21. package/dist/bin/cc-doctor.js +5 -4
  22. package/dist/bin/cleanup-stale-review-tasks.js +13 -12
  23. package/dist/bin/cli.js +20 -17
  24. package/dist/bin/deferred-daemon-restart.js +4 -4
  25. package/dist/bin/exe-agent-config.js +3 -2
  26. package/dist/bin/exe-agent.js +12 -11
  27. package/dist/bin/exe-assign.js +10 -9
  28. package/dist/bin/exe-boot.js +21 -20
  29. package/dist/bin/exe-call.js +5 -4
  30. package/dist/bin/exe-cloud.js +14 -13
  31. package/dist/bin/exe-config-dump.js +525 -0
  32. package/dist/bin/exe-dispatch.js +13 -12
  33. package/dist/bin/exe-doctor.js +2 -2
  34. package/dist/bin/exe-export-behaviors.js +8 -7
  35. package/dist/bin/exe-forget.js +7 -6
  36. package/dist/bin/exe-gateway.js +22 -11
  37. package/dist/bin/exe-healthcheck.js +7 -4
  38. package/dist/bin/exe-heartbeat.js +13 -12
  39. package/dist/bin/exe-kill.js +16 -15
  40. package/dist/bin/exe-launch-agent.js +41 -25
  41. package/dist/bin/exe-new-employee.js +9 -8
  42. package/dist/bin/exe-pending-messages.js +14 -13
  43. package/dist/bin/exe-pending-notifications.js +13 -12
  44. package/dist/bin/exe-pending-reviews.js +13 -12
  45. package/dist/bin/exe-preflight.js +108 -0
  46. package/dist/bin/exe-rename.js +5 -4
  47. package/dist/bin/exe-review.js +15 -14
  48. package/dist/bin/exe-search-quality.js +282 -0
  49. package/dist/bin/exe-search.js +6 -5
  50. package/dist/bin/exe-session-cleanup.js +18 -17
  51. package/dist/bin/exe-settings.js +15 -14
  52. package/dist/bin/exe-start-codex.js +13 -12
  53. package/dist/bin/exe-start-opencode.js +9 -8
  54. package/dist/bin/exe-status.js +14 -13
  55. package/dist/bin/exe-support.js +4 -4
  56. package/dist/bin/exe-team.js +4 -3
  57. package/dist/bin/exe-timers.js +237 -0
  58. package/dist/bin/git-sweep.js +14 -13
  59. package/dist/bin/graph-backfill.js +7 -6
  60. package/dist/bin/graph-export.js +6 -5
  61. package/dist/bin/import-history.js +10 -9
  62. package/dist/bin/install-launchd.js +5 -2
  63. package/dist/bin/install.js +16 -15
  64. package/dist/bin/intercom-check.js +4 -4
  65. package/dist/bin/mcp-sessions.js +2 -2
  66. package/dist/bin/orchestration-metrics.js +5 -4
  67. package/dist/bin/postgres-agentic-reflection-backfill.js +2 -2
  68. package/dist/bin/postgres-agentic-semantic-backfill.js +1 -1
  69. package/dist/bin/prune-orphan-chunks.js +43 -10
  70. package/dist/bin/scan-tasks.js +13 -12
  71. package/dist/bin/setup.js +1 -1
  72. package/dist/bin/shard-migrate.js +5 -4
  73. package/dist/bin/stack-update.js +30 -5
  74. package/dist/bin/vps-health-gate.js +1 -1
  75. package/dist/{capability-cards-BCTFKT4G.js → capability-cards-WQVY7WSS.js} +3 -2
  76. package/dist/{capacity-monitor-B4S3XEDO.js → capacity-monitor-BUKYPSWR.js} +14 -13
  77. package/dist/{catchup-brief-I35KHK62.js → catchup-brief-X5IVQAOH.js} +16 -15
  78. package/dist/{chunk-74TUYU5A.js → chunk-255DIG4B.js} +1 -1
  79. package/dist/{chunk-CXKR5Z5F.js → chunk-2EJ636HI.js} +4 -0
  80. package/dist/{chunk-ST3X3YMZ.js → chunk-2NGPZCWX.js} +4 -4
  81. package/dist/{chunk-LD2JKEJT.js → chunk-2NVEKSSP.js} +7 -7
  82. package/dist/{chunk-V6P7TPPA.js → chunk-2UVN2NBN.js} +148 -2
  83. package/dist/{chunk-OZRO37JW.js → chunk-3KH6GXXX.js} +2 -2
  84. package/dist/{chunk-IMJNYREY.js → chunk-43R2GILO.js} +1 -1
  85. package/dist/{chunk-6X2YNVLB.js → chunk-4BANMHJ5.js} +3 -3
  86. package/dist/{chunk-QNJPDIWY.js → chunk-4L4R6AQJ.js} +1 -1
  87. package/dist/{chunk-QWLPAEG5.js → chunk-4Q5VNQTV.js} +3 -3
  88. package/dist/{chunk-VLSYKMCJ.js → chunk-4RJJ5FJ2.js} +14 -2
  89. package/dist/{chunk-4AN6KMVG.js → chunk-5DPE7B7K.js} +1 -1
  90. package/dist/{chunk-H7ZNSQ2E.js → chunk-62UZWGFX.js} +2 -2
  91. package/dist/{chunk-ZQ2QHGWE.js → chunk-66JPZELX.js} +20 -24
  92. package/dist/{chunk-BUGMW7YF.js → chunk-6LNSZADA.js} +11 -11
  93. package/dist/{chunk-PBCRUNIK.js → chunk-6OD2RVIA.js} +492 -157
  94. package/dist/chunk-7ODTYODO.js +350 -0
  95. package/dist/{chunk-5YL4Y27D.js → chunk-7WDIGRSO.js} +1 -1
  96. package/dist/{chunk-WIL6LXNG.js → chunk-AC6DKMVS.js} +1 -1
  97. package/dist/{chunk-HZEAAKCR.js → chunk-B2J3BBJC.js} +3 -3
  98. package/dist/{chunk-LE7CDIOA.js → chunk-BUOMSIXH.js} +1 -1
  99. package/dist/{chunk-XKGNKH6V.js → chunk-DFUCFLZH.js} +1 -1
  100. package/dist/{chunk-U724Z3S3.js → chunk-DKHBMIVE.js} +100 -37
  101. package/dist/{chunk-MY36XDUK.js → chunk-DNFU56ZS.js} +1 -1
  102. package/dist/{chunk-GSWGOGP5.js → chunk-EOS7VPVT.js} +5 -5
  103. package/dist/{chunk-P33JVGSP.js → chunk-FM7XZDZI.js} +1 -1
  104. package/dist/chunk-G6EJKYK3.js +33 -0
  105. package/dist/{chunk-OMCNFTSI.js → chunk-GMZMBLHT.js} +1 -1
  106. package/dist/{chunk-4LZ54YGA.js → chunk-GSY6HL3T.js} +5 -5
  107. package/dist/{chunk-OVKNFTCL.js → chunk-HED6D5KR.js} +132 -21
  108. package/dist/{chunk-ALH6QLYF.js → chunk-HQODUV3G.js} +2 -2
  109. package/dist/{chunk-US64UR5O.js → chunk-IIHE3FNC.js} +3 -3
  110. package/dist/{chunk-H2ISTRWU.js → chunk-IRIPGGGL.js} +1 -1
  111. package/dist/{chunk-Z3EXECOX.js → chunk-ISILDO7P.js} +1 -1
  112. package/dist/{chunk-6ERZFDBS.js → chunk-IZPO6IOZ.js} +1 -1
  113. package/dist/{chunk-W65Z4SJS.js → chunk-J3QZPDNH.js} +1 -1
  114. package/dist/{chunk-3IJCP6ZH.js → chunk-JDYO76PI.js} +1 -1
  115. package/dist/{chunk-ORNKGGFX.js → chunk-JNARUXDC.js} +1 -1
  116. package/dist/{chunk-VME3UH4D.js → chunk-JV225JBX.js} +1 -1
  117. package/dist/{chunk-EI7PBUIA.js → chunk-JZW5AOWI.js} +1 -1
  118. package/dist/{chunk-TLOILHSL.js → chunk-K7KBAJQL.js} +36 -18
  119. package/dist/{chunk-RHMWSUJB.js → chunk-KAVUTP5G.js} +2 -2
  120. package/dist/{chunk-7FNELLMX.js → chunk-KPVVTHPK.js} +8 -8
  121. package/dist/{chunk-35U72CD2.js → chunk-KSMITUGY.js} +3 -3
  122. package/dist/{chunk-D6UICP3C.js → chunk-LMX7UKGX.js} +81 -3
  123. package/dist/{chunk-7ZWC5HVG.js → chunk-MTIJ2NXY.js} +1 -1
  124. package/dist/{chunk-KVHPBQV4.js → chunk-NCC3F6GM.js} +1 -1
  125. package/dist/{chunk-AYMSIV7X.js → chunk-NIZQ3CIS.js} +1 -1
  126. package/dist/{chunk-LTI2DLHY.js → chunk-OBJXZII2.js} +2 -2
  127. package/dist/{chunk-WHOT5NZ7.js → chunk-OR3TAZL3.js} +14 -2
  128. package/dist/{chunk-RQUHAXMM.js → chunk-PHFPIUWU.js} +2 -0
  129. package/dist/{chunk-N76VTMMF.js → chunk-PMEOSD6S.js} +2 -2
  130. package/dist/{chunk-TCJEFW5V.js → chunk-PPXQE7AS.js} +4 -4
  131. package/dist/{chunk-KHZQVLNZ.js → chunk-QE7UVQDP.js} +46 -0
  132. package/dist/{chunk-SXJACGQ5.js → chunk-QEGXNJGR.js} +4 -4
  133. package/dist/{chunk-W3E4VEEJ.js → chunk-RPBTYQKJ.js} +1 -1
  134. package/dist/chunk-RQ7OVXUZ.js +81 -0
  135. package/dist/{chunk-7L2EV3XX.js → chunk-RVXWGPD3.js} +39 -6
  136. package/dist/{chunk-YNALY3SX.js → chunk-RZJ6DXVE.js} +1 -1
  137. package/dist/{chunk-P3SIFDES.js → chunk-SCYWCWKY.js} +16 -4
  138. package/dist/{chunk-7I43GVER.js → chunk-SOT23KWW.js} +3 -3
  139. package/dist/{chunk-T62GAN7I.js → chunk-SPOXL7HF.js} +7 -7
  140. package/dist/{chunk-3YJMI422.js → chunk-TN2YCFQE.js} +4 -4
  141. package/dist/{chunk-YMSWCERY.js → chunk-U63BZI2B.js} +3 -3
  142. package/dist/{chunk-KA5RK5HZ.js → chunk-V42BSVX6.js} +2 -2
  143. package/dist/{chunk-PJTRAFL4.js → chunk-V4BW5HFQ.js} +3 -3
  144. package/dist/{chunk-4FG6IX7U.js → chunk-VFSYVQX4.js} +1 -1
  145. package/dist/chunk-VQRTO7IS.js +290 -0
  146. package/dist/{chunk-OR7J7TIA.js → chunk-VT6P7FV4.js} +1 -1
  147. package/dist/{chunk-BEIYF4NS.js → chunk-W2J2RF5N.js} +3 -3
  148. package/dist/{chunk-Y5ASPRP7.js → chunk-WBJXJ2R4.js} +17 -13
  149. package/dist/chunk-WCGNCH3Q.js +72 -0
  150. package/dist/{chunk-2LJDLQNO.js → chunk-WJ2PZGRV.js} +1 -1
  151. package/dist/{chunk-AAPPSMPH.js → chunk-WZBUKC5N.js} +1 -1
  152. package/dist/{chunk-RO7EXVSJ.js → chunk-X4AFBL4N.js} +1 -1
  153. package/dist/{chunk-BR74NYEF.js → chunk-XV5HRIXG.js} +5 -5
  154. package/dist/chunk-XXJRZ75E.js +43 -0
  155. package/dist/{chunk-VDHUHXFF.js → chunk-XYG722JW.js} +2 -2
  156. package/dist/{chunk-MM7SWUQ4.js → chunk-YB7U4WTY.js} +4 -4
  157. package/dist/{chunk-RMRZXPUU.js → chunk-Z6H6Y4BS.js} +1 -1
  158. package/dist/{token-budget-UMHRRDBT.js → chunk-ZOVHXTGC.js} +2 -9
  159. package/dist/{chunk-HNLBHP6P.js → chunk-ZZXADKAP.js} +3 -3
  160. package/dist/{co-activation-OKAHEMPE.js → co-activation-WN25AJOC.js} +3 -2
  161. package/dist/{co-occurrence-IHSPB53O.js → co-occurrence-YP2Q3OEA.js} +5 -4
  162. package/dist/{code-context-index-KGY4LKCA.js → code-context-index-ZWONKBKX.js} +4 -4
  163. package/dist/{conversation-entity-extractor-KXJSHRGJ.js → conversation-entity-extractor-KYOBXNID.js} +2 -2
  164. package/dist/{crdt-sync-K2G3C6XY.js → crdt-sync-ZFIFL2QM.js} +1 -1
  165. package/dist/{crm-webhook-ERZSQ3CW.js → crm-webhook-DGPHULOO.js} +2 -2
  166. package/dist/{cto-delegation-gate-ZCJ53LBQ.js → cto-delegation-gate-OF2A3TSS.js} +12 -11
  167. package/dist/daemon-auth-N6P2MZAV.js +17 -0
  168. package/dist/{daemon-orchestration-V6HALVDS.js → daemon-orchestration-G4RRTPP7.js} +16 -15
  169. package/dist/daemon-ready-signal-3L2MJT7C.js +13 -0
  170. package/dist/{db-backup-VZHSFUVA.js → db-backup-EORG4LBO.js} +1 -1
  171. package/dist/{doc-graph-extractor-LQ2IFND2.js → doc-graph-extractor-APZS3DFD.js} +5 -4
  172. package/dist/{dreaming-QAKUASYV.js → dreaming-Q4E3WBGY.js} +13 -12
  173. package/dist/{exe-drift-H3UWNFFY.js → exe-drift-CI5R6GUT.js} +4 -3
  174. package/dist/{exe-export-MN5D4JMT.js → exe-export-HQIQASIT.js} +7 -6
  175. package/dist/{exe-import-IFFMEVD7.js → exe-import-ULZK56WC.js} +7 -6
  176. package/dist/{exe-key-RSZC72QK.js → exe-key-ILNSBGRN.js} +5 -4
  177. package/dist/{exe-snapshot-I622MPLS.js → exe-snapshot-RUZTWZOF.js} +16 -15
  178. package/dist/{factory-CTFAGZUA.js → factory-YHQZ6RZZ.js} +14 -6
  179. package/dist/{fast-db-init-QEFVT7Q3.js → fast-db-init-KNJRL7T3.js} +1 -1
  180. package/dist/{founder-context-3SOKEW76.js → founder-context-QCTMEK66.js} +2 -2
  181. package/dist/gateway/index.js +12 -11
  182. package/dist/{git-staleness-E7RPQA5U.js → git-staleness-KIA5YI4H.js} +3 -2
  183. package/dist/{git-task-sweep-D3A2C5BW.js → git-task-sweep-WH6QE7HL.js} +13 -12
  184. package/dist/{global-procedures-POG4V6IK.js → global-procedures-SYZZCGDS.js} +4 -3
  185. package/dist/{graph-auto-extract-E7KALNWA.js → graph-auto-extract-J2TIWZEE.js} +5 -4
  186. package/dist/{graph-rag-3RZN7JR3.js → graph-rag-HITZZ63L.js} +2 -2
  187. package/dist/hooks/bug-report-worker.js +15 -14
  188. package/dist/hooks/codex-stop-task-finalizer.js +15 -14
  189. package/dist/hooks/commit-complete.js +15 -14
  190. package/dist/hooks/error-recall.js +8 -7
  191. package/dist/hooks/exe-heartbeat-hook.js +4 -3
  192. package/dist/hooks/ingest-worker.js +4 -4
  193. package/dist/hooks/ingest.js +8 -7
  194. package/dist/hooks/instructions-loaded.js +5 -4
  195. package/dist/hooks/manifest.json +20 -20
  196. package/dist/hooks/notification.js +5 -4
  197. package/dist/hooks/post-compact.js +14 -13
  198. package/dist/hooks/post-tool-combined.js +7 -7
  199. package/dist/hooks/pre-compact.js +19 -18
  200. package/dist/hooks/pre-tool-use.js +18 -17
  201. package/dist/hooks/prompt-submit.js +28 -27
  202. package/dist/hooks/session-end.js +24 -23
  203. package/dist/hooks/session-start.js +46 -22
  204. package/dist/hooks/stop.js +22 -21
  205. package/dist/hooks/subagent-stop.js +14 -13
  206. package/dist/hooks/summary-worker.js +22 -21
  207. package/dist/index.js +25 -23
  208. package/dist/{installer-IXUX73JN.js → installer-3NB3IBHF.js} +10 -9
  209. package/dist/{installer-RCUPPZ7Y.js → installer-IIJFCTMH.js} +10 -9
  210. package/dist/{installer-HDD6OH5Z.js → installer-PSDYMGN7.js} +7 -6
  211. package/dist/lib/cloud-sync.js +14 -13
  212. package/dist/lib/consolidation.js +8 -7
  213. package/dist/lib/database.js +3 -2
  214. package/dist/lib/db-daemon-client.js +4 -4
  215. package/dist/lib/db.js +3 -2
  216. package/dist/lib/device-registry.js +3 -1
  217. package/dist/lib/embedder.js +4 -4
  218. package/dist/lib/employee-templates.js +5 -4
  219. package/dist/lib/employees.js +3 -2
  220. package/dist/lib/exe-daemon-client.js +3 -3
  221. package/dist/lib/exe-daemon.js +256 -406
  222. package/dist/lib/hybrid-search.js +6 -5
  223. package/dist/lib/identity.js +3 -2
  224. package/dist/lib/license.js +2 -2
  225. package/dist/lib/messaging.js +13 -12
  226. package/dist/lib/reminders.js +4 -3
  227. package/dist/lib/schedules.js +6 -5
  228. package/dist/lib/session-registry.js +5 -4
  229. package/dist/lib/skill-learning.js +7 -6
  230. package/dist/lib/store.js +7 -4
  231. package/dist/lib/task-router.js +4 -3
  232. package/dist/lib/tasks.js +14 -13
  233. package/dist/lib/tmux-routing.js +16 -11
  234. package/dist/lib/token-spend.js +4 -3
  235. package/dist/{license-gate-V2UCK4KJ.js → license-gate-7SDHCJUO.js} +3 -3
  236. package/dist/mcp/register-tools.js +74 -73
  237. package/dist/mcp/server.js +97 -79
  238. package/dist/mcp/tools/complete-reminder.js +5 -4
  239. package/dist/mcp/tools/create-reminder.js +5 -4
  240. package/dist/mcp/tools/create-task.js +16 -15
  241. package/dist/mcp/tools/deactivate-behavior.js +8 -7
  242. package/dist/mcp/tools/list-reminders.js +5 -4
  243. package/dist/mcp/tools/list-tasks.js +16 -15
  244. package/dist/mcp/tools/send-message.js +15 -14
  245. package/dist/mcp/tools/update-task.js +15 -14
  246. package/dist/{mcp-http-config-7LFFPNGV.js → mcp-http-config-QSOVFXHW.js} +4 -3
  247. package/dist/mcp-tool-surface-CSPFXL3F.js +15 -0
  248. package/dist/{memory-cards-BGI6MJRN.js → memory-cards-II67WFL2.js} +3 -2
  249. package/dist/{memory-graph-extractor-DTRRNWHZ.js → memory-graph-extractor-PM4Q2KJG.js} +6 -5
  250. package/dist/{memory-poisoning-defense-MOCWJK6S.js → memory-poisoning-defense-ECXSX7HK.js} +3 -2
  251. package/dist/{memory-queue-client-DD4E5SIZ.js → memory-queue-client-HLURQEV6.js} +4 -4
  252. package/dist/{memory-reflection-UKKTGZYR.js → memory-reflection-PXF6YFDU.js} +3 -2
  253. package/dist/{notifications-AMJ2K3MM.js → notifications-UP7WQGG4.js} +12 -11
  254. package/dist/{orchestration-events-MB4QVHTD.js → orchestration-events-PZLLPCKY.js} +4 -3
  255. package/dist/orchestration-health-read-EJWVUARG.js +191 -0
  256. package/dist/{orchestrator-M7DJH6TN.js → orchestrator-PUT45J4O.js} +14 -13
  257. package/dist/{pipeline-router-4LT5YG5P.js → pipeline-router-5CRWKESM.js} +4 -3
  258. package/dist/{plan-limits-ENS7OJWP.js → plan-limits-SH2NDFNX.js} +6 -5
  259. package/dist/{project-boot-6W5JPGSB.js → project-boot-2TCPDZ4S.js} +25 -1
  260. package/dist/{projection-worker-WWXZK7QD.js → projection-worker-HWY37MNC.js} +2 -2
  261. package/dist/{prospective-memory-VNGLPMS6.js → prospective-memory-FQSVMCGG.js} +3 -2
  262. package/dist/{reranker-6XBS55FA.js → reranker-AALRMGMM.js} +1 -1
  263. package/dist/{retrieval-health-ATX5TIQ2.js → retrieval-health-PNWJSJNG.js} +1 -1
  264. package/dist/{review-polling-E4ALHTQE.js → review-polling-65JZUE3T.js} +13 -12
  265. package/dist/runtime/index.js +24 -22
  266. package/dist/{session-events-3J6APMKN.js → session-events-EJ3OSAA6.js} +13 -12
  267. package/dist/session-kill-telemetry-3HGM7SNQ.js +63 -0
  268. package/dist/{session-scope-E54VIYTJ.js → session-scope-JGDYAMJO.js} +12 -11
  269. package/dist/{setup-wizard-XV736U5D.js → setup-wizard-44LMFM2C.js} +1 -1
  270. package/dist/shared-rate-limit-store-QZPQEVE2.js +80 -0
  271. package/dist/{skill-refinement-FTURVWFN.js → skill-refinement-DUOO6B7V.js} +3 -2
  272. package/dist/{stack-release-JZVTSBJJ.js → stack-release-XYMKU7NT.js} +53 -4
  273. package/dist/{stack-update-BIK2QHOE.js → stack-update-XHQJRJEP.js} +5 -3
  274. package/dist/{steward-gate-YGU4VXPP.js → steward-gate-6NWRDLX2.js} +4 -3
  275. package/dist/{task-enforcement-GY3VTY6L.js → task-enforcement-UOULTGFB.js} +12 -11
  276. package/dist/{task-scope-PYOK7OQ6.js → task-scope-P5STKYAC.js} +12 -11
  277. package/dist/{tasks-crud-HLJEJ2UU.js → tasks-crud-24HMTULR.js} +12 -11
  278. package/dist/{tasks-notify-NGS3DWUJ.js → tasks-notify-LAIU7UUI.js} +13 -12
  279. package/dist/{tasks-review-BP5V2X34.js → tasks-review-AFJAU2DY.js} +12 -11
  280. package/dist/{telemetry-upload-AXYPFAPS.js → telemetry-upload-DWFGC2XJ.js} +8 -7
  281. package/dist/token-budget-4RFZKWGH.js +16 -0
  282. package/dist/{tool-capability-index-RHPVMMDD.js → tool-capability-index-ZEXBWKUZ.js} +1 -1
  283. package/dist/{tool-telemetry-BLSVABFJ.js → tool-telemetry-BS5JXM5M.js} +1 -1
  284. package/dist/tui/App.js +21 -20
  285. package/dist/{tui-data-EYPOFYLS.js → tui-data-UNO5B7PU.js} +12 -11
  286. package/dist/{worker-gate-SMFOBMMX.js → worker-gate-C6CTTYMD.js} +1 -1
  287. package/dist/{workflow-engine-TDJZ3TXB.js → workflow-engine-4W2KYCDV.js} +2 -2
  288. package/dist/{worktree-XISIYRM4.js → worktree-2CXY6NEB.js} +5 -4
  289. package/dist/{worktree-sweep-AS6GAR6X.js → worktree-sweep-K3CJFSUL.js} +6 -5
  290. package/package.json +9 -4
  291. package/release-notes.json +17 -88
  292. package/stack.release.json +51 -4
  293. package/dist/chunk-PHOQAVTK.js +0 -42
  294. package/dist/daemon-auth-BK7ON5ZH.js +0 -13
  295. package/dist/session-kill-telemetry-KOAHLRBT.js +0 -31
  296. package/dist/{chunk-XDE5IGBX.js → chunk-2NS3QRMJ.js} +0 -0
  297. package/dist/{chunk-JTMKRUX6.js → chunk-A2YZG4AD.js} +0 -0
  298. package/dist/{chunk-UZATXREL.js → chunk-AE74NOE6.js} +0 -0
  299. package/dist/{chunk-LUGWK6QB.js → chunk-FF5DA4XP.js} +3 -3
  300. /package/dist/{chunk-BGOMFGSW.js → chunk-GWPICNSO.js} +0 -0
  301. /package/dist/{chunk-NLHUA3YB.js → chunk-I3DPCSDG.js} +0 -0
  302. /package/dist/{chunk-YM3367VC.js → chunk-J3SR5MGG.js} +0 -0
  303. /package/dist/{chunk-FKFVNV7X.js → chunk-K4BKJJ4F.js} +0 -0
  304. /package/dist/{chunk-PTDBHQX7.js → chunk-MQCTK6EF.js} +0 -0
  305. /package/dist/{chunk-GBV5PV5T.js → chunk-QNXM2HXW.js} +0 -0
  306. /package/dist/{chunk-GZSG2ZOX.js → chunk-YUBTHJVV.js} +0 -0
  307. /package/dist/{core-memory-4OM7LR77.js → core-memory-OC5ITKKV.js} +0 -0
  308. /package/dist/{entity-boost-SFHIRXT5.js → entity-boost-NF3LE6OJ.js} +0 -0
  309. /package/dist/{message-queue-client-QTC3VHJT.js → message-queue-client-4VDU6BZY.js} +0 -0
  310. /package/dist/{oauth-server-BPN55EJM.js → oauth-server-EWUL7DMD.js} +0 -0
  311. /package/dist/{wiki-acl-45SIGG4O.js → wiki-acl-CONNF6ES.js} +0 -0
@@ -123,8 +123,14 @@ EXE_GATEWAY_WHATSAPP_VERIFY_TOKEN=CHANGEME_EXE_GATEWAY_WHATSAPP_VERIFY_TOKEN
123
123
  WHATSAPP_ACCESS_TOKEN=CHANGEME_WHATSAPP_ACCESS_TOKEN
124
124
  API_ROUTER_URL=https://gateway.askexe.com
125
125
  API_ROUTER_KEY=CHANGEME_API_ROUTER_KEY
126
- # BYOK: to use your own API keys instead of the Exe API Router,
127
- # set BYOK_ENABLED=true and provide ANTHROPIC_API_KEY below.
126
+ # Billing enforcement contract (bug df8823d0). These are the vars the gateway
127
+ # ACTUALLY reads to decide metered vs BYOK. Default: metered (fail-closed) —
128
+ # direct provider keys are rejected and all AI traffic is billed via the router.
129
+ EXE_BILLING_MODE=metered
130
+ EXE_ALLOW_DIRECT_PROVIDER_KEYS=false
131
+ # BYOK: to use your own API keys instead of the Exe API Router, set
132
+ # BYOK_ENABLED=true (the customer-facing alias for EXE_BILLING_MODE=byok +
133
+ # EXE_ALLOW_DIRECT_PROVIDER_KEYS=true) and provide ANTHROPIC_API_KEY below.
128
134
  # BYOK_ENABLED=false
129
135
  # ANTHROPIC_API_KEY=CHANGEME_ANTHROPIC_API_KEY
130
136
  GATEWAY_HTTP_HOST_PORT=3100
@@ -6,9 +6,13 @@ Customer VPS deployments (Hygo/High/etc.) must start from `.env.customer.example
6
6
 
7
7
  Full production stack for a single client VPS: CRM + wiki + gateway + exed
8
8
  backed by Postgres, ClickHouse, and Redis. Pinned image tags, healthchecks on
9
- every service, named volumes for persistence, and host-nginx-friendly port
9
+ all core services, named volumes for persistence, and host-nginx-friendly port
10
10
  publishing on `127.0.0.1`.
11
11
 
12
+ > **Note:** `exe-otel-collector` does not have a container-level healthcheck
13
+ > (scratch image with no shell) — it exposes a health extension on `:13133`
14
+ > which is monitored externally.
15
+
12
16
  This is the **Lane A-2** deliverable from the v1.6 execution plan
13
17
  (`exe/output/v16-execution-plan.md`). Pairs with **Lane A-1** Ansible roles
14
18
  which provision the host and install nginx-tls; the `nginx-tls` role includes
@@ -56,8 +60,8 @@ nginx (Lane A-1 `nginx-tls`) can reverse-proxy them. Override the host port via
56
60
 
57
61
  `exe-monitor-agent` reports host/container health to the monitoring hub, but it
58
62
  needs **sensitive read-only host access** to do so: `/var/run/docker.sock`
59
- (root-equivalent — can inspect every container), `/proc`, `/sys`, and
60
- `/etc/os-release`. Because that access is effectively root on the box, the agent
63
+ (root-equivalent — can inspect every container) and `/etc/os-release`.
64
+ Because that access is effectively root on the box, the agent
61
65
  is **gated behind the `monitor-agent` compose profile** and does **not** start by
62
66
  default.
63
67
 
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ # exe-os overlay-apply / post-deploy hook (add-routes.sh)
3
+ #
4
+ # Re-applies CUSTOMER OVERLAY files into the exe-gateway container after every
5
+ # image upgrade / container recreate, then reloads the gateway so the overlays
6
+ # take effect.
7
+ #
8
+ # ─────────────────────────────────────────────────────────────────────────────
9
+ # BUG HISTORY (02bc1bb8, P1)
10
+ # ─────────────────────────────────────────────────────────────────────────────
11
+ # On a stack image upgrade the gateway container is RECREATED. Anything that lived
12
+ # only inside the old container's writable layer (a customer overlay such as
13
+ # po-intake.js, copied in by hand) is DROPPED with no warning. HYGO's working
14
+ # po-intake.js overlay — 37 orders processed — was killed on v0.9.17 → v0.9.22.
15
+ # The intended post-deploy hook (this file) shipped as a 0-BYTE empty file, and
16
+ # /opt/exe-stack/overlay/ existed on the host but was never mounted into the
17
+ # container, so nothing re-applied it.
18
+ #
19
+ # This script + the compose bind-mount (`./overlay:/data/overlay:ro`) close the
20
+ # gap two ways:
21
+ # (a) overlays survive recreate via the persistent host bind-mount, AND
22
+ # (b) this hook re-applies them into the container's app dir + reloads after
23
+ # every recreate. Idempotent + safe: a no-op when no overlays exist.
24
+ #
25
+ # Mechanism:
26
+ # Host overlay dir : ${OVERLAY_DIR:-/opt/exe-stack/overlay}
27
+ # In-container view : /data/overlay (read-only bind-mount, always present)
28
+ # Applied into : ${OVERLAY_TARGET:-/app/overlay} inside the container
29
+ #
30
+ # Overlays are loaded by the gateway from EXE_GATEWAY_OVERLAY_DIR (defaults to
31
+ # /data/overlay — the bind-mount — so on a modern gateway no copy is even needed;
32
+ # the copy below is belt-and-suspenders for gateway builds that load from /app).
33
+
34
+ set -uo pipefail
35
+
36
+ STACK_DIR="${EXE_STACK_DIR:-/opt/exe-stack}"
37
+ OVERLAY_DIR="${OVERLAY_DIR:-${STACK_DIR}/overlay}"
38
+ CONTAINER="${EXE_GATEWAY_CONTAINER:-exe-gateway}"
39
+ # Where the gateway image expects overlays if it does NOT read the bind-mount directly.
40
+ OVERLAY_TARGET="${OVERLAY_TARGET:-/app/overlay}"
41
+
42
+ ts() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
43
+ log() { echo "[$(ts)] add-routes: $*"; }
44
+
45
+ # Nothing to do if there is no overlay dir or it is empty. This is the common
46
+ # (no customer overlay) case — exit cleanly so the post-deploy step is a no-op.
47
+ if [[ ! -d "$OVERLAY_DIR" ]]; then
48
+ log "no overlay dir at ${OVERLAY_DIR} — nothing to apply."
49
+ exit 0
50
+ fi
51
+
52
+ shopt -s nullglob dotglob
53
+ overlay_files=("$OVERLAY_DIR"/*)
54
+ shopt -u nullglob dotglob
55
+ if [[ ${#overlay_files[@]} -eq 0 ]]; then
56
+ log "overlay dir ${OVERLAY_DIR} is empty — nothing to apply."
57
+ exit 0
58
+ fi
59
+
60
+ # Gateway must be running to receive the overlay copy + reload.
61
+ running=$(docker inspect --format '{{.State.Running}}' "$CONTAINER" 2>/dev/null || echo "false")
62
+ if [[ "$running" != "true" ]]; then
63
+ log "WARN gateway container ${CONTAINER} is not running — cannot apply overlays now. They remain at ${OVERLAY_DIR} and the read-only bind-mount (/data/overlay) still exposes them once the gateway is up."
64
+ exit 0
65
+ fi
66
+
67
+ log "applying ${#overlay_files[@]} overlay file(s) from ${OVERLAY_DIR} into ${CONTAINER}:${OVERLAY_TARGET}"
68
+
69
+ # Ensure the in-container target dir exists, then copy each overlay file in.
70
+ # `docker cp` works even on a read-only-bind-mounted source because we copy from
71
+ # the HOST path, not from the (ro) /data/overlay mount.
72
+ docker exec "$CONTAINER" mkdir -p "$OVERLAY_TARGET" >/dev/null 2>&1 || {
73
+ log "ERROR could not create ${OVERLAY_TARGET} inside ${CONTAINER}."
74
+ exit 1
75
+ }
76
+
77
+ applied=0
78
+ for f in "${overlay_files[@]}"; do
79
+ [[ -f "$f" ]] || continue # skip sub-directories for the flat copy
80
+ base=$(basename "$f")
81
+ if docker cp "$f" "${CONTAINER}:${OVERLAY_TARGET}/${base}" >/dev/null 2>&1; then
82
+ log " applied ${base}"
83
+ applied=$((applied + 1))
84
+ else
85
+ log " ERROR failed to apply ${base}"
86
+ fi
87
+ done
88
+
89
+ if [[ "$applied" -eq 0 ]]; then
90
+ log "no overlay files applied."
91
+ exit 0
92
+ fi
93
+
94
+ # Reload the gateway so the overlays take effect. A graceful in-container reload is
95
+ # preferred; fall back to a container restart if the image exposes no reload hook.
96
+ # (The restart here is INTENTIONAL + one-shot after a deploy — not the watchdog
97
+ # loop — and only happens when overlays were actually applied.)
98
+ if docker exec "$CONTAINER" sh -c 'test -x /app/reload.sh' >/dev/null 2>&1; then
99
+ log "reloading gateway via /app/reload.sh"
100
+ docker exec "$CONTAINER" /app/reload.sh >/dev/null 2>&1 || log "WARN /app/reload.sh exited non-zero"
101
+ else
102
+ log "no in-container reload hook; restarting ${CONTAINER} once to load overlays"
103
+ docker restart "$CONTAINER" >/dev/null 2>&1 || log "WARN docker restart ${CONTAINER} failed"
104
+ fi
105
+
106
+ log "applied ${applied} overlay file(s)."
107
+ exit 0
@@ -3,10 +3,10 @@
3
3
  # Services: exe-db (postgres) + clickhouse + redis + exe-crm + exe-wiki + exe-os + exe-gateway + exe-erp + otel-collector
4
4
  # ONE postgres (exe-db) — all services connect to it via DATABASE_URL.
5
5
  # Optional: exe-monitor-agent reports fleet health to the monitoring hub. It needs
6
- # sensitive host access (docker.sock, /proc, /sys) so it is gated behind the
6
+ # sensitive host access (docker.sock, /etc/os-release) so it is gated behind the
7
7
  # "monitor-agent" compose profile and stays OFF until the operator consents.
8
8
  # See deploy/monitor/HOST-ACCESS-CONSENT.md.
9
- # All image tags pinned per-client via .env (no :latest). Healthchecks on every service.
9
+ # All image tags pinned per-client via .env (no :latest). Healthchecks on all core services.
10
10
  # Named volumes for state; explicit subnets; depends_on with service_healthy gates.
11
11
  #
12
12
  # Validate without secrets:
@@ -26,7 +26,10 @@ services:
26
26
  # ------------------------------------------------------------------
27
27
 
28
28
  exe-db:
29
- image: ${EXE_DB_IMAGE:-pgvector/pgvector:pg16}
29
+ # bug ee16e7ef: pin a versioned tag, not the mutable :pg16 rolling tag.
30
+ # pgvector/pgvector:pg16 can change upstream without notice; 0.8.0-pg16
31
+ # is the tested stable release. Override via EXE_DB_IMAGE in .env.
32
+ image: ${EXE_DB_IMAGE:-pgvector/pgvector:0.8.0-pg16@sha256:a132765ec351c65111b5b675928a3a0515a466a40f97277329db8b8209ad8bc9}
30
33
  container_name: exe-db
31
34
  restart: unless-stopped
32
35
  # SECURITY (bug 67d62490): no blanket `env_file: .env`. Each service
@@ -78,7 +81,7 @@ services:
78
81
  options: { max-size: "10m", max-file: "3" }
79
82
 
80
83
  clickhouse:
81
- image: clickhouse/clickhouse-server:24.8.4.13-alpine
84
+ image: clickhouse/clickhouse-server:24.8.4.13-alpine@sha256:88a45f9e328549b2579256c46ee38e5c0e25ae58303d9eb6d9c7ed8d6d2bbf3c
82
85
  container_name: clickhouse
83
86
  restart: unless-stopped
84
87
  # SECURITY (bug 67d62490): no blanket `env_file: .env`. Each service
@@ -119,7 +122,7 @@ services:
119
122
  options: { max-size: "10m", max-file: "3" }
120
123
 
121
124
  redis:
122
- image: redis:7.4-alpine
125
+ image: redis:7.4-alpine@sha256:6ab0b6e7381779332f97b8ca76193e45b0756f38d4c0dcda72dbb3c32061ab99
123
126
  container_name: redis
124
127
  restart: unless-stopped
125
128
  # SECURITY (bug 67d62490): no blanket `env_file: .env`. Each service
@@ -236,17 +239,21 @@ services:
236
239
  image: ${MONITOR_HUB_IMAGE_TAG:-ghcr.io/askexe/exe-monitor-hub:v0.9.4}
237
240
  container_name: exe-monitor-hub
238
241
  restart: unless-stopped
239
- # bug d3d49101: PocketBase's data dir for this hub is a RELATIVE path
240
- # (DefaultDataDir="exe-monitor_data", see exe-monitor internal/cmd/hub).
241
- # The scratch image runs the binary from / with no WORKDIR, so it would
242
- # persist to /exe-monitor_data NOT /app/pb_data and NOT /beszel_data.
243
- # Pin it explicitly with --dir=/beszel_data so the binary writes to the same
244
- # path the named volume is mounted at (and the same path the image VOLUME
245
- # declares), so data survives container recreation.
242
+ # bug d3d49101 / 1fdb8be9: PocketBase's data dir for this hub is a RELATIVE
243
+ # path (DefaultDataDir="exe-monitor_data", see exe-monitor internal/cmd/hub).
244
+ # The image's WORKDIR is /app, so DefaultDataDir resolves to /app/exe-monitor_data.
245
+ # Pin it explicitly with --dir=/app/exe-monitor_data so the binary writes to
246
+ # the same path the named volume is mounted at, so data survives container
247
+ # recreation. Previous bug: mount was at /beszel_data but data went to
248
+ # /app/exe-monitor_data writable layer wiped on every recreate.
246
249
  # bug 182c6da3 / a125785d: EXE_MONITOR_KEY enables /api/exe-monitor/errors
247
250
  # ingestion (fail-closed in the hub when unset) and GATEWAY_ALERT_URL enables
248
251
  # error-spike alerts (silently disabled in the hub when unset).
249
- command: ["serve", "--http=0.0.0.0:8090", "--dir=/beszel_data"]
252
+ # bug 1fdb8be9: the custom hub binary resolves DefaultDataDir="exe-monitor_data"
253
+ # relative to the WORKDIR (/app), so actual data lives at /app/exe-monitor_data.
254
+ # --dir must point there (NOT /beszel_data) and the volume must mount there too,
255
+ # otherwise container recreates wipe all monitor history.
256
+ command: ["serve", "--http=0.0.0.0:8090", "--dir=/app/exe-monitor_data"]
250
257
  environment:
251
258
  EXE_MONITOR_ADMIN_TOKEN: ${EXE_MONITOR_ADMIN_TOKEN:-}
252
259
  # bug a125785d: shared secret for POST /api/exe-monitor/errors. The hub
@@ -266,7 +273,11 @@ services:
266
273
  ports:
267
274
  - "127.0.0.1:${MONITOR_HUB_PORT:-8090}:8090"
268
275
  volumes:
269
- - monitor_hub_data:/beszel_data
276
+ # bug 1fdb8be9: mount at the ACTUAL data dir the hub binary writes to.
277
+ # Was /beszel_data (stock Beszel path), but the exe-monitor fork uses
278
+ # /app/exe-monitor_data. Mismatch meant data lived in the writable layer
279
+ # and was lost on every container recreate / stack-update.
280
+ - monitor_hub_data:/app/exe-monitor_data
270
281
  networks:
271
282
  - backend
272
283
  healthcheck:
@@ -329,6 +340,11 @@ services:
329
340
  NODE_PORT: "3000"
330
341
  EXE_LICENSE_KEY: ${EXE_LICENSE_KEY:?EXE_LICENSE_KEY is required — purchase at https://askexe.com}
331
342
  SERVER_URL: ${CRM_SERVER_URL:-https://crm.askexe.com}
343
+ # bug b99d3762 / 83192765: the CRM error-forwarder POSTs to the monitor hub and must
344
+ # send X-Monitor-Key once the hub requires it on every path (mirrors gateway/erp).
345
+ ERROR_REPORTING_ENABLED: "true"
346
+ MONITOR_ERROR_URL: http://exe-monitor-hub:8090/api/exe-monitor/errors
347
+ MONITOR_API_KEY: ${MONITOR_API_KEY:-${EXE_MONITOR_KEY:-}}
332
348
  APP_SECRET: ${CRM_APP_SECRET:?CRM_APP_SECRET is required}
333
349
  EXE_CRM_ADMIN_TOKEN: ${EXE_CRM_ADMIN_TOKEN:-}
334
350
  EXE_CRM_ADMIN_EMAIL: ${EXE_CRM_ADMIN_EMAIL:-}
@@ -444,6 +460,11 @@ services:
444
460
  SERVER_PORT: "3001"
445
461
  EXE_LICENSE_KEY: ${EXE_LICENSE_KEY:?EXE_LICENSE_KEY is required — purchase at https://askexe.com}
446
462
  STORAGE_DIR: /app/server/storage
463
+ # bug b99d3762 / 83192765: the wiki error-reporter POSTs to the monitor hub and must
464
+ # send X-Monitor-Key once the hub requires it on every path (mirrors gateway/erp).
465
+ ERROR_REPORTING_ENABLED: "true"
466
+ MONITOR_ERROR_URL: http://exe-monitor-hub:8090/api/exe-monitor/errors
467
+ MONITOR_API_KEY: ${MONITOR_API_KEY:-${EXE_MONITOR_KEY:-}}
447
468
  DATABASE_URL: postgres://${POSTGRES_USER:-exe}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@exe-db:5432/${POSTGRES_DB:-exedb}?schema=${WIKI_DB_SCHEMA:-wiki}&sslmode=disable
448
469
  AUTH_TOKEN: ${WIKI_AUTH_TOKEN:?WIKI_AUTH_TOKEN is required}
449
470
  EXE_WIKI_ADMIN_TOKEN: ${EXE_WIKI_ADMIN_TOKEN:-}
@@ -597,6 +618,14 @@ services:
597
618
  - exe_os_data:/home/exed/.exe-os
598
619
  networks:
599
620
  - backend
621
+ healthcheck:
622
+ # The worker is a long-running node process (embed-worker.js). Verify the
623
+ # PID-1 node process is alive via /proc/1/cmdline.
624
+ test: ["CMD-SHELL", "test -f /proc/1/cmdline"]
625
+ interval: 30s
626
+ timeout: 5s
627
+ start_period: 30s
628
+ retries: 3
600
629
  deploy:
601
630
  resources:
602
631
  limits:
@@ -637,6 +666,13 @@ services:
637
666
  NODE_ENV: production
638
667
  EXE_GATEWAY_HOME: /data
639
668
  EXE_GATEWAY_CONFIG: /data/gateway.json
669
+ # bug 02bc1bb8: customer overlay dir. The host ./overlay is bind-mounted
670
+ # read-only at /data/overlay (see volumes below) so customer overlays
671
+ # (e.g. po-intake.js) SURVIVE every image upgrade / container recreate
672
+ # instead of being silently dropped from the old container layer. The
673
+ # gateway loads overlays from this dir; the post-deploy add-routes.sh hook
674
+ # additionally re-applies them for gateway builds that load from /app.
675
+ EXE_GATEWAY_OVERLAY_DIR: /data/overlay
640
676
  DATABASE_URL: postgres://${POSTGRES_USER:-exe}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@exe-db:5432/${POSTGRES_DB:-exedb}?sslmode=disable
641
677
  EXE_GATEWAY_PORT: "3100"
642
678
  EXE_GATEWAY_HOST: "0.0.0.0"
@@ -665,6 +701,13 @@ services:
665
701
  # one secret without extra wiring.
666
702
  MONITOR_API_KEY: ${MONITOR_API_KEY:-${EXE_MONITOR_KEY:-}}
667
703
  EXE_DAEMON_TOKEN: ${EXED_MCP_TOKEN:?EXED_MCP_TOKEN is required}
704
+ # Billing enforcement contract (bug df8823d0). The gateway reads THESE to
705
+ # decide metered vs BYOK. Default metered + direct keys disallowed (fail
706
+ # closed). BYOK_ENABLED is the customer-facing alias: =true implies
707
+ # EXE_BILLING_MODE=byok + EXE_ALLOW_DIRECT_PROVIDER_KEYS=true. A mismatch
708
+ # (BYOK_ENABLED=true but EXE_BILLING_MODE=metered) resolves to metered.
709
+ EXE_BILLING_MODE: ${EXE_BILLING_MODE:-metered}
710
+ EXE_ALLOW_DIRECT_PROVIDER_KEYS: ${EXE_ALLOW_DIRECT_PROVIDER_KEYS:-false}
668
711
  BYOK_ENABLED: ${BYOK_ENABLED:-false}
669
712
  ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
670
713
  EXE_LICENSE_KEY: ${EXE_LICENSE_KEY:?EXE_LICENSE_KEY is required — purchase at https://askexe.com}
@@ -675,6 +718,14 @@ services:
675
718
  - gateway_data:/data
676
719
  - gateway_whatsapp_auth:/app/auth_info
677
720
  - ./gateway.json:/data/gateway.json:ro
721
+ # bug 02bc1bb8: PERSISTENT customer-overlay bind-mount. Anything the
722
+ # customer drops in the host ./overlay dir (e.g. po-intake.js) is exposed
723
+ # read-only inside the container at /data/overlay and therefore SURVIVES
724
+ # image upgrades / `docker compose up` recreate — unlike files that only
725
+ # ever lived in the container's writable layer (which a recreate drops).
726
+ # bootstrapStackHost ensures ./overlay exists so docker never auto-creates
727
+ # the bind path as a stray file. Empty for customers with no overlay.
728
+ - ./overlay:/data/overlay:ro
678
729
  networks:
679
730
  - backend
680
731
  - frontend
@@ -718,8 +769,9 @@ services:
718
769
  exe-monitor-hub:
719
770
  condition: service_healthy
720
771
  environment:
721
- # Reports to the local hub by default. stack-update auto-pairs this agent
722
- # with the hub via /api/register-agent and writes TOKEN + KEY back to .env.
772
+ # Reports to the local hub by default. The agent must be paired manually
773
+ # from the hub UI (Settings Add System) the hub does not expose a
774
+ # REST registration endpoint. Write the generated TOKEN + KEY into .env.
723
775
  HUB_URL: ${MONITOR_HUB_URL:-http://exe-monitor-hub:8090}
724
776
  # TOKEN and KEY are written automatically by `exe-os stack-update` during
725
777
  # bootstrap. They start empty; the agent retries connecting until the hub
@@ -838,7 +890,7 @@ services:
838
890
  # ------------------------------------------------------------------
839
891
 
840
892
  exe-sso-edge:
841
- image: nginx:alpine
893
+ image: nginx:alpine@sha256:20316569d8f81a160065d7d2a5eeffc7ca97d79022462ee255fd23fa103a6b5c
842
894
  container_name: exe-sso-edge
843
895
  restart: unless-stopped
844
896
  depends_on:
@@ -1069,7 +1121,7 @@ services:
1069
1121
  # API to gunicorn, WebSocket to socketio. Without this, gunicorn returns
1070
1122
  # 404 for all /assets/* requests (bug 6776edf0).
1071
1123
  exe-erp-nginx:
1072
- image: nginx:alpine
1124
+ image: nginx:alpine@sha256:20316569d8f81a160065d7d2a5eeffc7ca97d79022462ee255fd23fa103a6b5c
1073
1125
  container_name: exe-erp-nginx
1074
1126
  restart: unless-stopped
1075
1127
  depends_on:
@@ -1154,6 +1206,13 @@ services:
1154
1206
  - erp_assets:/home/frappe/frappe-bench/sites/assets
1155
1207
  networks:
1156
1208
  - backend
1209
+ healthcheck:
1210
+ # Verify the socketio node process is listening on port 9000.
1211
+ test: ["CMD-SHELL", "node -e \"const s=require('net').connect(9000,'127.0.0.1',()=>{s.destroy();process.exit(0)});s.setTimeout(4000,()=>{s.destroy();process.exit(1)});s.on('error',()=>process.exit(1))\""]
1212
+ interval: 30s
1213
+ timeout: 5s
1214
+ start_period: 30s
1215
+ retries: 3
1157
1216
  deploy:
1158
1217
  resources:
1159
1218
  limits:
@@ -1193,6 +1252,13 @@ services:
1193
1252
  - erp_assets:/home/frappe/frappe-bench/sites/assets
1194
1253
  networks:
1195
1254
  - backend
1255
+ healthcheck:
1256
+ # Verify the bench worker process is alive and Redis queue is reachable.
1257
+ test: ["CMD-SHELL", "pgrep -f 'rq worker' > /dev/null"]
1258
+ interval: 30s
1259
+ timeout: 5s
1260
+ start_period: 30s
1261
+ retries: 3
1196
1262
  deploy:
1197
1263
  resources:
1198
1264
  limits:
@@ -1232,6 +1298,13 @@ services:
1232
1298
  - erp_assets:/home/frappe/frappe-bench/sites/assets
1233
1299
  networks:
1234
1300
  - backend
1301
+ healthcheck:
1302
+ # Verify the bench schedule process is alive.
1303
+ test: ["CMD-SHELL", "pgrep -f 'bench schedule' > /dev/null"]
1304
+ interval: 30s
1305
+ timeout: 5s
1306
+ start_period: 30s
1307
+ retries: 3
1235
1308
  deploy:
1236
1309
  resources:
1237
1310
  limits:
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env bash
2
+ # exe-os gateway watchdog — verifies the exe-gateway WhatsApp adapter is actually
3
+ # CONNECTED (not just that the HTTP server answers) and restarts the gateway only
4
+ # when it is genuinely wedged.
5
+ #
6
+ # Install (cron, every 2 min):
7
+ # chmod +x /opt/exe-stack/gateway-watchdog.sh
8
+ # crontab -l | { cat; echo "*/2 * * * * /opt/exe-stack/gateway-watchdog.sh 2>&1 | logger -t exe-gw-watchdog"; } | crontab -
9
+ #
10
+ # ─────────────────────────────────────────────────────────────────────────────
11
+ # BUG HISTORY (facd5078, P0)
12
+ # ─────────────────────────────────────────────────────────────────────────────
13
+ # The previous watchdog curled the UNAUTHENTICATED http://localhost:3100/health
14
+ # and grepped for `"connected":true`. But the unauth /health only returns
15
+ # {status, uptime, adapterCount} — it carries NO adapter connection state — so the
16
+ # grep ALWAYS failed → 3 strikes every ~6 min → `docker restart exe-gateway`
17
+ # 24/7 (1,668 restarts in 72h on HYGO). `docker restart` does NOT increment the
18
+ # container RestartCount, which masked the loop entirely.
19
+ #
20
+ # This rewrite makes the check ACCURATE and FAIL-SAFE:
21
+ # 1. AUTHENTICATED probe of /admin/webhooks/health (Bearer EXE_GATEWAY_AUTH_TOKEN),
22
+ # which reports real per-adapter state. Preferred — correct regardless of
23
+ # gateway version.
24
+ # 2. If the running gateway already exposes `adapters_connected` on the UNAUTH
25
+ # /health (exe-gateway bug 7e9fc713), that is honored too — but we do NOT
26
+ # hard-depend on it shipping first.
27
+ # 3. FAIL SAFE: if EXE_GATEWAY_AUTH_TOKEN is unset, OR the health body cannot be
28
+ # parsed into a definite connected/disconnected verdict, the result is
29
+ # INCONCLUSIVE → we LOG and do NOT restart. A watchdog must never restart on
30
+ # an unknown.
31
+ # 4. STARTUP GRACE: the gateway is given GRACE_SECS of container uptime before
32
+ # any strike is counted, so a gateway that is merely (re)starting / pairing
33
+ # WhatsApp is never restart-looped.
34
+ # 5. BACKOFF: after a restart we refuse to restart again for COOLDOWN_SECS, so we
35
+ # never hammer a gateway that needs time to reconnect.
36
+
37
+ set -uo pipefail
38
+
39
+ # ── Config ───────────────────────────────────────────────────────────────────
40
+ STACK_DIR="${EXE_STACK_DIR:-/opt/exe-stack}"
41
+ ENV_FILE="${EXE_STACK_ENV_FILE:-${STACK_DIR}/.env}"
42
+ CONTAINER="${EXE_GATEWAY_CONTAINER:-exe-gateway}"
43
+ PORT="${EXE_GATEWAY_HTTP_PORT:-3100}"
44
+ HOST="${EXE_GATEWAY_HOST:-127.0.0.1}"
45
+ TIMEOUT="${EXE_GATEWAY_WATCHDOG_TIMEOUT:-5}"
46
+
47
+ # Strikes before a restart. Each cron tick = one probe.
48
+ MAX_STRIKES="${EXE_GATEWAY_WATCHDOG_STRIKES:-3}"
49
+ # Don't count strikes until the container has been up at least this long.
50
+ GRACE_SECS="${EXE_GATEWAY_WATCHDOG_GRACE_SECS:-120}"
51
+ # After a restart, refuse to restart again for this long (let it reconnect).
52
+ COOLDOWN_SECS="${EXE_GATEWAY_WATCHDOG_COOLDOWN_SECS:-300}"
53
+
54
+ STATE_DIR="${EXE_GATEWAY_WATCHDOG_STATE_DIR:-${STACK_DIR}/.gateway-watchdog}"
55
+ STRIKE_FILE="${STATE_DIR}/strikes"
56
+ LAST_RESTART_FILE="${STATE_DIR}/last-restart"
57
+
58
+ mkdir -p "$STATE_DIR" 2>/dev/null || true
59
+
60
+ ts() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
61
+ log() { echo "[$(ts)] gateway-watchdog: $*"; }
62
+
63
+ # Read a single KEY=VALUE from the stack .env WITHOUT sourcing it (the .env holds
64
+ # many secrets and arbitrary values; sourcing could execute / mangle them).
65
+ read_env() {
66
+ local key="$1"
67
+ [[ -f "$ENV_FILE" ]] || return 0
68
+ # Last assignment wins; strip surrounding quotes; ignore comments.
69
+ sed -n "s/^[[:space:]]*${key}=//p" "$ENV_FILE" 2>/dev/null | tail -n1 \
70
+ | sed -e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'$/\1/"
71
+ }
72
+
73
+ reset_strikes() { echo 0 > "$STRIKE_FILE" 2>/dev/null || true; }
74
+ read_strikes() { local s; s=$(cat "$STRIKE_FILE" 2>/dev/null || echo 0); [[ "$s" =~ ^[0-9]+$ ]] && echo "$s" || echo 0; }
75
+ bump_strikes() { local s; s=$(read_strikes); echo $((s + 1)) > "$STRIKE_FILE" 2>/dev/null || true; echo $((s + 1)); }
76
+
77
+ # Container uptime in seconds (0 if not running / unknown).
78
+ container_uptime_secs() {
79
+ local started running
80
+ running=$(docker inspect --format '{{.State.Running}}' "$CONTAINER" 2>/dev/null || echo "false")
81
+ [[ "$running" == "true" ]] || { echo 0; return; }
82
+ started=$(docker inspect --format '{{.State.StartedAt}}' "$CONTAINER" 2>/dev/null || echo "")
83
+ [[ -n "$started" ]] || { echo 0; return; }
84
+ local start_epoch now_epoch
85
+ start_epoch=$(date -u -d "$started" +%s 2>/dev/null || date -u -j -f "%Y-%m-%dT%H:%M:%S" "${started%%.*}" +%s 2>/dev/null || echo 0)
86
+ now_epoch=$(date -u +%s)
87
+ if [[ "$start_epoch" -gt 0 && "$now_epoch" -ge "$start_epoch" ]]; then
88
+ echo $((now_epoch - start_epoch))
89
+ else
90
+ echo 0
91
+ fi
92
+ }
93
+
94
+ seconds_since_last_restart() {
95
+ local last now
96
+ last=$(cat "$LAST_RESTART_FILE" 2>/dev/null || echo 0)
97
+ [[ "$last" =~ ^[0-9]+$ ]] || last=0
98
+ now=$(date -u +%s)
99
+ echo $((now - last))
100
+ }
101
+
102
+ # ── Adapter-state probe ──────────────────────────────────────────────────────
103
+ # Echoes exactly one of: connected | disconnected | inconclusive
104
+ probe_adapter_state() {
105
+ local token unauth auth
106
+
107
+ # 1) Forward-compat: unauth /health MAY expose adapters_connected (gw bug 7e9fc713).
108
+ unauth=$(curl -sk --max-time "$TIMEOUT" "http://${HOST}:${PORT}/health" 2>/dev/null || echo "")
109
+ if [[ -n "$unauth" ]]; then
110
+ if echo "$unauth" | grep -Eq '"adapters_connected"[[:space:]]*:[[:space:]]*true'; then
111
+ echo "connected"; return
112
+ fi
113
+ if echo "$unauth" | grep -Eq '"adapters_connected"[[:space:]]*:[[:space:]]*false'; then
114
+ echo "disconnected"; return
115
+ fi
116
+ # No adapters_connected field → this gateway predates 7e9fc713. Fall through to
117
+ # the authenticated probe; the bare {status,uptime,adapterCount} body is NOT a
118
+ # connection verdict and must never be treated as one.
119
+ fi
120
+
121
+ # 2) Authenticated /admin/webhooks/health — reports real per-adapter state.
122
+ token=$(read_env EXE_GATEWAY_AUTH_TOKEN)
123
+ if [[ -z "$token" ]]; then
124
+ # FAIL SAFE: no token → we cannot make an authoritative judgement. Never restart.
125
+ echo "inconclusive"; return
126
+ fi
127
+
128
+ auth=$(curl -sk --max-time "$TIMEOUT" \
129
+ -H "Authorization: Bearer ${token}" \
130
+ "http://${HOST}:${PORT}/admin/webhooks/health" 2>/dev/null || echo "")
131
+
132
+ if [[ -z "$auth" ]]; then
133
+ # Couldn't reach the authed endpoint at all → inconclusive (could be a transient
134
+ # network blip; the HTTP layer may still be fine). Don't restart on this alone.
135
+ echo "inconclusive"; return
136
+ fi
137
+
138
+ # Real connection verdict. The authed health reports adapter connection via
139
+ # "connected":true and/or a per-account "whatsapp":{... "connected":true}.
140
+ if echo "$auth" | grep -Eq '"connected"[[:space:]]*:[[:space:]]*true'; then
141
+ echo "connected"; return
142
+ fi
143
+ if echo "$auth" | grep -Eq '"handlerRegistered"[[:space:]]*:[[:space:]]*true' \
144
+ && echo "$auth" | grep -Eq '"connected"[[:space:]]*:[[:space:]]*false'; then
145
+ echo "disconnected"; return
146
+ fi
147
+ # An authed body we can't classify (schema drift) is NOT grounds to restart.
148
+ echo "inconclusive"
149
+ }
150
+
151
+ # ── Main ─────────────────────────────────────────────────────────────────────
152
+ main() {
153
+ # Gateway not running at all? Let Docker's own `restart: unless-stopped` handle
154
+ # a crashed container — the watchdog only acts on a RUNNING-but-wedged gateway.
155
+ local uptime
156
+ uptime=$(container_uptime_secs)
157
+ if [[ "$uptime" -eq 0 ]]; then
158
+ log "container ${CONTAINER} not running (or uptime unknown) — deferring to Docker restart policy; not counting a strike."
159
+ reset_strikes
160
+ exit 0
161
+ fi
162
+
163
+ local state
164
+ state=$(probe_adapter_state)
165
+
166
+ case "$state" in
167
+ connected)
168
+ reset_strikes
169
+ log "OK adapters connected (uptime ${uptime}s)."
170
+ exit 0
171
+ ;;
172
+ inconclusive)
173
+ # NEVER restart on an inconclusive check. Log and reset so a transient
174
+ # unknown can't accumulate toward a restart.
175
+ reset_strikes
176
+ log "INCONCLUSIVE health check (token unset or unparseable body) — NOT restarting."
177
+ exit 0
178
+ ;;
179
+ disconnected)
180
+ : # fall through to strike logic below
181
+ ;;
182
+ *)
183
+ reset_strikes
184
+ log "unexpected probe result '${state}' — treating as inconclusive, NOT restarting."
185
+ exit 0
186
+ ;;
187
+ esac
188
+
189
+ # ── disconnected ──
190
+ # Startup grace: a gateway that is merely (re)starting / re-pairing WhatsApp must
191
+ # not be counted against. Require GRACE_SECS of uptime before any strike.
192
+ if [[ "$uptime" -lt "$GRACE_SECS" ]]; then
193
+ log "adapter disconnected but within startup grace (uptime ${uptime}s < ${GRACE_SECS}s) — not counting a strike."
194
+ exit 0
195
+ fi
196
+
197
+ # Backoff: don't restart again within the cooldown window — give the gateway time
198
+ # to reconnect after the previous restart.
199
+ local since_restart
200
+ since_restart=$(seconds_since_last_restart)
201
+ if [[ "$since_restart" -lt "$COOLDOWN_SECS" ]]; then
202
+ log "adapter disconnected but within post-restart cooldown (${since_restart}s < ${COOLDOWN_SECS}s) — backing off, not restarting."
203
+ exit 0
204
+ fi
205
+
206
+ local strikes
207
+ strikes=$(bump_strikes)
208
+ log "adapter DISCONNECTED — strike ${strikes}/${MAX_STRIKES} (uptime ${uptime}s)."
209
+
210
+ if [[ "$strikes" -ge "$MAX_STRIKES" ]]; then
211
+ log "ALERT strike threshold reached — restarting ${CONTAINER}."
212
+ if docker restart "$CONTAINER" >/dev/null 2>&1; then
213
+ date -u +%s > "$LAST_RESTART_FILE" 2>/dev/null || true
214
+ reset_strikes
215
+ log "restarted ${CONTAINER}; entering ${COOLDOWN_SECS}s cooldown."
216
+ else
217
+ log "ERROR docker restart ${CONTAINER} failed."
218
+ fi
219
+ fi
220
+ exit 0
221
+ }
222
+
223
+ main "$@"
@@ -174,8 +174,14 @@ export function generateEnv(options: GenerateEnvOptions): string {
174
174
  "WHATSAPP_ACCESS_TOKEN=",
175
175
  `API_ROUTER_URL=${DEFAULT_API_ROUTER_URL}`,
176
176
  `API_ROUTER_KEY=${randomSecret(48)}`,
177
- "# BYOK: to use your own API keys instead of the Exe API Router,",
178
- "# set BYOK_ENABLED=true and provide ANTHROPIC_API_KEY below.",
177
+ "# Billing enforcement contract (bug df8823d0). These are the vars the gateway",
178
+ "# ACTUALLY reads to decide metered vs BYOK. Default: metered (fail-closed) —",
179
+ "# direct provider keys are rejected and all AI traffic is billed via the router.",
180
+ "EXE_BILLING_MODE=metered",
181
+ "EXE_ALLOW_DIRECT_PROVIDER_KEYS=false",
182
+ "# BYOK: to use your own API keys instead of the Exe API Router, set",
183
+ "# BYOK_ENABLED=true (the customer-facing alias for EXE_BILLING_MODE=byok +",
184
+ "# EXE_ALLOW_DIRECT_PROVIDER_KEYS=true) and provide ANTHROPIC_API_KEY below.",
179
185
  "# BYOK_ENABLED=false",
180
186
  "# ANTHROPIC_API_KEY=",
181
187
  `GATEWAY_HTTP_HOST_PORT=${GATEWAY_HTTP_HOST_PORT}`,
@@ -403,8 +409,14 @@ export function generateExampleEnv(): string {
403
409
  "WHATSAPP_ACCESS_TOKEN=CHANGEME_WHATSAPP_ACCESS_TOKEN",
404
410
  `API_ROUTER_URL=${DEFAULT_API_ROUTER_URL}`,
405
411
  "API_ROUTER_KEY=CHANGEME_API_ROUTER_KEY",
406
- "# BYOK: to use your own API keys instead of the Exe API Router,",
407
- "# set BYOK_ENABLED=true and provide ANTHROPIC_API_KEY below.",
412
+ "# Billing enforcement contract (bug df8823d0). These are the vars the gateway",
413
+ "# ACTUALLY reads to decide metered vs BYOK. Default: metered (fail-closed) —",
414
+ "# direct provider keys are rejected and all AI traffic is billed via the router.",
415
+ "EXE_BILLING_MODE=metered",
416
+ "EXE_ALLOW_DIRECT_PROVIDER_KEYS=false",
417
+ "# BYOK: to use your own API keys instead of the Exe API Router, set",
418
+ "# BYOK_ENABLED=true (the customer-facing alias for EXE_BILLING_MODE=byok +",
419
+ "# EXE_ALLOW_DIRECT_PROVIDER_KEYS=true) and provide ANTHROPIC_API_KEY below.",
408
420
  "# BYOK_ENABLED=false",
409
421
  "# ANTHROPIC_API_KEY=CHANGEME_ANTHROPIC_API_KEY",
410
422
  `GATEWAY_HTTP_HOST_PORT=${GATEWAY_HTTP_HOST_PORT}`,