@fastagent-sh/fastagent 0.13.0 → 0.15.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 (238) hide show
  1. package/README.md +5 -5
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -0
  4. package/dist/channels/context-buffer.d.ts +30 -0
  5. package/dist/channels/context-buffer.js +98 -0
  6. package/dist/channels/control.d.ts +28 -0
  7. package/dist/channels/control.js +214 -0
  8. package/dist/channels/feishu/card.js +1 -1
  9. package/dist/channels/feishu/context-buffer.d.ts +43 -0
  10. package/dist/channels/feishu/context-buffer.js +72 -0
  11. package/dist/channels/feishu/crypto.d.ts +4 -2
  12. package/dist/channels/feishu/crypto.js +4 -2
  13. package/dist/channels/feishu/feishu-api.d.ts +15 -7
  14. package/dist/channels/feishu/feishu-api.js +22 -4
  15. package/dist/channels/feishu/feishu.d.ts +38 -16
  16. package/dist/channels/feishu/feishu.js +286 -151
  17. package/dist/channels/feishu/invoke-turn.d.ts +24 -31
  18. package/dist/channels/feishu/invoke-turn.js +61 -62
  19. package/dist/channels/feishu/model.d.ts +98 -0
  20. package/dist/channels/feishu/model.js +9 -0
  21. package/dist/channels/feishu/normalize.d.ts +23 -0
  22. package/dist/channels/feishu/normalize.js +132 -0
  23. package/dist/channels/feishu/owned-threads.d.ts +7 -0
  24. package/dist/channels/feishu/owned-threads.js +47 -0
  25. package/dist/channels/feishu/parse.d.ts +21 -103
  26. package/dist/channels/feishu/parse.js +35 -145
  27. package/dist/channels/feishu/preview.d.ts +4 -7
  28. package/dist/channels/feishu/preview.js +26 -142
  29. package/dist/channels/feishu/register-app.d.ts +2 -1
  30. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  31. package/dist/channels/feishu/scaffold/channel.ts +10 -3
  32. package/dist/channels/feishu/setup-mode.d.ts +8 -0
  33. package/dist/channels/feishu/setup-mode.js +2 -0
  34. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  35. package/dist/channels/feishu/ws-ingress.js +136 -0
  36. package/dist/channels/github/github.js +8 -6
  37. package/dist/channels/http.d.ts +14 -0
  38. package/dist/channels/http.js +35 -2
  39. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  40. package/dist/channels/invoke-turn-kit.js +87 -0
  41. package/dist/channels/lark/lark.d.ts +4 -2
  42. package/dist/channels/lark/lark.js +4 -1
  43. package/dist/channels/lark/onboard.d.ts +8 -4
  44. package/dist/channels/lark/onboard.js +8 -0
  45. package/dist/channels/lark/scaffold/channel.ts +10 -3
  46. package/dist/channels/preview-kit.d.ts +109 -0
  47. package/dist/channels/preview-kit.js +183 -0
  48. package/dist/channels/seen.d.ts +5 -0
  49. package/dist/channels/seen.js +35 -0
  50. package/dist/channels/slack/bot-auth.d.ts +15 -0
  51. package/dist/channels/slack/bot-auth.js +146 -0
  52. package/dist/channels/slack/config-api.d.ts +60 -0
  53. package/dist/channels/slack/config-api.js +149 -0
  54. package/dist/channels/slack/context-buffer.d.ts +24 -0
  55. package/dist/channels/slack/context-buffer.js +37 -0
  56. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  57. package/dist/channels/slack/invoke-turn.js +63 -0
  58. package/dist/channels/slack/manifest.d.ts +49 -0
  59. package/dist/channels/slack/manifest.js +69 -0
  60. package/dist/channels/slack/model.d.ts +67 -0
  61. package/dist/channels/slack/model.js +2 -0
  62. package/dist/channels/slack/onboard.d.ts +41 -0
  63. package/dist/channels/slack/onboard.js +120 -0
  64. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  65. package/dist/channels/slack/onboarding-state.js +69 -0
  66. package/dist/channels/slack/owned-threads.d.ts +6 -0
  67. package/dist/channels/slack/owned-threads.js +43 -0
  68. package/dist/channels/slack/parse.d.ts +23 -0
  69. package/dist/channels/slack/parse.js +81 -0
  70. package/dist/channels/slack/preview.d.ts +24 -0
  71. package/dist/channels/slack/preview.js +359 -0
  72. package/dist/channels/slack/reaction.d.ts +24 -0
  73. package/dist/channels/slack/reaction.js +62 -0
  74. package/dist/channels/slack/register-webhook.d.ts +10 -0
  75. package/dist/channels/slack/register-webhook.js +49 -0
  76. package/dist/channels/slack/scaffold/channel.ts +33 -0
  77. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  78. package/dist/channels/slack/setup-server.d.ts +17 -0
  79. package/dist/channels/slack/setup-server.js +103 -0
  80. package/dist/channels/slack/slack-api.d.ts +77 -0
  81. package/dist/channels/slack/slack-api.js +415 -0
  82. package/dist/channels/slack/slack.d.ts +58 -0
  83. package/dist/channels/slack/slack.js +451 -0
  84. package/dist/channels/slack/welcomed.d.ts +5 -0
  85. package/dist/channels/slack/welcomed.js +32 -0
  86. package/dist/channels/state.js +3 -3
  87. package/dist/channels/stop-command.d.ts +6 -0
  88. package/dist/channels/stop-command.js +36 -0
  89. package/dist/channels/tasks.d.ts +13 -0
  90. package/dist/channels/tasks.js +10 -0
  91. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  92. package/dist/channels/telegram/context-buffer.js +6 -85
  93. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  94. package/dist/channels/telegram/invoke-turn.js +11 -58
  95. package/dist/channels/telegram/preview.d.ts +4 -7
  96. package/dist/channels/telegram/preview.js +24 -142
  97. package/dist/channels/telegram/telegram.js +23 -9
  98. package/dist/channels/{feishu/text.d.ts → text.d.ts} +1 -1
  99. package/dist/channels/{feishu/text.js → text.js} +1 -1
  100. package/dist/channels/turn-queue.js +1 -1
  101. package/dist/channels/turn-store.d.ts +1 -1
  102. package/dist/channels/turn-store.js +2 -3
  103. package/dist/cli/add-feishu.d.ts +27 -0
  104. package/dist/{cli-add-feishu.js → cli/add-feishu.js} +167 -44
  105. package/dist/cli/add-slack.d.ts +10 -0
  106. package/dist/cli/add-slack.js +204 -0
  107. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  108. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  109. package/dist/cli/commands/add.d.ts +13 -0
  110. package/dist/cli/commands/add.js +274 -0
  111. package/dist/cli/commands/attach.d.ts +82 -0
  112. package/dist/cli/commands/attach.js +559 -0
  113. package/dist/cli/commands/chat.d.ts +4 -0
  114. package/dist/cli/commands/chat.js +21 -0
  115. package/dist/cli/commands/deploy.d.ts +15 -0
  116. package/dist/cli/commands/deploy.js +394 -0
  117. package/dist/cli/commands/dev.d.ts +11 -0
  118. package/dist/cli/commands/dev.js +82 -0
  119. package/dist/cli/commands/fire.d.ts +7 -0
  120. package/dist/cli/commands/fire.js +45 -0
  121. package/dist/cli/commands/info.d.ts +7 -0
  122. package/dist/cli/commands/info.js +108 -0
  123. package/dist/cli/commands/init.d.ts +8 -0
  124. package/dist/cli/commands/init.js +81 -0
  125. package/dist/cli/commands/invoke.d.ts +7 -0
  126. package/dist/cli/commands/invoke.js +28 -0
  127. package/dist/cli/commands/login.d.ts +6 -0
  128. package/dist/cli/commands/login.js +52 -0
  129. package/dist/cli/commands/models.d.ts +1 -0
  130. package/dist/cli/commands/models.js +15 -0
  131. package/dist/cli/commands/schedule.d.ts +12 -0
  132. package/dist/cli/commands/schedule.js +89 -0
  133. package/dist/cli/commands/start.d.ts +10 -0
  134. package/dist/cli/commands/start.js +97 -0
  135. package/dist/cli/commands/tool.d.ts +1 -0
  136. package/dist/cli/commands/tool.js +38 -0
  137. package/dist/cli/fail.d.ts +17 -0
  138. package/dist/cli/fail.js +32 -0
  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 +90 -0
  142. package/dist/cli/kernel.js +190 -0
  143. package/dist/cli/models-view.d.ts +21 -0
  144. package/dist/cli/models-view.js +66 -0
  145. package/dist/cli/program.d.ts +11 -0
  146. package/dist/cli/program.js +479 -0
  147. package/dist/cli/serve.d.ts +48 -0
  148. package/dist/cli/serve.js +248 -0
  149. package/dist/cli/shared.d.ts +44 -0
  150. package/dist/cli/shared.js +246 -0
  151. package/dist/cli.js +8 -1329
  152. package/dist/collect.d.ts +14 -3
  153. package/dist/collect.js +24 -0
  154. package/dist/core.d.ts +3 -1
  155. package/dist/core.js +2 -0
  156. package/dist/deploy/docker/plan.d.ts +45 -0
  157. package/dist/deploy/docker/plan.js +141 -0
  158. package/dist/deploy/docker/run.d.ts +40 -0
  159. package/dist/deploy/docker/run.js +126 -0
  160. package/dist/deploy/fly/plan.d.ts +3 -1
  161. package/dist/deploy/fly/plan.js +12 -7
  162. package/dist/deploy/fly/run.d.ts +5 -4
  163. package/dist/deploy/fly/run.js +16 -5
  164. package/dist/deploy/preflight.d.ts +10 -4
  165. package/dist/deploy/preflight.js +57 -12
  166. package/dist/deploy/railway/plan.d.ts +3 -1
  167. package/dist/deploy/railway/plan.js +12 -4
  168. package/dist/deploy/railway/run.d.ts +5 -4
  169. package/dist/deploy/railway/run.js +15 -5
  170. package/dist/deploy/runner.d.ts +6 -2
  171. package/dist/deploy/runner.js +1 -0
  172. package/dist/deploy/secrets.d.ts +2 -1
  173. package/dist/deploy/secrets.js +23 -3
  174. package/dist/dev-supervisor.d.ts +0 -2
  175. package/dist/dev-supervisor.js +7 -3
  176. package/dist/engines/pi/auth.js +160 -46
  177. package/dist/engines/pi/channel.d.ts +22 -16
  178. package/dist/engines/pi/channel.js +90 -60
  179. package/dist/engines/pi/chat.d.ts +4 -16
  180. package/dist/engines/pi/chat.js +8 -188
  181. package/dist/engines/pi/config.d.ts +23 -9
  182. package/dist/engines/pi/config.js +35 -5
  183. package/dist/engines/pi/create.d.ts +36 -7
  184. package/dist/engines/pi/create.js +63 -22
  185. package/dist/engines/pi/harness.d.ts +65 -1
  186. package/dist/engines/pi/harness.js +166 -2
  187. package/dist/engines/pi/invoke.d.ts +56 -3
  188. package/dist/engines/pi/invoke.js +340 -20
  189. package/dist/engines/pi/login.d.ts +11 -0
  190. package/dist/engines/pi/login.js +17 -5
  191. package/dist/engines/pi/models.d.ts +56 -10
  192. package/dist/engines/pi/models.js +61 -23
  193. package/dist/engines/pi/search-tools.d.ts +10 -0
  194. package/dist/engines/pi/search-tools.js +138 -0
  195. package/dist/engines/pi/session-builder.d.ts +16 -0
  196. package/dist/engines/pi/session-builder.js +308 -0
  197. package/dist/engines/pi/session-control.d.ts +50 -0
  198. package/dist/engines/pi/session-control.js +604 -0
  199. package/dist/engines/pi/sessions.d.ts +17 -2
  200. package/dist/engines/pi/sessions.js +9 -0
  201. package/dist/engines/pi/tool-context.d.ts +46 -11
  202. package/dist/engines/pi/tool-context.js +11 -9
  203. package/dist/engines/pi/tool.d.ts +35 -6
  204. package/dist/engines/pi/tool.js +47 -1
  205. package/dist/engines/pi/wake-tool.d.ts +0 -3
  206. package/dist/engines/pi/wake-tool.js +9 -7
  207. package/dist/engines/pi/workspace.d.ts +56 -1
  208. package/dist/engines/pi/workspace.js +75 -16
  209. package/dist/feishu.d.ts +1 -1
  210. package/dist/feishu.js +1 -1
  211. package/dist/host/node.d.ts +23 -6
  212. package/dist/host/node.js +5 -4
  213. package/dist/index.d.ts +1 -0
  214. package/dist/index.js +1 -0
  215. package/dist/lark.d.ts +1 -1
  216. package/dist/lark.js +1 -1
  217. package/dist/observe.js +3 -0
  218. package/dist/pi.d.ts +6 -4
  219. package/dist/pi.js +2 -1
  220. package/dist/scaffold/add-channel.d.ts +19 -6
  221. package/dist/scaffold/add-channel.js +127 -16
  222. package/dist/scaffold/templates/fastagent.config.mjs +5 -3
  223. package/dist/schedule/wakeups.d.ts +0 -3
  224. package/dist/schedule/wakeups.js +1 -1
  225. package/dist/session-remote.d.ts +53 -0
  226. package/dist/session-remote.js +336 -0
  227. package/dist/session.d.ts +265 -0
  228. package/dist/session.js +37 -0
  229. package/dist/slack.d.ts +2 -0
  230. package/dist/slack.js +2 -0
  231. package/dist/tunnel.d.ts +7 -4
  232. package/dist/tunnel.js +21 -10
  233. package/package.json +22 -7
  234. package/dist/channels/feishu/seen.d.ts +0 -5
  235. package/dist/channels/feishu/seen.js +0 -47
  236. package/dist/cli-add-feishu.d.ts +0 -8
  237. package/dist/cli-models.d.ts +0 -11
  238. package/dist/cli-models.js +0 -20
