@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,150 +1,164 @@
1
1
  /**
2
- * The secret set a deployed agent needs, computed from the definition — host-neutral. Required values
3
- * gate every target; optional channel values travel only when configured. Only the SET command differs
4
- * (`fly secrets import` vs `railway variables set`). The runbooks list both classes; `--run` reads local values.
2
+ * The environment a deployed agent runs with — host-neutral.
5
3
  *
6
- * Both DIRECTIONS of the credential carry live here: the deploy-time assembly below, and the boot-time
7
- * seed read at the bottom. They were split across a host driver (`fly/run.ts`), which left `start`
8
- * — a serving path that deploys nothing, on Fly or anywhere — importing from it to boot a container.
4
+ * The value file (`.secrets/.env`) IS the deployed environment: every variable in it travels, on every host. What the
5
+ * definition declares (`defineTool`/`defineChannel`/`defineRoutine({ secrets })`, plus the model's env key) only says
6
+ * which of those must have a value, so a missing one stops a deploy before its first side effect instead of a boot.
9
7
  */
10
- import { CONTROL_TOKEN_ENV } from "../channels/control.js";
11
- import { CHANNEL_KINDS, channelSetup } from "../scaffold/add-channel.js";
12
- /** The declared channels this tool has setup metadata for. A custom channel carries its own secrets;
13
- * nothing here can name them, and guessing would print a runbook line no one can act on. */
14
- function firstPartyChannels(channels) {
15
- return channels.flatMap((channel) => CHANNEL_KINDS.includes(channel.name)
16
- ? [{ kind: channel.name, ingress: channel.ingress }]
17
- : []);
8
+ import { dedupeSecrets } from "../declared-secrets.js";
9
+ /** Is this local auth source an env-var API key (→ becomes a deploy secret) vs OAuth / stored / none? */
10
+ export function isEnvKey(source) {
11
+ return source !== undefined && /^[A-Z][A-Z0-9_]*$/.test(source);
18
12
  }
19
13
  /**
20
- * Is this local auth source an env-var API key (→ becomes a deploy secret) vs OAuth / stored / none?
21
- * Positive match on the UPPER_SNAKE env-var naming shape, NOT a negative exclude of today's sentinel
22
- * labels: a new non-env `AuthResult.source` (e.g. `keychain`) then degrades to guidance, never a fake
23
- * `keychain=<value>` secret — the two modules don't couple through an exhaustive string list.
14
+ * Names a deployment sets on the box itself. The value file's copy of one describes THIS machine (a local state path,
15
+ * a port) or rides another carrier (the model rides the release manifest), so it never travels.
24
16
  */
25
- export function isEnvKey(source) {
26
- return source !== undefined && /^[A-Z][A-Z0-9_]*$/.test(source);
17
+ const DEPLOY_OWNED = new Set([
18
+ "PORT",
19
+ "FASTAGENT_AGENT",
20
+ "FASTAGENT_MODEL",
21
+ "FASTAGENT_STATE_DIR",
22
+ "FASTAGENT_SECRETS_DIR",
23
+ "FASTAGENT_AUTH_PATH",
24
+ "FASTAGENT_RELEASE_FILE",
25
+ "FASTAGENT_STORAGE_DIR",
26
+ "FASTAGENT_AGENTCORE",
27
+ "FASTAGENT_INGRESS_SECRET",
28
+ "FASTAGENT_WAKE_SECRET",
29
+ "FASTAGENT_DEV_WORKER",
30
+ ]);
31
+ /** Is `name` one the deployment sets itself (see {@link DEPLOY_OWNED}), including the chunked carriers? */
32
+ function isDeployOwned(name) {
33
+ return DEPLOY_OWNED.has(name) || /^FASTAGENT_(AUTH_SEED|ENV)(_\d+)?$/.test(name);
34
+ }
35
+ /** The value-file variables that travel: everything with a value that the deployment does not set itself. */
36
+ function carriedValues(values) {
37
+ const carried = {};
38
+ for (const [name, value] of values)
39
+ if (value && !isDeployOwned(name))
40
+ carried[name] = value;
41
+ return carried;
27
42
  }
28
43
  /**
29
- * Secret NAMES + hints for a runbook: the model key (when local auth is an env key), discovered channel
30
- * secrets, and config extras. Channel metadata keeps optional values visible without presenting them as
31
- * deployment prerequisites. An OAuth/stored login has no env key here — it carries as
32
- * `FASTAGENT_AUTH_SEED` on the `--run` path (see each host's run module), not as a named runbook secret.
44
+ * What a runbook lists: the names that must have a value (the model's env key, every declaration — hinted by the file
45
+ * that declared it) and then every other variable the value file carries.
33
46
  */
34
- export function deploymentSecrets(modelAuth, channels, extraSecrets = []) {
47
+ export function deploymentSecrets(modelAuth, declared, values, valueFile) {
35
48
  const secrets = [];
49
+ const add = (name, hint) => {
50
+ if (!secrets.some((s) => s.name === name))
51
+ secrets.push({ name, hint });
52
+ };
36
53
  if (isEnvKey(modelAuth))
37
- secrets.push({ name: modelAuth, hint: "your model provider key", required: true });
38
- for (const { kind, ingress } of firstPartyChannels(channels)) {
39
- for (const e of channelSetup(kind, ingress === "long-connection" ? "websocket" : "webhook").env) {
40
- secrets.push({ name: e.name, hint: e.hint, required: e.required });
41
- }
42
- }
43
- // Dedup: a name already covered by the model key / a channel secret must not appear twice in the runbook.
44
- for (const name of extraSecrets) {
45
- if (!secrets.some((s) => s.name === name)) {
46
- const control = name === CONTROL_TOKEN_ENV;
47
- secrets.push({
48
- name,
49
- hint: control
50
- ? "the /control/* bearer token — mint one (uuidgen) and give the same value to callers"
51
- : "declared in fastagent.config deploy.secrets",
52
- // OPTIONAL, unlike every other extra: unset, the box mints a per-boot token and still serves,
53
- // and every host with a shell can read it back out of control.json. Gating would stop deploys
54
- // that work today to enforce a convenience — the pre-flight warning is where that argument
55
- // belongs.
56
- required: !control,
57
- });
58
- }
59
- }
54
+ add(modelAuth, "your model provider key");
55
+ for (const { name, source } of dedupeSecrets(declared))
56
+ add(name, `required by ${source}`);
57
+ for (const name of Object.keys(carriedValues(values)))
58
+ add(name, `from ${valueFile}`);
60
59
  return secrets;
61
60
  }
62
61
  /**
63
- * Assemble the secret VALUES a `--run` deploy sets on the host, from the local credential + channels —
64
- * pure, host-neutral (Fly sets them via `fly secrets import`, Railway via `railway variables set`), so
65
- * the security-sensitive key wiring is testable once. The model credential travels one of two ways: an
66
- * env-key auth as its own secret (value from `env`), OR an OAuth/stored login (no plaintext key) as
67
- * `FASTAGENT_AUTH_SEED` (base64 auth.json) which `start` materializes on first boot. `needsModelCredential`
68
- * (neither present) is a DISTINCT signal: its remediation is `fastagent login`, not the `.env` one that
69
- * `missingSecrets` (real secret NAMES with no value) carries.
62
+ * Assemble the VALUES a `--run` deploy sets on the host: every variable the value file carries, plus the local
63
+ * credential. Never minted here.
70
64
  *
71
- * Channel secrets come from the local env only — NEVER minted. A random mint would be wrong for a
72
- * human-shared secret (github's webhook secret must match the value the operator enters in the repo,
73
- * which a silent mint never surfaces) and would rotate every run (breaking idempotency). An absent
74
- * required value enters `missingSecrets`; an absent optional value is simply omitted.
65
+ * `values` is the selected value file, read as data — **the environment running `deploy` is not a source**. A
66
+ * deployment must be reproducible from what it carries, and a variable that happens to be exported on the builder is
67
+ * written down nowhere. CI supplies values by writing the file before running the command.
75
68
  */
76
69
  export function assembleSecrets(input) {
77
- const secrets = {};
70
+ const secrets = carriedValues(input.values);
78
71
  const missingSecrets = [];
79
72
  let needsModelCredential = false;
80
73
  if (isEnvKey(input.modelAuth)) {
81
- const v = input.env[input.modelAuth];
82
- if (v)
83
- secrets[input.modelAuth] = v;
84
- else
85
- missingSecrets.push(input.modelAuth); // an env-key name with no value — `.env` remediation fits
74
+ if (!secrets[input.modelAuth])
75
+ missingSecrets.push(input.modelAuth); // `.env` remediation fits
86
76
  }
87
77
  else if (input.authFile) {
88
78
  secrets.FASTAGENT_AUTH_SEED = input.authFile.toString("base64");
89
79
  }
90
80
  else if (input.modelKeyInDefinition) {
91
- // The definition authenticates itself (models.json literal key, or a command run on the host), so it
92
- // travels in the image with everything else. Gating here would be the worst kind of wrong: both
93
- // remedies we print are impossible for such an agent — `fastagent login` has no flow for a custom
94
- // provider, and there is no provider env key to set.
81
+ // The definition authenticates itself (models.json literal key, or a command run on the host), so it travels in
82
+ // the image with everything else.
95
83
  }
96
84
  else {
97
85
  needsModelCredential = true; // no env key, no auth.json — `fastagent login` remediation
98
86
  }
99
- for (const { kind, ingress } of firstPartyChannels(input.channels)) {
100
- for (const e of channelSetup(kind, ingress === "long-connection" ? "websocket" : "webhook").env) {
101
- const v = input.env[e.name];
102
- if (v)
103
- secrets[e.name] = v; // optional channel values travel when configured
104
- else if (e.required) {
105
- missingSecrets.push(e.name); // operator-provided (in .env); a human-shared secret can't be minted
106
- }
107
- }
108
- }
109
- for (const name of input.extraSecrets ?? []) {
110
- if (name in secrets || missingSecrets.includes(name))
111
- continue; // already covered by model/channel — no dup
112
- const v = input.env[name];
113
- if (v)
114
- secrets[name] = v;
115
- // The control token is CARRIED, never gated — see {@link deploymentSecrets}: unset, the box mints
116
- // one and serves; every other extra is declared as needed, so its absence is a stop.
117
- else if (name !== CONTROL_TOKEN_ENV)
87
+ for (const { name } of dedupeSecrets(input.declared ?? [])) {
88
+ if (!secrets[name] && !missingSecrets.includes(name))
118
89
  missingSecrets.push(name);
119
90
  }
120
91
  return { secrets, missingSecrets, needsModelCredential };
121
92
  }
122
93
  /**
123
- * The bytes to seed to the auth file, or undefined to leave it alone — the pure core of `start`'s
124
- * FASTAGENT_AUTH_SEED materialization (the read side of {@link assembleSecrets}'s carry). ABSENT-ONLY
125
- * by design: a present file (a refreshed volume copy) is never overwritten by the stale seed, so a box
126
- * that ran its own OAuth refresh is not rolled back.
94
+ * The ONE refusal for declared names the deployed environment does not supply a value for — every host reaches it
95
+ * before its first side effect. Host-neutral because the reason is: the value file IS the deployed environment, so a
96
+ * name missing from it is missing from the deployment, whatever platform receives it.
127
97
  */
98
+ export function missingValuesGate(missing, valueFile) {
99
+ if (missing.length === 0)
100
+ return undefined;
101
+ return (`no value for: ${missing.join(", ")} — the deployed environment is declared by ${valueFile}, and this deploy ` +
102
+ `reads only that file (exporting the variable here does not reach the deployment). Add them there and re-run`);
103
+ }
104
+ /** The bytes to seed to the auth file, or undefined to leave it alone. */
128
105
  export function authSeedBytes(seed, fileExists) {
129
106
  return !seed || fileExists ? undefined : Buffer.from(seed, "base64");
130
107
  }
131
108
  /**
132
- * Collect the (possibly CHUNKED) auth seed from the environment: `FASTAGENT_AUTH_SEED` plus numbered
133
- * continuations (`_2`, `_3`, …) concatenated in order. Hosts whose env values carry a small max
134
- * length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
135
- * them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
136
- * Collection stops at the first absent/empty continuation — the writer fills them contiguously.
109
+ * Collect a (possibly CHUNKED) carrier from the environment: `<name>` plus numbered continuations (`_2`, `_3`, …)
110
+ * concatenated in order. A host whose env values have a length cap splits a long value this way.
137
111
  */
138
- export function collectAuthSeed(env) {
139
- const first = env.FASTAGENT_AUTH_SEED;
112
+ function collectChunked(env, name) {
113
+ const first = env[name];
140
114
  if (!first)
141
115
  return undefined;
142
- let seed = first;
116
+ let value = first;
143
117
  for (let i = 2;; i++) {
144
- const part = env[`FASTAGENT_AUTH_SEED_${i}`];
118
+ const part = env[`${name}_${i}`];
145
119
  if (!part)
146
120
  break;
147
- seed += part;
121
+ value += part;
148
122
  }
149
- return seed;
123
+ return value;
124
+ }
125
+ /** `FASTAGENT_AUTH_SEED` (+ `_2`, …): the base64 auth.json a deploy carries. */
126
+ export function collectAuthSeed(env) {
127
+ return collectChunked(env, "FASTAGENT_AUTH_SEED");
128
+ }
129
+ /**
130
+ * `FASTAGENT_ENV` (+ `_2`, …): the carried variables as ONE base64 JSON object, for a host whose deployment artifact
131
+ * must not depend on which names the value file holds (AgentCore's template is committed and gated on drift). The
132
+ * inverse of {@link encodeCarriedEnv}.
133
+ */
134
+ function collectCarriedEnv(env) {
135
+ const encoded = collectChunked(env, "FASTAGENT_ENV");
136
+ if (encoded === undefined)
137
+ return undefined;
138
+ const parsed = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
139
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
140
+ throw new Error("FASTAGENT_ENV is not an object of variables — redeploy to regenerate it");
141
+ }
142
+ const carried = {};
143
+ for (const [name, value] of Object.entries(parsed)) {
144
+ if (typeof value !== "string")
145
+ throw new Error(`FASTAGENT_ENV: ${name} is not a string — redeploy to regenerate it`);
146
+ carried[name] = value;
147
+ }
148
+ return carried;
149
+ }
150
+ /**
151
+ * Expand `FASTAGENT_ENV` into the process environment — what `start` does first on a host that carries the value file
152
+ * that way. The platform's own variables still win, as they do over a value file anywhere.
153
+ */
154
+ export function applyCarriedEnv(env = process.env) {
155
+ const carried = collectCarriedEnv(env);
156
+ if (carried)
157
+ for (const [name, value] of Object.entries(carried))
158
+ if (!(name in env))
159
+ env[name] = value;
160
+ }
161
+ /** The carried variables as the base64 JSON {@link collectCarriedEnv} reads back; empty when nothing travels. */
162
+ export function encodeCarriedEnv(carried) {
163
+ return Object.keys(carried).length === 0 ? "" : Buffer.from(JSON.stringify(carried), "utf8").toString("base64");
150
164
  }
@@ -0,0 +1,50 @@
1
+ export declare const RELEASE_FILE = "fastagent.release.json";
2
+ export interface DeploymentRelease {
3
+ version: 1;
4
+ id: string;
5
+ agent: string;
6
+ /**
7
+ * The model this release resolved, when the deployed environment's value file named it (`config.model` needs
8
+ * nothing here — the config ships in the image too). NON-CREDENTIAL configuration only: the manifest travels
9
+ * inside the image, which is readable by anyone who can pull it, and it is rebuilt on every deploy — both are the
10
+ * opposite of what a credential needs (see docs/design/configuration.md §8).
11
+ */
12
+ model?: string;
13
+ }
14
+ /** The manifest names a directory the container joins onto the workspace root, so the spelling is
15
+ * constrained. `deploy` asks BEFORE generating artifacts — a name `init` accepts but this rejects
16
+ * is the author's directory name, not a corrupt manifest. */
17
+ export declare function isReleaseAgentName(name: string): boolean;
18
+ export declare function parseDeploymentRelease(raw: string): DeploymentRelease;
19
+ /**
20
+ * Project a release's own declarations into the environment it was resolved FOR — the receiving half of the model
21
+ * chain, run by `prepareStartWorkspace` BEFORE the workspace's `.env` is read (so either source outranks a value
22
+ * edited on the box).
23
+ *
24
+ * `||=`: a variable the platform already holds still wins, because the deployment declares only the half of this
25
+ * environment it can. The log names whichever source actually won — the remedies differ, and a redeploy does
26
+ * nothing at all to a platform-set variable.
27
+ */
28
+ export declare function applyReleaseEnv(release: DeploymentRelease, env?: NodeJS.ProcessEnv): void;
29
+ /** A `provider/modelId` spec. The id itself may carry `/` and `~` (`baseten/zai-org/GLM-5.3`, openrouter aliases);
30
+ * resolution only ever splits on the first slash. */
31
+ export declare function isModelSpec(value: string): boolean;
32
+ /** Call while holding the workspace lease. No deployment replaces existing work outside the definition. */
33
+ export declare function applyDeploymentRelease(source: string, root: string, release: DeploymentRelease): Promise<string>;
34
+ /** An unmounted directory must never look like a new disk. */
35
+ export declare function assertStorageMounted(root: string, paths?: string[]): Promise<void>;
36
+ /** `waitSeconds` covers a restart overlapping the old process's exit; a test that wants the refusal
37
+ * passes a short one rather than waiting out the deployment default. */
38
+ export declare function leaseDeployment(metadata: string, waitSeconds?: number): Promise<() => Promise<void>>;
39
+ /**
40
+ * Prepare the deployed workspace and return its path.
41
+ *
42
+ * The lease is NOT returned: it stays held for the process lifetime, because channel activation can
43
+ * start background writers that `close()` does not drain. The kernel drops it when the process exits.
44
+ */
45
+ export declare function prepareDeployment(source: string, root: string, release: DeploymentRelease): Promise<string>;
46
+ /**
47
+ * Install the deployed agent's dependencies into the storage when it has no installed CLI yet (a fresh volume), or
48
+ * when an earlier install died part-way — its `installing` marker is still there, so the next boot redoes it.
49
+ */
50
+ export declare function installAgentDependencies(root: string, agentDir: string): Promise<void>;
@@ -0,0 +1,197 @@
1
+ /** Deployment-owned initialization; the workspace itself belongs to the running agent. */
2
+ import { cp, lstat, mkdir, readFile, rename, rm } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { closeSync, openSync } from "node:fs";
6
+ import { once } from "node:events";
7
+ import { writeFileAtomic } from "../atomic-write.js";
8
+ import { log } from "../log.js";
9
+ import { exists } from "../paths.js";
10
+ import { detectRuntime, readPackageJson } from "../runtime.js";
11
+ export const RELEASE_FILE = "fastagent.release.json";
12
+ /** The deployment's own bookkeeping on the storage: the lease, the release journal, the install marker. */
13
+ const deploymentMeta = (root) => join(root, ".deployment");
14
+ /** The manifest names a directory the container joins onto the workspace root, so the spelling is
15
+ * constrained. `deploy` asks BEFORE generating artifacts — a name `init` accepts but this rejects
16
+ * is the author's directory name, not a corrupt manifest. */
17
+ export function isReleaseAgentName(name) {
18
+ return /^[a-zA-Z0-9_-]+$/.test(name);
19
+ }
20
+ export function parseDeploymentRelease(raw) {
21
+ const r = JSON.parse(raw);
22
+ if (r?.version !== 1 ||
23
+ typeof r.id !== "string" ||
24
+ !r.id ||
25
+ typeof r.agent !== "string" ||
26
+ !isReleaseAgentName(r.agent) ||
27
+ (r.model !== undefined && !isModelSpec(r.model))) {
28
+ throw new Error("invalid deployment release manifest");
29
+ }
30
+ return r;
31
+ }
32
+ /**
33
+ * Project a release's own declarations into the environment it was resolved FOR — the receiving half of the model
34
+ * chain, run by `prepareStartWorkspace` BEFORE the workspace's `.env` is read (so either source outranks a value
35
+ * edited on the box).
36
+ *
37
+ * `||=`: a variable the platform already holds still wins, because the deployment declares only the half of this
38
+ * environment it can. The log names whichever source actually won — the remedies differ, and a redeploy does
39
+ * nothing at all to a platform-set variable.
40
+ */
41
+ export function applyReleaseEnv(release, env = process.env) {
42
+ const fromPlatform = env.FASTAGENT_MODEL;
43
+ if (release.model && !fromPlatform)
44
+ env.FASTAGENT_MODEL = release.model;
45
+ const effective = env.FASTAGENT_MODEL;
46
+ if (!effective)
47
+ return;
48
+ log.info(`[fastagent] model ${effective} — from ${fromPlatform
49
+ ? `a FASTAGENT_MODEL already set in this environment${release.model ? ", which outranks the release manifest" : ""}`
50
+ : "the release manifest (redeploy to change it)"}; editing FASTAGENT_MODEL in this workspace's .env does not override it`);
51
+ }
52
+ /** A `provider/modelId` spec. The id itself may carry `/` and `~` (`baseten/zai-org/GLM-5.3`, openrouter aliases);
53
+ * resolution only ever splits on the first slash. */
54
+ export function isModelSpec(value) {
55
+ return /^\S+\/\S+$/.test(value);
56
+ }
57
+ /** A completed staging tree is published before its journal; readers start only after recovery. */
58
+ async function finishRelease(root, pending) {
59
+ const meta = deploymentMeta(root), staged = join(meta, "staged"), previous = join(meta, "previous");
60
+ const target = pending.initial ? join(root, "base") : join(root, "base", pending.release.agent);
61
+ if (await exists(staged)) {
62
+ if (await exists(target)) {
63
+ if (pending.initial || (await exists(previous)))
64
+ throw new Error(`deployment recovery conflict at ${target}`);
65
+ await rename(target, previous);
66
+ }
67
+ await rename(staged, target);
68
+ }
69
+ else if (!(await exists(target))) {
70
+ throw new Error(`deployment recovery has neither staged nor active content: ${target}`);
71
+ }
72
+ writeFileAtomic(join(meta, "applied.json"), JSON.stringify(pending.release));
73
+ await rm(previous, { recursive: true, force: true });
74
+ await rm(join(meta, "pending.json"));
75
+ }
76
+ /** Call while holding the workspace lease. No deployment replaces existing work outside the definition. */
77
+ export async function applyDeploymentRelease(source, root, release) {
78
+ const meta = deploymentMeta(root), staged = join(meta, "staged"), pendingPath = join(meta, "pending.json");
79
+ await mkdir(meta, { recursive: true });
80
+ if (await exists(pendingPath)) {
81
+ const pending = JSON.parse(await readFile(pendingPath, "utf8"));
82
+ pending.release = parseDeploymentRelease(JSON.stringify(pending.release));
83
+ if (typeof pending.initial !== "boolean")
84
+ throw new Error("invalid pending deployment journal");
85
+ await finishRelease(root, pending);
86
+ }
87
+ const appliedPath = join(meta, "applied.json");
88
+ const applied = (await exists(appliedPath)) ? parseDeploymentRelease(await readFile(appliedPath, "utf8")) : undefined;
89
+ const workspace = join(root, "base");
90
+ if (applied?.agent !== undefined && applied.agent !== release.agent) {
91
+ throw new Error(`deployment selects ${release.agent}, but this workspace belongs to ${applied.agent}`);
92
+ }
93
+ // Logged on BOTH paths: rebuilding an image without regenerating the manifest keeps the id, and a
94
+ // deployment that silently kept the old definition looks identical to one that took the new one.
95
+ if (applied?.id === release.id) {
96
+ log.info(`[fastagent] release ${release.id} is already applied — keeping the workspace's definition`);
97
+ return workspace;
98
+ }
99
+ const initial = applied === undefined;
100
+ if (initial && (await exists(workspace)))
101
+ throw new Error(`refusing to initialize over an existing unowned workspace: ${workspace}`);
102
+ const definition = join(source, release.agent);
103
+ if (!(await exists(definition)) || !(await lstat(definition)).isDirectory())
104
+ throw new Error(`the release must contain its agent directory: ${definition}`);
105
+ log.info(initial
106
+ ? `[fastagent] seeding the workspace from release ${release.id}`
107
+ : `[fastagent] publishing release ${release.id} over base/${release.agent}`);
108
+ await rm(staged, { recursive: true, force: true });
109
+ await cp(initial ? source : join(source, release.agent), staged, { recursive: true, verbatimSymlinks: true });
110
+ const pending = { release, initial };
111
+ writeFileAtomic(pendingPath, JSON.stringify(pending));
112
+ await finishRelease(root, pending);
113
+ return workspace;
114
+ }
115
+ async function mountedPaths() {
116
+ const mounts = await readFile("/proc/self/mountinfo", "utf8");
117
+ return mounts
118
+ .split("\n")
119
+ .map((line) => line
120
+ .split(" ")[4]
121
+ ?.replace(/\\([0-7]{3})/g, (_, octal) => String.fromCharCode(Number.parseInt(octal, 8))))
122
+ .filter((path) => path !== undefined);
123
+ }
124
+ /** An unmounted directory must never look like a new disk. */
125
+ export async function assertStorageMounted(root, paths) {
126
+ if (!(paths ?? (await mountedPaths())).includes(resolve(root)))
127
+ throw new Error(`persistent storage is not mounted at ${root}`);
128
+ }
129
+ /** `waitSeconds` covers a restart overlapping the old process's exit; a test that wants the refusal
130
+ * passes a short one rather than waiting out the deployment default. */
131
+ export async function leaseDeployment(metadata, waitSeconds = 35) {
132
+ // flock locks the shared open-file description. The parent's raw fd survives GC and closes at exit.
133
+ // Never unlink this inode: another starter may already have it open.
134
+ const fd = openSync(join(metadata, "lock"), "a", 0o600);
135
+ try {
136
+ const child = spawn("flock", ["--exclusive", "--wait", String(waitSeconds), "3"], {
137
+ stdio: ["ignore", "ignore", "pipe", fd],
138
+ });
139
+ let stderr = "";
140
+ child.stderr.setEncoding("utf8").on("data", (chunk) => {
141
+ stderr += chunk;
142
+ });
143
+ const [code, signal] = await once(child, "close");
144
+ if (code !== 0)
145
+ throw new Error(`could not acquire workspace lease at ${metadata}: flock ${signal ?? code} ${stderr.trim()}`);
146
+ return async () => {
147
+ closeSync(fd);
148
+ };
149
+ }
150
+ catch (error) {
151
+ closeSync(fd);
152
+ // A custom base image without util-linux fails every boot; the bare spawn error names neither
153
+ // the binary nor why a lease needs one.
154
+ if (error.code === "ENOENT")
155
+ throw new Error(`the deployed workspace lease needs the \`flock\` binary (util-linux): ${String(error)}`);
156
+ throw error;
157
+ }
158
+ }
159
+ /**
160
+ * Prepare the deployed workspace and return its path.
161
+ *
162
+ * The lease is NOT returned: it stays held for the process lifetime, because channel activation can
163
+ * start background writers that `close()` does not drain. The kernel drops it when the process exits.
164
+ */
165
+ export async function prepareDeployment(source, root, release) {
166
+ await assertStorageMounted(root);
167
+ const meta = deploymentMeta(root);
168
+ await mkdir(meta, { recursive: true });
169
+ const unlock = await leaseDeployment(meta);
170
+ try {
171
+ return await applyDeploymentRelease(source, root, release);
172
+ }
173
+ catch (error) {
174
+ await unlock();
175
+ throw error;
176
+ }
177
+ }
178
+ /**
179
+ * Install the deployed agent's dependencies into the storage when it has no installed CLI yet (a fresh volume), or
180
+ * when an earlier install died part-way — its `installing` marker is still there, so the next boot redoes it.
181
+ */
182
+ export async function installAgentDependencies(root, agentDir) {
183
+ const installing = join(deploymentMeta(root), "installing");
184
+ // A failed install can leave the CLI link in place before its dependencies are complete.
185
+ if ((await exists(installing)) || !(await exists(join(agentDir, "node_modules/.bin/fastagent")))) {
186
+ const { runtime, hasLockfile } = detectRuntime(agentDir, await readPackageJson(agentDir));
187
+ const args = runtime === "bun" ? ["install", ...(hasLockfile ? ["--frozen-lockfile"] : [])] : [hasLockfile ? "ci" : "install"];
188
+ writeFileAtomic(installing, "");
189
+ log.info(`[fastagent] installing the agent's dependencies (${runtime} ${args.join(" ")})…`);
190
+ // stdio inherited: a five-minute install with no output reads as a hang, and the default 1 MB capture would
191
+ // kill a noisy one outright.
192
+ const [code, signal] = (await once(spawn(runtime === "bun" ? "bun" : "npm", args, { cwd: agentDir, stdio: "inherit" }), "exit"));
193
+ if (code !== 0)
194
+ throw new Error(`dependency install failed (${signal ?? `exit ${code}`})`);
195
+ await rm(installing);
196
+ }
197
+ }
@@ -1,13 +1,5 @@
1
1
  import { type ResolvedPlacement } from "./paths.ts";
2
- /**
3
- * chokidar `ignored` matcher for the narrow watch scope (true = ignore), rooted at the AGENT DIR. When
4
- * the agent sits INSIDE the workspace that means the surrounding tree never triggers a restart at all;
5
- * when the agent IS the workspace the root is that tree, and the allowlist below is what keeps the
6
- * author's own files out of scope. Ignoring a directory prunes the whole subtree, so everything outside it —
7
- * `.state/` machine state, node_modules, .git, and any file/dir the agent writes as work product —
8
- * costs no watchers and triggers no restarts. Helper code imported from OUTSIDE tools//channels/ is
9
- * out of scope by design (keep it under tools/, or restart manually) — the startup log names the set.
10
- */
2
+ /** chokidar `ignored` matcher for the narrow watch scope (true = ignore), rooted at the AGENT DIR. */
11
3
  export declare function devWatchIgnored(root: string, envFile: string): (path: string) => boolean;
12
4
  /** Spawn the dev worker and restart it on agent-dir edits; supervise its lifecycle until the process exits. */
13
5
  export declare function runDevSupervisor(placement: ResolvedPlacement, options?: {