@fastagent-sh/fastagent 0.14.0 → 0.16.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 (277) hide show
  1. package/README.md +16 -6
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -0
  4. package/dist/channels/agentcore-limits.d.ts +7 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-state.d.ts +83 -0
  7. package/dist/channels/agentcore-state.js +258 -0
  8. package/dist/channels/agentcore.d.ts +98 -0
  9. package/dist/channels/agentcore.js +260 -0
  10. package/dist/channels/busy.d.ts +23 -0
  11. package/dist/channels/busy.js +53 -0
  12. package/dist/channels/context-buffer.d.ts +35 -0
  13. package/dist/channels/context-buffer.js +103 -0
  14. package/dist/channels/control.d.ts +28 -0
  15. package/dist/channels/control.js +214 -0
  16. package/dist/channels/feishu/card.js +1 -1
  17. package/dist/channels/feishu/context-buffer.d.ts +15 -13
  18. package/dist/channels/feishu/context-buffer.js +62 -69
  19. package/dist/channels/feishu/feishu-api.d.ts +11 -3
  20. package/dist/channels/feishu/feishu-api.js +24 -3
  21. package/dist/channels/feishu/feishu.d.ts +27 -27
  22. package/dist/channels/feishu/feishu.js +318 -207
  23. package/dist/channels/feishu/invoke-turn.d.ts +7 -20
  24. package/dist/channels/feishu/invoke-turn.js +44 -79
  25. package/dist/channels/feishu/model.d.ts +2 -2
  26. package/dist/channels/feishu/normalize.d.ts +2 -1
  27. package/dist/channels/feishu/normalize.js +0 -1
  28. package/dist/channels/feishu/parse.d.ts +22 -8
  29. package/dist/channels/feishu/parse.js +24 -7
  30. package/dist/channels/feishu/preview.d.ts +4 -7
  31. package/dist/channels/feishu/preview.js +27 -142
  32. package/dist/channels/feishu/register-app.d.ts +2 -1
  33. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  34. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  35. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  36. package/dist/channels/feishu/setup-mode.d.ts +37 -0
  37. package/dist/channels/feishu/setup-mode.js +28 -0
  38. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  39. package/dist/channels/feishu/ws-ingress.js +136 -0
  40. package/dist/channels/github/github.js +16 -7
  41. package/dist/channels/http.d.ts +14 -0
  42. package/dist/channels/http.js +36 -3
  43. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  44. package/dist/channels/invoke-turn-kit.js +87 -0
  45. package/dist/channels/lark/lark.d.ts +4 -2
  46. package/dist/channels/lark/lark.js +4 -1
  47. package/dist/channels/lark/onboard.d.ts +8 -4
  48. package/dist/channels/lark/onboard.js +8 -0
  49. package/dist/channels/lark/scaffold/channel.ts +9 -8
  50. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  51. package/dist/channels/preview-kit.d.ts +115 -0
  52. package/dist/channels/preview-kit.js +184 -0
  53. package/dist/channels/seen.d.ts +5 -0
  54. package/dist/channels/seen.js +35 -0
  55. package/dist/channels/slack/bot-auth.d.ts +15 -0
  56. package/dist/channels/slack/bot-auth.js +146 -0
  57. package/dist/channels/slack/config-api.d.ts +60 -0
  58. package/dist/channels/slack/config-api.js +149 -0
  59. package/dist/channels/slack/context-buffer.d.ts +24 -0
  60. package/dist/channels/slack/context-buffer.js +37 -0
  61. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  62. package/dist/channels/slack/invoke-turn.js +63 -0
  63. package/dist/channels/slack/manifest.d.ts +49 -0
  64. package/dist/channels/slack/manifest.js +69 -0
  65. package/dist/channels/slack/model.d.ts +67 -0
  66. package/dist/channels/slack/model.js +2 -0
  67. package/dist/channels/slack/onboard.d.ts +41 -0
  68. package/dist/channels/slack/onboard.js +120 -0
  69. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  70. package/dist/channels/slack/onboarding-state.js +69 -0
  71. package/dist/channels/slack/parse.d.ts +38 -0
  72. package/dist/channels/slack/parse.js +124 -0
  73. package/dist/channels/slack/preview.d.ts +23 -0
  74. package/dist/channels/slack/preview.js +403 -0
  75. package/dist/channels/slack/reaction.d.ts +24 -0
  76. package/dist/channels/slack/reaction.js +62 -0
  77. package/dist/channels/slack/register-webhook.d.ts +10 -0
  78. package/dist/channels/slack/register-webhook.js +49 -0
  79. package/dist/channels/slack/scaffold/channel.ts +33 -0
  80. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  81. package/dist/channels/slack/setup-server.d.ts +17 -0
  82. package/dist/channels/slack/setup-server.js +103 -0
  83. package/dist/channels/slack/slack-api.d.ts +57 -0
  84. package/dist/channels/slack/slack-api.js +399 -0
  85. package/dist/channels/slack/slack.d.ts +51 -0
  86. package/dist/channels/slack/slack.js +496 -0
  87. package/dist/channels/slack/welcomed.d.ts +5 -0
  88. package/dist/channels/slack/welcomed.js +32 -0
  89. package/dist/channels/state.d.ts +11 -4
  90. package/dist/channels/state.js +22 -15
  91. package/dist/channels/stop-command.d.ts +6 -0
  92. package/dist/channels/stop-command.js +36 -0
  93. package/dist/channels/tasks.d.ts +7 -0
  94. package/dist/channels/tasks.js +25 -0
  95. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  96. package/dist/channels/telegram/context-buffer.js +6 -85
  97. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  98. package/dist/channels/telegram/invoke-turn.js +11 -58
  99. package/dist/channels/telegram/parse.d.ts +0 -7
  100. package/dist/channels/telegram/parse.js +4 -2
  101. package/dist/channels/telegram/preview.d.ts +4 -7
  102. package/dist/channels/telegram/preview.js +24 -142
  103. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  104. package/dist/channels/telegram/telegram.js +24 -10
  105. package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
  106. package/dist/channels/{feishu/text.js → text.js} +15 -1
  107. package/dist/channels/thread-participants.d.ts +21 -0
  108. package/dist/channels/thread-participants.js +132 -0
  109. package/dist/channels/turn-queue.js +8 -1
  110. package/dist/channels/turn-store.d.ts +1 -1
  111. package/dist/channels/turn-store.js +2 -3
  112. package/dist/cli/add-feishu.d.ts +30 -0
  113. package/dist/cli/add-feishu.js +366 -0
  114. package/dist/cli/add-slack.d.ts +11 -0
  115. package/dist/cli/add-slack.js +199 -0
  116. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  117. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  118. package/dist/cli/commands/add.d.ts +4 -0
  119. package/dist/cli/commands/add.js +191 -60
  120. package/dist/cli/commands/attach.d.ts +82 -0
  121. package/dist/cli/commands/attach.js +563 -0
  122. package/dist/cli/commands/chat.d.ts +1 -0
  123. package/dist/cli/commands/chat.js +12 -7
  124. package/dist/cli/commands/deploy.d.ts +3 -1
  125. package/dist/cli/commands/deploy.js +384 -82
  126. package/dist/cli/commands/dev.js +34 -26
  127. package/dist/cli/commands/fire.js +17 -18
  128. package/dist/cli/commands/info.js +36 -29
  129. package/dist/cli/commands/init.d.ts +1 -1
  130. package/dist/cli/commands/init.js +65 -53
  131. package/dist/cli/commands/invoke.js +11 -9
  132. package/dist/cli/commands/login.js +48 -45
  133. package/dist/cli/commands/models.js +1 -1
  134. package/dist/cli/commands/schedule.js +6 -8
  135. package/dist/cli/commands/start.js +102 -39
  136. package/dist/cli/commands/tool.js +23 -18
  137. package/dist/cli/fail.d.ts +17 -2
  138. package/dist/cli/fail.js +25 -1
  139. package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
  140. package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
  141. package/dist/cli/kernel.d.ts +3 -2
  142. package/dist/cli/models-view.d.ts +21 -0
  143. package/dist/cli/models-view.js +66 -0
  144. package/dist/cli/program.js +135 -56
  145. package/dist/cli/serve.d.ts +60 -20
  146. package/dist/cli/serve.js +252 -47
  147. package/dist/cli/shared.d.ts +40 -7
  148. package/dist/cli/shared.js +180 -38
  149. package/dist/collect.d.ts +14 -3
  150. package/dist/collect.js +24 -0
  151. package/dist/core.d.ts +3 -1
  152. package/dist/core.js +2 -0
  153. package/dist/deploy/agentcore/plan.d.ts +117 -0
  154. package/dist/deploy/agentcore/plan.js +721 -0
  155. package/dist/deploy/agentcore/run.d.ts +73 -0
  156. package/dist/deploy/agentcore/run.js +412 -0
  157. package/dist/deploy/agentcore/zip.d.ts +17 -0
  158. package/dist/deploy/agentcore/zip.js +68 -0
  159. package/dist/deploy/container.d.ts +26 -25
  160. package/dist/deploy/container.js +93 -89
  161. package/dist/deploy/docker/plan.d.ts +3 -3
  162. package/dist/deploy/docker/plan.js +20 -23
  163. package/dist/deploy/fly/plan.d.ts +5 -1
  164. package/dist/deploy/fly/plan.js +39 -26
  165. package/dist/deploy/fly/run.d.ts +17 -5
  166. package/dist/deploy/fly/run.js +52 -7
  167. package/dist/deploy/preflight.d.ts +20 -8
  168. package/dist/deploy/preflight.js +267 -71
  169. package/dist/deploy/railway/plan.d.ts +10 -1
  170. package/dist/deploy/railway/plan.js +53 -20
  171. package/dist/deploy/railway/run.d.ts +13 -5
  172. package/dist/deploy/railway/run.js +22 -7
  173. package/dist/deploy/runner.d.ts +7 -3
  174. package/dist/deploy/runner.js +9 -3
  175. package/dist/deploy/secrets.d.ts +2 -1
  176. package/dist/deploy/secrets.js +23 -3
  177. package/dist/dev-supervisor.d.ts +11 -10
  178. package/dist/dev-supervisor.js +56 -50
  179. package/dist/engines/pi/auth.d.ts +8 -7
  180. package/dist/engines/pi/auth.js +12 -10
  181. package/dist/engines/pi/channel.d.ts +22 -16
  182. package/dist/engines/pi/channel.js +92 -62
  183. package/dist/engines/pi/chat.d.ts +4 -16
  184. package/dist/engines/pi/chat.js +8 -261
  185. package/dist/engines/pi/config.d.ts +20 -55
  186. package/dist/engines/pi/config.js +49 -121
  187. package/dist/engines/pi/create.d.ts +30 -10
  188. package/dist/engines/pi/create.js +33 -26
  189. package/dist/engines/pi/definition.d.ts +7 -26
  190. package/dist/engines/pi/definition.js +8 -54
  191. package/dist/engines/pi/harness.d.ts +49 -0
  192. package/dist/engines/pi/harness.js +90 -2
  193. package/dist/engines/pi/invoke.d.ts +55 -2
  194. package/dist/engines/pi/invoke.js +304 -19
  195. package/dist/engines/pi/login.d.ts +12 -1
  196. package/dist/engines/pi/login.js +16 -4
  197. package/dist/engines/pi/models.d.ts +59 -13
  198. package/dist/engines/pi/models.js +62 -24
  199. package/dist/engines/pi/open.d.ts +114 -0
  200. package/dist/engines/pi/open.js +127 -0
  201. package/dist/engines/pi/session-builder.d.ts +16 -0
  202. package/dist/engines/pi/session-builder.js +308 -0
  203. package/dist/engines/pi/session-control.d.ts +50 -0
  204. package/dist/engines/pi/session-control.js +604 -0
  205. package/dist/engines/pi/sessions.d.ts +17 -2
  206. package/dist/engines/pi/sessions.js +9 -0
  207. package/dist/engines/pi/tool-context.d.ts +18 -11
  208. package/dist/engines/pi/tool-context.js +3 -9
  209. package/dist/engines/pi/tool.d.ts +6 -8
  210. package/dist/engines/pi/tool.js +10 -1
  211. package/dist/engines/pi/wake-tool.d.ts +0 -3
  212. package/dist/engines/pi/wake-tool.js +9 -7
  213. package/dist/env.d.ts +16 -4
  214. package/dist/env.js +43 -5
  215. package/dist/feishu.d.ts +1 -1
  216. package/dist/feishu.js +1 -1
  217. package/dist/host/node.d.ts +25 -8
  218. package/dist/host/node.js +5 -4
  219. package/dist/index.d.ts +1 -0
  220. package/dist/index.js +1 -0
  221. package/dist/lark.d.ts +1 -1
  222. package/dist/lark.js +1 -1
  223. package/dist/loader.d.ts +2 -2
  224. package/dist/loader.js +3 -3
  225. package/dist/log.d.ts +1 -1
  226. package/dist/log.js +1 -1
  227. package/dist/observe.js +3 -0
  228. package/dist/paths.d.ts +138 -0
  229. package/dist/paths.js +326 -0
  230. package/dist/pi.d.ts +6 -5
  231. package/dist/pi.js +4 -3
  232. package/dist/runtime.d.ts +7 -5
  233. package/dist/runtime.js +2 -2
  234. package/dist/scaffold/add-channel.d.ts +26 -9
  235. package/dist/scaffold/add-channel.js +175 -40
  236. package/dist/scaffold/init.d.ts +32 -41
  237. package/dist/scaffold/init.js +161 -185
  238. package/dist/scaffold/templates/env.example +15 -6
  239. package/dist/scaffold/templates/fastagent.config.mjs +5 -4
  240. package/dist/scaffold/templates/gitignore +14 -6
  241. package/dist/scaffold/templates/persona.md +4 -2
  242. package/dist/scaffold/templates/secrets.gitignore +5 -0
  243. package/dist/scaffold/templates.d.ts +1 -7
  244. package/dist/scaffold/templates.js +3 -25
  245. package/dist/scaffold/vendor-skill.d.ts +2 -2
  246. package/dist/scaffold/vendor-skill.js +13 -13
  247. package/dist/schedule/discover.js +4 -4
  248. package/dist/schedule/scheduler.d.ts +40 -1
  249. package/dist/schedule/scheduler.js +89 -56
  250. package/dist/schedule/state.js +1 -1
  251. package/dist/schedule/wake-alarm.d.ts +47 -0
  252. package/dist/schedule/wake-alarm.js +136 -0
  253. package/dist/schedule/wakeups.d.ts +1 -3
  254. package/dist/schedule/wakeups.js +19 -1
  255. package/dist/session-remote.d.ts +53 -0
  256. package/dist/session-remote.js +336 -0
  257. package/dist/session.d.ts +265 -0
  258. package/dist/session.js +37 -0
  259. package/dist/slack.d.ts +2 -0
  260. package/dist/slack.js +2 -0
  261. package/dist/tunnel.d.ts +7 -4
  262. package/dist/tunnel.js +23 -12
  263. package/package.json +22 -7
  264. package/dist/channels/feishu/owned-threads.d.ts +0 -11
  265. package/dist/channels/feishu/owned-threads.js +0 -47
  266. package/dist/channels/feishu/seen.d.ts +0 -5
  267. package/dist/channels/feishu/seen.js +0 -47
  268. package/dist/cli-add-feishu.d.ts +0 -8
  269. package/dist/cli-add-feishu.js +0 -223
  270. package/dist/cli-models.d.ts +0 -11
  271. package/dist/cli-models.js +0 -20
  272. package/dist/engines/pi/workspace.d.ts +0 -56
  273. package/dist/engines/pi/workspace.js +0 -72
  274. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  275. package/dist/scaffold/templates/gitignore.kit +0 -2
  276. package/dist/workspace.d.ts +0 -9
  277. package/dist/workspace.js +0 -45
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * The portable container — Dockerfile + .dockerignore, host-neutral. Every deploy target ships these
3
3
  * unchanged; only the host config (fly.toml / railway.json) and the CLI sequence differ. The image