package/dist/collect.d.ts CHANGED
@@ -1,9 +1,20 @@
1
1
  /**
2
- * Buffered consumption helper (caller-side, SPEC §7): reduce an AgentEvent stream to a final value,
3
- * encoding the terminal discipline (failed → throw, missing terminal → error). Streaming consumers
4
- * for-await themselves.
2
+ * Caller-side stream helpers: `collect` (buffered consumption, SPEC §7) reduces an AgentEvent
3
+ * stream to a final value, encoding the terminal discipline (failed → throw, missing terminal →
4
+ * error) — streaming consumers for-await themselves. `abortFirstIterator` is the shared
5
+ * cancellation protocol for generator-backed streams.
5
6
  */
6
7
  import type { AgentEvent, Json } from "./agent.ts";
8
+ /**
9
+ * The abort-first cancellation protocol, ONCE: an async generator suspended on a quiet await (a
10
+ * tool mid-execution, a silent SSE read) parks inside that await, and `gen.return()`/`gen.throw()`
11
+ * queue behind the pending `next()` FOREVER (async-generator semantics) — a consumer's cancel
12
+ * would deadlock. The wrapper's `return()` first runs `cancel` (abort the underlying work, which
13
+ * settles the suspension), then delegates to `gen.return`, swallowing its rejection (the
14
+ * generator's own catch/finally already surfaced the outcome). `throw()` tears down identically
15
+ * and rethrows the caller's error deterministically instead of poking a completed generator.
16
+ */
17
+ export declare function abortFirstIterator<T>(gen: AsyncGenerator<T>, cancel: () => void): AsyncIterator<T>;
7
18
  /** Exception form of a failed event (thrown by collect). Carries the failed event's fields verbatim, so
8
19
  * a buffered consumer can branch on `code` (SPEC §8 failure subdivision) just like a streaming one. */
