@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,31 @@
1
- /**
2
- * `fastagent deploy agentcore --run` — drive the AWS CLI + Docker to completion. The middle of the
3
- * deploy the plain runbook hands to the operator; `--run` executes it so a coding agent runs ONE
4
- * command. Idempotent (ECR check-then-act; `cloudformation deploy` converges the stack) and
5
- * resumable: it STOPS at a human gate with one actionable line and a non-zero exit.
6
- *
7
- * TWO runners, one seam ({@link CliRunner}): `aws` (identity, ECR, CloudFormation) and `docker`
8
- * (buildx). AgentCore is the ONE host whose image builds on the operator's machine — the platform
9
- * requires linux/arm64 in the account's ECR and has no remote builder — so a missing Docker/buildx
10
- * is a first-class gate, not an incidental failure.
11
- *
12
- * Secrets ride CloudFormation NoEcho parameters. `--parameter-overrides` on argv would put the
13
- * values in the process listing (the same reason Fly imports secrets over stdin), so they go through
14
- * a caller-provided temp parameters file (`file://…`, mode 0600, deleted by the caller) — the write
15
- * is injected to keep this module pure and the security-sensitive wiring testable.
16
- */
1
+ /** `fastagent deploy agentcore --run` — drive the AWS CLI + Docker to completion. */
17
2
  import { RESERVED_PATHS } from "../../channels/agentcore-protocol.js";
18
3
  import { registerWebhooks } from "../channel-ingress.js";
4
+ import { awsCli, awsJson } from "./aws-cli.js";
19
5
  import { createHash } from "node:crypto";
20
6
  import { Buffer } from "node:buffer";
21
- import { AUTH_SEED_CHUNK_SIZE, AUTH_SEED_MAX_CHUNKS, cfnParamName, forwarderSource, ingressSessionId, stateBucketName, } from "./plan.js";
7
+ import { CARRIERS, CARRIER_CHUNK_SIZE, CARRIER_MAX_CHUNKS, MOUNT, agentcoreRepoName, agentcoreStackName, carrierChunk, deploymentBucketName, forwarderSource, ingressSessionId, } from "./plan.js";
22
8
  import { zipSingleFile } from "./zip.js";
23
- /** How long the post-deploy probe waits for the fresh session (image pull + microVM boot + snapshot
24
- * restore + channel construction) before gating with the last answer. */
25
- const PROBE_TIMEOUT_MS = 120_000;
26
- const PROBE_INTERVAL_MS = 3_000;
9
+ import { encodeCarriedEnv, missingValuesGate } from "../secrets.js";
27
10
  /**
28
- * Drive the forwarder's reserved probe path until it answers, and read the runtime's
29
- * STRUCTURED verdict. The path answers on every forwarder topology (a schedule-only URL refuses
30
- * ordinary public traffic, so a plain `GET /health` would 404 there), and the verdict rides a
31
- * transport-200 JSON body `{ ok, error? }` — the ordinary webhook relay folds a non-200 transport
32
- * into an opaque 502, which would strip the very diagnostics this probe exists to carry.
33
- *
34
- * Outcome policy: `ok:true` verifies the deploy; `ok:false` gates IMMEDIATELY with the runtime's own
35
- * error text (construction rejections are cached per session, so polling cannot change the answer);
36
- * anything else (unroutable URL, forwarder 4xx/5xx, malformed body) is retried to the deadline —
37
- * that budget's job is absorbing cold-start provisioning — and then gates with the last answer seen.
11
+ * Stack statuses that hold NO agent memory: a create that never succeeded, its rollback, a change set never
12
+ * executed, or a stack already gone. Everything else — including `UPDATE_ROLLBACK_COMPLETE` and any status AWS adds
13
+ * later — is treated as carrying state, so an unrecognized status over-warns instead of quietly promising nothing is
14
+ * lost.
38
15
  */
