@fastagent-sh/fastagent 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +25 -7
  2. package/dist/agent.d.ts +1 -1
  3. package/dist/agent.js +1 -1
  4. package/dist/atomic-write.d.ts +23 -0
  5. package/dist/atomic-write.js +53 -0
  6. package/dist/{host/node.d.ts → channel.d.ts} +24 -28
  7. package/dist/channel.js +1 -0
  8. package/dist/channels/agentcore-limits.d.ts +9 -0
  9. package/dist/channels/agentcore-limits.js +9 -0
  10. package/dist/channels/agentcore-service.d.ts +55 -0
  11. package/dist/channels/agentcore-service.js +107 -0
  12. package/dist/channels/agentcore.d.ts +22 -20
  13. package/dist/channels/agentcore.js +20 -11
  14. package/dist/channels/busy.d.ts +1 -1
  15. package/dist/channels/busy.js +1 -1
  16. package/dist/channels/control.d.ts +55 -7
  17. package/dist/channels/control.js +312 -67
  18. package/dist/channels/discover.d.ts +59 -0
  19. package/dist/{engines/pi/channel.js → channels/discover.js} +19 -34
  20. package/dist/channels/feishu/card.js +1 -1
  21. package/dist/channels/feishu/context-buffer.d.ts +1 -1
  22. package/dist/channels/feishu/context-buffer.js +4 -4
  23. package/dist/channels/feishu/feishu-api.js +6 -9
  24. package/dist/channels/feishu/feishu.d.ts +1 -1
  25. package/dist/channels/feishu/feishu.js +41 -24
  26. package/dist/channels/feishu/invoke-turn.d.ts +1 -1
  27. package/dist/channels/feishu/invoke-turn.js +3 -4
  28. package/dist/channels/feishu/preview.d.ts +1 -1
  29. package/dist/channels/feishu/preview.js +7 -3
  30. package/dist/channels/feishu/ws-ingress.d.ts +1 -1
  31. package/dist/channels/github/github.d.ts +1 -1
  32. package/dist/channels/http.d.ts +11 -7
  33. package/dist/channels/http.js +1 -109
  34. package/dist/channels/kit/attachment-path.d.ts +12 -0
  35. package/dist/channels/kit/attachment-path.js +43 -0
  36. package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +9 -0
  37. package/dist/channels/{context-buffer.js → kit/context-buffer.js} +13 -2
  38. package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
  39. package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
  40. package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
  41. package/dist/channels/{seen.js → kit/seen.js} +1 -1
  42. package/dist/channels/kit/signature.d.ts +12 -0
  43. package/dist/channels/kit/signature.js +17 -0
  44. package/dist/channels/{state.js → kit/state.js} +4 -7
  45. package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
  46. package/dist/channels/{stop-command.js → kit/stop-command.js} +5 -5
  47. package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +1 -1
  48. package/dist/channels/{tasks.js → kit/tasks.js} +11 -5
  49. package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
  50. package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
  51. package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +19 -3
  52. package/dist/channels/{turn-store.js → kit/turn-store.js} +30 -3
  53. package/dist/channels/lark/lark.d.ts +1 -1
  54. package/dist/channels/serve.d.ts +73 -0
  55. package/dist/channels/serve.js +243 -0
  56. package/dist/channels/slack/bot-auth.js +3 -14
  57. package/dist/channels/slack/context-buffer.d.ts +2 -2
  58. package/dist/channels/slack/context-buffer.js +2 -2
  59. package/dist/channels/slack/invoke-turn.d.ts +2 -2
  60. package/dist/channels/slack/invoke-turn.js +13 -6
  61. package/dist/channels/slack/onboard.js +5 -5
  62. package/dist/channels/slack/onboarding-state.d.ts +7 -3
  63. package/dist/channels/slack/onboarding-state.js +13 -20
  64. package/dist/channels/slack/parse.js +2 -2
  65. package/dist/channels/slack/preview.d.ts +1 -1
  66. package/dist/channels/slack/preview.js +11 -3
  67. package/dist/channels/slack/register-webhook.js +1 -1
  68. package/dist/channels/slack/scaffold/slack-send.ts +2 -17
  69. package/dist/channels/slack/slack-api.js +23 -55
  70. package/dist/channels/slack/slack.d.ts +1 -1
  71. package/dist/channels/slack/slack.js +20 -20
  72. package/dist/channels/slack/welcomed.js +1 -1
  73. package/dist/channels/telegram/context-buffer.d.ts +2 -2
  74. package/dist/channels/telegram/context-buffer.js +2 -2
  75. package/dist/channels/telegram/invoke-turn.d.ts +1 -1
  76. package/dist/channels/telegram/invoke-turn.js +1 -1
  77. package/dist/channels/telegram/parse.d.ts +21 -1
  78. package/dist/channels/telegram/parse.js +67 -13
  79. package/dist/channels/telegram/preview.d.ts +1 -1
  80. package/dist/channels/telegram/preview.js +6 -2
  81. package/dist/channels/telegram/scaffold/channel.ts +7 -3
  82. package/dist/channels/telegram/telegram-api.js +5 -6
  83. package/dist/channels/telegram/telegram.d.ts +3 -3
  84. package/dist/channels/telegram/telegram.js +60 -72
  85. package/dist/channels/telegram/turn-store.d.ts +2 -2
  86. package/dist/channels/telegram/turn-store.js +2 -2
  87. package/dist/cli/add-slack.js +6 -5
  88. package/dist/cli/commands/add.js +1 -5
  89. package/dist/cli/commands/attach.d.ts +2 -4
  90. package/dist/cli/commands/attach.js +17 -15
  91. package/dist/cli/commands/deploy.d.ts +50 -5
  92. package/dist/cli/commands/deploy.js +183 -134
  93. package/dist/cli/commands/dev.js +25 -40
  94. package/dist/cli/commands/fire.js +1 -1
  95. package/dist/cli/commands/info.js +7 -5
  96. package/dist/cli/commands/init.js +1 -2
  97. package/dist/cli/commands/schedule.js +1 -1
  98. package/dist/cli/commands/start.js +80 -121
  99. package/dist/cli/commands/tool.js +5 -7
  100. package/dist/cli/program.js +1 -1
  101. package/dist/cli/serve.d.ts +22 -70
  102. package/dist/cli/serve.js +66 -257
  103. package/dist/cli/shared.d.ts +40 -10
  104. package/dist/cli/shared.js +53 -6
  105. package/dist/core.d.ts +3 -5
  106. package/dist/core.js +7 -5
  107. package/dist/deploy/agentcore/logs.d.ts +2 -2
  108. package/dist/deploy/agentcore/logs.js +2 -2
  109. package/dist/deploy/agentcore/plan.d.ts +12 -10
  110. package/dist/deploy/agentcore/plan.js +39 -31
  111. package/dist/deploy/agentcore/run.d.ts +5 -4
  112. package/dist/deploy/agentcore/run.js +13 -36
  113. package/dist/deploy/channel-ingress.d.ts +73 -0
  114. package/dist/deploy/channel-ingress.js +101 -0
  115. package/dist/deploy/docker/plan.d.ts +3 -7
  116. package/dist/deploy/docker/plan.js +4 -15
  117. package/dist/deploy/docker/run.d.ts +18 -1
  118. package/dist/deploy/docker/run.js +10 -0
  119. package/dist/deploy/fly/plan.d.ts +4 -5
  120. package/dist/deploy/fly/plan.js +7 -22
  121. package/dist/deploy/fly/run.d.ts +9 -23
  122. package/dist/deploy/fly/run.js +32 -68
  123. package/dist/deploy/preflight.d.ts +6 -7
  124. package/dist/deploy/preflight.js +27 -23
  125. package/dist/deploy/railway/plan.d.ts +4 -5
  126. package/dist/deploy/railway/plan.js +11 -26
  127. package/dist/deploy/railway/run.d.ts +9 -9
  128. package/dist/deploy/railway/run.js +40 -40
  129. package/dist/deploy/runner.js +22 -2
  130. package/dist/deploy/secrets.d.ts +18 -9
  131. package/dist/deploy/secrets.js +64 -12
  132. package/dist/dev-supervisor.js +11 -6
  133. package/dist/engines/pi/agent-session-factory.d.ts +104 -0
  134. package/dist/engines/pi/agent-session-factory.js +295 -0
  135. package/dist/engines/pi/auth.js +43 -43
  136. package/dist/engines/pi/config.d.ts +5 -5
  137. package/dist/engines/pi/config.js +1 -1
  138. package/dist/engines/pi/create.d.ts +67 -42
  139. package/dist/engines/pi/create.js +142 -72
  140. package/dist/engines/pi/definition.d.ts +42 -8
  141. package/dist/engines/pi/definition.js +114 -11
  142. package/dist/engines/pi/invoke-session.d.ts +22 -19
  143. package/dist/engines/pi/invoke-session.js +286 -100
  144. package/dist/engines/pi/login.js +39 -20
  145. package/dist/engines/pi/models.d.ts +14 -0
  146. package/dist/engines/pi/models.js +9 -1
  147. package/dist/engines/pi/open.d.ts +10 -8
  148. package/dist/engines/pi/open.js +21 -11
  149. package/dist/engines/pi/report.d.ts +0 -4
  150. package/dist/engines/pi/report.js +0 -7
  151. package/dist/engines/pi/search-tools.js +1 -1
  152. package/dist/engines/pi/service.d.ts +23 -0
  153. package/dist/engines/pi/service.js +27 -0
  154. package/dist/engines/pi/session-builder.js +87 -118
  155. package/dist/engines/pi/session-control.d.ts +28 -14
  156. package/dist/engines/pi/session-control.js +570 -438
  157. package/dist/engines/pi/session-inheritance.d.ts +30 -0
  158. package/dist/engines/pi/session-inheritance.js +261 -0
  159. package/dist/engines/pi/session-markers.d.ts +48 -0
  160. package/dist/engines/pi/session-markers.js +59 -0
  161. package/dist/engines/pi/session-settings.d.ts +15 -3
  162. package/dist/engines/pi/session-settings.js +22 -1
  163. package/dist/engines/pi/session-store.d.ts +140 -0
  164. package/dist/engines/pi/session-store.js +666 -0
  165. package/dist/engines/pi/tool-context.d.ts +36 -13
  166. package/dist/engines/pi/tool-context.js +51 -4
  167. package/dist/engines/pi/tool.d.ts +5 -5
  168. package/dist/engines/pi/tool.js +3 -3
  169. package/dist/engines/pi/turn-kit.d.ts +32 -6
  170. package/dist/engines/pi/turn-kit.js +28 -1
  171. package/dist/engines/pi/wake-tool.js +1 -1
  172. package/dist/env.js +1 -2
  173. package/dist/index.d.ts +1 -0
  174. package/dist/index.js +1 -0
  175. package/dist/loader.d.ts +49 -7
  176. package/dist/loader.js +79 -18
  177. package/dist/log.d.ts +3 -0
  178. package/dist/log.js +5 -1
  179. package/dist/node.d.ts +16 -0
  180. package/dist/node.js +20 -0
  181. package/dist/paths.d.ts +26 -0
  182. package/dist/paths.js +37 -2
  183. package/dist/pi.d.ts +14 -8
  184. package/dist/pi.js +6 -5
  185. package/dist/scaffold/add-channel.js +2 -2
  186. package/dist/scaffold/init.js +7 -2
  187. package/dist/scaffold/templates/persona.md +2 -2
  188. package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
  189. package/dist/schedule/discover.js +3 -15
  190. package/dist/schedule/state.js +4 -9
  191. package/dist/schedule/wake-alarm.d.ts +13 -7
  192. package/dist/schedule/wake-alarm.js +88 -46
  193. package/dist/schedule/wakeups.d.ts +2 -2
  194. package/dist/schedule/wakeups.js +11 -8
  195. package/dist/service.d.ts +157 -0
  196. package/dist/service.js +376 -0
  197. package/dist/session-remote.d.ts +8 -3
  198. package/dist/session-remote.js +162 -92
  199. package/dist/session.d.ts +229 -93
  200. package/dist/session.js +61 -23
  201. package/dist/telegram.d.ts +1 -1
  202. package/dist/telegram.js +1 -1
  203. package/dist/tunnel.d.ts +18 -3
  204. package/dist/tunnel.js +32 -40
  205. package/package.json +14 -5
  206. package/dist/engines/pi/channel.d.ts +0 -37
  207. package/dist/engines/pi/harness.d.ts +0 -115
  208. package/dist/engines/pi/harness.js +0 -154
  209. package/dist/engines/pi/invoke.d.ts +0 -60
  210. package/dist/engines/pi/invoke.js +0 -459
  211. package/dist/engines/pi/read-image.d.ts +0 -4
  212. package/dist/engines/pi/read-image.js +0 -62
  213. package/dist/engines/pi/sessions.d.ts +0 -60
  214. package/dist/engines/pi/sessions.js +0 -464
  215. package/dist/host/node.js +0 -52
  216. /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
  217. /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
  218. /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
  219. /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
  220. /package/dist/channels/{text.js → kit/text.js} +0 -0
  221. /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
  222. /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