9
20
  export declare class AgentFailure extends Error {
package/dist/collect.js CHANGED
@@ -1,3 +1,27 @@
1
+ /**
2
+ * The abort-first cancellation protocol, ONCE: an async generator suspended on a quiet await (a
3
+ * tool mid-execution, a silent SSE read) parks inside that await, and `gen.return()`/`gen.throw()`
4
+ * queue behind the pending `next()` FOREVER (async-generator semantics) — a consumer's cancel
5
+ * would deadlock. The wrapper's `return()` first runs `cancel` (abort the underlying work, which
6
+ * settles the suspension), then delegates to `gen.return`, swallowing its rejection (the
7
+ * generator's own catch/finally already surfaced the outcome). `throw()` tears down identically
8
+ * and rethrows the caller's error deterministically instead of poking a completed generator.
9
+ */
10
+ export function abortFirstIterator(gen, cancel) {
11
+ return {
12
+ next: () => gen.next(),
13
+ async return(value) {
14
+ cancel();
15
+ await gen.return(value).catch(() => { });
16
+ return { done: true, value: undefined };
17
+ },
18
+ async throw(error) {
19
+ cancel();
20
+ await gen.return(undefined).catch(() => { });
21
+ throw error;
22
+ },
23
+ };
24
+ }
1
25
  /** Exception form of a failed event (thrown by collect). Carries the failed event's fields verbatim, so
2
26
  * a buffered consumer can branch on `code` (SPEC §8 failure subdivision) just like a streaming one. */
3
27
  export class AgentFailure extends Error {
package/dist/core.d.ts CHANGED
@@ -2,9 +2,11 @@ export type { Agent, AgentEvent, ImageRef, Json, Prompt, Scope } from "./agent.t
2
2
  export { collect, AgentFailure, type CollectResult } from "./collect.ts";
3
3
  export type { ModuleLoadFailure } from "./loader.ts";
4
4
  export { createInvokeHandler, nodeListener } from "./channels/http.ts";
5
+ export { controlRoutes, type ControlRoutesOptions, type WireEvent } from "./channels/control.ts";
6
+ export { ControlRequestError, connectAgent, connectSessionControl, type RemoteEndpointOptions, } from "./session-remote.ts";
5
7
  export { readBodyCapped } from "./channels/body.ts";
6
8
  export { text, textHeaders } from "./channels/respond.ts";
7
- export { type ChannelContext, type ChannelHandler, type ChannelModule, type Routes, router, serveNode, } from "./host/node.ts";
9
+ export { type ChannelContext, type ChannelHandler, type ChannelModule, type LongConnection, type LongConnectionChannelModule, type Routes, router, serveNode, } from "./host/node.ts";
8
10
  export { defineSchedule, type LoadedSchedule, type Schedule } from "./schedule/schedule.ts";
9
11
  export { discoverScheduleFiles, loadSchedules } from "./schedule/discover.ts";
10
12
  export { createScheduler, scheduleSession, type Scheduler, type SchedulerOptions } from "./schedule/scheduler.ts";
package/dist/core.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export { collect, AgentFailure } from "./collect.js";
2
2
  export { createInvokeHandler, nodeListener } from "./channels/http.js";
3
+ export { controlRoutes } from "./channels/control.js";
4
+ export { ControlRequestError, connectAgent, connectSessionControl, } from "./session-remote.js";
3
5
  export { readBodyCapped } from "./channels/body.js";
4
6
  export { text, textHeaders } from "./channels/respond.js";
5
7
  export { router, serveNode, } from "./host/node.js";
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `fastagent deploy docker` — the local-Docker plan. The application image stays ingress-neutral:
3
+ * this target generates only the shared Dockerfile plus a small Compose service for the FastAgent
4
+ * process, its loopback port, exact environment-variable names, and one persistent state volume.
5
+ * `--tunnel` can add an ephemeral Cloudflare Quick Tunnel service; durable ingress remains operator-owned.
6
+ */
7
+ import type { ChannelKind } from "../../scaffold/add-channel.ts";
8
+ import { type Artifact, type ContainerInput } from "../container.ts";
9
+ export interface DockerPlanInput extends ContainerInput {
10
+ /** Stable Compose project name, sanitized by {@link toDockerProjectName}. */
11
+ projectName: string;
12
+ /** Container + default host port. The host binding is loopback-only by default. */
13
+ port: number;
14
+ /** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
15
+ modelAuth: string | undefined;
16
+ /** Known channels contribute their environment-variable names and webhook registration. */
17
+ channels: ChannelKind[];
18
+ /** All long-connection channel basenames, including custom channels. */
19
+ longConnectionChannels?: string[];
20
+ /** Generate an optional Cloudflare Quick Tunnel service in Compose. Generation only; `--run` starts it. */
21
+ tunnel: boolean;
22
+ /** Extra environment-variable names declared in config.deploy.secrets. */
23
+ extraSecrets?: string[];
24
+ }
25
+ export interface DockerPlan {
26
+ /** fastagent.compose.yml + the shared Dockerfile/ignore artifacts. */
27
+ artifacts: Artifact[];
28
+ /** Compose file path relative to the workspace root (namespaced for agentDir layouts). */
29
+ composePath: string;
30
+ /** Ordered local build/run/operate instructions. */
31
+ runbook: string[];
32
+ }
33
+ export declare const MIN_DOCKER_COMPOSE_VERSION = "2.3.3";
34
+ /** Pinned: generated deployment artifacts must not silently move when Cloudflare publishes `latest`. */
35
+ export declare const CLOUDFLARED_IMAGE = "cloudflare/cloudflared:2026.7.1";
36
+ /** Whether a Compose file still opts into generated-drift warnings (drop the marker after taking ownership). */
37
+ export declare function isGeneratedCompose(content: string): boolean;
38
+ /** Whether an authoritative Compose file already carries the generated tunnel service contract. */
39
+ export declare function composeHasTunnelService(content: string): boolean;
40
+ /** Compose project names are lowercase [a-z0-9_-] and must start with an alphanumeric character. */
41
+ export declare function toDockerProjectName(directoryName: string): string;
42
+ /** Default first-party webhook paths. Workspace glue may remap them, so guidance labels them defaults. */
43
+ export declare function dockerWebhookPaths(channels: ChannelKind[]): string[];
44
+ /** Compute local-Docker artifacts + the runbook; no Docker process is touched here. */
45
+ export declare function planDockerDeploy(input: DockerPlanInput): DockerPlan;
@@ -0,0 +1,141 @@
1
+ import { containerArtifacts } from "../container.js";
2
+ import { deploymentSecrets, isEnvKey } from "../secrets.js";
3
+ const MOUNT = "/data";
4
+ const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
5
+ const GENERATED_COMPOSE_MARKER = "# Generated by `fastagent deploy docker`";
6
+ export const MIN_DOCKER_COMPOSE_VERSION = "2.3.3";
7
+ /** Pinned: generated deployment artifacts must not silently move when Cloudflare publishes `latest`. */
8
+ export const CLOUDFLARED_IMAGE = "cloudflare/cloudflared:2026.7.1";
9
+ /** Whether a Compose file still opts into generated-drift warnings (drop the marker after taking ownership). */
10
+ export function isGeneratedCompose(content) {
11
+ return content.startsWith(GENERATED_COMPOSE_MARKER);
12
+ }
13
+ /** Whether an authoritative Compose file already carries the generated tunnel service contract. */
14
+ export function composeHasTunnelService(content) {
15
+ return /^[ \t]+tunnel:\s*(?:#.*)?$/m.test(content);
16
+ }
17
+ /** Compose project names are lowercase [a-z0-9_-] and must start with an alphanumeric character. */
18
+ export function toDockerProjectName(directoryName) {
19
+ const slug = directoryName
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9_-]+/g, "-")
22
+ .replace(/^[-_]+|[-_]+$/g, "")
23
+ .slice(0, 54)
24
+ .replace(/[-_]+$/g, "");
25
+ return `fastagent-${slug || "agent"}`;
26
+ }
27
+ /** Default first-party webhook paths. Workspace glue may remap them, so guidance labels them defaults. */
28
+ export function dockerWebhookPaths(channels) {
29
+ const path = {
30
+ github: "/webhook",
31
+ telegram: "/telegram",
32
+ slack: "/slack",
33
+ feishu: "/feishu",
34
+ lark: "/lark",
35
+ };
36
+ return channels.map((kind) => path[kind]);
37
+ }
38
+ /** `${NAME:-}` without making JavaScript treat it as interpolation. */
39
+ function composeInterpolation(name) {
40
+ return `\${${name}:-}`;
41
+ }
42
+ /** Relative path from the namespaced Compose file's directory back to the workspace/build root. */
43
+ function buildContext(kitDir) {
44
+ if (!kitDir)
45
+ return ".";
46
+ return kitDir
47
+ .split("/")
48
+ .map(() => "..")
49
+ .join("/");
50
+ }
51
+ function composeYaml(input) {
52
+ const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets, input.longConnectionChannels);
53
+ // Always leave the auth-seed seam in the committed topology. `--run` uses it for OAuth/stored auth;
54
+ // it is empty otherwise. Values never land in this file — Compose interpolates them at invocation.
55
+ const envNames = [...new Set([...secrets.map((secret) => secret.name), "FASTAGENT_AUTH_SEED"])];
56
+ const secretEnv = envNames.map((name) => ` ${name}: "${composeInterpolation(name)}"`).join("\n");
57
+ const context = buildContext(input.kitDir);
58
+ const dockerfile = input.kitDir ? `${input.kitDir}/Dockerfile` : "Dockerfile";
59
+ const tunnelService = input.tunnel
60
+ ? `
61
+ # Cloudflare Quick Tunnel: ephemeral URL, generated only with \`deploy docker --tunnel\`.
62
+ # No restart policy on purpose: a new process gets a new URL and needs webhook re-registration.
63
+ tunnel:
64
+ image: ${CLOUDFLARED_IMAGE}
65
+ command:
66
+ - tunnel
67
+ - --no-autoupdate
68
+ - --url
69
+ - http://agent:${input.port}
70
+ depends_on:
71
+ - agent
72
+ environment:
73
+ # Docker Desktop may inject HTTP(S)_PROXY into containers. The origin is Docker-internal and must
74
+ # bypass it; preserve any operator NO_PROXY entries after the required service/loopback names.
75
+ NO_PROXY: "agent,localhost,127.0.0.1,${composeInterpolation("NO_PROXY")}"
76
+ no_proxy: "agent,localhost,127.0.0.1,${composeInterpolation("no_proxy")}"
77
+ restart: "no"
78
+ `
79
+ : "";
80
+ return `${GENERATED_COMPOSE_MARKER}. Edit freely — it is kept unless you pass --force.
81
+ # Durable/public ingress remains yours; --tunnel optionally adds an ephemeral Quick Tunnel service.
82
+ name: ${input.projectName}
83
+
84
+ services:
85
+ agent:
86
+ build:
87
+ context: ${context}
88
+ dockerfile: ${dockerfile}
89
+ ports:
90
+ - "127.0.0.1:${input.port}:${input.port}"
91
+ environment:
92
+ PORT: "${input.port}"
93
+ FASTAGENT_STATE_DIR: "${MOUNT}"
94
+ ${secretEnv}
95
+ volumes:
96
+ - state:${MOUNT}
97
+ restart: unless-stopped
98
+ ${tunnelService}
99
+ volumes:
100
+ state:
101
+ `;
102
+ }
103
+ /** Compute local-Docker artifacts + the runbook; no Docker process is touched here. */
104
+ export function planDockerDeploy(input) {
105
+ const composePath = input.kitDir ? `${input.kitDir}/${DOCKER_COMPOSE_FILE}` : DOCKER_COMPOSE_FILE;
106
+ const artifacts = [{ path: composePath, content: composeYaml(input) }, ...containerArtifacts(input)];
107
+ const compose = `docker compose -f ${composePath}`;
108
+ const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets, input.longConnectionChannels);
109
+ const required = secrets.filter((secret) => secret.required);
110
+ const optional = secrets.filter((secret) => !secret.required);
111
+ const routeChannels = input.channels.filter((kind) => !input.longConnectionChannels?.includes(kind));
112
+ const paths = dockerWebhookPaths(routeChannels);
113
+ const runbook = [
114
+ `# Run FastAgent in local Docker. ${composePath} / Dockerfile(.dockerignore) are generated above.`,
115
+ `# Existing artifacts are authoritative and kept; edit Dockerfile or ${composePath} freely.`,
116
+ `# Prereqs: Docker Engine/Desktop with Compose >= ${MIN_DOCKER_COMPOSE_VERSION} (\`docker compose version\`).`,
117
+ ];
118
+ if (required.length > 0) {
119
+ runbook.push(`# Required environment values (put them in the workspace .env or export them):`, ...required.map((secret) => `# ${secret.name}: ${secret.hint}`));
120
+ }
121
+ if (optional.length > 0) {
122
+ runbook.push(`# Optional environment values:`, ...optional.map((secret) => `# ${secret.name}: ${secret.hint}`));
123
+ }
124
+ if (!isEnvKey(input.modelAuth)) {
125
+ runbook.push(input.modelAuth === undefined
126
+ ? `# Model auth: none found locally — run \`fastagent login\` or set a provider API key before --run.`
127
+ : `# 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.`);
128
+ }
129
+ if (input.kitDir) {
130
+ 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.`);
131
+ }
132
+ 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`);
133
+ if (input.tunnel) {
134
+ 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`);
135
+ }
136
+ else if (paths.length > 0) {
137
+ 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.`);
138
+ }
139
+ runbook.push(`# Keep one agent replica: the local state volume is single-process; multiple replicas split/corrupt state.`);
140
+ return { artifacts, composePath, runbook };
141
+ }
@@ -0,0 +1,40 @@
1
+ import type { CliRunner } from "../runner.ts";
2
+ export interface DockerRunPlan {
3
+ /** Compose file relative to the runner cwd (the workspace root). */
4
+ composeFile: string;
5
+ /** Container port from config; used to ask Compose for the effective published host port. */
6
+ port: number;
7
+ /** Values interpolated by Compose. Keys/values are passed in the child environment, never argv. */
8
+ secrets: Record<string, string>;
9
+ /** Required names with no local value; gate before build/create. */
10
+ missingSecrets: string[];
11
+ /** Neither an env-key credential nor a readable auth.json is available. */
12
+ needsModelCredential: boolean;
13
+ /** `--tunnel` was requested for this run; a kept Compose file must actually contain that service. */
14
+ requireTunnel: boolean;
15
+ }
16
+ export type DockerRunOutcome = {
17
+ ok: true;
18
+ url?: string;
19
+ tunnelUrl?: string;
20
+ } | {
21
+ ok: false;
22
+ gate: string;
23
+ };
24
+ export type DockerHealthProbe = (healthUrl: string) => Promise<boolean>;
25
+ export type DockerTunnelUrlProbe = (docker: CliRunner, composeFile: string, env: NodeJS.ProcessEnv) => Promise<string | undefined>;
26
+ /** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
27
+ export declare function localUrlFromComposePort(stdout: string): string | undefined;
28
+ /** Poll the detached cloudflared service's logs until its assigned Quick Tunnel URL appears. */
29
+ export declare function waitForComposeTunnelUrl(docker: CliRunner, composeFile: string, env: NodeJS.ProcessEnv, options?: {
30
+ attempts?: number;
31
+ intervalMs?: number;
32
+ sleep?: (ms: number) => Promise<void>;
33
+ }): Promise<string | undefined>;
34
+ /**
35
+ * Drive Docker Compose. A custom Compose file remains authoritative: the driver invokes it as-is and
36
+ * only assumes the generated service contract (`agent`, config's container port) for optional URL/
37
+ * readiness reporting. If the service intentionally has no host-published port, a successful running
38
+ * service is still success (an operator-owned sidecar/reverse proxy may be its only ingress).
39
+ */
40
+ export declare function deployDockerRun(plan: DockerRunPlan, docker: CliRunner, log: (message: string) => void, healthProbe?: DockerHealthProbe, tunnelUrlProbe?: DockerTunnelUrlProbe): Promise<DockerRunOutcome>;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * `fastagent deploy docker --run` — reconcile the generated/user-owned Compose application locally.
3
+ * Compose owns container/network/volume lifecycle; this driver owns only actionable gates, secret/auth
4
+ * carry through the child environment (never argv), and a readiness check on the published loopback port.
5
+ */
6
+ import { waitForHealth } from "../../channels/wait-health.js";
7
+ import { parseTunnelUrl } from "../../tunnel.js";
8
+ import { MIN_DOCKER_COMPOSE_VERSION } from "./plan.js";
9
+ /** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
10
+ export function localUrlFromComposePort(stdout) {
11
+ const line = stdout
12
+ .split("\n")
13
+ .map((value) => value.trim())
14
+ .find(Boolean);
15
+ const port = line?.match(/:(\d+)$/)?.[1];
16
+ return port ? `http://127.0.0.1:${port}` : undefined;
17
+ }
18
+ const defaultHealthProbe = (healthUrl) => waitForHealth(healthUrl, 30_000, 500);
19
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
20
+ /** Poll the detached cloudflared service's logs until its assigned Quick Tunnel URL appears. */
21
+ export async function waitForComposeTunnelUrl(docker, composeFile, env, options = {}) {
22
+ const compose = ["compose", "-f", composeFile];
23
+ const attempts = options.attempts ?? 60;
24
+ for (let attempt = 0; attempt < attempts; attempt++) {
25
+ const logs = await docker([...compose, "logs", "--no-color", "tunnel"], { capture: true, env });
26
+ const url = logs.code === 0 ? parseTunnelUrl(logs.stdout) : undefined;
27
+ if (url)
28
+ return url;
29
+ if (attempt + 1 < attempts)
30
+ await (options.sleep ?? sleep)(options.intervalMs ?? 500);
31
+ }
32
+ return undefined;
33
+ }
34
+ const defaultTunnelUrlProbe = (docker, composeFile, env) => waitForComposeTunnelUrl(docker, composeFile, env);
35
+ /**
36
+ * Drive Docker Compose. A custom Compose file remains authoritative: the driver invokes it as-is and
37
+ * only assumes the generated service contract (`agent`, config's container port) for optional URL/
38
+ * readiness reporting. If the service intentionally has no host-published port, a successful running
39
+ * service is still success (an operator-owned sidecar/reverse proxy may be its only ingress).
40
+ */
41
+ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHealthProbe, tunnelUrlProbe = defaultTunnelUrlProbe) {
42
+ const gate = (message) => ({ ok: false, gate: message });
43
+ const compose = ["compose", "-f", plan.composeFile];
44
+ const env = plan.secrets;
45
+ // CLI/plugin gate first: unlike a daemon error, spawn ENOENT becomes 127 at the shared runner seam.
46
+ const version = await docker(["compose", "version"], { capture: true });
47
+ if (version.code === 127) {
48
+ return gate("Docker CLI not found — install Docker Engine/Desktop, then re-run");
49
+ }
50
+ if (version.code !== 0) {
51
+ return gate("Docker Compose plugin is unavailable — install/enable `docker compose`, then re-run");
52
+ }
53
+ // Credential gates precede the first side effect (build/create), with distinct remediation.
54
+ if (plan.needsModelCredential) {
55
+ return gate("no model credential — run `fastagent login`, or set a provider API key in .env, then re-run");
56
+ }
57
+ if (plan.missingSecrets.length > 0) {
58
+ return gate(`no local value for: ${plan.missingSecrets.join(", ")} — set them in .env (or the environment) and re-run`);
59
+ }
60
+ if ((await docker(["info"], { capture: true })).code !== 0) {
61
+ return gate("Docker daemon is unavailable — start Docker Engine/Desktop, then re-run");
62
+ }
63
+ // The file on disk is authoritative. Inspect its actual services before any build/create side effect,
64
+ // both to protect the `agent` run contract and to catch `--tunnel` against a kept non-tunnel topology.
65
+ const configured = await docker([...compose, "config", "--services"], { capture: true, env });
66
+ if (configured.code !== 0) {
67
+ return gate(`could not load ${plan.composeFile} — generated files require Docker Compose >= ` +
68
+ `${MIN_DOCKER_COMPOSE_VERSION}; upgrade Compose or fix the file, then re-run`);
69
+ }
70
+ const services = configured.stdout.split(/\s+/).filter(Boolean);
71
+ if (!services.includes("agent")) {
72
+ return gate(`Compose file must keep the "agent" service for \`fastagent deploy docker --run\``);
73
+ }
74
+ const hasTunnel = services.includes("tunnel");
75
+ if (plan.requireTunnel && !hasTunnel) {
76
+ return gate(`--tunnel was requested but the kept ${plan.composeFile} has no "tunnel" service — ` +
77
+ `edit it, delete it and regenerate, or pass --force`);
78
+ }
79
+ // Quick Tunnel logs are the control-plane output (the assigned URL). Remove its old container first so
80
+ // a rerun cannot read a stale URL from accumulated logs; `up` below creates one fresh tunnel, then the
81
+ // CLI registers that URL. The app container/volume are untouched.
82
+ if (hasTunnel) {
83
+ log("recreating the ephemeral tunnel service…");
84
+ if ((await docker([...compose, "rm", "-s", "-f", "tunnel"], { env })).code !== 0) {
85
+ return gate(`could not recreate the tunnel service — inspect \`docker compose -f ${plan.composeFile} ps\``);
86
+ }
87
+ }
88
+ log(`building and reconciling ${plan.composeFile}…`);
89
+ if ((await docker([...compose, "up", "-d", "--build"], { env })).code !== 0) {
90
+ return gate(`\`docker compose up\` failed — see the Docker output above; fix ${plan.composeFile} and re-run`);
91
+ }
92
+ // Detached `up` can return 0 just before a bad command exits. Verify the expected service is actually
93
+ // running so a broken custom Dockerfile/CMD cannot look deployed. Compose restart loops are excluded.
94
+ const running = await docker([...compose, "ps", "--status", "running", "--services"], {
95
+ capture: true,
96
+ env,
97
+ });
98
+ const runningServices = running.stdout.split(/\s+/).filter(Boolean);
99
+ if (running.code !== 0 || !runningServices.includes("agent")) {
100
+ return gate(`the Compose service "agent" is not running — inspect with \`docker compose -f ${plan.composeFile} logs agent\``);
101
+ }
102
+ if (hasTunnel && !runningServices.includes("tunnel")) {
103
+ return gate(`the Compose service "tunnel" is not running — inspect with \`docker compose -f ${plan.composeFile} logs tunnel\``);
104
+ }
105
+ // A user-owned topology may deliberately remove the host port and expose only through its own ingress.
106
+ // In that case Compose `port` is absent/non-zero: service-running is the available readiness floor.
107
+ const published = await docker([...compose, "port", "agent", String(plan.port)], { capture: true, env });
108
+ const url = published.code === 0 ? localUrlFromComposePort(published.stdout) : undefined;
109
+ if (!url) {
110
+ log("agent is running (no host-published port found; using the Compose ingress readiness floor)");
111
+ }
112
+ else {
113
+ const healthUrl = `${url}/health`;
114
+ if (!(await healthProbe(healthUrl))) {
115
+ return gate(`agent did not become healthy at ${healthUrl} — inspect \`docker compose -f ${plan.composeFile} logs agent\``);
116
+ }
117
+ }
118
+ if (!hasTunnel)
119
+ return { ok: true, url };
120
+ log("waiting for the Compose tunnel service to publish its Quick Tunnel URL…");
121
+ const tunnelUrl = await tunnelUrlProbe(docker, plan.composeFile, env);
122
+ if (!tunnelUrl) {
123
+ return gate(`tunnel did not publish a Quick Tunnel URL — inspect \`docker compose -f ${plan.composeFile} logs tunnel\``);
124
+ }
125
+ return { ok: true, url, tunnelUrl };
126
+ }
@@ -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`. */
@@ -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,9 +10,11 @@ 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)`;
@@ -52,7 +54,7 @@ export function planFlyDeploy(input) {
52
54
  const artifacts = [
53
55
  {
54
56
  path: flyTomlPath,
55
- content: flyToml(appName, port, channels.includes("github"), input.autostop, input.scaleToZero, input.hasTimeTriggers),
57
+ content: flyToml(appName, port, channels.includes("github"), input.autostop, input.scaleToZero, input.hasTimeTriggers, (input.longConnectionChannels?.length ?? 0) > 0),
56
58
  },
57
59
  ...containerArtifacts(input),
58
60
  ];
@@ -60,7 +62,7 @@ export function planFlyDeploy(input) {
60
62
  // model key (when local auth is an env key) + every discovered channel's secrets. Names + hints as
61
63
  // COMMENT lines (a `#` inside a `\`-continued command would break the shell), then one flat, executable
62
64
  // `fly secrets set` the coding agent fills — `<value>` placeholders, never inline comments.
63
- const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets);
65
+ const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets, input.longConnectionChannels);
64
66
  const requiredSecrets = secrets.filter((secret) => secret.required);