16
+ const EMPTY_STACK_STATUSES = new Set([
17
+ "CREATE_FAILED",
18
+ "CREATE_IN_PROGRESS",
19
+ "ROLLBACK_IN_PROGRESS",
20
+ "ROLLBACK_FAILED",
21
+ "ROLLBACK_COMPLETE",
22
+ "REVIEW_IN_PROGRESS",
23
+ "DELETE_COMPLETE",
24
+ ]);
25
+ /** Budget for image pull, storage initialization and channel construction. */
26
+ const PROBE_TIMEOUT_MS = 240_000;
27
+ const PROBE_INTERVAL_MS = 3_000;
28
+ /** Drive the forwarder's reserved probe path until it answers, and read the runtime's STRUCTURED verdict. */
39
29
  async function probeRuntime(probeUrl, auth, fetchImpl, timeoutMs = PROBE_TIMEOUT_MS, intervalMs = PROBE_INTERVAL_MS) {
40
30
  const deadline = Date.now() + timeoutMs;
41
31
  let last;
@@ -54,7 +44,7 @@ async function probeRuntime(probeUrl, auth, fetchImpl, timeoutMs = PROBE_TIMEOUT
54
44
  verdict = JSON.parse(bodyText);
55
45
  }
56
46
  catch {
57
- /* malformed — fall through to retry with it as the last answer */
47
+ // malformed — fall through to retry with it as the last answer
58
48
  }
59
49
  if (verdict?.ok === true)
60
50
  return { ok: true };
@@ -67,7 +57,7 @@ async function probeRuntime(probeUrl, auth, fetchImpl, timeoutMs = PROBE_TIMEOUT
67
57
  last = `${res.status}${firstLine ? ` ${firstLine}` : ""}`;
68
58
  }
69
59
  catch {
70
- /* not routable yet (Function URL DNS, cold start) — keep polling until the deadline */
60
+ // not routable yet (Function URL DNS, cold start) — keep polling until the deadline
71
61
  }
72
62
  if (Date.now() >= deadline) {
73
63
  return {
@@ -81,90 +71,79 @@ async function probeRuntime(probeUrl, auth, fetchImpl, timeoutMs = PROBE_TIMEOUT
81
71
  }
82
72
  }
83
73
  /** Stack outputs (`describe-stacks --query "Stacks[0].Outputs"`) → { OutputKey: OutputValue }. */
84
- export function parseStackOutputs(stdout) {
85
- try {
86
- const arr = JSON.parse(stdout);
87
- if (!Array.isArray(arr))
88
- return {};
89
- const out = {};
90
- for (const o of arr) {
91
- if (typeof o?.OutputKey === "string" && typeof o?.OutputValue === "string")
92
- out[o.OutputKey] = o.OutputValue;
93
- }
94
- return out;
95
- }
96
- catch {
74
+ /**
75
+ * `Stacks[0].Outputs` as a map, or `undefined` when the document is not that list at all.
76
+ *
77
+ * `null` IS AN ANSWER, and an empty map is what it says: that is what JMESPath prints for a stack with no
78
+ * Outputs section, i.e. a template edited or generated without one. Reading it as unreadable blamed
79
+ * `aws cloudformation describe-stacks` — a command that had just succeeded — instead of letting the caller's
80
+ * own "no RuntimeArn, regenerate with --force" gate say what to do. Distinct from empty stdout, which is the
81
+ * CLI printing nothing at all (aws-cli.ts, point 4).
82
+ */
83
+ export function pickStackOutputs(parsed) {
84
+ if (parsed === null)
97
85
  return {};
86
+ if (!Array.isArray(parsed))
87
+ return undefined;
88
+ const out = {};
89
+ for (const o of parsed) {
90
+ if (typeof o?.OutputKey === "string" && typeof o?.OutputValue === "string")
91
+ out[o.OutputKey] = o.OutputValue;
98
92
  }
93
+ return out;
94
+ }
95
+ /** The secrets `--run` mints for this host, each with the template parameter of its own. */
96
+ const MINTED_PARAMS = {
97
+ FASTAGENT_INGRESS_SECRET: "FastagentIngressSecret",
98
+ FASTAGENT_WAKE_SECRET: "FastagentWakeSecret",
99
+ };
100
+ /** What each carrier holds for this deploy: the credential seed, and every other variable as one encoded object. */
101
+ function carrierValues(secrets) {
102
+ const { FASTAGENT_AUTH_SEED: seed = "", ...rest } = secrets;
103
+ const carried = Object.fromEntries(Object.entries(rest).filter(([name]) => !(name in MINTED_PARAMS)));
104
+ return { FASTAGENT_AUTH_SEED: seed, FASTAGENT_ENV: encodeCarriedEnv(carried) };
99
105
  }
100
- /** The `--parameter-overrides file://` payload: a JSON array of "Key=Value" strings. The auth seed
101
- * is CHUNKED across FastagentAuthSeed(2…) — AgentCore env values cap at 2048 chars and a real OAuth
102
- * auth.json's base64 exceeds it; `start` reassembles (collectAuthSeed). Every chunk is emitted on
103
- * every deploy, including empty trailing chunks, so CloudFormation cannot retain stale values. */
106
+ /** The `--parameter-overrides file://` payload: a JSON array of "Key=Value" strings. */
104
107
  export function paramsFileContent(imageUri, secrets, forwarder) {
105
- const params = [`ImageUri=${imageUri}`];
106
- if (forwarder)
107
- params.push(`StateBucket=${forwarder.bucket}`, `ForwarderS3Key=${forwarder.key}`);
108
- for (const [k, v] of Object.entries(secrets)) {
109
- if (k !== "FASTAGENT_AUTH_SEED")
110
- params.push(`${cfnParamName(k)}=${v}`);
108
+ const params = [`ImageUri=${imageUri}`, `ForwarderBucket=${forwarder.bucket}`, `ForwarderS3Key=${forwarder.key}`];
109
+ for (const [name, param] of Object.entries(MINTED_PARAMS)) {
110
+ if (secrets[name] !== undefined)
111
+ params.push(`${param}=${secrets[name]}`);
111
112
  }
112
- const seed = secrets.FASTAGENT_AUTH_SEED ?? "";
113
- for (let i = 0; i < AUTH_SEED_MAX_CHUNKS; i++) {
114
- const param = i === 0 ? "FastagentAuthSeed" : `FastagentAuthSeed${i + 1}`;
115
- params.push(`${param}=${seed.slice(i * AUTH_SEED_CHUNK_SIZE, (i + 1) * AUTH_SEED_CHUNK_SIZE)}`);
116
- }
117
- return `${JSON.stringify(params)}\n`;
118
- }
119
- /** Parse the runtime's checkpoint acknowledgement without treating malformed output as success. */
120
- export function parseCheckpointReply(stdout) {
121
- try {
122
- const parsed = JSON.parse(stdout.trim());
123
- if (parsed === null ||
124
- typeof parsed !== "object" ||
125
- !("written" in parsed) ||
126
- typeof parsed.written !== "boolean") {
127
- return undefined;
113
+ // Every chunk is written, empty ones included: an omitted parameter keeps its PREVIOUS value on a stack update.
114
+ const values = carrierValues(secrets);
115
+ for (const carrier of CARRIERS) {
116
+ const value = values[carrier.env];
117
+ for (let i = 0; i < CARRIER_MAX_CHUNKS; i++) {
118
+ params.push(`${carrierChunk(carrier, i).param}=${value.slice(i * CARRIER_CHUNK_SIZE, (i + 1) * CARRIER_CHUNK_SIZE)}`);
128
119
  }
129
- return {
130
- written: parsed.written,
131
- reason: "reason" in parsed && typeof parsed.reason === "string" ? parsed.reason : undefined,
132
- };
133
- }
134
- catch {
135
- return undefined;
136
120
  }
121
+ return `${JSON.stringify(params)}\n`;
137
122
  }
138
- /**
139
- * Run the deploy through `aws` + `docker`. `log` reports progress; the injected registrars perform
140
- * post-deploy webhook steps from the builder machine against the forwarder's Function URL. Every
141
- * gate is fail-visible; `writeSecretFile` is the caller's 0600-temp-file seam (see the header).
142
- */
123
+ /** Run the deploy through `aws` + `docker`. */
143
124
  export async function deployAgentcoreRun(plan, aws, docker, log, writeSecretFile, writeForwarderZip, registrars,
144
125
  /** Injected in tests; the probe itself stays inside the run so no deploy can skip it. */
145
126
  probe = {}) {
146
127
  const gate = (g) => ({ ok: false, gate: g });
147
- const stack = `fastagent-${plan.name}`;
148
- const repo = `fastagent/${plan.name}`;
149
- // 1. Identity + region — the two facts everything downstream (registry hostname, stack region)
150
- // hangs on. `sts get-caller-identity` succeeds with any working credential source.
151
- const identity = await aws(["sts", "get-caller-identity", "--output", "json"], { capture: true });
152
- if (identity.code === 127) {
153
- return gate("aws CLI not found — install AWS CLI v2: https://docs.aws.amazon.com/cli/, then re-run");
154
- }
155
- if (identity.code !== 0) {
156
- return gate("no working AWS credentials — run `aws configure` (or set AWS_ACCESS_KEY_ID/…), then re-run");
157
- }
158
- let account;
159
- try {
160
- const parsed = JSON.parse(identity.stdout);
161
- if (typeof parsed.Account !== "string")
162
- throw new Error("no Account");
163
- account = parsed.Account;
164
- }
165
- catch {
166
- return gate("could not read the account id from `aws sts get-caller-identity` — see the output above");
128
+ const stack = agentcoreStackName(plan.name);
129
+ const repo = agentcoreRepoName(plan.name);
130
+ // 1.
131
+ const cli = awsCli(aws);
132
+ const identity = await cli.read(["sts", "get-caller-identity", "--output", "json"], awsJson((parsed) => {
133
+ const { Account, Arn } = (parsed ?? {});
134
+ return typeof Account === "string"
135
+ ? { account: Account, principal: typeof Arn === "string" ? Arn : undefined }
136
+ : undefined;
137
+ }));
138
+ if (!("ok" in identity)) {
139
+ if ("absent" in identity)
140
+ return gate("`aws sts get-caller-identity` found no caller — run `aws configure`");
141
+ if (identity.code === 127)
142
+ return gate(`${identity.unreadable}, then re-run`);
143
+ // QUOTED: expired token, a missing profile and a proxy that eats STS are three different next actions.
144
+ return gate(`no working AWS credentials (${identity.unreadable}) — run \`aws configure\`, then re-run`);
167
145
  }
146
+ const { account, principal } = identity.ok;
168
147
  let region = plan.region;
169
148
  if (!region) {
170
149
  const fromConfig = await aws(["configure", "get", "region"], { capture: true });
@@ -173,10 +152,7 @@ probe = {}) {
173
152
  if (!region) {
174
153
  return gate("no AWS region configured — set AWS_REGION (or `aws configure set region <region>`), then re-run");
175
154
  }
176
- // 2. Docker + buildx — this host builds LOCALLY (linux/arm64 into the account's ECR; AgentCore has
177
- // no remote builder), so their absence is a first-class gate with the install pointer. ANY
178
- // non-zero gates BEFORE side effects: `docker version` with the daemon down exits non-127, and
179
- // letting it through would create the ECR repo and then fail the build with a generic error.
155
+ // 2.
180
156
  const dockerVersion = await docker(["version"], { capture: true });
181
157
  if (dockerVersion.code === 127) {
182
158
  return gate("docker not found — install Docker (https://docs.docker.com/get-docker/), then re-run");
@@ -188,29 +164,91 @@ probe = {}) {
188
164
  return gate("docker buildx not available — the image must be linux/arm64 (cross-built); install buildx, then re-run");
189
165
  }
190
166
  // 3. Gate missing required secret VALUES before any side effect (no half-created infra).
191
- if (plan.missingSecrets.length > 0) {
192
- return gate(`no local value for: ${plan.missingSecrets.join(", ")} — set them in .env (or the environment) and re-run`);
193
- }
194
- // 3b. AgentCore env values cap at 2048 chars. The auth seed is chunked (paramsFileContent) up to
195
- // its ceiling; any OTHER oversized value has no chunk lane — gate it instead of a cryptic
196
- // CloudFormation "maxLength" failure mid-deploy.
197
- const seed = plan.secrets.FASTAGENT_AUTH_SEED;
198
- if (seed && seed.length > AUTH_SEED_CHUNK_SIZE * AUTH_SEED_MAX_CHUNKS) {
199
- return gate(`your auth.json is too large to carry (${seed.length} chars base64 > ${AUTH_SEED_CHUNK_SIZE * AUTH_SEED_MAX_CHUNKS}) — ` +
167
+ const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
168
+ if (missingValues)
169
+ return gate(missingValues);
170
+ // 3b.
171
+ const capacity = CARRIER_CHUNK_SIZE * CARRIER_MAX_CHUNKS;
172
+ const carried = carrierValues(plan.secrets);
173
+ if (carried.FASTAGENT_AUTH_SEED.length > capacity) {
174
+ return gate(`your auth.json is too large to carry (${carried.FASTAGENT_AUTH_SEED.length} chars base64 > ${capacity}) — ` +
200
175
  `slim it (keep only the model's credential), or set a provider API key in .env instead`);
201
176
  }
202
- for (const [k, v] of Object.entries(plan.secrets)) {
203
- if (k !== "FASTAGENT_AUTH_SEED" && v.length > 2048) {
204
- return gate(`secret ${k} is ${v.length} chars — AgentCore environment values cap at 2048; shorten it`);
205
- }
177
+ if (carried.FASTAGENT_ENV.length > capacity) {
178
+ return gate(`the variables in ${plan.valueFile} are too large to carry (${carried.FASTAGENT_ENV.length} chars encoded > ` +
179
+ `${capacity}) — move what the deployment does not need out of that file`);
206
180
  }
207
- // 4. ECR repository — check-then-act. A FAILED describe that isn't "not found" would misreport the
208
- // create, but ECR's not-found also exits non-zero — so try describe, and on failure attempt the
209
- // create; a create failing for a REAL reason (permissions) still gates with its own message.
181
+ // Name what travels from the value file onto the runtime.
182
+ const secretNames = Object.keys(plan.secrets);
183
+ if (secretNames.length > 0)
184
+ log(`carrying ${secretNames.length} secret(s): ${secretNames.join(", ")}`);
185
+ // 3c. Say what this deploy is ABOUT TO TOUCH while it can still be stopped for free: until here the account and
186
+ // region only surfaced several hundred log lines in, inside the ECR image URI.
187
+ const source = plan.region ? "the environment" : "aws configure";
188
+ log(`account ${account}${principal ? ` (${principal})` : ""}, region ${region} (from ${source}), image ${repo}:${plan.tag}`);
189
+ if (principal?.endsWith(":root")) {
190
+ log(`warn: deploying as the account root user — this stack's IAM roles are created under it; prefer an IAM role`);
191
+ }
192
+ // Warn, never gate: an aws CLI too old to know the service, or a role without ListAgentRuntimes, would make a gate
193
+ // refuse a valid deploy. The point is to say it BEFORE the multi-minute arm64 build, not to be authoritative.
194
+ // `--region` explicitly: every other region-dependent call here writes it out (the ECR registry URI, the bucket's
195
+ // LocationConstraint), and the warning below names ${region} — the probe must be about the SAME region it names.
196
+ const service = await cli.present([
197
+ "bedrock-agentcore-control",
198
+ "list-agent-runtimes",
199
+ "--max-items",
200
+ "1",
201
+ "--region",
202
+ region,
203
+ ]);
204
+ if (!("ok" in service)) {
205
+ const why = "unreadable" in service ? service.unreadable.split("\n")[0] : "";
206
+ log(`warn: could not confirm AgentCore is available in ${region}${why ? ` (${why})` : ""} — if it is not, ` +
207
+ `cloudformation deploy fails minutes from now, after the image build: ` +
208
+ `https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-regions.html`);
209
+ }
210
+ // 3d. Read the stack here, for the two questions that both depend on it: does this deploy destroy the agent's
211
+ // memory (below), and is there a failed first create to clear (step 7)? The destructive one is only worth saying
212
+ // while the multi-minute build has not run yet, and step 7 reuses this answer unless it was still in flight.
213
+ // An unanswered question must not read as "a first deploy, nothing to lose": `sts get-caller-identity` succeeding
214
+ // says nothing about whether this role can read CloudFormation. That three-way answer is aws-cli.ts's.
215
+ const readStackStatus = () => cli.read([
216
+ "cloudformation",
217
+ "describe-stacks",
218
+ "--stack-name",
219
+ stack,
220
+ "--query",
221
+ "Stacks[0].StackStatus",
222
+ "--output",
223
+ "text",
224
+ ], (stdout) => stdout.trim() || undefined);
225
+ const stackStatus = await readStackStatus();
226
+ const status = "ok" in stackStatus ? stackStatus.ok : "";
227
+ // The only answers the build can invalidate: one still in flight (a first create rolling back is exactly what step 7
228
+ // exists for, and minutes of arm64 build are long enough for it to settle), and one we never got.
229
+ const settling = status.endsWith("_IN_PROGRESS") || "unreadable" in stackStatus;
230
+ // Warn, never gate — same as the region probe above: a role without this read, or an older CLI, must not refuse a
231
+ // legitimate deploy.
232
+ if ("unreadable" in stackStatus) {
233
+ log(`warn: could not read stack ${stack} (${stackStatus.unreadable}) — if it exists, this deploy resets its ` +
234
+ `managed SessionStorage (${MOUNT}) and a failed first create will not be cleared`);
235
+ }
236
+ else if (status !== "" && !EMPTY_STACK_STATUSES.has(status)) {
237
+ log(`warn: this is a REDEPLOY and AWS resets managed SessionStorage (${MOUNT}) on every runtime version update — ` +
238
+ `sessions, channel state and pending wake-ups start blank` +
239
+ // Only the carried auth.json is re-seeded; a provider API key deployment has no such step to blame.
240
+ `${carried.FASTAGENT_AUTH_SEED ? ", and the model credential is re-seeded from FASTAGENT_AUTH_SEED" : ""}. ` +
241
+ `Cross-deploy memory needs a real volume: \`deploy fly\` or \`deploy railway\`.`);
242
+ }
243
+ // 4.
210
244
  const registry = `${account}.dkr.ecr.${region}.amazonaws.com`;
211
245
  const image = `${registry}/${repo}:${plan.tag}`;
212
- const described = await aws(["ecr", "describe-repositories", "--repository-names", repo], { capture: true });
213
- if (described.code === 0) {
246
+ const described = await cli.present(["ecr", "describe-repositories", "--repository-names", repo]);
247
+ if ("unreadable" in described) {
248
+ // NOT the create branch: a denial here must name this step, not the create that would follow.
249
+ return gate(`could not read ECR repository ${repo} (${described.unreadable}) — fix that, then re-run`);
250
+ }
251
+ if ("ok" in described) {
214
252
  log(`ECR repository ${repo} exists — skipping create`);
215
253
  }
216
254
  else {
@@ -219,84 +257,43 @@ probe = {}) {
219
257
  return gate("`aws ecr create-repository` failed — see the output above; fix and re-run");
220
258
  }
221
259
  }
222
- // 4b. The deployment bucket + the forwarder package. The bucket is created OUTSIDE the stack, on
223
- // purpose and unlike everything else here: it holds the agent's STATE SNAPSHOT, and AgentCore
224
- // wipes the /mnt/state mount on every runtime version update (i.e. every deploy). Keeping it
225
- // out of CloudFormation means a `delete-stack` — or a rolled-back create — cannot take the
226
- // agent's sessions, channel state and pending wake-ups with it.
227
- let forwarderParams;
228
- if (plan.topology.forwarder) {
229
- const bucket = stateBucketName(plan.name, account);
230
- if ((await aws(["s3api", "head-bucket", "--bucket", bucket], { capture: true })).code !== 0) {
231
- log(`creating deployment bucket ${bucket}…`);
232
- // us-east-1 is the ONE region that must not carry a LocationConstraint (the API rejects it).
233
- const createArgs = ["s3api", "create-bucket", "--bucket", bucket];
234
- if (region !== "us-east-1")
235
- createArgs.push("--create-bucket-configuration", `LocationConstraint=${region}`);
236
- if ((await aws(createArgs)).code !== 0) {
237
- return gate(`\`aws s3api create-bucket --bucket ${bucket}\` failed — see the output above; fix and re-run`);
238
- }
239
- }
240
- // CONVERGE the properties on EVERY deploy, not just at creation. They are what makes the bucket
241
- // safe (nothing public) and recoverable (a bad write is not the end of the agent's memory); doing
242
- // them only in the create branch means a run that failed halfway leaves a bucket that looks
243
- // finished forever after, and ignoring the exit codes means "deployed" would be reported over a
244
- // world-readable or unversioned store of the agent's credentials.
245
- const converge = [
246
- {
247
- label: "block public access",
248
- args: [
249
- "s3api",
250
- "put-public-access-block",
251
- "--bucket",
252
- bucket,
253
- "--public-access-block-configuration",
254
- "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true",
255
- ],
256
- },
257
- {
258
- label: "enable versioning",
259
- args: ["s3api", "put-bucket-versioning", "--bucket", bucket, "--versioning-configuration", "Status=Enabled"],
260
- },
261
- {
262
- label: "set the snapshot lifecycle",
263
- args: [
264
- "s3api",
265
- "put-bucket-lifecycle-configuration",
266
- "--bucket",
267
- bucket,
268
- "--lifecycle-configuration",
269
- JSON.stringify({
270
- Rules: [
271
- {
272
- ID: "fastagent-expire-old-snapshots",
273
- Status: "Enabled",
274
- Filter: { Prefix: "state/" },
275
- NoncurrentVersionExpiration: { NoncurrentDays: 7 },
276
- },
277
- ],
278
- }),
279
- ],
280
- },
281
- ];
282
- for (const step of converge) {
283
- // `capture` keeps the CLI's JSON off the deploy log: the exit code is the signal, and a failure
284
- // gates with the step's name below.
285
- if ((await aws(step.args, { capture: true })).code !== 0) {
286
- return gate(`could not ${step.label} on ${bucket} — refusing to store agent state in it; fix and re-run`);
287
- }
288
- }
289
- // Content-hashed key: CloudFormation rolls the function only when a parameter VALUE changes, so
290
- // identical source must map to an identical key (hence the deterministic zip) and changed source
291
- // to a new one.
292
- const zip = zipSingleFile("index.js", Buffer.from(forwarderSource()));
293
- const key = `forwarder/${createHash("sha256").update(zip).digest("hex").slice(0, 16)}.zip`;
294
- const zipPath = await writeForwarderZip(zip);
295
- if ((await aws(["s3", "cp", zipPath, `s3://${bucket}/${key}`])).code !== 0) {
296
- return gate("uploading the forwarder package to S3 failed — see the output above; fix and re-run");
260
+ // The forwarder package must exist before CloudFormation can create its Lambda.
261
+ const bucket = deploymentBucketName(plan.name, account);
262
+ const head = await cli.present(["s3api", "head-bucket", "--bucket", bucket]);
263
+ // Same shape as the repository above: a 403 is not a 404, and creating on top of it fails with a message
264
+ // about the wrong thing.
265
+ if ("unreadable" in head) {
266
+ return gate(`could not read deployment bucket ${bucket} (${head.unreadable}) — fix that, then re-run`);
267
+ }
268
+ if ("absent" in head) {
269
+ log(`creating deployment bucket ${bucket}…`);
270
+ // us-east-1 is the ONE region that must not carry a LocationConstraint (the API rejects it).
271
+ const createArgs = ["s3api", "create-bucket", "--bucket", bucket];
272
+ if (region !== "us-east-1")
273
+ createArgs.push("--create-bucket-configuration", `LocationConstraint=${region}`);
274
+ if ((await aws(createArgs)).code !== 0) {
275
+ return gate(`\`aws s3api create-bucket --bucket ${bucket}\` failed — see the output above; fix and re-run`);
297
276
  }
298
- forwarderParams = { bucket, key };
299
277
  }
278
+ if ((await aws([
279
+ "s3api",
280
+ "put-public-access-block",
281
+ "--bucket",
282
+ bucket,
283
+ "--public-access-block-configuration",
284
+ "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true",
285
+ ], { capture: true })).code !== 0) {
286
+ return gate(`could not block public access on ${bucket}; fix and re-run`);
287
+ }
288
+ // Content-hashed key: CloudFormation rolls the function only when a parameter VALUE changes, so identical source
289
+ // must map to an identical key (hence the deterministic zip) and changed source to a new one.
290
+ const zip = zipSingleFile("index.js", Buffer.from(forwarderSource()));
291
+ const key = `forwarder/${createHash("sha256").update(zip).digest("hex").slice(0, 16)}.zip`;
292
+ const zipPath = await writeForwarderZip(zip);
293
+ if ((await aws(["s3", "cp", zipPath, `s3://${bucket}/${key}`])).code !== 0) {
294
+ return gate("uploading the forwarder package to S3 failed — see the output above; fix and re-run");
295
+ }
296
+ const forwarderParams = { bucket, key };
300
297
  // 5. Registry login — the password flows stdout→stdin between the two runners, never argv.
301
298
  const password = await aws(["ecr", "get-login-password"], { capture: true });
302
299
  if (password.code !== 0)
@@ -307,29 +304,14 @@ probe = {}) {
307
304
  // 6. Build (linux/arm64) + push in one step.
308
305
  log(`building + pushing ${image} (linux/arm64)…`);
309
306
  const buildArgs = ["buildx", "build", "--platform", "linux/arm64", "-t", image, "--push"];
310
- if (plan.dockerfilePath)
311
- buildArgs.push("-f", plan.dockerfilePath);
307
+ buildArgs.push("-f", plan.dockerfilePath);
312
308
  buildArgs.push(".");
313
309
  if ((await docker(buildArgs)).code !== 0) {
314
310
  return gate("`docker buildx build` failed — see the output above; fix and re-run");
315
311
  }
316
- // 7. Deploy the stack. Secret values ride the temp params file (file://), never argv.
317
- // --no-fail-on-empty-changeset: a re-run whose only change already applied must not gate.
318
- // Self-heal the one un-resumable state first: a FAILED first create leaves the stack in
319
- // ROLLBACK_COMPLETE, which CloudFormation refuses to update — without this, "fix and re-run"
320
- // (our own gate advice) would dead-end on a different error. Nothing real is lost by deleting:
321
- // a ROLLBACK_COMPLETE stack holds no live resources.
322
- const status = await aws([
323
- "cloudformation",
324
- "describe-stacks",
325
- "--stack-name",
326
- stack,
327
- "--query",
328
- "Stacks[0].StackStatus",
329
- "--output",
330
- "text",
331
- ], { capture: true });
332
- if (status.code === 0 && status.stdout.trim() === "ROLLBACK_COMPLETE") {
312
+ // 7. The 3d answer stands unless it was still settling then — a second read is the narrow exception, not the rule.
313
+ const beforeDeploy = settling ? await readStackStatus() : stackStatus;
314
+ if ("ok" in beforeDeploy && beforeDeploy.ok === "ROLLBACK_COMPLETE") {
333
315
  log(`stack ${stack} is ROLLBACK_COMPLETE (a failed first create) — deleting it before re-creating…`);
334
316
  if ((await aws(["cloudformation", "delete-stack", "--stack-name", stack])).code !== 0) {
335
317
  return gate("`aws cloudformation delete-stack` failed — see the output above");
@@ -358,66 +340,18 @@ probe = {}) {
358
340
  `(aws cloudformation describe-stack-events --stack-name ${stack}), fix, and re-run`);
359
341
  }
360
342
  // 8. Outputs — the runtime ARN (the data plane) and the forwarder URL (the webhook surface).
361
- const outputsQuery = await aws(["cloudformation", "describe-stacks", "--stack-name", stack, "--query", "Stacks[0].Outputs", "--output", "json"], { capture: true });
362
- if (outputsQuery.code !== 0)
363
- return gate("`aws cloudformation describe-stacks` failed — see the output above");
364
- const outputs = parseStackOutputs(outputsQuery.stdout);
343
+ const outputsRead = await cli.read(["cloudformation", "describe-stacks", "--stack-name", stack, "--query", "Stacks[0].Outputs", "--output", "json"], awsJson(pickStackOutputs));
344
+ if (!("ok" in outputsRead)) {
345
+ const why = "absent" in outputsRead ? `${stack} is not there` : outputsRead.unreadable;
346
+ return gate(`\`aws cloudformation describe-stacks\` failed after a successful deploy (${why})`);
347
+ }
348
+ const outputs = outputsRead.ok;
365
349
  const runtimeArn = outputs.RuntimeArn;
366
350
  if (!runtimeArn)
367
351
  return gate("stack has no RuntimeArn output — was the template edited? Regenerate with --force");
368
352
  const url = outputs.ForwarderUrl?.replace(/\/$/, ""); // registrars append /<path>; no double slash
369
- // 8b. Restart the ingress session so the new image serves IMMEDIATELY. A live session keeps its
370
- // old compute until the idle timeout or the max compute lifetime (8 h) — without this, a
371
- // redeploy "succeeds" while an actively-chatting session keeps answering from the PREVIOUS
372
- // image (the exact silent trap the first real deploy hit). Failure is advisory, never a gate:
373
- // on a first deploy the session does not exist yet, and the stop is an immediacy optimization
374
- // — the platform's reclaim gets there eventually. An in-flight turn on the old compute is cut;
375
- // the checkpoint above is what lets a replaying channel re-run it. Only when a forwarder exists
376
- // (the ingress session is the forwarder's session; pure-invoke deployments have none).
377
- // Keyed on the FORWARDER: every current forwarder has a callback URL for state-capability refresh,
378
- // and every forwarder topology has an ingress session whose next event would otherwise land on compute still
379
- // running the previous image.
380
- if (plan.topology.forwarder) {
381
- // CHECKPOINT FIRST. The stop cuts whatever turn is running, and that turn's durable intent was
382
- // written to a mount the version update erases — so without this flush "replay re-runs it" would
383
- // be false: the intent never reaches S3 and the message is simply gone. Best-effort: a session
384
- // that is not up has nothing to lose, and a failure here must not block the (already applied)
385
- // deploy — it only downgrades the promise, so say so.
386
- const checkpointPayloadPath = await writeSecretFile(`${JSON.stringify({ kind: "checkpoint", auth: plan.secrets.FASTAGENT_INGRESS_SECRET })}\n`);
387
- const checkpoint = await aws([
388
- "bedrock-agentcore",
389
- "invoke-agent-runtime",
390
- "--agent-runtime-arn",
391
- runtimeArn,
392
- "--runtime-session-id",
393
- ingressSessionId(plan.name),
394
- "--payload",
395
- `file://${checkpointPayloadPath}`,
396
- "--cli-binary-format",
397
- "raw-in-base64-out",
398
- "/dev/stdout",
399
- ], { capture: true, captureStderr: true });
400
- // Report what the container ACTUALLY did. This line is the only signal an operator has about
401
- // whether an in-flight turn survived the deploy, so a blanket "checkpointed" — printed even when
402
- // nothing was written — would be worse than no line at all.
403
- if (checkpoint.code !== 0) {
404
- log("note: could not reach the ingress session to checkpoint — if a turn was in flight it is lost " +
405
- "rather than replayed (see the output above)");
406
- }
407
- else {
408
- const reply = parseCheckpointReply(checkpoint.stdout);
409
- if (reply?.written) {
410
- log("checkpointed the ingress session (an interrupted turn can be replayed)");
411
- }
412
- else if (reply) {
413
- // The ordinary case: the session was already idle-reclaimed, so its snapshot was written when
414
- // it settled and there is nothing in flight to lose.
415
- log(`note: nothing to checkpoint${reply.reason ? ` — ${reply.reason}` : " (no session was running)"}`);
416
- }
417
- else {
418
- log("warn: ingress session returned an invalid checkpoint response — could not verify the state snapshot");
419
- }
420
- }
353
+ // A live session keeps its previous image. Stop the fixed writer before verifying the new release.
354
+ {
421
355
  log("stopping the ingress session so the new image serves immediately…");
422
356
  const stopCommand = [
423
357
  "bedrock-agentcore",
@@ -427,21 +361,19 @@ probe = {}) {
427
361
  "--runtime-session-id",
428
362
  ingressSessionId(plan.name),
429
363
  ];
430
- const stopped = await aws(stopCommand, { capture: true, captureStderr: true });
431
- if (stopped.code !== 0) {
432
- // Classify, don't guess: "no session yet" (first deploy — expected, quiet note) vs a REAL stop
433
- // failure (permissions/CLI/network — the old image may keep serving, say so loudly with the
434
- // manual command). Not a gate: the deploy itself succeeded, and stop is an immediacy
435
- // optimization — the platform's reclaim converges regardless.
436
- const stderr = stopped.stderr ?? "";
437
- if (/ResourceNotFound|not\s*found|does not exist/i.test(stderr)) {
364
+ const stopped = await cli.write(stopCommand);
365
+ if (!("done" in stopped)) {
366
+ // Classify, don't guess: "no session yet" (first deploy — expected, quiet note) vs a REAL stop failure
367
+ // (permissions/CLI/network), which must stop verification against the previous image. Both words are
368
+ // aws-cli.ts's — this file had its own third spelling of "already gone".
369
+ const noSession = "absent" in stopped;
370
+ const stderr = noSession ? "" : stopped.refused;
371
+ if (noSession) {
438
372
  log("note: no ingress session to stop (first deploy, or already reclaimed)");
439
373
  }
440
374
  else {
441
- // A GATE, not a warning: the probe below reaches the SAME fixed session id, so a session
442
- // still running the previous image would answer it and the deploy would claim to have
443
- // verified a serving path it never touched. Unable to guarantee the session is fresh =
444
- // unable to verify = stop.
375
+ // A GATE, not a warning: the probe below reaches the SAME fixed session id, so a session still running the
376
+ // previous image would answer it and the deploy would claim to have verified a serving path it never touched.
445
377
  const firstLine = stderr.trim().split("\n")[0];
446
378
  return gate(`could not stop the ingress session — it may still be serving the PREVIOUS image, so the ` +
447
379
  `deploy cannot verify the new one${firstLine ? ` (${firstLine})` : ""}. ` +
@@ -449,31 +381,20 @@ probe = {}) {
449
381
  }
450
382
  }
451
383
  }
452
- // 8c. Every forwarder topology MUST carry the ForwarderUrl output — schedule-only and
453
- // selfSchedule-only deployments included, since the probe below is their only construction
454
- // check (there is no boot-time failStartup on this host). A missing output means an edited
455
- // template; skipping the probe silently would let such a deploy report success unverified.
456
- // Only a pure-invoke deployment (no forwarder) legitimately has no URL and nothing to probe.
457
- if (plan.topology.forwarder && !url) {
384
+ // 8c.
385
+ if (!url) {
458
386
  return gate("this deployment needs the forwarder but the stack has no ForwarderUrl output — regenerate the " +
459
387
  "template with --force");
460
388
  }
461
- // 8d. Warm + verify the NEW serving path end to end, BEFORE registration: the probe wakes a fresh
462
- // session on the new image through the forwarder's reserved path, which restores the state
463
- // snapshot and constructs the channels — construction is deferred to exactly that moment
464
- // (channels/agentcore.ts), so this is where a bad credential, a broken channels/ module, or an
465
- // unrestorable snapshot surfaces AT DEPLOY TIME with the runtime's own error text.
389
+ // Verify storage initialization and channel construction before registering webhooks.
466
390
  if (url) {
467
- log("probing the deployed runtime (state restore + channel construction)…");
391
+ log("probing the deployed runtime (workspace initialization + channel construction)…");
468
392
  const verdict = await probeRuntime(`${url}${RESERVED_PATHS.probe}`, plan.secrets.FASTAGENT_INGRESS_SECRET ?? "", probe.fetchImpl ?? fetch, probe.timeoutMs, probe.intervalMs);
469
393
  if (!verdict.ok)
470
394
  return gate(verdict.gate);
471
- log("runtime verified (state restored, channels constructed)");
395
+ log("runtime verified (workspace ready, channels constructed)");
472
396
  }
473
- // 9. Post-deploy webhook registration — same registrar seam as every host, pointed at the
474
- // forwarder's Function URL. Gate policy is the shared registration-gate kernel.
475
- // No long-connection channels reach here: `deploy.ts` gates them before the driver runs (AgentCore
476
- // has no resident process to hold a connection), so the deploy's channels are all webhook ones.
397
+ // 9.
477
398
  const registrationGateMsg = url
478
399
  ? await registerWebhooks({
479
400
  baseUrl: url,