@fastagent-sh/fastagent 0.21.1 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +9 -12
  2. package/dist/agent.d.ts +24 -37
  3. package/dist/agent.js +7 -15
  4. package/dist/atomic-write.d.ts +13 -17
  5. package/dist/atomic-write.js +18 -29
  6. package/dist/bind.d.ts +12 -22
  7. package/dist/bind.js +18 -27
  8. package/dist/channel.d.ts +11 -33
  9. package/dist/channels/agentcore-limits.d.ts +2 -13
  10. package/dist/channels/agentcore-limits.js +2 -13
  11. package/dist/channels/agentcore-protocol.d.ts +67 -58
  12. package/dist/channels/agentcore-protocol.js +23 -15
  13. package/dist/channels/agentcore-service.d.ts +19 -33
  14. package/dist/channels/agentcore-service.js +176 -59
  15. package/dist/channels/agentcore.d.ts +39 -33
  16. package/dist/channels/agentcore.js +90 -224
  17. package/dist/channels/body.d.ts +15 -2
  18. package/dist/channels/body.js +24 -2
  19. package/dist/channels/busy.d.ts +1 -21
  20. package/dist/channels/busy.js +2 -39
  21. package/dist/channels/control.d.ts +17 -62
  22. package/dist/channels/control.js +74 -182
  23. package/dist/channels/define-channel.d.ts +39 -0
  24. package/dist/channels/define-channel.js +9 -0
  25. package/dist/channels/discover.d.ts +10 -31
  26. package/dist/channels/discover.js +37 -31
  27. package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
  28. package/dist/channels/feishu/bootstrap-token.js +7 -23
  29. package/dist/channels/feishu/card.d.ts +14 -28
  30. package/dist/channels/feishu/card.js +16 -30
  31. package/dist/channels/feishu/cloud.d.ts +1 -5
  32. package/dist/channels/feishu/context-buffer.d.ts +4 -23
  33. package/dist/channels/feishu/context-buffer.js +8 -31
  34. package/dist/channels/feishu/crypto.d.ts +10 -5
  35. package/dist/channels/feishu/crypto.js +12 -19
  36. package/dist/channels/feishu/feishu-api.d.ts +2 -1
  37. package/dist/channels/feishu/feishu-api.js +5 -4
  38. package/dist/channels/feishu/feishu.d.ts +4 -10
  39. package/dist/channels/feishu/feishu.js +113 -227
  40. package/dist/channels/feishu/invoke-turn.d.ts +5 -4
  41. package/dist/channels/feishu/invoke-turn.js +36 -54
  42. package/dist/channels/feishu/model.d.ts +4 -21
  43. package/dist/channels/feishu/model.js +1 -8
  44. package/dist/channels/feishu/normalize.d.ts +2 -10
  45. package/dist/channels/feishu/normalize.js +7 -42
  46. package/dist/channels/feishu/parse.d.ts +9 -29
  47. package/dist/channels/feishu/parse.js +8 -26
  48. package/dist/channels/feishu/preview.d.ts +12 -14
  49. package/dist/channels/feishu/preview.js +108 -207
  50. package/dist/channels/feishu/register-app.d.ts +8 -23
  51. package/dist/channels/feishu/register-app.js +7 -20
  52. package/dist/channels/feishu/register-webhook.d.ts +2 -9
  53. package/dist/channels/feishu/register-webhook.js +9 -33
  54. package/dist/channels/feishu/scaffold/channel.ts +27 -19
  55. package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
  56. package/dist/channels/feishu/setup-mode.d.ts +11 -36
  57. package/dist/channels/feishu/setup-mode.js +7 -26
  58. package/dist/channels/feishu/ws-ingress.d.ts +1 -2
  59. package/dist/channels/feishu/ws-ingress.js +6 -8
  60. package/dist/channels/http.d.ts +15 -15
  61. package/dist/channels/http.js +19 -9
  62. package/dist/channels/kit/attachment-path.d.ts +4 -5
  63. package/dist/channels/kit/attachment-path.js +10 -34
  64. package/dist/channels/kit/context-buffer.d.ts +14 -21
  65. package/dist/channels/kit/context-buffer.js +25 -38
  66. package/dist/channels/kit/delivery.d.ts +35 -0
  67. package/dist/channels/kit/delivery.js +129 -0
  68. package/dist/channels/kit/event-stream.d.ts +5 -0
  69. package/dist/channels/kit/event-stream.js +29 -0
  70. package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
  71. package/dist/channels/kit/invoke-turn-kit.js +89 -32
  72. package/dist/channels/kit/preview-kit.d.ts +18 -72
  73. package/dist/channels/kit/preview-kit.js +22 -88
  74. package/dist/channels/kit/signature.d.ts +3 -9
  75. package/dist/channels/kit/signature.js +3 -9
  76. package/dist/channels/kit/state.d.ts +1 -2
  77. package/dist/channels/kit/state.js +5 -14
  78. package/dist/channels/kit/stop-command.d.ts +1 -2
  79. package/dist/channels/kit/stop-command.js +6 -9
  80. package/dist/channels/kit/tasks.d.ts +1 -1
  81. package/dist/channels/kit/tasks.js +8 -21
  82. package/dist/channels/kit/text.d.ts +2 -16
  83. package/dist/channels/kit/text.js +23 -31
  84. package/dist/channels/kit/thread-participants.d.ts +4 -17
  85. package/dist/channels/kit/thread-participants.js +8 -55
  86. package/dist/channels/kit/transport.d.ts +26 -0
  87. package/dist/channels/kit/transport.js +20 -0
  88. package/dist/channels/kit/turn-queue.d.ts +7 -11
  89. package/dist/channels/kit/turn-queue.js +21 -39
  90. package/dist/channels/kit/turn-runner.d.ts +32 -18
  91. package/dist/channels/kit/turn-runner.js +98 -53
  92. package/dist/channels/kit/turn-store.d.ts +32 -32
  93. package/dist/channels/kit/turn-store.js +45 -88
  94. package/dist/channels/lark/lark.d.ts +1 -5
  95. package/dist/channels/lark/onboard.d.ts +3 -12
  96. package/dist/channels/lark/onboard.js +3 -13
  97. package/dist/channels/lark/scaffold/channel.ts +27 -19
  98. package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
  99. package/dist/channels/registration.d.ts +9 -35
  100. package/dist/channels/registration.js +6 -37
  101. package/dist/channels/secret.js +2 -5
  102. package/dist/channels/serve.d.ts +47 -44
  103. package/dist/channels/serve.js +165 -101
  104. package/dist/channels/slack/config-api.d.ts +4 -27
  105. package/dist/channels/slack/config-api.js +5 -34
  106. package/dist/channels/slack/context-buffer.d.ts +2 -3
  107. package/dist/channels/slack/context-buffer.js +5 -5
  108. package/dist/channels/slack/invoke-turn.d.ts +3 -1
  109. package/dist/channels/slack/invoke-turn.js +25 -34
  110. package/dist/channels/slack/manifest.js +1 -3
  111. package/dist/channels/slack/model.d.ts +1 -2
  112. package/dist/channels/slack/model.js +1 -1
  113. package/dist/channels/slack/onboard.d.ts +4 -3
  114. package/dist/channels/slack/onboard.js +8 -11
  115. package/dist/channels/slack/onboarding-state.d.ts +3 -5
  116. package/dist/channels/slack/onboarding-state.js +9 -6
  117. package/dist/channels/slack/parse.d.ts +4 -13
  118. package/dist/channels/slack/parse.js +7 -28
  119. package/dist/channels/slack/preview.d.ts +12 -2
  120. package/dist/channels/slack/preview.js +292 -269
  121. package/dist/channels/slack/reaction.d.ts +11 -3
  122. package/dist/channels/slack/reaction.js +19 -8
  123. package/dist/channels/slack/register-webhook.js +4 -7
  124. package/dist/channels/slack/scaffold/channel.ts +20 -13
  125. package/dist/channels/slack/scaffold/slack-send.ts +2 -2
  126. package/dist/channels/slack/setup-server.d.ts +1 -5
  127. package/dist/channels/slack/setup-server.js +1 -5
  128. package/dist/channels/slack/shared-api.d.ts +1 -4
  129. package/dist/channels/slack/shared-api.js +2 -13
  130. package/dist/channels/slack/slack-api.d.ts +2 -1
  131. package/dist/channels/slack/slack-api.js +6 -4
  132. package/dist/channels/slack/slack.d.ts +11 -14
  133. package/dist/channels/slack/slack.js +121 -122
  134. package/dist/channels/sse.d.ts +1 -1
  135. package/dist/channels/sse.js +1 -1
  136. package/dist/channels/telegram/context-buffer.d.ts +7 -22
  137. package/dist/channels/telegram/context-buffer.js +6 -23
  138. package/dist/channels/telegram/invoke-turn.d.ts +7 -7
  139. package/dist/channels/telegram/invoke-turn.js +24 -43
  140. package/dist/channels/telegram/parse.d.ts +24 -51
  141. package/dist/channels/telegram/parse.js +32 -84
  142. package/dist/channels/telegram/preview.d.ts +10 -1
  143. package/dist/channels/telegram/preview.js +79 -100
  144. package/dist/channels/telegram/register-webhook.js +5 -23
  145. package/dist/channels/telegram/scaffold/channel.ts +25 -18
  146. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
  147. package/dist/channels/telegram/telegram-api.d.ts +4 -3
  148. package/dist/channels/telegram/telegram-api.js +9 -7
  149. package/dist/channels/telegram/telegram.d.ts +4 -18
  150. package/dist/channels/telegram/telegram.js +38 -94
  151. package/dist/channels/telegram/turn-store.d.ts +11 -14
  152. package/dist/channels/telegram/turn-store.js +8 -6
  153. package/dist/channels/wait-health.d.ts +4 -6
  154. package/dist/channels/wait-health.js +10 -14
  155. package/dist/cli/add-feishu.d.ts +2 -12
  156. package/dist/cli/add-feishu.js +24 -71
  157. package/dist/cli/add-slack.d.ts +5 -3
  158. package/dist/cli/add-slack.js +53 -76
  159. package/dist/cli/auth-view.d.ts +2 -16
  160. package/dist/cli/auth-view.js +2 -16
  161. package/dist/cli/commands/add.js +25 -40
  162. package/dist/cli/commands/chat.d.ts +0 -1
  163. package/dist/cli/commands/chat.js +5 -6
  164. package/dist/cli/commands/deploy/agentcore.js +60 -65
  165. package/dist/cli/commands/deploy/docker.js +50 -23
  166. package/dist/cli/commands/deploy/fly.js +23 -61
  167. package/dist/cli/commands/deploy/railway.js +23 -18
  168. package/dist/cli/commands/deploy/shared.d.ts +38 -57
  169. package/dist/cli/commands/deploy/shared.js +40 -59
  170. package/dist/cli/commands/deploy.d.ts +10 -27
  171. package/dist/cli/commands/deploy.js +38 -60
  172. package/dist/cli/commands/destroy.d.ts +4 -0
  173. package/dist/cli/commands/destroy.js +56 -0
  174. package/dist/cli/commands/dev.d.ts +2 -1
  175. package/dist/cli/commands/dev.js +20 -23
  176. package/dist/cli/commands/info.d.ts +0 -2
  177. package/dist/cli/commands/info.js +83 -49
  178. package/dist/cli/commands/init.d.ts +1 -2
  179. package/dist/cli/commands/init.js +16 -55
  180. package/dist/cli/commands/invoke.d.ts +0 -1
  181. package/dist/cli/commands/invoke.js +5 -7
  182. package/dist/cli/commands/login.d.ts +2 -1
  183. package/dist/cli/commands/login.js +33 -38
  184. package/dist/cli/commands/logs.js +3 -4
  185. package/dist/cli/commands/models.js +1 -5
  186. package/dist/cli/commands/routine-run.d.ts +6 -0
  187. package/dist/cli/commands/routine-run.js +47 -0
  188. package/dist/cli/commands/routine.d.ts +25 -0
  189. package/dist/cli/commands/routine.js +122 -0
  190. package/dist/cli/commands/start.d.ts +31 -3
  191. package/dist/cli/commands/start.js +131 -81
  192. package/dist/cli/commands/tool.js +26 -12
  193. package/dist/cli/fail.d.ts +13 -23
  194. package/dist/cli/fail.js +20 -23
  195. package/dist/cli/invoke-stream.d.ts +1 -8
  196. package/dist/cli/kernel.d.ts +6 -38
  197. package/dist/cli/kernel.js +14 -77
  198. package/dist/cli/models-view.d.ts +1 -9
  199. package/dist/cli/models-view.js +11 -22
  200. package/dist/cli/program.d.ts +5 -6
  201. package/dist/cli/program.js +120 -157
  202. package/dist/cli/serve.d.ts +57 -41
  203. package/dist/cli/serve.js +94 -121
  204. package/dist/cli/shared.d.ts +15 -61
  205. package/dist/cli/shared.js +55 -181
  206. package/dist/cli.js +1 -8
  207. package/dist/collect.d.ts +6 -0
  208. package/dist/collect.js +6 -0
  209. package/dist/core.d.ts +4 -1
  210. package/dist/core.js +9 -7
  211. package/dist/declared-secrets.d.ts +55 -0
  212. package/dist/declared-secrets.js +88 -0
  213. package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
  214. package/dist/deploy/agentcore/aws-cli.js +64 -0
  215. package/dist/deploy/agentcore/destroy.d.ts +51 -0
  216. package/dist/deploy/agentcore/destroy.js +273 -0
  217. package/dist/deploy/agentcore/forwarder.js +28 -79
  218. package/dist/deploy/agentcore/logs.d.ts +2 -18
  219. package/dist/deploy/agentcore/logs.js +27 -51
  220. package/dist/deploy/agentcore/plan.d.ts +85 -105
  221. package/dist/deploy/agentcore/plan.js +152 -314
  222. package/dist/deploy/agentcore/run.d.ts +28 -28
  223. package/dist/deploy/agentcore/run.js +227 -306
  224. package/dist/deploy/agentcore/zip.d.ts +2 -15
  225. package/dist/deploy/agentcore/zip.js +3 -17
  226. package/dist/deploy/build-context.d.ts +21 -0
  227. package/dist/deploy/build-context.js +137 -0
  228. package/dist/deploy/channel-ingress.d.ts +26 -39
  229. package/dist/deploy/channel-ingress.js +57 -28
  230. package/dist/deploy/container.d.ts +20 -34
  231. package/dist/deploy/container.js +49 -100
  232. package/dist/deploy/docker/plan.d.ts +12 -11
  233. package/dist/deploy/docker/plan.js +43 -24
  234. package/dist/deploy/docker/run.d.ts +22 -25
  235. package/dist/deploy/docker/run.js +39 -37
  236. package/dist/deploy/fly/plan.d.ts +15 -44
  237. package/dist/deploy/fly/plan.js +41 -73
  238. package/dist/deploy/fly/run.d.ts +16 -51
  239. package/dist/deploy/fly/run.js +42 -95
  240. package/dist/deploy/hosts.d.ts +24 -3
  241. package/dist/deploy/hosts.js +24 -3
  242. package/dist/deploy/preflight.d.ts +47 -41
  243. package/dist/deploy/preflight.js +282 -383
  244. package/dist/deploy/railway/plan.d.ts +16 -42
  245. package/dist/deploy/railway/plan.js +42 -60
  246. package/dist/deploy/railway/run.d.ts +30 -68
  247. package/dist/deploy/railway/run.js +86 -100
  248. package/dist/deploy/registration-gate.d.ts +6 -13
  249. package/dist/deploy/residency.d.ts +37 -0
  250. package/dist/deploy/residency.js +24 -0
  251. package/dist/deploy/runner.d.ts +10 -9
  252. package/dist/deploy/runner.js +7 -18
  253. package/dist/deploy/secrets.d.ts +40 -41
  254. package/dist/deploy/secrets.js +117 -103
  255. package/dist/deploy/workspace.d.ts +50 -0
  256. package/dist/deploy/workspace.js +197 -0
  257. package/dist/dev-supervisor.d.ts +1 -9
  258. package/dist/dev-supervisor.js +21 -64
  259. package/dist/effect-port.d.ts +50 -0
  260. package/dist/effect-port.js +77 -0
  261. package/dist/engines/pi/agent-session-factory.d.ts +50 -69
  262. package/dist/engines/pi/agent-session-factory.js +249 -146
  263. package/dist/engines/pi/auth.d.ts +15 -11
  264. package/dist/engines/pi/auth.js +96 -99
  265. package/dist/engines/pi/chat.d.ts +1 -5
  266. package/dist/engines/pi/chat.js +2 -14
  267. package/dist/engines/pi/config.d.ts +53 -51
  268. package/dist/engines/pi/config.js +83 -92
  269. package/dist/engines/pi/create.d.ts +60 -75
  270. package/dist/engines/pi/create.js +108 -141
  271. package/dist/engines/pi/definition.d.ts +14 -58
  272. package/dist/engines/pi/definition.js +21 -85
  273. package/dist/engines/pi/invoke-session.d.ts +5 -25
  274. package/dist/engines/pi/invoke-session.js +233 -244
  275. package/dist/engines/pi/login.d.ts +95 -35
  276. package/dist/engines/pi/login.js +259 -107
  277. package/dist/engines/pi/machine.d.ts +31 -0
  278. package/dist/engines/pi/machine.js +106 -0
  279. package/dist/engines/pi/models.d.ts +70 -89
  280. package/dist/engines/pi/models.js +188 -81
  281. package/dist/engines/pi/open.d.ts +65 -53
  282. package/dist/engines/pi/open.js +129 -69
  283. package/dist/engines/pi/report.d.ts +3 -12
  284. package/dist/engines/pi/report.js +2 -16
  285. package/dist/engines/pi/search-tools.d.ts +1 -9
  286. package/dist/engines/pi/search-tools.js +26 -57
  287. package/dist/engines/pi/service.d.ts +2 -16
  288. package/dist/engines/pi/service.js +2 -16
  289. package/dist/engines/pi/session-builder.d.ts +5 -9
  290. package/dist/engines/pi/session-builder.js +44 -82
  291. package/dist/engines/pi/session-control.js +286 -269
  292. package/dist/engines/pi/session-effects.d.ts +14 -0
  293. package/dist/engines/pi/session-effects.js +30 -0
  294. package/dist/engines/pi/session-inheritance.d.ts +9 -18
  295. package/dist/engines/pi/session-inheritance.js +103 -160
  296. package/dist/engines/pi/session-markers.d.ts +31 -30
  297. package/dist/engines/pi/session-markers.js +26 -25
  298. package/dist/engines/pi/session-settings.d.ts +6 -22
  299. package/dist/engines/pi/session-settings.js +10 -21
  300. package/dist/engines/pi/session-store.d.ts +26 -92
  301. package/dist/engines/pi/session-store.js +96 -310
  302. package/dist/engines/pi/tool-context.d.ts +17 -45
  303. package/dist/engines/pi/tool-context.js +7 -26
  304. package/dist/engines/pi/tool.d.ts +45 -36
  305. package/dist/engines/pi/tool.js +43 -57
  306. package/dist/engines/pi/turn-kit.d.ts +14 -52
  307. package/dist/engines/pi/turn-kit.js +18 -67
  308. package/dist/engines/pi/wake-tool.d.ts +2 -10
  309. package/dist/engines/pi/wake-tool.js +7 -27
  310. package/dist/env.d.ts +27 -28
  311. package/dist/env.js +45 -50
  312. package/dist/index.js +0 -2
  313. package/dist/lark.d.ts +1 -2
  314. package/dist/lark.js +1 -2
  315. package/dist/loader.d.ts +14 -55
  316. package/dist/loader.js +24 -43
  317. package/dist/log.d.ts +2 -12
  318. package/dist/log.js +3 -24
  319. package/dist/node.d.ts +2 -15
  320. package/dist/node.js +2 -18
  321. package/dist/observe.d.ts +2 -8
  322. package/dist/open-url.d.ts +1 -1
  323. package/dist/open-url.js +1 -1
  324. package/dist/paths.d.ts +70 -134
  325. package/dist/paths.js +165 -232
  326. package/dist/pi.d.ts +4 -6
  327. package/dist/pi.js +3 -3
  328. package/dist/proxy.d.ts +16 -6
  329. package/dist/proxy.js +52 -16
  330. package/dist/runtime.d.ts +18 -12
  331. package/dist/runtime.js +18 -11
  332. package/dist/scaffold/add-channel.d.ts +14 -31
  333. package/dist/scaffold/add-channel.js +48 -112
  334. package/dist/scaffold/init.d.ts +9 -31
  335. package/dist/scaffold/init.js +45 -138
  336. package/dist/scaffold/templates/env.example +6 -4
  337. package/dist/scaffold/templates/fastagent.config.ts +36 -0
  338. package/dist/scaffold/templates.d.ts +3 -3
  339. package/dist/scaffold/templates.js +5 -8
  340. package/dist/scaffold/vendor-skill.d.ts +2 -5
  341. package/dist/scaffold/vendor-skill.js +19 -29
  342. package/dist/schedule/cron.d.ts +9 -5
  343. package/dist/schedule/cron.js +9 -11
  344. package/dist/schedule/discover.d.ts +12 -12
  345. package/dist/schedule/discover.js +43 -34
  346. package/dist/schedule/routine.d.ts +43 -0
  347. package/dist/schedule/routine.js +32 -0
  348. package/dist/schedule/run.d.ts +55 -0
  349. package/dist/schedule/run.js +146 -0
  350. package/dist/schedule/scheduler.d.ts +44 -55
  351. package/dist/schedule/scheduler.js +291 -265
  352. package/dist/schedule/state.d.ts +105 -10
  353. package/dist/schedule/state.js +293 -19
  354. package/dist/schedule/wake-alarm.d.ts +10 -23
  355. package/dist/schedule/wake-alarm.js +91 -121
  356. package/dist/schedule/wakeups.d.ts +23 -39
  357. package/dist/schedule/wakeups.js +33 -75
  358. package/dist/secrets-gate.d.ts +32 -0
  359. package/dist/secrets-gate.js +33 -0
  360. package/dist/service.d.ts +137 -98
  361. package/dist/service.js +250 -252
  362. package/dist/session-remote.d.ts +33 -44
  363. package/dist/session-remote.js +203 -192
  364. package/dist/session.d.ts +166 -229
  365. package/dist/session.js +33 -55
  366. package/dist/tunnel.d.ts +8 -36
  367. package/dist/tunnel.js +24 -65
  368. package/dist/version.d.ts +2 -2
  369. package/dist/version.js +2 -2
  370. package/package.json +10 -16
  371. package/dist/channels/agentcore-state.d.ts +0 -77
  372. package/dist/channels/agentcore-state.js +0 -276
  373. package/dist/channels/github/github.d.ts +0 -31
  374. package/dist/channels/github/github.js +0 -89
  375. package/dist/channels/github/scaffold/channel.ts +0 -25
  376. package/dist/cli/commands/attach.d.ts +0 -108
  377. package/dist/cli/commands/attach.js +0 -637
  378. package/dist/cli/commands/fire.d.ts +0 -7
  379. package/dist/cli/commands/fire.js +0 -38
  380. package/dist/cli/commands/schedule.d.ts +0 -12
  381. package/dist/cli/commands/schedule.js +0 -87
  382. package/dist/github.d.ts +0 -3
  383. package/dist/github.js +0 -2
  384. package/dist/scaffold/templates/fastagent.config.mjs +0 -23
  385. package/dist/schedule/audit.d.ts +0 -18
  386. package/dist/schedule/audit.js +0 -57
  387. package/dist/schedule/schedule.d.ts +0 -35
  388. package/dist/schedule/schedule.js +0 -23
