@fastagent-sh/fastagent 0.21.1 → 0.22.0

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 (388) hide show
  1. package/README.md +9 -12
  2. package/dist/agent.d.ts +24 -37
  3. package/dist/agent.js +7 -15
  4. package/dist/atomic-write.d.ts +13 -17
  5. package/dist/atomic-write.js +18 -29
  6. package/dist/bind.d.ts +12 -22
  7. package/dist/bind.js +18 -27
  8. package/dist/channel.d.ts +11 -33
  9. package/dist/channels/agentcore-limits.d.ts +2 -13
  10. package/dist/channels/agentcore-limits.js +2 -13
  11. package/dist/channels/agentcore-protocol.d.ts +67 -58
  12. package/dist/channels/agentcore-protocol.js +23 -15
  13. package/dist/channels/agentcore-service.d.ts +19 -33
  14. package/dist/channels/agentcore-service.js +176 -59
  15. package/dist/channels/agentcore.d.ts +39 -33
  16. package/dist/channels/agentcore.js +90 -224
  17. package/dist/channels/body.d.ts +15 -2
  18. package/dist/channels/body.js +24 -2
  19. package/dist/channels/busy.d.ts +1 -21
  20. package/dist/channels/busy.js +2 -39
  21. package/dist/channels/control.d.ts +17 -62
  22. package/dist/channels/control.js +74 -182
  23. package/dist/channels/define-channel.d.ts +39 -0
  24. package/dist/channels/define-channel.js +9 -0
  25. package/dist/channels/discover.d.ts +10 -31
  26. package/dist/channels/discover.js +37 -31
  27. package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
  28. package/dist/channels/feishu/bootstrap-token.js +7 -23
  29. package/dist/channels/feishu/card.d.ts +14 -28
  30. package/dist/channels/feishu/card.js +16 -30
  31. package/dist/channels/feishu/cloud.d.ts +1 -5
  32. package/dist/channels/feishu/context-buffer.d.ts +4 -23
  33. package/dist/channels/feishu/context-buffer.js +8 -31
  34. package/dist/channels/feishu/crypto.d.ts +10 -5
  35. package/dist/channels/feishu/crypto.js +12 -19
  36. package/dist/channels/feishu/feishu-api.d.ts +2 -1
  37. package/dist/channels/feishu/feishu-api.js +5 -4
  38. package/dist/channels/feishu/feishu.d.ts +4 -10
  39. package/dist/channels/feishu/feishu.js +113 -227
  40. package/dist/channels/feishu/invoke-turn.d.ts +5 -4
  41. package/dist/channels/feishu/invoke-turn.js +36 -54
  42. package/dist/channels/feishu/model.d.ts +4 -21
  43. package/dist/channels/feishu/model.js +1 -8
  44. package/dist/channels/feishu/normalize.d.ts +2 -10
  45. package/dist/channels/feishu/normalize.js +7 -42
  46. package/dist/channels/feishu/parse.d.ts +9 -29
  47. package/dist/channels/feishu/parse.js +8 -26
  48. package/dist/channels/feishu/preview.d.ts +12 -14
  49. package/dist/channels/feishu/preview.js +108 -207
  50. package/dist/channels/feishu/register-app.d.ts +8 -23
  51. package/dist/channels/feishu/register-app.js +7 -20
  52. package/dist/channels/feishu/register-webhook.d.ts +2 -9
  53. package/dist/channels/feishu/register-webhook.js +9 -33
  54. package/dist/channels/feishu/scaffold/channel.ts +27 -19
  55. package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
  56. package/dist/channels/feishu/setup-mode.d.ts +11 -36
  57. package/dist/channels/feishu/setup-mode.js +7 -26
  58. package/dist/channels/feishu/ws-ingress.d.ts +1 -2
  59. package/dist/channels/feishu/ws-ingress.js +6 -8
  60. package/dist/channels/http.d.ts +15 -15
  61. package/dist/channels/http.js +19 -9
  62. package/dist/channels/kit/attachment-path.d.ts +4 -5
  63. package/dist/channels/kit/attachment-path.js +10 -34
  64. package/dist/channels/kit/context-buffer.d.ts +14 -21
  65. package/dist/channels/kit/context-buffer.js +25 -38
  66. package/dist/channels/kit/delivery.d.ts +35 -0
  67. package/dist/channels/kit/delivery.js +129 -0
  68. package/dist/channels/kit/event-stream.d.ts +5 -0
  69. package/dist/channels/kit/event-stream.js +29 -0
  70. package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
  71. package/dist/channels/kit/invoke-turn-kit.js +89 -32
  72. package/dist/channels/kit/preview-kit.d.ts +18 -72
  73. package/dist/channels/kit/preview-kit.js +22 -88
  74. package/dist/channels/kit/signature.d.ts +3 -9
  75. package/dist/channels/kit/signature.js +3 -9
  76. package/dist/channels/kit/state.d.ts +1 -2
  77. package/dist/channels/kit/state.js +5 -14
  78. package/dist/channels/kit/stop-command.d.ts +1 -2
  79. package/dist/channels/kit/stop-command.js +6 -9
  80. package/dist/channels/kit/tasks.d.ts +1 -1
  81. package/dist/channels/kit/tasks.js +8 -21
  82. package/dist/channels/kit/text.d.ts +2 -16
  83. package/dist/channels/kit/text.js +23 -31
  84. package/dist/channels/kit/thread-participants.d.ts +4 -17
  85. package/dist/channels/kit/thread-participants.js +8 -55
  86. package/dist/channels/kit/transport.d.ts +26 -0
  87. package/dist/channels/kit/transport.js +20 -0
  88. package/dist/channels/kit/turn-queue.d.ts +7 -11
  89. package/dist/channels/kit/turn-queue.js +21 -39
  90. package/dist/channels/kit/turn-runner.d.ts +32 -18
  91. package/dist/channels/kit/turn-runner.js +98 -53
  92. package/dist/channels/kit/turn-store.d.ts +32 -32
  93. package/dist/channels/kit/turn-store.js +45 -88
  94. package/dist/channels/lark/lark.d.ts +1 -5
  95. package/dist/channels/lark/onboard.d.ts +3 -12
  96. package/dist/channels/lark/onboard.js +3 -13
  97. package/dist/channels/lark/scaffold/channel.ts +27 -19
  98. package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
  99. package/dist/channels/registration.d.ts +9 -35
  100. package/dist/channels/registration.js +6 -37
  101. package/dist/channels/secret.js +2 -5
  102. package/dist/channels/serve.d.ts +47 -44
  103. package/dist/channels/serve.js +165 -101
  104. package/dist/channels/slack/config-api.d.ts +4 -27
  105. package/dist/channels/slack/config-api.js +5 -34
  106. package/dist/channels/slack/context-buffer.d.ts +2 -3
  107. package/dist/channels/slack/context-buffer.js +5 -5
  108. package/dist/channels/slack/invoke-turn.d.ts +3 -1
  109. package/dist/channels/slack/invoke-turn.js +25 -34
  110. package/dist/channels/slack/manifest.js +1 -3
  111. package/dist/channels/slack/model.d.ts +1 -2
  112. package/dist/channels/slack/model.js +1 -1
  113. package/dist/channels/slack/onboard.d.ts +4 -3
  114. package/dist/channels/slack/onboard.js +8 -11
  115. package/dist/channels/slack/onboarding-state.d.ts +3 -5
  116. package/dist/channels/slack/onboarding-state.js +9 -6
  117. package/dist/channels/slack/parse.d.ts +4 -13
  118. package/dist/channels/slack/parse.js +7 -28
  119. package/dist/channels/slack/preview.d.ts +12 -2
  120. package/dist/channels/slack/preview.js +292 -269
  121. package/dist/channels/slack/reaction.d.ts +11 -3
  122. package/dist/channels/slack/reaction.js +19 -8
  123. package/dist/channels/slack/register-webhook.js +4 -7
  124. package/dist/channels/slack/scaffold/channel.ts +20 -13
  125. package/dist/channels/slack/scaffold/slack-send.ts +2 -2
  126. package/dist/channels/slack/setup-server.d.ts +1 -5
  127. package/dist/channels/slack/setup-server.js +1 -5
  128. package/dist/channels/slack/shared-api.d.ts +1 -4
  129. package/dist/channels/slack/shared-api.js +2 -13
  130. package/dist/channels/slack/slack-api.d.ts +2 -1
  131. package/dist/channels/slack/slack-api.js +6 -4
  132. package/dist/channels/slack/slack.d.ts +11 -14
  133. package/dist/channels/slack/slack.js +121 -122
  134. package/dist/channels/sse.d.ts +1 -1
  135. package/dist/channels/sse.js +1 -1
  136. package/dist/channels/telegram/context-buffer.d.ts +7 -22
  137. package/dist/channels/telegram/context-buffer.js +6 -23
  138. package/dist/channels/telegram/invoke-turn.d.ts +7 -7
  139. package/dist/channels/telegram/invoke-turn.js +24 -43
  140. package/dist/channels/telegram/parse.d.ts +24 -51
  141. package/dist/channels/telegram/parse.js +32 -84
  142. package/dist/channels/telegram/preview.d.ts +10 -1
  143. package/dist/channels/telegram/preview.js +79 -100
  144. package/dist/channels/telegram/register-webhook.js +5 -23
  145. package/dist/channels/telegram/scaffold/channel.ts +25 -18
  146. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
  147. package/dist/channels/telegram/telegram-api.d.ts +4 -3
  148. package/dist/channels/telegram/telegram-api.js +9 -7
  149. package/dist/channels/telegram/telegram.d.ts +4 -18
  150. package/dist/channels/telegram/telegram.js +38 -94
  151. package/dist/channels/telegram/turn-store.d.ts +11 -14
  152. package/dist/channels/telegram/turn-store.js +8 -6
  153. package/dist/channels/wait-health.d.ts +4 -6
  154. package/dist/channels/wait-health.js +10 -14
  155. package/dist/cli/add-feishu.d.ts +2 -12
  156. package/dist/cli/add-feishu.js +24 -71
  157. package/dist/cli/add-slack.d.ts +5 -3
  158. package/dist/cli/add-slack.js +53 -76
  159. package/dist/cli/auth-view.d.ts +2 -16
  160. package/dist/cli/auth-view.js +2 -16
  161. package/dist/cli/commands/add.js +25 -40
  162. package/dist/cli/commands/chat.d.ts +0 -1
  163. package/dist/cli/commands/chat.js +5 -6
  164. package/dist/cli/commands/deploy/agentcore.js +60 -65
  165. package/dist/cli/commands/deploy/docker.js +50 -23
  166. package/dist/cli/commands/deploy/fly.js +23 -61
  167. package/dist/cli/commands/deploy/railway.js +23 -18
  168. package/dist/cli/commands/deploy/shared.d.ts +38 -57
  169. package/dist/cli/commands/deploy/shared.js +40 -59
  170. package/dist/cli/commands/deploy.d.ts +10 -27
  171. package/dist/cli/commands/deploy.js +38 -60
  172. package/dist/cli/commands/destroy.d.ts +4 -0
  173. package/dist/cli/commands/destroy.js +56 -0
  174. package/dist/cli/commands/dev.d.ts +2 -1
  175. package/dist/cli/commands/dev.js +20 -23
  176. package/dist/cli/commands/info.d.ts +0 -2
  177. package/dist/cli/commands/info.js +83 -49
  178. package/dist/cli/commands/init.d.ts +1 -2
  179. package/dist/cli/commands/init.js +16 -55
  180. package/dist/cli/commands/invoke.d.ts +0 -1
  181. package/dist/cli/commands/invoke.js +5 -7
  182. package/dist/cli/commands/login.d.ts +2 -1
  183. package/dist/cli/commands/login.js +33 -38
  184. package/dist/cli/commands/logs.js +3 -4
  185. package/dist/cli/commands/models.js +1 -5
  186. package/dist/cli/commands/routine-run.d.ts +6 -0
  187. package/dist/cli/commands/routine-run.js +47 -0
  188. package/dist/cli/commands/routine.d.ts +25 -0
  189. package/dist/cli/commands/routine.js +122 -0
  190. package/dist/cli/commands/start.d.ts +31 -3
  191. package/dist/cli/commands/start.js +131 -81
  192. package/dist/cli/commands/tool.js +26 -12
  193. package/dist/cli/fail.d.ts +13 -23
  194. package/dist/cli/fail.js +20 -23
  195. package/dist/cli/invoke-stream.d.ts +1 -8
  196. package/dist/cli/kernel.d.ts +6 -38
  197. package/dist/cli/kernel.js +14 -77
  198. package/dist/cli/models-view.d.ts +1 -9
  199. package/dist/cli/models-view.js +11 -22
  200. package/dist/cli/program.d.ts +5 -6
  201. package/dist/cli/program.js +120 -157
  202. package/dist/cli/serve.d.ts +57 -41
  203. package/dist/cli/serve.js +94 -121
  204. package/dist/cli/shared.d.ts +15 -61
  205. package/dist/cli/shared.js +55 -181
  206. package/dist/cli.js +1 -8
  207. package/dist/collect.d.ts +6 -0
  208. package/dist/collect.js +6 -0
  209. package/dist/core.d.ts +4 -1
  210. package/dist/core.js +9 -7
  211. package/dist/declared-secrets.d.ts +55 -0
  212. package/dist/declared-secrets.js +88 -0
  213. package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
  214. package/dist/deploy/agentcore/aws-cli.js +64 -0
  215. package/dist/deploy/agentcore/destroy.d.ts +51 -0
  216. package/dist/deploy/agentcore/destroy.js +273 -0
  217. package/dist/deploy/agentcore/forwarder.js +28 -79
  218. package/dist/deploy/agentcore/logs.d.ts +2 -18
  219. package/dist/deploy/agentcore/logs.js +27 -51
  220. package/dist/deploy/agentcore/plan.d.ts +85 -105
  221. package/dist/deploy/agentcore/plan.js +152 -314
  222. package/dist/deploy/agentcore/run.d.ts +28 -28
  223. package/dist/deploy/agentcore/run.js +227 -306
  224. package/dist/deploy/agentcore/zip.d.ts +2 -15
  225. package/dist/deploy/agentcore/zip.js +3 -17
  226. package/dist/deploy/build-context.d.ts +21 -0
  227. package/dist/deploy/build-context.js +137 -0
  228. package/dist/deploy/channel-ingress.d.ts +26 -39
  229. package/dist/deploy/channel-ingress.js +57 -28
  230. package/dist/deploy/container.d.ts +20 -34
  231. package/dist/deploy/container.js +49 -100
  232. package/dist/deploy/docker/plan.d.ts +12 -11
  233. package/dist/deploy/docker/plan.js +43 -24
  234. package/dist/deploy/docker/run.d.ts +22 -25
  235. package/dist/deploy/docker/run.js +39 -37
  236. package/dist/deploy/fly/plan.d.ts +15 -44
  237. package/dist/deploy/fly/plan.js +41 -73
  238. package/dist/deploy/fly/run.d.ts +16 -51
  239. package/dist/deploy/fly/run.js +42 -95
  240. package/dist/deploy/hosts.d.ts +24 -3
  241. package/dist/deploy/hosts.js +24 -3
  242. package/dist/deploy/preflight.d.ts +47 -41
  243. package/dist/deploy/preflight.js +282 -383
  244. package/dist/deploy/railway/plan.d.ts +16 -42
  245. package/dist/deploy/railway/plan.js +42 -60
  246. package/dist/deploy/railway/run.d.ts +30 -68
  247. package/dist/deploy/railway/run.js +86 -100
  248. package/dist/deploy/registration-gate.d.ts +6 -13
  249. package/dist/deploy/residency.d.ts +37 -0
  250. package/dist/deploy/residency.js +24 -0
  251. package/dist/deploy/runner.d.ts +10 -9
  252. package/dist/deploy/runner.js +7 -18
  253. package/dist/deploy/secrets.d.ts +40 -41
  254. package/dist/deploy/secrets.js +117 -103
  255. package/dist/deploy/workspace.d.ts +50 -0
  256. package/dist/deploy/workspace.js +197 -0
  257. package/dist/dev-supervisor.d.ts +1 -9
  258. package/dist/dev-supervisor.js +21 -64
  259. package/dist/effect-port.d.ts +50 -0
  260. package/dist/effect-port.js +77 -0
  261. package/dist/engines/pi/agent-session-factory.d.ts +50 -69
  262. package/dist/engines/pi/agent-session-factory.js +249 -146
  263. package/dist/engines/pi/auth.d.ts +15 -11
  264. package/dist/engines/pi/auth.js +96 -99
  265. package/dist/engines/pi/chat.d.ts +1 -5
  266. package/dist/engines/pi/chat.js +2 -14
  267. package/dist/engines/pi/config.d.ts +53 -51
  268. package/dist/engines/pi/config.js +83 -92
  269. package/dist/engines/pi/create.d.ts +60 -75
  270. package/dist/engines/pi/create.js +108 -141
  271. package/dist/engines/pi/definition.d.ts +14 -58
  272. package/dist/engines/pi/definition.js +21 -85
  273. package/dist/engines/pi/invoke-session.d.ts +5 -25
  274. package/dist/engines/pi/invoke-session.js +209 -244
  275. package/dist/engines/pi/login.d.ts +95 -35
  276. package/dist/engines/pi/login.js +259 -107
  277. package/dist/engines/pi/machine.d.ts +31 -0
  278. package/dist/engines/pi/machine.js +106 -0
  279. package/dist/engines/pi/models.d.ts +70 -89
  280. package/dist/engines/pi/models.js +188 -81
  281. package/dist/engines/pi/open.d.ts +65 -53
  282. package/dist/engines/pi/open.js +129 -69
  283. package/dist/engines/pi/report.d.ts +3 -12
  284. package/dist/engines/pi/report.js +2 -16
  285. package/dist/engines/pi/search-tools.d.ts +1 -9
  286. package/dist/engines/pi/search-tools.js +26 -57
  287. package/dist/engines/pi/service.d.ts +2 -16
  288. package/dist/engines/pi/service.js +2 -16
  289. package/dist/engines/pi/session-builder.d.ts +5 -9
  290. package/dist/engines/pi/session-builder.js +44 -82
  291. package/dist/engines/pi/session-control.js +286 -269
  292. package/dist/engines/pi/session-effects.d.ts +14 -0
  293. package/dist/engines/pi/session-effects.js +30 -0
  294. package/dist/engines/pi/session-inheritance.d.ts +9 -18
  295. package/dist/engines/pi/session-inheritance.js +103 -160
  296. package/dist/engines/pi/session-markers.d.ts +31 -30
  297. package/dist/engines/pi/session-markers.js +26 -25
  298. package/dist/engines/pi/session-settings.d.ts +6 -22
  299. package/dist/engines/pi/session-settings.js +10 -21
  300. package/dist/engines/pi/session-store.d.ts +26 -92
  301. package/dist/engines/pi/session-store.js +96 -310
  302. package/dist/engines/pi/tool-context.d.ts +17 -45
  303. package/dist/engines/pi/tool-context.js +7 -26
  304. package/dist/engines/pi/tool.d.ts +45 -36
  305. package/dist/engines/pi/tool.js +43 -57
  306. package/dist/engines/pi/turn-kit.d.ts +14 -52
  307. package/dist/engines/pi/turn-kit.js +18 -67
  308. package/dist/engines/pi/wake-tool.d.ts +2 -10
  309. package/dist/engines/pi/wake-tool.js +7 -27
  310. package/dist/env.d.ts +27 -28
  311. package/dist/env.js +45 -50
  312. package/dist/index.js +0 -2
  313. package/dist/lark.d.ts +1 -2
  314. package/dist/lark.js +1 -2
  315. package/dist/loader.d.ts +14 -55
  316. package/dist/loader.js +24 -43
  317. package/dist/log.d.ts +2 -12
  318. package/dist/log.js +3 -24
  319. package/dist/node.d.ts +2 -15
  320. package/dist/node.js +2 -18
  321. package/dist/observe.d.ts +2 -8
  322. package/dist/open-url.d.ts +1 -1
  323. package/dist/open-url.js +1 -1
  324. package/dist/paths.d.ts +70 -134
  325. package/dist/paths.js +165 -232
  326. package/dist/pi.d.ts +4 -6
  327. package/dist/pi.js +3 -3
  328. package/dist/proxy.d.ts +16 -6
  329. package/dist/proxy.js +52 -16
  330. package/dist/runtime.d.ts +18 -12
  331. package/dist/runtime.js +18 -11
  332. package/dist/scaffold/add-channel.d.ts +14 -31
  333. package/dist/scaffold/add-channel.js +48 -112
  334. package/dist/scaffold/init.d.ts +9 -31
  335. package/dist/scaffold/init.js +45 -138
  336. package/dist/scaffold/templates/env.example +6 -4
  337. package/dist/scaffold/templates/fastagent.config.ts +36 -0
  338. package/dist/scaffold/templates.d.ts +3 -3
  339. package/dist/scaffold/templates.js +5 -8
  340. package/dist/scaffold/vendor-skill.d.ts +2 -5
  341. package/dist/scaffold/vendor-skill.js +19 -29
  342. package/dist/schedule/cron.d.ts +9 -5
  343. package/dist/schedule/cron.js +9 -11
  344. package/dist/schedule/discover.d.ts +12 -12
  345. package/dist/schedule/discover.js +43 -34
  346. package/dist/schedule/routine.d.ts +43 -0
  347. package/dist/schedule/routine.js +32 -0
  348. package/dist/schedule/run.d.ts +55 -0
  349. package/dist/schedule/run.js +146 -0
  350. package/dist/schedule/scheduler.d.ts +44 -55
  351. package/dist/schedule/scheduler.js +291 -265
  352. package/dist/schedule/state.d.ts +105 -10
  353. package/dist/schedule/state.js +293 -19
  354. package/dist/schedule/wake-alarm.d.ts +10 -23
  355. package/dist/schedule/wake-alarm.js +91 -121
  356. package/dist/schedule/wakeups.d.ts +23 -39
  357. package/dist/schedule/wakeups.js +33 -75
  358. package/dist/secrets-gate.d.ts +32 -0
  359. package/dist/secrets-gate.js +33 -0
  360. package/dist/service.d.ts +137 -98
  361. package/dist/service.js +250 -252
  362. package/dist/session-remote.d.ts +33 -44
  363. package/dist/session-remote.js +203 -192
  364. package/dist/session.d.ts +153 -228
  365. package/dist/session.js +33 -55
  366. package/dist/tunnel.d.ts +8 -36
  367. package/dist/tunnel.js +24 -65
  368. package/dist/version.d.ts +2 -2
  369. package/dist/version.js +2 -2
  370. package/package.json +10 -16
  371. package/dist/channels/agentcore-state.d.ts +0 -77
  372. package/dist/channels/agentcore-state.js +0 -276
  373. package/dist/channels/github/github.d.ts +0 -31
  374. package/dist/channels/github/github.js +0 -89
  375. package/dist/channels/github/scaffold/channel.ts +0 -25
  376. package/dist/cli/commands/attach.d.ts +0 -108
  377. package/dist/cli/commands/attach.js +0 -637
  378. package/dist/cli/commands/fire.d.ts +0 -7
  379. package/dist/cli/commands/fire.js +0 -38
  380. package/dist/cli/commands/schedule.d.ts +0 -12
  381. package/dist/cli/commands/schedule.js +0 -87
  382. package/dist/github.d.ts +0 -3
  383. package/dist/github.js +0 -2
  384. package/dist/scaffold/templates/fastagent.config.mjs +0 -23
  385. package/dist/schedule/audit.d.ts +0 -18
  386. package/dist/schedule/audit.js +0 -57
  387. package/dist/schedule/schedule.d.ts +0 -35
  388. package/dist/schedule/schedule.js +0 -23
