@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,41 +1,14 @@
1
- /**
2
- * `fastagent deploy railway --run` — drive the Railway CLI to completion. The middle of the deploy the
3
- * plain runbook hands to the operator; `--run` executes it so a coding agent runs ONE command.
4
- *
5
- * Railway's model forces differences from the Fly runner (fly/run.ts), all validated against CLI 5.15.0:
6
- *
7
- * - **`--run` PROVISIONS a project and only runs on an UNLINKED directory** — so it can never deploy
8
- * into a project it didn't create. `railway init` isn't check-then-act (it ALWAYS makes a new project)
9
- * and `railway status` exits 0 even when unlinked (linkedness is read from stdout: JSON when linked,
10
- * empty when not). We do NOT track ownership: a pre-existing link is refused (Railway has no globally
11
- * unique name to give free identity like Fly's app name, and synthesizing one — a machine-local marker
12
- * — was a large, bug-prone premature optimization). The only "yes, this project" signal is the operator's
13
- * explicit `--into-linked`, which provisions INTO the linked project; a routine redeploy is just
14
- * `railway up`. The volume is check-then-act, so `--into-linked` (or a create that failed at volume-add) still gets it.
15
- * - **Every command needs `--service` explicitly** to stay non-interactive (a bare command prompts to
16
- * pick a service). `railway volume add` is the exception — it has NO `--service` flag and rides the
17
- * linked service, so the service must be created (and thus linked) first.
18
- * - **The public URL is minted, not deterministic** — bare `railway domain --json` returns the service
19
- * domain (minting one if absent), read for the webhook. We avoid the newer `domain list` subcommand:
20
- * on a CLI without it, `railway domain list` registers a bogus custom domain named "list" (destructive).
21
- *
22
- * Secrets go in one-per-`variable set --stdin` (value on stdin, never argv/process listing — Railway has
23
- * no bulk stdin import like Fly's `secrets import`). Auth needs an ACCOUNT credential (login or
24
- * `RAILWAY_API_TOKEN`), not the project-scoped `RAILWAY_TOKEN`: `init` creates a project that a project
25
- * token can't predate.
26
- */
27
- import { registerWebhooks } from "../channel-ingress.js";
28
- /**
29
- * Whether `railway status --json` shows a linked project: non-empty stdout. Unlinked prints its message
30
- * to stderr and leaves stdout EMPTY (the exit code is 0 either way, so it can't be the signal). ANY
31
- * non-empty output — parseable or not — counts as linked, so an unreadable shape is refused, never
32
- * mistaken for unlinked (which would `init` a duplicate). Verified against CLI 5.15.0.
33
- */
1
+ /** `fastagent deploy railway --run` — drive the Railway CLI to completion. */
2
+ import { publicHealthGate, registerWebhooks } from "../channel-ingress.js";
3
+ import { missingValuesGate } from "../secrets.js";
4
+ /** Whether `railway status --json` shows a linked project: non-empty stdout. */
34
5
  export function isLinked(stdout) {
35
6
  return stdout.trim() !== "";
36
7
  }