@@ -1,107 +1,58 @@
1
- /**
2
- * `fastagent deploy agentcore` — the AWS Bedrock AgentCore deploy PLAN, computed from the resolved
3
- * definition. Pure: facts in, artifact contents + an ordered runbook out; the CLI writes the files
4
- * and prints the runbook. AgentCore is the fourth target, and its shape differs from Fly/Railway in
5
- * kind, not degree:
6
- *
7
- * 1. **No public URL, no resident process.** The Runtime's only ingress is the SigV4
8
- * `InvokeAgentRuntime` API, and compute is per-session microVMs that stop when idle. So the
9
- * topology carries TWO extra pieces a Fly box never needs: a forwarder Lambda (public Function
10
- * URL → envelope → InvokeAgentRuntime) fronting the webhooks, and EventBridge Scheduler rules
11
- * delivering each cron slot (the container arms no resident timers — serve's externalClock mode).
12
- * 2. **One template is the whole topology.** CloudFormation (`AWS::BedrockAgentCore::Runtime` is a
13
- * first-class resource type) declares Runtime + roles + forwarder + schedules in one stack —
14
- * unlike Railway, identity DOES live in a committed file; the stack name pins it.
15
- * 3. **All ingress traffic shares ONE fixed runtime session** (`ingressSessionId`): fastagent's
16
- * channel state is single-writer by design, and one session = at most one microVM at a time.
17
- * AgentCore keeps a stopped session's id valid until the Runtime is deleted (a new compute is
18
- * provisioned on the next invoke), so the fixed id needs no rotation. State lives on the
19
- * platform's SessionStorage mount (`/mnt/state`) — persistent across compute stop/resume, no
20
- * VPC/EFS required. Named trade-off: that state is tied to THIS Runtime resource — a stack
21
- * replacement (renaming the runtime) starts blank. EFS (VPC mode) is the upgrade path when
22
- * state must outlive the runtime; the runbook says so instead of silently shipping a VPC+NAT
23
- * bill (~$35/mo) every deployment.
24
- *
25
- * The image is the SAME portable container every host ships (containerArtifacts) — AgentCore's
26
- * extras (PORT=8080, FASTAGENT_AGENTCORE=1, the state dir) ride the Runtime resource's environment,
27
- * never a forked Dockerfile. The build must be linux/arm64 (the platform requirement) — the ONE
28
- * host where the build runs on the operator's machine (docker buildx) instead of remotely.
29
- */
1
+ /** `fastagent deploy agentcore` — the AWS Bedrock AgentCore deploy PLAN, computed from the resolved definition. */
30
2
  import { createHash } from "node:crypto";
