@fastagent-sh/fastagent 0.19.0 → 0.21.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 (212) hide show
  1. package/README.md +5 -1
  2. package/dist/atomic-write.d.ts +11 -7
  3. package/dist/atomic-write.js +16 -11
  4. package/dist/channels/agentcore-limits.d.ts +9 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-protocol.d.ts +112 -0
  7. package/dist/channels/agentcore-protocol.js +22 -0
  8. package/dist/channels/agentcore-service.d.ts +12 -15
  9. package/dist/channels/agentcore-service.js +15 -24
  10. package/dist/channels/agentcore-state.d.ts +5 -11
  11. package/dist/channels/agentcore-state.js +4 -1
  12. package/dist/channels/agentcore.d.ts +9 -83
  13. package/dist/channels/agentcore.js +101 -93
  14. package/dist/channels/control.d.ts +50 -12
  15. package/dist/channels/control.js +251 -157
  16. package/dist/channels/discover.d.ts +26 -5
  17. package/dist/channels/discover.js +13 -26
  18. package/dist/channels/feishu/context-buffer.d.ts +6 -0
  19. package/dist/channels/feishu/context-buffer.js +0 -38
  20. package/dist/channels/feishu/crypto.d.ts +0 -2
  21. package/dist/channels/feishu/crypto.js +3 -8
  22. package/dist/channels/feishu/feishu-api.js +7 -12
  23. package/dist/channels/feishu/feishu.js +86 -128
  24. package/dist/channels/feishu/parse.d.ts +4 -3
  25. package/dist/channels/feishu/parse.js +5 -4
  26. package/dist/channels/feishu/register-webhook.d.ts +5 -5
  27. package/dist/channels/feishu/register-webhook.js +43 -57
  28. package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
  29. package/dist/channels/feishu/setup-mode.d.ts +30 -0
  30. package/dist/channels/feishu/setup-mode.js +26 -0
  31. package/dist/channels/feishu/shared-api.d.ts +10 -0
  32. package/dist/channels/feishu/shared-api.js +38 -0
  33. package/dist/channels/http.d.ts +0 -8
  34. package/dist/channels/http.js +4 -56
  35. package/dist/channels/kit/attachment-path.d.ts +12 -0
  36. package/dist/channels/kit/attachment-path.js +43 -0
  37. package/dist/channels/kit/context-buffer.d.ts +9 -0
  38. package/dist/channels/kit/context-buffer.js +11 -0
  39. package/dist/channels/kit/signature.d.ts +12 -0
  40. package/dist/channels/kit/signature.js +17 -0
  41. package/dist/channels/kit/stop-command.js +3 -3
  42. package/dist/channels/kit/tasks.d.ts +1 -1
  43. package/dist/channels/kit/tasks.js +10 -4
  44. package/dist/channels/kit/turn-runner.d.ts +59 -0
  45. package/dist/channels/kit/turn-runner.js +84 -0
  46. package/dist/channels/kit/turn-store.d.ts +19 -3
  47. package/dist/channels/kit/turn-store.js +29 -2
  48. package/dist/channels/lark/scaffold/lark-send.ts +12 -67
  49. package/dist/channels/registration.d.ts +36 -1
  50. package/dist/channels/registration.js +57 -1
  51. package/dist/channels/secret.d.ts +1 -0
  52. package/dist/channels/secret.js +16 -0
  53. package/dist/channels/slack/config-api.d.ts +35 -3
  54. package/dist/channels/slack/config-api.js +51 -9
  55. package/dist/channels/slack/invoke-turn.d.ts +1 -1
  56. package/dist/channels/slack/invoke-turn.js +5 -1
  57. package/dist/channels/slack/manifest.js +5 -1
  58. package/dist/channels/slack/onboard.d.ts +5 -5
  59. package/dist/channels/slack/onboard.js +44 -23
  60. package/dist/channels/slack/onboarding-state.d.ts +7 -3
  61. package/dist/channels/slack/onboarding-state.js +13 -20
  62. package/dist/channels/slack/register-webhook.d.ts +3 -2
  63. package/dist/channels/slack/register-webhook.js +35 -15
  64. package/dist/channels/slack/scaffold/channel.ts +3 -10
  65. package/dist/channels/slack/scaffold/slack-send.ts +18 -141
  66. package/dist/channels/slack/shared-api.d.ts +10 -0
  67. package/dist/channels/slack/shared-api.js +34 -0
  68. package/dist/channels/slack/slack-api.d.ts +20 -2
  69. package/dist/channels/slack/slack-api.js +100 -73
  70. package/dist/channels/slack/slack.d.ts +0 -10
  71. package/dist/channels/slack/slack.js +68 -107
  72. package/dist/channels/sse.d.ts +4 -0
  73. package/dist/channels/sse.js +66 -0
  74. package/dist/channels/telegram/parse.d.ts +21 -1
  75. package/dist/channels/telegram/parse.js +65 -11
  76. package/dist/channels/telegram/register-webhook.d.ts +6 -9
  77. package/dist/channels/telegram/register-webhook.js +44 -42
  78. package/dist/channels/telegram/scaffold/channel.ts +7 -3
  79. package/dist/channels/telegram/telegram-api.js +5 -6
  80. package/dist/channels/telegram/telegram.d.ts +2 -2
  81. package/dist/channels/telegram/telegram.js +97 -204
  82. package/dist/channels/wait-health.js +7 -4
  83. package/dist/cli/add-feishu.js +3 -10
  84. package/dist/cli/add-slack.js +13 -25
  85. package/dist/cli/commands/add.d.ts +0 -1
  86. package/dist/cli/commands/add.js +22 -35
  87. package/dist/cli/commands/attach.d.ts +2 -4
  88. package/dist/cli/commands/attach.js +17 -15
  89. package/dist/cli/commands/chat.js +6 -12
  90. package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
  91. package/dist/cli/commands/deploy/agentcore.js +178 -0
  92. package/dist/cli/commands/deploy/docker.d.ts +2 -0
  93. package/dist/cli/commands/deploy/docker.js +119 -0
  94. package/dist/cli/commands/deploy/fly.d.ts +2 -0
  95. package/dist/cli/commands/deploy/fly.js +131 -0
  96. package/dist/cli/commands/deploy/railway.d.ts +2 -0
  97. package/dist/cli/commands/deploy/railway.js +71 -0
  98. package/dist/cli/commands/deploy/shared.d.ts +114 -0
  99. package/dist/cli/commands/deploy/shared.js +124 -0
  100. package/dist/cli/commands/deploy.d.ts +38 -25
  101. package/dist/cli/commands/deploy.js +101 -631
  102. package/dist/cli/commands/dev.js +20 -76
  103. package/dist/cli/commands/fire.js +6 -12
  104. package/dist/cli/commands/info.js +1 -1
  105. package/dist/cli/commands/init.js +1 -2
  106. package/dist/cli/commands/invoke.js +4 -11
  107. package/dist/cli/commands/schedule.js +1 -1
  108. package/dist/cli/commands/start.js +41 -80
  109. package/dist/cli/commands/tool.js +3 -8
  110. package/dist/cli/kernel.d.ts +0 -2
  111. package/dist/cli/kernel.js +0 -2
  112. package/dist/cli/program.js +7 -12
  113. package/dist/cli/serve.d.ts +42 -34
  114. package/dist/cli/serve.js +107 -28
  115. package/dist/cli/shared.d.ts +52 -27
  116. package/dist/cli/shared.js +69 -6
  117. package/dist/deploy/agentcore/forwarder.js +250 -0
  118. package/dist/deploy/agentcore/logs.d.ts +2 -2
  119. package/dist/deploy/agentcore/logs.js +2 -2
  120. package/dist/deploy/agentcore/plan.d.ts +34 -11
  121. package/dist/deploy/agentcore/plan.js +52 -259
  122. package/dist/deploy/agentcore/run.d.ts +9 -24
  123. package/dist/deploy/agentcore/run.js +35 -43
  124. package/dist/deploy/channel-ingress.d.ts +73 -0
  125. package/dist/deploy/channel-ingress.js +101 -0
  126. package/dist/deploy/docker/plan.d.ts +3 -7
  127. package/dist/deploy/docker/plan.js +4 -15
  128. package/dist/deploy/docker/run.d.ts +35 -4
  129. package/dist/deploy/docker/run.js +40 -8
  130. package/dist/deploy/fly/plan.d.ts +4 -5
  131. package/dist/deploy/fly/plan.js +14 -22
  132. package/dist/deploy/fly/run.d.ts +36 -23
  133. package/dist/deploy/fly/run.js +129 -83
  134. package/dist/deploy/hosts.d.ts +5 -0
  135. package/dist/deploy/hosts.js +4 -0
  136. package/dist/deploy/preflight.d.ts +6 -7
  137. package/dist/deploy/preflight.js +27 -23
  138. package/dist/deploy/railway/plan.d.ts +9 -5
  139. package/dist/deploy/railway/plan.js +18 -26
  140. package/dist/deploy/railway/run.d.ts +11 -10
  141. package/dist/deploy/railway/run.js +44 -43
  142. package/dist/deploy/runner.js +22 -2
  143. package/dist/deploy/secrets.d.ts +18 -9
  144. package/dist/deploy/secrets.js +63 -28
  145. package/dist/dev-supervisor.js +3 -4
  146. package/dist/engines/pi/agent-session-factory.d.ts +37 -17
  147. package/dist/engines/pi/agent-session-factory.js +111 -106
  148. package/dist/engines/pi/auth.js +43 -43
  149. package/dist/engines/pi/config.d.ts +2 -2
  150. package/dist/engines/pi/create.d.ts +22 -27
  151. package/dist/engines/pi/create.js +59 -74
  152. package/dist/engines/pi/definition.d.ts +1 -1
  153. package/dist/engines/pi/definition.js +11 -12
  154. package/dist/engines/pi/invoke-session.js +13 -30
  155. package/dist/engines/pi/login.js +32 -16
  156. package/dist/engines/pi/open.d.ts +3 -4
  157. package/dist/engines/pi/open.js +32 -42
  158. package/dist/engines/pi/retry-event.d.ts +6 -0
  159. package/dist/engines/pi/retry-event.js +15 -0
  160. package/dist/engines/pi/search-tools.js +1 -1
  161. package/dist/engines/pi/service.d.ts +1 -1
  162. package/dist/engines/pi/service.js +8 -0
  163. package/dist/engines/pi/session-builder.js +26 -140
  164. package/dist/engines/pi/session-control.d.ts +27 -22
  165. package/dist/engines/pi/session-control.js +551 -490
  166. package/dist/engines/pi/session-inheritance.d.ts +8 -22
  167. package/dist/engines/pi/session-inheritance.js +95 -76
  168. package/dist/engines/pi/session-markers.d.ts +48 -0
  169. package/dist/engines/pi/session-markers.js +59 -0
  170. package/dist/engines/pi/session-settings.d.ts +5 -5
  171. package/dist/engines/pi/session-settings.js +8 -5
  172. package/dist/engines/pi/session-store.d.ts +91 -26
  173. package/dist/engines/pi/session-store.js +413 -82
  174. package/dist/engines/pi/tool-context.d.ts +24 -11
  175. package/dist/engines/pi/tool-context.js +29 -4
  176. package/dist/engines/pi/tool.d.ts +6 -9
  177. package/dist/engines/pi/tool.js +3 -2
  178. package/dist/env.js +1 -2
  179. package/dist/feishu.d.ts +1 -0
  180. package/dist/feishu.js +1 -0
  181. package/dist/lark.d.ts +1 -0
  182. package/dist/lark.js +1 -0
  183. package/dist/loader.d.ts +51 -7
  184. package/dist/loader.js +84 -18
  185. package/dist/log.d.ts +9 -17
  186. package/dist/log.js +25 -30
  187. package/dist/paths.d.ts +26 -3
  188. package/dist/paths.js +43 -5
  189. package/dist/scaffold/add-channel.d.ts +6 -1
  190. package/dist/scaffold/add-channel.js +50 -67
  191. package/dist/scaffold/init.js +7 -2
  192. package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
  193. package/dist/schedule/discover.js +3 -15
  194. package/dist/schedule/wake-alarm.d.ts +14 -19
  195. package/dist/schedule/wake-alarm.js +89 -48
  196. package/dist/schedule/wakeups.d.ts +1 -1
  197. package/dist/schedule/wakeups.js +10 -7
  198. package/dist/service.d.ts +24 -24
  199. package/dist/service.js +33 -77
  200. package/dist/session-remote.d.ts +12 -7
  201. package/dist/session-remote.js +185 -118
  202. package/dist/session.d.ts +227 -93
  203. package/dist/session.js +61 -23
  204. package/dist/slack.d.ts +2 -0
  205. package/dist/slack.js +1 -0
  206. package/dist/telegram.d.ts +1 -1
  207. package/dist/telegram.js +1 -1
  208. package/dist/tunnel.d.ts +34 -9
  209. package/dist/tunnel.js +83 -50
  210. package/package.json +5 -4
  211. package/dist/channels/slack/bot-auth.d.ts +0 -15
  212. package/dist/channels/slack/bot-auth.js +0 -135
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `deploy fly`: fly.toml + a state volume. KEEP (no --force): an existing fly.toml is authoritative;
3
+ * --force: the template is. `--run` drives flyctl to completion (idempotent, resumable).
4
+ */
5
+ import { readFile } from "node:fs/promises";
6
+ import { basename, join } from "node:path";
7
+ import { isGeneratedFlyToml, parseFlyAppName, parseFlyMinMachines, parseFlyRegion, planFlyDeploy, toFlyAppName, } from "../../../deploy/fly/plan.js";
8
+ import { deployFlyRun } from "../../../deploy/fly/run.js";
9
+ import { spawnRunner } from "../../../deploy/runner.js";
10
+ import { readTextIfExists } from "../../../paths.js";
11
+ import { failStartup } from "../../fail.js";
12
+ import { carryCredentials, gateOnModelCredential, registrarsFor } from "./shared.js";
13
+ export const flyHost = {
14
+ isOurs: (path, content) => path.endsWith("fly.toml") && isGeneratedFlyToml(content),
15
+ async deploy(ctx) {
16
+ const { opts, agentDir, workspace, channels, longConnectionChannels, pre, write } = ctx;
17
+ const { hasTimeTriggers, modelAuth, modelKeyInDefinition, authPath, container, port, extraSecrets } = pre;
18
+ // The replay floor that makes scale-to-zero safe is Telegram-only (its L1 turn store). GitHub turns
19
+ // are fire-and-forget (no replay), so the generated fly.toml keeps one machine running for them —
20
+ // a note, not a warn, since the plan already did the safe thing (definition-aware autostop).
21
+ if (channels.some((channel) => channel.name === "github")) {
22
+ console.error(`[fastagent] note: github turns have no replay — the generated fly.toml uses min_machines_running=1 ` +
23
+ `(no scale-to-zero) so autostop can't drop an in-flight review. Set it to 0 to accept that trade.`);
24
+ }
25
+ // Two consistent modes. KEEP (no --force): an existing fly.toml is authoritative — not rewritten,
26
+ // and the runbook reads its `app=` (Fly app names are globally unique, so the basename guess may be
27
+ // taken and the user renamed it). --force: the template is authoritative — the WHOLE fly.toml resets
28
+ // (app→basename, region→iad, vm→defaults), so we do NOT round-trip `app` and warn that hand edits go.
29
+ // fly.toml lives in the agent dir (fastagent/fly.toml) — the workspace's own
30
+ // fly.toml (if any) belongs to the host's product deploy and is never read or written here.
31
+ const flyTomlPath = join(agentDir, "fly.toml");
32
+ const flyToml = await readTextIfExists(flyTomlPath).catch(failStartup);
33
+ // Every decision below turns on ONE question — will `writeArtifacts` keep this file? — and the answer is
34
+ // OWNERSHIP, not the flag: `--force` resets a fly.toml we generated and leaves a hand-written one alone.
35
+ // Keying these on `--force` alone meant a forced deploy of a hand-owned fly.toml took the app name from
36
+ // the directory basename (shipping that file at a DIFFERENT app than it declares) and skipped the
37
+ // scale-to-zero gate below (a schedules deploy that silently sleeps). Read once, decide once.
38
+ const flyTomlKept = flyToml !== undefined && (!opts.force || !isGeneratedFlyToml(flyToml));
39
+ const keptApp = flyTomlKept ? parseFlyAppName(flyToml) : undefined;
40
+ const appName = keptApp ?? toFlyAppName(basename(workspace));
41
+ if (keptApp)
42
+ console.error(`[fastagent] app: ${keptApp} (from fly.toml)`);
43
+ if (flyToml !== undefined && !flyTomlKept) {
44
+ console.error(`[fastagent] warn: --force resets fly.toml to defaults (app, region, vm) — re-apply any hand edits`);
45
+ }
46
+ // Autostop flags shape the GENERATED fly.toml only. In KEEP mode (fly.toml exists, no --force) it is
47
+ // not rewritten, so the flags would silently do nothing — surface that instead of a confusing no-op.
48
+ if (flyTomlKept && (opts.stop || opts.scaleToZero === false)) {
49
+ console.error(`[fastagent] warn: --stop/--no-scale-to-zero only shape a freshly generated fly.toml — yours exists and ` +
50
+ `was kept. Edit auto_stop_machines/min_machines_running in fly.toml, or pass --force to regenerate.`);
51
+ }
52
+ // KEEP mode + time triggers: the kept fly.toml may still scale to zero — which would sleep through every
53
+ // cron instant / wake-up. The generated plan can't fix a kept file, so surface it instead of the preflight
54
+ // note silently not applying (the author who deployed FIRST and added schedules LATER hits exactly this).
55
+ // Under `--run` this is a GATE (same discipline as the model-travel gate): a full deploy whose schedules
56
+ // silently never fire is worse than a crash-loop — nothing fails visibly when a cron instant passes on a
57
+ // sleeping machine, and unlike github's min=0 there is no legitimate trade to accept here.
58
+ if (flyTomlKept && (hasTimeTriggers || longConnectionChannels.length > 0)) {
59
+ const min = parseFlyMinMachines(flyToml);
60
+ if ((min ?? 0) === 0) {
61
+ // undefined = the line is absent — Fly's platform default for min_machines_running is 0, so a
62
+ // hand-written fly.toml without the line scales to zero exactly like an explicit 0.
63
+ const reason = hasTimeTriggers
64
+ ? `schedules/self-scheduling need a running machine (no external wake-up)`
65
+ : `long-connection channel (${longConnectionChannels.map((c) => c.name).join(", ")}) needs an always-on outbound connection`;
66
+ const msg = `your kept fly.toml scales to zero (min_machines_running = ${min ?? "absent → platform default 0"}), but ` +
67
+ `${reason}. Set min_machines_running = 1, or pass --force to regenerate.`;
68
+ if (opts.run)
69
+ failStartup(new Error(`deploy stopped: ${msg}`));
70
+ console.error(`[fastagent] warn: ${msg}`);
71
+ }
72
+ }
73
+ const plan = planFlyDeploy({
74
+ appName,
75
+ port,
76
+ modelAuth,
77
+ channels,
78
+ extraSecrets,
79
+ hasTimeTriggers,
80
+ ...container,
81
+ autostop: opts.stop ? "stop" : "suspend",
82
+ scaleToZero: opts.scaleToZero !== false,
83
+ });
84
+ await write(plan.artifacts, { force: !!opts.force });
85
+ if (opts.run) {
86
+ return runDeployFly({
87
+ agentDir,
88
+ workspace,
89
+ agentPrefix: container.agentPrefix,
90
+ appName,
91
+ modelAuth,
92
+ modelKeyInDefinition,
93
+ authPath,
94
+ channels,
95
+ flyTomlPath,
96
+ extraSecrets,
97
+ });
98
+ }
99
+ console.log(plan.runbook.join("\n"));
100
+ },
101
+ };
102
+ /**
103
+ * `deploy fly --run`: drive flyctl to completion (idempotent, resumable). Gathers the secret VALUES
104
+ * from the local env — the model key (env auth) or the whole auth.json as a `FASTAGENT_AUTH_SEED` seed
105
+ * (OAuth/stored auth: the deployed box materializes it onto the /data volume on first boot, so a
106
+ * personal deploy runs on the SAME subscription) plus channel secrets — then runs the flyctl steps
107
+ * behind the shared {@link spawnRunner} seam (spawned `fly`, cwd = the workspace so the build context is the whole workspace).
108
+ */
109
+ async function runDeployFly(params) {
110
+ const { agentDir, workspace, agentPrefix, appName, channels, flyTomlPath } = params;
111
+ const fly = spawnRunner("fly", workspace);
112
+ // Fail fast if flyctl is absent (spawn ENOENT → 127), with the install link — not a confusing auth gate.
113
+ if ((await fly(["version"], { capture: true })).code === 127) {
114
+ failStartup(new Error(`flyctl not found — install it: https://fly.io/docs/flyctl/install, then re-run`));
115
+ }
116
+ const region = parseFlyRegion(await readFile(flyTomlPath, "utf8")) ?? "iad";
117
+ const { secrets, missingSecrets, needsModelCredential } = await carryCredentials(params);
118
+ gateOnModelCredential(needsModelCredential);
119
+ const outcome = await deployFlyRun({
120
+ appName,
121
+ region,
122
+ secrets,
123
+ missingSecrets,
124
+ channels,
125
+ flyConfig: `${agentPrefix}fly.toml`,
126
+ dockerfile: `${agentPrefix}Dockerfile`,
127
+ }, fly, (m) => console.error(`[fastagent] ${m}`), registrarsFor(agentDir));
128
+ if (!outcome.ok)
129
+ failStartup(new Error(`deploy stopped: ${outcome.gate}`));
130
+ console.error(`[fastagent] deployed → https://${appName}.fly.dev`);
131
+ }
@@ -0,0 +1,2 @@
1
+ import { type HostDeploy } from "./shared.ts";
2
+ export declare const railwayHost: HostDeploy;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `deploy railway`: a thin config file, scale-to-zero is a manual dashboard step, the URL is minted
3
+ * (see planRailwayDeploy). `--run` drives the railway CLI to completion; otherwise print the runbook.
4
+ */
5
+ import { basename } from "node:path";
6
+ import { dockerfilePathVar, isGeneratedRailwayJson, planRailwayDeploy, toRailwayName, } from "../../../deploy/railway/plan.js";
7
+ import { deployRailwayRun } from "../../../deploy/railway/run.js";
8
+ import { spawnRunner } from "../../../deploy/runner.js";
9
+ import { failStartup } from "../../fail.js";
10
+ import { carryCredentials, gateOnModelCredential, registrarsFor } from "./shared.js";
11
+ export const railwayHost = {
12
+ isOurs: (path, content) => path.endsWith("railway.json") && isGeneratedRailwayJson(content),
13
+ async deploy(ctx) {
14
+ const { opts, agentDir, workspace, pre, channels, write } = ctx;
15
+ const { hasTimeTriggers, modelAuth, modelKeyInDefinition, authPath, container, extraSecrets } = pre;
16
+ const serviceName = toRailwayName(basename(workspace));
17
+ const plan = planRailwayDeploy({
18
+ serviceName,
19
+ modelAuth,
20
+ channels,
21
+ extraSecrets,
22
+ hasTimeTriggers,
23
+ ...container,
24
+ });
25
+ await write(plan.artifacts, { force: !!opts.force });
26
+ if (opts.run) {
27
+ // The BUILD entry is guaranteed by the RAILWAY_DOCKERFILE_PATH service variable the runner sets
28
+ // (Railway's documented non-root-Dockerfile route), and Railway's default restart policy already
29
+ // equals the file's ON_FAILURE — the dashboard-only Config-as-code pointer only adds the /health
30
+ // deploy gate (boot-crash visibility), so it is an OPTIONAL note, not a gate.
31
+ console.error(`[fastagent] note: optional — point the service at fastagent/railway.json (Service → Settings → ` +
32
+ `Config-as-code, dashboard-only) so the /health healthcheck marks a boot-crashing deploy as FAILED; ` +
33
+ `the build already uses fastagent/Dockerfile via the RAILWAY_DOCKERFILE_PATH variable`);
34
+ return runDeployRailway({
35
+ agentDir,
36
+ workspace,
37
+ name: serviceName,
38
+ modelAuth,
39
+ modelKeyInDefinition,
40
+ authPath,
41
+ channels,
42
+ extraSecrets,
43
+ intoLinked: !!opts.intoLinked,
44
+ dockerfilePath: dockerfilePathVar(pre.container.agentPrefix),
45
+ });
46
+ }
47
+ console.log(plan.runbook.join("\n"));
48
+ return;
49
+ },
50
+ };
51
+ /**
52
+ * `deploy railway --run`: drive the railway CLI to completion. Mirrors the fly driver — same
53
+ * credential carry (env key OR the OAuth auth.json as `FASTAGENT_AUTH_SEED`) via {@link carryCredentials},
54
+ * same runner seam (spawned `railway`, cwd = the workspace so `railway up`'s upload is the whole workspace). The
55
+ * Railway-specific sequence (linked-check → init/add/volume when fresh → variables → up → domain →
56
+ * webhook) lives in {@link deployRailwayRun}; see there for why Railway differs from Fly.
57
+ */
58
+ async function runDeployRailway(params) {
59
+ const { agentDir, workspace, name, channels, intoLinked, dockerfilePath } = params;
60
+ const railway = spawnRunner("railway", workspace);
61
+ // Fail fast if the railway CLI is absent (spawn ENOENT → 127), with the install link.
62
+ if ((await railway(["--version"], { capture: true })).code === 127) {
63
+ failStartup(new Error(`railway CLI not found — install it: https://docs.railway.com/guides/cli, then re-run`));
64
+ }
65
+ const { secrets, missingSecrets, needsModelCredential } = await carryCredentials(params);
66
+ gateOnModelCredential(needsModelCredential);
67
+ const outcome = await deployRailwayRun({ name, mountPath: "/data", secrets, missingSecrets, channels, intoLinked, dockerfilePath }, railway, (m) => console.error(`[fastagent] ${m}`), registrarsFor(agentDir));
68
+ if (!outcome.ok)
69
+ failStartup(new Error(`deploy stopped: ${outcome.gate}`));
70
+ console.error(`[fastagent] deployed → ${outcome.url}`);
71
+ }
@@ -0,0 +1,114 @@
1
+ import type { DeclaredChannel } from "../../../channels/discover.ts";
2
+ import type { Registrars } from "../../../deploy/channel-ingress.ts";
3
+ import type { DeployPreflight } from "../../../deploy/preflight.ts";
4
+ import type { FastagentConfig } from "../../../engines/pi/config.ts";
5
+ export interface DeployOptions {
6
+ run?: boolean;
7
+ tunnel?: boolean;
8
+ force?: boolean;
9
+ stop?: boolean;
10
+ /** false ⇔ `--no-scale-to-zero`. */
11
+ scaleToZero?: boolean;
12
+ intoLinked?: boolean;
13
+ model?: string;
14
+ authPath?: string;
15
+ /** false ⇔ `--no-input`. */
16
+ input?: boolean;
17
+ }
18
+ /** What the dispatcher resolved before handing off: the placement, the flags, the config and the
19
+ * host-neutral pre-flight, plus the channel lists every host asks about. */
20
+ interface DeployContext {
21
+ opts: DeployOptions;
22
+ agentDir: string;
23
+ workspace: string;
24
+ config: FastagentConfig;
25
+ pre: Extract<DeployPreflight, {
26
+ ok: true;
27
+ }>;
28
+ channels: readonly DeclaredChannel[];
29
+ webhookChannels: readonly DeclaredChannel[];
30
+ longConnectionChannels: readonly DeclaredChannel[];
31
+ /** Write this host's planned artifacts into the workspace under the ownership rule. The dispatcher
32
+ * binds {@link HostDeploy.isOurs} — a host cannot name a predicate here, because every host's has
33
+ * the same type: passing a neighbour's, or forgetting a new host's, type-checks and then silently
34
+ * turns `--force` into a no-op for that host's own artifact. */
35
+ write(artifacts: {
36
+ path: string;
37
+ content: string;
38
+ }[], options: {
39
+ force: boolean;
40
+ alwaysWrite?: string[];
41
+ }): Promise<void>;
42
+ }
43
+ /** One deploy target, as the dispatcher sees it. A new host is a `deploy/<host>/` (plan + driver)
44
+ * plus one of these beside it, and a row in `HOSTS` (cli/commands/deploy.ts). */
45
+ export interface HostDeploy {
46
+ /** Did this host generate the file at `path`? Only the host's OWN artifacts — the container's
47
+ * (Dockerfile, .dockerignore) are answered by {@link writeArtifacts} itself. Read only through the
48
+ * {@link DeployContext.write} the dispatcher binds from it. */
49
+ isOurs(path: string, content: string): boolean;
50
+ /** Plan the artifacts from the pre-flight facts and what is on disk, write them, then either drive
51
+ * the host CLI (`--run`) or print the runbook. Exits through `failStartup` on a gate. */
52
+ deploy(ctx: DeployContext): Promise<void>;
53
+ }
54
+ /** The registrars every host driver gets. One wiring: a channel's credentials are the channel's, not
55
+ * the host's, so which of them can run end-to-end never varies by deployment target.
56
+ *
57
+ * All three get {@link DEPLOY_REGISTRATION_ATTEMPTS} rather than the default: a host CLI returns
58
+ * before the deployment answers, and a registration that gives up first GATES the deploy — reporting
59
+ * a working deployment as one to re-run. `dev --tunnel` keeps the shorter default; it is a resident
60
+ * process whose URL is live when it is printed, as does `deploy docker --run` — it reuses that same
61
+ * announcer, having already health-probed the container and waited for the Quick Tunnel URL.
62
+ *
63
+ * Accepted cost: pointChannelsAt registers serially, so a deployment whose channels are ALL
64
+ * unreachable spends the budget once per channel (3 x 180s) before it gates. Registering in parallel
65
+ * would not shorten a single failing channel, and the failing case is the one nobody is waiting on. */
66
+ export declare function registrarsFor(agentDir: string): Registrars;
67
+ /**
68
+ * The `--run` credential carry, for every host: the local model credential (an env key, or the whole
69
+ * auth.json as a `FASTAGENT_AUTH_SEED`) plus channel secrets. Four drivers assembled this identically.
70
+ */
71
+ export declare function carryCredentials(params: {
72
+ modelAuth: string | undefined;
73
+ modelKeyInDefinition: boolean;
74
+ authPath: string;
75
+ channels: readonly DeclaredChannel[];
76
+ extraSecrets: string[];
77
+ }): Promise<{
78
+ secrets: Record<string, string>;
79
+ missingSecrets: string[];
80
+ needsModelCredential: boolean;
81
+ }>;
82
+ /** Gate a `--run` that has no model credential to carry. Its remediation is `fastagent login`, which
83
+ * is why it is separate from `missingSecrets` (a `.env` fix) rather than folded into it. Docker states
84
+ * the same gate inside its driver, where it belongs after the daemon check. */
85
+ export declare function gateOnModelCredential(needsModelCredential: boolean): void;
86
+ /**
87
+ * Write each generated artifact under `target`, under ONE ownership rule: **deploy only ever overwrites
88
+ * its own output.** Each generated file opens with a marker, so a file on disk is either OURS (a previous
89
+ * `deploy` wrote it) or the author's — and `--force` means "my generated artifact is authoritative",
90
+ * which never licenses clobbering a file we did not write. To hand a path back to fastagent, delete it.
91
+ *
92
+ * That single rule replaced a second mechanism (a per-host list of paths exempt from `--force`), which
93
+ * only described one shape: with the agent inside the workspace, the root `.dockerignore` is the
94
+ * WORKSPACE's file and was listed; with the agent flat, the list was empty — so `--force` would have
95
+ * overwritten a hand-written root `Dockerfile` in a repository `init --flat` had explicitly adopted.
96
+ * Ownership is a property of the file, not of where the agent sits.
97
+ *
98
+ * A KEPT file of OURS that no longer matches what deploy would generate now (config/channel/lockfile/
99
+ * version drift — or the author's edits, which we cannot tell apart) is flagged stale; `--force`
100
+ * regenerates that one.
101
+ *
102
+ * Exported for its own test — this is a four-branch state machine over (exists, ours, force) that used
103
+ * to be proven by spawning the CLI eight times, which is command LOGIC re-run through a subprocess
104
+ * (see vitest.config.ts) and the suite's slowest test.
105
+ */
106
+ export declare function writeArtifacts(target: string, artifacts: {
107
+ path: string;
108
+ content: string;
109
+ }[], options: {
110
+ force: boolean;
111
+ alwaysWrite?: string[];
112
+ isOurs: HostDeploy["isOurs"];
113
+ }): Promise<void>;
114
+ export {};
@@ -0,0 +1,124 @@
1
+ /**
2
+ * What every host's deploy command shares: the context the dispatcher hands a host, the artifact
3
+ * writer with its ownership rule, and the `--run` credential carry. Host facts live in
4
+ * `cli/commands/deploy/<host>.ts`; this file holds nothing that names one.
5
+ */
6
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
7
+ import { dirname, join } from "node:path";
8
+ import { registerFeishuWebhook } from "../../../channels/feishu/register-webhook.js";
9
+ import { DEPLOY_REGISTRATION_ATTEMPTS } from "../../../channels/registration.js";
10
+ import { registerSlackWebhook } from "../../../channels/slack/register-webhook.js";
11
+ import { registerTelegramWebhook } from "../../../channels/telegram/register-webhook.js";
12
+ import { isGeneratedDockerfile, isGeneratedDockerignore } from "../../../deploy/container.js";
13
+ import { assembleSecrets } from "../../../deploy/secrets.js";
14
+ import { exists, resolveStateRoot } from "../../../paths.js";
15
+ import { failStartup } from "../../fail.js";
16
+ /** The registrars every host driver gets. One wiring: a channel's credentials are the channel's, not
17
+ * the host's, so which of them can run end-to-end never varies by deployment target.
18
+ *
19
+ * All three get {@link DEPLOY_REGISTRATION_ATTEMPTS} rather than the default: a host CLI returns
20
+ * before the deployment answers, and a registration that gives up first GATES the deploy — reporting
21
+ * a working deployment as one to re-run. `dev --tunnel` keeps the shorter default; it is a resident
22
+ * process whose URL is live when it is printed, as does `deploy docker --run` — it reuses that same
23
+ * announcer, having already health-probed the container and waited for the Quick Tunnel URL.
24
+ *
25
+ * Accepted cost: pointChannelsAt registers serially, so a deployment whose channels are ALL
26
+ * unreachable spends the budget once per channel (3 x 180s) before it gates. Registering in parallel
27
+ * would not shorten a single failing channel, and the failing case is the one nobody is waiting on. */
28
+ export function registrarsFor(agentDir) {
29
+ const attempts = DEPLOY_REGISTRATION_ATTEMPTS;
30
+ return {
31
+ telegram: (baseUrl) => registerTelegramWebhook(baseUrl, { attempts }),
32
+ slack: (baseUrl) => registerSlackWebhook(baseUrl, { stateRoot: resolveStateRoot(agentDir), attempts }),
33
+ feishu: (baseUrl, kind) => registerFeishuWebhook(baseUrl, kind, { attempts }),
34
+ };
35
+ }
36
+ /**
37
+ * The `--run` credential carry, for every host: the local model credential (an env key, or the whole
38
+ * auth.json as a `FASTAGENT_AUTH_SEED`) plus channel secrets. Four drivers assembled this identically.
39
+ */
40
+ export async function carryCredentials(params) {
41
+ const { modelAuth, modelKeyInDefinition, authPath, channels, extraSecrets } = params;
42
+ return assembleSecrets({
43
+ modelAuth,
44
+ modelKeyInDefinition,
45
+ authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
46
+ channels,
47
+ extraSecrets,
48
+ env: process.env,
49
+ });
50
+ }
51
+ /** Gate a `--run` that has no model credential to carry. Its remediation is `fastagent login`, which
52
+ * is why it is separate from `missingSecrets` (a `.env` fix) rather than folded into it. Docker states
53
+ * the same gate inside its driver, where it belongs after the daemon check. */
54
+ export function gateOnModelCredential(needsModelCredential) {
55
+ if (!needsModelCredential)
56
+ return;
57
+ failStartup(new Error(`deploy stopped: no model credential — run \`fastagent login\`, or set a provider API key in .env, then re-run`));
58
+ }
59
+ /**
60
+ * Write each generated artifact under `target`, under ONE ownership rule: **deploy only ever overwrites
61
+ * its own output.** Each generated file opens with a marker, so a file on disk is either OURS (a previous
62
+ * `deploy` wrote it) or the author's — and `--force` means "my generated artifact is authoritative",
63
+ * which never licenses clobbering a file we did not write. To hand a path back to fastagent, delete it.
64
+ *
65
+ * That single rule replaced a second mechanism (a per-host list of paths exempt from `--force`), which
66
+ * only described one shape: with the agent inside the workspace, the root `.dockerignore` is the
67
+ * WORKSPACE's file and was listed; with the agent flat, the list was empty — so `--force` would have
68
+ * overwritten a hand-written root `Dockerfile` in a repository `init --flat` had explicitly adopted.
69
+ * Ownership is a property of the file, not of where the agent sits.
70
+ *
71
+ * A KEPT file of OURS that no longer matches what deploy would generate now (config/channel/lockfile/
72
+ * version drift — or the author's edits, which we cannot tell apart) is flagged stale; `--force`
73
+ * regenerates that one.
74
+ *
75
+ * Exported for its own test — this is a four-branch state machine over (exists, ours, force) that used
76
+ * to be proven by spawning the CLI eight times, which is command LOGIC re-run through a subprocess
77
+ * (see vitest.config.ts) and the suite's slowest test.
78
+ */
79
+ export async function writeArtifacts(target, artifacts, options) {
80
+ for (const a of artifacts) {
81
+ const abs = join(target, a.path);
82
+ // Pure build output, not operator-owned configuration. It must track the generated template/runbook.
83
+ if (options.alwaysWrite?.includes(a.path)) {
84
+ await mkdir(dirname(abs), { recursive: true });
85
+ await writeFile(abs, a.content);
86
+ console.error(`[fastagent] wrote ${a.path}`);
87
+ continue;
88
+ }
89
+ const existing = (await exists(abs)) ? await readFile(abs, "utf8") : undefined;
90
+ const ours = existing !== undefined && isOurArtifact(a.path, existing, options.isOurs);
91
+ if (existing !== undefined && !ours) {
92
+ // Only the `.dockerignore` has content checks in preflight — pointing at "the preflight warnings"
93
+ // for the other kinds would send the reader looking for output that is never printed.
94
+ console.error(`[fastagent] kept ${a.path} — not generated by fastagent, so --force does not touch it ` +
95
+ `(delete it to let deploy own the path)` +
96
+ (a.path.endsWith(".dockerignore")
97
+ ? `; see the preflight warnings for what it must exclude`
98
+ : a.path.endsWith("Dockerfile")
99
+ ? `; deploy still assumes it listens on $PORT and runs \`fastagent start /app\``
100
+ : ``));
101
+ continue;
102
+ }
103
+ if (existing !== undefined && !options.force) {
104
+ console.error(existing !== a.content
105
+ ? `[fastagent] kept ${a.path} — it no longer matches what deploy would generate (config changed, or ` +
106
+ `you edited it); pass --force to regenerate.`
107
+ : `[fastagent] kept ${a.path} (unchanged)`);
108
+ continue;
109
+ }
110
+ await mkdir(dirname(abs), { recursive: true }); // artifacts live under fastagent/
111
+ await writeFile(abs, a.content);
112
+ console.error(`[fastagent] wrote ${a.path}`);
113
+ }
114
+ }
115
+ /** Did fastagent generate the file at `path`? The container's two artifacts are every host's; the
116
+ * rest is the host's own answer. A kind neither names is permanently classified as the author's,
117
+ * which silently turns `--force` into a no-op for that file. */
118
+ function isOurArtifact(path, content, host) {
119
+ if (path.endsWith("Dockerfile"))
120
+ return isGeneratedDockerfile(content);
121
+ if (path.endsWith(".dockerignore"))
122
+ return isGeneratedDockerignore(content);
123
+ return host(path, content);
124
+ }
@@ -1,28 +1,41 @@
1
- export type DeployHost = "docker" | "fly" | "railway" | "agentcore";
2
- export interface DeployOptions {
3
- run?: boolean;
4
- tunnel?: boolean;
5
- force?: boolean;
6
- stop?: boolean;
7
- /** false `--no-scale-to-zero`. */
8
- scaleToZero?: boolean;
9
- intoLinked?: boolean;
10
- model?: string;
11
- authPath?: string;
12
- /** false `--no-input`. */
13
- input?: boolean;
1
+ /**
2
+ * `fastagent deploy <host> [dir]`: generate host artifacts from the resolved definition and print an
3
+ * ordered deploy runbook. Host-scoped (`docker` | `fly` | `railway` — the extension seam). It does NOT
4
+ * run the host CLI by default: fastagent owns the definition-aware artifacts and precise runbook;
5
+ * Docker may opt into a generated ephemeral tunnel, while durable ingress stays operator-owned. The pre-flight
6
+ * (config/model/channels/container facts) is host-neutral; each host's module (`./deploy/<host>.ts`)
7
+ * adds its artifacts, its kept-file semantics, its gates and its `--run` drive over the neutral
8
+ * `deploy/<host>/` plan + driver. Read-only on the definition; the only writes are generated
9
+ * artifacts (never clobbered without --force). `--run` drives the target CLI instead of printing.
10
+ */
11
+ import type { DeployHost } from "../../deploy/hosts.ts";
12
+ import { type DeployOptions, type HostDeploy, writeArtifacts } from "./deploy/shared.ts";
13
+ export { writeArtifacts };
14
+ /** Every host, by the name the CLI takes. Typed over {@link DeployHost} so a host added to the list
15
+ * without a module here fails to compile. */
16
+ export declare const HOSTS: Record<DeployHost, HostDeploy>;
17
+ /** A flag exactly one host honours, and what the OTHERS do instead. `instead` is exhaustive over the
18
+ * non-owners, so a host added to `DEPLOY_HOSTS` cannot silently lose its line. */
19
+ interface HostOnlyFlag<Owner extends DeployHost> {
20
+ flag: string;
21
+ owner: Owner;
22
+ passed: (opts: DeployOptions) => boolean;
23
+ instead: Record<Exclude<DeployHost, Owner>, string>;
14
24
  }
15
- export declare function runDeploy(host: DeployHost, dirArg: string, opts: DeployOptions): Promise<void>;
16
25
  /**
17
- * Write the plan's artifacts under `target`, honouring the ownership rule: a file we did not generate is
18
- * never touched, ours is kept unless `--force`. Exported for its own test this is a four-branch state
19
- * machine over (exists, ours, force) that used to be proven by spawning the CLI eight times, which is
20
- * command LOGIC re-run through a subprocess (see vitest.config.ts) and the suite's slowest test.
26
+ * ONE table for "this flag belongs to that host", because the fact is symmetric and was not stored
27
+ * that way: each host branch stated the OTHER hosts' flags in its own words, so the same sentence
28
+ * existed three times per flag and had already drifted (`Railway-only` twice, `railway-only` once).
29
+ *
30
+ * Every row is a flag that only WARNS elsewhere. `--tunnel` is host-only too and is deliberately not
31
+ * here: it is a usage GATE (`failUsage`, exit 2) raised in `runDeploy`, not a warning — moving it in
32
+ * would downgrade a refusal to a line of advice.
33
+ *
34
+ * Exported for the exhaustiveness test: the type stops a missing `instead` line, and the test stops it
35
+ * from being typed away.
21
36
  */
22
- export declare function writeArtifacts(target: string, artifacts: {
23
- path: string;
24
- content: string;
25
- }[], options: {
26
- force: boolean;
27
- alwaysWrite?: string[];
28
- }): Promise<void>;
37
+ export declare const HOST_ONLY_FLAGS: (HostOnlyFlag<"fly"> | HostOnlyFlag<"railway">)[];
38
+ /** Exported for its own test: nothing covered these six sentences, which is how three of them came to
39
+ * disagree about the spelling of a host name. */
40
+ export declare function warnHostOnlyFlags(host: DeployHost, opts: DeployOptions): void;
41
+ export declare function runDeploy(host: DeployHost, dirArg: string, opts: DeployOptions): Promise<void>;