65
67
  const optionalSecrets = secrets.filter((secret) => !secret.required);
66
68
  const deployCmd = kitDir
@@ -111,8 +113,11 @@ export function planFlyDeploy(input) {
111
113
  if (channels.includes("github")) {
112
114
  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
115
  }
116
+ if (channels.includes("slack")) {
117
+ 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.`);
118
+ }
114
119
  for (const kind of ["feishu", "lark"]) {
115
- if (!channels.includes(kind))
120
+ if (!channels.includes(kind) || input.longConnectionChannels?.includes(kind))
116
121
  continue;
117
122
  const label = kind === "feishu" ? "Feishu" : "Lark";
118
123
  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}`);
@@ -35,6 +35,7 @@ export interface FlyRunPlan {
35
35
  /** Required secret names with NO local value — the run gates on these before any side effect. */
36
36
  missingSecrets: string[];
37
37
  channels: ChannelKind[];
38
+ longConnectionChannels?: string[];
38
39
  /** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the workspace dir). */
39
40
  flyConfig: string;
40
41
  }
@@ -46,9 +47,9 @@ export type FlyRunOutcome = {
46
47
  gate: string;
47
48
  };
48
49
  /**
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
50
+ * Run the deploy through `fly`. `log` reports progress; the injected Telegram/Feishu/Slack registrars
51
+ * perform post-deploy webhook steps from the builder machine (Slack's control credential never travels
52
+ * to the host). Absent, the manual console
52
53
  * instruction is printed. Every gate is fail-visible.
53
54
  */
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>;
55
+ 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>;
@@ -19,12 +19,12 @@ function listHasName(stdout, name) {
19
19
  }
20
20
  }