4
- * runs `fastagent start /app` on `$PORT` with state under `$FASTAGENT_STATE_DIR` — standard container
5
- * conventions, no host coupling. Extracted from the Fly plan when Railway became the second target.
4
+ * runs `fastagent start /app` on `$PORT` with machinery under `$FASTAGENT_STATE_DIR` /
5
+ * `$FASTAGENT_SECRETS_DIR` — standard container conventions, no host coupling.
6
+ *
7
+ * ONE deploy semantic: bake the WORKSPACE as the image (`COPY . .` — what you see is what ships),
8
+ * with the agent at `./fastagent` inside it. The resolver in the image finds it exactly like dev does.
9
+ * Deps install under `/app/fastagent`, and the Dockerfile lives there too — namespaced so it never
10
+ * collides with one the workspace already owns.
6
11
  */
12
+ import { SECRETS_DIRNAME, STATE_DIRNAME } from "../paths.js";
7
13
  /** Fixed first-line prefix of a fastagent-generated Dockerfile — the ONE source both the generator (the
8
14
  * `head` below) and the "is this ours?" check (cli deploy's kept-Dockerfile apt warn) share, so they
9
15
  * can't drift into detecting a marker the generator no longer writes. */
@@ -15,41 +21,13 @@ export const GENERATED_DOCKERFILE_MARKER = "# Generated by `fastagent deploy`";
15
21
  export function isGeneratedDockerfile(content) {
16
22
  return content.startsWith(GENERATED_DOCKERFILE_MARKER);
17
23
  }
