@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,62 +1,32 @@
1
1
  /**
2
- * How a {@link Routes} table becomes a running server: the path rule, dispatch, the totality
3
- * boundary, and the node:http binding.
4
- *
5
- * Shared ground, not a deployment target — every host in `src/deploy/` runs this same process; what
6
- * differs between them is process, storage, credentials and deployment (core.md §1), none of it
7
- * here.
8
- *
9
- * Dispatch is a Map lookup rather than a router because a deployment mounts a handful of LITERAL
10
- * paths. A routing library would answer through a pattern language we do not use, and its extra
11
- * semantics (decode-before-match, HEAD fallback, wildcard precedence) would have to be predicted by
12
- * every collision check.
13
- *
14
- * The types a channel author or an embedder writes stay pure Fetch — SPEC §11 fixes the gateway
15
- * contract as `(Request) => Response`, and an embedding app may already run its own framework.
16
- * `overrideGlobalObjects: false` is part of that: the adapter otherwise swaps
17
- * `globalThis.Request`/`Response` process-wide, which breaks a channel holding a `Response`
18
- * captured before mount (it fails `instanceof` in {@link totalFetch} and is answered 500).
19
- *
20
- * Post-ACK work (a webhook channel's fire-and-forget turns) runs on this event loop and is lost on
21
- * shutdown — the accepted tradeoff until durable execution (the K axis) exists.
2
+ * How a {@link Routes} table becomes a running server: the path rule, dispatch, WHO MAY CALL IT FROM A BROWSER, the
3
+ * totality boundary, and the node:http binding.
22
4
  */
23
5
  import { serve, getRequestListener } from "@hono/node-server";
24
6
  import { log } from "../log.js";
7
+ import { refuseNonJsonBody } from "./body.js";
25
8
  import { text } from "./respond.js";
26
- /** Parse a route key: `"METHOD /path"` → `{ method, path }`, or `"/path"` → `{ path }` (any method).
27
- * An empty method (`" /x"`) parses as `""`, which {@link assertRouteKey} refuses — see there. */
9
+ /** Methods whose request carries a body a handler will parse — what the JSON gate applies to. */
10
+ const BODY_METHODS = new Set(["POST", "PUT", "PATCH"]);
11
+ /** Parse a route key: `"METHOD /path"` → `{ method, path }`, or `"/path"` → `{ path }` (any method). */
28
12
  export function parseRouteKey(key) {
29
13
  const sp = key.indexOf(" ");
30
14
  return sp === -1 ? { path: key } : { method: key.slice(0, sp).toUpperCase(), path: key.slice(sp + 1) };
31
15
  }
32
- /**
33
- * A route key is `"METHOD /path"` or `"/path"`, with a literal path.
34
- *
35
- * Every rule here exists because breaking it costs SOMEONE ELSE. Two keys naming one route, a route
36
- * under a mount, a path a URL rewrites (`/a/../x` and `/x` are one route spelled two ways, while
37
- * the conflict check compares strings) — in each case a channel goes dark and its author did
38
- * nothing wrong.
39
- *
40
- * Mistakes that only cost their own author a debugging session are NOT policed: `:id` and `*` are
41
- * literal path characters here, so such a key simply never matches, and an unusual method is the
42
- * same. Refusing those would trade a working capability for a lecture.
43
- */
16
+ /** A route key is `"METHOD /path"` or `"/path"`, with a literal path. */
44
17
  export function assertRouteKey(key, describe) {
45
18
  const { method, path } = parseRouteKey(key);
46
19
  if (method === "")
47
20
  throw new Error(describe('a leading space is not a method — write "/path" for any method'));
48
21
  if (!path.startsWith("/"))
49
22
  throw new Error(describe('must start with "/"'));
50
- // Asked of `URL` rather than by listing what it rewrites (`?`/`#`, `.`/`..`, `\`, `%2e`). A key it
51
- // rewrites is unreachable AND compares as a different string, hiding that `/a/../x` and `/x` are
52
- // one route.
23
+ // Asked of `URL` rather than by listing what it rewrites (`?`/`#`, `.`/`..`, `\`, `%2e`).
53
24
  const arrives = new URL(path, "http://x").pathname;
54
25
  if (arrives !== path) {
55
26
  throw new Error(describe(`is not the path a request would carry — that request arrives as "${arrives}"`));
56
27
  }
57
28
  }