@@ -1,35 +1,21 @@
1
- /**
2
- * The portable container — Dockerfile + .dockerignore, host-neutral. Every deploy target ships these
3
- * unchanged; only the host config (fly.toml / railway.json) and the CLI sequence differ. The image
4
- * runs `fastagent start /app` on `$PORT` with machinery under `$FASTAGENT_STATE_DIR` /
5
- * `$FASTAGENT_SECRETS_DIR` — standard container conventions, no host coupling.
6
- *
7
- * ONE deploy semantic: bake the WORKSPACE as the image (`COPY . .` — what you see is what ships),
8
- * with the agent at `./fastagent` inside it. The resolver in the image finds it exactly like dev does.
9
- * Deps install under `/app/fastagent`, and the Dockerfile lives there too — namespaced so it never
10
- * collides with one the workspace already owns.
11
- */
1
+ /** The portable container — Dockerfile + .dockerignore, host-neutral. */
12
2
  import { SECRETS_DIRNAME, STATE_DIRNAME } from "../paths.js";
13
- /** Fixed first-line prefix of a fastagent-generated Dockerfile — the ONE source both the generator (the
14
- * `head` below) and the "is this ours?" check (cli deploy's kept-Dockerfile apt warn) share, so they
15
- * can't drift into detecting a marker the generator no longer writes. */
3
+ import { RELEASE_FILE, parseDeploymentRelease } from "./workspace.js";
4
+ /** Fixed first-line prefix of a fastagent-generated Dockerfile. */
16
5
  export const GENERATED_DOCKERFILE_MARKER = "# Generated by `fastagent deploy`";
