@fastagent-sh/fastagent 0.21.1 → 0.22.1

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 +233 -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 +166 -229
  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,46 +1,26 @@
1
- /**
2
- * `fastagent deploy fly` — the Fly.io deploy PLAN, computed from the resolved definition. Pure: facts
3
- * in, artifact contents + an ordered runbook out; the CLI (deploy side effects live there) writes the
4
- * files and prints the runbook.
5
- *
6
- * fastagent owns the two ends only it can know — generate definition-aware artifacts (state root →
7
- * volume, autostop tuned to the turn model, the exact secret list) and the post-deploy webhook step —
8
- * and GUIDES the middle (flyctl app/volume/secrets/deploy) as a precise, values-resolved runbook. By
9
- * default a coding agent (or you) runs flyctl from that runbook; `deploy fly --run` drives it from the
10
- * CLI instead. This module stays pure either way — it produces the plan, never runs flyctl. The runbook
11
- * is a FIRST-deploy sequence: `apps`/`volumes create` are one-time (marked so — re-running would make a
12
- * second volume, the state split it warns against); a redeploy is `fly deploy` alone.
13
- *
14
- * autostop = "suspend": the machine snapshots and suspends when idle (Fly Proxy sees inbound load 0),
15
- * resumes on the next webhook in ~hundreds of ms. A long turn interrupted by an idle-suspend whose
16
- * snapshot is discarded replays on the next start (the Telegram L1 turn store) — at-least-once, the
17
- * documented floor. State on the /data volume survives stop/suspend on the same machine.
18
- */
1
+ /** `fastagent deploy fly` — the Fly.io deploy PLAN, computed from the resolved definition. */
19
2
  import type { DeclaredChannel } from "../../channels/discover.ts";
20
3
  import { type Artifact, type ContainerInput } from "../container.ts";
4
+ import { type DeploymentSecret } from "../secrets.ts";
21
5
  export interface FlyPlanInput extends ContainerInput {
22
6
  /** Fly app name — globally unique, lowercase; the CLI sanitizes it from the dir basename. */
23
7
  appName: string;
24
8
  /** The port the app listens on (config.http.port ?? 8787); fly.toml routes to it. */
25
9
  port: number;
10
+ /** What satisfies model auth locally ({@link probeAuthSource}). */
11
+ modelAuth: string | undefined;
26
12
  /**
27
- * What satisfies model auth locally ({@link probeAuthSource}): an env-var name (`OPENAI_API_KEY`),
28
- * `"OAuth"`/`"stored credential"` (a local login the server can't use), or undefined (unconfigured).
13
+ * Every declared channel and its ingress — the source of the webhook steps, and whether a machine
14
+ * must stay up for an outbound connection.
29
15
  */
30
- modelAuth: string | undefined;
31
- /** Every declared channel and its ingress — the source of the secret list, the webhook steps, and
32
- * whether a machine must stay up for an outbound connection. */
33
16
  channels: readonly DeclaredChannel[];
34
- /** Extra secret env-var names (fastagent.config deploy.secrets) — added to the runbook's secret list. */
35
- extraSecrets?: string[];
36
- /** `auto_stop_machines` — `"suspend"` (default, fast resume) or `"stop"` (cold start). CLI `--stop`. */
37
- autostop: "suspend" | "stop";
38
- /** Allow scaling to zero when idle (default true → `min_machines_running=0`). CLI `--no-scale-to-zero`
39
- * forces one machine up; a github channel forces it too (fire-and-forget turns have no replay). */
40
- scaleToZero: boolean;
41
- /** Time triggers present (schedules/ or selfSchedule) — forces one machine up: cron/wake has no
42
- * external wake-up, so a scaled-to-zero box would sleep through them. */
43
- hasTimeTriggers: boolean;
17
+ /**
18
+ * The runbook's variable list (`deploymentSecrets`): what must have a value, then everything else the value file
19
+ * carries.
20
+ */
21
+ secrets?: readonly DeploymentSecret[];
22
+ /** `routines/` declares a cron — one of the things that forces a machine up (deploy/residency.ts). */
23
+ hasCron: boolean;
44
24
  }