package/dist/cli/serve.js CHANGED
@@ -1,188 +1,18 @@
1
1
  /**
2
- * The serving spine shared by `dev` (its worker) and `start`: channel assembly, Node HTTP binding,
3
- * long-connection lifecycle, scheduler lifecycle, and the optional Cloudflare quick tunnel.
2
+ * What `dev` (its worker) and `start` need beyond the service itself: binding a port, the shutdown
3
+ * order, the startup report, and the optional Cloudflare quick tunnel.
4
+ *
5
+ * The ASSEMBLY is not here — it lives in `src/service.ts`, which a public entry may import and this
6
+ * directory may not be (it decides process-level things: `fail.ts` calls `process.exit`).
4
7
  */
5
- import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
- import { join } from "node:path";
7
- import { createStateSync } from "../channels/agentcore-state.js";
8
- import { agentcoreRoutes, UnknownScheduleError } from "../channels/agentcore.js";
9
- import { activeWork } from "../channels/busy.js";
10
- import { controlRoutes } from "../channels/control.js";
11
- import { INVOKE_EXAMPLE_BODY, createInvokeHandler } from "../channels/http.js";
12
- import { text } from "../channels/respond.js";
13
- import { loadChannels } from "../engines/pi/channel.js";
14
- import { reportModuleLoadFailures } from "../engines/pi/report.js";
8
+ import { INVOKE_EXAMPLE_BODY } from "../channels/http.js";
15
9
  import { answersLocalhost, bindLabel, classifyBind, clientHost } from "../bind.js";