18
- /** The repo-as-workspace Dockerfile: bake the repo as cwd, install the kit's deps, run from the kit. */
19
- function kitDockerfile(input, kit) {
20
- const apt = aptLayer(input.apt);
21
- if (!input.hasPackageJson) {
22
- // Markdown-only kit: no deps to install the pinned global CLI serves the repo directly.
23
- return `${GENERATED_DOCKERFILE_MARKER}. Repo-as-workspace: the whole repo is the agent's cwd; the kit lives in ${kit}/.
24
- FROM node:22-slim
25
- ${apt}WORKDIR /app
26
- RUN npm i -g @fastagent-sh/fastagent@${input.version}
27
- COPY . .
28
- CMD ["fastagent", "start", "/app"]
29
- `;
30
- }
31
- const isBun = input.runtime === "bun";
32
- const base = isBun ? `oven/bun:${input.bunVersion ?? "1"}` : "node:22-slim";
33
- const head = `${GENERATED_DOCKERFILE_MARKER}. Repo-as-workspace: the whole repo is the agent's cwd; the kit lives in ${kit}/.
34
- # Only the KIT's deps are installed — the host repo's own deps are the agent's runtime concern (it can
35
- # install them in its workspace when its job needs them). .git IS shipped: write-back needs it.
36
- FROM ${base}
37
- ${apt}WORKDIR /app
38
- `;
39
- if (isBun) {
40
- const install = input.hasLockfile ? "bun install --frozen-lockfile" : "bun install";
41
- return `${head}COPY ${kit}/package.json ${kit}/bun.lock* ./${kit}/
42
- RUN cd ${kit} && ${install}
43
- COPY . .
44
- CMD ["sh", "-c", "cd ${kit} && bun run fastagent start /app"]
45
- `;
46
- }
47
- const install = input.hasLockfile ? "npm ci" : "npm install";
48
- return `${head}COPY ${kit}/package.json ${kit}/package-lock.json* ./${kit}/
49
- RUN cd ${kit} && ${install}
50
- COPY . .
51
- CMD ["./${kit}/node_modules/.bin/fastagent", "start", "/app"]
52
- `;
24
+ /** First line of a generated `.dockerignore` and, for the workspace-root one, the single artifact
25
+ * deploy writes outside the agent dir. The marker is what splits the two cases: ours is refreshed
26
+ * (regenerated under `--force`), yours is kept. */
27
+ const GENERATED_DOCKERIGNORE_MARKER = "# Generated by `fastagent deploy`";
28
+ /** Did fastagent generate this `.dockerignore`? (Same ownership rule as {@link isGeneratedDockerfile}.) */
29
+ export function isGeneratedDockerignore(content) {
30
+ return content.startsWith(GENERATED_DOCKERIGNORE_MARKER);
53
31
  }
54
32
  /** The apt layer (cached right after FROM). Debian default repos only. */
55
33
  function aptLayer(packages) {
@@ -61,21 +39,46 @@ function aptLayer(packages) {
61
39
  : "";
62
40
  }
63
41
  function dockerfile(input) {
42
+ // The whole workspace is baked at /app; deps install (and the local bin lives) under the agent, which
43
+ // is either a subdirectory of it or /app itself. `into` prefixes a path, `at` runs a command there.
44
+ const prefix = input.agentPrefix;
45
+ const into = (p) => `${prefix}${p}`;
46
+ // A build step that must run IN the agent dir (`RUN` already gets a shell from docker).
47
+ const at = (cmd) => (prefix ? `cd ${prefix.replace(/\/$/, "")} && ${cmd}` : cmd);
48
+ // The entrypoint. Only the BUN path needs a working directory (`bun run` resolves the script from the
49
+ // package.json beside it), and a `cd` there means a shell — with `exec`, or sh stays PID 1 and swallows
50
+ // the container's SIGTERM. The npm path needs no shell at all: the binary path is context-relative and
51
+ // `start /app` is absolute, so exec form is both correct and signal-clean. (A `cd` on that path was a
52
+ // bug: after `cd fastagent`, `./fastagent/node_modules/...` resolves one level too deep.)
53
+ const bunCmd = prefix
54
+ ? `["sh", "-c", "cd ${prefix.replace(/\/$/, "")} && exec bun run fastagent start /app"]`
55
+ : `["bun", "run", "fastagent", "start", "/app"]`;
56
+ const layoutNote = prefix
57
+ ? `The whole directory is the agent's workspace; the agent itself lives in ${prefix}`
58
+ : `The directory IS the agent — it is also its own workspace.`;
64
59
  // apt layer right after FROM (cached across code changes): the agent's tools may shell out to git etc.,
65
60
  // which node:22-slim lacks. Debian default repos only — a package needing a custom repo (gh) or a
66
61
  // different base is the operator's own Dockerfile (kept if present). deploy.apt is package-name-validated.
67
62
  const apt = aptLayer(input.apt);
63
+ // PIN the agent into the image. The container re-resolves placement at `/app` (the baked workspace),
64
+ // and a workspace may hold SEVERAL agents — all of which ship, since the build context is the whole
65
+ // tree. Without this the image would pick by the container's own rules (the `fastagent`-named one, or
66
+ // nothing) instead of the agent this deploy was FOR: the artifact would depend on the builder's
67
+ // environment, which is the one thing it must never do. Baked whenever the agent sits inside the
68
+ // workspace, single agent or not — the value is a fact of this image, and asserting it means a build
69
+ // context that dropped the agent fails loudly rather than serving a sibling.
70
+ const pin = prefix ? `ENV FASTAGENT_AGENT=${prefix.replace(/\/$/, "")}\n` : "";
68
71
  // No package.json → pure markdown/skills agent: install the pinned CLI GLOBALLY and run `fastagent`
69
72
  // from PATH. That needs npm + a global bin, which live on node:22-slim, NOT on oven/bun — so this path
70
73
  // pins node:22-slim regardless of input.runtime (a stray bun lockfile with no package.json would
71
74
  // otherwise select a bun base here and crash the `npm i -g` build). input.runtime is meaningful only
72
- // for a code workspace, so the bun-vs-node base below is scoped to that path.
75
+ // for a code agent, so the bun-vs-node base below is scoped to that path.
73
76
  if (!input.hasPackageJson) {
74
- return `${GENERATED_DOCKERFILE_MARKER}. The directory IS the agent — no build step.
77
+ return `${GENERATED_DOCKERFILE_MARKER}. ${layoutNote}
75
78
  # npm-based (a markdown/skills agent installs the pinned CLI globally; node:22-slim has npm).
76
79
  FROM node:22-slim
77
80
  ${apt}WORKDIR /app
78
- RUN npm i -g @fastagent-sh/fastagent@${input.version}
81
+ ${pin}RUN npm i -g @fastagent-sh/fastagent@${input.version}
79
82
  COPY . .
80
83
  CMD ["fastagent", "start", "/app"]
81
84
  `;
@@ -85,14 +88,16 @@ CMD ["fastagent", "start", "/app"]
85
88
  const note = isBun
86
89
  ? "# Bun-based (packageManager: bun / a bun lockfile detected). fastagent runs under Bun (bun run)."
87
90
  : "# npm-based — for pnpm/yarn, adapt the install line + the lockfile COPY (corepack enable, etc.).";
88
- const head = `${GENERATED_DOCKERFILE_MARKER}. The directory IS the agent — no build step.
91
+ const head = `${GENERATED_DOCKERFILE_MARKER}. ${layoutNote}
89
92
  ${note}
90
93
  FROM ${base}
91
94
  ${apt}WORKDIR /app
92
- `;
95
+ ${pin}`;
93
96
  // Install ALL deps (no --omit=dev / --production): a repo-as-agent (e.g. an Astro site it operates on)
94
97
  // needs its full toolchain — the build/check tools that live in devDependencies — to do its work, and
95
- // we can't tell a repo-as-agent from a purpose-built workspace, so the safe default keeps everything.
98
+ // we can't tell a repo-as-agent from a purpose-built agent, so the safe default keeps everything.
99
+ // Only the AGENT's deps are installed — the workspace's own deps are the agent's runtime concern
100
+ // (it can install them in its workspace when its job needs them).
96
101
  if (isBun) {
97
102
  // `--frozen-lockfile` needs bun.lock and hard-fails without it; fall back to a plain `bun install`
98
103
  // (resolves at build time — not reproducible; the CLI warns to commit the lockfile).
@@ -100,13 +105,13 @@ ${apt}WORKDIR /app
100
105
  // `bunx fastagent` would fall back to installing the npm package named `fastagent`, which is an
101
106
  // unrelated third-party package (ours is the scoped @fastagent-sh/fastagent).
102
107
  const install = input.hasLockfile ? "bun install --frozen-lockfile" : "bun install";
103
- return `${head}COPY package.json bun.lock* ./
104
- RUN ${install}
108
+ return `${head}COPY ${into("package.json")} ${into("bun.lock*")} ./${prefix}
109
+ RUN ${at(install)}
105
110
  COPY . .
106
- CMD ["bun", "run", "fastagent", "start", "/app"]
111
+ CMD ${bunCmd}
107
112
  `;
108
113
  }
109
- // `npm ci` requires a lockfile and hard-fails without one (a common `init --no-install` workspace);
114
+ // `npm ci` requires a lockfile and hard-fails without one (a common `init --no-install` agent);
110
115
  // fall back to `npm install` when there is none so the build never breaks. Caveat: `npm install`
111
116
  // resolves caret ranges at build time, so THIS branch is NOT reproducible — unlike the pinned
112
117
  // `npm ci` (lockfile) and pinned global (markdown) paths. The CLI warns to commit a lockfile.
@@ -114,60 +119,59 @@ CMD ["bun", "run", "fastagent", "start", "/app"]
114
119
  // `fastagent` when the dep is absent — an unrelated third-party package (ours is scoped). The local
115
120
  // path fails fast and visibly instead.
116
121
  const install = input.hasLockfile ? "npm ci" : "npm install";
117
- return `${head}COPY package.json package-lock.json* ./
118
- RUN ${install}
122
+ return `${head}COPY ${into("package.json")} ${into("package-lock.json*")} ./${prefix}
123
+ RUN ${at(install)}
119
124
  COPY . .
120
- CMD ["./node_modules/.bin/fastagent", "start", "/app"]
125
+ CMD ["./${into("node_modules/.bin/fastagent")}", "start", "/app"]
121
126
  `;
122
127
  }
123
128
  /** Patterns are RECURSIVE (`**​/`) on purpose — dockerignore patterns are root-anchored (unlike
124
- * .gitignore), and a repo-as-agent can hold nested projects: a bare `node_modules` would upload their
125
- * build-machine deps (macOS binaries!) and a bare `.env` would bake their secrets into the image.
126
- * `.git` stays root-anchored: nested projects' own `.git` ships (the agent's write-back needs it). */
127
- const DOCKERIGNORE = `**/node_modules
128
- **/.fastagent
129
+ * .gitignore), and a baked workspace can hold nested projects: a bare `node_modules` would upload
130
+ * their build-machine deps (macOS binaries!) and a bare `.env` would bake their secrets into the
131
+ * image. `.secrets`/`.state` are fastagent machinery secrets travel through the host's secret
132
+ * store, state lives on the volume; neither may ever enter an image. `.cache` is generic hygiene
133
+ * (a baked project's own build cache), not a fastagent directory.
134
+ * `.git` is deliberately SHIPPED: the deployed agent's write-back (pull/commit/push) needs the
135
+ * repo's history+remote — the WYSIWYG bake's freshness/durability loop runs through git, driven by
136
+ * the agent itself, not by deploy machinery. Shipping `.git` is only half of that loop: preflight
137
+ * bakes the git BINARY into the generated image iff the workspace ships a `.git` (the `shipsGit`
138
+ * fact); a non-git workspace that still needs git declares config.deploy.apt. */
139
+ const dockerignore = (input) => DOCKERIGNORE_BASE +
140
+ (input.machineryPaths ?? [])
141
+ // Skip what the name-based rules above already cover: the default `<agent>/.secrets` / `<agent>/.state`
142
+ // and anything inside them. What remains is a relocated dir whose NAME those rules cannot match.
143
+ .filter((p) => [`${input.agentPrefix}${SECRETS_DIRNAME}`, `${input.agentPrefix}${STATE_DIRNAME}`].every((covered) => p !== covered && !p.startsWith(`${covered}/`)))
144
+ .map((p) => `# resolved machinery path (FASTAGENT_SECRETS_DIR / FASTAGENT_AUTH_PATH / FASTAGENT_STATE_DIR)\n/${p}\n`)
145
+ .join("");
146
+ const DOCKERIGNORE_BASE = `${GENERATED_DOCKERIGNORE_MARKER}. Delete this line to take ownership (deploy then keeps your file).
147
+ **/node_modules
148
+ **/${SECRETS_DIRNAME}
149
+ **/${STATE_DIRNAME}
150
+ **/.cache
129
151
  **/.env
130
152
  **/.env.*
131
153
  !**/.env.example
132
154
  **/*.log
133
- # .git is excluded to keep the image small. If your agent runs git on its OWN history
134
- # (git log/blame over the repo it ships in), delete the next line so that history is in the image.
135
- .git
136
- `;
137
- /** The kit-layout ignore: `.git` is deliberately NOT excluded (write-back wants it — though whether it
138
- * survives is host-CLI-dependent; see containerArtifacts). Patterns are RECURSIVE (`**​/`) on purpose —
139
- * dockerignore patterns are root-anchored (unlike .gitignore), so a bare `node_modules` would NOT
140
- * exclude `agent/node_modules`: the build machine's kit deps (macOS binaries!) would be uploaded by
141
- * `COPY . .` and clobber the image's freshly-installed linux ones. */
142
- const KIT_DOCKERIGNORE = `**/node_modules
143
- **/.fastagent
144
- **/.env
145
- **/.env.*
146
- !**/.env.example
147
- **/*.log
148
- # .git is DELIBERATELY shipped: the agent's write-back (commit/push) needs the repo's history+remote.
155
+ # .git is deliberately shipped: the agent can pull to freshen content and push its work back
156
+ # (the generated image installs the git binary when this directory ships a .git; otherwise
157
+ # add deploy.apt ["git"]). For a smaller image with no git needs, add a ".git" line here.
149
158
  `;
150
159
  /**
151
- * The Dockerfile + ignore artifacts — spread into any host's artifact list. Kit layout ({@link
152
- * ContainerInput.kitDir}): the Dockerfile is namespaced under the kit (`agent/Dockerfile`) so it never
153
- * collides with the host repo's own. The ignore ships in TWO forms because context packing is
154
- * host-CLI-owned and inconsistent: (1) a ROOT `.dockerignore` the only form flyctl/railway's own
155
- * context packers reliably read (kept if the host already has one preflight then warns specifically
156
- * about a .git exclude / missing recursive node_modules)and (2) a
157
- * per-Dockerfile `agent/Dockerfile.dockerignore` for plain docker/buildx builds. Whether `.git`
158
- * actually reaches the box is host-CLI-dependent (some strip it from the upload regardless of any
159
- * ignore file) — the runbook's write-back note carries the runtime-clone fallback.
160
+ * The Dockerfile + ignore artifacts — spread into any host's artifact list. Everything is prefixed with
161
+ * {@link ContainerInput.agentPrefix}, so a NESTED agent's Dockerfile lands under `fastagent/` (never
162
+ * colliding with one the workspace already owns) while an agent that IS the workspace lands it at the root, where it IS
163
+ * the agent's own file. The ignore ships in TWO forms because context packing is host-CLI-owned and
164
+ * inconsistent: (1) a ROOT `.dockerignore` the only form flyctl/railway's own context packers
165
+ * reliably read (kept if the workspace already has one preflight then checks the machinery/secret
166
+ * excludes it must carry) — and (2) a per-Dockerfile `Dockerfile.dockerignore` for plain docker/buildx
167
+ * builds. When the agent sits inside the workspace that root file is the ONE write deploy makes outside the agent dir, and
168
+ * only at deploy time without it the host CLI's packer would bake `.secrets/` into the image.
160
169
  */
161
170
  export function containerArtifacts(input) {
162
- if (input.kitDir) {
163
- return [
164
- { path: `${input.kitDir}/Dockerfile`, content: kitDockerfile(input, input.kitDir) },
165
- { path: ".dockerignore", content: KIT_DOCKERIGNORE },
166
- { path: `${input.kitDir}/Dockerfile.dockerignore`, content: KIT_DOCKERIGNORE },
167
- ];
168
- }
171
+ const ignore = dockerignore(input);
169
172
  return [
170
- { path: "Dockerfile", content: dockerfile(input) },
171
- { path: ".dockerignore", content: DOCKERIGNORE },
173
+ { path: `${input.agentPrefix}Dockerfile`, content: dockerfile(input) },
174
+ { path: ".dockerignore", content: ignore },
175
+ { path: `${input.agentPrefix}Dockerfile.dockerignore`, content: ignore },
172
176
  ];
173
177
  }
@@ -15,6 +15,8 @@ export interface DockerPlanInput extends ContainerInput {
15
15
  modelAuth: string | undefined;
16
16
  /** Known channels contribute their environment-variable names and webhook registration. */
17
17
  channels: ChannelKind[];
18
+ /** All long-connection channel basenames, including custom channels. */
19
+ longConnectionChannels?: string[];
18
20
  /** Generate an optional Cloudflare Quick Tunnel service in Compose. Generation only; `--run` starts it. */
19
21
  tunnel: boolean;
20
22
  /** Extra environment-variable names declared in config.deploy.secrets. */
@@ -23,13 +25,11 @@ export interface DockerPlanInput extends ContainerInput {
23
25
  export interface DockerPlan {
24
26
  /** fastagent.compose.yml + the shared Dockerfile/ignore artifacts. */
25
27
  artifacts: Artifact[];
26
- /** Compose file path relative to the workspace root (namespaced for agentDir layouts). */
28
+ /** Compose file path relative to the workspace root (under the agent prefix). */
27
29
  composePath: string;
28
30
  /** Ordered local build/run/operate instructions. */
29
31
  runbook: string[];
30
32
  }
31
- export declare const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
32
- export declare const GENERATED_COMPOSE_MARKER = "# Generated by `fastagent deploy docker`";
33
33
  export declare const MIN_DOCKER_COMPOSE_VERSION = "2.3.3";
34
34
  /** Pinned: generated deployment artifacts must not silently move when Cloudflare publishes `latest`. */
35
35
  export declare const CLOUDFLARED_IMAGE = "cloudflare/cloudflared:2026.7.1";
@@ -1,8 +1,8 @@
1
1
  import { containerArtifacts } from "../container.js";
2
2
  import { deploymentSecrets, isEnvKey } from "../secrets.js";
3
3
  const MOUNT = "/data";
4
- export const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
5
- export const GENERATED_COMPOSE_MARKER = "# Generated by `fastagent deploy docker`";
4
+ const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
5
+ const GENERATED_COMPOSE_MARKER = "# Generated by `fastagent deploy docker`";
6
6
  export const MIN_DOCKER_COMPOSE_VERSION = "2.3.3";
7
7
  /** Pinned: generated deployment artifacts must not silently move when Cloudflare publishes `latest`. */
8
8
  export const CLOUDFLARED_IMAGE = "cloudflare/cloudflared:2026.7.1";
@@ -29,6 +29,7 @@ export function dockerWebhookPaths(channels) {
29
29
  const path = {
30
30
  github: "/webhook",
31
31
  telegram: "/telegram",
32
+ slack: "/slack",
32
33
  feishu: "/feishu",
33
34
  lark: "/lark",
34
35
  };
@@ -38,23 +39,16 @@ export function dockerWebhookPaths(channels) {
38
39
  function composeInterpolation(name) {
39
40
  return `\${${name}:-}`;
40
41
  }
41
- /** Relative path from the namespaced Compose file's directory back to the workspace/build root. */
42
- function buildContext(kitDir) {
43
- if (!kitDir)
44
- return ".";
45
- return kitDir
46
- .split("/")
47
- .map(() => "..")
48
- .join("/");
49
- }
50
42
  function composeYaml(input) {
51
- const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
43
+ const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets, input.longConnectionChannels);
52
44
  // Always leave the auth-seed seam in the committed topology. `--run` uses it for OAuth/stored auth;
53
45
  // it is empty otherwise. Values never land in this file — Compose interpolates them at invocation.
54
46
  const envNames = [...new Set([...secrets.map((secret) => secret.name), "FASTAGENT_AUTH_SEED"])];
55
47
  const secretEnv = envNames.map((name) => ` ${name}: "${composeInterpolation(name)}"`).join("\n");
56
- const context = buildContext(input.kitDir);
57
- const dockerfile = input.kitDir ? `${input.kitDir}/Dockerfile` : "Dockerfile";
48
+ // Compose sits beside the Dockerfile, under the agent prefix; the build context is always the
49
+ // WORKSPACE, so it climbs back out of the prefix (`..` per level, `.` when there is none).
50
+ const context = input.agentPrefix ? ".." : ".";
51
+ const dockerfile = `${input.agentPrefix}Dockerfile`;
58
52
  const tunnelService = input.tunnel
59
53
  ? `
60
54
  # Cloudflare Quick Tunnel: ephemeral URL, generated only with \`deploy docker --tunnel\`.
@@ -89,7 +83,9 @@ services:
89
83
  - "127.0.0.1:${input.port}:${input.port}"
90
84
  environment:
91
85
  PORT: "${input.port}"
92
- FASTAGENT_STATE_DIR: "${MOUNT}"
86
+ # Machinery on the ONE state volume: mutable state and (seeded, possibly rotated) secrets.
87
+ FASTAGENT_STATE_DIR: "${MOUNT}/.state"
88
+ FASTAGENT_SECRETS_DIR: "${MOUNT}/.secrets"
93
89
  ${secretEnv}
94
90
  volumes:
95
91
  - state:${MOUNT}
@@ -101,20 +97,21 @@ volumes:
101
97
  }
102
98
  /** Compute local-Docker artifacts + the runbook; no Docker process is touched here. */
103
99
  export function planDockerDeploy(input) {
104
- const composePath = input.kitDir ? `${input.kitDir}/${DOCKER_COMPOSE_FILE}` : DOCKER_COMPOSE_FILE;
100
+ const composePath = `${input.agentPrefix}${DOCKER_COMPOSE_FILE}`;
105
101
  const artifacts = [{ path: composePath, content: composeYaml(input) }, ...containerArtifacts(input)];
106
102
  const compose = `docker compose -f ${composePath}`;
107
- const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
103
+ const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets, input.longConnectionChannels);
108
104
  const required = secrets.filter((secret) => secret.required);
109
105
  const optional = secrets.filter((secret) => !secret.required);
110
- const paths = dockerWebhookPaths(input.channels);
106
+ const routeChannels = input.channels.filter((kind) => !input.longConnectionChannels?.includes(kind));
107
+ const paths = dockerWebhookPaths(routeChannels);
111
108
  const runbook = [
112
109
  `# Run FastAgent in local Docker. ${composePath} / Dockerfile(.dockerignore) are generated above.`,
113
110
  `# Existing artifacts are authoritative and kept; edit Dockerfile or ${composePath} freely.`,
114
111
  `# Prereqs: Docker Engine/Desktop with Compose >= ${MIN_DOCKER_COMPOSE_VERSION} (\`docker compose version\`).`,
115
112
  ];
116
113
  if (required.length > 0) {
117
- runbook.push(`# Required environment values (put them in the workspace .env or export them):`, ...required.map((secret) => `# ${secret.name}: ${secret.hint}`));
114
+ runbook.push(`# Required environment values (put them in the agent's .secrets/.env or export them):`, ...required.map((secret) => `# ${secret.name}: ${secret.hint}`));
118
115
  }
119
116
  if (optional.length > 0) {
120
117
  runbook.push(`# Optional environment values:`, ...optional.map((secret) => `# ${secret.name}: ${secret.hint}`));
@@ -124,12 +121,12 @@ export function planDockerDeploy(input) {
124
121
  ? `# Model auth: none found locally — run \`fastagent login\` or set a provider API key before --run.`
125
122
  : `# Model auth: local source is "${input.modelAuth}" — \`--run\` carries auth.json as FASTAGENT_AUTH_SEED.`, `# For a manual Compose run, provide a provider API key or set FASTAGENT_AUTH_SEED yourself.`);
126
123
  }
127
- if (input.kitDir) {
128
- runbook.push(`# Repo-as-workspace: run from the REPO ROOT. Compose lives under ${input.kitDir}/ but builds`, `# the whole repository; only the kit's dependencies are installed by ${input.kitDir}/Dockerfile.`);
129
- }
124
+ runbook.push(input.agentPrefix
125
+ ? `# Run from the WORKSPACE ROOT (the directory containing ${input.agentPrefix}).`
126
+ : `# Run from this directory — it is both the agent and its workspace.`, `# The build bakes the whole directory as the agent's workspace; only the agent's own`, `# dependencies (${input.agentPrefix}package.json) are installed.`);
130
127
  runbook.push(``, `# Build/create/reconcile the service. Re-running this is the complete local redeploy; state stays`, `# in the Compose volume mounted at ${MOUNT}.`, `${compose} up -d --build`, `curl --fail http://127.0.0.1:${input.port}/health`, ``, `# Operate it:`, `${compose} logs -f agent`, `${compose} ps`, `${compose} down # stops containers; keeps the state volume`, `# ${compose} down -v # DESTRUCTIVE: also deletes auth, sessions, and channel state`);
131
128
  if (input.tunnel) {
132
- runbook.push(``, `# Quick Tunnel: \`--run\` starts the Compose tunnel service and reads its ephemeral public URL.`, `# Detected Telegram/Feishu/Lark channels auto-register; GitHub prints the URL. Re-run after the`, `# tunnel container/Docker daemon restarts: a new Quick Tunnel URL must replace the old webhook URL.`, `${compose} logs -f tunnel`);
129
+ runbook.push(``, `# Quick Tunnel: \`--run\` starts the Compose tunnel service and reads its ephemeral public URL.`, `# Telegram/Feishu/Lark and locally onboarded Slack auto-register; GitHub/manual Slack print console URLs. Re-run after the`, `# tunnel container/Docker daemon restarts: a new Quick Tunnel URL must replace the old webhook URL.`, `${compose} logs -f tunnel`);
133
130
  }
134
131
  else if (paths.length > 0) {
135
132
  runbook.push(``, `# Public ingress is operator-owned: add your tunnel/proxy/DNS/TLS, then configure the`, `# detected default endpoint(s): ${paths.map((path) => `https://<your-domain>${path}`).join(", ")}.`, `# If your channels/*.ts glue remaps a route, use that path instead.`);
@@ -28,8 +28,10 @@ export interface FlyPlanInput extends ContainerInput {
28
28
  * `"OAuth"`/`"stored credential"` (a local login the server can't use), or undefined (unconfigured).
29
29
  */
30
30
  modelAuth: string | undefined;
31
- /** Channels discovered in the workspace — each contributes its secret metadata + webhook step. */
31
+ /** Known first-party channels — each contributes its secret metadata + webhook step. */
32
32
  channels: ChannelKind[];
33
+ /** All long-connection channel basenames, including custom channels — require one running machine. */
34
+ longConnectionChannels?: string[];
33
35
  /** Extra secret env-var names (fastagent.config deploy.secrets) — added to the runbook's secret list. */
34
36
  extraSecrets?: string[];
35
37
  /** `auto_stop_machines` — `"suspend"` (default, fast resume) or `"stop"` (cold start). CLI `--stop`. */
@@ -47,6 +49,8 @@ export interface FlyPlan {
47
49
  /** The ordered, values-resolved deploy runbook — printed to stdout for the coding agent to execute. */
48
50
  runbook: string[];
49
51
  }
52
+ /** Did fastagent generate this `fly.toml`? */
53
+ export declare function isGeneratedFlyToml(content: string): boolean;
50
54
  /** Compute the Fly deploy plan from the resolved definition. */
51
55
  export declare function planFlyDeploy(input: FlyPlanInput): FlyPlan;
52
56
  /**
@@ -1,6 +1,6 @@
1
1
  import { containerArtifacts } from "../container.js";
2
2
  import { deploymentSecrets, isEnvKey } from "../secrets.js";
3
- function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTriggers) {
3
+ function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTriggers, hasLongConnectionChannel) {
4
4
  // min_machines_running: 1 (keep one up) when a github channel is present, TIME triggers exist, OR the
5
5
  // operator opted out of scale-to-zero. GitHub's is a SAFETY default — its fire-and-forget turns have no
6
6
  // replay, so scaling to zero could drop an in-flight review. Time triggers (schedules/wake) have no
@@ -10,20 +10,23 @@ function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTrigger
10
10
  ? ` min_machines_running = 1 # github turns have no replay — don't scale to zero (an in-flight review would be lost)`
11
11
  : hasTimeTriggers
12
12
  ? ` min_machines_running = 1 # schedules/wake-ups need a running machine (no external wake-up for a cron instant)`
13
- : !scaleToZero
14
- ? ` min_machines_running = 1 # kept running (--no-scale-to-zero)`
15
- : ` min_machines_running = 0 # scale to zero`;
13
+ : hasLongConnectionChannel
14
+ ? ` min_machines_running = 1 # long-connection channel needs a running machine (cannot wake from zero)`
15
+ : !scaleToZero
16
+ ? ` min_machines_running = 1 # kept running (--no-scale-to-zero)`
17
+ : ` min_machines_running = 0 # scale to zero`;
16
18
  const stopLine = autostop === "stop"
17
19
  ? ` auto_stop_machines = "stop" # stop on idle (cold start on the next webhook)`
18
20
  : ` auto_stop_machines = "suspend" # suspend on idle (fast resume on the next webhook)`;
19
- return `# Generated by \`fastagent deploy fly\`. Edit freely — it is not regenerated unless you pass --force.
21
+ return `${GENERATED_FLY_TOML_MARKER}. Edit freely — it is not regenerated unless you pass --force.
20
22
  app = "${appName}"
21
23
  primary_region = "iad" # set your region (list: \`fly platform regions\`)
22
24
 
23
25
  [build]
24
26
 
25
27
  [env]
26
- FASTAGENT_STATE_DIR = "/data" # the ONE machine-state root auth, sessions, channel state
28
+ FASTAGENT_STATE_DIR = "/data/.state" # mutable machine state — sessions, channel state, schedule
29
+ FASTAGENT_SECRETS_DIR = "/data/.secrets" # seeded (and rotated) credentials — must persist across restarts
27
30
  PORT = "${port}"
28
31
 
29
32
  [http_service]
@@ -35,24 +38,31 @@ ${min}
35
38
 
36
39
  [mounts]
37
40
  source = "data"
38
- destination = "/data" # FASTAGENT_STATE_DIR — persists across stop/suspend/redeploy
41
+ destination = "/data" # .state + .secrets — persists across stop/suspend/redeploy
39
42
 
40
43
  [[vm]]
41
44
  size = "shared-cpu-1x"
42
45
  memory = "512mb" # suspend is not recommended above 2 GB
43
46
  `;
44
47
  }
48
+ /** First line of a generated `fly.toml`, and the predicate that reads it back. Ownership is what decides
49
+ * whether `--force` may reset the file (app/region/vm state a user tuned by hand is theirs). */
50
+ const GENERATED_FLY_TOML_MARKER = "# Generated by `fastagent deploy fly`";
51
+ /** Did fastagent generate this `fly.toml`? */
52
+ export function isGeneratedFlyToml(content) {
53
+ return content.startsWith(GENERATED_FLY_TOML_MARKER);
54
+ }
45
55
  /** Compute the Fly deploy plan from the resolved definition. */
46
56
  export function planFlyDeploy(input) {
47
- const { appName, port, modelAuth, channels, kitDir } = input;
48
- // Kit layout: every artifact is namespaced under the kit (agent/fly.toml, agent/Dockerfile) so the
49
- // host repo's own deploy files are never touched; the runbook passes explicit -c/--dockerfile flags
50
- // (unambiguous across flyctl versions — no reliance on config-relative path resolution).
51
- const flyTomlPath = kitDir ? `${kitDir}/fly.toml` : "fly.toml";
57
+ const { appName, port, modelAuth, channels } = input;
58
+ // Artifacts sit under the agent prefix, so a NESTED agent never touches the workspace's own deploy
59
+ // files; the runbook passes explicit -c/--dockerfile flags either way (unambiguous across flyctl
60
+ // versions — no reliance on config-relative path resolution).
61
+ const flyTomlPath = `${input.agentPrefix}fly.toml`;
52
62
  const artifacts = [
53
63
  {
54
64
  path: flyTomlPath,
55
- content: flyToml(appName, port, channels.includes("github"), input.autostop, input.scaleToZero, input.hasTimeTriggers),
65
+ content: flyToml(appName, port, channels.includes("github"), input.autostop, input.scaleToZero, input.hasTimeTriggers, (input.longConnectionChannels?.length ?? 0) > 0),
56
66
  },
57
67
  ...containerArtifacts(input),
58
68
  ];
@@ -60,12 +70,10 @@ export function planFlyDeploy(input) {
60
70
  // model key (when local auth is an env key) + every discovered channel's secrets. Names + hints as
61
71
  // COMMENT lines (a `#` inside a `\`-continued command would break the shell), then one flat, executable
62
72
  // `fly secrets set` the coding agent fills — `<value>` placeholders, never inline comments.
63
- const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets);
73
+ const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets, input.longConnectionChannels);
64
74
  const requiredSecrets = secrets.filter((secret) => secret.required);
65
75
  const optionalSecrets = secrets.filter((secret) => !secret.required);
66
- const deployCmd = kitDir
67
- ? `fly deploy . --config ${kitDir}/fly.toml --dockerfile ${kitDir}/Dockerfile --app ${appName}`
68
- : `fly deploy --app ${appName}`;
76
+ const deployCmd = `fly deploy . --config ${flyTomlPath} --dockerfile ${input.agentPrefix}Dockerfile --app ${appName}`;
69
77
  const runbook = [
70
78
  `# Deploy "${appName}" to Fly.io. ${flyTomlPath} / Dockerfile(.dockerignore) are generated above.`,
71
79
  `# Prereqs: flyctl installed (https://fly.io/docs/flyctl/install) and \`fly auth login\`.`,
@@ -74,7 +82,7 @@ export function planFlyDeploy(input) {
74
82
  `# Fly app names are GLOBALLY unique: if this fails as taken, set a unique "app" in fly.toml and`,
75
83
  `# re-run \`fastagent deploy fly\` — the runbook follows fly.toml's app name.`,
76
84
  `fly apps create ${appName}`,
77
- `# volume persists FASTAGENT_STATE_DIR=/data (sessions, auth, channel state) across stop/suspend/redeploy.`,
85
+ `# volume persists /data/.state (sessions, channel state) + /data/.secrets (seeded auth) across stop/suspend/redeploy.`,
78
86
  `# <region> MUST equal primary_region in fly.toml (a volume in another region can't mount) — fly.toml`,
79
87
  `# is the single source for the region; skip this if the volume exists (fly volumes list --app ${appName}):`,
80
88
  `fly volumes create data --app ${appName} --region <region> --size 1`,
@@ -85,20 +93,22 @@ export function planFlyDeploy(input) {
85
93
  if (optionalSecrets.length > 0) {
86
94
  runbook.push(``, `# Optional secrets — set only when the matching feature is configured:`, ...optionalSecrets.map((s) => `# ${s.name}: ${s.hint}`), `# fly secrets set --app ${appName} ${optionalSecrets.map((s) => `${s.name}=<value>`).join(" ")}`);
87
95
  }
88
- if (kitDir) {
89
- runbook.push(``, `# Repo-as-workspace: the build context is the REPO ROOT (the whole repo is the agent's cwd); the`, `# config/Dockerfile live under ${kitDir}/ so they never collide with the repo's own deploy files.`, `# Run this from the repo root:`);
96
+ runbook.push(``, `# The build context is the WORKSPACE ROOT (the whole directory is baked as the agent's cwd).`, ...(input.agentPrefix
97
+ ? [`# The config/Dockerfile live under ${input.agentPrefix} so they never collide with the workspace's own.`]
98
+ : []), `# Run this from ${input.agentPrefix ? "the workspace root" : "this directory"}:`, deployCmd);
99
+ if (input.shipsGit) {
100
+ runbook.push(``, `# The image is a WYSIWYG snapshot of this directory. Freshness/durability run through git, driven`, `# by the agent itself: .git ships in the image (see .dockerignore) and git is baked in, so the agent`, `# can pull to freshen content and commit/push its work back (creds ride config.deploy.secrets; the`, `# POLICY — push vs PR, identity — lives in persona.md). CAVEAT: whether .git survives the upload is`, `# host-CLI-dependent — verify \`git status\` on the box; if missing, have the agent clone instead.`, `# Un-pushed changes on the box never survive a redeploy; durability lives in git.`);
90
101
  }
91
- runbook.push(deployCmd);
92
- if (kitDir) {
93
- runbook.push(``, `# Write-back mechanics: git ships in the image and GH_TOKEN-style creds ride config.deploy.secrets;`, `# the POLICY (push vs PR, identity, remote) lives in persona.md. CAVEAT — whether .git survives is`, `# host-CLI-dependent (some context packers strip it from the upload): verify \`git status\` on the`, `# box after the first deploy; if it is missing, have the agent \`git clone\` its repo in the`, `# workspace instead (same token). Un-pushed changes never survive a redeploy — the image is a`, `# snapshot; durability lives in git.`);
102
+ else {
103
+ runbook.push(``, `# The image is a WYSIWYG snapshot of this directory. No .git here, so no history ships and the`, `# generated image does not install git — changes on the box are ephemeral and never survive a`, `# redeploy. If the agent should clone/push repos as part of its work, add deploy: { apt: ["git"] }.`);
94
104
  }
95
105
  // Model-auth guidance: an env key becomes a secret above. Otherwise the plan can't read the local
96
106
  // credential's VALUE to set as a secret — true for OAuth AND a stored API key (both are
97
107
  // `AuthResult.source` non-env labels), so the wording doesn't prejudge whether it's migratable.
98
108
  if (!isEnvKey(modelAuth)) {
99
109
  runbook.push(``, modelAuth === undefined
100
- ? `# Model auth: none found at the local auth path — a global \`fastagent login\` isn't read here; pass --auth-path <file> (e.g. ~/.fastagent/auth.json), or \`--run\` carries it automatically.`
101
- : `# Model auth: your local auth is "${modelAuth}" — the plan can't read its value to set as a secret.`, `# Set your provider API key as a Fly secret (fly secrets set KEY=...), OR place auth.json on the /data volume.`);
110
+ ? `# Model auth: none found at the local auth path — a global \`fastagent login\` isn't read here; pass --auth-path <file> (e.g. ~/.fastagent/.secrets/auth.json), or \`--run\` carries it automatically.`
111
+ : `# Model auth: your local auth is "${modelAuth}" — the plan can't read its value to set as a secret.`, `# Set your provider API key as a Fly secret (fly secrets set KEY=...), OR place auth.json at /data/.secrets/ on the volume.`);
102
112
  }
103
113
  // The fastagent-only post-step: point each channel's webhook at the live URL. Only fastagent knows the routes.
104
114
  // The URLs below assume each channel's DEFAULT route key (POST /telegram, POST /webhook). Reading the
@@ -111,8 +121,11 @@ export function planFlyDeploy(input) {
111
121
  if (channels.includes("github")) {
112
122
  post.push(`# After deploy — set the GitHub webhook (repo Settings → Webhooks). Path assumes the default route`, `# (POST /webhook); if you remapped it in channels/github.ts, use your path:`, `# Payload URL = https://${appName}.fly.dev/webhook, content type application/json, secret = GITHUB_WEBHOOK_SECRET`);
113
123
  }
124
+ if (channels.includes("slack")) {
125
+ post.push(`# After deploy — set Slack Event Subscriptions → Request URL. Path assumes POST /slack;`, `# Slack verifies the running endpoint with a challenge:`, `# Request URL = https://${appName}.fly.dev/slack`, `# Ensure OAuth scopes + message.* subscriptions match the groupBehavior in channels/slack.ts.`);
126
+ }
114
127
  for (const kind of ["feishu", "lark"]) {
115
- if (!channels.includes(kind))
128
+ if (!channels.includes(kind) || input.longConnectionChannels?.includes(kind))
116
129
  continue;
117
130
  const label = kind === "feishu" ? "Feishu" : "Lark";
118
131
  post.push(`# After deploy — set the ${label} event Request URL (developer console → Events & Callbacks).`, `# Path assumes the default route (POST /${kind}); the app must be RUNNING when you save (the console`, `# verifies the URL with a challenge):`, `# Request URL = https://${appName}.fly.dev/${kind}`);
@@ -26,6 +26,14 @@ import type { CliRunner } from "../runner.ts";
26
26
  * is never overwritten by the stale seed, so a box that ran its own OAuth refresh is not rolled back.
27
27
  */
28
28
  export declare function authSeedBytes(seed: string | undefined, fileExists: boolean): Buffer | undefined;
29
+ /**
30
+ * Collect the (possibly CHUNKED) auth seed from the environment: `FASTAGENT_AUTH_SEED` plus numbered
31
+ * continuations (`_2`, `_3`, …) concatenated in order. Hosts whose env values carry a small max
32
+ * length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
33
+ * them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
34
+ * Collection stops at the first absent/empty continuation — the writer fills them contiguously.
35
+ */
36
+ export declare function collectAuthSeed(env: NodeJS.ProcessEnv): string | undefined;
29
37
  export interface FlyRunPlan {
30
38
  appName: string;
31
39
  region: string;
@@ -35,8 +43,12 @@ export interface FlyRunPlan {
35
43
  /** Required secret names with NO local value — the run gates on these before any side effect. */
36
44
  missingSecrets: string[];
37
45
  channels: ChannelKind[];
38
- /** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the workspace dir). */
46
+ longConnectionChannels?: string[];
47
+ /** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the workspace root). */
39
48
  flyConfig: string;
49
+ /** Dockerfile path passed explicitly (`fastagent/Dockerfile`, with the workspace as context —
50
+ * flyctl would otherwise resolve it relative to the config's own directory). */
51
+ dockerfile: string;
40
52
  }
41
53
  /** Done, or a gate the operator must clear before re-running (printed + non-zero exit by the CLI). */
42
54
  export type FlyRunOutcome = {
@@ -46,9 +58,9 @@ export type FlyRunOutcome = {
46
58
  gate: string;
47
59
  };
48
60
  /**
49
- * Run the deploy through `fly`. `log` reports progress; `registerTelegram(baseUrl)` /
50
- * `registerFeishu(baseUrl, kind)` perform the post-deploy webhook steps (the CLI passes its canonical
51
- * Feishu registrar, which also serves the Lark compatibility profile). Absent, the manual console
61
+ * Run the deploy through `fly`. `log` reports progress; the injected Telegram/Feishu/Slack registrars
62
+ * perform post-deploy webhook steps from the builder machine (Slack's control credential never travels
63
+ * to the host). Absent, the manual console
52
64
  * instruction is printed. Every gate is fail-visible.
53
65
  */
54
- export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registerTelegram: (baseUrl: string) => Promise<RegistrationOutcome>, registerFeishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>): Promise<FlyRunOutcome>;
66
+ export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registerTelegram: (baseUrl: string) => Promise<RegistrationOutcome>, registerFeishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>, registerSlack?: (baseUrl: string) => Promise<RegistrationOutcome>): Promise<FlyRunOutcome>;