17
- /** The ONE predicate for "did fastagent generate this Dockerfile?" — a generated file opens with the
18
- * marker, so a hand-written one (or a file where the marker was dropped to take ownership) reads false.
19
- * Both the "keep vs flag-as-stale" decision and the apt warning share this; a split would let them
20
- * disagree on the same file (e.g. a marker not at byte 0). */
6
+ /** The ONE predicate for "did fastagent generate this Dockerfile?". */
21
7
  export function isGeneratedDockerfile(content) {
22
8
  return content.startsWith(GENERATED_DOCKERFILE_MARKER);
23
9
  }
24
- /** First line of a generated `.dockerignore` — and, for the workspace-root one, the single artifact
25
- * deploy writes outside the agent dir. The marker is what splits the two cases: ours is refreshed
26
- * (regenerated under `--force`), yours is kept. */
10
+ /**
11
+ * First line of a generated `.dockerignore` — and, for the workspace-root one, the single artifact deploy writes
12
+ * outside the agent dir.
13
+ */
27
14
  const GENERATED_DOCKERIGNORE_MARKER = "# Generated by `fastagent deploy`";
28
- /** Did fastagent generate this `.dockerignore`? (Same ownership rule as {@link isGeneratedDockerfile}.) */
29
15
  export function isGeneratedDockerignore(content) {
30
16
  return content.startsWith(GENERATED_DOCKERIGNORE_MARKER);
31
17
  }
