@fastagent-sh/fastagent 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +14 -4
  2. package/dist/bind.d.ts +34 -0
  3. package/dist/bind.js +74 -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 +266 -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 +5 -0
  13. package/dist/channels/context-buffer.js +5 -0
  14. package/dist/channels/feishu/context-buffer.d.ts +13 -8
  15. package/dist/channels/feishu/context-buffer.js +60 -6
  16. package/dist/channels/feishu/feishu-api.js +4 -1
  17. package/dist/channels/feishu/feishu.d.ts +0 -12
  18. package/dist/channels/feishu/feishu.js +143 -54
  19. package/dist/channels/feishu/invoke-turn.js +35 -18
  20. package/dist/channels/feishu/model.d.ts +0 -1
  21. package/dist/channels/feishu/normalize.js +0 -1
  22. package/dist/channels/feishu/parse.d.ts +21 -7
  23. package/dist/channels/feishu/parse.js +24 -7
  24. package/dist/channels/feishu/preview.js +3 -2
  25. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  26. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  27. package/dist/channels/feishu/setup-mode.d.ts +30 -1
  28. package/dist/channels/feishu/setup-mode.js +27 -1
  29. package/dist/channels/github/github.js +8 -1
  30. package/dist/channels/http.js +1 -1
  31. package/dist/channels/lark/scaffold/channel.ts +9 -8
  32. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  33. package/dist/channels/preview-kit.d.ts +7 -1
  34. package/dist/channels/preview-kit.js +3 -2
  35. package/dist/channels/slack/parse.d.ts +16 -1
  36. package/dist/channels/slack/parse.js +46 -3
  37. package/dist/channels/slack/preview.d.ts +1 -2
  38. package/dist/channels/slack/preview.js +68 -24
  39. package/dist/channels/slack/scaffold/channel.ts +5 -5
  40. package/dist/channels/slack/slack-api.d.ts +3 -23
  41. package/dist/channels/slack/slack-api.js +6 -22
  42. package/dist/channels/slack/slack.d.ts +13 -20
  43. package/dist/channels/slack/slack.js +95 -50
  44. package/dist/channels/state.d.ts +11 -4
  45. package/dist/channels/state.js +19 -12
  46. package/dist/channels/tasks.d.ts +0 -6
  47. package/dist/channels/tasks.js +16 -1
  48. package/dist/channels/telegram/parse.d.ts +0 -7
  49. package/dist/channels/telegram/parse.js +4 -2
  50. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  51. package/dist/channels/telegram/telegram.js +1 -1
  52. package/dist/channels/text.d.ts +14 -0
  53. package/dist/channels/text.js +14 -0
  54. package/dist/channels/thread-participants.d.ts +21 -0
  55. package/dist/channels/thread-participants.js +132 -0
  56. package/dist/channels/turn-queue.js +7 -0
  57. package/dist/cli/add-feishu.d.ts +7 -4
  58. package/dist/cli/add-feishu.js +57 -37
  59. package/dist/cli/add-slack.d.ts +2 -1
  60. package/dist/cli/add-slack.js +6 -11
  61. package/dist/cli/commands/add.js +50 -51
  62. package/dist/cli/commands/attach.js +8 -4
  63. package/dist/cli/commands/chat.js +8 -8
  64. package/dist/cli/commands/deploy.d.ts +14 -1
  65. package/dist/cli/commands/deploy.js +330 -78
  66. package/dist/cli/commands/dev.d.ts +1 -0
  67. package/dist/cli/commands/dev.js +38 -25
  68. package/dist/cli/commands/fire.js +15 -16
  69. package/dist/cli/commands/info.js +36 -29
  70. package/dist/cli/commands/init.d.ts +1 -1
  71. package/dist/cli/commands/init.js +65 -53
  72. package/dist/cli/commands/invoke.js +9 -6
  73. package/dist/cli/commands/login.js +35 -21
  74. package/dist/cli/commands/schedule.js +6 -8
  75. package/dist/cli/commands/start.d.ts +1 -0
  76. package/dist/cli/commands/start.js +101 -37
  77. package/dist/cli/commands/tool.js +30 -18
  78. package/dist/cli/fail.d.ts +17 -0
  79. package/dist/cli/fail.js +24 -0
  80. package/dist/cli/program.js +66 -36
  81. package/dist/cli/serve.d.ts +52 -8
  82. package/dist/cli/serve.js +133 -32
  83. package/dist/cli/shared.d.ts +21 -2
  84. package/dist/cli/shared.js +44 -18
  85. package/dist/deploy/agentcore/plan.d.ts +117 -0
  86. package/dist/deploy/agentcore/plan.js +721 -0
  87. package/dist/deploy/agentcore/run.d.ts +73 -0
  88. package/dist/deploy/agentcore/run.js +412 -0
  89. package/dist/deploy/agentcore/zip.d.ts +17 -0
  90. package/dist/deploy/agentcore/zip.js +68 -0
  91. package/dist/deploy/container.d.ts +26 -25
  92. package/dist/deploy/container.js +93 -89
  93. package/dist/deploy/docker/plan.d.ts +1 -1
  94. package/dist/deploy/docker/plan.js +12 -17
  95. package/dist/deploy/fly/plan.d.ts +2 -0
  96. package/dist/deploy/fly/plan.js +27 -19
  97. package/dist/deploy/fly/run.d.ts +12 -1
  98. package/dist/deploy/fly/run.js +36 -2
  99. package/dist/deploy/preflight.d.ts +11 -5
  100. package/dist/deploy/preflight.js +235 -65
  101. package/dist/deploy/railway/plan.d.ts +7 -0
  102. package/dist/deploy/railway/plan.js +41 -16
  103. package/dist/deploy/railway/run.d.ts +8 -1
  104. package/dist/deploy/railway/run.js +7 -2
  105. package/dist/deploy/runner.d.ts +5 -2
  106. package/dist/deploy/runner.js +9 -3
  107. package/dist/dev-supervisor.d.ts +11 -8
  108. package/dist/dev-supervisor.js +53 -51
  109. package/dist/engines/pi/auth.d.ts +8 -7
  110. package/dist/engines/pi/auth.js +12 -10
  111. package/dist/engines/pi/channel.d.ts +1 -1
  112. package/dist/engines/pi/channel.js +5 -5
  113. package/dist/engines/pi/chat.js +2 -2
  114. package/dist/engines/pi/config.d.ts +12 -48
  115. package/dist/engines/pi/config.js +29 -110
  116. package/dist/engines/pi/create.d.ts +32 -24
  117. package/dist/engines/pi/create.js +47 -23
  118. package/dist/engines/pi/definition.d.ts +7 -26
  119. package/dist/engines/pi/definition.js +8 -54
  120. package/dist/engines/pi/harness.d.ts +19 -5
  121. package/dist/engines/pi/harness.js +3 -5
  122. package/dist/engines/pi/login.d.ts +1 -1
  123. package/dist/engines/pi/models.d.ts +3 -3
  124. package/dist/engines/pi/models.js +1 -1
  125. package/dist/engines/pi/{workspace.d.ts → open.d.ts} +31 -25
  126. package/dist/engines/pi/{workspace.js → open.js} +27 -29
  127. package/dist/engines/pi/read-image.d.ts +4 -0
  128. package/dist/engines/pi/read-image.js +62 -0
  129. package/dist/engines/pi/search-tools.d.ts +6 -4
  130. package/dist/engines/pi/search-tools.js +3 -1
  131. package/dist/engines/pi/session-builder.d.ts +2 -2
  132. package/dist/engines/pi/session-builder.js +18 -13
  133. package/dist/engines/pi/tool.d.ts +13 -5
  134. package/dist/engines/pi/tool.js +4 -0
  135. package/dist/engines/pi/wake-tool.d.ts +3 -3
  136. package/dist/env.d.ts +16 -4
  137. package/dist/env.js +43 -5
  138. package/dist/host/node.d.ts +4 -2
  139. package/dist/host/node.js +2 -1
  140. package/dist/loader.d.ts +2 -2
  141. package/dist/loader.js +3 -3
  142. package/dist/log.d.ts +1 -1
  143. package/dist/log.js +1 -1
  144. package/dist/paths.d.ts +138 -0
  145. package/dist/paths.js +326 -0
  146. package/dist/pi.d.ts +2 -2
  147. package/dist/pi.js +2 -2
  148. package/dist/runtime.d.ts +7 -5
  149. package/dist/runtime.js +2 -2
  150. package/dist/scaffold/add-channel.d.ts +7 -3
  151. package/dist/scaffold/add-channel.js +55 -29
  152. package/dist/scaffold/init.d.ts +32 -41
  153. package/dist/scaffold/init.js +161 -185
  154. package/dist/scaffold/templates/env.example +15 -6
  155. package/dist/scaffold/templates/fastagent.config.mjs +1 -1
  156. package/dist/scaffold/templates/gitignore +14 -6
  157. package/dist/scaffold/templates/persona.md +4 -2
  158. package/dist/scaffold/templates/secrets.gitignore +5 -0
  159. package/dist/scaffold/templates.d.ts +1 -7
  160. package/dist/scaffold/templates.js +3 -25
  161. package/dist/scaffold/vendor-skill.d.ts +2 -2
  162. package/dist/scaffold/vendor-skill.js +13 -13
  163. package/dist/schedule/discover.js +4 -4
  164. package/dist/schedule/scheduler.d.ts +40 -1
  165. package/dist/schedule/scheduler.js +89 -56
  166. package/dist/schedule/state.js +1 -1
  167. package/dist/schedule/wake-alarm.d.ts +47 -0
  168. package/dist/schedule/wake-alarm.js +136 -0
  169. package/dist/schedule/wakeups.d.ts +1 -0
  170. package/dist/schedule/wakeups.js +18 -0
  171. package/dist/tunnel.d.ts +3 -3
  172. package/dist/tunnel.js +7 -7
  173. package/package.json +7 -4
  174. package/dist/channels/feishu/owned-threads.d.ts +0 -7
  175. package/dist/channels/feishu/owned-threads.js +0 -47
  176. package/dist/channels/slack/owned-threads.d.ts +0 -6
  177. package/dist/channels/slack/owned-threads.js +0 -43
  178. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  179. package/dist/scaffold/templates/gitignore.kit +0 -2
  180. package/dist/workspace.d.ts +0 -9
  181. 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
  }