58
- /** Do these two keys fight over the same request? Equal paths, and a method each answers; a key
59
- * with no method answers all of them. */
29
+ /** Do these two keys fight over the same request? */
60
30
  export function routeKeysConflict(a, b) {
61
31
  const ka = parseRouteKey(a);
62
32
  const kb = parseRouteKey(b);
@@ -64,28 +34,97 @@ export function routeKeysConflict(a, b) {
64
34
  return false;
65
35
  return ka.method === undefined || kb.method === undefined || ka.method === kb.method;
66
36
  }
67
- /** Same status and headers, no content (RFC 9110's HEAD). The discarded body is cancelled, or a
68
- * streaming producer keeps running with no reader. Shared with the control plane, which answers
69
- * HEAD too. */
37
+ /** Same status and headers, no content (RFC 9110's HEAD). */
70
38
  export function withoutBody(res) {
71
39
  void res.body?.cancel().catch(() => { });
72
40
  return new Response(null, { status: res.status, statusText: res.statusText, headers: res.headers });
73
41
  }
74
- /** Is `path` inside `prefix`? Segment-wise, so `/controlled` is not inside `/control`. */
75
42
  export function pathUnderPrefix(path, prefix) {
76
43
  return path === prefix || path.startsWith(`${prefix}/`);
77
44
  }
78
45
  /**
79
- * Compose a {@link Routes} table and its {@link PrefixMount}s into one handler.
46
+ * THE cross-origin policy, in one place, for the routes that authenticate nobody.
47
+ *
48
+ * THIS IS AN API, so the default is an API's: `*`, unconditionally. Origin is not access control — what makes any
49
+ * API safe to call from a page is a credential the page does not have, which makes the caller's origin irrelevant.
50
+ * We have no credential, and the boundary that replaces it — who may reach this port — belongs to the deployment,
51
+ * the same rule that removed the bearer token. Deciding for the operator from a signal this process can observe
52
+ * (which address it bound) is not that rule: a wildcard bind is what a container needs, not something the operator
53
+ * said, and a loopback one is `dev`'s default, not a claim about who may call.
80
54
  *
81
- * Refuses at assembly anything that could not receive a request: a key naming the same route as
82
- * another (`"/x"` and `"GET /x"`), a route inside a mount, two mounts claiming the same ground. A
83
- * channel must never go dark unannounced.
55
+ * WHAT `*` GRANTS, stated because it is the default and nobody opts into it: any page the user's browser visits can
56
+ * call this port cross-origin and read the reply — a turn with the agent's full tool authority, and with
57
+ * `sessionControl: true`, reading and deleting sessions. On a published port that is nothing an attacker could not
58
+ * already curl. On a loopback `dev` serve it is the whole attack path, and this is the posture the decision costs:
59
+ * `announceControl` says so at every boot, and `http.cors` is the one way to take it back.
84
60
  *
85
- * 404 and 405 stay distinct: a remote client reads 404 as "this serve predates the route" (version
86
- * skew) rather than as a fault.
61
+ * `allow` is `http.cors`, and when it is set it REPLACES the default rather than adding to it: `["*"]` is the
62
+ * default said out loud, a list of origins is the narrowing.
63
+ *
64
+ * An origin that is not allowed is simply not ANSWERED — no headers, no refusal. That is safe only because a route
65
+ * that authenticates nobody also refuses a body that is not `application/json` (channels/body.ts): without that, a
66
+ * cross-origin `text/plain` POST is a CORS simple request, sent with no preflight, and withholding the headers
67
+ * would only stop the page from reading a turn that had already run.
87
68
  */
88
- export function router(routes, mounts = []) {
69
+ function allowedOrigin(origin, allow) {
70
+ if (allow.length === 0)
71
+ return "*";
72
+ return allow.includes("*") ? "*" : allow.includes(origin) ? origin : undefined;
73
+ }
74
+ /**
75
+ * Refuse a cross-origin allow-list that cannot match anything.
76
+ *
77
+ * ONE check for BOTH ways in: `http.cors` in a config file, and `corsOrigins` handed to `mountAgentService` by an
78
+ * embedder. `allowedOrigin` compares exact strings, so `"https://app.example.com/"` — one trailing slash — never
79
+ * matches and the front end is refused with nothing pointing at the rule. A rule that silently matches nothing is
80
+ * worse than one that refuses to load.
81
+ */
82
+ export function assertCorsOrigins(origins, where) {
83
+ if (!Array.isArray(origins) || origins.some((o) => typeof o !== "string" || o === "")) {
84
+ throw new Error(`${where} must be an array of origin strings (e.g. ["https://app.example.com"])`);
85
+ }
86
+ // An EMPTY list is the one spelling that means the OPPOSITE of what it looks like. `allowedOrigin` reads "nothing
87
+ // configured" as the `*` default and cannot tell that apart from a list someone wrote as empty, so `http.cors: []`
88
+ // — the obvious way to write "no page may call this" — would grant the widest policy there is, and silence the
89
+ // boot warning that names it (`announceControl` treats a set list as the operator having chosen the origins).
90
+ if (origins.length === 0) {
91
+ throw new Error(`${where} is empty — list at least one origin, or write ["*"] to say the default out loud`);
92
+ }
93
+ for (const origin of origins) {
94
+ if (origin === "*")
95
+ continue;
96
+ let parsed;
97
+ try {
98
+ parsed = new URL(origin);
99
+ }
100
+ catch {
101
+ // `new URL` throwing IS the answer here ("not a URL at all"); rethrown as the rule that cannot load.
102
+ throw new Error(`${where} entry ${JSON.stringify(origin)} is not an origin ("https://host[:port]")`);
103
+ }
104
+ // An ORIGIN, not a URL with a path: `new URL(...).origin` is what a browser sends.
105
+ if (parsed.origin !== origin) {
106
+ throw new Error(`${where} entry ${JSON.stringify(origin)} is not the origin a browser sends — use ${JSON.stringify(parsed.origin)}`);
107
+ }
108
+ }
109
+ }
110
+ /**
111
+ * Compose the routes that AUTHENTICATE NOBODY, the self-verifying channel routes, and the {@link PrefixMount}s
112
+ * into one handler.
113
+ *
114
+ * That is the axis, not authorship. Two things follow from it and nothing else does: whether a browser may drive
115
+ * the route, and whether its body must declare JSON. A channel route is exempt from both because it checks its
116
+ * platform's signature inside itself — a Telegram webhook is public on purpose. Ours check nothing, so the router
117
+ * checks for them.
118
+ *
119
+ * TWO TABLES, not one table plus a list of which keys are which, so the three readers (browser reachability, the JSON
120
+ * body gate, the startup report) cannot answer differently.
121
+ *
122
+ * CAVEAT, stated because we cannot enforce it: a CUSTOM channel that verifies nothing lands in `selfVerifying`
123
+ * anyway and gets neither guard. That is decision B (docs/design/session-control.md §14) and it belongs to the
124
+ * channel's author, who owns the credential the platform issued — but it is an assumption here, not a property.
125
+ */
126
+ export function router(surface) {
127
+ const { unverified = {}, selfVerifying = {}, mounts = [], corsOrigins = [] } = surface;
89
128
  for (const [i, mount] of mounts.entries()) {
90
129
  assertRouteKey(mount.prefix, (problem) => `mount prefix "${mount.prefix}" is invalid — ${problem}`);
91
130
  if (mount.prefix === "/") {
@@ -103,7 +142,8 @@ export function router(routes, mounts = []) {
103
142
  }
104
143
  const byKey = new Map();
105
144
  const paths = new Set();
106
- for (const [key, handler] of Object.entries(routes)) {
145
+ const unguarded = new Set();
146
+ for (const [key, handler] of [...Object.entries(unverified), ...Object.entries(selfVerifying)]) {
107
147
  assertRouteKey(key, (problem) => `route "${key}" is not a valid route key — ${problem}`);
108
148
  const { path } = parseRouteKey(key);
109
149
  for (const mount of mounts) {
@@ -115,16 +155,52 @@ export function router(routes, mounts = []) {
115
155
  if (shadowed) {
116
156
  throw new Error(`route "${key}" conflicts with "${shadowed}" — one of them would never receive a request`);
117
157
  }
118
- // Stored normalised: `parseRouteKey` upper-cases the method, so `"get /x"` validates under
119
- // `GET` and would otherwise be looked up under a name nothing stores.
158
+ // Stored normalised: `parseRouteKey` upper-cases the method, so `"get /x"` validates under `GET` and would
159
+ // otherwise be looked up under a name nothing stores.
120
160
  const { method } = parseRouteKey(key);
121
- byKey.set(method ? `${method} ${path}` : path, handler);
161
+ const normalised = method ? `${method} ${path}` : path;
162
+ byKey.set(normalised, handler);
122
163
  paths.add(path);
164
+ if (key in unverified)
165
+ unguarded.add(normalised);
123
166
  }
167
+ /** Does this route authenticate nobody? Every mount does (the control plane), plus what we registered ourselves. */
168
+ const authenticatesNobody = (method, path) => mounts.some((mount) => pathUnderPrefix(path, mount.prefix)) ||
169
+ unguarded.has(`${method} ${path}`) ||
170
+ unguarded.has(path) ||
171
+ // The router answers HEAD from a GET route, so the question follows it.
172
+ (method === "HEAD" && unguarded.has(`GET ${path}`));
124
173
  return (req) => {
125
174
  // `URL` normalises the path (`/a/../x` → `/x`) and drops query/fragment.
126
175
  const path = new URL(req.url).pathname;
176
+ const origin = req.headers.get("origin");
177
+ // A preflight asks ABOUT a method; ownership is that method's, not `OPTIONS`'s (nothing registers OPTIONS).
178
+ const asking = req.method === "OPTIONS"
179
+ ? (req.headers.get("access-control-request-method")?.toUpperCase() ?? "OPTIONS")
180
+ : req.method;
181
+ // A BROWSER request against a route that authenticates nobody. A non-browser client sends no `Origin` and is
182
+ // untouched by any of this.
183
+ const owned = authenticatesNobody(asking, path);
184
+ const fromPage = origin !== null && owned;
185
+ // Decided BEFORE dispatch, so a 404, a 405 and a handler's own reply all leave with the same verdict — a browser
186
+ // that cannot read the 405 gets an opaque network error instead of the reason.
187
+ const cors = fromPage ? allowedOrigin(origin, corsOrigins) : undefined;
127
188
  const answer = () => {
189
+ // The gate that makes "an origin we do not allow is simply not answered" safe: a POST carrying `text/plain`
190
+ // is a CORS simple request, sent with no preflight at all, so a route that authenticates nobody must refuse
191
+ // the body itself. HERE, over the whole unguarded surface, rather than inside each handler — the next route
192
+ // we add is covered by existing. A self-verifying channel is exempt: Slack posts urlencoded, and a page
193
+ // cannot forge its signature anyway.
194
+ if (owned && BODY_METHODS.has(req.method)) {
195
+ const wrongType = refuseNonJsonBody(req);
196
+ if (wrongType)
197
+ return wrongType;
198
+ }
199
+ // The preflight is the ROUTER's to answer: it names a method the route table does not register, so leaving it
200
+ // to the routes below is a 405 with no CORS headers, which is a browser client that cannot call a route that
201
+ // works.
202
+ if (req.method === "OPTIONS" && cors !== undefined)
203
+ return new Response(null, { status: 204 });
128
204
  for (const mount of mounts)
129
205
  if (pathUnderPrefix(path, mount.prefix))
130
206
  return mount.handler(req);
@@ -138,22 +214,36 @@ export function router(routes, mounts = []) {
138
214
  }
139
215
  return paths.has(path) ? text("method not allowed\n", 405) : text("not found\n", 404);
140
216
  };
141
- // ONE exit, so the HEAD rule holds for every reply — a mount's, a route's, the GET fallback's,
142
- // and the 404/405 this router writes itself. RFC 9110: HEAD is GET without the content.
217
+ /**
218
+ * ONE exit, so the HEAD rule and the CORS verdict hold for every reply — a mount's, a route's, the GET
219
+ * fallback's, and the 404/405 this router writes itself.
220
+ */
221
+ const finish = (res) => {
222
+ const out = req.method === "HEAD" ? withoutBody(res) : res;
223
+ // `vary` whether or not the origin was allowed: the answer DEPENDS on the request's origin either way, and a
224
+ // cache that does not know it would serve one caller's verdict to another.
225
+ if (owned)
226
+ out.headers.append("vary", "origin");
227
+ if (cors === undefined)
228
+ return out;
229
+ out.headers.set("access-control-allow-origin", cors);
230
+ // Echoed for the same reason the methods are: what a caller needs is not ours to enumerate. A gateway in front
231
+ // of this port demands its own header (`connectSessionControl({ fetchFn })` is how a client sends one), and a
232
+ // fixed list turns that preflight into a 204 the browser then refuses to act on. The default names the two a
233
+ // client of ours always needs, for a preflight that asked about nothing.
234
+ out.headers.set("access-control-allow-headers", req.headers.get("access-control-request-headers") ?? "authorization, content-type");
235
+ // The method the preflight ASKED about, not a table lookup: a mount owns a prefix and does not publish which
236
+ // methods each path under it serves. Naming one the path does not serve costs nothing now that the 405 it
237
+ // produces carries these same headers.
238
+ const asked = req.headers.get("access-control-request-method")?.toUpperCase();
239
+ out.headers.set("access-control-allow-methods", [...new Set([asked, "OPTIONS"].filter(Boolean))].join(", "));
240
+ return out;
241
+ };
143
242
  const res = answer();
144
- if (req.method !== "HEAD")
145
- return res;
146
- return res instanceof Promise ? res.then(withoutBody) : withoutBody(res);
243
+ return res instanceof Promise ? res.then(finish) : finish(res);
147
244
  };
148
245
  }
149
- /**
150
- * The totality boundary every serving path shares. `loadChannels` imports arbitrary author code, so
151
- * a channel that throws — or forgets to return — must become a logged 500 rather than escape as an
152
- * unhandled rejection. Both `nodeListener` and `serveNode` route through it.
153
- *
154
- * The message stays internal: an adapter's default error page would echo exception text (a stack, a
155
- * path, a key inside an error string) to whoever made the request. Visibility is the LOG's job.
156
- */
246
+ /** The totality boundary every serving path shares. */
157
247
  function totalFetch(handler) {
158
248
  return async (req) => {
159
249
  try {
@@ -168,30 +258,15 @@ function totalFetch(handler) {
168
258
  }
169
259
  };
170
260
  }
171
- /**
172
- * Has upstream middleware already drained the request body?
173
- *
174
- * Node's `req` is one-shot, so a body parser mounted ahead of this route consumes it — a property of
175
- * the Node/Fetch seam, not of this adapter. What is avoidable is the diagnosis: undici answers
176
- * `TypeError: Body is unusable`, naming neither the cause nor the fix, and for a webhook channel
177
- * that reads as "the integration is broken and the platform keeps retrying".
178
- *
179
- * Answers only when CERTAIN — a positive `content-length`. `readableEnded` alone is true for any
180
- * request something upstream merely touched, and an empty chunked body is legal, arrives drained,
181
- * and is indistinguishable from an eaten one. Guessing there rejects valid requests, making this
182
- * guard the outage it explains.
183
- */
261
+ /** Has upstream middleware already drained the request body? */
184
262
  function bodyAlreadyRead(req) {
185
263
  const length = Number(req.headers["content-length"]);
186
264
  return Number.isFinite(length) && length > 0 && req.readableEnded;
187
265
  }
188
- /** The node:http adapter for a Fetch handler — the embedded server uses it, and an embedder mounting
189
- * fastagent on its OWN node:http server can too.
190
- *
191
- * Takes the same {@link ChannelHandler} `serveNode` does, so both doors accept the same thing: a
192
- * handler may answer synchronously, and `router()` returns exactly that. Requiring a Promise here
193
- * made the most natural mount — `nodeListener(router(routes))`, the whole agent on the app's own
194
- * server — a type error. */
266
+ /**
267
+ * The node:http adapter for a Fetch handler — the embedded server uses it, and an embedder mounting fastagent on its
268
+ * OWN node:http server can too.
269
+ */
195
270
  export function nodeListener(handler) {
196
271
  const listener = getRequestListener(totalFetch(handler), { overrideGlobalObjects: false });
197
272
  return (req, res) => {
@@ -206,12 +281,6 @@ export function nodeListener(handler) {
206
281
  listener(req, res);
207
282
  };
208
283
  }
209
- /**
210
- * Serve `handler` on a Node HTTP server. Thin mechanism: bind, report the port, let the caller stop
211
- * accepting or force-close active connections — no logging/signals/exit (the CLI owns those).
212
- * `listening` resolves with the bound port (useful for port 0) or rejects on a bind error.
213
- * `host` is the bind address; unset means all interfaces (what containers need).
214
- */
215
284
  export function serveNode(handler, options) {
216
285
  let onListening;
217
286
  let onBindError;
@@ -219,20 +288,15 @@ export function serveNode(handler, options) {
219
288
  onListening = resolve;
220
289
  onBindError = reject;
221
290
  });
222
- // serve() types its return as the union of every server it CAN build (incl. http2). We never pass
223
- // a createServer/http2 option, so it is always node:http's Server — the one carrying
224
- // closeAllConnections, which the caller-owned force-close depends on.
291
+ // serve() types its return as the union of every server it CAN build (incl. http2).
225
292
  const server = serve({
226
293
  fetch: totalFetch(handler),
227
294
  overrideGlobalObjects: false,
228
295
  port: options.port,
229
296
  ...(options.host !== undefined ? { hostname: options.host } : {}),
230
297
  }, (info) => {
231
- // Detach before resolving: this listener answers the BIND, and leaving it attached would let
232
- // a later runtime error call reject() on a settled promise — swallowed, with nothing raised
233
- // anywhere. Detached, an error after bind is an unhandled 'error' event, which is loud.
234
- // (Not reachable from a test without forging an event on a server this function does not
235
- // expose; the bind-failure half below is covered.)
298
+ // Detach before resolving: this listener answers the BIND, and leaving it attached would let a later runtime
299
+ // error call reject() on a settled promise.
236
300
  server.off("error", onBindError);
237
301
  onListening(info.port);
238
302
  });
@@ -10,37 +10,16 @@ interface SlackErrorShape {
10
10
  export declare class SlackConfigApiError extends Error {
11
11
  readonly code: string;
12
12
  readonly status: number;
13
- /** Kept, not just formatted into the message: which FIELD Slack rejected is how a caller tells a
14
- * not-yet-reachable URL from a malformed one, and a message match cannot see the difference. */
13
+ /** Kept, not just formatted into the message. */
15
14
  readonly errors: {
16
15
  message?: string;
17
16
  pointer?: string;
18
17
  }[];
19
18
  constructor(method: string, status: number, data: SlackErrorShape, fallback: string);
20
19
  }
21
- /**
22
- * Whether a manifest call failed because Slack could not verify the `request_url` it carries — the
23
- * platform's own readiness verdict on a freshly minted public URL, and the reason no local `/health`
24
- * poll precedes these calls (#421). Slack validates the manifest BEFORE acting on it, so a call that
25
- * ends here changed nothing and is safe to repeat.
26
- *
27
- * Read from the rejected FIELD (`invalid_manifest` + {@link rejectsRequestUrl}), not from `request_url`
28
- * appearing anywhere in the formatted string: that also matched errors which merely mention the field.
29
- *
30
- * KNOWN IMPRECISION: this cannot separate "not reachable yet" from "this URL is unacceptable" — both
31
- * arrive the same way, and the wording that would tell them apart has not been observed against the
32
- * real API, so guessing at it would risk never retrying at all. A permanently bad URL therefore costs
33
- * the full budget before it is reported. Every URL here is a tunnel's or a host's, so it is well-formed
34
- * https by construction; narrow this the day a real rejection is captured.
35
- */
20
+ /** Whether a manifest call failed because Slack could not verify the `request_url` it carries. */
36
21
  export declare function isSlackRequestUrlUnverified(error: unknown): boolean;
37
- /**
38
- * Whether a manifest write was refused because the app has token rotation on, which Slack never lets
39
- * off again — the refusal every app created by a release up to 0.20 meets, since this release sends
40
- * `token_rotation_enabled: false`. Slack documents field rejections as `invalid_manifest` plus an
41
- * `errors[]` of message + pointer; the bare code was what a real refusal once showed. Both shapes are
42
- * read: a miss here sends the operator to repair configuration tokens that are not the problem.
43
- */
22
+ /** Whether a manifest write was refused because the app has token rotation on, which Slack never lets off again. */
44
23
  export declare function isSlackRotationLocked(error: unknown): boolean;
45
24
  /** The upgrade every rotating-app refusal points at. */
46
25
  export declare const SLACK_ROTATING_APP_UPGRADE = "this app has token rotation on, which this release no longer uses and Slack cannot turn off \u2014 create a new app (docs/slack.md \u2192 \"Upgrading from a rotating-token app\")";
@@ -51,9 +30,7 @@ export interface SlackAppCredentials {
51
30
  signingSecret: string;
52
31
  oauthAuthorizeUrl?: string;
53
32
  }
54
- /** Single-attempt BY DESIGN: an ambiguous response may already have created the app, so this never
55
- * retries itself. The one repeatable failure — a `request_url` Slack could not verify, which it
56
- * rejects before creating anything — is retried by the caller ({@link isSlackRequestUrlUnverified}). */
33
+ /** Single-attempt BY DESIGN: an ambiguous response may already have created the app, so this never retries itself. */
57
34
  export declare function createSlackApp(configToken: string, manifest: SlackAppManifest, options?: {
58
35
  apiBaseUrl?: string;
59
36
  fetch?: typeof fetch;
@@ -1,19 +1,12 @@
1
1
  const SLACK_API = "https://slack.com/api";
2
2
  const REQUEST_TIMEOUT_MS = 30_000;
3
- /** One `errors[]` entry that means "Slack could not verify this URL", by either signal it may carry.
4
- * The pointer is the precise one but an INFERENCE — the only reply captured from the real API is the
5
- * PERMANENT shape error, which reports on the parent pointer (`/settings/event_subscriptions`). If the
6
- * unverifiable case reports there too, a pointer-only check would retry zero times and gate the deploy
7
- * immediately, which is the failure this whole path exists to prevent. So a message about VERIFICATION
8
- * (challenge wording included) counts as well, while one that merely names the field does not — the
9
- * captured permanent error says "Event Subscription requires a Request URL". */
3
+ /** One `errors[]` entry that means "Slack could not verify this URL", by either signal it may carry. */
10
4
  const rejectsRequestUrl = (e) => e.pointer?.endsWith("/request_url") === true ||
11
5
  (/request.?url/i.test(e.message ?? "") && /verif|challenge/i.test(e.message ?? ""));
12
6
  export class SlackConfigApiError extends Error {
13
7
  code;
14
8
  status;
15
- /** Kept, not just formatted into the message: which FIELD Slack rejected is how a caller tells a
16
- * not-yet-reachable URL from a malformed one, and a message match cannot see the difference. */
9
+ /** Kept, not just formatted into the message. */
17
10
  errors;
18
11
  constructor(method, status, data, fallback) {
19
12
  const code = data.error ?? fallback;
@@ -27,31 +20,11 @@ export class SlackConfigApiError extends Error {
27
20
  this.errors = data.errors ?? [];
28
21
  }
29
22
  }
30
- /**
31
- * Whether a manifest call failed because Slack could not verify the `request_url` it carries — the
32
- * platform's own readiness verdict on a freshly minted public URL, and the reason no local `/health`
33
- * poll precedes these calls (#421). Slack validates the manifest BEFORE acting on it, so a call that
34
- * ends here changed nothing and is safe to repeat.
35
- *
36
- * Read from the rejected FIELD (`invalid_manifest` + {@link rejectsRequestUrl}), not from `request_url`
37
- * appearing anywhere in the formatted string: that also matched errors which merely mention the field.
38
- *
39
- * KNOWN IMPRECISION: this cannot separate "not reachable yet" from "this URL is unacceptable" — both
40
- * arrive the same way, and the wording that would tell them apart has not been observed against the
41
- * real API, so guessing at it would risk never retrying at all. A permanently bad URL therefore costs
42
- * the full budget before it is reported. Every URL here is a tunnel's or a host's, so it is well-formed
43
- * https by construction; narrow this the day a real rejection is captured.
44
- */
23
+ /** Whether a manifest call failed because Slack could not verify the `request_url` it carries. */
45
24
  export function isSlackRequestUrlUnverified(error) {
46
25
  return (error instanceof SlackConfigApiError && error.code === "invalid_manifest" && error.errors.some(rejectsRequestUrl));
47
26
  }
48
- /**
49
- * Whether a manifest write was refused because the app has token rotation on, which Slack never lets
50
- * off again — the refusal every app created by a release up to 0.20 meets, since this release sends
51
- * `token_rotation_enabled: false`. Slack documents field rejections as `invalid_manifest` plus an
52
- * `errors[]` of message + pointer; the bare code was what a real refusal once showed. Both shapes are
53
- * read: a miss here sends the operator to repair configuration tokens that are not the problem.
54
- */
27
+ /** Whether a manifest write was refused because the app has token rotation on, which Slack never lets off again. */
55
28
  export function isSlackRotationLocked(error) {
56
29
  if (!(error instanceof SlackConfigApiError))
57
30
  return false;
@@ -91,9 +64,7 @@ async function slackJson(method, body, options = {}) {
91
64
  }
92
65
  return data;
93
66
  }
94
- /** Single-attempt BY DESIGN: an ambiguous response may already have created the app, so this never
95
- * retries itself. The one repeatable failure — a `request_url` Slack could not verify, which it
96
- * rejects before creating anything — is retried by the caller ({@link isSlackRequestUrlUnverified}). */
67
+ /** Single-attempt BY DESIGN: an ambiguous response may already have created the app, so this never retries itself. */
97
68
  export async function createSlackApp(configToken, manifest, options = {}) {
98
69
  const data = await slackJson("apps.manifest.create", { manifest: JSON.stringify(manifest) }, { ...options, token: configToken });
99
70
  const credentials = data.credentials;
@@ -1,7 +1,6 @@
1
1
  /**
2
- * Slack's half of the shared context buffer (mechanics + consume protocol: ../kit/context-buffer.ts):
3
- * the entry shape, its fold-line rendering, and buffered-file selection. Durable, bounded
4
- * unsummoned Slack discussion folded into the next answered turn in the same place.
2
+ * Slack's half of the shared context buffer (mechanics + consume protocol: ../kit/context-buffer.ts): the entry shape,
3
+ * its fold-line rendering, and buffered-file selection.
5
4
  */
6
5
  import { type ContextBuffer } from "../kit/context-buffer.ts";
7
6
  export interface SlackBufferEntry {
@@ -1,9 +1,8 @@
1
1
  /**
2
- * Slack's half of the shared context buffer (mechanics + consume protocol: ../kit/context-buffer.ts):
3
- * the entry shape, its fold-line rendering, and buffered-file selection. Durable, bounded
4
- * unsummoned Slack discussion folded into the next answered turn in the same place.
2
+ * Slack's half of the shared context buffer (mechanics + consume protocol: ../kit/context-buffer.ts): the entry shape,
3
+ * its fold-line rendering, and buffered-file selection.
5
4
  */
6
- import { BUFFER_ATTACH_MAX, createContextBuffer as createGenericContextBuffer, } from "../kit/context-buffer.js";
5
+ import { capBufferedRefs, createContextBuffer as createGenericContextBuffer, } from "../kit/context-buffer.js";
7
6
  function bufferLine(entry) {
8
7
  const meta = [`msg ${entry.messageId}`, entry.replyTo ? `thread root ${entry.replyTo}` : undefined]
9
8
  .filter(Boolean)
@@ -21,7 +20,8 @@ export function collectSlackBufferedFiles(consumed, primaryIds) {
21
20
  files.push({ id, from: entry.sender, messageId: entry.messageId });
22
21
  }
23
22
  }
24
- return { files: files.slice(-BUFFER_ATTACH_MAX), skipped: Math.max(0, files.length - BUFFER_ATTACH_MAX) };
23
+ const capped = capBufferedRefs(files);
24
+ return { files: capped.kept, skipped: capped.skipped };
25
25
  }
26
26
  function isEntry(value) {
27
27
  const entry = value;
@@ -1,5 +1,7 @@
1
1
  /** Resolve Slack file IDs at dequeue, then stream one engine-neutral Agent turn. */
2
2
  import type { Agent, AgentEvent } from "../../agent.ts";
3
+ import type * as Stream from "effect/Stream";
4
+ import type { PortFailure } from "../../effect-port.ts";
3
5
  import { type BusyRetry } from "../kit/invoke-turn-kit.ts";
4
6
  import type { SlackBufferedFileRef } from "./context-buffer.ts";
5
7
  import { type SlackApi } from "./slack-api.ts";
@@ -16,4 +18,4 @@ export interface SlackTurnAttachments {
16
18
  skipped: number;
17
19
  };
18
20
  }
19
- export declare function invokeSlackTurn(agent: Agent, session: string, text: string, transport: SlackTurnTransport, attachments: SlackTurnAttachments, onCompleted?: () => void, busyRetry?: BusyRetry): AsyncIterable<AgentEvent>;
21
+ export declare function slackTurnStream(agent: Agent, session: string, text: string, transport: SlackTurnTransport, attachments: SlackTurnAttachments, busyRetry?: BusyRetry): Stream.Stream<AgentEvent, PortFailure>;
@@ -1,5 +1,4 @@
1
- import { log } from "../../log.js";
2
- import { DEFAULT_BUSY_RETRY, attachedFilesManifest, attributedFileName, backgroundImagesManifest, missingAttachmentsNote, streamTurnWithBusyRetry, } from "../kit/invoke-turn-kit.js";
1
+ import { DEFAULT_BUSY_RETRY, attachedFilesManifest, attributedFileName, backgroundImagesManifest, loadBackground, missingAttachmentsNote, turnStream, } from "../kit/invoke-turn-kit.js";
3
2
  import { SlackApiError } from "./slack-api.js";
4
3
  const MARKDOWN_INSTRUCTION = "\n\n(Format your reply as standard Markdown. Slack renders it natively. Do not use HTML or Slack control-mention syntax such as <!here>, <!channel>, or <!everyone>.)";
5
4
  async function resolveFile(transport, fileId) {
@@ -20,25 +19,17 @@ async function resolveInputs(transport, attachments) {
20
19
  }
21
20
  const backgroundImages = [];
22
21
  const backgroundFiles = [];
23
- let lost = 0;
24
- const results = await Promise.allSettled(attachments.buffered.files.map(async (ref) => ({
25
- ref,
26
- resolved: await resolveFile(transport, ref.id),
27
- })));
28
- for (const result of results) {
29
- if (result.status === "fulfilled") {
30
- const { ref, resolved } = result.value;
31
- if (resolved.image)
32
- backgroundImages.push({ image: resolved.image, ref });
33
- if (resolved.file)
34
- backgroundFiles.push({ file: resolved.file, ref });
35
- }
36
- else {
37
- lost++;
38
- log.warn(`${transport.label} could not load an earlier (buffered) Slack file: ${String(result.reason)}`);
39
- }
22
+ const background = await loadBackground(attachments.buffered.files, (ref) => resolveFile(transport, ref.id), {
23
+ label: transport.label,
24
+ what: "Slack file",
25
+ });
26
+ for (const { ref, value } of background.loaded) {
27
+ if (value.image)
28
+ backgroundImages.push({ image: value.image, ref });
29
+ if (value.file)
30
+ backgroundFiles.push({ file: value.file, ref });
40
31
  }
41
- const missingNote = missingAttachmentsNote(lost + attachments.buffered.skipped);
32
+ const missingNote = missingAttachmentsNote(background.lost + attachments.buffered.skipped);
42
33
  const imageManifest = backgroundImagesManifest(images.length, backgroundImages.map(({ ref }) => ref));
43
34
  const allFiles = [
44
35
  ...files,
@@ -53,18 +44,18 @@ async function resolveInputs(transport, attachments) {
53
44
  promptSuffix: `${missingNote}${imageManifest}${attachedFilesManifest(allFiles)}`,
54
45
  };
55
46
  }
56
- export async function* invokeSlackTurn(agent, session, text, transport, attachments, onCompleted, busyRetry = DEFAULT_BUSY_RETRY) {
57
- let resolved;
58
- try {
59
- resolved = await resolveInputs(transport, attachments);
60
- }
61
- catch (error) {
62
- // transient (network, exhausted 429 retries, Slack 5xx) is worth re-sending; an access or shape
63
- // error reads the same every time, and "try again in a moment" is the wrong thing to tell the user
64
- const retryable = error instanceof SlackApiError && (error.status === 0 || error.status === 429 || error.status >= 500);
65
- yield { type: "failed", details: `could not load Slack attachment: ${String(error)}`, retryable };
66
- return;
67
- }
68
- const prompt = { text: `${text}${resolved.promptSuffix}${MARKDOWN_INSTRUCTION}`, images: resolved.images };
69
- yield* streamTurnWithBusyRetry(agent, { session }, prompt, { label: transport.label, onCompleted, busyRetry });
47
+ export function slackTurnStream(agent, session, text, transport, attachments, busyRetry = DEFAULT_BUSY_RETRY) {
48
+ return turnStream({
49
+ agent,
50
+ label: transport.label,
51
+ busyRetry,
52
+ resolve: () => resolveInputs(transport, attachments),
53
+ turn: (resolved) => ({
54
+ scope: { session },
55
+ prompt: { text: `${text}${resolved.promptSuffix}${MARKDOWN_INSTRUCTION}`, images: resolved.images },
56
+ }),
57
+ // Slack says which failures are transient; anything else (a deleted file, a missing scope) will
58
+ // fail the same way on redelivery.
59
+ retryableLoadFailure: (cause) => cause instanceof SlackApiError && (cause.status === 0 || cause.status === 429 || cause.status >= 500),
60
+ });
70
61
  }
@@ -64,9 +64,7 @@ export function buildSlackManifest(input) {
64
64
  org_deploy_enabled: false,
65
65
  socket_mode_enabled: false,
66
66
  // Off, and Slack makes the alternative permanent: an app with rotation on can never turn it off
67
- // (`cannot_disable_once_enabled`, verified against the API). Rotation would ship the refresh token
68
- // and the client secret to every host beside the access token — the same blast radius as one
69
- // long-lived bot token, plus a 12-hour refresh lineage to keep single and durable.
67
+ // (`cannot_disable_once_enabled`, verified against the API).
70
68
  token_rotation_enabled: false,
71
69
  },
72
70
  };