32
- /** The apt layer (cached right after FROM). Debian default repos only. */
18
+ /** The apt layer (cached right after FROM). */
33
19
  function aptLayer(packages) {
34
20
  return packages && packages.length > 0
35
21
  ? `RUN apt-get update \\
@@ -39,46 +25,30 @@ function aptLayer(packages) {
39
25
  : "";
40
26
  }
41
27
  function dockerfile(input) {
42
- // The whole workspace is baked at /app; deps install (and the local bin lives) under the agent, which
43
- // is either a subdirectory of it or /app itself. `into` prefixes a path, `at` runs a command there.
28
+ // The whole workspace is baked at /app; deps install (and the local bin lives) under the agent, which is always a
29
+ // SUBDIRECTORY of it (the workspace is the agent dir's parent), so the prefix is never empty.
44
30
  const prefix = input.agentPrefix;
31
+ const dir = prefix.replace(/\/$/, "");
45
32
  const into = (p) => `${prefix}${p}`;
46
33
  // A build step that must run IN the agent dir (`RUN` already gets a shell from docker).
47
- const at = (cmd) => (prefix ? `cd ${prefix.replace(/\/$/, "")} && ${cmd}` : cmd);
48
- // The entrypoint. Only the BUN path needs a working directory (`bun run` resolves the script from the
49
- // package.json beside it), and a `cd` there means a shell — with `exec`, or sh stays PID 1 and swallows
50
- // the container's SIGTERM. The npm path needs no shell at all: the binary path is context-relative and
51
- // `start /app` is absolute, so exec form is both correct and signal-clean. (A `cd` on that path was a
52
- // bug: after `cd fastagent`, `./fastagent/node_modules/...` resolves one level too deep.)
53
- const bunCmd = prefix
54
- ? `["sh", "-c", "cd ${prefix.replace(/\/$/, "")} && exec bun run fastagent start /app"]`
55
- : `["bun", "run", "fastagent", "start", "/app"]`;
56
- const layoutNote = prefix
57
- ? `The whole directory is the agent's workspace; the agent itself lives in ${prefix}`
58
- : `The directory IS the agent — it is also its own workspace.`;
59
- // apt layer right after FROM (cached across code changes): the agent's tools may shell out to git etc.,
60
- // which node:22-slim lacks. Debian default repos only — a package needing a custom repo (gh) or a
61
- // different base is the operator's own Dockerfile (kept if present). deploy.apt is package-name-validated.
34
+ const at = (cmd) => `cd ${dir} && ${cmd}`;
35
+ // The entrypoint.
36
+ const bunCmd = `["sh", "-c", "cd ${dir} && exec bun run fastagent start /app"]`;
37
+ const layoutNote = `The whole directory is the agent's workspace; the agent itself lives in ${prefix}`;
38
+ // apt layer right after FROM (cached across code changes): the agent's tools may shell out to git etc., which
39
+ // node:22-slim lacks.
62
40
  const apt = aptLayer(input.apt);
63
- // PIN the agent into the image. The container re-resolves placement at `/app` (the baked workspace),
64
- // and a workspace may hold SEVERAL agents — all of which ship, since the build context is the whole
65
- // tree. Without this the image would pick by the container's own rules (the `fastagent`-named one, or
66
- // nothing) instead of the agent this deploy was FOR: the artifact would depend on the builder's
67
- // environment, which is the one thing it must never do. Baked whenever the agent sits inside the
68
- // workspace, single agent or not — the value is a fact of this image, and asserting it means a build
69
- // context that dropped the agent fails loudly rather than serving a sibling.
70
- const pin = prefix ? `ENV FASTAGENT_AGENT=${prefix.replace(/\/$/, "")}\n` : "";
71
- // No package.json → pure markdown/skills agent: install the pinned CLI GLOBALLY and run `fastagent`
72
- // from PATH. That needs npm + a global bin, which live on node:22-slim, NOT on oven/bun — so this path
73
- // pins node:22-slim regardless of input.runtime (a stray bun lockfile with no package.json would
74
- // otherwise select a bun base here and crash the `npm i -g` build). input.runtime is meaningful only
75
- // for a code agent, so the bun-vs-node base below is scoped to that path.
41
+ // PIN the agent into the image.
42
+ const pin = `ENV FASTAGENT_AGENT=${dir}\n`;
43
+ // The caches stay off the volume: they are rebuildable, and a host mount is the slow disk.
44
+ const deployment = `ENV FASTAGENT_RELEASE_FILE=/app/${into(RELEASE_FILE)}\nENV FASTAGENT_STORAGE_DIR=/data\nENV npm_config_cache=/tmp/fastagent/npm\nENV BUN_INSTALL_CACHE_DIR=/tmp/fastagent/bun\n`;
45
+ // No package.json → pure markdown/skills agent: install the pinned CLI GLOBALLY and run `fastagent` from PATH.
76
46
  if (!input.hasPackageJson) {
77
47
  return `${GENERATED_DOCKERFILE_MARKER}. ${layoutNote}
78
48
  # npm-based (a markdown/skills agent installs the pinned CLI globally; node:22-slim has npm).
79
49
  FROM node:22-slim
80
50
  ${apt}WORKDIR /app
81
- ${pin}RUN npm i -g @fastagent-sh/fastagent@${input.version}
51
+ ${deployment}${pin}RUN npm i -g @fastagent-sh/fastagent@${input.version}
82
52
  COPY . .
83
53
  CMD ["fastagent", "start", "/app"]
84
54
  `;
@@ -92,56 +62,37 @@ CMD ["fastagent", "start", "/app"]
92
62
  ${note}
93
63
  FROM ${base}
94
64
  ${apt}WORKDIR /app
95
- ${pin}`;
96
- // Install ALL deps (no --omit=dev / --production): a repo-as-agent (e.g. an Astro site it operates on)
97
- // needs its full toolchain — the build/check tools that live in devDependencies — to do its work, and
98
- // we can't tell a repo-as-agent from a purpose-built agent, so the safe default keeps everything.
99
- // Only the AGENT's deps are installed — the workspace's own deps are the agent's runtime concern
100
- // (it can install them in its workspace when its job needs them).
65
+ ${deployment}${pin}`;
66
+ // Install ALL deps (no --omit=dev / --production): a repo-as-agent (e.g. an Astro site it operates on) needs its
67
+ // full toolchain.
68
+ //
69
+ // `*.tgz` RIDES ALONG with the manifest, because the install layer runs BEFORE `COPY . .` and a
70
+ // `"dep": "file:./something.tgz"` resolves against the directory it is installing in. Without this the file is
71
+ // not there yet and the build fails on a dependency the author can see sitting in the agent directory. It is
72
+ // also what lets a live probe deploy the CHECKOUT (`npm pack` output) rather than the published release.
101
73
  if (isBun) {
102
- // `--frozen-lockfile` needs bun.lock and hard-fails without it; fall back to a plain `bun install`
103
- // (resolves at build time — not reproducible; the CLI warns to commit the lockfile).
104
- // `bun run` executes the LOCAL node_modules/.bin entry only — never the registry. A bare
105
- // `bunx fastagent` would fall back to installing the npm package named `fastagent`, which is an
106
- // unrelated third-party package (ours is the scoped @fastagent-sh/fastagent).
74
+ // `--frozen-lockfile` needs bun.lock and hard-fails without it.
107
75
  const install = input.hasLockfile ? "bun install --frozen-lockfile" : "bun install";
108
- return `${head}COPY ${into("package.json")} ${into("bun.lock*")} ./${prefix}
76
+ return `${head}COPY ${into("package.json")} ${into("bun.lock*")} ${into("*.tgz")} ./${prefix}
109
77
  RUN ${at(install)}
110
78
  COPY . .
111
79
  CMD ${bunCmd}
112
80
  `;
113
81
  }
114
- // `npm ci` requires a lockfile and hard-fails without one (a common `init --no-install` agent);
115
- // fall back to `npm install` when there is none so the build never breaks. Caveat: `npm install`
116
- // resolves caret ranges at build time, so THIS branch is NOT reproducible — unlike the pinned
117
- // `npm ci` (lockfile) and pinned global (markdown) paths. The CLI warns to commit a lockfile.
118
- // The explicit local bin, not `npx fastagent`: npx falls back to fetching the npm package named
119
- // `fastagent` when the dep is absent — an unrelated third-party package (ours is scoped). The local
120
- // path fails fast and visibly instead.
82
+ // `npm ci` requires a lockfile and hard-fails without one (a common `init --no-install` agent); fall back to `npm
83
+ // install` when there is none so the build never breaks.
121
84
  const install = input.hasLockfile ? "npm ci" : "npm install";
122
- return `${head}COPY ${into("package.json")} ${into("package-lock.json*")} ./${prefix}
85
+ return `${head}COPY ${into("package.json")} ${into("package-lock.json*")} ${into("*.tgz")} ./${prefix}
123
86
  RUN ${at(install)}
124
87
  COPY . .
125
88
  CMD ["./${into("node_modules/.bin/fastagent")}", "start", "/app"]
126
89
  `;
127
90
  }
128
- /** Patterns are RECURSIVE (`**​/`) on purpose — dockerignore patterns are root-anchored (unlike
129
- * .gitignore), and a baked workspace can hold nested projects: a bare `node_modules` would upload
130
- * their build-machine deps (macOS binaries!) and a bare `.env` would bake their secrets into the
131
- * image. `.secrets`/`.state` are fastagent machinery — credential contents travel through the host's
132
- * secret store and state lives on the volume, so neither may enter an image. The two tracked secrets
133
- * scaffolds (`.env.example` + `.gitignore`) are the narrow exception: they carry no values and must stay
134
- * beside the shipped `.git`, or the image starts with tracked deletions. `.cache` is generic hygiene
135
- * (a baked project's own build cache), not a fastagent directory.
136
- * `.git` is deliberately SHIPPED: the deployed agent's write-back (pull/commit/push) needs the
137
- * repo's history+remote — the WYSIWYG bake's freshness/durability loop runs through git, driven by
138
- * the agent itself, not by deploy machinery. Shipping `.git` is only half of that loop: preflight
139
- * bakes the git BINARY into the generated image iff the workspace ships a `.git` (the `shipsGit`
140
- * fact); a non-git workspace that still needs git declares config.deploy.apt. */
91
+ /** Patterns are RECURSIVE (`**​/`) on purpose. */
141
92
  const dockerignore = (input) => DOCKERIGNORE_BASE +
142
93
  (input.machineryPaths ?? [])
143
- // Skip what the name-based rules above already cover: the default `<agent>/.secrets` / `<agent>/.state`
144
- // and anything inside them. What remains is a relocated dir whose NAME those rules cannot match.
94
+ // Skip what the name-based rules above already cover: the default `<agent>/.secrets` / `<agent>/.state` and
95
+ // anything inside them.
145
96
  .filter((p) => [`${input.agentPrefix}${SECRETS_DIRNAME}`, `${input.agentPrefix}${STATE_DIRNAME}`].every((covered) => p !== covered && !p.startsWith(`${covered}/`)))
146
97
  .map((p) => `# resolved machinery path (FASTAGENT_SECRETS_DIR / FASTAGENT_AUTH_PATH / FASTAGENT_STATE_DIR)\n/${p}\n`)
147
98
  .join("");
@@ -159,20 +110,18 @@ const DOCKERIGNORE_BASE = `${GENERATED_DOCKERIGNORE_MARKER}. Delete this line to
159
110
  # (the generated image installs the git binary when this directory ships a .git; otherwise
160
111
  # add deploy.apt ["git"]). For a smaller image with no git needs, add a ".git" line here.
161
112
  `;
162
- /**
163
- * The Dockerfile + ignore artifacts — spread into any host's artifact list. Everything is prefixed with
164
- * {@link ContainerInput.agentPrefix}, so a NESTED agent's Dockerfile lands under `fastagent/` (never
165
- * colliding with one the workspace already owns) while an agent that IS the workspace lands it at the root, where it IS
166
- * the agent's own file. The ignore ships in TWO forms because context packing is host-CLI-owned and
167
- * inconsistent: (1) a ROOT `.dockerignore` — the only form flyctl/railway's own context packers
168
- * reliably read (kept if the workspace already has one — preflight then checks the machinery/secret
169
- * excludes it must carry) — and (2) a per-Dockerfile `Dockerfile.dockerignore` for plain docker/buildx
170
- * builds. When the agent sits inside the workspace that root file is the ONE write deploy makes outside the agent dir, and
171
- * only at deploy time — without it the host CLI's packer would bake `.secrets/` into the image.
172
- */
113
+ /** The Dockerfile + ignore artifacts — spread into any host's artifact list. */
173
114
  export function containerArtifacts(input) {
174
115
  const ignore = dockerignore(input);
116
+ const release = {
117
+ version: 1,
118
+ id: input.releaseId,
119
+ agent: input.agentPrefix.replace(/\/$/, ""),
120
+ ...(input.modelSpec !== undefined ? { model: input.modelSpec } : {}),
121
+ };
122
+ parseDeploymentRelease(JSON.stringify(release));
175
123
  return [
124
+ { path: `${input.agentPrefix}${RELEASE_FILE}`, content: `${JSON.stringify(release, null, 2)}\n` },
176
125
  { path: `${input.agentPrefix}Dockerfile`, content: dockerfile(input) },
177
126
  { path: ".dockerignore", content: ignore },
178
127
  { path: `${input.agentPrefix}Dockerfile.dockerignore`, content: ignore },
@@ -1,24 +1,25 @@
1
- /**
2
- * `fastagent deploy docker` — the local-Docker plan. The application image stays ingress-neutral:
3
- * this target generates only the shared Dockerfile plus a small Compose service for the FastAgent
4
- * process, its loopback port, exact environment-variable names, and one persistent state volume.
5
- * `--tunnel` can add an ephemeral Cloudflare Quick Tunnel service; durable ingress remains operator-owned.
6
- */
1
+ /** `fastagent deploy docker` — the local-Docker plan. */
7
2
  import type { DeclaredChannel } from "../../channels/discover.ts";
8
3
  import { type Artifact, type ContainerInput } from "../container.ts";
4
+ import { type DeploymentSecret } from "../secrets.ts";
9
5
  export interface DockerPlanInput extends ContainerInput {
10
6
  /** Stable Compose project name, sanitized by {@link toDockerProjectName}. */
11
7
  projectName: string;
12
- /** Container + default host port. The host binding is loopback-only by default. */
8
+ /** Container + default host port. */
13
9
  port: number;
14
10
  /** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
15
11
  modelAuth: string | undefined;
16
- /** Every declared channel and its ingress — the source of the secret list and the ingress note. */
12
+ /** Every declared channel and its ingress — the source of the ingress note. */
17
13
  channels: readonly DeclaredChannel[];
18
- /** Generate an optional Cloudflare Quick Tunnel service in Compose. Generation only; `--run` starts it. */
14
+ /** Generate an optional Cloudflare Quick Tunnel service in Compose. */
19
15
  tunnel: boolean;
20
- /** Extra environment-variable names declared in config.deploy.secrets. */
21
- extraSecrets?: string[];
16
+ /**
17
+ * The runbook's variable list (`deploymentSecrets`): what must have a value, then everything else the value file
18
+ * carries.
19
+ */
20
+ secrets?: readonly DeploymentSecret[];
21
+ /** The value file as the pre-flight resolved it (it follows `FASTAGENT_SECRETS_DIR`), workspace-relative. */
22
+ valueFile: string;
22
23
  }
23
24
  export interface DockerPlan {
24
25
  /** fastagent.compose.yml + the shared Dockerfile/ignore artifacts. */
@@ -1,6 +1,7 @@
1
1
  import { webhookPaths } from "../channel-ingress.js";
2
2
  import { containerArtifacts } from "../container.js";
3
- import { deploymentSecrets, isEnvKey } from "../secrets.js";
3
+ import { isEnvKey } from "../secrets.js";
4
+ import { SECRETS_DIRNAME } from "../../paths.js";
4
5
  const MOUNT = "/data";
5
6
  const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
6
7
  const GENERATED_COMPOSE_MARKER = "# Generated by `fastagent deploy docker`";
@@ -30,14 +31,9 @@ function composeInterpolation(name) {
30
31
  return `\${${name}:-}`;
31
32
  }
32
33
  function composeYaml(input) {
33
- const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
34
- // Always leave the auth-seed seam in the committed topology. `--run` uses it for OAuth/stored auth;
35
- // it is empty otherwise. Values never land in this file — Compose interpolates them at invocation.
36
- const envNames = [...new Set([...secrets.map((secret) => secret.name), "FASTAGENT_AUTH_SEED"])];
37
- const secretEnv = envNames.map((name) => ` ${name}: "${composeInterpolation(name)}"`).join("\n");
38
- // Compose sits beside the Dockerfile, under the agent prefix; the build context is always the
39
- // WORKSPACE, so it climbs back out of the prefix (`..` per level, `.` when there is none).
40
- const context = input.agentPrefix ? ".." : ".";
34
+ // Compose sits beside the Dockerfile, under the agent prefix; the build context is always the WORKSPACE, so it
35
+ // climbs back out of the one-level prefix deploy requires.
36
+ const context = "..";
41
37
  const dockerfile = `${input.agentPrefix}Dockerfile`;
42
38
  const tunnelService = input.tunnel
43
39
  ? `
@@ -57,6 +53,12 @@ function composeYaml(input) {
57
53
  # bypass it; preserve any operator NO_PROXY entries after the required service/loopback names.
58
54
  NO_PROXY: "agent,localhost,127.0.0.1,${composeInterpolation("NO_PROXY")}"
59
55
  no_proxy: "agent,localhost,127.0.0.1,${composeInterpolation("no_proxy")}"
56
+ # Quieter than the agent, but bounded for the same reason: nothing else on this host bounds it.
57
+ logging:
58
+ driver: json-file
59
+ options:
60
+ max-size: "10m"
61
+ max-file: "3"
60
62
  restart: "no"
61
63
  `
62
64
  : "";
@@ -71,14 +73,36 @@ services:
71
73
  dockerfile: ${dockerfile}
72
74
  ports:
73
75
  - "127.0.0.1:${input.port}:${input.port}"
76
+ # The deployed environment's own declaration, read by the container itself. \`env_file\`, not per-name
77
+ # \${NAME:-} interpolation: interpolation resolves from the SHELL or the project .env, which is exactly the
78
+ # source a deployment must not have. A FIXED path, never the builder's FASTAGENT_SECRETS_DIR — this file is a
79
+ # committed artifact and must mean the same thing on every machine. \`deploy\` creates it if it is missing.
80
+ env_file:
81
+ - ${SECRETS_DIRNAME}/.env
74
82
  environment:
75
83
  PORT: "${input.port}"
76
- # Machinery on the ONE state volume: mutable state and (seeded, possibly rotated) secrets.
84
+ # Machinery on the ONE state volume: mutable state and (seeded, possibly rotated) secrets. Pinned AFTER
85
+ # env_file so a local path in that file (the scaffold lists these) cannot send the container's state, sessions
86
+ # or credentials somewhere outside the volume — or at a host path that does not exist here at all.
77
87
  FASTAGENT_STATE_DIR: "${MOUNT}/.state"
78
88
  FASTAGENT_SECRETS_DIR: "${MOUNT}/.secrets"
79
- ${secretEnv}
89
+ FASTAGENT_AUTH_PATH: "${MOUNT}/.secrets/auth.json"
90
+ # The ONE value that is not in the value file: \`--run\` mints it from the local auth.json. Left as a seam in
91
+ # the committed topology so a hand-run \`up\` can supply it the same way — from the ENVIRONMENT of that \`up\`.
92
+ # Writing this one key into the value file does nothing: \`environment:\` is applied after \`env_file\`, so the
93
+ # line below would blank it and the container would start without seeding auth.json.
94
+ FASTAGENT_AUTH_SEED: "${composeInterpolation("FASTAGENT_AUTH_SEED")}"
80
95
  volumes:
81
96
  - state:${MOUNT}
97
+ # The agent's log is where a failure's reason is recorded (a scheduled fire logs the detail; what the turn
98
+ # SAID is in its session, not here), and Docker's default json-file driver never rotates — so on this host
99
+ # the bound has to be written down. Fly and Railway bound their own log retention; AgentCore does NOT
100
+ # (CloudWatch keeps data indefinitely until an operator sets a retention policy — its runbook says so).
101
+ logging:
102
+ driver: json-file
103
+ options:
104
+ max-size: "10m"
105
+ max-file: "3"
82
106
  restart: unless-stopped
83
107
  ${tunnelService}
84
108
  volumes:
@@ -89,33 +113,28 @@ volumes:
89
113
  export function planDockerDeploy(input) {
90
114
  const composePath = `${input.agentPrefix}${DOCKER_COMPOSE_FILE}`;
91
115
  const artifacts = [{ path: composePath, content: composeYaml(input) }, ...containerArtifacts(input)];
116
+ // One spelling for every command: the generated file names the value file itself (`env_file`), so no command
117
+ // needs a flag to reach the deployed environment's declaration.
92
118
  const compose = `docker compose -f ${composePath}`;
93
- const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
94
- const required = secrets.filter((secret) => secret.required);
95
- const optional = secrets.filter((secret) => !secret.required);
119
+ const secrets = input.secrets ?? [];
96
120
  const paths = webhookPaths(input.channels);
97
121
  const runbook = [
98
122
  `# Run FastAgent in local Docker. ${composePath} / Dockerfile(.dockerignore) are generated above.`,
99
123
  `# Existing artifacts are authoritative and kept; edit Dockerfile or ${composePath} freely.`,
100
124
  `# Prereqs: Docker Engine/Desktop with Compose >= ${MIN_DOCKER_COMPOSE_VERSION} (\`docker compose version\`).`,
101
125
  ];
102
- if (required.length > 0) {
103
- runbook.push(`# Required environment values (put them in the agent's .secrets/.env or export them):`, ...required.map((secret) => `# ${secret.name}: ${secret.hint}`));
104
- }
105
- if (optional.length > 0) {
106
- runbook.push(`# Optional environment values:`, ...optional.map((secret) => `# ${secret.name}: ${secret.hint}`));
126
+ if (secrets.length > 0) {
127
+ runbook.push(`# Environment: the container reads ${input.valueFile} whole (Compose \`env_file\`) — that file declares the`, `# deployed environment (in CI, write it before running the command):`, ...secrets.map((secret) => `# ${secret.name}: ${secret.hint}`));
107
128
  }
108
129
  if (!isEnvKey(input.modelAuth)) {
109
130
  runbook.push(input.modelAuth === undefined
110
131
  ? `# Model auth: none found locally — run \`fastagent login\` or set a provider API key before --run.`
111
132
  : `# Model auth: local source is "${input.modelAuth}" — \`--run\` carries auth.json as FASTAGENT_AUTH_SEED.`, `# For a manual Compose run, provide a provider API key or set FASTAGENT_AUTH_SEED yourself.`);
112
133
  }
113
- runbook.push(input.agentPrefix
114
- ? `# Run from the WORKSPACE ROOT (the directory containing ${input.agentPrefix}).`
115
- : `# Run from this directory — it is both the agent and its workspace.`, `# The build bakes the whole directory as the agent's workspace; only the agent's own`, `# dependencies (${input.agentPrefix}package.json) are installed.`);
116
- runbook.push(``, `# Build/create/reconcile the service. Re-running this is the complete local redeploy; state stays`, `# in the Compose volume mounted at ${MOUNT}.`, `${compose} up -d --build`, `curl --fail http://127.0.0.1:${input.port}/health`, ``, `# Operate it:`, `${compose} logs -f agent`, `${compose} ps`, `${compose} down # stops containers; keeps the state volume`, `# ${compose} down -v # DESTRUCTIVE: also deletes auth, sessions, and channel state`);
134
+ runbook.push(`# Run from the WORKSPACE ROOT (the directory containing ${input.agentPrefix}).`, `# The build bakes the whole directory as the agent's workspace; only the agent's own`, `# dependencies (${input.agentPrefix}package.json) are installed.`);
135
+ runbook.push(``, `# Before building a new definition release, run \`fastagent deploy docker\` to refresh its manifest.`, `# The Compose volume at ${MOUNT} retains the workspace, state and credentials.`, `${compose} up -d --build`, `curl --fail http://127.0.0.1:${input.port}/health`, ``, `# Operate it:`, `${compose} logs -f agent`, `${compose} ps`, `${compose} down # stops containers; keeps the state volume`, `# ${compose} down -v # DESTRUCTIVE: deletes workspace, credentials and all agent state`);
117
136
  if (input.tunnel) {
118
- runbook.push(``, `# Quick Tunnel: \`--run\` starts the Compose tunnel service and reads its ephemeral public URL.`, `# Telegram/Feishu/Lark and locally onboarded Slack auto-register; GitHub/manual Slack print console URLs. Re-run after the`, `# tunnel container/Docker daemon restarts: a new Quick Tunnel URL must replace the old webhook URL.`, `${compose} logs -f tunnel`);
137
+ runbook.push(``, `# Quick Tunnel: \`--run\` starts the Compose tunnel service and reads its ephemeral public URL.`, `# Telegram/Feishu/Lark and locally onboarded Slack auto-register; manual Slack prints its console URL. Re-run after the`, `# tunnel container/Docker daemon restarts: a new Quick Tunnel URL must replace the old webhook URL.`, `${compose} logs -f tunnel`);
119
138
  }
120
139
  else if (paths.length > 0) {
121
140
  runbook.push(``, `# Public ingress is operator-owned: add your tunnel/proxy/DNS/TLS, then configure the`, `# detected default endpoint(s): ${paths.map((path) => `https://<your-domain>${path}`).join(", ")}.`, `# If your channels/*.ts glue remaps a route, use that path instead.`);
@@ -5,15 +5,19 @@ export interface DockerRunPlan {
5
5
  composeFile: string;
6
6
  /** Container port from config; used to ask Compose for the effective published host port. */
7
7
  port: number;
8
- /** Values interpolated by Compose. Keys/values are passed in the child environment, never argv. */
8
+ /**
9
+ * What `--run` assembled. The container reads the value file itself through the generated `env_file`, so the only
10
+ * entry that has to travel through this process is `FASTAGENT_AUTH_SEED`, which is minted here; the rest is
11
+ * reported to the operator and otherwise unused.
12
+ */
9
13
  secrets: Record<string, string>;
10
- /** Required names with no local value; gate before build/create. */
14
+ /** Declared names the value file supplies no value for — the run gates on these before any side effect. */
11
15
  missingSecrets: string[];
16
+ /** That value file, workspace-relative, so the gate names the file this deploy actually read. */
17
+ valueFile: string;
12
18
  /** Neither an env-key credential nor a readable auth.json is available. */
13
19
  needsModelCredential: boolean;
14
- /** Register the deployment's webhooks against the tunnel URL, reporting what each registrar
15
- * answered. REQUIRED, not optional: an absent one would delete this whole step in silence, which
16
- * is the failure this driver's gate exists to end. A topology with no tunnel never calls it. */
20
+ /** Register the deployment's webhooks against the tunnel URL, reporting what each registrar answered. */
17
21
  announce: DockerAnnounce;
18
22
  /** `--tunnel` was requested for this run; a kept Compose file must actually contain that service. */
19
23
  requireTunnel: boolean;
@@ -23,24 +27,27 @@ export type DockerRunOutcome = {
23
27
  url?: string;
24
28
  tunnelUrl?: string;
25
29
  }
26
- /** `url`/`tunnelUrl` travel with a gate too: Compose is up, so the operator still needs to know
27
- * where it is and what to re-run. */
30
+ /**
31
+ * `url`/`tunnelUrl` travel with a gate too: Compose is up, so the operator still needs to know where it is and what
32
+ * to re-run.
33
+ */
28
34
  | {
29
35
  ok: false;
30
36
  gate: string;
31
37
  url?: string;
32
38
  tunnelUrl?: string;
33
39
  };
34
- /** Register the deployment's webhooks against its public URL, reporting what each registrar
35
- * answered. Injected so the driver stays free of channel specifics — and so a test can fail one. */
40
+ /** Register the deployment's webhooks against its public URL, reporting what each registrar answered. */
36
41
  type DockerAnnounce = (baseUrl: string) => Promise<{
37
42
  kind: string;
38
43
  outcome: RegistrationOutcome;
39
44
  }[]>;
40
- export type DockerHealthProbe = (healthUrl: string) => Promise<boolean>;
41
- /** A published Quick Tunnel URL, and whether its tunnel ever reported an edge connection. Both, because
42
- * a URL that never connected still gets served (retrying meets the same network) and the operator has
43
- * to be told which of the two they are looking at. */
45
+ /**
46
+ * `stillStarting` answers whether the agent container is still up; a probe that ignores it simply waits out its whole
47
+ * budget.
48
+ */
49
+ export type DockerHealthProbe = (healthUrl: string, stillStarting: () => Promise<boolean>) => Promise<boolean>;
50
+ /** A published Quick Tunnel URL, and whether its tunnel ever reported an edge connection. */
44
51
  export interface ComposeTunnel {
45
52
  url: string;
46
53
  connected: boolean;
@@ -49,23 +56,13 @@ export type DockerTunnelUrlProbe = (docker: CliRunner, composeFile: string, env:
49
56
  /** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
50
57
  export declare function localUrlFromComposePort(stdout: string): string | undefined;
51
58
  /**
52
- * Poll the detached cloudflared service's logs until its Quick Tunnel URL is assigned AND the tunnel
53
- * reports an edge connection. Waiting for the second is the same requirement `startCloudflareTunnel`
54
- * has and for the same reason — the hostname does not exist until then (#435), and this driver hands
55
- * the URL straight to `announce`. Returns a URL that never connected rather than nothing: the
56
- * registrars downstream report their own outcome, and a gate saying "no URL" would misname it — but it
57
- * says WHICH it is returning, because the caller owes the operator that sentence.
59
+ * Poll the detached cloudflared service's logs until its Quick Tunnel URL is assigned AND the tunnel reports an edge
60
+ * connection.
58
61
  */
59
62
  export declare function waitForComposeTunnelUrl(docker: CliRunner, composeFile: string, env: NodeJS.ProcessEnv, options?: {
60
63
  attempts?: number;
61
64
  intervalMs?: number;
62
65
  sleep?: (ms: number) => Promise<void>;
63
66
  }): Promise<ComposeTunnel | undefined>;
64
- /**
65
- * Drive Docker Compose. A custom Compose file remains authoritative: the driver invokes it as-is and
66
- * only assumes the generated service contract (`agent`, config's container port) for optional URL/
67
- * readiness reporting. If the service intentionally has no host-published port, a successful running
68
- * service is still success (an operator-owned sidecar/reverse proxy may be its only ingress).
69
- */
70
67
  export declare function deployDockerRun(plan: DockerRunPlan, docker: CliRunner, log: (message: string) => void, healthProbe?: DockerHealthProbe, tunnelUrlProbe?: DockerTunnelUrlProbe): Promise<DockerRunOutcome>;
71
68
  export {};
@@ -1,12 +1,9 @@
1
- /**
2
- * `fastagent deploy docker --run` — reconcile the generated/user-owned Compose application locally.
3
- * Compose owns container/network/volume lifecycle; this driver owns only actionable gates, secret/auth
4
- * carry through the child environment (never argv), and a readiness check on the published loopback port.
5
- */
1
+ /** `fastagent deploy docker --run` — reconcile the generated/user-owned Compose application locally. */
6
2
  import { waitForHealth } from "../../channels/wait-health.js";
7
3
  import { TUNNEL_DNS_LAG_MS, hasTunnelConnection, parseTunnelUrl } from "../../tunnel.js";
8
4
  import { registrationGate } from "../registration-gate.js";
9
5
  import { MIN_DOCKER_COMPOSE_VERSION } from "./plan.js";
6
+ import { missingValuesGate } from "../secrets.js";
10
7
  /** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
11
8
  export function localUrlFromComposePort(stdout) {
12
9
  const line = stdout
@@ -16,15 +13,15 @@ export function localUrlFromComposePort(stdout) {
16
13
  const port = line?.match(/:(\d+)$/)?.[1];
17
14
  return port ? `http://127.0.0.1:${port}` : undefined;
18
15
  }
19
- const defaultHealthProbe = (healthUrl) => waitForHealth(healthUrl, 30_000, 500);
16
+ /**
17
+ * The FIRST boot seeds the whole workspace onto the volume (the image's `node_modules` included) before it binds a
18
+ * port, so this budget covers a copy on a slow Docker Desktop disk, not a listen.
19
+ */
20
+ const defaultHealthProbe = (healthUrl, stillStarting) => waitForHealth(healthUrl, 180_000, 500, stillStarting);
20
21
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
21
22
  /**
22
- * Poll the detached cloudflared service's logs until its Quick Tunnel URL is assigned AND the tunnel
23
- * reports an edge connection. Waiting for the second is the same requirement `startCloudflareTunnel`
24
- * has and for the same reason — the hostname does not exist until then (#435), and this driver hands
25
- * the URL straight to `announce`. Returns a URL that never connected rather than nothing: the
26
- * registrars downstream report their own outcome, and a gate saying "no URL" would misname it — but it
27
- * says WHICH it is returning, because the caller owes the operator that sentence.
23
+ * Poll the detached cloudflared service's logs until its Quick Tunnel URL is assigned AND the tunnel reports an edge
24
+ * connection.
28
25
  */
29
26
  export async function waitForComposeTunnelUrl(docker, composeFile, env, options = {}) {
30
27
  const compose = ["compose", "-f", composeFile];
@@ -45,16 +42,13 @@ export async function waitForComposeTunnelUrl(docker, composeFile, env, options
45
42
  return assigned ? { url: assigned, connected: false } : undefined;
46
43
  }
47
44
  const defaultTunnelUrlProbe = (docker, composeFile, env) => waitForComposeTunnelUrl(docker, composeFile, env);
48
- /**
49
- * Drive Docker Compose. A custom Compose file remains authoritative: the driver invokes it as-is and
50
- * only assumes the generated service contract (`agent`, config's container port) for optional URL/
51
- * readiness reporting. If the service intentionally has no host-published port, a successful running
52
- * service is still success (an operator-owned sidecar/reverse proxy may be its only ingress).
53
- */
54
45
  export async function deployDockerRun(plan, docker, log, healthProbe = defaultHealthProbe, tunnelUrlProbe = defaultTunnelUrlProbe) {
55
46
  const gate = (message) => ({ ok: false, gate: message });
56
47
  const compose = ["compose", "-f", plan.composeFile];
57
- const env = plan.secrets;
48
+ // The ONE value that is not in the value file, so the ONE that has to cross this process. Set even when absent, so
49
+ // a same-named variable in the builder's shell cannot interpolate into the container in its place — `spawnRunner`
50
+ // merges over `process.env`.
51
+ const env = { FASTAGENT_AUTH_SEED: plan.secrets.FASTAGENT_AUTH_SEED ?? "" };
58
52
  // CLI/plugin gate first: unlike a daemon error, spawn ENOENT becomes 127 at the shared runner seam.
59
53
  const version = await docker(["compose", "version"], { capture: true });
60
54
  if (version.code === 127) {
@@ -67,14 +61,20 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
67
61
  if (plan.needsModelCredential) {
68
62
  return gate("no model credential — run `fastagent login`, or set a provider API key in .env, then re-run");
69
63
  }
70
- if (plan.missingSecrets.length > 0) {
71
- return gate(`no local value for: ${plan.missingSecrets.join(", ")} — set them in .env (or the environment) and re-run`);
64
+ const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
65
+ if (missingValues)
66
+ return gate(missingValues);
67
+ // Name what the container must find, and WHERE — the generated Compose reads the value file itself, so this run
68
+ // hands Compose nothing but the seed. Saying "passing N secrets to Compose" would be false, and doubly so on a
69
+ // hand-owned Compose file that has no `env_file` entry at all.
70
+ const secretNames = Object.keys(plan.secrets).filter((name) => name !== "FASTAGENT_AUTH_SEED");
71
+ if (secretNames.length > 0) {
72
+ log(`${secretNames.length} value(s) the container reads from ${plan.valueFile}: ${secretNames.join(", ")}`);
72
73
  }
73
74
  if ((await docker(["info"], { capture: true })).code !== 0) {
74
75
  return gate("Docker daemon is unavailable — start Docker Engine/Desktop, then re-run");
75
76
  }
76
- // The file on disk is authoritative. Inspect its actual services before any build/create side effect,
77
- // both to protect the `agent` run contract and to catch `--tunnel` against a kept non-tunnel topology.
77
+ // The file on disk is authoritative.
78
78
  const configured = await docker([...compose, "config", "--services"], { capture: true, env });
79
79
  if (configured.code !== 0) {
80
80
  return gate(`could not load ${plan.composeFile} — generated files require Docker Compose >= ` +
@@ -89,9 +89,7 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
89
89
  return gate(`--tunnel was requested but the kept ${plan.composeFile} has no "tunnel" service — ` +
90
90
  `edit it, delete it and regenerate, or pass --force`);
91
91
  }
92
- // Quick Tunnel logs are the control-plane output (the assigned URL). Remove its old container first so
93
- // a rerun cannot read a stale URL from accumulated logs; `up` below creates one fresh tunnel, then the
94
- // CLI registers that URL. The app container/volume are untouched.
92
+ // Quick Tunnel logs are the control-plane output (the assigned URL).
95
93
  if (hasTunnel) {
96
94
  log("recreating the ephemeral tunnel service…");
97
95
  if ((await docker([...compose, "rm", "-s", "-f", "tunnel"], { env })).code !== 0) {
@@ -102,8 +100,7 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
102
100
  if ((await docker([...compose, "up", "-d", "--build"], { env })).code !== 0) {
103
101
  return gate(`\`docker compose up\` failed — see the Docker output above; fix ${plan.composeFile} and re-run`);
104
102
  }
105
- // Detached `up` can return 0 just before a bad command exits. Verify the expected service is actually
106
- // running so a broken custom Dockerfile/CMD cannot look deployed. Compose restart loops are excluded.
103
+ // Detached `up` can return 0 just before a bad command exits.
107
104
  const running = await docker([...compose, "ps", "--status", "running", "--services"], {
108
105
  capture: true,
109
106
  env,
@@ -116,7 +113,6 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
116
113
  return gate(`the Compose service "tunnel" is not running — inspect with \`docker compose -f ${plan.composeFile} logs tunnel\``);
117
114
  }
118
115
  // A user-owned topology may deliberately remove the host port and expose only through its own ingress.
119
- // In that case Compose `port` is absent/non-zero: service-running is the available readiness floor.
120
116
  const published = await docker([...compose, "port", "agent", String(plan.port)], { capture: true, env });
121
117
  const url = published.code === 0 ? localUrlFromComposePort(published.stdout) : undefined;
122
118
  if (!url) {
@@ -124,7 +120,17 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
124
120
  }
125
121
  else {
126
122
  const healthUrl = `${url}/health`;
127
- if (!(await healthProbe(healthUrl))) {
123
+ // Throttled, and an unreadable answer reads as "still starting": the health poll runs twice a second, and a
124
+ // `compose ps` per poll would cost more than the wait it shortens.
125
+ let lastCheck = Date.now();
126
+ const stillStarting = async () => {
127
+ if (Date.now() - lastCheck < 5_000)
128
+ return true;
129
+ lastCheck = Date.now();
130
+ const ps = await docker([...compose, "ps", "--status", "running", "--services"], { capture: true, env });
131
+ return ps.code !== 0 || ps.stdout.split(/\s+/).includes("agent");
132
+ };
133
+ if (!(await healthProbe(healthUrl, stillStarting))) {
128
134
  return gate(`agent did not become healthy at ${healthUrl} — inspect \`docker compose -f ${plan.composeFile} logs agent\``);
129
135
  }
130
136
  }
@@ -135,19 +141,15 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
135
141
  if (!tunnel) {
136
142
  return gate(`tunnel did not publish a Quick Tunnel URL — inspect \`docker compose -f ${plan.composeFile} logs tunnel\``);
137
143
  }
138
- // The same sentence `startCloudflareTunnel` prints for the same state, and needed MORE here: this
139
- // cloudflared runs in a container, so the author cannot see the logs that would explain the
140
- // registration failures about to follow. Silence sends them to debug the platform for what is a
141
- // local tunnel that never came up.
144
+ // The same sentence `startCloudflareTunnel` prints for the same state, and needed MORE here.
142
145
  if (!tunnel.connected) {
143
146
  log(`warn: the tunnel service never reported an edge connection for ${tunnel.url} — announcing it anyway. ` +
144
147
  `Nothing reaches a tunnel that has not connected, so a webhook registration that cannot resolve the ` +
145
148
  `host is this, not the platform. Inspect \`docker compose -f ${plan.composeFile} logs tunnel\`.`);
146
149
  }
147
150
  const tunnelUrl = tunnel.url;
148
- // Registration lives HERE, like every other host's driver, and not at the CLI: this is the layer
149
- // that owns the outcome, so it is the layer that can gate on one. While it sat above, docker was
150
- // the one target whose `--run` could exit 0 with a webhook that never registered.
151
+ // Registration lives HERE, like every other host's driver, and not at the CLI: this is the layer that owns the
152
+ // outcome, so it is the layer that can gate on one.
151
153
  const reg = registrationGate(log, `re-run this deploy to retry registration (Compose is already up)`);
152
154
  for (const { kind, outcome } of await plan.announce(tunnelUrl))
153
155
  reg.track(kind, outcome);