21
21
  /**
22
- * Run the deploy through `fly`. `log` reports progress; `registerTelegram(baseUrl)` /
23
- * `registerFeishu(baseUrl, kind)` perform the post-deploy webhook steps (the CLI passes its canonical
24
- * Feishu registrar, which also serves the Lark compatibility profile). Absent, the manual console
22
+ * Run the deploy through `fly`. `log` reports progress; the injected Telegram/Feishu/Slack registrars
23
+ * perform post-deploy webhook steps from the builder machine (Slack's control credential never travels
24
+ * to the host). Absent, the manual console
25
25
  * instruction is printed. Every gate is fail-visible.
26
26
  */
27
- export async function deployFlyRun(plan, fly, log, registerTelegram, registerFeishu) {
27
+ export async function deployFlyRun(plan, fly, log, registerTelegram, registerFeishu, registerSlack) {
28
28
  const gate = (g) => ({ ok: false, gate: g });
29
29
  // 1. Auth is the one gate a coding agent can't clear itself (browser OAuth). `whoami` succeeds with
30
30
  // either an interactive login or FLY_API_TOKEN, so one check covers both.
@@ -91,8 +91,19 @@ export async function deployFlyRun(plan, fly, log, registerTelegram, registerFei
91
91
  log(`github: set the webhook in the repo (Settings → Webhooks) → https://${plan.appName}.fly.dev/webhook`);
92
92
  reg.track("github", "manual"); // always a human step — re-surface it after the registrar output
93
93
  }
94
+ if (plan.channels.includes("slack")) {
95
+ const baseUrl = `https://${plan.appName}.fly.dev`;
96
+ if (registerSlack) {
97
+ log("registering slack event URL…");
98
+ reg.track("slack", await registerSlack(baseUrl));
99
+ }
100
+ else {
101
+ log(`slack: set Event Subscriptions → Request URL → ${baseUrl}/slack`);
102
+ reg.track("slack", "manual");
103
+ }
104
+ }
94
105
  for (const kind of ["feishu", "lark"]) {
95
- if (!plan.channels.includes(kind))
106
+ if (!plan.channels.includes(kind) || plan.longConnectionChannels?.includes(kind))
96
107
  continue;
97
108
  if (registerFeishu) {
98
109
  log(`registering ${kind} event URL…`);
@@ -2,14 +2,18 @@ import type { FastagentConfig } from "../engines/pi/config.ts";
2
2
  import { type ChannelKind } from "../scaffold/add-channel.ts";
3
3
  import { type ContainerInput } from "./container.ts";
4
4
  /** A stderr line the CLI prints (`[fastagent] warn: …` / `[fastagent] note: …`). Host-neutral advisories. */
5
- export interface DeployMessage {
5
+ interface DeployMessage {
6
6
  level: "warn" | "note";
7
7
  text: string;
8
8
  }
9
9
  /** The resolved facts every host plan needs (the container shape, channels, model auth, ports/secrets). */
10
- export interface DeployFacts {
10
+ interface DeployFacts {
11
11
  messages: DeployMessage[];
12
12
  channels: ChannelKind[];
13
+ /** Every structurally detected HTTP-route channel basename, including custom channels. */
14
+ routeChannels: string[];
15
+ /** Every structurally detected long-connection channel basename, including custom channels. */
16
+ longConnectionChannels: string[];
13
17
  /** Whether the agent has TIME triggers — `schedules/` files or `selfSchedule` (the wake tool). Cron/wake
14
18
  * has no external wake-up, so the deployment must keep one machine running: the fly plan forces
15
19
  * `min_machines_running=1`, the railway runbook forbids App Sleeping. */
@@ -46,8 +50,9 @@ export declare function preflightDeploy(input: {
46
50
  run: boolean;
47
51
  /** `--force` regenerates artifacts, so the kept-hand-written-Dockerfile apt warning does not apply. */
48
52
  force: boolean;
49
- /** `--auth-path` / `FASTAGENT_AUTH_PATH`; falls back to the project default `<state root>/auth.json`. */
50
- authPathOverride: string | undefined;
53
+ /** The raw `--auth-path` flag; the chain (flag > FASTAGENT_AUTH_PATH > `<state root>/auth.json`)
54
+ * is resolved HERE via {@link resolveAuthPath} — the one owner, same as every serving command. */
55
+ authPathFlag: string | undefined;
51
56
  }): Promise<DeployPreflight>;
52
57
  /**
53
58
  * Why the resolved model won't reach the deployed box, or undefined if it will — host-neutral. `fastagent.config.ts`
@@ -57,3 +62,4 @@ export declare function preflightDeploy(input: {
57
62
  * (`--run`). Single source on purpose — a host env block (fly.toml `[env]`) is NOT advertised as a second home.
58
63
  */
59
64
  export declare function modelTravelIssue(configModel: string | undefined, modelSpec: string | undefined): string | undefined;
65
+ export {};