45
25
  export interface FlyPlan {
46
26
  /** fly.toml / Dockerfile / .dockerignore — written by the CLI (skipped if present unless --force). */
@@ -48,21 +28,12 @@ export interface FlyPlan {
48
28
  /** The ordered, values-resolved deploy runbook — printed to stdout for the coding agent to execute. */
49
29
  runbook: string[];
50
30
  }
51
- /** Did fastagent generate this `fly.toml`? */
52
31
  export declare function isGeneratedFlyToml(content: string): boolean;
53
32
  /** Compute the Fly deploy plan from the resolved definition. */
54
33
  export declare function planFlyDeploy(input: FlyPlanInput): FlyPlan;
55
- /**
56
- * The `app` name from an existing fly.toml's `app = "…"` line, or undefined if absent — the KEEP-mode
57
- * single source (a user who renamed the app is not overridden by the basename guess). Accepts TOML's
58
- * double OR single quotes; anything else (or no `app`) is undefined → the caller falls back to basename.
59
- */
34
+ /** The `app` name from an existing fly.toml's `app = "…"` line, or undefined if absent. */
60
35
  export declare function parseFlyAppName(toml: string): string | undefined;
61
- /** The `primary_region` from a fly.toml, or undefined — `--run` passes it to `fly volumes create` so the
62
- * volume lands in the machine's region (fly.toml is the single source; see {@link parseFlyAppName}). */
63
- export declare function parseFlyRegion(toml: string): string | undefined;
64
- /** The `min_machines_running` from a fly.toml, or undefined — the KEEP-mode check reads it so a kept
65
- * file that still scales to zero can be warned about when time triggers (schedules/wake) exist. */
36
+ /** The `min_machines_running` from a fly.toml, or undefined. */
66
37
  export declare function parseFlyMinMachines(toml: string): number | undefined;
67
38
  /** Sanitize a directory basename into a Fly app name: lowercase, [a-z0-9-], must start with a letter. */
68
39
  export declare function toFlyAppName(basename: string): string;
@@ -1,24 +1,23 @@
1
1
  import { webhookRunbook } from "../channel-ingress.js";
2
2
  import { containerArtifacts } from "../container.js";
3
- import { deploymentSecrets, isEnvKey } from "../secrets.js";
4
- function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTriggers, hasLongConnectionChannel) {
5
- // min_machines_running: 1 (keep one up) when a github channel is present, TIME triggers exist, OR the
6
- // operator opted out of scale-to-zero. GitHub's is a SAFETY default — its fire-and-forget turns have no
7
- // replay, so scaling to zero could drop an in-flight review. Time triggers (schedules/wake) have no
8
- // external wake-up at all — a scaled-to-zero box sleeps through the cron instant. Reason-tagged so the
9
- // comment is honest.
10
- const min = hasGithub
11
- ? ` min_machines_running = 1 # github turns have no replay — don't scale to zero (an in-flight review would be lost)`
12
- : hasTimeTriggers
13
- ? ` min_machines_running = 1 # schedules/wake-ups need a running machine (no external wake-up for a cron instant)`
14
- : hasLongConnectionChannel
15
- ? ` min_machines_running = 1 # long-connection channel needs a running machine (cannot wake from zero)`
16
- : !scaleToZero
17
- ? ` min_machines_running = 1 # kept running (--no-scale-to-zero)`
18
- : ` min_machines_running = 0 # scale to zero`;
19
- const stopLine = autostop === "stop"
20
- ? ` auto_stop_machines = "stop" # stop on idle (cold start on the next webhook)`
21
- : ` auto_stop_machines = "suspend" # suspend on idle (fast resume on the next webhook)`;
3
+ import { CRON_CAN_BE_EXTERNAL, WAKEUPS_WHEN_ASLEEP, residencyFor } from "../residency.js";
4
+ import { isEnvKey } from "../secrets.js";
5
+ function flyToml(appName, port, residency) {
6
+ // WHY is residency.ts's to decide; the SETTING and its wording are Fly's. Anything else is an edit to the
7
+ // generated file, which is the artifact's job.
8
+ const min = residency
9
+ ? ` min_machines_running = 1 # ${residency.why}`
10
+ : ` min_machines_running = 0 # scale to zero — ${WAKEUPS_WHEN_ASLEEP}`;
11
+ // The one reason with a way out gets it stated NEXT TO the line it is about, which is where an operator reading
12
+ // `min_machines_running = 1` and wondering what it costs them is looking.
13
+ const alternative = residency?.reason === CRON_CAN_BE_EXTERNAL
14
+ ? ` # …or keep the time somewhere else and set this to 0: a scheduler you own (Fly Cron Manager,\n` +
15
+ ` # supercronic, GitHub Actions) calls \`POST /run\`, which runs one declared unit of work by name.\n` +
16
+ ` # It is an API, not a clock: read its contract first — docs/api-reference.md#post-run.\n` +
17
+ ` # At 0, ${WAKEUPS_WHEN_ASLEEP}.\n`
18
+ : "";
19
+ // Suspend, not stop: a resume is fast enough that a webhook does not time out. Edit the line to change it.
20
+ const stopLine = ` auto_stop_machines = "suspend" # suspend on idle (fast resume on the next webhook)`;
22
21
  return `${GENERATED_FLY_TOML_MARKER}. Edit freely — it is not regenerated unless you pass --force.
23
22
  app = "${appName}"
24
23
  primary_region = "iad" # set your region (list: \`fly platform regions\`)
@@ -36,44 +35,36 @@ primary_region = "iad" # set your region (list: \`fly platform regions\`)
36
35
  ${stopLine}
37
36
  auto_start_machines = true
38
37
  ${min}
39
-
38
+ ${alternative}
40
39
  [mounts]
41
40
  source = "data"
42
- destination = "/data" # .state + .secrets — persists across stop/suspend/redeploy
41
+ destination = "/data" # workspace, state and credentials survive stop/suspend/redeploy
42
+ initial_size = "1gb" # the volume is created by the FIRST deploy — it is pinned to one host, and only
43
+ # deploy tells the scheduler which machine (size + image) must fit there too
43
44
 
44
45
  [[vm]]
45
46
  size = "shared-cpu-1x"
46
47
  memory = "512mb" # suspend is not recommended above 2 GB
47
48
  `;
48
49
  }
49
- /** First line of a generated `fly.toml`, and the predicate that reads it back. Ownership is what decides
50
- * whether `--force` may reset the file (app/region/vm state a user tuned by hand is theirs). */
50
+ /** First line of a generated `fly.toml`, and the predicate that reads it back. */
51
51
  const GENERATED_FLY_TOML_MARKER = "# Generated by `fastagent deploy fly`";
52
- /** Did fastagent generate this `fly.toml`? */
53
52
  export function isGeneratedFlyToml(content) {
54
53
  return content.startsWith(GENERATED_FLY_TOML_MARKER);
55
54
  }
56
55
  /** Compute the Fly deploy plan from the resolved definition. */
57
56
  export function planFlyDeploy(input) {
58
57
  const { appName, port, modelAuth, channels } = input;
59
- // Artifacts sit under the agent prefix, so a NESTED agent never touches the workspace's own deploy
60
- // files; the runbook passes explicit -c/--dockerfile flags either way (unambiguous across flyctl
61
- // versions — no reliance on config-relative path resolution).
58
+ // Artifacts sit under the agent prefix, so they never touch the workspace's own deploy files.
62
59
  const flyTomlPath = `${input.agentPrefix}fly.toml`;
63
60
  const artifacts = [
64
61
  {
65
62
  path: flyTomlPath,
66
- content: flyToml(appName, port, channels.some((channel) => channel.name === "github"), input.autostop, input.scaleToZero, input.hasTimeTriggers, channels.some((channel) => channel.ingress === "long-connection")),
63
+ content: flyToml(appName, port, residencyFor({ channels, hasCron: input.hasCron })),
67
64
  },
68
65
  ...containerArtifacts(input),
69
66
  ];
70
- // The exact secret list the deployed machine needs, computed from the definition (host-neutral): the
71
- // model key (when local auth is an env key) + every discovered channel's secrets. Names + hints as
72
- // COMMENT lines (a `#` inside a `\`-continued command would break the shell), then one flat, executable
73
- // `fly secrets set` the coding agent fills — `<value>` placeholders, never inline comments.
74
- const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets);
75
- const requiredSecrets = secrets.filter((secret) => secret.required);
76
- const optionalSecrets = secrets.filter((secret) => !secret.required);
67
+ const secrets = input.secrets ?? [];
77
68
  const deployCmd = `fly deploy . --config ${flyTomlPath} --dockerfile ${input.agentPrefix}Dockerfile --app ${appName}`;
78
69
  const runbook = [
79
70
  `# Deploy "${appName}" to Fly.io. ${flyTomlPath} / Dockerfile(.dockerignore) are generated above.`,
@@ -83,10 +74,9 @@ export function planFlyDeploy(input) {
83
74
  `# Fly app names are GLOBALLY unique: if this fails as taken, set a unique "app" in fly.toml and`,
84
75
  `# re-run \`fastagent deploy fly\` — the runbook follows fly.toml's app name.`,
85
76
  `fly apps create ${appName}`,
86
- `# volume persists /data/.state (sessions, channel state) + /data/.secrets (seeded auth) across stop/suspend/redeploy.`,
87
- `# <region> MUST equal primary_region in fly.toml (a volume in another region can't mount) — fly.toml`,
88
- `# is the single source for the region; skip this if the volume exists (fly volumes list --app ${appName}):`,
89
- `fly volumes create data --app ${appName} --region <region> --size 1`,
77
+ `# The volume (workspace, state, credentials — kept across stop/suspend/redeploy) is created by the deploy`,
78
+ `# below, in primary_region and sized by [mounts].initial_size. Creating it yourself first pins it to a host`,
79
+ `# picked without the machine, which can fail the deploy with "insufficient resources … existing volume".`,
90
80
  ``,
91
81
  `# An address to be reached ON: [http_service] declares a service, it does not allocate an IP.`,
92
82
  `# \`fly deploy\` allocates one on a FIRST deploy only, and merely WARNS when that fails — which`,
@@ -95,55 +85,33 @@ export function planFlyDeploy(input) {
95
85
  `fly ips allocate-v4 --shared --app ${appName}`,
96
86
  `fly ips allocate-v6 --app ${appName}`,
97
87
  ];
98
- if (requiredSecrets.length > 0) {
99
- runbook.push(``, `# Required secrets (replace each <value>):`, ...requiredSecrets.map((s) => `# ${s.name}: ${s.hint}`), `fly secrets set --app ${appName} ${requiredSecrets.map((s) => `${s.name}=<value>`).join(" ")}`);
100
- }
101
- if (optionalSecrets.length > 0) {
102
- runbook.push(``, `# Optional secrets — set only when the matching feature is configured:`, ...optionalSecrets.map((s) => `# ${s.name}: ${s.hint}`), `# fly secrets set --app ${appName} ${optionalSecrets.map((s) => `${s.name}=<value>`).join(" ")}`);
103
- }
104
- runbook.push(``, `# The build context is the WORKSPACE ROOT (the whole directory is baked as the agent's cwd).`, ...(input.agentPrefix
105
- ? [`# The config/Dockerfile live under ${input.agentPrefix} so they never collide with the workspace's own.`]
106
- : []), `# Run this from ${input.agentPrefix ? "the workspace root" : "this directory"}:`, deployCmd);
107
- if (input.shipsGit) {
108
- runbook.push(``, `# The image is a WYSIWYG snapshot of this directory. Freshness/durability run through git, driven`, `# by the agent itself: .git ships in the image (see .dockerignore) and git is baked in, so the agent`, `# can pull to freshen content and commit/push its work back (creds ride config.deploy.secrets; the`, `# POLICY — push vs PR, identity — lives in persona.md). CAVEAT: whether .git survives the upload is`, `# host-CLI-dependent — verify \`git status\` on the box; if missing, have the agent clone instead.`, `# Un-pushed changes on the box never survive a redeploy; durability lives in git.`);
88
+ if (secrets.length > 0) {
89
+ runbook.push(``, `# Secrets (replace each <value>):`, ...secrets.map((s) => `# ${s.name}: ${s.hint}`), `fly secrets set --app ${appName} ${secrets.map((s) => `${s.name}=<value>`).join(" ")}`);
109
90
  }
110
- else {
111
- runbook.push(``, `# The image is a WYSIWYG snapshot of this directory. No .git here, so no history ships and the`, `# generated image does not install git — changes on the box are ephemeral and never survive a`, `# redeploy. If the agent should clone/push repos as part of its work, add deploy: { apt: ["git"] }.`);
112
- }
113
- // Model-auth guidance: an env key becomes a secret above. Otherwise the plan can't read the local
114
- // credential's VALUE to set as a secret — true for OAuth AND a stored API key (both are
115
- // `AuthResult.source` non-env labels), so the wording doesn't prejudge whether it's migratable.
91
+ runbook.push(``, `# The build context is the WORKSPACE ROOT (the whole directory is baked as the agent's cwd).`, `# The config/Dockerfile live under ${input.agentPrefix} so they never collide with the workspace's own.`, `# Before a new definition release, run \`fastagent deploy fly\` to refresh the release manifest.`, `# Run this from the workspace root:`, deployCmd);
92
+ runbook.push(``, `# The volume keeps /data/base (including uncommitted work), .state and .secrets across restarts and deploys.`, `# Each generated release replaces only /data/base/${input.agentPrefix}; other workspace files are initialized once.`, input.shipsGit
93
+ ? `# Git is available for optional collaboration. Verify uploaded history with \`git status\`; clone if the host stripped it.`
94
+ : `# To use Git for collaboration, add deploy: { apt: ["git"] }. Storage durability does not require Git.`);
95
+ // Model-auth guidance: an env key becomes a secret above.
116
96
  if (!isEnvKey(modelAuth)) {
117
97
  runbook.push(``, modelAuth === undefined
118
- ? `# Model auth: none found at the local auth path — a global \`fastagent login\` isn't read here; pass --auth-path <file> (e.g. ~/.fastagent/.secrets/auth.json), or \`--run\` carries it automatically.`
98
+ ? `# Model auth: none found at the local auth path — a global \`fastagent login\` isn't read here; set FASTAGENT_AUTH_PATH (e.g. ~/.fastagent/.secrets/auth.json), or \`--run\` carries it automatically.`
119
99
  : `# Model auth: your local auth is "${modelAuth}" — the plan can't read its value to set as a secret.`, `# Set your provider API key as a Fly secret (fly secrets set KEY=...), OR place auth.json at /data/.secrets/ on the volume.`);
120
100
  }
121
- // The fastagent-only post-step: point each channel at the live URL. WHICH channels and in what words
122
- // is the shared channel-ingress kernel's answer; Fly's contribution is that its URL is deterministic.
101
+ // The fastagent-only post-step: point each channel at the live URL.
123
102
  const steps = webhookRunbook(`https://${appName}.fly.dev`, channels);
124
103
  const post = steps.length > 0 ? [`# After deploy:`, ...steps] : [];
125
104
  if (post.length > 0)
126
105
  runbook.push(``, ...post);
127
- // Single-machine tier: state lives on ONE volume tied to ONE machine. Scaling to multiple machines
128
- // splits state (each gets its own volume) — that needs a shared/external backend, not this recipe.
106
+ // Single-machine tier: state lives on ONE volume tied to ONE machine.
129
107
  runbook.push(``, `# Keep this a SINGLE machine: the /data volume (and all state on it) is tied to one machine.`, `# Multiple machines would each get their own volume and split sessions/turns — don't scale past 1.`);
130
108
  return { artifacts, runbook };
131
109
  }
132
- /**
133
- * The `app` name from an existing fly.toml's `app = "…"` line, or undefined if absent — the KEEP-mode
134
- * single source (a user who renamed the app is not overridden by the basename guess). Accepts TOML's
135
- * double OR single quotes; anything else (or no `app`) is undefined → the caller falls back to basename.
136
- */
110
+ /** The `app` name from an existing fly.toml's `app = "…"` line, or undefined if absent. */
137
111
  export function parseFlyAppName(toml) {
138
112
  return toml.match(/^\s*app\s*=\s*["']([^"']+)["']/m)?.[1];
139
113
  }
140
- /** The `primary_region` from a fly.toml, or undefined — `--run` passes it to `fly volumes create` so the
141
- * volume lands in the machine's region (fly.toml is the single source; see {@link parseFlyAppName}). */
142
- export function parseFlyRegion(toml) {
143
- return toml.match(/^\s*primary_region\s*=\s*["']([^"']+)["']/m)?.[1];
144
- }
145
- /** The `min_machines_running` from a fly.toml, or undefined — the KEEP-mode check reads it so a kept
146
- * file that still scales to zero can be warned about when time triggers (schedules/wake) exist. */
114
+ /** The `min_machines_running` from a fly.toml, or undefined. */
147
115
  export function parseFlyMinMachines(toml) {
148
116
  const m = toml.match(/^\s*min_machines_running\s*=\s*(\d+)/m)?.[1];
149
117
  return m === undefined ? undefined : Number(m);
@@ -1,39 +1,23 @@
1
- /**
2
- * `fastagent deploy fly --run` — drive flyctl to completion. The middle of the deploy (app / volume /
3
- * secrets / deploy) that the plain runbook hands to the operator; `--run` executes it instead, so a
4
- * coding agent runs ONE command. Idempotent (app/volume check-then-act; channel secrets come from the
5
- * local env — NOT minted — so a re-run sets the same values) and resumable: it STOPS at a human gate
6
- * (not logged in, a missing secret value, a taken app name, a failed webhook registration) with one actionable line and a non-zero
7
- * exit, so the agent clears the gate and re-runs. A `generate` channel secret absent from `.env` is a
8
- * gate too (`missingSecrets`), not a silent mint — fill it in `.env` (use the random string that
9
- * `add <channel>` prints).
10
- *
11
- * flyctl is behind the shared {@link CliRunner} seam — production spawns `fly`, tests inject a fake that
12
- * records the command sequence and scripts outputs. That seam is the benchmark: the agent's journey
13
- * encoded as an asserted command sequence + gate behavior, validated without a real Fly account.
14
- *
15
- * Non-interactive incantations the agent would otherwise get wrong: `--remote-only` (build on Fly's
16
- * builders — no local Docker in a sandbox), `--yes` (no prompts), `--ha=false` + the mounted volume
17
- * (one machine, the single-machine tier). Secrets go in via `secrets import` over stdin, so values
18
- * never land in argv/process listings.
19
- */
20
- import { type Registrars } from "../channel-ingress.ts";
1
+ /** `fastagent deploy fly --run` — drive flyctl to completion. */
2
+ import { type PublicHealthProbe, type Registrars } from "../channel-ingress.ts";
21
3
  import type { DeclaredChannel } from "../../channels/discover.ts";
22
4
  import type { CliRunner } from "../runner.ts";
23
5
  export interface FlyRunPlan {
24
6
  appName: string;
25
- region: string;
26
- /** `KEY=value` secrets to set on Fly: model key (env auth) or `FASTAGENT_AUTH_SEED` (file auth) +
27
- * channel secrets. Set via stdin, never argv. */
7
+ /** `KEY=value` secrets to set on Fly: the value file's variables + `FASTAGENT_AUTH_SEED` for a file credential. */
28
8
  secrets: Record<string, string>;
29
- /** Required secret names with NO local value — the run gates on these before any side effect. */
9
+ /** Declared names the value file supplies no value for — the run gates on these before any side effect. */
30
10
  missingSecrets: string[];
11
+ /** That value file, workspace-relative, so the gate names the file this deploy actually read. */
12
+ valueFile: string;
31
13
  /** Every declared channel and its ingress — the driver asks which of them have a webhook. */
32
14
  channels: readonly DeclaredChannel[];
33
15
  /** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the workspace root). */
34
16
  flyConfig: string;
35
- /** Dockerfile path passed explicitly (`fastagent/Dockerfile`, with the workspace as context —
36
- * flyctl would otherwise resolve it relative to the config's own directory). */
17
+ /**
18
+ * Dockerfile path passed explicitly (`fastagent/Dockerfile`, with the workspace as context — flyctl would otherwise
19
+ * resolve it relative to the config's own directory).
20
+ */
37
21
  dockerfile: string;
38
22
  }
39
23
  /** Done, or a gate the operator must clear before re-running (printed + non-zero exit by the CLI). */
@@ -44,36 +28,17 @@ export type FlyRunOutcome = {
44
28
  gate: string;
45
29
  };
46
30
  /**
47
- * Which PUBLIC ingress families `fly ips list --json` shows — asked per family, because "has an
48
- * ingress address" is not the question the two allocate commands answer. An app holding only a `v6`
49
- * passes the coarse test and still resolves `<app>.fly.dev` to an AAAA record alone, so an IPv4-only
50
- * webhook sender (Telegram, GitHub) reproduces #425 against it.
51
- *
52
- * The list is EVERY assignment the app holds, and a Flycast or egress address carries a non-empty
53
- * `Address` too — reading one as routable pre-empts flyctl's own first-deploy fallback, which returns
54
- * early once any assignment exists.
55
- *
56
- * Output that is not a JSON array THROWS rather than reading as "no address": the caller turns that
57
- * into a gate, because a list we cannot read is not a state we can act on.
31
+ * Which PUBLIC ingress families `fly ips list --json` shows — asked per family, because "has an ingress address" is
32
+ * not the question the two allocate commands answer.
58
33
  */
59
34
  export declare function ingressAddresses(stdout: string): {
60
35
  v4: boolean;
61
36
  v6: boolean;
62
37
  };
63
38
  /**
64
- * Whether a `fly … list --json` array contains an object named `name` (Fly capitalizes `Name`; accept
65
- * both). Exported for the same reason railway's parsers are: what it encodes is an assumption about
66
- * another tool's output, and the live probe checks that assumption against the real `flyctl`.
67
- *
68
- * THROWS on output that is not a JSON array, for the same reason {@link ingressAddresses} does —
69
- * `false` here means "the host does not have it", and answering that for a list nobody could read
70
- * creates a SECOND volume, or in teardown destroys nothing at all.
39
+ * Whether `fly apps list --json` lists an app called `name`. flyctl capitalizes the field; the live probe
40
+ * (test/live/fly.live.test.ts) is what pins that against real output, since an offline fixture can only repeat what
41
+ * we already believe.
71
42
  */
72
43
  export declare function listHasName(stdout: string, name: string): boolean;
73
- /**
74
- * Run the deploy through `fly`. `log` reports progress; the injected {@link Registrars} perform the
75
- * post-deploy webhook steps from the builder machine (Slack's control credential never travels to the
76
- * host). A registrar the caller did not wire becomes the printed manual step. Every gate is
77
- * fail-visible.
78
- */
79
- export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registrars: Registrars): Promise<FlyRunOutcome>;
44
+ export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registrars: Registrars, healthProbe?: PublicHealthProbe): Promise<FlyRunOutcome>;
@@ -1,40 +1,14 @@
1
+ /** `fastagent deploy fly --run` — drive flyctl to completion. */
2
+ import { publicHealthGate, registerWebhooks } from "../channel-ingress.js";
3
+ import { missingValuesGate } from "../secrets.js";
1
4
  /**
2
- * `fastagent deploy fly --run` — drive flyctl to completion. The middle of the deploy (app / volume /
3
- * secrets / deploy) that the plain runbook hands to the operator; `--run` executes it instead, so a
4
- * coding agent runs ONE command. Idempotent (app/volume check-then-act; channel secrets come from the
5
- * local env — NOT minted — so a re-run sets the same values) and resumable: it STOPS at a human gate
6
- * (not logged in, a missing secret value, a taken app name, a failed webhook registration) with one actionable line and a non-zero
7
- * exit, so the agent clears the gate and re-runs. A `generate` channel secret absent from `.env` is a
8
- * gate too (`missingSecrets`), not a silent mint — fill it in `.env` (use the random string that
9
- * `add <channel>` prints).
10
- *
11
- * flyctl is behind the shared {@link CliRunner} seam — production spawns `fly`, tests inject a fake that
12
- * records the command sequence and scripts outputs. That seam is the benchmark: the agent's journey
13
- * encoded as an asserted command sequence + gate behavior, validated without a real Fly account.
14
- *
15
- * Non-interactive incantations the agent would otherwise get wrong: `--remote-only` (build on Fly's
16
- * builders — no local Docker in a sandbox), `--yes` (no prompts), `--ha=false` + the mounted volume
17
- * (one machine, the single-machine tier). Secrets go in via `secrets import` over stdin, so values
18
- * never land in argv/process listings.
5
+ * The `Type` values that put an app on `https://<app>.fly.dev`, BY FAMILY — one entry per allocate command, because
6
+ * that is the granularity the answer is acted on at.
19
7
  */
20
- import { registerWebhooks } from "../channel-ingress.js";
21
- /** The `Type` values that put an app on `https://<app>.fly.dev`, BY FAMILY — one entry per allocate
22
- * command, because that is the granularity the answer is acted on at. The rest of flyctl's
23
- * vocabulary — `private_v6` (Flycast), `egress_v4`, `egress_v6`, `egress_pair` — is internal or
24
- * outbound. */
25
8
  const INGRESS_TYPES = { v4: ["v4", "shared_v4"], v6: ["v6"] };
26
9
  /**
27
- * Which PUBLIC ingress families `fly ips list --json` shows — asked per family, because "has an
28
- * ingress address" is not the question the two allocate commands answer. An app holding only a `v6`
29
- * passes the coarse test and still resolves `<app>.fly.dev` to an AAAA record alone, so an IPv4-only
30
- * webhook sender (Telegram, GitHub) reproduces #425 against it.
31
- *
32
- * The list is EVERY assignment the app holds, and a Flycast or egress address carries a non-empty
33
- * `Address` too — reading one as routable pre-empts flyctl's own first-deploy fallback, which returns
34
- * early once any assignment exists.
35
- *
36
- * Output that is not a JSON array THROWS rather than reading as "no address": the caller turns that
37
- * into a gate, because a list we cannot read is not a state we can act on.
10
+ * Which PUBLIC ingress families `fly ips list --json` shows — asked per family, because "has an ingress address" is
11
+ * not the question the two allocate commands answer.
38
12
  */
39
13
  export function ingressAddresses(stdout) {
40
14
  const entries = JSON.parse(stdout);
@@ -44,30 +18,22 @@ export function ingressAddresses(stdout) {
44
18
  return { v4: has(INGRESS_TYPES.v4), v6: has(INGRESS_TYPES.v6) };
45
19
  }
46
20
  /**
47
- * Whether a `fly … list --json` array contains an object named `name` (Fly capitalizes `Name`; accept
48
- * both). Exported for the same reason railway's parsers are: what it encodes is an assumption about
49
- * another tool's output, and the live probe checks that assumption against the real `flyctl`.
50
- *
51
- * THROWS on output that is not a JSON array, for the same reason {@link ingressAddresses} does —
52
- * `false` here means "the host does not have it", and answering that for a list nobody could read
53
- * creates a SECOND volume, or in teardown destroys nothing at all.
21
+ * Whether `fly apps list --json` lists an app called `name`. flyctl capitalizes the field; the live probe
22
+ * (test/live/fly.live.test.ts) is what pins that against real output, since an offline fixture can only repeat what
23
+ * we already believe.
54
24
  */
55
25
  export function listHasName(stdout, name) {
56
26
  const entries = JSON.parse(stdout);
57
27
  if (!Array.isArray(entries))
58
28
  throw new Error(`expected a JSON array, got ${typeof entries}`);
59
- return entries.some((o) => o.Name === name || o.name === name);
29
+ return entries.some((o) => o.Name === name);
60
30
  }
61
31
  /**
62
- * A read-only `fly … list --json`, reduced to the question the next step asks of it — or the gate for
63
- * a list we cannot act on. THE place the three answers stay three: the command failed, the output was
64
- * unreadable, or the host gave a verdict. Collapsing the middle one into `false` is what shipped #425
65
- * and what would have skipped a teardown.
32
+ * A read-only `fly … list --json`, reduced to the question the next step asks of it — or the gate for a list we cannot
33
+ * act on.
66
34
  */
67
35
  async function readList(fly, args, read) {
68
- // The command as RUN, not a restatement of it: a gate that tells the operator to run it themselves
69
- // has to name one that works from their cwd, and `fly ips list --json` without `-a <app>` does not
70
- // (fly.toml lives under the agent prefix, so flyctl finds no app).
36
+ // The command as RUN, not a restatement of it.
71
37
  const cmd = `fly ${args.join(" ")}`;
72
38
  const result = await fly(args, { capture: true });
73
39
  if (result.code !== 0)
@@ -76,34 +42,24 @@ async function readList(fly, args, read) {
76
42
  return { value: read(result.stdout) };
77
43
  }
78
44
  catch (error) {
79
- // The one place a parse failure is allowed to stop being an exception: this IS the boundary that
80
- // turns it into the operator's gate (printed + non-zero exit), never a default answer.
45
+ // The one place a parse failure is allowed to stop being an exception.
81
46
  return {
82
47
  gate: `\`${cmd}\` was unreadable (${error instanceof Error ? error.message : String(error)}) — ` +
83
48
  `run it yourself and check the flyctl version; fix and re-run`,
84
49
  };
85
50
  }
86
51
  }
87
- /**
88
- * Run the deploy through `fly`. `log` reports progress; the injected {@link Registrars} perform the
89
- * post-deploy webhook steps from the builder machine (Slack's control credential never travels to the
90
- * host). A registrar the caller did not wire becomes the printed manual step. Every gate is
91
- * fail-visible.
92
- */
93
- export async function deployFlyRun(plan, fly, log, registrars) {
52
+ export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
94
53
  const gate = (g) => ({ ok: false, gate: g });
95
- // 1. Auth is the one gate a coding agent can't clear itself (browser OAuth). `whoami` succeeds with
96
- // either an interactive login or FLY_API_TOKEN, so one check covers both.
54
+ // 1.
97
55
  if ((await fly(["auth", "whoami"], { capture: true })).code !== 0) {
98
56
  return gate("not logged in to Fly — run `fly auth login` (opens a browser), or set FLY_API_TOKEN, then re-run");
99
57
  }
100
58
  // 2. Gate missing required secret VALUES before any side effect (no half-created infra).
101
- if (plan.missingSecrets.length > 0) {
102
- return gate(`no local value for: ${plan.missingSecrets.join(", ")} — set them in .env (or the environment) and re-run`);
103
- }
104
- // 3. App — idempotent (create only if absent; a taken global name is a gate). An unusable list is its
105
- // own gate ({@link readList}): inferring "absent" from a query nobody could read would then
106
- // misreport the create as a name clash.
59
+ const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
60
+ if (missingValues)
61
+ return gate(missingValues);
62
+ // 3.
107
63
  const appExists = await readList(fly, ["apps", "list", "--json"], (out) => listHasName(out, plan.appName));
108
64
  if ("gate" in appExists)
109
65
  return gate(appExists.gate);
@@ -117,34 +73,15 @@ export async function deployFlyRun(plan, fly, log, registrars) {
117
73
  `Set a unique \`app\` in fly.toml and re-run.`);
118
74
  }
119
75
  }
120
- // 4. Volume — idempotent; region comes from fly.toml (must match the machine's region). A failed list
121
- // gates for the same reason as the app list above.
122
- const volumeExists = await readList(fly, ["volumes", "list", "-a", plan.appName, "--json"], (out) => listHasName(out, "data"));
123
- if ("gate" in volumeExists)
124
- return gate(volumeExists.gate);
125
- if (volumeExists.value) {
126
- log(`volume data exists — skipping create`);
127
- }
128
- else {
129
- log(`creating volume data in ${plan.region}…`);
130
- if ((await fly(["volumes", "create", "data", "-a", plan.appName, "--region", plan.region, "--size", "1", "--yes"]))
131
- .code !== 0) {
132
- return gate("`fly volumes create` failed — see the flyctl output above");
133
- }
134
- }
135
- // 5. Ingress address — `[http_service]` in fly.toml DECLARES a service; it does not create an address
136
- // to reach it on. `fly launch` allocates one as part of its flow, and this driver does not use it,
137
- // so without this step the deploy succeeds, the machine serves, and `https://<app>.fly.dev` has no
138
- // DNS record at all — reported as a healthy deploy, and handed to the webhook registrars as a URL
139
- // they then fail to reach.
76
+ // 4. NO volume step: `fly deploy` creates it from [mounts] on a first deploy, and only it can tell the scheduler
77
+ // which machine (guest size + image) must fit on the host the volume is pinned to. Pre-creating one here placed it
78
+ // blind, and a host with disk but no compute failed the deploy with `insufficient resources … with existing volume`.
79
+ // 5.
140
80
  const addresses = await readList(fly, ["ips", "list", "-a", plan.appName, "--json"], ingressAddresses);
141
81
  if ("gate" in addresses)
142
82
  return gate(addresses.gate);
143
- // Check-then-act PER FAMILY: an app that already holds one must still be given the other, or the
144
- // gate between the two allocations below heals into a permanent half-state — the re-run the gate
145
- // asks for sees the v4 it just made, skips, and reports success on an app with no v6 (and, the way
146
- // that matters, no v4). v4 SHARED and v6 are both free; a dedicated v4 is billed, so it stays an
147
- // operator decision — `fly ips allocate-v4 -a <app>` after the fact, which this step reads as v4.
83
+ // Check-then-act PER FAMILY: an app that already holds one must still be given the other, or the gate between the
84
+ // two allocations below heals into a permanent half-state.
148
85
  for (const [family, allocate] of [
149
86
  ["v4", ["ips", "allocate-v4", "--shared", "-a", plan.appName]],
150
87
  ["v6", ["ips", "allocate-v6", "-a", plan.appName]],
@@ -167,8 +104,7 @@ export async function deployFlyRun(plan, fly, log, registrars) {
167
104
  return gate("`fly secrets import` failed — see the flyctl output above");
168
105
  }
169
106
  }
170
- // 7. Deploy — remote builder (no local Docker), one machine. Context + Dockerfile are passed
171
- // explicitly (the workspace root is the context; the Dockerfile lives under fastagent/).
107
+ // 7.
172
108
  log("deploying (remote build)…");
173
109
  const deployArgs = [
174
110
  "deploy",
@@ -186,10 +122,21 @@ export async function deployFlyRun(plan, fly, log, registrars) {
186
122
  if ((await fly(deployArgs)).code !== 0) {
187
123
  return gate("`fly deploy` failed — see the flyctl output above; fix and re-run");
188
124
  }
189
- // 8. Post-deploy webhook — which channels, in what words, and the gate policy are all the shared
190
- // kernel's; Fly contributes its deterministic URL and how to retry.
125
+ // 8. `fly deploy` exits 0 on a machine that then crash-loops, so readiness is asked here and not inferred.
126
+ const baseUrl = `https://${plan.appName}.fly.dev`;
127
+ const healthGate = await publicHealthGate({
128
+ baseUrl,
129
+ channels: plan.channels,
130
+ log,
131
+ inspectHint: `the app itself deployed — inspect \`fly logs -a ${plan.appName}\`, then re-run once it answers ` +
132
+ `(a re-run repeats the remote build)`,
133
+ probe: healthProbe,
134
+ });
135
+ if (healthGate)
136
+ return gate(healthGate);
137
+ // 9.
191
138
  const registrationGateMsg = await registerWebhooks({
192
- baseUrl: `https://${plan.appName}.fly.dev`,
139
+ baseUrl,
193
140
  channels: plan.channels,
194
141
  registrars,
195
142
  log,
@@ -1,5 +1,26 @@
1
- /** The deploy targets, as a value: the CLI's `<host>` choices and the host-only-flag table's
2
- * exhaustiveness check both read it. Dependency-free, so `cli/program.ts` can import it at load
3
- * time without pulling a command module. */
1
+ /**
2
+ * The deploy targets, as a value: the CLI's `<host>` choices and the host-only-flag table's
3
+ * exhaustiveness check both read it. Dependency-free, so `cli/program.ts` can import it at load
4
+ * time without pulling a command module.
5
+ *
6
+ * ── ADDING A HOST ───────────────────────────────────────────────────────────
7
+ *
8
+ * Three edits: a directory beside this file (copy `fly/`: `plan.ts` pure, `run.ts` driving the host
9
+ * CLI behind the runner seam), its `HostDeploy` in `cli/commands/deploy/<host>.ts` (kept-file
10
+ * semantics, gates, the drive glue — the half that may exit the process), and its name in the array
11
+ * below. `deploy/` itself is a neutral kernel plus one directory per host; nothing host-specific
12
+ * belongs in the kernel, and no host may hold a fact about another one (a host-only flag is a row in
13
+ * `HOST_ONLY_FLAGS` in `cli/commands/deploy.ts`, warning elsewhere; `--tunnel` stays a usage GATE in `runDeploy`
14
+ * because a refusal is not a row).
15
+ *
16
+ * BEFORE writing one, read that host's docs for what it does NOT do implicitly — above all, whether
17
+ * a created resource is REACHABLE without a further step. On Fly, `[http_service]` declares a service and allocates
18
+ * no address, and v4 and v6 are separate commands, so an app with only v6 is unreachable from an IPv4-only webhook
19
+ * sender. The host's own docs and CLI source usually say so; read them before a real deploy has to.
20
+ *
21
+ * And when a driver PARSES what a host CLI printed: the judgement must match the GRANULARITY of the
22
+ * action it drives (one question per command), and "we could not read the host" is a third answer,
23
+ * never "absent".
24
+ */
4
25
  export declare const DEPLOY_HOSTS: readonly ["docker", "fly", "railway", "agentcore"];
5
26
  export type DeployHost = (typeof DEPLOY_HOSTS)[number];