@fastagent-sh/fastagent 0.29.0 → 0.30.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 (312) hide show
  1. package/README.md +9 -4
  2. package/dist/atomic-write.d.ts +4 -5
  3. package/dist/atomic-write.js +4 -5
  4. package/dist/authoring.d.ts +32 -15
  5. package/dist/authoring.js +147 -127
  6. package/dist/channels/agentcore-protocol.d.ts +8 -3
  7. package/dist/channels/agentcore-protocol.js +3 -1
  8. package/dist/channels/agentcore-service.d.ts +1 -0
  9. package/dist/channels/agentcore-service.js +14 -4
  10. package/dist/channels/agentcore.d.ts +9 -0
  11. package/dist/channels/agentcore.js +29 -4
  12. package/dist/channels/context.d.ts +2 -0
  13. package/dist/channels/context.js +5 -1
  14. package/dist/channels/feishu/feishu-api.d.ts +2 -1
  15. package/dist/channels/feishu/feishu.d.ts +0 -3
  16. package/dist/channels/feishu/feishu.js +10 -10
  17. package/dist/channels/feishu/register-app.js +3 -5
  18. package/dist/channels/feishu/scaffold/channel.ts +32 -34
  19. package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
  20. package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
  21. package/dist/channels/feishu/setup-mode.d.ts +2 -12
  22. package/dist/channels/feishu/setup-mode.js +2 -22
  23. package/dist/channels/feishu/setup.d.ts +12 -0
  24. package/dist/channels/feishu/setup.js +197 -0
  25. package/dist/channels/kit/abortable.d.ts +8 -0
  26. package/dist/channels/kit/abortable.js +22 -0
  27. package/dist/channels/lark/lark.d.ts +0 -3
  28. package/dist/channels/lark/lark.js +0 -5
  29. package/dist/channels/lark/scaffold/channel.ts +33 -35
  30. package/dist/channels/lark/scaffold/lark-send.ts +5 -5
  31. package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
  32. package/dist/channels/lark/setup.d.ts +9 -0
  33. package/dist/channels/lark/setup.js +57 -0
  34. package/dist/channels/setup.d.ts +112 -0
  35. package/dist/channels/setup.js +31 -0
  36. package/dist/channels/slack/history.js +1 -1
  37. package/dist/channels/slack/manifest.d.ts +17 -7
  38. package/dist/channels/slack/manifest.js +12 -16
  39. package/dist/channels/slack/scaffold/channel.ts +44 -30
  40. package/dist/channels/slack/scaffold/slack-send.ts +8 -8
  41. package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
  42. package/dist/channels/slack/setup.d.ts +7 -0
  43. package/dist/channels/slack/setup.js +119 -0
  44. package/dist/channels/slack/slack-api.d.ts +2 -0
  45. package/dist/channels/slack/slack-api.js +4 -0
  46. package/dist/channels/slack/slack.d.ts +21 -2
  47. package/dist/channels/slack/slack.js +398 -338
  48. package/dist/channels/slack/socket.d.ts +57 -0
  49. package/dist/channels/slack/socket.js +305 -0
  50. package/dist/channels/telegram/polling.d.ts +27 -0
  51. package/dist/channels/telegram/polling.js +105 -0
  52. package/dist/channels/telegram/scaffold/channel.ts +52 -32
  53. package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
  54. package/dist/channels/telegram/setup.d.ts +2 -0
  55. package/dist/channels/telegram/setup.js +42 -0
  56. package/dist/channels/telegram/telegram-api.d.ts +27 -1
  57. package/dist/channels/telegram/telegram-api.js +7 -6
  58. package/dist/channels/telegram/telegram.d.ts +11 -3
  59. package/dist/channels/telegram/telegram.js +184 -157
  60. package/dist/channels/webhook-evidence.d.ts +11 -0
  61. package/dist/channels/webhook-evidence.js +71 -0
  62. package/dist/channels/webhooks.d.ts +37 -0
  63. package/dist/channels/webhooks.js +70 -0
  64. package/dist/child-env.d.ts +16 -0
  65. package/dist/child-env.js +30 -0
  66. package/dist/cli/box-login.js +4 -1
  67. package/dist/cli/channel-app.d.ts +3 -0
  68. package/dist/cli/channel-app.js +35 -0
  69. package/dist/cli/channel-setup.d.ts +38 -0
  70. package/dist/cli/channel-setup.js +195 -0
  71. package/dist/cli/commands/add.d.ts +6 -14
  72. package/dist/cli/commands/add.js +45 -303
  73. package/dist/cli/commands/auth.js +8 -2
  74. package/dist/cli/commands/build.d.ts +5 -0
  75. package/dist/cli/commands/build.js +16 -0
  76. package/dist/cli/commands/channels.d.ts +19 -0
  77. package/dist/cli/commands/channels.js +230 -0
  78. package/dist/cli/commands/content.d.ts +7 -3
  79. package/dist/cli/commands/content.js +16 -10
  80. package/dist/cli/commands/deploy/agentcore.js +55 -23
  81. package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
  82. package/dist/cli/commands/deploy/channel-apps.js +30 -0
  83. package/dist/cli/commands/deploy/docker.js +32 -65
  84. package/dist/cli/commands/deploy/fly.js +28 -12
  85. package/dist/cli/commands/deploy/railway.js +38 -13
  86. package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
  87. package/dist/cli/commands/deploy/secrets-step.js +66 -0
  88. package/dist/cli/commands/deploy/shared.d.ts +16 -5
  89. package/dist/cli/commands/deploy/shared.js +25 -8
  90. package/dist/cli/commands/deploy.js +53 -60
  91. package/dist/cli/commands/dev.d.ts +2 -0
  92. package/dist/cli/commands/dev.js +15 -3
  93. package/dist/cli/commands/env.js +1 -1
  94. package/dist/cli/commands/info.d.ts +2 -0
  95. package/dist/cli/commands/info.js +60 -25
  96. package/dist/cli/commands/init.js +3 -1
  97. package/dist/cli/commands/invoke.d.ts +2 -0
  98. package/dist/cli/commands/invoke.js +20 -7
  99. package/dist/cli/commands/login.d.ts +0 -7
  100. package/dist/cli/commands/login.js +22 -10
  101. package/dist/cli/commands/logout.js +1 -1
  102. package/dist/cli/commands/mcp.js +14 -6
  103. package/dist/cli/commands/models.js +3 -2
  104. package/dist/cli/commands/schedules.d.ts +4 -2
  105. package/dist/cli/commands/schedules.js +14 -10
  106. package/dist/cli/commands/secrets.d.ts +14 -0
  107. package/dist/cli/commands/secrets.js +213 -0
  108. package/dist/cli/commands/start.d.ts +14 -14
  109. package/dist/cli/commands/start.js +46 -60
  110. package/dist/cli/content-view.d.ts +4 -6
  111. package/dist/cli/content-view.js +5 -11
  112. package/dist/cli/first-run-channels.d.ts +3 -0
  113. package/dist/cli/first-run-channels.js +63 -0
  114. package/dist/cli/instance.d.ts +41 -0
  115. package/dist/cli/instance.js +155 -0
  116. package/dist/cli/invoke-stream.d.ts +5 -2
  117. package/dist/cli/invoke-stream.js +7 -2
  118. package/dist/cli/kernel.js +3 -0
  119. package/dist/cli/program.js +282 -110
  120. package/dist/cli/serve.d.ts +4 -5
  121. package/dist/cli/serve.js +4 -7
  122. package/dist/cli/shared.d.ts +11 -2
  123. package/dist/cli/shared.js +32 -19
  124. package/dist/cli/ticket.d.ts +7 -0
  125. package/dist/cli/ticket.js +106 -30
  126. package/dist/content/declare.d.ts +31 -37
  127. package/dist/content/declare.js +59 -60
  128. package/dist/content/file.d.ts +23 -12
  129. package/dist/content/file.js +48 -35
  130. package/dist/content/git.d.ts +0 -10
  131. package/dist/content/git.js +5 -39
  132. package/dist/content/resolve.d.ts +18 -22
  133. package/dist/content/resolve.js +32 -66
  134. package/dist/content/source.d.ts +7 -8
  135. package/dist/content/source.js +10 -28
  136. package/dist/credentials-page.d.ts +26 -0
  137. package/dist/credentials-page.js +185 -0
  138. package/dist/declared-secrets.d.ts +10 -3
  139. package/dist/declared-secrets.js +8 -4
  140. package/dist/deploy/agentcore/destroy.d.ts +4 -1
  141. package/dist/deploy/agentcore/destroy.js +25 -1
  142. package/dist/deploy/agentcore/forwarder.js +1 -1
  143. package/dist/deploy/agentcore/plan.d.ts +7 -21
  144. package/dist/deploy/agentcore/plan.js +47 -49
  145. package/dist/deploy/agentcore/run.d.ts +18 -10
  146. package/dist/deploy/agentcore/run.js +93 -70
  147. package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
  148. package/dist/deploy/agentcore/runtime-secrets.js +108 -0
  149. package/dist/deploy/agentcore/shell.js +12 -19
  150. package/dist/deploy/agentcore/sigv4.d.ts +7 -0
  151. package/dist/deploy/agentcore/sigv4.js +15 -0
  152. package/dist/deploy/agentcore/slr.d.ts +18 -0
  153. package/dist/deploy/agentcore/slr.js +27 -0
  154. package/dist/deploy/agentcore/store.d.ts +7 -0
  155. package/dist/deploy/agentcore/store.js +115 -0
  156. package/dist/deploy/api.d.ts +14 -14
  157. package/dist/deploy/api.js +26 -24
  158. package/dist/deploy/box-shell.d.ts +3 -1
  159. package/dist/deploy/build-context.d.ts +4 -4
  160. package/dist/deploy/build-context.js +11 -15
  161. package/dist/deploy/channel-ingress.d.ts +35 -9
  162. package/dist/deploy/channel-ingress.js +119 -22
  163. package/dist/deploy/container.d.ts +35 -10
  164. package/dist/deploy/container.js +79 -37
  165. package/dist/deploy/delivered-text.d.ts +13 -0
  166. package/dist/deploy/delivered-text.js +86 -0
  167. package/dist/deploy/docker/plan.d.ts +7 -8
  168. package/dist/deploy/docker/plan.js +31 -52
  169. package/dist/deploy/docker/run.d.ts +5 -43
  170. package/dist/deploy/docker/run.js +9 -84
  171. package/dist/deploy/file-store.d.ts +2 -0
  172. package/dist/deploy/file-store.js +17 -0
  173. package/dist/deploy/fly/plan.d.ts +3 -2
  174. package/dist/deploy/fly/plan.js +20 -9
  175. package/dist/deploy/fly/run.d.ts +18 -8
  176. package/dist/deploy/fly/run.js +24 -42
  177. package/dist/deploy/fly/store.d.ts +12 -0
  178. package/dist/deploy/fly/store.js +83 -0
  179. package/dist/deploy/hosts.d.ts +1 -2
  180. package/dist/deploy/hosts.js +1 -2
  181. package/dist/deploy/preflight.d.ts +18 -5
  182. package/dist/deploy/preflight.js +135 -63
  183. package/dist/deploy/railway/ignore.d.ts +8 -0
  184. package/dist/deploy/railway/ignore.js +57 -0
  185. package/dist/deploy/railway/plan.d.ts +1 -2
  186. package/dist/deploy/railway/plan.js +8 -6
  187. package/dist/deploy/railway/run.d.ts +21 -15
  188. package/dist/deploy/railway/run.js +32 -78
  189. package/dist/deploy/railway/store.d.ts +29 -0
  190. package/dist/deploy/railway/store.js +143 -0
  191. package/dist/deploy/runner.d.ts +5 -2
  192. package/dist/deploy/runner.js +4 -3
  193. package/dist/deploy/secret-store.d.ts +63 -0
  194. package/dist/deploy/secret-store.js +98 -0
  195. package/dist/deploy/secrets.d.ts +29 -46
  196. package/dist/deploy/secrets.js +49 -122
  197. package/dist/deploy/workspace.d.ts +6 -12
  198. package/dist/deploy/workspace.js +18 -94
  199. package/dist/dev-supervisor.js +6 -8
  200. package/dist/environment/declare.js +3 -2
  201. package/dist/environment/mise.d.ts +3 -1
  202. package/dist/environment/mise.js +4 -2
  203. package/dist/feishu.d.ts +1 -1
  204. package/dist/feishu.js +1 -1
  205. package/dist/harnesses/models/catalog.d.ts +1 -0
  206. package/dist/harnesses/models/catalog.js +1 -2
  207. package/dist/harnesses/models/environment.d.ts +7 -2
  208. package/dist/harnesses/models/environment.js +9 -5
  209. package/dist/harnesses/models/host.d.ts +0 -7
  210. package/dist/harnesses/models/host.js +10 -1
  211. package/dist/harnesses/models/login.d.ts +0 -8
  212. package/dist/harnesses/models/login.js +5 -4
  213. package/dist/harnesses/models/models.d.ts +7 -2
  214. package/dist/harnesses/models/models.js +28 -4
  215. package/dist/harnesses/pi-durable/agent.js +59 -38
  216. package/dist/harnesses/pi-durable/build.d.ts +12 -0
  217. package/dist/harnesses/pi-durable/build.js +270 -0
  218. package/dist/harnesses/pi-durable/built.d.ts +24 -0
  219. package/dist/harnesses/pi-durable/built.js +136 -0
  220. package/dist/harnesses/pi-durable/config.d.ts +81 -0
  221. package/dist/harnesses/pi-durable/config.js +266 -0
  222. package/dist/harnesses/pi-durable/define.d.ts +63 -13
  223. package/dist/harnesses/pi-durable/define.js +23 -2
  224. package/dist/harnesses/pi-durable/definition.d.ts +72 -0
  225. package/dist/harnesses/pi-durable/definition.js +143 -0
  226. package/dist/harnesses/pi-durable/events.d.ts +4 -3
  227. package/dist/harnesses/pi-durable/events.js +179 -136
  228. package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
  229. package/dist/harnesses/pi-durable/execution-env.js +11 -0
  230. package/dist/harnesses/pi-durable/mcp.js +1 -3
  231. package/dist/harnesses/pi-durable/open.d.ts +35 -37
  232. package/dist/harnesses/pi-durable/open.js +91 -153
  233. package/dist/harnesses/pi-durable/reads.d.ts +0 -1
  234. package/dist/harnesses/pi-durable/reads.js +1 -3
  235. package/dist/harnesses/pi-durable/registry.d.ts +4 -6
  236. package/dist/harnesses/pi-durable/registry.js +29 -26
  237. package/dist/harnesses/pi-durable/release.d.ts +2 -2
  238. package/dist/harnesses/pi-durable/release.js +10 -7
  239. package/dist/harnesses/pi-durable/serve.d.ts +91 -15
  240. package/dist/harnesses/pi-durable/serve.js +109 -47
  241. package/dist/harnesses/pi-durable/sessions.js +15 -12
  242. package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
  243. package/dist/harnesses/pi-durable/text-sources.js +55 -0
  244. package/dist/harnesses/pi-durable/text.d.ts +16 -4
  245. package/dist/harnesses/pi-durable/text.js +115 -59
  246. package/dist/harnesses/pi-durable/wake.js +8 -8
  247. package/dist/http/client.js +9 -3
  248. package/dist/http/contract.d.ts +3 -3
  249. package/dist/http/server.d.ts +1 -1
  250. package/dist/http/server.js +1 -1
  251. package/dist/index.d.ts +8 -5
  252. package/dist/index.js +4 -1
  253. package/dist/instance-name.d.ts +7 -0
  254. package/dist/instance-name.js +26 -0
  255. package/dist/lark.d.ts +1 -1
  256. package/dist/lark.js +1 -1
  257. package/dist/mcp/client.js +11 -2
  258. package/dist/mcp/config.d.ts +70 -18
  259. package/dist/mcp/config.js +136 -53
  260. package/dist/mcp/host.d.ts +5 -2
  261. package/dist/mcp/host.js +14 -6
  262. package/dist/mcp/sign-in.js +2 -2
  263. package/dist/mcp-api.d.ts +14 -0
  264. package/dist/paths.d.ts +22 -8
  265. package/dist/paths.js +44 -11
  266. package/dist/runtime-websocket.d.ts +14 -0
  267. package/dist/runtime-websocket.js +39 -0
  268. package/dist/scaffold/add-channel.d.ts +9 -16
  269. package/dist/scaffold/add-channel.js +13 -83
  270. package/dist/scaffold/templates/env.example +4 -5
  271. package/dist/scaffold/templates/fastagent.config.ts +20 -14
  272. package/dist/scaffold/templates/gitignore +9 -6
  273. package/dist/scaffold/templates.d.ts +6 -2
  274. package/dist/scaffold/templates.js +7 -2
  275. package/dist/scaffold/vendor-skill.js +4 -2
  276. package/dist/service.d.ts +44 -6
  277. package/dist/service.js +110 -10
  278. package/dist/serving-command.d.ts +10 -0
  279. package/dist/serving-command.js +1 -0
  280. package/dist/slack.d.ts +1 -1
  281. package/dist/slack.js +1 -1
  282. package/dist/strip-edges.d.ts +10 -0
  283. package/dist/strip-edges.js +22 -0
  284. package/dist/telegram.d.ts +1 -1
  285. package/dist/telegram.js +1 -1
  286. package/dist/tunnel.d.ts +0 -1
  287. package/dist/tunnel.js +2 -8
  288. package/dist/value-file.d.ts +11 -6
  289. package/dist/value-file.js +21 -9
  290. package/dist/version.d.ts +7 -1
  291. package/dist/version.js +9 -3
  292. package/package.json +6 -5
  293. package/dist/channels/lark/credentials-page.d.ts +0 -25
  294. package/dist/channels/lark/credentials-page.js +0 -206
  295. package/dist/channels/lark/onboard.d.ts +0 -52
  296. package/dist/channels/lark/onboard.js +0 -86
  297. package/dist/channels/slack/config-api.d.ts +0 -69
  298. package/dist/channels/slack/config-api.js +0 -162
  299. package/dist/channels/slack/onboard.d.ts +0 -40
  300. package/dist/channels/slack/onboard.js +0 -136
  301. package/dist/channels/slack/onboarding-state.d.ts +0 -31
  302. package/dist/channels/slack/onboarding-state.js +0 -64
  303. package/dist/channels/slack/register-webhook.d.ts +0 -12
  304. package/dist/channels/slack/register-webhook.js +0 -67
  305. package/dist/channels/slack/setup-server.d.ts +0 -32
  306. package/dist/channels/slack/setup-server.js +0 -246
  307. package/dist/cli/add-feishu.d.ts +0 -41
  308. package/dist/cli/add-feishu.js +0 -391
  309. package/dist/cli/add-slack.d.ts +0 -18
  310. package/dist/cli/add-slack.js +0 -216
  311. package/dist/harnesses/pi-durable/parked.d.ts +0 -18
  312. package/dist/harnesses/pi-durable/parked.js +0 -27