31
3
  import { readFileSync } from "node:fs";
32
4
  import { MAX_WEBHOOK_BODY_BYTES } from "../../channels/agentcore-limits.js";
33
5
  import { SECRETS_DIRNAME } from "../../paths.js";
34
6
  import { webhookKinds, webhookRunbook } from "../channel-ingress.js";
35
7
  import { containerArtifacts } from "../container.js";
36
- import { deploymentSecrets, isEnvKey } from "../secrets.js";
37
- function agentcoreTopology(input,
38
- /** Schedules EventBridge CAN express — the ones that become rules. */
39
- translatedSchedules) {
40
- const webhooks = input.channels.some((channel) => channel.ingress === "webhook");
41
- return {
42
- webhooks,
43
- forwarder: webhooks || translatedSchedules > 0 || input.selfSchedule,
44
- wakeAlarms: input.selfSchedule,
45
- };
8
+ import { isEnvKey } from "../secrets.js";
9
+ function agentcoreTopology(input) {
10
+ return { webhooks: input.channels.some((channel) => channel.ingress === "webhook") };
46
11
  }
47
- /** SessionStorage mount = FASTAGENT_STATE_DIR (AgentCore requires exactly `/mnt/<one-level>`). It is
48
- * a fast LOCAL disk only: the platform wipes it on every runtime version update (= every deploy).
49
- * Durability across deploys comes from the S3 snapshot (channels/agentcore-state.ts). */
50
- export const MOUNT = "/mnt/state";
51
- /**
52
- * FASTAGENT_SECRETS_DIR — the seeded-then-ROTATED auth.json, deliberately INSIDE the state root
53
- * rather than beside it.
54
- *
55
- * Every other host mounts a real volume and puts the two machinery dirs side by side (`/data/.state`
56
- * + `/data/.secrets`), because there the persistence boundary is the MOUNT POINT: anything under it
57
- * survives. AgentCore has no volume. Its persistence boundary is `packStateRoot(stateRoot)` — the one
58
- * directory tree the S3 snapshot copies out and back (channels/agentcore-state.ts) — while {@link MOUNT}
59
- * itself is wiped on every runtime version update, i.e. on every deploy.
60
- *
61
- * So the sibling layout would put credentials INSIDE the mount but OUTSIDE the snapshot: nothing
62
- * copies them out, the platform wipes them, and the next microVM re-seeds the deploy-time copy. With
63
- * single-use OAuth refresh tokens that is a slow-motion outage — the box works until the seeded token
64
- * is rotated away, then loses model access with only a redeploy to restore it.
65
- *
66
- * Nesting is what makes agentcore-state.ts's stated contract ("restores VERBATIM — including
67
- * auth.json") reachable at all; `packStateRoot` walks the whole tree, so no snapshot code knows about
68
- * this. Tests assert the containment, not just the two names — the sibling spelling looks tidier and
69
- * reintroduces the outage silently.
70
- */
12
+ /** The platform's managed SessionStorage mount (AgentCore requires exactly `/mnt/<one-level>`). */
13
+ export const MOUNT = "/mnt/data";
14
+ /** Beside the state root on the one mount, as every volume-backed host does. */
71
15
  export const SECRETS_DIR = `${MOUNT}/${SECRETS_DIRNAME}`;
72
16
  /**
73
- * How long an idle session keeps its microVM. Memory is billed per second across the WHOLE session
74
- * — idle included, at the peak level reached — so this tail is the standing cost of every burst of
75
- * activity, while CPU stops billing the moment the agent stops working. 3 minutes rather than the
76
- * platform's 15: the tail shrinks 5×, and the cost is a cold start (image + Node + snapshot restore)
77
- * for anyone who returns after a longer gap. `/ping` reports HealthyBusy + time_of_last_update while
78
- * work is in flight (the FIELD is what the platform's idle measurement actually reads — agentcore.ts),
79
- * so this timer only ever starts once the agent has genuinely settled — a long turn is never cut short.
80
- * AWS accepts 60–28800.
17
+ * How long an idle session keeps its microVM when the config names no other value. Below AWS's own default (900) on
18
+ * purpose: memory bills for the whole idle tail, so the cheap posture is the default and a longer one is a choice.
81
19
  */
82
- export const IDLE_TIMEOUT_SECONDS = 180;
83
- /** The platform ceiling on one session's compute (8 h). The session ID outlives it: the next invoke
84
- * simply gets fresh compute with the same storage. */
20
+ export const DEFAULT_IDLE_TIMEOUT_SECONDS = 180;
21
+ /** One session's compute lifetime — AWS's own default. Both lifecycle fields cap at 1209600 (14 d). */
85
22
  export const MAX_LIFETIME_SECONDS = 28800;
86
- /** The state snapshot's object key in the deployment bucket (one object; see agentcore-state.ts). */
87
- export const STATE_KEY = "state/snapshot.json.gz";
88
- /** The forwarder artifact. Named `index.js` because it IS the Lambda deployment package's entry:
89
- * zipping it as-is produces a valid package (`Handler: index.handler`), with nothing to rename. */
90
23
  export const FORWARDER_FILE = "lambda/index.js";
91
- /** The deployment bucket: forwarder code + the state snapshot. Account-suffixed for S3's GLOBAL
92
- * namespace, and created OUTSIDE the stack (like the ECR repo) so a `delete-stack` cannot take the
93
- * agent's memory with it. Bucket names cap at 63 chars; `name` is already gated to 40. */
94
- export function stateBucketName(name, account) {
24
+ /**
25
+ * Where the forwarder puts the one-shot schedule for a wake-up. The id inside the name is minted in the
26
+ * container, so this prefix is the only handle anything outside has on the alarms a deployment has pending.
27
+ */
28
+ export function wakeAlarmPrefix(name) {
29
+ return `fa-${name}-wk-`;
30
+ }
31
+ /** Account-suffixed artifact bucket for S3's global namespace. */
32
+ export function deploymentBucketName(name, account) {
95
33
  return `fa-${name}-${account}`;
96
34
  }
97
- /** AgentCore env values max 2048 chars — a real OAuth auth.json's base64 exceeds it, so the seed is
98
- * CHUNKED across FASTAGENT_AUTH_SEED + _2… (collectAuthSeed reassembles at boot). 2000 keeps margin. */
99
- export const AUTH_SEED_CHUNK_SIZE = 2000;
100
- export const AUTH_SEED_MAX_CHUNKS = 4;
35
+ /**
36
+ * AgentCore env values max 2048 chars — a real OAuth auth.json's base64 exceeds it, so each carrier is CHUNKED across
37
+ * `<NAME>` + `_2`… (collectChunked reassembles at boot): FASTAGENT_AUTH_SEED for the credential, FASTAGENT_ENV for the
38
+ * value file's variables.
39
+ */
40
+ export const CARRIER_CHUNK_SIZE = 2000;
41
+ export const CARRIER_MAX_CHUNKS = 4;
42
+ /** The template's two chunked carriers: CloudFormation parameter prefix → container env-var name. */
43
+ export const CARRIERS = [
44
+ { param: "FastagentAuthSeed", env: "FASTAGENT_AUTH_SEED", what: "base64 auth.json" },
45
+ { param: "FastagentEnv", env: "FASTAGENT_ENV", what: "base64 JSON of the value file's variables" },
46
+ ];
47
+ /** Chunk `i` (0-based) of a carrier: its parameter and env-var names. */
48
+ export function carrierChunk(carrier, i) {
49
+ return i === 0
50
+ ? { param: carrier.param, env: carrier.env }
51
+ : { param: `${carrier.param}${i + 1}`, env: `${carrier.env}_${i + 1}` };
52
+ }
101
53
  /** The generated template's filename (namespaced under the kit in the agentDir layout). */
102
54
  export const TEMPLATE_FILE = "agentcore.template.yaml";
103
- /** The generated template's first-line marker — the ONE source for both the generator and the
104
- * "did fastagent generate this?" check (deploy's drift gate), so they cannot drift apart. */
55
+ /** The generated template's first-line marker. */
105
56
  export const GENERATED_TEMPLATE_MARKER = "# Generated by `fastagent deploy agentcore`";
106
57
  /** Whether an on-disk template is fastagent-generated (vs hand-written — kept, never gated). */
107
58
  export function isGeneratedAgentcoreTemplate(content) {
@@ -119,28 +70,48 @@ export function toRuntimeName(basename) {
119
70
  const slug = basename.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
120
71
  return (/^[a-zA-Z]/.test(slug) ? slug : `agent_${slug || "fastagent"}`).slice(0, 48);
121
72
  }
122
- /** The ONE fixed ingress session id (webhooks + schedule fires) — ≥ 33 chars (the API minimum),
123
- * deterministic (the Lambda holds it in env), padded so any name clears the floor. */
124
- export function ingressSessionId(name) {
125
- return `fastagent-ingress-${name}`.padEnd(33, "0").slice(0, 128);
73
+ /** The forwarder Lambda's name — and therefore its log group, which AWS derives from it. */
74
+ function forwarderFunctionName(name) {
75
+ return `${agentcoreStackName(name)}-forwarder`;
126
76
  }
127
- /** CFN parameter logical id for a secret env-var name: TELEGRAM_BOT_TOKEN → TelegramBotToken
128
- * (parameter names must be alphanumeric). Deterministic — run.ts builds the same mapping. */
129
- export function cfnParamName(envName) {
130
- return envName
131
- .toLowerCase()
132
- .split("_")
133
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
134
- .join("");
77
+ /**
78
+ * The two names every part of this host derives from the deployment name, and the reason they are functions:
79
+ * a deploy CREATES them and a destroy LOOKS FOR them, so a drift between the two spellings is silent in the
80
+ * worst direction. `describe-repositories` on a mis-spelled repo answers `RepositoryNotFoundException`, which
81
+ * is "already gone" — the teardown would report nothing left to delete while the repository kept billing.
82
+ */
83
+ export function agentcoreStackName(name) {
84
+ return `fastagent-${name}`;
85
+ }
86
+ export function agentcoreRepoName(name) {
87
+ return `fastagent/${name}`;
88
+ }
89
+ /**
90
+ * Where a RUNTIME's logs land, by the identifier the caller happens to hold: `fastagent logs` knows the runtime id
91
+ * from the stack's `RuntimeArn`, `destroy` knows only the {@link toRuntimeName} the template deployed — and AgentCore
92
+ * names the id `<AgentRuntimeName>-<suffix>`, so the same prefix serves both. MEASURED (ap-southeast-1, 2026-09-22):
93
+ * a deploy of `destroy-probe-yehg` produced `/aws/bedrock-agentcore/runtimes/destroy_probe_yehg-6oR1zq7pBZ-DEFAULT`.
94
+ *
95
+ * ONE OWNER because of what the callers do with it: `logs` picks a group to tail, `destroy` DELETES everything the
96
+ * prefix matches. The trailing `-` is what keeps `probe-` off `probe2-…`, and a second hand-spelled copy of this
97
+ * string is a copy that can lose it.
98
+ */
99
+ export function runtimeLogGroupPrefix(runtimeOrName) {
100
+ return `/aws/bedrock-agentcore/runtimes/${runtimeOrName}-`;
135
101
  }
136
102
  /**
137
- * Remap ONE day-of-week field from standard cron numbering (0–7, 0/7 = Sunday) to EventBridge's
138
- * (1–7, 1 = Sunday). Parsed, not regex-replaced: only VALUES and RANGE ENDPOINTS are renumbered — a
139
- * step divisor (`*\/2`, `1-5/2`) is a count, not a weekday, and must pass through untouched. Names
140
- * (SUN..SAT) pass through. A numeric range whose endpoints INVERT under renumbering (`5-7` → `6-1`)
141
- * wraps across the week — not expressible as an EventBridge range — and is refused, never silently
142
- * reordered.
103
+ * Where the forwarder's logs land. Lambda creates this group itself, from the function name, so three parties have
104
+ * to agree on a string none of them can check against the others: the template that names the function, the runbook
105
+ * and `--run` summary that tell an operator to set its retention, and `fastagent logs --source forwarder`.
143
106
  */
107
+ export function forwarderLogGroup(name) {
108
+ return `/aws/lambda/${forwarderFunctionName(name)}`;
109
+ }
110
+ /** The ONE fixed ingress session id (webhooks + routine fires). */
111
+ export function ingressSessionId(name) {
112
+ return `fastagent-ingress-${name}`.padEnd(33, "0").slice(0, 128);
113
+ }
114
+ /** Remap ONE day-of-week field from standard cron numbering (0–7, 0/7 = Sunday) to EventBridge's (1–7, 1 = Sunday). */
144
115
  function mapDowField(dow) {
145
116
  const items = [];
146
117
  for (const item of dow.split(",")) {
@@ -175,16 +146,7 @@ function mapDowField(dow) {
175
146
  }
176
147
  return { value: items.join(",") };
177
148
  }
178
- /**
179
- * Translate a 5-field cron into EventBridge Scheduler's `cron(m h dom mon dow *)`, or say why it
180
- * can't be. The two dialects disagree exactly where silent translation would misfire:
181
- * - EventBridge numbers day-of-week 1–7 (1 = Sunday); standard cron uses 0–6 (0/7 = Sunday) —
182
- * numeric dow values and range endpoints are remapped ({@link mapDowField}); steps and names
183
- * pass through; a range that wraps under renumbering is refused.
184
- * - EventBridge requires `?` in dom or dow: a `*` on either side becomes `?`; BOTH restricted is
185
- * standard cron's OR semantics, which EventBridge cannot express — refused, never approximated.
186
- * - A 6-field (seconds) expression and L/# day-of-week forms are refused for the same reason.
187
- */
149
+ /** Translate a 5-field cron into EventBridge Scheduler's `cron(m h dom mon dow *)`, or say why it can't be. */
188
150
  export function toEventBridgeCron(cron) {
189
151
  const fields = cron.trim().split(/\s+/);
190
152
  if (fields.length !== 5) {
@@ -194,13 +156,7 @@ export function toEventBridgeCron(cron) {
194
156
  if (/[L#]/i.test(dow) || /[L#]/i.test(dom)) {
195
157
  return { error: "L/# day forms don't translate to EventBridge numbering — set this schedule up manually" };
196
158
  }
197
- // `?` FIRST, and not as a synonym for `*`. Croner treats it as a day field that matches everything
198
- // and — unlike `*` — does NOT trigger cron's "one field is unrestricted, so the other governs"
199
- // special case. It therefore ORs with the other field to mean EVERY DAY, whatever that field says.
200
- // Measured against croner: `0 9 ? * MON` and `0 9 1 * ?` both fire daily, while `0 9 * * MON`
201
- // fires on Mondays. Translating `?` to `*` would deploy a rule that fires on a DIFFERENT set of
202
- // days than the same file fires on locally — the silent divergence this whole target exists to
203
- // avoid — so a `?` in either field becomes an explicitly daily EventBridge rule.
159
+ // `?` FIRST, and not as a synonym for `*`.
204
160
  if (dom === "?" || dow === "?") {
205
161
  return { expression: `cron(${min} ${hour} * ${mon} ? *)` };
206
162
  }
@@ -222,36 +178,18 @@ function logicalId(name) {
222
178
  const slug = name.replace(/[^a-zA-Z0-9]+/g, "");
223
179
  return slug.charAt(0).toUpperCase() + slug.slice(1) || "Schedule";
224
180
  }
225
- /** YAML single-quoted scalar (the one escape: `'` doubles). Used for values carrying user input. */
181
+ /** YAML single-quoted scalar (the one escape: `'` doubles). */
226
182
  function yamlSingleQuote(value) {
227
183
  return `'${value.replace(/'/g, "''")}'`;
228
184
  }
229
185
  /**
230
- * The forwarder Lambda source — `forwarder.js` beside this file, the ONE text both the deployment
231
- * package and the readable `lambda/index.js` artifact are generated from. A real file, not a template
232
- * literal: it is linted, and it needs no interpolation — the one value it once took from here
233
- * (`MAX_WEBHOOK_BODY_BYTES`) rides in as a Lambda environment variable. Zero-dependency: the
234
- * Lambda Node runtime bundles AWS SDK v3. CommonJS ON PURPOSE: the package's entry lands as
235
- * `index.js` ({@link zipSingleFile}), where ESM `import` is a syntax error. Two
236
- * event shapes: a Function URL webhook (reconstructed verbatim into a `webhook` envelope; the
237
- * channel's REAL response rides back inside the transport reply and is re-emitted byte-exact —
238
- * Feishu's URL-verification challenge depends on it), and an EventBridge Scheduler fire
239
- * (`{ scheduleFire }`, slot = the scheduled instant — the container's idempotency key).
240
- *
241
- * It ships as an S3 object rather than inline `ZipFile`, so there is no 4096-byte ceiling on it — a
242
- * constraint this comment asserted for long after the template stopped inlining, which is the kind
243
- * of stale rule that stops the next person making a correct change.
186
+ * The forwarder Lambda source — `forwarder.js` beside this file, the ONE text both the deployment package and the
187
+ * readable `lambda/index.js` artifact are generated from.
244
188
  */
245
189
  export function forwarderSource() {
246
190
  return readFileSync(new URL("./forwarder.js", import.meta.url), "utf8");
247
191
  }
248
- /**
249
- * The EventBridge physical name for a schedule. A schedule's local name is an arbitrary MODULE FILE
250
- * NAME (`schedules/晨报.ts`, `schedules/deploy check.ts`), while AWS requires `[0-9A-Za-z-_.]+` within
251
- * 64 chars — and the `fa-<agent>-` prefix already eats up to 44 of them. So: sanitize, bound the
252
- * readable part, and end with a hash of the ORIGINAL name, which keeps distinct schedules distinct
253
- * where sanitizing or truncation would have merged them (one rule silently firing for two).
254
- */
192
+ /** The EventBridge physical name for a schedule. */
255
193
  export function scheduleResourceName(agent, schedule) {
256
194
  const prefix = `fa-${agent}-`;
257
195
  const hash = createHash("sha256").update(schedule).digest("hex").slice(0, 8);
@@ -262,61 +200,38 @@ export function scheduleResourceName(agent, schedule) {
262
200
  /** The CloudFormation template — the whole topology in one stack. */
263
201
  function template(input, translated, topology) {
264
202
  const runtimeName = toRuntimeName(input.name);
265
- const needsForwarder = topology.forwarder;
266
- const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
203
+ const idleTimeout = input.idleTimeoutSeconds ?? DEFAULT_IDLE_TIMEOUT_SECONDS;
267
204
  const forwarderFnArn = `!Sub arn:aws:lambda:\${AWS::Region}:\${AWS::AccountId}:function:fastagent-${input.name}-forwarder`;
268
- // Secret env vars ride CFN NoEcho parameters. FASTAGENT_AUTH_SEED is always declared (Default "")
269
- // so a `--run` OAuth carry has a slot; required secrets have NO default — `cloudformation deploy`
270
- // fails loudly without a value instead of booting a half-configured box.
205
+ // Secret env vars ride CFN NoEcho parameters.
271
206
  const params = [
272
207
  ` ImageUri:`,
273
208
  ` Type: String`,
274
209
  ` Description: ECR image URI (linux/arm64) — <account>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag>`,
275
210
  ];
276
- if (needsForwarder) {
277
- params.push(` StateBucket:`, ` Type: String`, ` Description: S3 bucket holding the forwarder deployment package + the agent's state snapshot (created outside this stack)`, ` ForwarderS3Key:`, ` Type: String`, ` Description: key of the forwarder .zip in StateBucket — CONTENT-HASHED, so new code is a new value and CloudFormation rolls the function`);
278
- }
211
+ params.push(` ForwarderBucket:`, ` Type: String`, ` Description: S3 bucket holding the forwarder deployment package (created outside this stack)`, ` ForwarderS3Key:`, ` Type: String`, ` Description: content-hashed key of the forwarder .zip in ForwarderBucket`);
279
212
  const envLines = [
280
213
  ` PORT: "8080"`, // the Runtime service contract's fixed port (config.http.port does not apply here)
281
214
  ` FASTAGENT_AGENTCORE: "1"`, // serve mounts /invocations + /ping, arms no resident cron
282
- ` FASTAGENT_STATE_DIR: ${MOUNT}`,
283
- // Inside the state root on purpose — the snapshot is this host's only durable store, and it copies
284
- // exactly one tree. See {@link SECRETS_DIR}: the sibling layout every other host uses would leave a
285
- // rotated OAuth credential outside it, i.e. discarded with the microVM.
215
+ ` FASTAGENT_STORAGE_DIR: ${MOUNT}`,
216
+ ` FASTAGENT_STATE_DIR: ${MOUNT}/.state`,
286
217
  ` FASTAGENT_SECRETS_DIR: ${SECRETS_DIR}`,
287
218
  ];
288
- // The auth seed is chunked (env values max 2048 chars — see AUTH_SEED_CHUNK_SIZE): N parameters,
289
- // each riding its own env var; `start` reassembles them (collectAuthSeed). Empty defaults = unused.
290
- for (let i = 1; i <= AUTH_SEED_MAX_CHUNKS; i++) {
291
- const param = i === 1 ? "FastagentAuthSeed" : `FastagentAuthSeed${i}`;
292
- const envName = i === 1 ? "FASTAGENT_AUTH_SEED" : `FASTAGENT_AUTH_SEED_${i}`;
293
- params.push(` ${param}:`, ` Type: String`, ` Default: ""`, ` NoEcho: true`, ` Description: base64 auth.json carried by --run, chunk ${i}/${AUTH_SEED_MAX_CHUNKS} (env values cap at 2048 chars); empty = unused`);
294
- envLines.push(` ${envName}: !Ref ${param}`);
295
- }
296
- for (const s of secrets) {
297
- const p = cfnParamName(s.name);
298
- params.push(` ${p}:`, ` Type: String`);
299
- if (!s.required)
300
- params.push(` Default: ""`);
301
- params.push(` NoEcho: true`, ` Description: ${s.hint}`);
302
- envLines.push(` ${s.name}: !Ref ${p}`);
303
- }
304
- if (needsForwarder) {
305
- // The INGRESS secret authenticates forwarder→runtime envelopes. Without it the envelope union is
306
- // an unauthenticated control plane: `InvokeAgentRuntime` is an ordinary IAM action, so any
307
- // principal holding it could forge a `schedule-fire`, or ride `state`/`wake` on a public `invoke`
308
- // to redirect the state snapshot (exfiltrating auth.json) or the wake-alarm callback (leaking the
309
- // wake secret) to an address of their choosing. Public `invoke` stays unauthenticated by design —
310
- // it is the programmatic data plane — but it may not carry internal fields.
311
- params.push(` FastagentIngressSecret:`, ` Type: String`, ` NoEcho: true`, ` Description: shared secret authenticating forwarder→runtime envelopes (any random string; --run mints one)`);
312
- envLines.push(` FASTAGENT_INGRESS_SECRET: !Ref FastagentIngressSecret`);
313
- }
314
- if (input.selfSchedule) {
315
- // The wake-alarm shared secret: the container authenticates its alarm callbacks to the forwarder
316
- // with it. Required (no default) — a selfSchedule deployment without it would silently degrade.
317
- params.push(` FastagentWakeSecret:`, ` Type: String`, ` NoEcho: true`, ` Description: shared secret between the container and the forwarder's wake-alarm callback (any random string; --run mints one)`);
318
- envLines.push(` FASTAGENT_WAKE_SECRET: !Ref FastagentWakeSecret`);
219
+ // Both carriers are chunked (env values max 2048 chars — see CARRIER_CHUNK_SIZE): N parameters, each riding its
220
+ // own env var; `start` reassembles them. ONE parameter set for every variable, so the template does not change
221
+ // when the value file gains a name.
222
+ for (const carrier of CARRIERS) {
223
+ for (let i = 0; i < CARRIER_MAX_CHUNKS; i++) {
224
+ const { param, env } = carrierChunk(carrier, i);
225
+ params.push(` ${param}:`, ` Type: String`, ` Default: ""`, ` NoEcho: true`, ` Description: ${carrier.what} carried by --run, chunk ${i + 1}/${CARRIER_MAX_CHUNKS} (env values cap at 2048 chars); empty = unused`);
226
+ envLines.push(` ${env}: !Ref ${param}`);
227
+ }
319
228
  }
229
+ // IAM invocation permission does not prove forwarder origin. Internal envelopes need a secret.
230
+ params.push(` FastagentIngressSecret:`, ` Type: String`, ` NoEcho: true`, ` Description: shared secret authenticating forwarder→runtime envelopes (any random string; --run mints one)`);
231
+ envLines.push(` FASTAGENT_INGRESS_SECRET: !Ref FastagentIngressSecret`);
232
+ // The wake-alarm shared secret: the container authenticates its alarm callbacks to the forwarder with it.
233
+ params.push(` FastagentWakeSecret:`, ` Type: String`, ` NoEcho: true`, ` Description: shared secret between the container and the forwarder's wake-alarm callback (any random string; --run mints one)`);
234
+ envLines.push(` FASTAGENT_WAKE_SECRET: !Ref FastagentWakeSecret`);
320
235
  const lines = [
321
236
  `${GENERATED_TEMPLATE_MARKER}. Edit freely — deploy then treats it as hand-written and never gates on drift.`,
322
237
  `AWSTemplateFormatVersion: "2010-09-09"`,
@@ -368,72 +283,43 @@ function template(input, translated, topology) {
368
283
  ` RoleArn: !GetAtt ExecutionRole.Arn`,
369
284
  ` ProtocolConfiguration: HTTP`,
370
285
  ` NetworkConfiguration: { NetworkMode: PUBLIC }`,
371
- ` # SessionStorage is the agent's LOCAL disk: it survives compute stop/resume within a runtime`,
372
- ` # version, but AWS wipes it on every VERSION UPDATE (= every deploy) and after 14 idle days.`,
373
- ` # Durability therefore comes from the S3 snapshot the container pulls on its first`,
374
- ` # invocation and pushes when work settles (presigned by the forwarder — the container holds`,
375
- ` # no AWS credentials). A persistent MOUNT instead needs EfsAccessPoint + VPC mode, which`,
376
- ` # forces a NAT gateway for model/channel egress (~$33/mo) — deliberately not the default.`,
286
+ ` # Managed SessionStorage: no VPC, no NAT, no operator-owned filesystem. It survives compute`,
287
+ ` # stop/resume, and AWS RESETS it on every runtime version update (= every deploy) and after`,
288
+ ` # 14 idle days — so a deploy replaces the state along with the image. Cross-deploy memory`,
289
+ ` # would need EFS or S3 Files, both VPC-only and therefore a standing NAT bill.`,
377
290
  ` FilesystemConfigurations:`,
378
291
  ` - SessionStorage: { MountPath: ${MOUNT} }`,
379
- ` # Idle ${IDLE_TIMEOUT_SECONDS}s (the ping's HealthyBusy + time_of_last_update keeps BUSY sessions alive), max compute`,
380
- ` # lifetime ${MAX_LIFETIME_SECONDS}s — the platform ceiling; the session id stays valid, so the next invoke`,
292
+ ` # Idle ${idleTimeout}s (the ping's HealthyBusy + time_of_last_update keeps BUSY sessions alive), max compute`,
293
+ ` # lifetime ${MAX_LIFETIME_SECONDS}s; the session id stays valid, so the next invoke`,
381
294
  ` # just gets fresh compute with the same storage. Memory bills per second for the whole`,
382
- ` # session INCLUDING the idle tail, so a shorter tail is the main cost lever here.`,
383
- ` LifecycleConfiguration: { IdleRuntimeSessionTimeout: ${IDLE_TIMEOUT_SECONDS}, MaxLifetime: ${MAX_LIFETIME_SECONDS} }`,
295
+ ` # session INCLUDING the idle tail, so a shorter tail is the main cost lever here —`,
296
+ ` # deploy.agentcore.idleTimeoutSeconds buys latency back for a chat workload.`,
297
+ ` LifecycleConfiguration: { IdleRuntimeSessionTimeout: ${idleTimeout}, MaxLifetime: ${MAX_LIFETIME_SECONDS} }`,
384
298
  ` EnvironmentVariables:`,
385
299
  ...envLines,
386
300
  ];
387
- if (needsForwarder) {
388
- lines.push(``, ` ForwarderRole:`, ` Type: AWS::IAM::Role`, ` Properties:`, ` AssumeRolePolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Principal: { Service: lambda.amazonaws.com }`, ` Action: sts:AssumeRole`, ` ManagedPolicyArns: [arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]`, ` Policies:`, ` - PolicyName: invoke-runtime`, ` PolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Action: bedrock-agentcore:InvokeAgentRuntime`, ` Resource:`, ` - !GetAtt Runtime.AgentRuntimeArn`, ` - !Sub "\${Runtime.AgentRuntimeArn}/*"`, ` - Effect: Allow # mint the presigned URLs the container uses for its state snapshot`, ` Action: [s3:GetObject, s3:PutObject]`, ` Resource: !Sub arn:aws:s3:::\${StateBucket}/${STATE_KEY}`, ` # Without s3:ListBucket, S3 folds "key absent" into 403 (anti-enumeration), which is`, ` # indistinguishable from a broken signature — so the container's restore contract`, ` # (agentcore-state.ts: ONLY 404 means first deploy) would dead-end every first deploy.`, ` # Scoped to the snapshot prefix: this grants "may know whether the snapshot exists",`, ` # not a listing of the whole deployment bucket.`, ` - Effect: Allow`, ` Action: s3:ListBucket`, ` Resource: !Sub arn:aws:s3:::\${StateBucket}`, ` Condition:`, ` StringLike: { s3:prefix: state/* }`, ...(input.selfSchedule
389
- ? [
390
- ` - Effect: Allow # wake alarms: mirror pending wake-ups into one-shot schedules`,
391
- ` Action: [scheduler:CreateSchedule, scheduler:UpdateSchedule]`,
392
- ` Resource: !Sub arn:aws:scheduler:\${AWS::Region}:\${AWS::AccountId}:schedule/default/fa-${input.name}-wk-*`,
393
- ` - Effect: Allow # hand the poke schedules their invoke role`,
394
- ` Action: iam:PassRole`,
395
- ` Resource: !GetAtt WakeSchedulerRole.Arn`,
396
- ]
397
- : []), ...(needsForwarder
398
- ? [
399
- ` - Effect: Allow # self-resolve callback URL for state-URL refresh / wake alarms`,
400
- ` Action: lambda:GetFunctionUrlConfig`,
401
- ` Resource: ${forwarderFnArn}`,
402
- ]
403
- : []), ``, ` Forwarder:`, ` Type: AWS::Lambda::Function`, ` Properties:`, ` FunctionName: fastagent-${input.name}-forwarder`, ` Runtime: nodejs22.x`, ` Handler: index.handler`, ` # Webhook ACKs are fast, but schedule-fire holds the connection for the WHOLE agent turn`, ` # (claim-before-run means a timeout never double-fires; the turn also continues and is`, ` # audited container-side). EventBridge→Lambda is async, so the long timeout costs nothing.`, ` Timeout: 900`, ` MemorySize: 256`, ` Role: !GetAtt ForwarderRole.Arn`, ` Environment:`, ` Variables:`, ` RUNTIME_ARN: !GetAtt Runtime.AgentRuntimeArn`, ` INGRESS_SESSION_ID: ${ingressSessionId(input.name)}`, ...(needsForwarder ? [` STATE_REFRESH_SECRET: !Ref FastagentIngressSecret`] : []), ...(topology.webhooks ? [` WEBHOOKS_ENABLED: "1"`] : []), ...(input.selfSchedule
404
- ? [
405
- ` WAKE_SECRET: !Ref FastagentWakeSecret`,
406
- ` WAKE_ROLE_ARN: !GetAtt WakeSchedulerRole.Arn`,
407
- ` WAKE_PREFIX: fa-${input.name}-wk-`,
408
- ]
409
- : []), ` INGRESS_SECRET: !Ref FastagentIngressSecret`, ` STATE_BUCKET: !Ref StateBucket`, ` STATE_KEY: ${STATE_KEY}`, ` MAX_WEBHOOK_BODY_BYTES: "${MAX_WEBHOOK_BODY_BYTES}"`, ` # From S3, not inline: the forwarder mints SigV4-presigned URLs for the state snapshot and`, ` # no longer fits CloudFormation's 4096-byte inline cap. The key is content-hashed, so a`, ` # code change is a parameter change — CloudFormation cannot miss it.`, ` Code:`, ` S3Bucket: !Ref StateBucket`, ` S3Key: !Ref ForwarderS3Key`);
410
- }
411
- if (needsForwarder) {
412
- lines.push(``, ` ForwarderUrl:`, ` Type: AWS::Lambda::Url`, ` Properties:`, ` TargetFunctionArn: !GetAtt Forwarder.Arn`, ` # NONE is deliberate: webhook callers (Telegram/Feishu) cannot SigV4-sign. Authenticity is`, ` # verified downstream by each channel (secret token / signature), exactly as on every host.`, ` AuthType: NONE`, ``, ` ForwarderUrlPermission:`, ` Type: AWS::Lambda::Permission`, ` Properties:`, ` FunctionName: !Ref Forwarder`, ` Action: lambda:InvokeFunctionUrl`, ` Principal: "*"`, ` FunctionUrlAuthType: NONE`, ``, ` # Function URLs created after Oct 2025 require lambda:InvokeFunction IN ADDITION to`, ` # lambda:InvokeFunctionUrl for public (NONE) access — with only the first, every request 403s`, ` # (found by the first real deploy). InvokedViaFunctionUrl scopes it to URL traffic: without it`, ` # the bare * principal would also let any AWS principal call the Lambda API directly, bypassing`, ` # the Function URL event shape to forge internal events.`, ` ForwarderInvokePermission:`, ` Type: AWS::Lambda::Permission`, ` Properties:`, ` FunctionName: !Ref Forwarder`, ` Action: lambda:InvokeFunction`, ` Principal: "*"`, ` InvokedViaFunctionUrl: true`);
413
- if (input.selfSchedule) {
414
- lines.push(``, ` # The role the wake-poke schedules assume to invoke the forwarder. Its policy names the`, ` # function by CONSTRUCTED arn (not !Ref) — the forwarder's env references this role, so a`, ` # !Ref back would be a circular dependency.`, ` WakeSchedulerRole:`, ` Type: AWS::IAM::Role`, ` Properties:`, ` AssumeRolePolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Principal: { Service: scheduler.amazonaws.com }`, ` Action: sts:AssumeRole`, ` Condition:`, ` StringEquals: { aws:SourceAccount: !Ref AWS::AccountId }`, ` Policies:`, ` - PolicyName: poke-forwarder`, ` PolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Action: lambda:InvokeFunction`, ` Resource: ${forwarderFnArn}`);
415
- }
416
- }
301
+ lines.push(``, ` ForwarderRole:`, ` Type: AWS::IAM::Role`, ` Properties:`, ` AssumeRolePolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Principal: { Service: lambda.amazonaws.com }`, ` Action: sts:AssumeRole`, ` ManagedPolicyArns: [arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]`, ` Policies:`, ` - PolicyName: invoke-runtime`, ` PolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Action: bedrock-agentcore:InvokeAgentRuntime`, ` Resource:`, ` - !GetAtt Runtime.AgentRuntimeArn`, ` - !Sub "\${Runtime.AgentRuntimeArn}/*"`, ` - Effect: Allow # wake alarms: mirror pending wake-ups into one-shot schedules`, ` Action: [scheduler:CreateSchedule, scheduler:UpdateSchedule]`, ` Resource: !Sub arn:aws:scheduler:\${AWS::Region}:\${AWS::AccountId}:schedule/default/${wakeAlarmPrefix(input.name)}*`, ` - Effect: Allow # hand the poke schedules their invoke role`, ` Action: iam:PassRole`, ` Resource: !GetAtt WakeSchedulerRole.Arn`, ` - Effect: Allow # self-resolve callback URL for state-URL refresh / wake alarms`, ` Action: lambda:GetFunctionUrlConfig`, ` Resource: ${forwarderFnArn}`, ``, ` Forwarder:`, ` Type: AWS::Lambda::Function`, ` Properties:`, ` FunctionName: ${forwarderFunctionName(input.name)}`, ` Runtime: nodejs22.x`, ` Handler: index.handler`, ` # Webhook ACKs are fast, but routine-fire holds the connection for the WHOLE agent turn`, ` # (claim-before-run means a timeout never double-fires; the turn also continues and is`, ` # audited container-side). EventBridge→Lambda is async, so the long timeout costs nothing.`, ` Timeout: 900`, ` MemorySize: 256`, ` Role: !GetAtt ForwarderRole.Arn`, ` Environment:`, ` Variables:`, ` RUNTIME_ARN: !GetAtt Runtime.AgentRuntimeArn`, ` INGRESS_SESSION_ID: ${ingressSessionId(input.name)}`, ...(topology.webhooks ? [` WEBHOOKS_ENABLED: "1"`] : []), ` WAKE_SECRET: !Ref FastagentWakeSecret`, ` WAKE_ROLE_ARN: !GetAtt WakeSchedulerRole.Arn`, ` WAKE_PREFIX: ${wakeAlarmPrefix(input.name)}`, ` INGRESS_SECRET: !Ref FastagentIngressSecret`, ` MAX_WEBHOOK_BODY_BYTES: "${MAX_WEBHOOK_BODY_BYTES}"`, ` # A content-hashed key makes code changes visible to CloudFormation.`, ` Code:`, ` S3Bucket: !Ref ForwarderBucket`, ` S3Key: !Ref ForwarderS3Key`);
302
+ lines.push(``, ` ForwarderUrl:`, ` Type: AWS::Lambda::Url`, ` Properties:`, ` TargetFunctionArn: !GetAtt Forwarder.Arn`, ` # NONE is deliberate: webhook callers (Telegram/Feishu) cannot SigV4-sign. Authenticity is`, ` # verified downstream by each channel (secret token / signature), exactly as on every host.`, ` AuthType: NONE`, ``, ` ForwarderUrlPermission:`, ` Type: AWS::Lambda::Permission`, ` Properties:`, ` FunctionName: !Ref Forwarder`, ` Action: lambda:InvokeFunctionUrl`, ` Principal: "*"`, ` FunctionUrlAuthType: NONE`, ``, ` # Function URLs created after Oct 2025 require lambda:InvokeFunction IN ADDITION to`, ` # lambda:InvokeFunctionUrl for public (NONE) access — with only the first, every request 403s`, ` # (found by the first real deploy). InvokedViaFunctionUrl scopes it to URL traffic: without it`, ` # the bare * principal would also let any AWS principal call the Lambda API directly, bypassing`, ` # the Function URL event shape to forge internal events.`, ` ForwarderInvokePermission:`, ` Type: AWS::Lambda::Permission`, ` Properties:`, ` FunctionName: !Ref Forwarder`, ` Action: lambda:InvokeFunction`, ` Principal: "*"`, ` InvokedViaFunctionUrl: true`);
303
+ lines.push(``, ` # The role the wake-poke schedules assume to invoke the forwarder. Its policy names the`, ` # function by CONSTRUCTED arn (not !Ref) — the forwarder's env references this role, so a`, ` # !Ref back would be a circular dependency.`, ` WakeSchedulerRole:`, ` Type: AWS::IAM::Role`, ` Properties:`, ` AssumeRolePolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Principal: { Service: scheduler.amazonaws.com }`, ` Action: sts:AssumeRole`, ` Condition:`, ` StringEquals: { aws:SourceAccount: !Ref AWS::AccountId }`, ` Policies:`, ` - PolicyName: poke-forwarder`, ` PolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Action: lambda:InvokeFunction`, ` Resource: ${forwarderFnArn}`);
417
304
  if (translated.length > 0) {
418
305
  lines.push(``, ` SchedulerRole:`, ` Type: AWS::IAM::Role`, ` Properties:`, ` AssumeRolePolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Principal: { Service: scheduler.amazonaws.com }`, ` Action: sts:AssumeRole`, ` Condition:`, ` StringEquals: { aws:SourceAccount: !Ref AWS::AccountId }`, ` Policies:`, ` - PolicyName: fire-forwarder`, ` PolicyDocument:`, ` Version: "2012-10-17"`, ` Statement:`, ` - Effect: Allow`, ` Action: lambda:InvokeFunction`, ` Resource: !GetAtt Forwarder.Arn`);
419
306
  for (const { fact, expression } of translated) {
420
- lines.push(``, ` Schedule${logicalId(fact.name)}:`, ` Type: AWS::Scheduler::Schedule`, ` Properties:`, ` Name: ${scheduleResourceName(input.name, fact.name)}`, ` ScheduleExpression: ${expression}`, ` ScheduleExpressionTimezone: ${fact.tz ?? "Etc/UTC"}`, ` FlexibleTimeWindow: { Mode: "OFF" }`, ` Target:`, ` Arn: !GetAtt Forwarder.Arn`, ` RoleArn: !GetAtt SchedulerRole.Arn`, ` # <aws.scheduler.scheduled-time> = the slot instant — the container's idempotency key`, ` # (EventBridge delivery is at-least-once; a duplicate slot must not double-fire).`, ` Input: ${yamlSingleQuote(JSON.stringify({ scheduleFire: { name: fact.name, slot: "<aws.scheduler.scheduled-time>" } }))}`);
307
+ lines.push(``, ` Schedule${logicalId(fact.name)}:`, ` Type: AWS::Scheduler::Schedule`, ` Properties:`, ` Name: ${scheduleResourceName(input.name, fact.name)}`, ` ScheduleExpression: ${expression}`, ` ScheduleExpressionTimezone: ${fact.tz ?? "Etc/UTC"}`, ` FlexibleTimeWindow: { Mode: "OFF" }`, ` Target:`, ` Arn: !GetAtt Forwarder.Arn`, ` RoleArn: !GetAtt SchedulerRole.Arn`, ` # <aws.scheduler.scheduled-time> is the clock's NAME for this occurrence: EventBridge repeats it`, ` # byte-identical on every redelivery, which is what makes the container's dedup work.`, ` Input: ${yamlSingleQuote(JSON.stringify({
308
+ scheduleFire: { name: fact.name, occurrence: "<aws.scheduler.scheduled-time>" },
309
+ }))}`);
421
310
  }
422
311
  }
423
312
  lines.push(``, `Outputs:`, ` RuntimeArn:`, ` Value: !GetAtt Runtime.AgentRuntimeArn`);
424
- if (needsForwarder) {
425
- lines.push(` ForwarderUrl:`, ` Value: !GetAtt ForwarderUrl.FunctionUrl`);
426
- }
313
+ lines.push(` ForwarderUrl:`, ` Value: !GetAtt ForwarderUrl.FunctionUrl`);
427
314
  return `${lines.join("\n")}\n`;
428
315
  }
429
316
  /** Compute the AgentCore deploy plan from the resolved definition. */
430
317
  export function planAgentcoreDeploy(input) {
431
318
  const { name, channels } = input;
432
- const stack = `fastagent-${name}`;
433
- const repo = `fastagent/${name}`;
319
+ const stack = agentcoreStackName(name);
320
+ const repo = agentcoreRepoName(name);
434
321
  const prefix = input.agentPrefix;
435
- // Translate every schedule; the ones EventBridge cannot express become explicit runbook warnings —
436
- // a schedule silently missing from the template would be the worst failure mode (nothing ever fires).
322
+ // Translate every schedule; the ones EventBridge cannot express become explicit runbook warnings.
437
323
  const translated = [];
438
324
  const untranslatable = [];
439
325
  for (const fact of input.schedules) {
@@ -443,10 +329,9 @@ export function planAgentcoreDeploy(input) {
443
329
  else
444
330
  untranslatable.push({ name: fact.name, reason: result.error });
445
331
  }
446
- // Identifier collisions: the author-side → AWS-side name mappings are lossy (logical ids strip
447
- // punctuation; parameter names collapse underscores), so two DISTINCT legal inputs can land on one
448
- // CloudFormation key — which would generate a silently wrong stack. Fail visibly at plan time; a
449
- // rename is the fix (a hash-mangled allocator would trade readability for an edge case).
332
+ // Identifier collisions: the author-side → AWS-side name mappings are lossy (logical ids strip punctuation;
333
+ // parameter names collapse underscores), so two DISTINCT legal inputs can land on one CloudFormation key — which
334
+ // would generate a silently wrong stack.
450
335
  const logicalIds = new Map();
451
336
  for (const { fact } of translated) {
452
337
  const id = `Schedule${logicalId(fact.name)}`;
@@ -456,35 +341,21 @@ export function planAgentcoreDeploy(input) {
456
341
  }
457
342
  logicalIds.set(id, fact.name);
458
343
  }
459
- const paramNames = new Map();
460
- for (const s of deploymentSecrets(input.modelAuth, channels, input.extraSecrets)) {
461
- const p = cfnParamName(s.name);
462
- const clash = paramNames.get(p);
463
- if (clash !== undefined && clash !== s.name) {
464
- throw new Error(`secrets "${clash}" and "${s.name}" collapse to the same CloudFormation parameter (${p}) — rename one`);
465
- }
466
- paramNames.set(p, s.name);
467
- }
468
- const topology = agentcoreTopology(input, translated.length);
469
- const needsForwarder = topology.forwarder;
344
+ const topology = agentcoreTopology(input);
470
345
  const artifacts = [
471
346
  { path: `${prefix}${TEMPLATE_FILE}`, content: template(input, translated, topology) },
472
- ...(needsForwarder ? [{ path: `${prefix}${FORWARDER_FILE}`, content: forwarderSource() }] : []),
347
+ { path: `${prefix}${FORWARDER_FILE}`, content: forwarderSource() },
473
348
  ...containerArtifacts(input),
474
349
  ];
475
- const secrets = deploymentSecrets(input.modelAuth, channels, input.extraSecrets);
476
- const requiredSecrets = secrets.filter((s) => s.required);
477
- const optionalSecrets = secrets.filter((s) => !s.required);
478
- const paramHint = (list) => list.map((s) => `${cfnParamName(s.name)}=<value>`).join(" ");
350
+ const secrets = input.secrets ?? [];
479
351
  const image = `<account-id>.dkr.ecr.<region>.amazonaws.com/${repo}:<tag>`;
480
- const bucketHint = stateBucketName(name, "<account-id>");
352
+ const bucketHint = deploymentBucketName(name, "<account-id>");
481
353
  const runbook = [
482
354
  `# Deploy "${name}" to AWS Bedrock AgentCore. ${prefix}${TEMPLATE_FILE} / Dockerfile(.dockerignore) are generated above.`,
483
355
  `# Prereqs: AWS CLI v2 with credentials + a region where AgentCore is available, and Docker with buildx`,
484
356
  `# (the image MUST be linux/arm64 — the one host whose build runs on YOUR machine, not remotely).`,
485
357
  ``,
486
- `# 1. ECR repository + the deployment bucket (one-time; skip what exists). The bucket lives OUTSIDE`,
487
- `# the stack on purpose: it holds the agent's state snapshot, which must survive a delete-stack.`,
358
+ `# 1. ECR repository + the forwarder artifact bucket (one-time; skip what exists).`,
488
359
  `aws ecr create-repository --repository-name ${repo}`,
489
360
  `aws s3api create-bucket --bucket ${bucketHint} --region us-east-1 # us-east-1 ONLY`,
490
361
  `aws s3api create-bucket --bucket ${bucketHint} --region <region> \\ # every OTHER region`,
@@ -492,79 +363,46 @@ export function planAgentcoreDeploy(input) {
492
363
  `aws s3api put-public-access-block --bucket ${bucketHint} \\`,
493
364
  ` --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true`,
494
365
  ``,
495
- ...(needsForwarder
496
- ? [
497
- `# 1b. Package the forwarder and upload it. Name the object by its CONTENT (a hash/date):`,
498
- `# CloudFormation rolls the function only when the ForwarderS3Key VALUE changes.`,
499
- `(cd ${prefix}lambda && zip -q forwarder.zip index.js)`,
500
- `aws s3 cp ${prefix}lambda/forwarder.zip s3://${bucketHint}/forwarder/<hash>.zip`,
501
- ``,
502
- ]
503
- : []),
366
+ `# 1b. Package the forwarder and upload it. Name the object by its CONTENT (a hash/date):`,
367
+ `# CloudFormation rolls the function only when the ForwarderS3Key VALUE changes.`,
368
+ `(cd ${prefix}lambda && zip -q forwarder.zip index.js)`,
369
+ `aws s3 cp ${prefix}lambda/forwarder.zip s3://${bucketHint}/forwarder/<hash>.zip`,
370
+ ``,
504
371
  `# 2. Build (linux/arm64) + push. Use a UNIQUE tag per deploy (a git sha / date): CloudFormation only`,
505
372
  `# rolls the runtime when the ImageUri VALUE changes — re-pushing the same tag deploys nothing.`,
506
373
  `aws ecr get-login-password | docker login --username AWS --password-stdin <account-id>.dkr.ecr.<region>.amazonaws.com`,
507
- prefix
508
- ? `docker buildx build --platform linux/arm64 -f ${prefix}Dockerfile -t ${image} --push .`
509
- : `docker buildx build --platform linux/arm64 -t ${image} --push .`,
374
+ `docker buildx build --platform linux/arm64 -f ${prefix}Dockerfile -t ${image} --push .`,
510
375
  ``,
511
- `# 3. Deploy the stack (runtime + ingress + schedules in one template). Secrets ride NoEcho parameters:`,
376
+ `# 3. Deploy the stack (runtime + ingress + schedules in one template).`,
512
377
  ];
513
- if (requiredSecrets.length > 0) {
514
- runbook.push(`# Required parameters:`, ...requiredSecrets.map((s) => `# ${cfnParamName(s.name)}: ${s.hint}`));
515
- }
516
- if (optionalSecrets.length > 0) {
517
- runbook.push(`# Optional parameters (set only when the matching feature is configured):`, ...optionalSecrets.map((s) => `# ${cfnParamName(s.name)}: ${s.hint}`));
518
- }
519
- const wakeSecretHint = input.selfSchedule ? " FastagentWakeSecret=<any random string>" : "";
520
- if (input.selfSchedule) {
521
- runbook.push(`# FastagentWakeSecret: the wake-alarm shared secret — any random string (\`--run\` mints one)`);
378
+ if (secrets.length > 0) {
379
+ runbook.push(`# Variables ride ONE NoEcho parameter, FastagentEnv: base64 of a JSON object {"NAME": "value", …},`, `# split every ${CARRIER_CHUNK_SIZE} chars across FastagentEnv, FastagentEnv2 … (\`--run\` builds it):`, ...secrets.map((s) => `# ${s.name}: ${s.hint}`));
522
380
  }
523
- runbook.push(`aws cloudformation deploy --stack-name ${stack} --template-file ${prefix}${TEMPLATE_FILE} \\`, ` --capabilities CAPABILITY_IAM \\`, ` --parameter-overrides ImageUri=${image}${needsForwarder ? ` StateBucket=${bucketHint} ForwarderS3Key=forwarder/<hash>.zip` : ""}${requiredSecrets.length > 0 ? ` ${paramHint(requiredSecrets)}` : ""}${wakeSecretHint}`, ``, needsForwarder
524
- ? `# 4. Read the outputs (the runtime ARN + callback URL; it serves webhooks only when configured):`
525
- : `# 4. Read the outputs (the runtime ARN — this topology has NO public URL: nothing outside AWS`, ...(needsForwarder
526
- ? []
527
- : [`# sends to it, so no Function URL is created and the agent is reachable only via SigV4).`]), `aws cloudformation describe-stacks --stack-name ${stack} --query "Stacks[0].Outputs"`, ``, `# 5. Tail the Runtime's application stdout/stderr (same fastagent messages + log level as locally).`, `# Discovery resolves the per-endpoint log group from the stack's RuntimeArn:`, `fastagent logs agentcore --follow`, ...(needsForwarder
528
- ? [
529
- `# The ingress transport is a separate Lambda and therefore a separate log source:`,
530
- `fastagent logs agentcore --source forwarder --follow`,
531
- ]
532
- : []));
533
- // Model-auth guidance mirrors the other hosts: an env key became a parameter above; OAuth/stored
534
- // can't be read at plan time — `--run` carries it as FastagentAuthSeed.
381
+ const wakeSecretHint = " FastagentWakeSecret=<any random string>";
382
+ runbook.push(`# FastagentWakeSecret: the wake-alarm shared secret — any random string (\`--run\` mints one)`);
383
+ runbook.push(`aws cloudformation deploy --stack-name ${stack} --template-file ${prefix}${TEMPLATE_FILE} \\`, ` --capabilities CAPABILITY_IAM \\`, ` --parameter-overrides ImageUri=${image} ForwarderBucket=${bucketHint} ForwarderS3Key=forwarder/<hash>.zip${secrets.length > 0 ? " FastagentEnv=<base64 JSON>" : ""}${wakeSecretHint}`, ``, `# 4. Read the outputs (the runtime ARN + callback URL; it serves webhooks only when configured):`, `aws cloudformation describe-stacks --stack-name ${stack} --query "Stacks[0].Outputs"`, ``, `# 5. Tail the Runtime's application stdout/stderr (same fastagent messages + log level as locally).`, `# Discovery resolves the per-endpoint log group from the stack's RuntimeArn:`, `fastagent logs agentcore --follow`, `# The ingress transport is a separate Lambda and therefore a separate log source:`, `fastagent logs agentcore --source forwarder --follow`, ``, `# 6. Set a log retention period on EVERY group this stack writes to. Each is created by the service that`, `# writes it, not by this template, and CloudWatch keeps log data indefinitely by default — which is`, `# forever-billed storage, and the logs are where WHY a scheduled turn failed is recorded. Logs are the`, `# ONE state path this host does not reclaim on its own.`, `aws logs put-retention-policy --log-group-name <the group \`fastagent logs agentcore\` resolves> --retention-in-days 14`, `# The forwarder is a separate Lambda, so its group is separate and defaults the same way:`, `aws logs put-retention-policy --log-group-name ${forwarderLogGroup(input.name)} --retention-in-days 14`);
384
+ // Model-auth guidance mirrors the other hosts: an env key became a parameter above; OAuth/stored can't be read at
385
+ // plan time — `--run` carries it as FastagentAuthSeed.
535
386
  if (!isEnvKey(input.modelAuth)) {
536
387
  runbook.push(``, input.modelAuth === undefined
537
- ? `# Model auth: none found at the local auth path — pass --auth-path <file>, or \`--run\` carries it`
388
+ ? `# Model auth: none found at the local auth path — set FASTAGENT_AUTH_PATH, or \`--run\` carries it`
538
389
  : `# Model auth: your local auth is "${input.modelAuth}" — the plan can't read its value; \`--run\` carries it`, `# as the FastagentAuthSeed parameter (base64 of auth.json), materialized on first boot.`);
539
390
  }
540
- // Post-deploy webhook registration — the shared channel-ingress steps, pointed at the forwarder's
541
- // Function URL (read from the stack outputs). `--run` REFUSES a long-connection channel here, but
542
- // generate-only only warns and still prints this runbook, so the steps are filtered on ingress like
543
- // every other host's rather than on the CLI having gated.
391
+ // Post-deploy webhook registration — the shared channel-ingress steps, pointed at the forwarder's Function URL
392
+ // (read from the stack outputs).
544
393
  const post = webhookRunbook(`<ForwarderUrl>`, channels);
545
- // AgentCore asides the shared steps cannot carry: nothing else routes through a forwarder, and
546
- // nothing else has a compute ceiling.
547
- // Named rather than positional: the asides land after ALL the steps, so "the console" has to say
548
- // WHICH console. One line for both kinds — an agent that declares feishu AND lark reads the same
549
- // fact twice otherwise and looks for a second step that does not exist.
394
+ // AgentCore asides the shared steps cannot carry: nothing else routes through a forwarder, and nothing else has a
395
+ // compute ceiling.
550
396
  if (webhookKinds(channels).some((kind) => kind === "feishu" || kind === "lark")) {
551
397
  post.push(`# NOTE: the Feishu/Lark console's challenge rides through the forwarder to the channel and back`, `# verbatim, so the stack must be deployed when you save the Request URL.`);
552
398
  }
553
- if (channels.some((channel) => channel.name === "github")) {
554
- post.push(`# NOTE: github turns are fire-and-forget with no replay — a compute reclaimed mid-review drops it`, `# (the ping's HealthyBusy + time_of_last_update holds the session while turns run, but the 8 h compute ceiling is hard).`);
555
- }
556
399
  if (post.length > 0)
557
400
  runbook.push(``, ...post);
558
401
  for (const u of untranslatable) {
559
402
  runbook.push(``, `# WARNING: schedule "${u.name}" has NO EventBridge rule — ${u.reason}.`, `# It will NOT fire on this deployment until you create an equivalent trigger yourself.`);
560
403
  }
561
- if (input.selfSchedule) {
562
- runbook.push(``, `# selfSchedule: the agent's wake-ups are EventBridge-backed — each pending wake-up is mirrored`, `# (via the forwarder, authenticated by FastagentWakeSecret) into a self-deleting one-shot`, `# schedule (fa-${name}-wk-*) that wakes the container at the right instant. Reliable even when`, `# the compute is reclaimed. Caveat: only for wake-ups set through the INGRESS surface (chat`, `# channels/schedules); a wake set inside a direct InvokeAgentRuntime session stays in that`, `# session's own storage and fires only while that session is awake.`);
563
- }
564
- runbook.push(``, `# Invoke the agent programmatically (any session id ≥ 33 chars; the response streams as SSE):`, `aws bedrock-agentcore invoke-agent-runtime --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "my-conversation-000000000000000000" \\`, ` --payload '{"kind":"invoke","session":"cli","text":"hello"}' --cli-binary-format raw-in-base64-out /dev/stdout`);
565
- if (needsForwarder) {
566
- runbook.push(``, `# After a REDEPLOY, stop the ingress session so the new image serves immediately — a live session`, `# keeps its old compute (and the OLD image) until ${IDLE_TIMEOUT_SECONDS}s idle / the 8 h compute ceiling`, `# (\`--run\` does this automatically):`, `aws bedrock-agentcore stop-runtime-session --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "${ingressSessionId(name)}"`);
567
- }
568
- runbook.push(``, `# Redeploy = step 1b (new forwarder key, if its code changed) + step 2 with a NEW tag + step 3.`, `# STATE: ${MOUNT} is a LOCAL disk — AWS wipes it on every runtime version update (i.e. every`, `# deploy) and after 14 idle days. What survives is the S3 snapshot under s3://${bucketHint}/${STATE_KEY}:`, `# the container restores it on its first invocation and pushes it whenever work settles. Keep that`, `# bucket and the agent keeps its sessions, channel state and pending wake-ups across deploys;`, `# delete it and the agent starts blank. (A persistent MOUNT would need EFS + VPC mode + a NAT`, `# gateway for model/channel egress — see the template comment.)`, `# CREDENTIALS RIDE THAT SNAPSHOT TOO: FASTAGENT_SECRETS_DIR is ${SECRETS_DIR}, inside the state`, `# root, so an OAuth auth.json ROTATED on the box persists (a refresh token is single-use — without`, `# this the next microVM would re-seed the deploy-time copy and eventually fail to authenticate).`, `# The bucket is therefore credential storage: it is created with public access blocked and`, `# versioning on, and deleting it costs model access until the next deploy re-seeds.`);
404
+ runbook.push(``, `# Wake-ups: the agent's own follow-up turns (the wake tool) are EventBridge-backed — each pending wake-up is mirrored`, `# (via the forwarder, authenticated by FastagentWakeSecret) into a self-deleting one-shot`, `# schedule (fa-${name}-wk-*) that wakes the container at the right instant. Reliable even when`, `# the compute is reclaimed. The forwarder supplies the callback URL; use the deployment's`, `# fixed runtime session id for programmatic calls too.`);
405
+ runbook.push(``, `# Invoke the agent programmatically (reuse the deployment session; conversations remain separate):`, `aws bedrock-agentcore invoke-agent-runtime --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "${ingressSessionId(name)}" \\`, ` --payload '{"kind":"invoke","session":"cli","text":"hello"}' --cli-binary-format raw-in-base64-out /dev/stdout`);
406
+ runbook.push(``, `# Stop the fixed runtime session after redeploy so the next call uses the new image.`, `# \`--run\` does this automatically. In-flight turns are interrupted.`, `aws bedrock-agentcore stop-runtime-session --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "${ingressSessionId(name)}"`, ``, `# Redeploy = \`fastagent deploy agentcore\` (a new definition needs a fresh release manifest) + step 1b`, `# (new forwarder key, if its code changed) + step 2 with a NEW tag + step 3.`, `# STATE: ${MOUNT} is managed SessionStorage. It keeps base/ (including unfinished work), .state/`, `# and .secrets/ across compute stop/resume — an idle-reclaimed agent resumes with its memory. AWS`, `# RESETS it on every runtime version update (i.e. every deploy) and after 14 idle days, so a deploy`, `# replaces the state along with the image: sessions, channel state and pending wake-ups start blank,`, `# and the model credential is re-seeded from FASTAGENT_AUTH_SEED. Deploying IS re-authenticating.`, `# An OAuth refresh token is single-use and shared with your machine, so the box can lose model`, `# access between deploys — deploy again to refresh it, or use a provider API key.`, `# Cross-deploy memory needs EFS or S3 Files, which are VPC-only (a NAT gateway for model/channel`, `# egress, ~$33/mo standing): use \`deploy fly\` or \`deploy railway\` for a real volume instead.`, `# Keep one runtime writer per workspace; use the fixed runtime session id printed above for every entry point.`);
569
407
  return { artifacts, runbook, untranslatableSchedules: untranslatable, topology };
570
408
  }