16
- import { parseRouteKey, router, serveNode } from "../host/node.js";
10
+ import { serveNode } from "../channels/serve.js";
17
11
  import { log } from "../log.js";
18
12
  import { openExternalUrl } from "../open-url.js";
19
- import { loadSchedules } from "../schedule/discover.js";
20
- import { createScheduler, fireScheduleOnce } from "../schedule/scheduler.js";
13
+ import { declaredChannels } from "../channels/discover.js";
21
14
  import { announceWebhooks, startCloudflareTunnel } from "../tunnel.js";
22
15
  import { failStartup, failUsage } from "./fail.js";
23
- /**
24
- * The surface this deployment serves: default `GET /health` plus discovered channels, or the default
25
- * POST `/invoke` only when neither a route nor a long-connection channel was declared.
26
- */
27
- export async function routesFor(agentDir, agent, stateRoot, control, options = {}) {
28
- const { routes, longConnections, routeChannels, collisions, failures } = await loadChannels(agentDir, {
29
- agent,
30
- stateRoot,
31
- control,
32
- });
33
- for (const c of collisions) {
34
- console.error(`[fastagent] warn: channel route "${c.route}" (${c.source}) collides with an earlier channel — not mounted`);
35
- }
36
- reportModuleLoadFailures(failures);
37
- if (failures.length > 0 || collisions.length > 0) {
38
- throw new Error(`channel setup is invalid (${failures.length} load failure(s), ${collisions.length} route collision(s)) — ` +
39
- `fix it, or rename an intentionally disabled file to *.disabled`);
40
- }
41
- const builtinInvoke = options.builtinInvoke !== false && Object.keys(routes).length === 0 && longConnections.length === 0;
42
- const channels = builtinInvoke ? { "POST /invoke": createInvokeHandler(agent) } : routes;
43
- const healthCovered = Object.keys(channels).some((key) => {
44
- const entry = parseRouteKey(key);
45
- return entry.path === "/health" && (entry.method === undefined || entry.method === "GET");
46
- });
47
- let ready = longConnections.length === 0;
48
- const health = () => (ready ? text("ok\n", 200) : text("starting\n", 503));
49
- return {
50
- routes: healthCovered ? channels : { "GET /health": health, ...channels },
51
- longConnections,
52
- routeChannels,
53
- builtinInvoke,
54
- markReady() {
55
- ready = true;
56
- },
57
- };
58
- }
59
- /**
60
- * Mount the session control plane (`/control/*`) when the agent enabled it
61
- * (`config.sessionControl`): merge the bearer-authenticated routes and return an announcer that
62
- * writes `<stateRoot>/control.json` — `{ url, token }`, 0600 — once the port is known. The file is
63
- * the LOCAL discovery channel (`fastagent attach`, a local desktop app); filesystem permissions are
64
- * its trust boundary, and each boot overwrites it with a fresh per-boot token. A user channel
65
- * colliding on `/control/*` fails startup — the same disposition as a channel-channel collision
66
- * (routesFor): `sessionControl` is an explicit opt-in, so declaring both is a configuration error,
67
- * and silently shadowing either side would serve a surface the author didn't write.
68
- */
69
- export function mountSessionControl(routes, control, stateRoot, options = {}) {
70
- if (!control)
71
- return { routes, announce: () => { } };
72
- const token = crypto.randomUUID();
73
- const mounted = controlRoutes(control, { token, agent: options.agent });
74
- // PATH-level collision, matching the router's semantics (an any-method "/control/dispatch"
75
- // channel key would dodge an exact-key check yet still shadow the method-qualified control
76
- // route at match time — the router matches by path first).
77
- const mountedPaths = new Set(Object.keys(mounted).map((key) => parseRouteKey(key).path));
78
- const collisions = Object.keys(routes).filter((key) => mountedPaths.has(parseRouteKey(key).path));
79
- if (collisions.length > 0) {
80
- throw new Error(`channel route(s) ${collisions.map((key) => `"${key}"`).join(", ")} collide with the session control plane — ` +
81
- `rename the channel route or disable sessionControl in fastagent.config`);
82
- }
83
- return {
84
- routes: { ...routes, ...mounted },
85
- announce: (boundPort) => {
86
- // The state root normally exists (the opener mkdirs the sessions dir under it), but an
87
- // external --sessions-dir leaves it uncreated — and announce runs inside serve's listening
88
- // callback, where a throw is an unhandled rejection, not a one-line startup diagnostic.
89
- mkdirSync(stateRoot, { recursive: true, mode: 0o700 });
90
- const path = join(stateRoot, "control.json");
91
- // Atomic (tmp+rename, the state.ts pattern): attach re-reads this file exactly during the
92
- // restart window — a torn read would be misdiagnosed as "serve gone".
93
- const tmp = `${path}.tmp`;
94
- writeFileSync(tmp, `${JSON.stringify({ url: `http://${clientHost(options.host)}:${boundPort}`, token })}\n`, {
95
- mode: 0o600,
96
- });
97
- chmodSync(tmp, 0o600); // an existing file keeps its old mode on rewrite — pin it
98
- renameSync(tmp, path);
99
- log.info(`[fastagent] session control on /control/* (token in ${path})`);
100
- // The serve binds ALL interfaces by DEFAULT (containers require it), so /control/* is
101
- // LAN-reachable with the bearer token as the only protection — the tunnel and deploy paths warn
102
- // loudly, and the LAN path must not be the silent third way past the local trust story. A
103
- // loopback bind closes exactly that reach, so it earns silence.
104
- const bind = classifyBind(options.host);
105
- if (bind !== "loopback") {
106
- log.warn(`[fastagent] the port binds ${bind === "wildcard" ? "all interfaces" : `${options.host} (off this machine)`}: ` +
107
- "/control/* is reachable on your LAN, protected only by the bearer token — bind loopback " +
108
- "(--bind 127.0.0.1), firewall the port, or wrap it for real exposure (docs: design §14)");
109
- }
110
- if (options.tunnel) {
111
- // Local trust = the token + its file permissions; --tunnel takes the whole port PUBLIC
112
- // (beyond even the LAN reach the mount already warned about). The operator asked for the tunnel (webhooks), but must not DISCOVER the control
113
- // plane went public with it — say it loudly.
114
- log.warn("[fastagent] --tunnel exposes /control/* (steer/abort/set_model) at the public tunnel URL, " +
115
- "protected ONLY by the bearer token — wrap it with real auth before sharing that URL (docs: design §14)");
116
- }
117
- // Best-effort lifecycle end: a clean exit removes the discovery file so a later `attach`
118
- // fails with "cannot read" (accurate) instead of a stale token's misleading 401/ECONNREFUSED.
119
- const unlink = () => {
120
- try {
121
- rmSync(path, { force: true });
122
- }
123
- catch {
124
- /* the file is advisory — exit must not fail on it */
125
- }
126
- };
127
- // Signal handlers MUST NOT absorb termination: registering any listener disables Node's
128
- // default kill, so clean up and RE-RAISE. The mechanism: `process.kill` delivery is ASYNC —
129
- // it lands after the current emit completes, so every listener of this same emit (scheduler
130
- // stop, tunnel close — regardless of registration order) runs first, and the re-raised
131
- // signal then hits the default action because each `once` handler is already consumed. When
132
- // some listener exits the process itself (the tunnel path calls process.exit(0)), the
133
- // re-raise is harmless redundancy. Without this, the first Ctrl+C would leave the serve
134
- // alive minus its control.json, and dev's watch restart (SIGTERM → wait for exit → respawn)
135
- // would hang on a worker that never exits.
136
- const unlinkAndReraise = (signal) => () => {
137
- unlink();
138
- process.kill(process.pid, signal);
139
- };
140
- process.once("SIGINT", unlinkAndReraise("SIGINT"));
141
- process.once("SIGTERM", unlinkAndReraise("SIGTERM"));
142
- process.once("exit", unlink);
143
- },
144
- };
145
- }
146
- /**
147
- * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) over the serving routes —
148
- * the deployed container's ONLY reachable surface (channels/agentcore.ts). Wired by `start` when
149
- * `FASTAGENT_AGENTCORE=1` (set by the generated deploy artifacts, never by hand). A channel colliding
150
- * on either path fails startup, same disposition as the control-plane mount: the adapter's paths are
151
- * the platform's contract, so a channel shadowing them would silently unserve the whole deployment.
152
- */
153
- export function mountAgentcore(routes, options) {
154
- const { agent, stateRoot, schedules, onStateReady, lazyChannels } = options;
155
- const mounted = agentcoreRoutes({
156
- routes: lazyChannels ?? routes,
157
- agent,
158
- stateRoot,
159
- isBusy: () => activeWork() > 0,
160
- // Cross-deploy durability: AgentCore wipes the state mount on every runtime version update, so
161
- // the state root is restored from (and pushed to) an S3 snapshot through presigned URLs the
162
- // forwarder mints per envelope. Always wired on this path — the platform gives no other way to
163
- // keep an agent's memory across a deploy.
164
- stateSync: createStateSync({ stateRoot }),
165
- // What separates a forwarder envelope from any IAM principal's InvokeAgentRuntime call. Absent =
166
- // no forwarder in this topology, so only the public `invoke` kind is servable.
167
- ingressSecret: process.env.FASTAGENT_INGRESS_SECRET,
168
- onStateReady,
169
- fire: schedules.length === 0
170
- ? undefined
171
- : (name, slot) => {
172
- const schedule = schedules.find((s) => s.name === name);
173
- if (!schedule)
174
- throw new UnknownScheduleError(name);
175
- return fireScheduleOnce({ agent, stateRoot, schedule, slot });
176
- },
177
- });
178
- const mountedPaths = new Set(Object.keys(mounted).map((key) => parseRouteKey(key).path));
179
- const collisions = Object.keys(routes).filter((key) => mountedPaths.has(parseRouteKey(key).path));
180
- if (collisions.length > 0) {
181
- throw new Error(`channel route(s) ${collisions.map((key) => `"${key}"`).join(", ")} collide with the AgentCore adapter ` +
182
- `(/invocations, /ping) — rename the channel route`);
183
- }
184
- return { ...routes, ...mounted };
185
- }
186
16
  /**
187
17
  * Refuse `--tunnel` with a bind that cloudflared cannot reach: it dials the NAME `localhost:<port>`
188
18
  * (the dev supervisor's tunnel too), so anything outside `127.0.0.1`/`::1`/wildcard — including a
@@ -205,6 +35,20 @@ export function assertTunnelBindable(host, tunnel, source) {
205
35
  failUsage(message);
206
36
  failStartup(new Error(message));
207
37
  }
38
+ /**
39
+ * The "we are serving" report: the supervisor message `dev`'s watcher waits for, the addresses, and
40
+ * what mounted. One function because both commands must say the same thing at the same moment —
41
+ * after readiness, never at socket bind.
42
+ */
43
+ export function reportServing(service, host, boundPort) {
44
+ process.send?.({ type: "ready", port: boundPort, routeChannels: service.channels.routes });
45
+ for (const line of readyAddressLines(host, boundPort, service.channels.builtinInvoke))
46
+ log.info(line);
47
+ log.info(`[fastagent] routes: ${Object.keys(service.routes).join(", ") || "(none)"}`);
48
+ if (service.channels.longConnections.length > 0) {
49
+ log.info(`[fastagent] long connections: ${service.channels.longConnections.join(", ")}`);
50
+ }
51
+ }
208
52
  /**
209
53
  * The startup lines that name WHERE the serve is: the bind report, and the curl the reader copies.
210
54
  * ONE function because they are one message — they were two, and `--bind` updated the first while the
@@ -224,81 +68,64 @@ export function readyAddressLines(host, boundPort, builtinInvoke) {
224
68
  }
225
69
  return lines;
226
70
  }
71
+ /** What the CLI gives a service to stop in, and the hard exit that follows it. The order matters:
72
+ * a forced exit before the service answers would report a clean shutdown over a stuck channel. */
73
+ export const SHUTDOWN_GRACE_MS = 800;
74
+ const FORCED_EXIT_MS = 1_500;
227
75
  /**
228
- * Bind HTTP, open long-connection channels, and report ready only when both forms are usable. Each
229
- * adapter owns reconnects; a terminal close rejects `closed` and fails the process visibly. Abort is
230
- * the sole clean-shutdown command. `host` unset binds all interfaces.
76
+ * Bind HTTP and report ready — but only once the SERVICE is, which is not the same moment: a bound
77
+ * socket is not a serving agent while a declared long-connection channel is still dialling, so this
78
+ * awaits `hooks.ready` (mountAgentService owns the connections themselves) before announcing
79
+ * anything. Signals are the sole clean-shutdown command; `host` unset binds all interfaces.
231
80
  */