37
- /** The linked project's name for the gate message, or undefined if it can't be read (still linked —
38
- * `railway status --json` puts `name` at the top level on 5.15.0). */
8
+ /**
9
+ * The linked project's name for the gate message, or undefined if it can't be read (still linked — `railway status
10
+ * --json` puts `name` at the top level on 5.15.0).
11
+ */
39
12
  export function linkedName(stdout) {
40
13
  try {
41
14
  const v = JSON.parse(stdout);
@@ -43,13 +16,11 @@ export function linkedName(stdout) {
43
16
  return v.name;
44
17
  }
45
18
  catch {
46
- /* non-JSON but non-empty → still linked, just no name to show */
19
+ // non-JSON but non-empty → still linked, just no name to show
47
20
  }
48
21
  return undefined;
49
22
  }
50
- /** Every string leaf of a parsed JSON value — the shape-agnostic scan both readers below share: Railway's
51
- * `--json` field names aren't guaranteed, so we match on VALUES, not paths. `stdout` that isn't JSON
52
- * (an empty/errored query) yields []. */
23
+ /** Every string leaf of a parsed JSON value. */
53
24
  function jsonStrings(stdout) {
54
25
  const walk = (v) => typeof v === "string"
55
26
  ? [v]
@@ -65,12 +36,7 @@ function jsonStrings(stdout) {
65
36
  return [];
66
37
  }
67
38
  }
68
- /**
69
- * The first Railway-provided domain as an https URL, or undefined if none is present — reads `railway
70
- * domain --json` without assuming exact field names: any string value carrying a `*.railway.app` host
71
- * wins (unanchored, so a scheme/port/path around it still yields the bare host). Undefined → the CLI
72
- * gates (register manually).
73
- */
39
+ /** The first Railway-provided domain as an https URL, or undefined if none is present. */
74
40
  export function parseDomainUrl(stdout) {
75
41
  for (const s of jsonStrings(stdout)) {
76
42
  const host = s.match(/[a-z0-9-]+(?:\.[a-z0-9-]+)*\.railway\.app/i)?.[0];
@@ -80,42 +46,50 @@ export function parseDomainUrl(stdout) {
80
46
  return undefined;
81
47
  }
82
48
  /**
83
- * Whether `railway volume list --json` shows a volume at `mountPath` — shape-agnostic, like
84
- * {@link parseDomainUrl}. Drives check-then-act so a half-provisioned project (service created, volume
85
- * add failed) self-heals on re-run instead of silently deploying with no volume.
49
+ * The status of the volume `railway volume list --json` shows mounted at `mountPath` ON `service`, or undefined when
50
+ * that service has none there. The list is the whole PROJECT's: another service's volume, or one a deleted service
51
+ * left behind (`serviceName: null` — deleting a service keeps its volume), sits at the same path and is not this
52
+ * one. Read from any object carrying `mountPath` and `serviceName`, so a wrapper key moving does not matter.
86
53
  */
87
- export function parseHasVolume(stdout, mountPath) {
88
- return jsonStrings(stdout).includes(mountPath);
54
+ export function volumeOn(stdout, service, mountPath) {
55
+ const walk = (v) => {
56
+ if (Array.isArray(v))
57
+ return v.map(walk).find((status) => status !== undefined);
58
+ if (!v || typeof v !== "object")
59
+ return undefined;
60
+ const o = v;
61
+ if (o.mountPath === mountPath && o.serviceName === service)
62
+ return typeof o.status === "string" ? o.status : "";
63
+ return walk(Object.values(o));
64
+ };
65
+ try {
66
+ return walk(JSON.parse(stdout));
67
+ }
68
+ catch {
69
+ return undefined;
70
+ }
89
71
  }
90
72
  /**
91
- * Run the deploy through `railway`. `log` reports progress; the injected {@link Registrars} perform
92
- * the post-deploy webhook steps from the builder machine (Slack's control credential never travels to
93
- * the host). A registrar the caller did not wire becomes the printed manual step. Every gate is
94
- * fail-visible.
73
+ * How long `--run` waits for a new volume to reach its service before deploying. Railway's `volume add` returns
74
+ * before the volume is attached, and a deployment started in that window runs WITHOUT it — no `/data` at all —
75
+ * while a later one gets it (measured 2026-09-27: listed on the service as `Ready` 5s after the add).
95
76
  */
96
- export async function deployRailwayRun(plan, railway, log, registrars) {
77
+ const VOLUME_ATTACH_TIMEOUT_MS = 60_000;
78
+ const VOLUME_POLL_MS = 2_000;
79
+ export async function deployRailwayRun(plan, railway, log, registrars, healthProbe, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
97
80
  const gate = (g) => ({ ok: false, gate: g });
98
- // Every --service below targets plan.name — the name this tool gives BOTH the project and the service
99
- // (`init --name` + `add --service`). On a fresh create they match; on `--into-linked` into a hand-made
100
- // project whose service is named differently, the FIRST --service command (`variables set`) gates —
101
- // and it's ordered before the volume (which has no --service), so the mismatch fails visibly with no
102
- // side effect. (Pre-checking the name means walking status's nested multi-service shape — not worth it.)
81
+ // Every --service below targets plan.name — the name this tool gives BOTH the project and the service (`init
82
+ // --name` + `add --service`).
103
83
  const svc = ["--service", plan.name];
104
- // 1. Auth needs an ACCOUNT credential (browser login or RAILWAY_API_TOKEN) — the project-scoped
105
- // RAILWAY_TOKEN can't predate the project `init` creates. `whoami` succeeds with either.
84
+ // 1.
106
85
  if ((await railway(["whoami"], { capture: true })).code !== 0) {
107
86
  return gate("not logged in to Railway — run `railway login`, or set RAILWAY_API_TOKEN (an account token), then re-run");
108
87
  }
109
88
  // 2. Gate missing required secret VALUES before any side effect (no half-created infra).
110
- if (plan.missingSecrets.length > 0) {
111
- return gate(`no local value for: ${plan.missingSecrets.join(", ")} — set them in .env (or the environment) and re-run`);
112
- }
113
- // 3. Linked? `--run` provisions a project and only runs on an UNLINKED directory, so it can never deploy
114
- // into a project it didn't create. A linked directory is REFUSED (naming the project so the operator
115
- // sees what it is) UNLESS they pass --into-linked to provision INTO that project deliberately.
116
- // Unlinked → create. No ownership is tracked — a pre-existing link is never assumed ours; the only
117
- // "yes, this project" signal is the explicit --into-linked. Any non-empty status counts as linked (an
118
- // unreadable shape is refused, not mistaken for unlinked, which would `init` a duplicate).
89
+ const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
90
+ if (missingValues)
91
+ return gate(missingValues);
92
+ // 3.
119
93
  const status = await railway(["status", "--json"], { capture: true });
120
94
  if (isLinked(status.stdout)) {
121
95
  if (!plan.intoLinked) {
@@ -128,8 +102,7 @@ export async function deployRailwayRun(plan, railway, log, registrars) {
128
102
  log(`provisioning into linked project ${linkedName(status.stdout) ?? plan.name} (--into-linked)`);
129
103
  }
130
104
  else {
131
- // --into-linked means "provision into the project this dir is linked to" — but it isn't linked. Don't
132
- // swallow the flag silently: the operator expected an existing project; say we're creating a fresh one.
105
+ // --into-linked means "provision into the project this dir is linked to" — but it isn't linked.
133
106
  if (plan.intoLinked) {
134
107
  log("warn: --into-linked was passed but this directory isn't linked to any project — creating a fresh one");
135
108
  }
@@ -137,29 +110,28 @@ export async function deployRailwayRun(plan, railway, log, registrars) {
137
110
  if ((await railway(["init", "--name", plan.name])).code !== 0) {
138
111
  return gate("`railway init` failed — if you have multiple workspaces, run it once interactively (or pass --workspace) to pick one, then re-run");
139
112
  }
140
- // Create + link the service (init makes only a project); it precedes the volume, which has no
141
- // --service flag and rides the linked service.
113
+ // Create + link the service (init makes only a project); it precedes the volume, which has no --service flag and
114
+ // rides the linked service.
142
115
  log(`creating service ${plan.name}…`);
143
116
  if ((await railway(["add", "--service", plan.name])).code !== 0) {
144
- // Precise recovery, not "fix and re-run": init already created + linked the project, so a plain
145
- // re-run hits the linked-gate, and --into-linked SKIPS `add` and then fails at the volume (no
146
- // service to ride). The clean paths are to create the service by hand then --into-linked, or unlink.
117
+ // Precise recovery, not "fix and re-run": init already created + linked the project, so a plain re-run hits the
118
+ // linked-gate, and --into-linked SKIPS `add` and then fails at the volume (no service to ride).
147
119
  return gate(`\`railway add --service\` failed — \`railway init\` already created the project (this directory is now ` +
148
120
  `linked) but not the service. Finish with \`railway add --service ${plan.name}\` here, then re-run with ` +
149
121
  `--into-linked; or \`railway unlink\` to detach and start fresh.`);
150
122
  }
151
123
  }
152
- // 3b. Variables — set BEFORE the volume/deploy. This is deliberately the FIRST `--service` command: on
153
- // an --into-linked into a hand-made project whose service name ≠ plan.name, it gates HERE, before the
154
- // volume (which has no --service and would otherwise attach to that service) — so the mismatch fails
155
- // visibly with NO side effect. State root on argv (not secret); secrets one-per-`set --stdin` (value
156
- // on stdin, never argv). Idempotent. (Order vs the volume is free — both just need to precede `up`.)
124
+ // 3b.
157
125
  const machineryVars = [
158
126
  `FASTAGENT_STATE_DIR=${plan.mountPath}/.state`,
159
127
  `FASTAGENT_SECRETS_DIR=${plan.mountPath}/.secrets`,
160
128
  `RAILWAY_DOCKERFILE_PATH=${plan.dockerfilePath}`,
161
129
  ];
162
- log(`setting ${machineryVars.map((v) => v.split("=")[0]).join("/")} + ${Object.keys(plan.secrets).length} secret(s)…`);
130
+ // The secret NAMES, not a count: `--run` uploads the whole value file, so the operator has to be able to see the
131
+ // list on every host.
132
+ const secretNames = Object.keys(plan.secrets);
133
+ log(`setting ${machineryVars.map((v) => v.split("=")[0]).join("/")} + ${secretNames.length} secret(s)` +
134
+ `${secretNames.length > 0 ? `: ${secretNames.join(", ")}` : ""}…`);
163
135
  if ((await railway(["variables", "set", ...machineryVars, ...svc])).code !== 0) {
164
136
  return gate("`railway variables set` failed — see the railway output above");
165
137
  }
@@ -168,39 +140,53 @@ export async function deployRailwayRun(plan, railway, log, registrars) {
168
140
  return gate(`\`railway variables set ${k}\` failed — see the railway output above`);
169
141
  }
170
142
  }
171
- // 3c. Volume — check-then-act, ALWAYS (like Fly's per-resource skip-if-present). An --into-linked into an
172
- // existing project may already have it; a first deploy that failed after this step has it, one that
173
- // failed before needs it. Verify + add so we never deploy with NO persistence (FASTAGENT_STATE_DIR
174
- // =/data would be empty every redeploy — silent state loss). Runs after variables so a mismatched
175
- // service (see above) has already gated — this step won't attach a volume to the wrong service.
176
- const vols = await railway(["volume", "list", "--json"], { capture: true });
177
- if (parseHasVolume(vols.stdout, plan.mountPath)) {
178
- log(`volume at ${plan.mountPath} exists — skipping`);
143
+ // 3c.
144
+ const volumeStatus = async () => volumeOn((await railway(["volume", "list", "--json"], { capture: true })).stdout, plan.name, plan.mountPath);
145
+ if ((await volumeStatus()) !== undefined) {
146
+ log(`volume at ${plan.mountPath} exists on ${plan.name} — skipping`);
179
147
  }
180
148
  else {
181
149
  log(`creating volume at ${plan.mountPath}…`);
182
150
  if ((await railway(["volume", "add", "--mount-path", plan.mountPath])).code !== 0) {
183
151
  return gate("`railway volume add` failed — see the railway output above");
184
152
  }
153
+ // Deploying before the volume is attached boots a container with no volume, which `start` refuses (it must
154
+ // never seed a workspace onto a disk that vanishes on restart).
155
+ let status;
156
+ for (let waited = 0;; waited += VOLUME_POLL_MS) {
157
+ status = await volumeStatus();
158
+ if (status === "Ready" || waited >= VOLUME_ATTACH_TIMEOUT_MS)
159
+ break;
160
+ await sleep(VOLUME_POLL_MS);
161
+ }
162
+ if (status !== "Ready") {
163
+ return gate(`the volume at ${plan.mountPath} is not attached to service ${plan.name} after ` +
164
+ `${VOLUME_ATTACH_TIMEOUT_MS / 1000}s (${status === undefined ? "not listed on it" : `status ${status}`}) — ` +
165
+ "check `railway volume list`, then re-run with --into-linked once it shows Ready");
166
+ }
185
167
  }
186
168
  // 5. Deploy — CI mode streams build logs then exits (no interactive attach). Build runs on Railway.
187
169
  log("deploying (railway up)…");
188
170
  if ((await railway(["up", "--ci", ...svc])).code !== 0) {
189
171
  return gate("`railway up` failed — see the railway output above; fix and re-run");
190
172
  }
191
- // 6. Public domain — a Railway service isn't reachable until one is minted (unlike Fly's deterministic
192
- // <app>.fly.dev), so every deploy needs it (the /invoke + /health surface), not just webhook channels.
193
- // The top-level `railway domain` (no subcommand; --json/--service flags are fine) is the long-standing
194
- // core command: it returns the service domain, minting one if absent. We do NOT pre-check with the
195
- // `domain list` SUBCOMMAND — it's newer, and on a CLI without it `railway domain list` falls through
196
- // to the [DOMAIN] positional and registers a bogus custom domain named "list" (destructive). Version-robust.
173
+ // 6.
197
174
  log("getting the public domain…");
198
175
  const url = parseDomainUrl((await railway(["domain", "--json", ...svc], { capture: true })).stdout);
199
176
  if (!url) {
200
177
  return gate("couldn't read a domain from `railway domain` — run `railway domain` manually, then set any webhook");
201
178
  }
202
- // 7. Post-deploy webhook — which channels, in what words, and the gate policy are all the shared
203
- // kernel's; Railway contributes the minted URL and how to retry.
179
+ // 6b. `railway up --ci` exits 0 once the build is accepted, so readiness is asked here and not inferred.
180
+ const healthGate = await publicHealthGate({
181
+ baseUrl: url,
182
+ channels: plan.channels,
183
+ log,
184
+ inspectHint: "the service itself deployed — inspect `railway logs`, then re-run once it answers",
185
+ probe: healthProbe,
186
+ });
187
+ if (healthGate)
188
+ return gate(healthGate);
189
+ // 7.
204
190
  const registrationGateMsg = await registerWebhooks({
205
191
  baseUrl: url,
206
192
  channels: plan.channels,
@@ -1,20 +1,13 @@
1
1
  /**
2
- * Host-NEUTRAL step-7 gate policy, shared by the host runners: the registrars report facts
3
- * ({@link RegistrationOutcome}), this module owns what to do about them.
4
- *
5
- * - "failed" gates — exit 0 would tell a coding agent "done" while the deployed agent cannot receive
6
- * messages, and the runners' earlier steps are idempotent so a re-run just retries registration.
7
- * - "manual" does NOT gate — re-running can never change it (an unclearable gate would spin a coding
8
- * agent forever) — but is re-surfaced after all registrar output (and before any gate message) so
9
- * the operator cannot miss it under the registrar's earlier log lines.
10
- *
11
- * The runners attempt ALL channels first (one failure doesn't skip the rest), then apply this policy
12
- * once. Only the retry remediation differs per host (`retryHint`).
2
+ * Host-NEUTRAL step-7 gate policy, shared by the host runners: the registrars report facts ({@link
3
+ * RegistrationOutcome}), this module owns what to do about them.
13
4
  */
14
5
  import type { RegistrationOutcome } from "../channels/registration.ts";
15
6
  export declare function registrationGate(log: (msg: string) => void, retryHint: string): {
16
7
  track: (kind: string, outcome: RegistrationOutcome) => void;
17
- /** Logs the manual notices; returns the gate message (composes with cli.ts's "deploy stopped:"
18
- * prefix — it leads with the failure, not "the deploy succeeded"), or undefined for no gate. */
8
+ /**
9
+ * Logs the manual notices; returns the gate message (composes with cli.ts's "deploy stopped:" prefix — it leads with
10
+ * the failure, not "the deploy succeeded"), or undefined for no gate.
11
+ */
19
12
  gate: () => string | undefined;
20
13
  };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * WHAT FORBIDS SCALING TO ZERO — one rule, read by every host that can scale.
3
+ *
4
+ * A host may only phrase its own REMEDY — the setting it owns (`min_machines_running`, App Sleeping) — never
5
+ * re-derive the reason.
6
+ *
7
+ * ORDER IS THE POINT, not just precedence: the reason reported has to be the one an operator cannot work around,
8
+ * because the message it produces differs. A declared schedule has a substitute, because the clock does not have to
9
+ * be ours — see {@link CRON_CAN_BE_EXTERNAL}; a long connection has none.
10
+ *
11
+ * A WAKE-UP IS NOT A REASON. Every serve mounts `wake`, so counting it would pin every deployment up. It does not
12
+ * need to: the wake-up store is on the volume and the poll drains what is due when the process starts, so a box that
13
+ * slept fires it late, when a request next wakes it — never loses it. {@link WAKEUPS_WHEN_ASLEEP} says so where
14
+ * scaling to zero is offered.
15
+ */
16
+ import type { DeclaredChannel } from "../channels/discover.ts";
17
+ /** Why one machine has to stay up. Also the order they are checked in. */
18
+ export type ResidencyReason = "long-connection" | "cron";
19
+ export interface Residency {
20
+ reason: ResidencyReason;
21
+ /** The host-neutral cause, one clause, for a generated comment or a runbook line. */
22
+ why: string;
23
+ }
24
+ /**
25
+ * The ONE reason a caller may offer a way out of: a cron is a TIME, and a time can be kept elsewhere — a platform
26
+ * scheduler, a CI cron, a crontab — which then calls this agent. Nothing else here can be moved out: a long
27
+ * connection is one this process holds.
28
+ */
29
+ export declare const CRON_CAN_BE_EXTERNAL: ResidencyReason;
30
+ /** What scaling to zero costs the agent's own wake-ups — said next to every setting that allows it. */
31
+ export declare const WAKEUPS_WHEN_ASLEEP = "the agent's own wake-ups then fire when a request next wakes the machine, not on time";
32
+ /** What forbids scale-to-zero for this deployment, or `undefined` when nothing does. */
33
+ export declare function residencyFor(facts: {
34
+ channels: readonly DeclaredChannel[];
35
+ /** `routines/` declares at least one cron. */
36
+ hasCron: boolean;
37
+ }): Residency | undefined;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The ONE reason a caller may offer a way out of: a cron is a TIME, and a time can be kept elsewhere — a platform
3
+ * scheduler, a CI cron, a crontab — which then calls this agent. Nothing else here can be moved out: a long
4
+ * connection is one this process holds.
5
+ */
6
+ export const CRON_CAN_BE_EXTERNAL = "cron";
7
+ /** What scaling to zero costs the agent's own wake-ups — said next to every setting that allows it. */
8
+ export const WAKEUPS_WHEN_ASLEEP = "the agent's own wake-ups then fire when a request next wakes the machine, not on time";
9
+ /** What forbids scale-to-zero for this deployment, or `undefined` when nothing does. */
10
+ export function residencyFor(facts) {
11
+ if (facts.channels.some((channel) => channel.ingress === "long-connection")) {
12
+ return {
13
+ reason: "long-connection",
14
+ why: "a long-connection channel must stay connected — it cannot wake from zero",
15
+ };
16
+ }
17
+ if (facts.hasCron) {
18
+ return {
19
+ reason: "cron",
20
+ why: "a cron instant has no external wake-up here, so a sleeping box sleeps through it",
21
+ };
22
+ }
23
+ return undefined;
24
+ }
@@ -2,14 +2,16 @@ interface RunResult {
2
2
  code: number;
3
3
  /** Captured stdout (for `--json` queries); empty when the command streamed to the terminal. */
4
4
  stdout: string;
5
- /** Captured stderr — ONLY when `captureStderr` was set (a caller that must CLASSIFY a failure, e.g.
6
- * "not found" vs "denied"); otherwise undefined and stderr streams to the terminal as always. */
5
+ /**
6
+ * Captured stderr — ONLY when `captureStderr` was set (a caller that must CLASSIFY a failure, e.g. "not found" vs
7
+ * "denied").
8
+ */
7
9
  stderr?: string;
8
10
  }
9
- /** Run `bin args`: `capture` collects stdout (for `--json` queries), else the command streams to the
10
- * terminal (create/deploy) and stdout is empty; `input` is fed to stdin (secrets over stdin, never argv).
11
- * `env` adds child-only environment values — Docker Compose interpolates secrets from it without putting
12
- * values in argv or mutating the long-lived CLI process. */
11
+ /**
12
+ * Run `bin args`: `capture` collects stdout (for `--json` queries), else the command streams to the terminal
13
+ * (create/deploy) and stdout is empty.
14
+ */
13
15
  export type CliRunner = (args: string[], opts?: {
14
16
  capture?: boolean;
15
17
  captureStderr?: boolean;
@@ -17,9 +19,8 @@ export type CliRunner = (args: string[], opts?: {
17
19
  env?: NodeJS.ProcessEnv;
18
20
  }) => Promise<RunResult>;
19
21
  /**
20
- * Production {@link CliRunner}: spawn `bin` in `cwd` (the workspace, so a build/upload context is the
21
- * agent). stderr is always inherited to the terminal; stdout is inherited unless `capture`. A spawn
22
- * ENOENT (the CLI not on PATH) resolves to code 127 so the caller can gate with an install hint.
22
+ * Production {@link CliRunner}: spawn `bin` in `cwd` (the workspace, so a build/upload context is the agent). stderr
23
+ * is always inherited to the terminal.
23
24
  */
24
25
  export declare function spawnRunner(bin: string, cwd: string): CliRunner;
25
26
  export {};
@@ -1,14 +1,9 @@
1
- /**
2
- * The host-CLI dispatcher seam, shared by every `deploy <host> --run` driver (fly/run.ts, railway/run.ts).
3
- * WHAT it does — run a CLI, optionally capture stdout or feed stdin — is identical across hosts; only the
4
- * binary and the command sequence differ. Tests inject a fake recorder; production spawns the real CLI.
5
- */
1
+ /** The host-CLI dispatcher seam, shared by every `deploy <host> --run` driver (fly/run.ts, railway/run.ts). */
6
2
  import { spawn } from "node:child_process";
7
3
  import { log } from "../log.js";
8
4
  /**
9
- * Production {@link CliRunner}: spawn `bin` in `cwd` (the workspace, so a build/upload context is the
10
- * agent). stderr is always inherited to the terminal; stdout is inherited unless `capture`. A spawn
11
- * ENOENT (the CLI not on PATH) resolves to code 127 so the caller can gate with an install hint.
5
+ * Production {@link CliRunner}: spawn `bin` in `cwd` (the workspace, so a build/upload context is the agent). stderr
6
+ * is always inherited to the terminal.
12
7
  */
13
8
  export function spawnRunner(bin, cwd) {
14
9
  return (args, opts) => new Promise((res) => {
@@ -27,22 +22,16 @@ export function spawnRunner(bin, cwd) {
27
22
  child.stdout?.on("data", (d) => (out += String(d)));
28
23
  child.stderr?.on("data", (d) => (err += String(d)));
29
24
  if (opts?.input) {
30
- // A host CLI that rejects before reading (bad auth, a refused command) closes stdin under us:
31
- // the write then emits `error` on a stream with no listener, which Node turns into an uncaught
32
- // exception — crashing the deploy and losing the gate message the exit code was about to carry.
33
- // HELD, not dropped: the exit code says it only when the CLI exits non-zero (see below).
25
+ // A host CLI that rejects before reading (bad auth, a refused command) closes stdin under us.
34
26
  child.stdin?.on("error", (error) => (stdinError ??= error));
35
27
  child.stdin?.end(opts.input);
36
28
  }
37
29
  child.on("close", (code) => {
38
- // A CLI that exits 0 having refused part of its stdin took TRUNCATED input — an auth seed runs
39
- // well past the 64KB pipe buffer, so "success" here would deploy a half-written secret and say
40
- // nothing. The exit code is what callers read, so the failure has to reach them as one.
30
+ // A CLI that exits 0 having refused part of its stdin took TRUNCATED input.
41
31
  const truncated = stdinError !== undefined && (code ?? 1) === 0;
42
32
  if (truncated) {
43
- // `error`, not `warn`: this line is the ONLY evidence of the failure — the caller's gate says
44
- // "see the output above", and above it the host CLI printed success. A log level that mutes
45
- // it (CI often sets `error`) leaves a stopped deploy with no diagnosable cause.
33
+ // `error`, not `warn`: this line is the ONLY evidence of the failure — the caller's gate says "see the
34
+ // output above", and above it the host CLI printed success.
46
35
  log.error(`[fastagent] ${bin} exited 0 without reading all of its input — ${stdinError?.message}`);
47
36
  }
48
37
  res({ code: truncated ? 1 : (code ?? 1), stdout: out, stderr: opts?.captureStderr ? err : undefined });
@@ -1,63 +1,62 @@
1
- import type { DeclaredChannel } from "../channels/discover.ts";
2
1
  /**
3
- * Is this local auth source an env-var API key (→ becomes a deploy secret) vs OAuth / stored / none?
4
- * Positive match on the UPPER_SNAKE env-var naming shape, NOT a negative exclude of today's sentinel
5
- * labels: a new non-env `AuthResult.source` (e.g. `keychain`) then degrades to guidance, never a fake
6
- * `keychain=<value>` secret — the two modules don't couple through an exhaustive string list.
2
+ * The environment a deployed agent runs with — host-neutral.
3
+ *
4
+ * The value file (`.secrets/.env`) IS the deployed environment: every variable in it travels, on every host. What the
5
+ * definition declares (`defineTool`/`defineChannel`/`defineRoutine({ secrets })`, plus the model's env key) only says
6
+ * which of those must have a value, so a missing one stops a deploy before its first side effect instead of a boot.
7
7
  */
8
+ import { type DeclaredSecret } from "../declared-secrets.ts";
9
+ /** Is this local auth source an env-var API key (→ becomes a deploy secret) vs OAuth / stored / none? */
8
10
  export declare function isEnvKey(source: string | undefined): source is string;
9
- /**
10
- * Secret NAMES + hints for a runbook: the model key (when local auth is an env key), discovered channel
11
- * secrets, and config extras. Channel metadata keeps optional values visible without presenting them as
12
- * deployment prerequisites. An OAuth/stored login has no env key here — it carries as
13
- * `FASTAGENT_AUTH_SEED` on the `--run` path (see each host's run module), not as a named runbook secret.
14
- */
15
- export declare function deploymentSecrets(modelAuth: string | undefined, channels: readonly DeclaredChannel[], extraSecrets?: string[]): {
11
+ /** One variable a runbook tells the operator to set, and where to look for its value. */
12
+ export interface DeploymentSecret {
16
13
  name: string;
17
14
  hint: string;
18
- required: boolean;
19
- }[];
15
+ }
20
16
  /**
21
- * Assemble the secret VALUES a `--run` deploy sets on the host, from the local credential + channels —
22
- * pure, host-neutral (Fly sets them via `fly secrets import`, Railway via `railway variables set`), so
23
- * the security-sensitive key wiring is testable once. The model credential travels one of two ways: an
24
- * env-key auth as its own secret (value from `env`), OR an OAuth/stored login (no plaintext key) as
25
- * `FASTAGENT_AUTH_SEED` (base64 auth.json) which `start` materializes on first boot. `needsModelCredential`
26
- * (neither present) is a DISTINCT signal: its remediation is `fastagent login`, not the `.env` one that
27
- * `missingSecrets` (real secret NAMES with no value) carries.
17
+ * What a runbook lists: the names that must have a value (the model's env key, every declaration — hinted by the file
18
+ * that declared it) and then every other variable the value file carries.
19
+ */
20
+ export declare function deploymentSecrets(modelAuth: string | undefined, declared: readonly DeclaredSecret[], values: ReadonlyMap<string, string>, valueFile: string): DeploymentSecret[];
21
+ /**
22
+ * Assemble the VALUES a `--run` deploy sets on the host: every variable the value file carries, plus the local
23
+ * credential. Never minted here.
28
24
  *
29
- * Channel secrets come from the local env only — NEVER minted. A random mint would be wrong for a
30
- * human-shared secret (github's webhook secret must match the value the operator enters in the repo,
31
- * which a silent mint never surfaces) and would rotate every run (breaking idempotency). An absent
32
- * required value enters `missingSecrets`; an absent optional value is simply omitted.
25
+ * `values` is the selected value file, read as data — **the environment running `deploy` is not a source**. A
26
+ * deployment must be reproducible from what it carries, and a variable that happens to be exported on the builder is
27
+ * written down nowhere. CI supplies values by writing the file before running the command.
33
28
  */
34
29
  export declare function assembleSecrets(input: {
35
30
  modelAuth: string | undefined;
36
- /** The definition carries the model key itself (a models.json literal `apiKey` / `!command`): there is
37
- * no value to carry and no gate to raise — see {@link modelCredentialCarry}. */
31
+ /**
32
+ * The definition carries the model key itself (a models.json literal `apiKey` / `!command`): there is no value to
33
+ * carry and no gate to raise.
34
+ */
38
35
  modelKeyInDefinition?: boolean;
39
36
  authFile: Buffer | undefined;
40
- channels: readonly DeclaredChannel[];
41
- /** Extra secret env-var names from `fastagent.config` deploy.secrets — carried like channel secrets. */
42
- extraSecrets?: string[];
43
- env: NodeJS.ProcessEnv;
37
+ /** Every name the definition declared it needs — what must have a value. */
38
+ declared?: readonly DeclaredSecret[];
39
+ /** The selected value file's contents (`loadEnvValues`). The ONLY source of operator-supplied values. */
40
+ values: ReadonlyMap<string, string>;
44
41
  }): {
45
42
  secrets: Record<string, string>;
46
43
  missingSecrets: string[];
47
44
  needsModelCredential: boolean;
48
45
  };
49
46
  /**
50
- * The bytes to seed to the auth file, or undefined to leave it alone — the pure core of `start`'s
51
- * FASTAGENT_AUTH_SEED materialization (the read side of {@link assembleSecrets}'s carry). ABSENT-ONLY
52
- * by design: a present file (a refreshed volume copy) is never overwritten by the stale seed, so a box
53
- * that ran its own OAuth refresh is not rolled back.
47
+ * The ONE refusal for declared names the deployed environment does not supply a value for — every host reaches it
48
+ * before its first side effect. Host-neutral because the reason is: the value file IS the deployed environment, so a
49
+ * name missing from it is missing from the deployment, whatever platform receives it.
54
50
  */
51
+ export declare function missingValuesGate(missing: readonly string[], valueFile: string): string | undefined;
52
+ /** The bytes to seed to the auth file, or undefined to leave it alone. */
55
53
  export declare function authSeedBytes(seed: string | undefined, fileExists: boolean): Buffer | undefined;
54
+ /** `FASTAGENT_AUTH_SEED` (+ `_2`, …): the base64 auth.json a deploy carries. */
55
+ export declare function collectAuthSeed(env: NodeJS.ProcessEnv): string | undefined;
56
56
  /**
57
- * Collect the (possibly CHUNKED) auth seed from the environment: `FASTAGENT_AUTH_SEED` plus numbered
58
- * continuations (`_2`, `_3`, …) concatenated in order. Hosts whose env values carry a small max
59
- * length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
60
- * them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
61
- * Collection stops at the first absent/empty continuation — the writer fills them contiguously.
57
+ * Expand `FASTAGENT_ENV` into the process environment — what `start` does first on a host that carries the value file
58
+ * that way. The platform's own variables still win, as they do over a value file anywhere.
62
59
  */
63
- export declare function collectAuthSeed(env: NodeJS.ProcessEnv): string | undefined;
60
+ export declare function applyCarriedEnv(env?: NodeJS.ProcessEnv): void;
61
+ /** The carried variables as the base64 JSON {@link collectCarriedEnv} reads back; empty when nothing travels. */
62
+ export declare function encodeCarriedEnv(carried: Readonly<Record<string, string>>): string;