@@ -25,7 +25,7 @@ export interface DockerPlanInput extends ContainerInput {
25
25
  export interface DockerPlan {
26
26
  /** fastagent.compose.yml + the shared Dockerfile/ignore artifacts. */
27
27
  artifacts: Artifact[];
28
- /** 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). */
29
29
  composePath: string;
30
30
  /** Ordered local build/run/operate instructions. */
31
31
  runbook: string[];
@@ -39,23 +39,16 @@ export function dockerWebhookPaths(channels) {
39
39
  function composeInterpolation(name) {
40
40
  return `\${${name}:-}`;
41
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
42
  function composeYaml(input) {
52
43
  const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets, input.longConnectionChannels);
53
44
  // Always leave the auth-seed seam in the committed topology. `--run` uses it for OAuth/stored auth;
54
45
  // it is empty otherwise. Values never land in this file — Compose interpolates them at invocation.
55
46
  const envNames = [...new Set([...secrets.map((secret) => secret.name), "FASTAGENT_AUTH_SEED"])];
56
47
  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";
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`;
59
52
  const tunnelService = input.tunnel
60
53
  ? `
61
54
  # Cloudflare Quick Tunnel: ephemeral URL, generated only with \`deploy docker --tunnel\`.
@@ -90,7 +83,9 @@ services:
90
83
  - "127.0.0.1:${input.port}:${input.port}"
91
84
  environment:
92
85
  PORT: "${input.port}"
93
- 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"
94
89
  ${secretEnv}
95
90
  volumes:
96
91
  - state:${MOUNT}
@@ -102,7 +97,7 @@ volumes:
102
97
  }
103
98
  /** Compute local-Docker artifacts + the runbook; no Docker process is touched here. */
104
99
  export function planDockerDeploy(input) {
105
- const composePath = input.kitDir ? `${input.kitDir}/${DOCKER_COMPOSE_FILE}` : DOCKER_COMPOSE_FILE;
100
+ const composePath = `${input.agentPrefix}${DOCKER_COMPOSE_FILE}`;
106
101
  const artifacts = [{ path: composePath, content: composeYaml(input) }, ...containerArtifacts(input)];
107
102
  const compose = `docker compose -f ${composePath}`;
108
103
  const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets, input.longConnectionChannels);
@@ -116,7 +111,7 @@ export function planDockerDeploy(input) {
116
111
  `# Prereqs: Docker Engine/Desktop with Compose >= ${MIN_DOCKER_COMPOSE_VERSION} (\`docker compose version\`).`,
117
112
  ];
118
113
  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}`));
114
+ runbook.push(`# Required environment values (put them in the agent's .secrets/.env or export them):`, ...required.map((secret) => `# ${secret.name}: ${secret.hint}`));
120
115
  }
121
116
  if (optional.length > 0) {
122
117
  runbook.push(`# Optional environment values:`, ...optional.map((secret) => `# ${secret.name}: ${secret.hint}`));
@@ -126,9 +121,9 @@ export function planDockerDeploy(input) {
126
121
  ? `# Model auth: none found locally — run \`fastagent login\` or set a provider API key before --run.`
127
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.`);
128
123
  }
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
- }
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.`);
132
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`);
133
128
  if (input.tunnel) {
134
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`);
@@ -49,6 +49,8 @@ export interface FlyPlan {
49
49
  /** The ordered, values-resolved deploy runbook — printed to stdout for the coding agent to execute. */
50
50
  runbook: string[];
51
51
  }
52
+ /** Did fastagent generate this `fly.toml`? */
53
+ export declare function isGeneratedFlyToml(content: string): boolean;
52
54
  /** Compute the Fly deploy plan from the resolved definition. */
53
55
  export declare function planFlyDeploy(input: FlyPlanInput): FlyPlan;
54
56
  /**
@@ -18,14 +18,15 @@ function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTrigger
18
18
  const stopLine = autostop === "stop"
19
19
  ? ` auto_stop_machines = "stop" # stop on idle (cold start on the next webhook)`
20
20
  : ` auto_stop_machines = "suspend" # suspend on idle (fast resume on the next webhook)`;
21
- 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.
22
22
  app = "${appName}"
23
23
  primary_region = "iad" # set your region (list: \`fly platform regions\`)
24
24
 
25
25
  [build]
26
26
 
27
27
  [env]
28
- 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
29
30
  PORT = "${port}"
30
31
 
31
32
  [http_service]
@@ -37,20 +38,27 @@ ${min}
37
38
 
38
39
  [mounts]
39
40
  source = "data"
40
- destination = "/data" # FASTAGENT_STATE_DIR — persists across stop/suspend/redeploy
41
+ destination = "/data" # .state + .secrets — persists across stop/suspend/redeploy
41
42
 
42
43
  [[vm]]
43
44
  size = "shared-cpu-1x"
44
45
  memory = "512mb" # suspend is not recommended above 2 GB
45
46
  `;
46
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
+ }
47
55
  /** Compute the Fly deploy plan from the resolved definition. */
48
56
  export function planFlyDeploy(input) {
49
- const { appName, port, modelAuth, channels, kitDir } = input;
50
- // Kit layout: every artifact is namespaced under the kit (agent/fly.toml, agent/Dockerfile) so the
51
- // host repo's own deploy files are never touched; the runbook passes explicit -c/--dockerfile flags
52
- // (unambiguous across flyctl versions — no reliance on config-relative path resolution).
53
- 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`;
54
62
  const artifacts = [
55
63
  {
56
64
  path: flyTomlPath,
@@ -65,9 +73,7 @@ export function planFlyDeploy(input) {
65
73
  const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets, input.longConnectionChannels);
66
74
  const requiredSecrets = secrets.filter((secret) => secret.required);
67
75
  const optionalSecrets = secrets.filter((secret) => !secret.required);
68
- const deployCmd = kitDir
69
- ? `fly deploy . --config ${kitDir}/fly.toml --dockerfile ${kitDir}/Dockerfile --app ${appName}`
70
- : `fly deploy --app ${appName}`;
76
+ const deployCmd = `fly deploy . --config ${flyTomlPath} --dockerfile ${input.agentPrefix}Dockerfile --app ${appName}`;
71
77
  const runbook = [
72
78
  `# Deploy "${appName}" to Fly.io. ${flyTomlPath} / Dockerfile(.dockerignore) are generated above.`,
73
79
  `# Prereqs: flyctl installed (https://fly.io/docs/flyctl/install) and \`fly auth login\`.`,
@@ -76,7 +82,7 @@ export function planFlyDeploy(input) {
76
82
  `# Fly app names are GLOBALLY unique: if this fails as taken, set a unique "app" in fly.toml and`,
77
83
  `# re-run \`fastagent deploy fly\` — the runbook follows fly.toml's app name.`,
78
84
  `fly apps create ${appName}`,
79
- `# 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.`,
80
86
  `# <region> MUST equal primary_region in fly.toml (a volume in another region can't mount) — fly.toml`,
81
87
  `# is the single source for the region; skip this if the volume exists (fly volumes list --app ${appName}):`,
82
88
  `fly volumes create data --app ${appName} --region <region> --size 1`,
@@ -87,20 +93,22 @@ export function planFlyDeploy(input) {
87
93
  if (optionalSecrets.length > 0) {
88
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(" ")}`);
89
95
  }
90
- if (kitDir) {
91
- 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.`);
92
101
  }
93
- runbook.push(deployCmd);
94
- if (kitDir) {
95
- 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"] }.`);
96
104
  }
97
105
  // Model-auth guidance: an env key becomes a secret above. Otherwise the plan can't read the local
98
106
  // credential's VALUE to set as a secret — true for OAuth AND a stored API key (both are
99
107
  // `AuthResult.source` non-env labels), so the wording doesn't prejudge whether it's migratable.
100
108
  if (!isEnvKey(modelAuth)) {
101
109
  runbook.push(``, modelAuth === undefined
102
- ? `# 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.`
103
- : `# 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.`);
104
112
  }
105
113
  // The fastagent-only post-step: point each channel's webhook at the live URL. Only fastagent knows the routes.
106
114
  // The URLs below assume each channel's DEFAULT route key (POST /telegram, POST /webhook). Reading the
@@ -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;
@@ -36,8 +44,11 @@ export interface FlyRunPlan {
36
44
  missingSecrets: string[];
37
45
  channels: ChannelKind[];
38
46
  longConnectionChannels?: string[];
39
- /** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the workspace dir). */
47
+ /** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the workspace root). */
40
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;
41
52
  }
42
53
  /** Done, or a gate the operator must clear before re-running (printed + non-zero exit by the CLI). */
43
54
  export type FlyRunOutcome = {
@@ -7,6 +7,26 @@ import { registrationGate } from "../registration-gate.js";
7
7
  export function authSeedBytes(seed, fileExists) {
8
8
  return !seed || fileExists ? undefined : Buffer.from(seed, "base64");
9
9
  }
10
+ /**
11
+ * Collect the (possibly CHUNKED) auth seed from the environment: `FASTAGENT_AUTH_SEED` plus numbered
12
+ * continuations (`_2`, `_3`, …) concatenated in order. Hosts whose env values carry a small max
13
+ * length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
14
+ * them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
15
+ * Collection stops at the first absent/empty continuation — the writer fills them contiguously.
16
+ */
17
+ export function collectAuthSeed(env) {
18
+ const first = env.FASTAGENT_AUTH_SEED;
19
+ if (!first)
20
+ return undefined;
21
+ let seed = first;
22
+ for (let i = 2;; i++) {
23
+ const part = env[`FASTAGENT_AUTH_SEED_${i}`];
24
+ if (!part)
25
+ break;
26
+ seed += part;
27
+ }
28
+ return seed;
29
+ }
10
30
  /** Whether a `fly … list --json` array contains an object named `name` (Fly capitalizes `Name`; accept both). */
11
31
  function listHasName(stdout, name) {
12
32
  try {
@@ -74,9 +94,23 @@ export async function deployFlyRun(plan, fly, log, registerTelegram, registerFei
74
94
  return gate("`fly secrets import` failed — see the flyctl output above");
75
95
  }
76
96
  }
77
- // 6. Deploy — remote builder (no local Docker), one machine.
97
+ // 6. Deploy — remote builder (no local Docker), one machine. Context + Dockerfile are passed
98
+ // explicitly (the workspace root is the context; the Dockerfile lives under fastagent/).
78
99
  log("deploying (remote build)…");
79
- if ((await fly(["deploy", "-a", plan.appName, "-c", plan.flyConfig, "--remote-only", "--yes", "--ha=false"])).code !== 0) {
100
+ const deployArgs = [
101
+ "deploy",
102
+ ".",
103
+ "-a",
104
+ plan.appName,
105
+ "-c",
106
+ plan.flyConfig,
107
+ "--dockerfile",
108
+ plan.dockerfile,
109
+ "--remote-only",
110
+ "--yes",
111
+ "--ha=false",
112
+ ];
113
+ if ((await fly(deployArgs)).code !== 0) {
80
114
  return gate("`fly deploy` failed — see the flyctl output above; fix and re-run");
81
115
  }
82
116
  // 7. Post-deploy webhook — telegram end-to-end (fastagent has the token + the live URL); github is a
@@ -1,4 +1,5 @@
1
1
  import type { FastagentConfig } from "../engines/pi/config.ts";
2
+ import { type ResolvedPlacement } from "../paths.ts";
2
3
  import { type ChannelKind } from "../scaffold/add-channel.ts";
3
4
  import { type ContainerInput } from "./container.ts";
4
5
  /** A stderr line the CLI prints (`[fastagent] warn: …` / `[fastagent] note: …`). Host-neutral advisories. */
@@ -40,17 +41,22 @@ export type DeployPreflight = {
40
41
  * provider) — the CLI wraps the call in its `failStartup` so the fault surfaces and exits, never silently.
41
42
  */
42
43
  export declare function preflightDeploy(input: {
43
- target: string;
44
- /** The agent-definition dir (config.agentDir resolved against target; = target when unset) where
45
- * channels are discovered. Container facts (package.json/lockfile) still read `target`, the run root. */
46
- agentDir: string;
44
+ /** The resolved placement. `agentDir` is where channels/schedules are discovered and where the
45
+ * container facts (package.json/lockfile) are read the AGENT's manifest drives the image's install
46
+ * step, never the workspace's (whose manifest belongs to its own deploy); `workspace` is the build
47
+ * context (the whole tree is baked). One value, because one derives from the other: two loose
48
+ * strings could be handed in disagreeing, and nothing would notice. */
49
+ placement: ResolvedPlacement;
47
50
  config: FastagentConfig;
48
51
  modelSpec: string | undefined;
49
52
  /** `--run` fully deploys, so a model that won't travel is a GATE (a known crash-loop); else it warns. */
50
53
  run: boolean;
51
54
  /** `--force` regenerates artifacts, so the kept-hand-written-Dockerfile apt warning does not apply. */
52
55
  force: boolean;
53
- /** The raw `--auth-path` flag; the chain (flag > FASTAGENT_AUTH_PATH > `<state root>/auth.json`)
56
+ /** The target delivers cron slots from an external clock and holds no resident process (AgentCore):
57
+ * resident-host keep-alive notes do not apply; the host branch owns its capability gates. */
58
+ externalClock?: boolean;
59
+ /** The raw `--auth-path` flag; the chain (flag > FASTAGENT_AUTH_PATH > `<agentDir>/.secrets/auth.json`)
54
60
  * is resolved HERE via {@link resolveAuthPath} — the one owner, same as every serving command. */
55
61
  authPathFlag: string | undefined;
56
62
  }): Promise<DeployPreflight>;