232
- export function serve(surface, bind, onListening) {
81
+ export function serve(handler, bind, hooks = {}) {
233
82
  const { port, host } = bind;
234
- const hosted = serveNode(router(surface.routes), { port, host });
235
- const abort = new AbortController();
83
+ const hosted = serveNode(handler, { port, host });
236
84
  let stopping = false;
237
85
  const stop = (exitCode) => {
238
86
  if (stopping)
239
87
  return;
240
88
  stopping = true;
241
- abort.abort();
242
- const deadline = setTimeout(() => process.exit(exitCode), 1_000);
243
- void hosted
244
- .close()
245
- .catch(() => { })
246
- .finally(() => {
89
+ // Bounded: shutdown must not hang on a channel that will not close, so the deadline fires
90
+ // regardless. No drain an in-flight turn is cut, which is the existing contract.
91
+ // Later than the service's own close deadline (SHUTDOWN_GRACE_MS below), or the process leaves
92
+ // at 0 before `close()` has said a channel would not stop.
93
+ const deadline = setTimeout(() => {
94
+ log.error(`[fastagent] shutdown did not finish within ${FORCED_EXIT_MS}ms; exiting`);
95
+ process.exit(1);
96
+ }, FORCED_EXIT_MS);
97
+ // Stop accepting FIRST, before anything is awaited. `onShutdown` waits for long connections to
98
+ // close, and a socket still listening through that wait would dispatch new work into channels
99
+ // and a scheduler that are already shutting down.
100
+ const closingServer = hosted.close();
101
+ hosted.closeAllConnections();
102
+ // A cleanup that failed is not a clean exit: `close()` reports a channel that would not stop,
103
+ // and swallowing it here would end the process at 0 over a resource still holding on.
104
+ void Promise.allSettled([closingServer, Promise.resolve(hooks.onShutdown?.())]).then((outcomes) => {
105
+ let code = exitCode;
106
+ for (const outcome of outcomes) {
107
+ if (outcome.status === "rejected") {
108
+ log.error(`[fastagent] shutdown failed: ${String(outcome.reason)}`);
109
+ code = 1;
110
+ }
111
+ }
247
112
  clearTimeout(deadline);
248
- process.exit(exitCode);
113
+ process.exit(code);
249
114
  });
250
- // Preserve the existing no-drain shutdown contract: stop accepting first, then cut active streams.
251
- hosted.closeAllConnections();
252
115
  };
253
116
  process.once("SIGINT", () => stop(0));
254
117
  process.once("SIGTERM", () => stop(0));
255
118
  hosted.listening.then(async (boundPort) => {
256
119
  try {
257
- const runs = surface.longConnections.map((connection) => {
258
- const run = connection.connect(abort.signal);
259
- if (run === null ||
260
- typeof run !== "object" ||
261
- typeof run.ready?.then !== "function" ||
262
- typeof run.closed?.then !== "function") {
263
- throw new Error(`${connection.name} connect(signal) must return { ready: Promise, closed: Promise }`);
264
- }
265
- void run.closed.then(() => {
266
- if (!abort.signal.aborted)
267
- failStartup(new Error(`${connection.name} closed unexpectedly`));
268
- }, (error) => {
269
- if (!abort.signal.aborted)
270
- failStartup(new Error(`${connection.name} failed: ${String(error)}`));
271
- });
272
- return { connection, run };
273
- });
274
- await Promise.all(runs.map(async ({ connection, run }) => {
275
- await run.ready;
276
- if (!abort.signal.aborted)
277
- log.info(`[fastagent] long connection ready: ${connection.name}`);
278
- }));
279
- // Shutdown raced startup: a pre-ready abort settles `ready` as cancellation, not readiness —
280
- // stop() already owns the exit; don't mark ready or report a surface being torn down.
281
- if (abort.signal.aborted)
120
+ // A bound socket is NOT a serving agent: a declared socket-mode channel still has to come
121
+ // up, and reporting ready before it does tells the supervisor (and --tunnel, and the
122
+ // operator) that a surface is live while a channel is dead.
123
+ await hooks.ready;
124
+ if (stopping)
282
125
  return;
283
- surface.markReady();
284
- process.send?.({
285
- type: "ready",
286
- port: boundPort,
287
- routeChannels: surface.routeChannels,
288
- });
289
- for (const line of readyAddressLines(host, boundPort, surface.builtinInvoke))
290
- log.info(line);
291
- log.info(`[fastagent] routes: ${Object.keys(surface.routes).join(", ") || "(none)"}`);
292
- if (surface.longConnections.length > 0) {
293
- log.info(`[fastagent] long connections: ${surface.longConnections.map((connection) => connection.name).join(", ")}`);
294
- }
295
- onListening?.(boundPort);
126
+ hooks.onListening?.(boundPort);
296
127
  }
297
128
  catch (error) {
298
- abort.abort();
299
- const closing = hosted.close().catch(() => { });
300
- hosted.closeAllConnections();
301
- await closing;
302
129
  failStartup(error);
303
130
  }
304
131
  }, (error) => {
@@ -320,30 +147,12 @@ export function maybeTunnel(agentDir, routeChannels, boundPort, tunnel, stateRoo
320
147
  void startCloudflareTunnel(boundPort).then((instance) => {
321
148
  if (!instance)
322
149
  return;
323
- void announceWebhooks(agentDir, instance.url, { openUrl: openExternalUrl, routeChannels, stateRoot });
150
+ void announceWebhooks(agentDir, instance.url, declaredChannels(routeChannels), {
151
+ openUrl: openExternalUrl,
152
+ stateRoot,
153
+ });
324
154
  const cleanup = () => instance.close();
325
155
  process.once("SIGINT", cleanup);
326
156
  process.once("SIGTERM", cleanup);
327
157
  });
328
158
  }
329
- /**
330
- * Load and start the agent's `schedules/` — a time-trigger firing the agent on each cron. Starts iff
331
- * there are static schedules OR `selfSchedule` is on. Best-effort stop on process signals. Returns the
332
- * loaded schedules so a serving surface that needs them (the AgentCore adapter's fire binding) shares
333
- * ONE load instead of re-discovering. `externalClock` (AgentCore) arms no resident cron timers.
334
- */
335
- export async function startSchedules(agentDir, agent, stateRoot, selfSchedule, options = {}) {
336
- const { schedules, failures } = await loadSchedules(agentDir).catch(failStartup);
337
- reportModuleLoadFailures(failures);
338
- if (schedules.length === 0 && !selfSchedule)
339
- return schedules;
340
- const scheduler = createScheduler({ agent, stateRoot, schedules, externalClock: options.externalClock });
341
- scheduler.start();
342
- if (schedules.length > 0) {
343
- log.info(`[fastagent] schedules: ${schedules.map((s) => s.name).join(", ")}${options.externalClock ? " (external clock — no resident cron timers)" : ""}`);
344
- }
345
- const stop = () => scheduler.stop();
346
- process.once("SIGINT", stop);
347
- process.once("SIGTERM", stop);
348
- return schedules;
349
- }
@@ -1,16 +1,46 @@
1
1
  import { type LoginIO, type LoginMethod, type LoginResult } from "../engines/pi/login.ts";
2
+ import type { LoadedDefinition } from "../engines/pi/definition.ts";
3
+ import type { ModuleLoadFailure } from "../loader.ts";
4
+ import type { ToolCollision } from "../engines/pi/tool.ts";
5
+ /** What the startup report reads off an opened directory — a structural subset of the opener's return,
6
+ * spelled out so this module does not depend on that function's whole shape. */
7
+ export interface ReportableAssembly {
8
+ agentDir: string;
9
+ workspace: string;
10
+ modelSpec: string;
11
+ authPath: string;
12
+ config: {
13
+ thinkingLevel?: string;
14
+ };
15
+ definition: LoadedDefinition;
16
+ toolNames: string[];
17
+ deferredToolNames: string[];
18
+ toolCollisions: ToolCollision[];
19
+ toolFailures: ModuleLoadFailure[];
20
+ }
2
21
  /**
3
- * The padded label writer for the STARTUP report (`dev`/`start`, stderr via the log level). Hand-spaced
4
- * labels drift out of alignment the moment a longer one appears — which is exactly what happened when
5
- * `workspace:` joined `config:`/`model:`/`state:`. `info` keeps its own writer on purpose: its report is
6
- * stdout DATA (pipeable, its own label set, its own width), not a log line — the shared thing is the
7
- * policy (pad, never hand-space), not a constant.
22
+ * What `dev` and `start` say about the directory they just opened, in the order they say it.
23
+ *
24
+ * ONE function because it is one report: the two commands wrote it out line by line, and the copies
25
+ * had already diverged over which lines exist at all `dev` naming the config file, `start` naming
26
+ * state and sessions. A line added to one of two hand-written copies is invisible in the other.
27
+ *
28
+ * The divergence itself is PRESERVED, not resolved: `start`'s `state:`/`sessions:` pair introduces the
29
+ * persistence warnings that follow it in production posture, and `dev`'s `config:` has no counterpart
30
+ * there. Both are passed as explicit extras by the caller, so the asymmetry is visible at the call
31
+ * site rather than buried in two copies of a list. Whether it is RIGHT is a separate question from
32
+ * whether it has one owner.
33
+ *
34
+ * Findings (skill collisions, definition diagnostics) CLOSE the report — they are about the assembly
35
+ * just printed. What a command says next is its own posture talk, not report: `start`'s persistence
36
+ * notes now follow them rather than precede them.
8
37
  */
9
- export declare function reportLine(label: string, value: string): void;
10
- /** The workspace hint under the `agent:`/`workspace:` pair, when there is one ({@link workspaceHint}):
11
- * you pointed at the agent, and the project around it is probably what you meant. A hint, so it renders
12
- * as one and is silent otherwise `dev` and `start` both print the pair, so both ask for it. */
13
- export declare function reportWorkspaceHint(hint: string | undefined): void;
38
+ export declare function reportAssembly(a: ReportableAssembly, extras?: {
39
+ /** Printed between `workspace:`/`hint:` and `model:` (`dev` names the config file here). */
40
+ beforeModel?: [label: string, value: string][];
41
+ /** Printed after the tool lines, before findings (`start` names state + sessions here). */
42
+ afterTools?: [label: string, value: string][];
43
+ }): Promise<void>;
14
44
  /** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
15
45
  export declare function isInteractive(): boolean;
16
46
  /**
@@ -12,27 +12,74 @@ import { isValidPort, listModels, loadConfig, providerOf, resolveAuthPath, resol
12
12
  import { LoginCancelled, loginFlow } from "../engines/pi/login.js";
13
13
  import { createPiModelRuntime, createPiModels, probeApiKey, probeAuthSource, providerAuthStatuses, } from "../engines/pi/models.js";
14
14
  import { formatAuthReport } from "./auth-view.js";
15
- import { log } from "../log.js";
15
+ import { CODING_TOOL_NAMES } from "../engines/pi/create.js";
16
+ import { reportFindingsIfChanged, reportToolCollisions } from "../engines/pi/report.js";
17
+ import { workspaceHint } from "../paths.js";
18
+ import { log, reportModuleLoadFailures } from "../log.js";
16
19
  import { openExternalUrl } from "../open-url.js";
17
20
  import { bindAddress, isBindAddress } from "../bind.js";
18
21
  import { failStartup, failUsage } from "./fail.js";
19
22
  /**
20
23
  * The padded label writer for the STARTUP report (`dev`/`start`, stderr via the log level). Hand-spaced
21
24
  * labels drift out of alignment the moment a longer one appears — which is exactly what happened when
22
- * `workspace:` joined `config:`/`model:`/`state:`. `info` keeps its own writer on purpose: its report is
25
+ * `codingTools:` joined `workspace:`/`config:`/`model:`/`state:`. `info` keeps its own writer on purpose: its report is
23
26
  * stdout DATA (pipeable, its own label set, its own width), not a log line — the shared thing is the
24
27
  * policy (pad, never hand-space), not a constant.
28
+ *
29
+ * Private to this module: {@link reportAssembly} is the report, and a command reaching past it for a
30
+ * line of its own is how the two copies of that report came to differ.
25
31
  */
26
- export function reportLine(label, value) {
27
- log.info(`[fastagent] ${`${label}:`.padEnd(11)}${value}`);
32
+ function reportLine(label, value) {
33
+ log.info(`[fastagent] ${`${label}:`.padEnd(13)}${value}`);
28
34
  }
29
35
  /** The workspace hint under the `agent:`/`workspace:` pair, when there is one ({@link workspaceHint}):
30
36
  * you pointed at the agent, and the project around it is probably what you meant. A hint, so it renders
31
- * as one and is silent otherwise — `dev` and `start` both print the pair, so both ask for it. */
32
- export function reportWorkspaceHint(hint) {
37
+ * as one and is silent otherwise. */
38
+ function reportWorkspaceHint(hint) {
33
39
  if (hint)
34
40
  reportLine("hint", hint);
35
41
  }
42
+ /**
43
+ * What `dev` and `start` say about the directory they just opened, in the order they say it.
44
+ *
45
+ * ONE function because it is one report: the two commands wrote it out line by line, and the copies
46
+ * had already diverged over which lines exist at all — `dev` naming the config file, `start` naming
47
+ * state and sessions. A line added to one of two hand-written copies is invisible in the other.
48
+ *
49
+ * The divergence itself is PRESERVED, not resolved: `start`'s `state:`/`sessions:` pair introduces the
50
+ * persistence warnings that follow it in production posture, and `dev`'s `config:` has no counterpart
51
+ * there. Both are passed as explicit extras by the caller, so the asymmetry is visible at the call
52
+ * site rather than buried in two copies of a list. Whether it is RIGHT is a separate question from
53
+ * whether it has one owner.
54
+ *
55
+ * Findings (skill collisions, definition diagnostics) CLOSE the report — they are about the assembly
56
+ * just printed. What a command says next is its own posture talk, not report: `start`'s persistence
57
+ * notes now follow them rather than precede them.
58
+ */
59
+ export async function reportAssembly(a, extras = {}) {
60
+ reportLine("agent", a.agentDir);
61
+ reportLine("workspace", a.workspace);
62
+ reportWorkspaceHint(workspaceHint(a));
63
+ for (const [label, value] of extras.beforeModel ?? [])
64
+ reportLine(label, value);
65
+ reportLine("model", `${a.modelSpec}${a.config.thinkingLevel ? ` (thinking: ${a.config.thinkingLevel})` : ""}`);
66
+ await reportAuth(a.agentDir, a.modelSpec, a.authPath);
67
+ reportLine("context", a.definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
68
+ if (a.definition.persona)
69
+ reportLine("persona", "persona.md");
70
+ reportLine("skills", a.definition.skills.map((s) => s.name).join(", ") || "(none)");
71
+ reportLine("codingTools", CODING_TOOL_NAMES.join(", "));
72
+ if (a.toolNames.length > 0)
73
+ reportLine("tools", a.toolNames.join(", "));
74
+ if (a.deferredToolNames.length > 0) {
75
+ reportLine("deferred", `${a.deferredToolNames.join(", ")} (activated via search_tools)`);
76
+ }
77
+ reportToolCollisions(a.toolCollisions);
78
+ reportModuleLoadFailures(a.toolFailures);
79
+ for (const [label, value] of extras.afterTools ?? [])
80
+ reportLine(label, value);
81
+ reportFindingsIfChanged(a.definition.dir, a.definition);
82
+ }
36
83
  /** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
37
84
  export function isInteractive() {
38
85
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
package/dist/core.d.ts CHANGED
@@ -1,12 +1,10 @@
1
1
  export type { Agent, AgentEvent, ImageRef, Json, Prompt, Scope } from "./agent.ts";
2
2
  export { collect, AgentFailure, type CollectResult } from "./collect.ts";
3
3
  export type { ModuleLoadFailure } from "./loader.ts";
4
- export { createInvokeHandler, nodeListener } from "./channels/http.ts";
5
- export { controlRoutes, type ControlRoutesOptions, type WireEvent } from "./channels/control.ts";
4
+ export { createInvokeHandler } from "./channels/http.ts";
5
+ export type { WireEvent } from "./channels/control.ts";
6
6
  export { ControlRequestError, connectAgent, connectSessionControl, type RemoteEndpointOptions, } from "./session-remote.ts";
7
7
  export { readBodyCapped } from "./channels/body.ts";
8
8
  export { text, textHeaders } from "./channels/respond.ts";
9
- export { type ChannelContext, type ChannelHandler, type ChannelModule, type LongConnection, type LongConnectionChannelModule, type Routes, router, serveNode, } from "./host/node.ts";
9
+ export type { ChannelContext, ChannelHandler, ChannelModule, LongConnection, LongConnectionChannelModule, Routes, } from "./channel.ts";
10
10
  export { defineSchedule, type LoadedSchedule, type Schedule } from "./schedule/schedule.ts";
11
- export { discoverScheduleFiles, loadSchedules } from "./schedule/discover.ts";
12
- export { createScheduler, scheduleSession, type Scheduler, type SchedulerOptions } from "./schedule/scheduler.ts";
package/dist/core.js CHANGED
@@ -1,10 +1,12 @@
1
1
  export { collect, AgentFailure } from "./collect.js";
2
- export { createInvokeHandler, nodeListener } from "./channels/http.js";
3
- export { controlRoutes } from "./channels/control.js";
2
+ export { createInvokeHandler } from "./channels/http.js";
4
3
  export { ControlRequestError, connectAgent, connectSessionControl, } from "./session-remote.js";
5
4
  export { readBodyCapped } from "./channels/body.js";
6
5
  export { text, textHeaders } from "./channels/respond.js";
7
- export { router, serveNode, } from "./host/node.js";
6
+ // Mounting only. Composing a route table (`router`) and owning a prefix (`PrefixMount`) are how
7
+ // `createAgentService` assembles a service — not something a caller has to reproduce.
8
+ // Binding to a Node server lives at `/node`: it is the only runtime-specific piece here, and the
9
+ // only one that costs a third-party package.
10
+ // `defineSchedule` is what a `schedules/*.ts` file is written against. Discovering those files and
11
+ // running the clock is what `createAgentService` does with them — parts a caller does not reproduce.
8
12
  export { defineSchedule } from "./schedule/schedule.js";
9
- export { discoverScheduleFiles, loadSchedules } from "./schedule/discover.js";
10
- export { createScheduler, scheduleSession } from "./schedule/scheduler.js";
@@ -29,7 +29,7 @@ export type AgentcoreLogsOutcome = {
29
29
  };
30
30
  /**
31
31
  * Find and tail one AgentCore log source. Discovery is dynamic rather than spelling `-DEFAULT`:
32
- * endpoint naming belongs to AWS, and an edited stack may use a different endpoint. Runtime tailing
33
- * filters the log STREAM prefix so OTEL/spans in the same group never pollute the application log.
32
+ * endpoint naming belongs to AWS, and an edited stack may use a different endpoint. The whole group
33
+ * is tailed — see the file header for why a stream filter cannot narrow it and must not be added.
34
34
  */
35
35
  export declare function tailAgentcoreLogs(plan: AgentcoreLogsPlan, aws: CliRunner, announce?: (message: string) => void): Promise<AgentcoreLogsOutcome>;
@@ -17,8 +17,8 @@ function parseLogGroupNames(stdout) {
17
17
  }
18
18
  /**
19
19
  * Find and tail one AgentCore log source. Discovery is dynamic rather than spelling `-DEFAULT`:
20
- * endpoint naming belongs to AWS, and an edited stack may use a different endpoint. Runtime tailing
21
- * filters the log STREAM prefix so OTEL/spans in the same group never pollute the application log.
20
+ * endpoint naming belongs to AWS, and an edited stack may use a different endpoint. The whole group
21
+ * is tailed — see the file header for why a stream filter cannot narrow it and must not be added.
22
22
  */
23
23
  export async function tailAgentcoreLogs(plan, aws, announce = () => { }) {
24
24
  const stack = `fastagent-${plan.name}`;
@@ -1,4 +1,4 @@
1
- import type { ChannelKind } from "../../scaffold/add-channel.ts";
1
+ import type { DeclaredChannel } from "../../channels/discover.ts";
2
2
  import { type Artifact, type ContainerInput } from "../container.ts";
3
3
  /** The one schedule fact the plan needs (from loadSchedules) — name + cron + tz. */
4
4
  export interface ScheduleFact {
@@ -11,10 +11,9 @@ export interface AgentcorePlanInput extends ContainerInput {
11
11
  name: string;
12
12
  /** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
13
13
  modelAuth: string | undefined;
14
- /** Known first-party channelseach contributes its secret metadata + webhook step. */
15
- channels: ChannelKind[];
16
- /** ALL route-channel basenames (customs included) — any of them requires the forwarder. */
17
- routeChannels: string[];
14
+ /** Every declared channel and its ingress the source of the secret list, the webhook steps, and
15
+ * whether the forwarder is needed at all (ANY webhook channel requires it, customs included). */
16
+ channels: readonly DeclaredChannel[];
18
17
  /** Extra secret env-var names (fastagent.config deploy.secrets). */
19
18
  extraSecrets?: string[];
20
19
  /** Static schedules — each becomes an EventBridge Scheduler rule targeting the forwarder. */
@@ -118,15 +117,18 @@ export declare function toEventBridgeCron(cron: string): {
118
117
  error: string;
119
118
  };
120
119
  /**
121
- * The forwarder Lambda source — the ONLY string both the template's inline ZipFile and the readable
120
+ * The forwarder Lambda source — the ONLY string both the deployment package and the readable
122
121
  * `lambda/forwarder.js` artifact are generated from (one source, no drift). Zero-dependency: the
123
- * Lambda Node runtime bundles AWS SDK v3. CommonJS ON PURPOSE: CloudFormation inline code always
124
- * lands as `index.js`, where ESM `import` is a syntax error (found by the first real deploy). Two
122
+ * Lambda Node runtime bundles AWS SDK v3. CommonJS ON PURPOSE: the package's entry lands as
123
+ * `index.js` ({@link zipSingleFile}), where ESM `import` is a syntax error. Two
125
124
  * event shapes: a Function URL webhook (reconstructed verbatim into a `webhook` envelope; the
126
125
  * channel's REAL response rides back inside the transport reply and is re-emitted byte-exact —
127
126
  * Feishu's URL-verification challenge depends on it), and an EventBridge Scheduler fire
128
- * (`{ scheduleFire }`, slot = the scheduled instant — the container's idempotency key). MUST stay
129
- * under CloudFormation's 4096-byte inline-code cap.
127
+ * (`{ scheduleFire }`, slot = the scheduled instant — the container's idempotency key).
128
+ *
129
+ * It ships as an S3 object rather than inline `ZipFile`, so there is no 4096-byte ceiling on it — a
130
+ * constraint this comment asserted for long after the template stopped inlining, which is the kind
131
+ * of stale rule that stops the next person making a correct change.
130
132
  */
131
133
  export declare function forwarderSource(): string;
132
134
  /**