@@ -7,46 +7,23 @@ import { dirname, join } from "node:path";
7
7
  import { detectRuntime } from "../runtime.js";
8
8
  import { assertInsideAgentDir, exists } from "../paths.js";
9
9
  import { channelBundleFiles, channelTemplate } from "./templates.js";
10
- import { writeValues } from "../value-file.js";
11
10
  import { envExamplePath } from "../value-file.js";
12
- import { FEISHU_AGENT_SCOPES } from "../channels/feishu/setup-mode.js";
13
- import { slackBotScopes } from "../channels/slack/manifest.js";
14
- const AGENT_SCOPE_LIST = FEISHU_AGENT_SCOPES.map((entry) => entry.request).join(", ");
15
- const FEISHU_PERMISSION_STEP = `before publishing: the app needs ${AGENT_SCOPE_LIST} — \`add feishu\` requests them with the app and names any ` +
16
- "your tenant withheld (approve those, then publish)";
17
- /** The setting both clouds read, said the same way in `.env.example` and in `add`'s next steps. */
18
- const ingressEnv = (prefix) => ({
19
- name: `${prefix}_INGRESS`,
20
- hint: "webhook | websocket — unset, `dev` connects by websocket, and `start` and every deployment receive by webhook",
21
- required: false,
22
- });
23
- const LARK_PERMISSION_STEP = `before publishing: add ${AGENT_SCOPE_LIST} — \`add lark\` checks them and opens the permission page for any that ` +
24
- "are missing";
25
11
  const CHANNEL_SCAFFOLDS = {
12
+ // A channel's app is set up by `channels setup`, which asks for these values; `add` only names them.
26
13
  telegram: {
27
14
  env: [
28
15
  { name: "TELEGRAM_BOT_TOKEN", hint: "from @BotFather → /newbot", required: true },
29
- {
30
- name: "TELEGRAM_SECRET_TOKEN",
31
- hint: "any random string; verifies inbound updates",
32
- required: true,
33
- generate: true,
34
- },
35
- ],
36
- steps: [
37
- "edit {channel} — customise routing with route() (optional; the defaults already work)",
38
- "the agent can send messages or files back by calling the scaffolded {tools}/telegram-send.ts tool",
16
+ { name: "TELEGRAM_SECRET_TOKEN", hint: "webhook only: verifies inbound updates", required: true },
39
17
  ],
18
+ steps: ["the agent can send messages or files back by calling the scaffolded {tools}/telegram-send.ts tool"],
40
19
  },
41
20
  slack: {
42
21
  env: [
43
- { name: "SLACK_BOT_TOKEN", hint: "Slack app → OAuth & Permissions → Bot User OAuth Token", required: true },
44
- { name: "SLACK_SIGNING_SECRET", hint: "Slack app → Basic Information → App Credentials", required: true },
22
+ { name: "SLACK_BOT_TOKEN", hint: "Install App → Bot User OAuth Token", required: true },
23
+ { name: "SLACK_APP_TOKEN", hint: "connection only: Basic Information → App-Level Tokens", required: true },
24
+ { name: "SLACK_SIGNING_SECRET", hint: "webhook only: Basic Information → Signing Secret", required: true },
45
25
  ],
46
26
  steps: [
47
- `Slack Bot Token Scopes: ${slackBotScopes().join(", ")}`,
48
- "enable Agents (agent_view) and leave token rotation OFF (it cannot be turned off again); subscribe app_home_opened, app_context_changed, app_mention, message.im, message.channels, message.groups, message.mpim; set Request URL to <public-url>/slack",
49
- "reinstall the app after changing scopes, then invite it to each channel it should read",
50
27
  "the agent can send messages or files by calling the scaffolded {tools}/slack-send.ts tool",
51
28
  "the agent reads the threads of the channel it is asked in via the scaffolded {tools}/slack-threads.ts tool",
52
29
  ],
@@ -57,17 +34,17 @@ const CHANNEL_SCAFFOLDS = {
57
34
  env: [
58
35
  {
59
36
  name: "FEISHU_APP_ID",
60
- hint: "created + written automatically by `add feishu` (console → Credentials & Basic Info)",
37
+ hint: "`channels setup feishu` creates the app (console → Credentials & Basic Info)",
61
38
  required: true,
62
39
  },
63
40
  {
64
41
  name: "FEISHU_APP_SECRET",
65
- hint: "created + written automatically by `add feishu` (console → Credentials & Basic Info)",
42
+ hint: "`channels setup feishu` keeps it (console → Credentials & Basic Info)",
66
43
  required: true,
67
44
  },
68
45
  {
69
46
  name: "FEISHU_VERIFICATION_TOKEN",
70
- hint: "webhook only (`start`, every deployment) — captured by `deploy --run` (console → Events & Callbacks)",
47
+ hint: "webhook only: `channels setup feishu` captures it (console → Events & Callbacks)",
71
48
  required: true,
72
49
  },
73
50
  {
@@ -75,13 +52,8 @@ const CHANNEL_SCAFFOLDS = {
75
52
  hint: "webhook only; optional but recommended — set one in the console and copy it here",
76
53
  required: false,
77
54
  },
78
- ingressEnv("FEISHU"),
79
55
  ],
80
56
  steps: [
81
- FEISHU_PERMISSION_STEP,
82
- "PUBLISH the app version in the developer console after permission approval — the switch to webhook mode takes effect on publish (one click, once ever; no API for it)",
83
- "edit {channel} — routing policy (the header walks through the console setup, for hand-made apps)",
84
- "for webhook ingress, run `{serve} --tunnel` locally or `deploy --run` to register the selected app's event Request URL",
85
57
  "the agent can push messages from scheduled turns via the scaffolded {tools}/feishu-send.ts tool",
86
58
  "the agent reads the threads of the group it is asked in via the scaffolded {tools}/feishu-threads.ts tool",
87
59
  ],
@@ -92,7 +64,7 @@ const CHANNEL_SCAFFOLDS = {
92
64
  { name: "LARK_APP_SECRET", hint: "developer console → Credentials & Basic Info", required: true },
93
65
  {
94
66
  name: "LARK_VERIFICATION_TOKEN",
95
- hint: "webhook only (`start`, every deployment) — console → Events & Callbacks; authenticates inbound events",
67
+ hint: "webhook only — console → Events & Callbacks; authenticates inbound events",
96
68
  required: true,
97
69
  },
98
70
  {
@@ -100,12 +72,8 @@ const CHANNEL_SCAFFOLDS = {
100
72
  hint: "webhook only; optional but recommended — set one in the console and copy it here",
101
73
  required: false,
102
74
  },
103
- ingressEnv("LARK"),
104
75
  ],
105
76
  steps: [
106
- "finish the console setup: enable Bot and add the required permissions + im.message.receive_v1 event listed in {channel} (do not publish yet)",
107
- LARK_PERMISSION_STEP,
108
- "for webhook ingress, run `{serve} --tunnel` and keep it running; if auto-registration reports a config-API 404, manually switch Subscription mode to webhook, set its printed https://…/lark Request URL, save, then create + publish a version",
109
77
  "the agent can push messages from scheduled turns via the scaffolded {tools}/lark-send.ts tool",
110
78
  "the agent reads the threads of the group it is asked in via the scaffolded {tools}/lark-threads.ts tool",
111
79
  ],
@@ -113,35 +81,9 @@ const CHANNEL_SCAFFOLDS = {
113
81
  };
114
82
  /** The channel kinds `fastagent add <kind>` can scaffold. */
115
83
  export const CHANNEL_KINDS = Object.keys(CHANNEL_SCAFFOLDS);
116
- /** Local setup never changes the production app or ingress. */
117
- const deploymentStep = (kind, pinned) => pinned
118
- ? `run \`{serve}\` (no tunnel needed: it connects by WebSocket); ${kind.toUpperCase()}_INGRESS=websocket is ` +
119
- "set for this environment; production has its own app and ingress setting"
120
- : "run `{serve}` (no tunnel needed: it connects by WebSocket); " +
121
- "`deploy --run` creates a separate production app for webhook";
122
- const websocketSetup = (kind, pinned) => {
123
- const prefix = kind.toUpperCase();
124
- return {
125
- env: CHANNEL_SCAFFOLDS[kind].env.filter((entry) => [`${prefix}_APP_ID`, `${prefix}_APP_SECRET`, `${prefix}_INGRESS`].includes(entry.name)),
126
- steps: [
127
- kind === "feishu" ? FEISHU_PERMISSION_STEP : LARK_PERMISSION_STEP,
128
- kind === "feishu"
129
- ? "PUBLISH the app version in the developer console after permission approval — long-connection event subscriptions become active with the published version"
130
- : "in Events & Callbacks choose long connection, subscribe im.message.receive_v1, then create + publish a version",
131
- "edit {channel} — routing policy",
132
- deploymentStep(kind, pinned),
133
- `the agent can push messages from scheduled turns via the scaffolded {tools}/${kind}-send.ts tool`,
134
- `the agent reads the threads of the group it is asked in via the scaffolded {tools}/${kind}-threads.ts tool`,
135
- ],
136
- };
137
- };
138
- /** The mode-specific env vars + next-step lines a scaffolded channel needs. */
139
- export function channelSetup(kind, ingress = "webhook",
140
- /** Whether `<PREFIX>_INGRESS` is explicitly set in this environment. */
141
- pinned = false) {
142
- if (ingress !== "websocket" || (kind !== "feishu" && kind !== "lark"))
143
- return CHANNEL_SCAFFOLDS[kind];
144
- return websocketSetup(kind, pinned);
84
+ /** The env vars and next-step lines a scaffolded channel has. */
85
+ export function channelSetup(kind) {
86
+ return CHANNEL_SCAFFOLDS[kind];
145
87
  }
146
88
  /**
147
89
  * Append a channel's env vars (commented placeholders + hints) to `.env.example`, so a developer who copies it to
@@ -167,18 +109,6 @@ export async function appendChannelEnv(dir, kind) {
167
109
  await appendFile(file, block);
168
110
  return true;
169
111
  }
170
- /**
171
- * Append generated channel secrets to the selected environment's value file, and a placeholder for each the channel
172
- * reads that nothing generated. Existing non-empty values are kept.
173
- */
174
- export function appendChannelDotEnv(dir, kind, generated, overwrite = [], ingress = "webhook") {
175
- const entries = channelSetup(kind, ingress).env.map(({ name, hint }) => ({
176
- name,
177
- hint,
178
- ...(generated[name] === undefined ? {} : { value: generated[name] }),
179
- }));
180
- return writeValues(dir, `${kind} channel`, entries, overwrite);
181
- }
182
112
  /** The path `add <kind>` scaffolds to. */
183
113
  function channelPath(dir, kind) {
184
114
  return join(dir, "src", "channels", `${kind}.ts`);
@@ -9,11 +9,10 @@
9
9
  # ANTHROPIC_API_KEY=
10
10
 
11
11
  # --- Model selection (overrides fastagent.config) ---
12
- # Precedence: --model flag > FASTAGENT_MODEL > config — one chain, evaluated in whichever environment
13
- # is running. THIS FILE also declares the environment `fastagent deploy` resolves in, so a value here rides the
14
- # release manifest into the deployment and a deployment can run a model the committed config does not
15
- # name. That recorded value outranks the deployed box's own .env, so switching models there means
16
- # editing this line and redeploying.
12
+ # Precedence: --model flag > FASTAGENT_MODEL > config, evaluated in whichever environment is running. This file is
13
+ # `dev`'s alone: `fastagent start` and `fastagent deploy` read .secrets/production/.env and never this one, and a
14
+ # deployment's values live on its target (`fastagent secrets list <host>`). To fix the model for every instance, set
15
+ # `defaults.model` in fastagent.config.ts.
17
16
  # FASTAGENT_MODEL=openai/gpt-5.5
18
17
 
19
18
  # --- Serving (fastagent start) ---
@@ -1,24 +1,30 @@
1
- // fastagent.config.ts — the agent's configuration: its default model and how it is served. Its prompt lives in
2
- // SYSTEM.md, its capabilities in skills/ and src/ (tools, extensions, channels, schedules) — never here.
1
+ // fastagent.config.ts — the agent's configuration: what it is, and what each instance of it starts with. Its prompt
2
+ // lives in SYSTEM.md, its capabilities in skills/ and src/ (tools, extensions, channels, schedules) — never here.
3
3
  //
4
- // Model precedence: `--model` flag > FASTAGENT_MODEL env > this default. No model is preset: `fastagent dev` shows
5
- // the model catalog (models you already have credentials for come first; picking one that needs auth logs you in
6
- // inline) and runs with your choice; set it below to keep it. `fastagent models` lists every "provider/modelId".
7
- // A self-hosted model (vLLM, Ollama, …) or your own gateway is a provider in code: `providers: [...]`, built with
8
- // pi-ai's `createProvider`.
4
+ // Model precedence: `--model` flag > FASTAGENT_MODEL env > an instance's `fastagent.json` > this default. No model is
5
+ // preset: `fastagent dev` shows the model catalog (models you already have credentials for come first; picking one
6
+ // that needs auth logs you in inline) and runs with your choice; set it below to keep it. `fastagent models` lists
7
+ // every "provider/modelId". A self-hosted model (vLLM, Ollama, …) or your own gateway is a provider in code:
8
+ // `providers: [...]`, built with pi-ai's `createProvider`.
9
9
  //
10
10
  // Every key is optional; the commented lines carry an example. `satisfies` is what makes your editor complete these
11
11
  // keys and turns a typo into an error you see while writing. The import is TYPE-only: nothing is loaded at runtime,
12
- // so the agent opens before `npm install` has run.
12
+ // so the agent opens before `npm install` has run. To let each instance choose values of its own, declare `settings`
13
+ // with `obj({ ... })` from @earendil-works/pi-schema and switch to `defineAgent`.
13
14
  import type { AgentConfig } from "@fastagent-sh/fastagent";
14
15
 
15
16
  export default {
16
- // model: "openai/gpt-5.5",
17
- // thinkingLevel: "high", // reasoning effort (off|minimal|low|medium|high|xhigh|max)
18
- http: {
19
- port: 8787, // the port a serve listens on, for its channels' webhooks and its health check
20
- // api: false, // serve no Agent API under /v1, whatever key the environment holds; deploy gives none a key
17
+ // Constants (what this agent is; an instance cannot change them): name, description, providers, extensions.
18
+
19
+ // Defaults: what an instance starts with, which its own `fastagent.json` may override.
20
+ defaults: {
21
+ // model: "openai/gpt-5.5",
22
+ // thinkingLevel: "high", // reasoning effort (off|minimal|low|medium|high|xhigh|max)
23
+ http: {
24
+ port: 8787, // the port a serve listens on, for its channels' webhooks and its health check
25
+ // api: false, // serve no Agent API under /v1, whatever key the environment holds; deploy gives none a key
26
+ },
27
+ // deploy: { agentcore: { idleTimeoutSeconds: 180 } }, // `deploy agentcore` only: idle microVM tail, 60–1209600s
21
28
  },
22
29
  // The commands the agent needs (CLIs, runtimes, apt packages) live in context.json: `fastagent env add gh`.
23
- // deploy: { agentcore: { idleTimeoutSeconds: 180 } }, // `deploy agentcore` only: idle microVM tail, 60–1209600s
24
30
  } satisfies AgentConfig;
@@ -5,17 +5,20 @@
5
5
  # DIRECTORY, so a symlinked one (pnpm, a shared store) would show up as untracked.
6
6
  node_modules
7
7
 
8
- # generated from src/ every time the agent opens
8
+ # generated: `fastagent build`'s package, and FastAgent's own files, a local instance's state, working directory and
9
+ # clones of the agent's content included (`.fastagent/dev/`, `.fastagent/start/`)
10
+ /dist
9
11
  .fastagent
10
- # machine state: the agent's storage, channel state, schedule state
12
+ # the same, where an instance is placed in the agent's directory (`--state`, `--root`), and on a deployed host, where
13
+ # `content/` is a link to the volume (no trailing slash, since a link is not a directory)
11
14
  .state
12
- # where fastagent clones the agent's content (each clone is its repository's, not this one's). No trailing
13
- # slash: on a deployed host it is a link itself.
14
15
  /content
15
- # the directories of this machine the agent is bound to: this machine's, never the agent's
16
- /context.local.json
17
16
  .cache
18
17
 
18
+ # the development instance's configuration: its app is one person's, so it is personal. What a team shares goes in
19
+ # `defaults` of fastagent.config.ts, or in a deployment's configuration (`deploy --config`).
20
+ fastagent.json
21
+
19
22
  # fastagent reads the agent's env from .secrets/.env (which un-ignores its own .env.example). A bare
20
23
  # .env at THIS level is the file habit puts here — `dev` says so when it finds one; this keeps it
21
24
  # uncommittable meanwhile.
@@ -5,12 +5,16 @@ export declare const channelTemplate: (kind: string, name: string) => string;
5
5
  /** The .ts files in a channel's bundle: `channel.ts` is the channel template; the rest are companion tools. */
6
6
  export declare const channelBundleFiles: (kind: string) => string[];
7
7
  /**
8
- * package.json for the complete agent: fastagent at this build's version, and the packages an agent's code is written
9
- * against (its tools and extensions are pi-durable code, its providers pi-ai's) at the versions this build runs.
8
+ * package.json for the complete agent: fastagent at this build's version, the packages an agent's code is written
9
+ * against (its tools and extensions are pi-durable code, its providers pi-ai's, its tool schemas pi-schema's) at the
10
+ * versions this build runs, and the TypeScript `fastagent build` compiles it with. Its scripts are the framework's
11
+ * three entries, so `npm run dev` and `npm start` work as in any web project; `start` builds first, since it opens the
12
+ * built `dist/` and refuses one the project has moved past.
10
13
  */
11
14
  export declare function packageJson(name: string, build: {
12
15
  version: string;
13
16
  harness: Record<string, string>;
17
+ typescript: string;
14
18
  }): string;
15
19
  /** Sanitize a directory basename into a valid npm package name (lowercase, safe chars). */
16
20
  export declare function toPackageName(dir: string): string;
@@ -12,15 +12,20 @@ export const channelTemplate = (kind, name) => readFileSync(new URL(name, channe
12
12
  /** The .ts files in a channel's bundle: `channel.ts` is the channel template; the rest are companion tools. */
13
13
  export const channelBundleFiles = (kind) => readdirSync(channelScaffoldDir(kind)).filter((f) => f.endsWith(".ts"));
14
14
  /**
15
- * package.json for the complete agent: fastagent at this build's version, and the packages an agent's code is written
16
- * against (its tools and extensions are pi-durable code, its providers pi-ai's) at the versions this build runs.
15
+ * package.json for the complete agent: fastagent at this build's version, the packages an agent's code is written
16
+ * against (its tools and extensions are pi-durable code, its providers pi-ai's, its tool schemas pi-schema's) at the
17
+ * versions this build runs, and the TypeScript `fastagent build` compiles it with. Its scripts are the framework's
18
+ * three entries, so `npm run dev` and `npm start` work as in any web project; `start` builds first, since it opens the
19
+ * built `dist/` and refuses one the project has moved past.
17
20
  */
18
21
  export function packageJson(name, build) {
19
22
  return `${JSON.stringify({
20
23
  name,
21
24
  private: true,
22
25
  type: "module",
26
+ scripts: { dev: "fastagent dev", build: "fastagent build", start: "fastagent build && fastagent start" },
23
27
  dependencies: { "@fastagent-sh/fastagent": `^${build.version}`, ...build.harness },
28
+ devDependencies: { typescript: build.typescript },
24
29
  }, null, 2)}\n`;
25
30
  }
26
31
  /** Sanitize a directory basename into a valid npm package name (lowercase, safe chars). */
@@ -8,11 +8,13 @@ import { randomUUID } from "node:crypto";
8
8
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
9
9
  import { homedir } from "node:os";
10
10
  import { readSkills } from "../harnesses/pi-durable/text.js";
11
+ import { programText } from "../harnesses/pi-durable/text-sources.js";
11
12
  import { assertInsideAgentDir } from "../paths.js";
13
+ import { stripTrailingSlashes } from "../strip-edges.js";
12
14
  /** Derive the destination skill name from a source ref: the last path segment, sans `#ref`. */
13
15
  function skillNameFromSource(source) {
14
16
  const noRef = source.split("#")[0] ?? source;
15
- return basename(noRef.replace(/\/+$/, ""));
17
+ return basename(stripTrailingSlashes(noRef));
16
18
  }
17
19
  /**
18
20
  * The skills/ path must be safe to write through: never follow a symlink that escapes the agent dir (mkdir would), and
@@ -135,7 +137,7 @@ export async function vendorSkill(agentDir, source, options = {}) {
135
137
  // would prefix-pollute a sibling `<name>-x` and break on Windows path separators). A skill it refuses is said by
136
138
  // that loader, and is missing here.
137
139
  const rel = join("skills", name);
138
- const skill = readSkills(agentDir).find((sk) => relative(agentDir, dirname(sk.filePath)) === rel);
140
+ const skill = readSkills(programText(agentDir).skills).find((sk) => relative(agentDir, dirname(sk.filePath)) === rel);
139
141
  return {
140
142
  name: skill?.name ?? name,
141
143
  description: skill?.description,
package/dist/service.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import type { Agent } from "./agent-api.ts";
2
2
  import type { ChannelHandler, Routes } from "./channel.ts";
3
3
  import { type AnyChannelModule, type LoadedLongConnectionChannel } from "./channels/discover.ts";
4
+ import { type WebhookRegistration } from "./channels/webhooks.ts";
4
5
  import { type Scheduler } from "./schedule/scheduler.ts";
5
6
  import type { Schedule } from "./schedule/schedule.ts";
7
+ export type { WebhookRegistration } from "./channels/webhooks.ts";
6
8
  /** The port a serve binds when neither a flag, `PORT` (start only) nor `http.port` names one. */
7
9
  export declare const DEFAULT_HTTP_PORT = 8787;
8
10
  /** What a serve answers: its channels' routes, and `GET /health` unless a channel serves it. */
@@ -14,8 +16,11 @@ interface ServingSurface {
14
16
  /** Flip health between 200 and 503. */
15
17
  setReady(value: boolean): void;
16
18
  }
17
- /** Mount the channels and the health check. */
18
- export declare function routesFor(agent: Agent, stateRoot: string, channels: ReadonlyMap<string, AnyChannelModule>): ServingSurface;
19
+ /**
20
+ * Mount the channels and the health check: JSON, `{ status, webhooks }`, each webhook channel only by name and outcome
21
+ * (SPEC.md §9.1), since it is public; a step and its reasons are in the log.
22
+ */
23
+ export declare function routesFor(agent: Agent, stateRoot: string, channels: ReadonlyMap<string, AnyChannelModule>, webhooks?: () => readonly WebhookRegistration[]): ServingSurface;
19
24
  /** Start the clock: the definition's schedules, and the wake-ups the agent sets for itself, on every serve. */
20
25
  export declare function startSchedules(agent: Agent, stateRoot: string, schedules: readonly Schedule[], options?: {
21
26
  localClock?: boolean;
@@ -24,8 +29,19 @@ export declare function startSchedules(agent: Agent, stateRoot: string, schedule
24
29
  export interface Service {
25
30
  /** For an app's own server to route to. */
26
31
  fetch(request: Request): Promise<Response>;
27
- /** Or a server of its own, on this runtime's server: the one way to start one. Answers the port it bound. */
32
+ /**
33
+ * Or a server of its own, on this runtime's server: the one way to start one. Answers the port it bound. Once the
34
+ * serve is ready it registers the webhook channels at `http.publicUrl`, in the background (`webhooks`).
35
+ */
28
36
  listen(port: number, host?: string): Promise<number>;
37
+ /**
38
+ * Point each webhook channel's platform at `<baseUrl ?? http.publicUrl>/<its path>`, for an app serving `fetch` on its
39
+ * own server, once that server answers and the serve is ready: set on every call. Answers every webhook channel's
40
+ * outcome; rejects only for a `baseUrl` that is not an absolute http(s) URL.
41
+ */
42
+ registerWebhooks(baseUrl?: string): Promise<WebhookRegistration[]>;
43
+ /** Every webhook channel's outcome, `pending` until its registration ends; empty only with no webhook channel. */
44
+ readonly webhooks: readonly WebhookRegistration[];
29
45
  /** Settles when every long connection is up: immediately when there are none. */
30
46
  ready: Promise<void>;
31
47
  /**
@@ -52,11 +68,21 @@ export interface ServedAgent extends Service {
52
68
  /** Whether the Agent API is served, under `/v1`: only with a key (SPEC.md §9.1). */
53
69
  api: boolean;
54
70
  }
71
+ /** A serve's webhook channels: their outcomes, and the registration that sets them, one at a time. */
72
+ export interface WebhookRegistry {
73
+ /** The outcomes, as one array kept current. */
74
+ list(): readonly WebhookRegistration[];
75
+ register(baseUrl?: string): Promise<WebhookRegistration[]>;
76
+ /** Stop hearing the platforms reach it. */
77
+ dispose(): void;
78
+ }
79
+ /** The webhook channels of an opened agent, to be pointed at `http.publicUrl` or at a URL the host names. */
80
+ export declare function webhooksOf(opened: ServableAgent): WebhookRegistry;
55
81
  /**
56
82
  * A Service over `handler`: answered in process, or served on a server of its own. `endStreams` ends what would hold
57
83
  * the server's close open (a Caller watching events), and `shutdown` stops the rest once the server has.
58
84
  */
59
- export declare function serviceOver(handler: ChannelHandler, ready: Promise<void>, shutdown: () => Promise<void>, endStreams?: () => Promise<void>): Service;
85
+ export declare function serviceOver(handler: ChannelHandler, ready: Promise<void>, shutdown: () => Promise<void>, endStreams?: () => Promise<void>, webhooks?: WebhookRegistry): Service;
60
86
  /** What {@link serveAgent} needs beyond an opened agent. */
61
87
  export interface ServeOptions {
62
88
  /** Called when a long connection ends on its own — a dropped socket-mode channel, say. */
@@ -69,7 +95,20 @@ export interface ServeOptions {
69
95
  /** What the assembly serves. */
70
96
  export interface ServableAgent {
71
97
  agent: Agent;
72
- /** The definition's directory: the agent's working directory. */
98
+ /** The instance's secrets: what its webhook channels are pointed with. */
99
+ secrets: Readonly<Record<string, string>>;
100
+ /** The configuration's part a serve reads: where the platforms reach it, and the URLs a person set there. */
101
+ config: {
102
+ http?: {
103
+ publicUrl?: string;
104
+ };
105
+ channels?: Readonly<Record<string, {
106
+ app?: {
107
+ url?: string;
108
+ };
109
+ }>>;
110
+ };
111
+ /** The project directory the instance's `.secrets/` belong to. */
73
112
  agentDir: string;
74
113
  /** Where durable state lives (channel state, schedule fires, wake-ups). */
75
114
  stateRoot: string;
@@ -78,4 +117,3 @@ export interface ServableAgent {
78
117
  }
79
118
  /** The assembly itself: channels, schedules and long connections, composed into one handler. */
80
119
  export declare function serveAgent(opened: ServableAgent, options?: ServeOptions): Promise<ServedAgent>;
81
- export {};
package/dist/service.js CHANGED
@@ -4,16 +4,19 @@ import * as Effect from "effect/Effect";
4
4
  import * as Exit from "effect/Exit";
5
5
  import * as Fiber from "effect/Fiber";
6
6
  import * as Scope from "effect/Scope";
7
- import { mountChannels } from "./channels/discover.js";
8
- import { text } from "./channels/respond.js";
7
+ import { channelIngress, mountChannels, } from "./channels/discover.js";
9
8
  import { parseRouteKey, router } from "./channels/router.js";
10
9
  import { serveHandler, totalFetch } from "./channels/serve.js";
10
+ import { pendingWebhooks, registerOwnWebhooks, webhookKinds, webhookPath, } from "./channels/webhooks.js";
11
+ import { addressedTo, onReached, readReached } from "./channels/webhook-evidence.js";
12
+ import { channelStateDir } from "./channels/context.js";
11
13
  import { portCleanup } from "./effect-port.js";
12
14
  import { API_PREFIX } from "./http/contract.js";
13
15
  import { apiHandler, isApiPath } from "./http/server.js";
14
16
  import { log } from "./log.js";
15
17
  import { once } from "./once.js";
16
18
  import { createScheduler } from "./schedule/scheduler.js";
19
+ import { stripTrailingSlashes } from "./strip-edges.js";
17
20
  /** Default wait for a channel's `closed` before reporting it stuck. */
18
21
  const CLOSE_DEADLINE_MS = 5_000;
19
22
  /** One deadline for all connections; a failed close must not hide a sibling that is still running. */
@@ -41,8 +44,11 @@ function closeWithin(runs, names, deadlineMs) {
41
44
  }
42
45
  /** The port a serve binds when neither a flag, `PORT` (start only) nor `http.port` names one. */
43
46
  export const DEFAULT_HTTP_PORT = 8787;
44
- /** Mount the channels and the health check. */
45
- export function routesFor(agent, stateRoot, channels) {
47
+ /**
48
+ * Mount the channels and the health check: JSON, `{ status, webhooks }`, each webhook channel only by name and outcome
49
+ * (SPEC.md §9.1), since it is public; a step and its reasons are in the log.
50
+ */
51
+ export function routesFor(agent, stateRoot, channels, webhooks = () => []) {
46
52
  const { routes, longConnections, routeChannels } = mountChannels(channels, agent, stateRoot);
47
53
  for (const key of Object.keys(routes)) {
48
54
  if (isApiPath(parseRouteKey(key).path)) {
@@ -50,7 +56,10 @@ export function routesFor(agent, stateRoot, channels) {
50
56
  }
51
57
  }
52
58
  let ready = longConnections.length === 0;
53
- const health = () => (ready ? text("ok\n", 200) : text("starting\n", 503));
59
+ const health = () => Response.json({
60
+ status: ready ? "ok" : "starting",
61
+ webhooks: webhooks().map(({ channel, outcome }) => ({ channel, outcome })),
62
+ }, { status: ready ? 200 : 503 });
54
63
  const probed = Object.keys(routes).some((key) => {
55
64
  const entry = parseRouteKey(key);
56
65
  return entry.path === "/health" && (entry.method === undefined || entry.method === "GET");
@@ -78,16 +87,96 @@ export function startSchedules(agent, stateRoot, schedules, options = {}) {
78
87
  log.info(`[fastagent] schedules: ${schedules.map((s) => s.name).join(", ")}`);
79
88
  return scheduler;
80
89
  }
90
+ /** The channels whose platform lets only a person set the URL (SPEC.md §8): Slack, and Lark without its config API. */
91
+ const PERSON_SET = ["slack", "lark"];
92
+ /** Nothing to point: a serve with no webhook channel. */
93
+ const NONE = Object.freeze([]);
94
+ const NO_WEBHOOKS = { list: () => NONE, register: async () => [], dispose: () => { } };
95
+ /**
96
+ * The webhook channels among `kinds`, registered with `env`, the instance's own secrets, at `publicUrl` unless a call
97
+ * names another base. Each call waits for the one before it, so two never interleave their platform calls.
98
+ */
99
+ function webhookRegistry(kinds, env, publicUrl, confirmation, options = {}) {
100
+ // One array, replaced in place: a Service is spread into the objects that extend it, and each keeps reading this.
101
+ const current = pendingWebhooks(kinds);
102
+ const replace = (next) => current.splice(0, current.length, ...next);
103
+ let previous = Promise.resolve();
104
+ let base;
105
+ // A URL only a person sets is confirmed by the platform reaching it: told as it happens, or read from the record.
106
+ const confirmed = (kind, address, at) => {
107
+ const path = webhookPath(kind);
108
+ if (confirmation.appUrl(kind) === `${at}${path}`)
109
+ return true;
110
+ if (address === undefined)
111
+ return false;
112
+ if (addressedTo(address, at, path))
113
+ return true;
114
+ log.warn(`[fastagent] ${kind}: the platform reached this instance at ${address}, not ${at}${path}`);
115
+ return false;
116
+ };
117
+ const confirm = (registrations, at) => registrations.map((entry) => {
118
+ const kind = entry.channel;
119
+ if (at === undefined || entry.outcome !== "manual" || !PERSON_SET.includes(kind))
120
+ return entry;
121
+ if (!confirmed(kind, readReached(confirmation.stateDir(kind)), at))
122
+ return entry;
123
+ return { channel: entry.channel, outcome: "registered", url: `${at}${webhookPath(kind)}` };
124
+ });
125
+ const stops = kinds
126
+ .filter((kind) => PERSON_SET.includes(kind))
127
+ .map((kind) => onReached(confirmation.stateDir(kind), (address) => {
128
+ const index = current.findIndex((entry) => entry.channel === kind);
129
+ const entry = current[index];
130
+ if (base === undefined || entry?.outcome !== "manual" || !confirmed(kind, address, base))
131
+ return;
132
+ current[index] = { channel: kind, outcome: "registered", url: `${base}${webhookPath(kind)}` };
133
+ }));
134
+ return {
135
+ list: () => current,
136
+ register(baseUrl) {
137
+ if (baseUrl !== undefined && !/^https?:\/\/[^/?#\s]+/.test(baseUrl)) {
138
+ return Promise.reject(new Error(`registerWebhooks: ${JSON.stringify(baseUrl)} is not an absolute http(s) URL`));
139
+ }
140
+ const target = baseUrl ?? publicUrl;
141
+ const at = target === undefined ? undefined : stripTrailingSlashes(target);
142
+ const run = previous.then(async () => {
143
+ base = at;
144
+ replace(pendingWebhooks(kinds));
145
+ if (at === undefined && kinds.length > 0) {
146
+ log.info(`[fastagent] webhooks: no http.publicUrl, so nothing points ${kinds.join(", ")} at this instance`);
147
+ }
148
+ replace(confirm(await registerOwnWebhooks(at, kinds, env, options), at));
149
+ return [...current];
150
+ });
151
+ previous = run.catch(() => { });
152
+ return run;
153
+ },
154
+ dispose: () => {
155
+ for (const stop of stops)
156
+ stop();
157
+ },
158
+ };
159
+ }
160
+ /** The webhook channels of an opened agent, to be pointed at `http.publicUrl` or at a URL the host names. */
161
+ export function webhooksOf(opened) {
162
+ const kinds = webhookKinds([...opened.channels].map(([name, module]) => ({ name, ingress: channelIngress(module) })));
163
+ return webhookRegistry(kinds, opened.secrets, opened.config.http?.publicUrl, {
164
+ stateDir: (kind) => channelStateDir(opened.stateRoot, kind),
165
+ appUrl: (kind) => opened.config.channels?.[kind]?.app?.url,
166
+ });
167
+ }
81
168
  /**
82
169
  * A Service over `handler`: answered in process, or served on a server of its own. `endStreams` ends what would hold
83
170
  * the server's close open (a Caller watching events), and `shutdown` stops the rest once the server has.
84
171
  */
85
- export function serviceOver(handler, ready, shutdown, endStreams = async () => { }) {
172
+ export function serviceOver(handler, ready, shutdown, endStreams = async () => { }, webhooks = NO_WEBHOOKS) {
86
173
  let listener;
87
174
  let closing;
88
175
  const fetch = totalFetch(handler);
89
176
  return {
90
177
  fetch,
178
+ registerWebhooks: (baseUrl) => closing !== undefined ? Promise.reject(new Error("the service is closed")) : webhooks.register(baseUrl),
179
+ webhooks: webhooks.list(),
91
180
  listen(port, host) {
92
181
  if (closing !== undefined)
93
182
  return Promise.reject(new Error("the service is closed"));
@@ -95,7 +184,17 @@ export function serviceOver(handler, ready, shutdown, endStreams = async () => {
95
184
  return Promise.reject(new Error("the service is already listening"));
96
185
  const current = serveHandler(handler, { port, ...(host === undefined ? {} : { host }) });
97
186
  listener = current;
98
- return current.listening.catch(async (error) => {
187
+ return current.listening.then((bound) => {
188
+ // After ready: a host may route to the instance only once its health check passes, and a platform verifies
189
+ // the URL as it is set. In the background, so the port is answered without waiting for a platform.
190
+ // A ready that fails is the failure its awaiter reports: nothing is registered for a serve that never was.
191
+ void ready.then(() => closing === undefined
192
+ ? webhooks
193
+ .register()
194
+ .catch((error) => log.error(`[fastagent] webhook registration failed: ${String(error)}`))
195
+ : undefined, () => { });
196
+ return bound;
197
+ }, async (error) => {
99
198
  // A bind that failed holds nothing once its scope is closed, so the service may listen again, elsewhere.
100
199
  if (listener === current)
101
200
  listener = undefined;
@@ -108,7 +207,7 @@ export function serviceOver(handler, ready, shutdown, endStreams = async () => {
108
207
  closing ??= (async () => {
109
208
  // The server first, so nothing new reaches what stops after it; each step runs, and every failure is said.
110
209
  const failures = [];
111
- for (const step of [endStreams, () => listener?.close(), shutdown]) {
210
+ for (const step of [endStreams, () => listener?.close(), shutdown, webhooks.dispose]) {
112
211
  await Promise.resolve()
113
212
  .then(step)
114
213
  .catch((error) => failures.push(error));
@@ -125,7 +224,8 @@ export function serviceOver(handler, ready, shutdown, endStreams = async () => {
125
224
  export async function serveAgent(opened, options = {}) {
126
225
  const { agent, agentDir, stateRoot, schedules } = opened;
127
226
  const closeTimeoutMs = options.closeTimeoutMs ?? CLOSE_DEADLINE_MS;
128
- const routed = routesFor(agent, stateRoot, opened.channels);
227
+ const webhooks = webhooksOf(opened);
228
+ const routed = routesFor(agent, stateRoot, opened.channels, webhooks.list);
129
229
  // Composed BEFORE anything starts.
130
230
  const channels = router(routed.routes);
131
231
  const api = options.apiKeys?.length ? apiHandler(agent, { keys: options.apiKeys }) : undefined;
@@ -200,7 +300,7 @@ export async function serveAgent(opened, options = {}) {
200
300
  // The rejection is still delivered to callers that await ready.
201
301
  ready.catch(() => { });
202
302
  return {
203
- ...serviceOver(handler, ready, close, async () => api?.endStreams()),
303
+ ...serviceOver(handler, ready, close, async () => api?.endStreams(), webhooks),
204
304
  routes: routed.routes,
205
305
  agentDir,
206
306
  channels: { routes: routed.routeChannels, longConnections: names },
@@ -8,3 +8,13 @@ export declare function markDevServe(env?: NodeJS.ProcessEnv): void;
8
8
  /** For a command that is never `dev` (`start`, `deploy`), run from a shell or a tool that a `dev` process spawned. */
9
9
  export declare function unmarkDevServe(env?: NodeJS.ProcessEnv): void;
10
10
  export declare function servedByDev(env?: NodeJS.ProcessEnv): boolean;
11
+ /**
12
+ * Who runs an instance, as an open is told: `dev` serves it, a deployed box (the release manifest's variable) runs it,
13
+ * on AgentCore. The CLI reads its own process's variables for these (`runsAsIn`); an app's instance is none of them.
14
+ */
15
+ export interface RunsAs {
16
+ dev: boolean;
17
+ deployed: boolean;
18
+ agentcore: boolean;
19
+ }
20
+ export declare const NOT_HOSTED: RunsAs;
@@ -14,3 +14,4 @@ export function unmarkDevServe(env = process.env) {
14
14
  export function servedByDev(env = process.env) {
15
15
  return env[DEV_SERVE_ENV] === "1";
16
16
  }
17
+ export const NOT_HOSTED = { dev: false, deployed: false, agentcore: false };