@fastagent-sh/fastagent 0.29.0 → 0.30.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 (312) hide show
  1. package/README.md +9 -4
  2. package/dist/atomic-write.d.ts +4 -5
  3. package/dist/atomic-write.js +4 -5
  4. package/dist/authoring.d.ts +32 -15
  5. package/dist/authoring.js +147 -127
  6. package/dist/channels/agentcore-protocol.d.ts +8 -3
  7. package/dist/channels/agentcore-protocol.js +3 -1
  8. package/dist/channels/agentcore-service.d.ts +1 -0
  9. package/dist/channels/agentcore-service.js +14 -4
  10. package/dist/channels/agentcore.d.ts +9 -0
  11. package/dist/channels/agentcore.js +29 -4
  12. package/dist/channels/context.d.ts +2 -0
  13. package/dist/channels/context.js +5 -1
  14. package/dist/channels/feishu/feishu-api.d.ts +2 -1
  15. package/dist/channels/feishu/feishu.d.ts +0 -3
  16. package/dist/channels/feishu/feishu.js +10 -10
  17. package/dist/channels/feishu/register-app.js +3 -5
  18. package/dist/channels/feishu/scaffold/channel.ts +32 -34
  19. package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
  20. package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
  21. package/dist/channels/feishu/setup-mode.d.ts +2 -12
  22. package/dist/channels/feishu/setup-mode.js +2 -22
  23. package/dist/channels/feishu/setup.d.ts +12 -0
  24. package/dist/channels/feishu/setup.js +197 -0
  25. package/dist/channels/kit/abortable.d.ts +8 -0
  26. package/dist/channels/kit/abortable.js +22 -0
  27. package/dist/channels/lark/lark.d.ts +0 -3
  28. package/dist/channels/lark/lark.js +0 -5
  29. package/dist/channels/lark/scaffold/channel.ts +33 -35
  30. package/dist/channels/lark/scaffold/lark-send.ts +5 -5
  31. package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
  32. package/dist/channels/lark/setup.d.ts +9 -0
  33. package/dist/channels/lark/setup.js +57 -0
  34. package/dist/channels/setup.d.ts +112 -0
  35. package/dist/channels/setup.js +31 -0
  36. package/dist/channels/slack/history.js +1 -1
  37. package/dist/channels/slack/manifest.d.ts +17 -7
  38. package/dist/channels/slack/manifest.js +12 -16
  39. package/dist/channels/slack/scaffold/channel.ts +44 -30
  40. package/dist/channels/slack/scaffold/slack-send.ts +8 -8
  41. package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
  42. package/dist/channels/slack/setup.d.ts +7 -0
  43. package/dist/channels/slack/setup.js +119 -0
  44. package/dist/channels/slack/slack-api.d.ts +2 -0
  45. package/dist/channels/slack/slack-api.js +4 -0
  46. package/dist/channels/slack/slack.d.ts +21 -2
  47. package/dist/channels/slack/slack.js +398 -338
  48. package/dist/channels/slack/socket.d.ts +57 -0
  49. package/dist/channels/slack/socket.js +305 -0
  50. package/dist/channels/telegram/polling.d.ts +27 -0
  51. package/dist/channels/telegram/polling.js +105 -0
  52. package/dist/channels/telegram/scaffold/channel.ts +52 -32
  53. package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
  54. package/dist/channels/telegram/setup.d.ts +2 -0
  55. package/dist/channels/telegram/setup.js +42 -0
  56. package/dist/channels/telegram/telegram-api.d.ts +27 -1
  57. package/dist/channels/telegram/telegram-api.js +7 -6
  58. package/dist/channels/telegram/telegram.d.ts +11 -3
  59. package/dist/channels/telegram/telegram.js +184 -157
  60. package/dist/channels/webhook-evidence.d.ts +11 -0
  61. package/dist/channels/webhook-evidence.js +71 -0
  62. package/dist/channels/webhooks.d.ts +37 -0
  63. package/dist/channels/webhooks.js +70 -0
  64. package/dist/child-env.d.ts +16 -0
  65. package/dist/child-env.js +30 -0
  66. package/dist/cli/box-login.js +4 -1
  67. package/dist/cli/channel-app.d.ts +3 -0
  68. package/dist/cli/channel-app.js +35 -0
  69. package/dist/cli/channel-setup.d.ts +38 -0
  70. package/dist/cli/channel-setup.js +195 -0
  71. package/dist/cli/commands/add.d.ts +6 -14
  72. package/dist/cli/commands/add.js +45 -303
  73. package/dist/cli/commands/auth.js +8 -2
  74. package/dist/cli/commands/build.d.ts +5 -0
  75. package/dist/cli/commands/build.js +16 -0
  76. package/dist/cli/commands/channels.d.ts +19 -0
  77. package/dist/cli/commands/channels.js +230 -0
  78. package/dist/cli/commands/content.d.ts +7 -3
  79. package/dist/cli/commands/content.js +16 -10
  80. package/dist/cli/commands/deploy/agentcore.js +55 -23
  81. package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
  82. package/dist/cli/commands/deploy/channel-apps.js +30 -0
  83. package/dist/cli/commands/deploy/docker.js +32 -65
  84. package/dist/cli/commands/deploy/fly.js +28 -12
  85. package/dist/cli/commands/deploy/railway.js +38 -13
  86. package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
  87. package/dist/cli/commands/deploy/secrets-step.js +66 -0
  88. package/dist/cli/commands/deploy/shared.d.ts +16 -5
  89. package/dist/cli/commands/deploy/shared.js +25 -8
  90. package/dist/cli/commands/deploy.js +53 -60
  91. package/dist/cli/commands/dev.d.ts +2 -0
  92. package/dist/cli/commands/dev.js +15 -3
  93. package/dist/cli/commands/env.js +1 -1
  94. package/dist/cli/commands/info.d.ts +2 -0
  95. package/dist/cli/commands/info.js +60 -25
  96. package/dist/cli/commands/init.js +3 -1
  97. package/dist/cli/commands/invoke.d.ts +2 -0
  98. package/dist/cli/commands/invoke.js +20 -7
  99. package/dist/cli/commands/login.d.ts +0 -7
  100. package/dist/cli/commands/login.js +22 -10
  101. package/dist/cli/commands/logout.js +1 -1
  102. package/dist/cli/commands/mcp.js +14 -6
  103. package/dist/cli/commands/models.js +3 -2
  104. package/dist/cli/commands/schedules.d.ts +4 -2
  105. package/dist/cli/commands/schedules.js +14 -10
  106. package/dist/cli/commands/secrets.d.ts +14 -0
  107. package/dist/cli/commands/secrets.js +213 -0
  108. package/dist/cli/commands/start.d.ts +14 -14
  109. package/dist/cli/commands/start.js +46 -60
  110. package/dist/cli/content-view.d.ts +4 -6
  111. package/dist/cli/content-view.js +5 -11
  112. package/dist/cli/first-run-channels.d.ts +3 -0
  113. package/dist/cli/first-run-channels.js +63 -0
  114. package/dist/cli/instance.d.ts +41 -0
  115. package/dist/cli/instance.js +155 -0
  116. package/dist/cli/invoke-stream.d.ts +5 -2
  117. package/dist/cli/invoke-stream.js +7 -2
  118. package/dist/cli/kernel.js +3 -0
  119. package/dist/cli/program.js +282 -110
  120. package/dist/cli/serve.d.ts +4 -5
  121. package/dist/cli/serve.js +4 -7
  122. package/dist/cli/shared.d.ts +11 -2
  123. package/dist/cli/shared.js +32 -19
  124. package/dist/cli/ticket.d.ts +7 -0
  125. package/dist/cli/ticket.js +106 -30
  126. package/dist/content/declare.d.ts +31 -37
  127. package/dist/content/declare.js +59 -60
  128. package/dist/content/file.d.ts +23 -12
  129. package/dist/content/file.js +48 -35
  130. package/dist/content/git.d.ts +0 -10
  131. package/dist/content/git.js +5 -39
  132. package/dist/content/resolve.d.ts +18 -22
  133. package/dist/content/resolve.js +32 -66
  134. package/dist/content/source.d.ts +7 -8
  135. package/dist/content/source.js +10 -28
  136. package/dist/credentials-page.d.ts +26 -0
  137. package/dist/credentials-page.js +185 -0
  138. package/dist/declared-secrets.d.ts +10 -3
  139. package/dist/declared-secrets.js +8 -4
  140. package/dist/deploy/agentcore/destroy.d.ts +4 -1
  141. package/dist/deploy/agentcore/destroy.js +25 -1
  142. package/dist/deploy/agentcore/forwarder.js +1 -1
  143. package/dist/deploy/agentcore/plan.d.ts +7 -21
  144. package/dist/deploy/agentcore/plan.js +47 -49
  145. package/dist/deploy/agentcore/run.d.ts +18 -10
  146. package/dist/deploy/agentcore/run.js +93 -70
  147. package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
  148. package/dist/deploy/agentcore/runtime-secrets.js +108 -0
  149. package/dist/deploy/agentcore/shell.js +12 -19
  150. package/dist/deploy/agentcore/sigv4.d.ts +7 -0
  151. package/dist/deploy/agentcore/sigv4.js +15 -0
  152. package/dist/deploy/agentcore/slr.d.ts +18 -0
  153. package/dist/deploy/agentcore/slr.js +27 -0
  154. package/dist/deploy/agentcore/store.d.ts +7 -0
  155. package/dist/deploy/agentcore/store.js +115 -0
  156. package/dist/deploy/api.d.ts +14 -14
  157. package/dist/deploy/api.js +26 -24
  158. package/dist/deploy/box-shell.d.ts +3 -1
  159. package/dist/deploy/build-context.d.ts +4 -4
  160. package/dist/deploy/build-context.js +11 -15
  161. package/dist/deploy/channel-ingress.d.ts +35 -9
  162. package/dist/deploy/channel-ingress.js +119 -22
  163. package/dist/deploy/container.d.ts +35 -10
  164. package/dist/deploy/container.js +79 -37
  165. package/dist/deploy/delivered-text.d.ts +13 -0
  166. package/dist/deploy/delivered-text.js +86 -0
  167. package/dist/deploy/docker/plan.d.ts +7 -8
  168. package/dist/deploy/docker/plan.js +31 -52
  169. package/dist/deploy/docker/run.d.ts +5 -43
  170. package/dist/deploy/docker/run.js +9 -84
  171. package/dist/deploy/file-store.d.ts +2 -0
  172. package/dist/deploy/file-store.js +17 -0
  173. package/dist/deploy/fly/plan.d.ts +3 -2
  174. package/dist/deploy/fly/plan.js +20 -9
  175. package/dist/deploy/fly/run.d.ts +18 -8
  176. package/dist/deploy/fly/run.js +24 -42
  177. package/dist/deploy/fly/store.d.ts +12 -0
  178. package/dist/deploy/fly/store.js +83 -0
  179. package/dist/deploy/hosts.d.ts +1 -2
  180. package/dist/deploy/hosts.js +1 -2
  181. package/dist/deploy/preflight.d.ts +18 -5
  182. package/dist/deploy/preflight.js +135 -63
  183. package/dist/deploy/railway/ignore.d.ts +8 -0
  184. package/dist/deploy/railway/ignore.js +57 -0
  185. package/dist/deploy/railway/plan.d.ts +1 -2
  186. package/dist/deploy/railway/plan.js +8 -6
  187. package/dist/deploy/railway/run.d.ts +21 -15
  188. package/dist/deploy/railway/run.js +32 -78
  189. package/dist/deploy/railway/store.d.ts +29 -0
  190. package/dist/deploy/railway/store.js +143 -0
  191. package/dist/deploy/runner.d.ts +5 -2
  192. package/dist/deploy/runner.js +4 -3
  193. package/dist/deploy/secret-store.d.ts +63 -0
  194. package/dist/deploy/secret-store.js +98 -0
  195. package/dist/deploy/secrets.d.ts +29 -46
  196. package/dist/deploy/secrets.js +49 -122
  197. package/dist/deploy/workspace.d.ts +6 -12
  198. package/dist/deploy/workspace.js +18 -94
  199. package/dist/dev-supervisor.js +6 -8
  200. package/dist/environment/declare.js +3 -2
  201. package/dist/environment/mise.d.ts +3 -1
  202. package/dist/environment/mise.js +4 -2
  203. package/dist/feishu.d.ts +1 -1
  204. package/dist/feishu.js +1 -1
  205. package/dist/harnesses/models/catalog.d.ts +1 -0
  206. package/dist/harnesses/models/catalog.js +1 -2
  207. package/dist/harnesses/models/environment.d.ts +7 -2
  208. package/dist/harnesses/models/environment.js +9 -5
  209. package/dist/harnesses/models/host.d.ts +0 -7
  210. package/dist/harnesses/models/host.js +10 -1
  211. package/dist/harnesses/models/login.d.ts +0 -8
  212. package/dist/harnesses/models/login.js +5 -4
  213. package/dist/harnesses/models/models.d.ts +7 -2
  214. package/dist/harnesses/models/models.js +28 -4
  215. package/dist/harnesses/pi-durable/agent.js +59 -38
  216. package/dist/harnesses/pi-durable/build.d.ts +12 -0
  217. package/dist/harnesses/pi-durable/build.js +270 -0
  218. package/dist/harnesses/pi-durable/built.d.ts +24 -0
  219. package/dist/harnesses/pi-durable/built.js +136 -0
  220. package/dist/harnesses/pi-durable/config.d.ts +81 -0
  221. package/dist/harnesses/pi-durable/config.js +266 -0
  222. package/dist/harnesses/pi-durable/define.d.ts +63 -13
  223. package/dist/harnesses/pi-durable/define.js +23 -2
  224. package/dist/harnesses/pi-durable/definition.d.ts +72 -0
  225. package/dist/harnesses/pi-durable/definition.js +143 -0
  226. package/dist/harnesses/pi-durable/events.d.ts +4 -3
  227. package/dist/harnesses/pi-durable/events.js +179 -136
  228. package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
  229. package/dist/harnesses/pi-durable/execution-env.js +11 -0
  230. package/dist/harnesses/pi-durable/mcp.js +1 -3
  231. package/dist/harnesses/pi-durable/open.d.ts +35 -37
  232. package/dist/harnesses/pi-durable/open.js +91 -153
  233. package/dist/harnesses/pi-durable/reads.d.ts +0 -1
  234. package/dist/harnesses/pi-durable/reads.js +1 -3
  235. package/dist/harnesses/pi-durable/registry.d.ts +4 -6
  236. package/dist/harnesses/pi-durable/registry.js +29 -26
  237. package/dist/harnesses/pi-durable/release.d.ts +2 -2
  238. package/dist/harnesses/pi-durable/release.js +10 -7
  239. package/dist/harnesses/pi-durable/serve.d.ts +91 -15
  240. package/dist/harnesses/pi-durable/serve.js +109 -47
  241. package/dist/harnesses/pi-durable/sessions.js +15 -12
  242. package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
  243. package/dist/harnesses/pi-durable/text-sources.js +55 -0
  244. package/dist/harnesses/pi-durable/text.d.ts +16 -4
  245. package/dist/harnesses/pi-durable/text.js +115 -59
  246. package/dist/harnesses/pi-durable/wake.js +8 -8
  247. package/dist/http/client.js +9 -3
  248. package/dist/http/contract.d.ts +3 -3
  249. package/dist/http/server.d.ts +1 -1
  250. package/dist/http/server.js +1 -1
  251. package/dist/index.d.ts +8 -5
  252. package/dist/index.js +4 -1
  253. package/dist/instance-name.d.ts +7 -0
  254. package/dist/instance-name.js +26 -0
  255. package/dist/lark.d.ts +1 -1
  256. package/dist/lark.js +1 -1
  257. package/dist/mcp/client.js +11 -2
  258. package/dist/mcp/config.d.ts +70 -18
  259. package/dist/mcp/config.js +136 -53
  260. package/dist/mcp/host.d.ts +5 -2
  261. package/dist/mcp/host.js +14 -6
  262. package/dist/mcp/sign-in.js +2 -2
  263. package/dist/mcp-api.d.ts +14 -0
  264. package/dist/paths.d.ts +22 -8
  265. package/dist/paths.js +44 -11
  266. package/dist/runtime-websocket.d.ts +14 -0
  267. package/dist/runtime-websocket.js +39 -0
  268. package/dist/scaffold/add-channel.d.ts +9 -16
  269. package/dist/scaffold/add-channel.js +13 -83
  270. package/dist/scaffold/templates/env.example +4 -5
  271. package/dist/scaffold/templates/fastagent.config.ts +20 -14
  272. package/dist/scaffold/templates/gitignore +9 -6
  273. package/dist/scaffold/templates.d.ts +6 -2
  274. package/dist/scaffold/templates.js +7 -2
  275. package/dist/scaffold/vendor-skill.js +4 -2
  276. package/dist/service.d.ts +44 -6
  277. package/dist/service.js +110 -10
  278. package/dist/serving-command.d.ts +10 -0
  279. package/dist/serving-command.js +1 -0
  280. package/dist/slack.d.ts +1 -1
  281. package/dist/slack.js +1 -1
  282. package/dist/strip-edges.d.ts +10 -0
  283. package/dist/strip-edges.js +22 -0
  284. package/dist/telegram.d.ts +1 -1
  285. package/dist/telegram.js +1 -1
  286. package/dist/tunnel.d.ts +0 -1
  287. package/dist/tunnel.js +2 -8
  288. package/dist/value-file.d.ts +11 -6
  289. package/dist/value-file.js +21 -9
  290. package/dist/version.d.ts +7 -1
  291. package/dist/version.js +9 -3
  292. package/package.json +6 -5
  293. package/dist/channels/lark/credentials-page.d.ts +0 -25
  294. package/dist/channels/lark/credentials-page.js +0 -206
  295. package/dist/channels/lark/onboard.d.ts +0 -52
  296. package/dist/channels/lark/onboard.js +0 -86
  297. package/dist/channels/slack/config-api.d.ts +0 -69
  298. package/dist/channels/slack/config-api.js +0 -162
  299. package/dist/channels/slack/onboard.d.ts +0 -40
  300. package/dist/channels/slack/onboard.js +0 -136
  301. package/dist/channels/slack/onboarding-state.d.ts +0 -31
  302. package/dist/channels/slack/onboarding-state.js +0 -64
  303. package/dist/channels/slack/register-webhook.d.ts +0 -12
  304. package/dist/channels/slack/register-webhook.js +0 -67
  305. package/dist/channels/slack/setup-server.d.ts +0 -32
  306. package/dist/channels/slack/setup-server.js +0 -246
  307. package/dist/cli/add-feishu.d.ts +0 -41
  308. package/dist/cli/add-feishu.js +0 -391
  309. package/dist/cli/add-slack.d.ts +0 -18
  310. package/dist/cli/add-slack.js +0 -216
  311. package/dist/harnesses/pi-durable/parked.d.ts +0 -18
  312. package/dist/harnesses/pi-durable/parked.js +0 -27
@@ -0,0 +1,83 @@
1
+ import { readOutput } from "../runner.js";
2
+ /**
3
+ * Whether `fly apps list --json` lists an app called `name`. flyctl capitalizes the field, and an
4
+ * offline fixture can only repeat what we already believe, so a change in flyctl's output is not caught here.
5
+ */
6
+ function listHasName(stdout, name) {
7
+ const entries = JSON.parse(stdout);
8
+ if (!Array.isArray(entries))
9
+ throw new Error(`expected a JSON array, got ${typeof entries}`);
10
+ return entries.some((o) => o.Name === name);
11
+ }
12
+ /** The names `fly secrets list --json` shows (it never prints a value): each entry's `Name`. */
13
+ function secretNames(stdout) {
14
+ const entries = JSON.parse(stdout);
15
+ if (!Array.isArray(entries))
16
+ throw new Error(`expected a JSON array, got ${typeof entries}`);
17
+ return new Set(entries.flatMap((entry) => {
18
+ const { Name, name } = entry;
19
+ const found = Name ?? name;
20
+ return typeof found === "string" ? [found] : [];
21
+ }));
22
+ }
23
+ /** Create the app when `fly apps list` does not show it. Resolves the gate line when it cannot be made. */
24
+ export async function ensureFlyApp(fly, app, log) {
25
+ const exists = await readOutput(fly, "fly", ["apps", "list", "--json"], (out) => listHasName(out, app));
26
+ if ("gate" in exists)
27
+ return exists.gate;
28
+ if (exists.value) {
29
+ log(`app ${app} exists — skipping create`);
30
+ return undefined;
31
+ }
32
+ log(`creating app ${app}…`);
33
+ if ((await fly(["apps", "create", app])).code !== 0) {
34
+ return (`\`fly apps create ${app}\` failed — see the flyctl output above: the name may be taken (Fly app names are ` +
35
+ `globally unique: set a unique \`app\` in fly.toml) or the account may need payment information. Then re-run.`);
36
+ }
37
+ return undefined;
38
+ }
39
+ /** What `fly secrets set NAME=-` reads from standard input. */
40
+ const FLY_STDIN_LIMIT = 64 * 1024;
41
+ /**
42
+ * `stage`: set the secrets without deploying them, for a deploy that follows (`fly deploy` applies staged ones); off, a
43
+ * running app restarts on them at once.
44
+ */
45
+ export function flyStore(fly, app, log, options) {
46
+ return {
47
+ async exists() {
48
+ const listed = await readOutput(fly, "fly", ["apps", "list", "--json"], (out) => listHasName(out, app));
49
+ if ("gate" in listed)
50
+ throw new Error(listed.gate);
51
+ return listed.value;
52
+ },
53
+ async ensure() {
54
+ const gate = await ensureFlyApp(fly, app, log);
55
+ if (gate !== undefined)
56
+ throw new Error(gate);
57
+ },
58
+ async names() {
59
+ const listed = await readOutput(fly, "fly", ["secrets", "list", "-a", app, "--json"], secretNames);
60
+ if ("gate" in listed)
61
+ throw new Error(listed.gate);
62
+ return listed.value;
63
+ },
64
+ async write(values) {
65
+ const names = Object.keys(values);
66
+ const tooLong = names.find((name) => Buffer.byteLength(values[name]) > FLY_STDIN_LIMIT);
67
+ if (tooLong !== undefined) {
68
+ throw new Error(`${tooLong} is longer than the ${FLY_STDIN_LIMIT} bytes \`fly secrets set\` reads from standard input`);
69
+ }
70
+ log(`setting ${names.length} secret(s) on ${app}: ${names.join(", ")}`);
71
+ // `NAME=-` reads the value from standard input exactly as it is, and never from argv. `import` reads a dotenv-like
72
+ // line instead and changes a value that holds `#` or quotes, so it is not used. Staged, a running app restarts once,
73
+ // on the last.
74
+ for (const [index, name] of names.entries()) {
75
+ const last = index === names.length - 1;
76
+ const args = ["secrets", "set", ...(options.stage || !last ? ["--stage"] : []), "-a", app, `${name}=-`];
77
+ if ((await fly(args, { input: values[name] })).code !== 0) {
78
+ throw new Error(`\`fly secrets set ${name}=-\` failed — see the flyctl output above`);
79
+ }
80
+ }
81
+ },
82
+ };
83
+ }
@@ -11,8 +11,7 @@
11
11
  * below. Locally known refusals belong in `HostDeploy.validate`: the dispatcher runs it before app onboarding
12
12
  * and again on the facts onboarding produces. `deploy/` itself is a neutral kernel plus one directory per host; nothing host-specific
13
13
  * belongs in the kernel, and no host may hold a fact about another one (a host-only flag is a row in
14
- * `HOST_ONLY_FLAGS` in `cli/commands/deploy.ts`, warning elsewhere; `--tunnel` stays a usage GATE in `runDeploy`
15
- * because a refusal is not a row).
14
+ * `HOST_ONLY_FLAGS` in `cli/commands/deploy.ts`, warning elsewhere).
16
15
  *
17
16
  * BEFORE writing one, read that host's docs for what it does NOT do implicitly — above all, whether
18
17
  * a created resource is REACHABLE without a further step. On Fly, `[http_service]` declares a service and allocates
@@ -11,8 +11,7 @@
11
11
  * below. Locally known refusals belong in `HostDeploy.validate`: the dispatcher runs it before app onboarding
12
12
  * and again on the facts onboarding produces. `deploy/` itself is a neutral kernel plus one directory per host; nothing host-specific
13
13
  * belongs in the kernel, and no host may hold a fact about another one (a host-only flag is a row in
14
- * `HOST_ONLY_FLAGS` in `cli/commands/deploy.ts`, warning elsewhere; `--tunnel` stays a usage GATE in `runDeploy`
15
- * because a refusal is not a row).
14
+ * `HOST_ONLY_FLAGS` in `cli/commands/deploy.ts`, warning elsewhere).
16
15
  *
17
16
  * BEFORE writing one, read that host's docs for what it does NOT do implicitly — above all, whether
18
17
  * a created resource is REACHABLE without a further step. On Fly, `[http_service]` declares a service and allocates
@@ -1,4 +1,4 @@
1
- import type { AgentConfig } from "../harnesses/pi-durable/define.ts";
1
+ import { type EffectiveConfig, type InstanceConfigSource } from "../harnesses/pi-durable/config.ts";
2
2
  import { type DeclaredChannel } from "../channels/discover.ts";
3
3
  import type { DeclaredSecret } from "../declared-secrets.ts";
4
4
  import { type ContainerInput } from "./container.ts";
@@ -27,15 +27,23 @@ interface DeployFacts {
27
27
  * ({@link credentialRoute}).
28
28
  */
29
29
  modelAuth: string | undefined;
30
+ /**
31
+ * The variables the model's provider reads its key from, and whether the deployment needs one of them set on its
32
+ * target: when no key is in the value file and the provider has no login to run on the box, the key is the only way in.
33
+ */
34
+ modelKey: {
35
+ variables: string[];
36
+ required: boolean;
37
+ };
30
38
  /**
31
39
  * The provider the deployment must log in to itself (`fastagent login --deployment`): the model's credential does
32
40
  * not travel, because it is neither a variable in the value file nor resolved by the definition's own provider code.
33
41
  * Nothing from this machine's credentials file is ever copied, so a login on the box is its only holder.
34
42
  */
35
43
  boxLogin: string | undefined;
36
- /** Every declaration the definition makes — the names the value file must supply, by declaring file. */
37
- declaredSecrets: DeclaredSecret[];
38
- /** The runbook's variable list: the declared names, then everything else the value file carries. */
44
+ /** Every declaration the definition makes — the names the target must hold, by declaring file. */
45
+ declaredSecrets: readonly DeclaredSecret[];
46
+ /** The runbook's variable list: the declared names, each hinted by the file that declared it. */
39
47
  secrets: DeploymentSecret[];
40
48
  /** Container facts shared by the plan and the generated Dockerfile — ONE source, so they can't drift. */
41
49
  container: ContainerInput;
@@ -59,7 +67,12 @@ export interface DeployReport {
59
67
  }
60
68
  interface PreflightInput {
61
69
  agentDir: string;
62
- config: AgentConfig;
70
+ config: EffectiveConfig;
71
+ /** The instance configuration this deployment delivers (`--config`): as read, and its file's text. */
72
+ delivered?: {
73
+ source: InstanceConfigSource;
74
+ text: string;
75
+ };
63
76
  /** `--run` fully deploys, so a definition that resolves NO model is a GATE (a known crash-loop); else it warns. */
64
77
  run: boolean;
65
78
  /** `--force` regenerates the artifacts fastagent OWNS, so a kept `.dockerignore`'s content checks do not apply. */
@@ -8,42 +8,78 @@ import { basename, join, relative } from "node:path";
8
8
  import { isModelSpec, isReleaseAgentName } from "./workspace.js";
9
9
  import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
10
10
  import { resolveAuthPath } from "../harnesses/models/credentials.js";
11
- import { environmentAuth, interactiveLoginKind, loginProviders, modelsOver, providerOf, } from "../harnesses/models/models.js";
11
+ import { environmentAuth, interactiveLoginKind, keyVariables, listModels, loginProviders, modelHint, modelsOver, providerOf, } from "../harnesses/models/models.js";
12
12
  import { modelEnvironment } from "../harnesses/models/environment.js";
13
- import { buildDefinitionShape, definitionSecrets, prepareDefinition, } from "../harnesses/pi-durable/open.js";
14
- import { LOCAL_CONTEXT_FILE, exists } from "../paths.js";
13
+ import { projectInstanceConfig, } from "../harnesses/pi-durable/config.js";
14
+ import { definitionSecrets } from "../harnesses/pi-durable/definition.js";
15
+ import { buildDefinitionShape, loadSourceDefinition } from "../harnesses/pi-durable/open.js";
16
+ import { INSTANCE_CONFIG_FILE, exists } from "../paths.js";
15
17
  import { channelIngress } from "../channels/discover.js";
16
18
  import { loadContent, loadEnvironment } from "../content/file.js";
17
19
  import { CHANNEL_KINDS } from "../scaffold/add-channel.js";
18
20
  import { RUNTIME_FLOOR, belowFloor, detectRuntime, listsFastagent, readPackageJson } from "../runtime.js";
19
- import { fastagentVersion } from "../version.js";
20
- import { isGeneratedDockerfile } from "./container.js";
21
+ import { DELIVERED_CONFIG_DIR, isGeneratedDockerfile } from "./container.js";
22
+ import { deliveredTextProblem } from "./delivered-text.js";
23
+ import { declareMcp, literalCredentialAdvice, literalCredentials, relativeCommands, } from "../mcp/config.js";
21
24
  import { buildContextPaths, checkKeptIgnoreFiles } from "./build-context.js";
22
25
  import { dotEnvPath, loadEnvValues } from "../value-file.js";
23
26
  import { deploymentSecrets, isEnvKey } from "./secrets.js";
24
27
  import { DEFAULT_HTTP_PORT } from "../service.js";
25
28
  import { declaredVariables, declaresTool, runtimeAdvice } from "../environment/declare.js";
26
29
  import { checkPackages, MISE_PACKAGE_NAMES, miseBinary, packageSpecs } from "../environment/mise.js";
30
+ /** What a host's clone of a `github` entry reads; optional, since a public repository needs none. */
31
+ const GITHUB_TOKEN_SECRET = {
32
+ name: "GITHUB_TOKEN",
33
+ declaredIn: "context.json (a github content entry)",
34
+ description: "git's credential for a private repository, and for the agent to push",
35
+ optional: true,
36
+ };
37
+ /**
38
+ * The MCP servers the box will open: the program's, in the image, and the delivered configuration's. A name both
39
+ * declare, or one that cannot be read, is an issue (the box would refuse to open); a literal credential is a warning,
40
+ * since it ships in the image and FastAgent cannot tell a secret from a setting; a relative stdio command in the
41
+ * delivered configuration is an issue, since nothing delivers the script it names. (The program's own is relative to
42
+ * its package, and `build` refuses one that it does not copy.)
43
+ * `args` are not checked: FastAgent cannot know which strings are paths. Answers whether the list reads.
44
+ */
45
+ function checkMcp(agentDir, delivered, report) {
46
+ let declarations;
47
+ try {
48
+ declarations = declareMcp(agentDir, delivered);
49
+ }
50
+ catch (error) {
51
+ report.issue(messageOf(error));
52
+ return false;
53
+ }
54
+ for (const literal of literalCredentials(declarations)) {
55
+ report.warn(`${literal.file} mcpServers.${literal.server}.${literal.field}.${literal.key} is a literal value, and this ` +
56
+ `deploy puts it in the image: ${literalCredentialAdvice(literal)} and set that name with ` +
57
+ `\`fastagent secrets set <host> <NAME>\``);
58
+ }
59
+ for (const { name, file, command } of relativeCommands(declarations).filter(({ origin }) => origin === "instance")) {
60
+ report.issue(`${file} mcpServers.${name}: the command "${command}" is a relative path, and a deploy delivers the configuration ` +
61
+ `but not the script it names: write an absolute path on the box, or a command found on its PATH`);
62
+ }
63
+ return true;
64
+ }
27
65
  /**
28
66
  * What each content entry becomes on the host, one line each, so none is missing there unsaid (agent-model.md §3). A
29
- * repository `context.json` declares is cloned there, as on this machine without a checkout bound. A directory only
30
- * this machine binds stays here: the deployed agent works without it, which the line says and says how to change. It
31
- * is a warning, since the deployed agent lacks data it was meant to work on.
67
+ * repository, the program's or the delivered configuration's, is cloned there. A directory of this machine is not on
68
+ * the host, and FastAgent maps none onto it, so the delivered configuration naming one is an issue: the deployed agent
69
+ * would lack data it was configured to work on.
32
70
  */
33
- function checkContent({ declared, bound }, storageResets, report) {
71
+ function checkContent(declared, delivered, storageResets, report) {
72
+ const fate = storageResets
73
+ ? "cloned afresh on every deployment, since the host's storage starts over; what the agent did not push is lost"
74
+ : "cloned on the host, and brought up to date in place at each start";
34
75
  for (const entry of declared) {
35
- const fate = storageResets
36
- ? "cloned afresh on every deployment, since the host's storage starts over; what the agent did not push is lost"
37
- : "cloned on the host, and brought up to date in place at each start";
38
- report.note(`works on ${entry.name}: github ${entry.repo}${entry.ref ? `@${entry.ref}` : ""}, ${fate}`);
39
- }
40
- for (const binding of bound) {
41
- if (binding.kind !== "local")
76
+ if (entry.kind === "github") {
77
+ report.note(`works on ${entry.name}: github ${entry.repo}${entry.ref ? `@${entry.ref}` : ""}, ${fate}`);
42
78
  continue;
43
- const stays = `works on ${binding.name}: a directory of this machine (${binding.path}, bound in ` +
44
- `${LOCAL_CONTEXT_FILE}), stays here, and the deployed agent works without it`;
45
- report.warn(`${stays}; to work on it from a host, move it to a GitHub repository and add it as github:owner/repo, or copy ` +
46
- `what it only needs to read there into the agent directory, which every release carries`);
79
+ }
80
+ report.issue(`works on ${entry.name}: a directory of this machine (${entry.path}), in ${delivered}, which the host does not ` +
81
+ `have: declare a repository instead (github:owner/repo), or copy what the agent only needs to read into the ` +
82
+ `agent directory, which every release carries`);
47
83
  }
48
84
  }
49
85
  /**
@@ -52,12 +88,12 @@ function checkContent({ declared, bound }, storageResets, report) {
52
88
  * the agent's mise cannot check them, which is an issue (a warning without `--run`), as a code input that cannot load
53
89
  * is.
54
90
  */
55
- async function checkEnvironment(agentDir, packages, hasPackageJson, pkg, report) {
91
+ async function checkEnvironment(agentDir, packages, pkg, report) {
56
92
  if (Object.keys(packages).length === 0)
57
93
  return;
58
94
  const linux = MISE_PACKAGE_NAMES.filter((name) => name.includes("-linux-"));
59
95
  const missing = linux.filter((name) => !(name in (pkg.optionalDependencies ?? {})));
60
- if (!hasPackageJson || missing.length > 0) {
96
+ if (missing.length > 0) {
61
97
  report.issue(`context.json declares packages, and the image would have no mise to install them with: the agent's ` +
62
98
  `package.json does not list ${missing.join(", ")}. Run \`fastagent env install\` in the agent directory.`);
63
99
  return;
@@ -103,7 +139,7 @@ export async function preflightDeploy(input) {
103
139
  }
104
140
  }
105
141
  async function gatherFacts(input, report) {
106
- const { agentDir, config, force, noResidentProcess, storageResets = false, servesApi = false } = input;
142
+ const { agentDir, config, delivered, force, noResidentProcess, storageResets = false, servesApi = false } = input;
107
143
  // The release manifest carries this name into the container, where it is joined onto the storage root — so `init`'s
108
144
  // "one path segment" is not enough here.
109
145
  if (!isReleaseAgentName(basename(agentDir))) {
@@ -111,13 +147,28 @@ async function gatherFacts(input, report) {
111
147
  `may use only letters, digits, "-" and "_"; rename it (the fastagent.config.ts inside is what ` +
112
148
  `makes it an agent, never its name)`);
113
149
  }
114
- const content = loadContent(agentDir);
115
- checkContent(content, storageResets, report);
150
+ // A deployed agent is built (`start` opens `dist/`), and a build needs the project's own manifest and TypeScript.
151
+ if (!(await exists(join(agentDir, "package.json")))) {
152
+ throw new DeployGate(`the agent directory has no package.json, and a deployed agent is built: \`fastagent build\` needs one with ` +
153
+ `"type": "module" and typescript (\`fastagent init\` writes both)`);
154
+ }
155
+ const content = loadContent(agentDir, delivered?.source);
156
+ checkContent(content, delivered?.source.label, storageResets, report);
157
+ const textProblem = delivered === undefined ? undefined : deliveredTextProblem(agentDir, delivered.source);
158
+ if (textProblem !== undefined)
159
+ report.issue(textProblem);
160
+ const mcpOpens = checkMcp(agentDir, delivered?.source, report);
161
+ // The project's own, the development instance's: a deploy carries only the configuration it is given.
162
+ const own = await projectInstanceConfig(agentDir);
163
+ if (own !== undefined && own.label !== delivered?.source.label) {
164
+ report.note(`${INSTANCE_CONFIG_FILE} is the development instance's configuration and a deploy does not carry it` +
165
+ (delivered === undefined ? ": pass --config <file> to deliver one" : `; it carries ${delivered.source.label}`));
166
+ }
116
167
  // The definition the box will load on every start, read here first: a refusal in it (a module that fails to import,
117
168
  // two tools under one name) builds a perfectly good image that crash-loops on fly/railway/docker and fails every
118
169
  // invocation on AgentCore. This is the boundary that turns that refusal into a deploy finding, so the error is
119
170
  // carried whole into the issue rather than rethrown.
120
- const definition = await prepareDefinition(agentDir).catch((error) => {
171
+ const definition = await loadSourceDefinition(agentDir).catch((error) => {
121
172
  report.issue(`the definition does not load, so the deployed agent would not start: ${messageOf(error)}`);
122
173
  return undefined;
123
174
  });
@@ -125,11 +176,12 @@ async function gatherFacts(input, report) {
125
176
  // cannot disagree about it.
126
177
  const valueFile = relative(agentDir, dotEnvPath(agentDir));
127
178
  const values = loadEnvValues(dotEnvPath(agentDir));
128
- // A host has no credential of the author's: what it clones with is GITHUB_TOKEN, which travels like every value.
129
- if (content.declared.length > 0 && !values.get("GITHUB_TOKEN")) {
130
- report.note(`no GITHUB_TOKEN in ${valueFile}: the host clones without a credential, which reaches public repositories ` +
131
- `only, and the agent cannot push from its clones. Set one there to give it access; it travels with the ` +
132
- `other values`);
179
+ // A host has no credential of the author's: what it clones with is GITHUB_TOKEN, a value of the target like any other.
180
+ // This reads this machine's file only, so a target that already holds one is told as much.
181
+ if (content.length > 0 && !values.get("GITHUB_TOKEN")) {
182
+ report.note(`no GITHUB_TOKEN in ${valueFile}: unless the target holds one, the host clones without a credential, which ` +
183
+ `reaches public repositories only, and the agent cannot push from its clones. \`fastagent secrets set <host> ` +
184
+ `GITHUB_TOKEN\` gives it access`);
133
185
  }
134
186
  const model = resolveDeployModel(config, values, valueFile);
135
187
  if (model.invalid !== undefined) {
@@ -138,7 +190,7 @@ async function gatherFacts(input, report) {
138
190
  throw new DeployGate(`the model in ${model.source} is not a "provider/modelId" spec: ${JSON.stringify(model.invalid)}`);
139
191
  }
140
192
  if (!model.spec) {
141
- const issue = `no model resolves for this deployment — set \`model: "provider/id"\` in fastagent.config.ts (it travels ` +
193
+ const issue = `no model resolves for this deployment — set \`defaults: { model: "provider/id" }\` in fastagent.config.ts (it travels ` +
142
194
  `in the image), or FASTAGENT_MODEL in ${valueFile} (deploy records that one in the release manifest)` +
143
195
  // Whoever has the variable set right here sees `fastagent info` report a model, so "no model resolves" reads
144
196
  // like a bug until the message says which environment was read. It states that fact WITHOUT attributing the
@@ -154,8 +206,14 @@ async function gatherFacts(input, report) {
154
206
  report.note(`model ${model.spec} (source: ${model.source})`);
155
207
  }
156
208
  const modelSpec = model.spec;
157
- const declaredSecrets = definition ? definitionSecrets(definition) : declaredVariables(loadEnvironment(agentDir));
158
- const code = definition ? buildShapes(definition, values, report) : undefined;
209
+ const declaredSecrets = [
210
+ ...(definition
211
+ ? definitionSecrets(definition, config, mcpOpens ? delivered?.source : undefined)
212
+ : declaredVariables(loadEnvironment(agentDir))),
213
+ // The host's clone of a github entry reads it, so `secrets list|push|set` and `deploy` know the name.
214
+ ...(content.some((entry) => entry.kind === "github") ? [GITHUB_TOKEN_SECRET] : []),
215
+ ];
216
+ const code = definition ? buildShapes(definition, values, config, report) : undefined;
159
217
  const channels = [...(code?.channels ?? [])].map(([name, module]) => ({
160
218
  name,
161
219
  ingress: channelIngress(module),
@@ -163,7 +221,7 @@ async function gatherFacts(input, report) {
163
221
  for (const { name, ingress } of channels) {
164
222
  if (CHANNEL_KINDS.includes(name))
165
223
  continue;
166
- const secretsPart = `its variables travel from ${valueFile} like every other`;
224
+ const secretsPart = `the variables it declares are the target's to hold (\`fastagent secrets\`), like every other`;
167
225
  report.note(ingress === "long-connection"
168
226
  ? `long-connection channel "${name}" is custom — ${secretsPart}; generated deploy plans keep the process running and skip webhook registration`
169
227
  : `route channel "${name}" is custom — ${secretsPart}; configure its webhook yourself`);
@@ -195,19 +253,24 @@ async function gatherFacts(input, report) {
195
253
  valueFile,
196
254
  });
197
255
  }
256
+ const modelProvider = modelSpec ? loginProviders(providers).find((p) => p.id === providerOf(modelSpec)) : undefined;
257
+ const modelKey = {
258
+ variables: modelProvider ? await keyVariables(modelProvider) : [],
259
+ required: boxLogin !== undefined && !hasLogin(boxLogin, providers),
260
+ };
198
261
  if (boxLogin !== undefined) {
199
- if (!hasLogin(boxLogin, providers)) {
200
- report.issue(`no credential for ${modelSpec} reaches the deployment, and "${boxLogin}" has no login to run there — set ` +
201
- `its API key in ${valueFile}`);
262
+ if (modelKey.required) {
263
+ report.note(`${modelSpec}: "${boxLogin}" has no login to run on the box, so its API key (${modelKey.variables.join(" or ")}) ` +
264
+ `must be set on the target`);
202
265
  }
203
266
  else {
204
267
  report.note(`${modelSpec}: no credential ships with this deploy (this machine's logins and shell stay here) — once the ` +
205
268
  `box is up, \`--run\` asks it: it keeps what it already authenticates ${boxLogin} with, else logs in ` +
206
- `(\`fastagent login ${boxLogin} --deployment\`). Or set ${boxLogin}'s API key in ${valueFile}`);
269
+ `(\`fastagent login ${boxLogin} --deployment\`). Or set ${boxLogin}'s API key on the target ` +
270
+ `(\`fastagent secrets set <host> ${modelKey.variables[0] ?? "<KEY_NAME>"}\`)`);
207
271
  }
208
272
  }
209
273
  // Container facts (shared by every host) + the warnings that follow.
210
- const hasPackageJson = await exists(join(agentDir, "package.json"));
211
274
  const pkg = await readPackageJson(agentDir);
212
275
  const { runtime, bunVersion, hasLockfile } = detectRuntime(agentDir, pkg);
213
276
  const kept = await keptDockerfile(agentDir);
@@ -226,18 +289,13 @@ async function gatherFacts(input, report) {
226
289
  ((await exists(join(agentDir, "pnpm-lock.yaml"))) || (await exists(join(agentDir, "yarn.lock"))));
227
290
  // Does the baked agent directory ship a `.git`?
228
291
  const shipsGit = await exists(join(agentDir, ".git"));
229
- // After the facts: the deps sentence must match the agent's actual shape (a markdown-only agent has no package.json
230
- // and installs nothing — the note must not point at a file that doesn't exist).
231
- const deps = hasPackageJson
232
- ? `its package.json dependencies are installed in the image`
233
- : `the agent has no package.json, so no deps are installed (the pinned global CLI serves the directory)`;
234
292
  // What a RELEASE does — host-neutral, because how long the storage under it lives is the host's own answer and its
235
293
  // runbook gives it (a Fly volume outlives every deploy; AgentCore's mount does not).
236
294
  report.note(`the agent directory is baked as the definition (WYSIWYG — what you see is what ships, git or not, clean or ` +
237
- `not); ${deps}. Every release replaces the deployed definition and leaves the instance's state and ` +
238
- `credentials in place — for how long, see this host's storage note below`);
295
+ `not); its package.json dependencies are installed in the image. Every release replaces the image and leaves the instance's working directory, state and ` +
296
+ `credentials in place on the storage — for how long, see this host's storage note below`);
239
297
  // A code agent with no lockfile builds via a non-frozen install (ranges resolve at build time) — not reproducible.
240
- if (hasPackageJson && !hasLockfile) {
298
+ if (!hasLockfile) {
241
299
  const lock = runtime === "bun" ? "bun.lock" : "package-lock.json";
242
300
  report.warn(hasOtherLock
243
301
  ? `the generated Dockerfile is npm-based — your pnpm/yarn lockfile is NOT used (build runs ` +
@@ -246,7 +304,7 @@ async function gatherFacts(input, report) {
246
304
  `Run \`${install}\` and commit the lockfile for pinned redeploys.`);
247
305
  }
248
306
  // The code-path Dockerfile runs `${runner}`.
249
- if (hasPackageJson && !listsFastagent(pkg)) {
307
+ if (!listsFastagent(pkg)) {
250
308
  report.warn(`package.json does not list @fastagent-sh/fastagent — the image's \`${runner}\` has no local bin to run, ` +
251
309
  `so the container fails at start. Add it to dependencies and re-run \`${install}\`.`);
252
310
  }
@@ -254,12 +312,12 @@ async function gatherFacts(input, report) {
254
312
  await checkKeptIgnoreFiles({ agentDir, force, paths }, report);
255
313
  const environment = loadEnvironment(agentDir);
256
314
  const packages = environment?.packages ?? {};
257
- await checkEnvironment(agentDir, packages, hasPackageJson, pkg, report);
315
+ await checkEnvironment(agentDir, packages, pkg, report);
258
316
  const installsPackages = Object.keys(packages).length > 0;
259
317
  // Write-back mechanics are fastagent's (the policy is the agent's prompt's). A repository content entry is cloned on the
260
318
  // host, which takes git too. mise downloads over TLS, and the slim base image has no CA certificates; rustup, which
261
319
  // mise installs rust with, downloads with curl. The agent's own apt packages join them, in one layer.
262
- const needsGit = shipsGit || content.declared.length > 0;
320
+ const needsGit = shipsGit || content.length > 0;
263
321
  const apt = [
264
322
  ...new Set([
265
323
  ...(needsGit ? ["git"] : []),
@@ -272,30 +330,30 @@ async function gatherFacts(input, report) {
272
330
  releaseId: randomUUID(),
273
331
  agent: basename(agentDir),
274
332
  machineryPaths: paths.machineryPaths,
275
- hasPackageJson,
276
333
  runtime,
277
334
  bunVersion,
278
335
  hasLockfile,
279
- version: await fastagentVersion(),
280
336
  apt,
281
337
  ...(installsPackages ? { packages: packageSpecs(packages) } : {}),
282
338
  ...(model.envValue !== undefined ? { modelSpec: model.envValue } : {}),
339
+ ...(delivered !== undefined ? { config: delivered.text } : {}),
283
340
  shipsGit,
284
341
  };
285
342
  const port = config.http?.port ?? DEFAULT_HTTP_PORT;
286
343
  if (kept !== undefined)
287
- checkKeptDockerfile(kept, agentDir, model.envValue, valueFile, report);
344
+ checkKeptDockerfile(kept, agentDir, model.envValue, valueFile, delivered !== undefined, report);
288
345
  return {
289
346
  channels,
290
347
  hasCron,
291
348
  values,
292
349
  valueFile,
293
350
  modelAuth,
351
+ modelKey,
294
352
  boxLogin,
295
353
  container,
296
354
  port,
297
355
  declaredSecrets,
298
- secrets: deploymentSecrets(modelAuth, declaredSecrets, values, valueFile),
356
+ secrets: deploymentSecrets(modelAuth, declaredSecrets),
299
357
  };
300
358
  }
301
359
  /**
@@ -303,9 +361,9 @@ async function gatherFacts(input, report) {
303
361
  * deploy runs on a machine that may hold none of them, and whether the value file supplies them is the values gate's
304
362
  * question, asked by name. A build that fails is an issue, as a module that fails to load is.
305
363
  */
306
- function buildShapes(definition, values, report) {
364
+ function buildShapes(definition, values, config, report) {
307
365
  try {
308
- return buildDefinitionShape(definition, Object.fromEntries(values));
366
+ return buildDefinitionShape(definition, Object.fromEntries(values), config);
309
367
  }
310
368
  catch (error) {
311
369
  report.issue(`the definition does not build, so the deployed agent would not start: ${messageOf(error)}`);
@@ -343,8 +401,8 @@ async function checkModelExists(agentDir, spec, providers, report, box) {
343
401
  `as on a first deploy, fetches it from pi.dev as it starts, and refuses to start on "${spec}" if it cannot`);
344
402
  return;
345
403
  }
346
- report.issue(`no provider of the deployed agent offers "${spec}" — pi-ai's built-ins, pi's published catalog and the ` +
347
- `providers fastagent.config.ts declares are all it has`);
404
+ report.issue(`no provider of the deployed agent offers "${spec}": ${modelHint(listModels(models), spec)}. Its providers are ` +
405
+ `pi-ai's built-ins, pi's published catalog and the ones fastagent.config.ts declares`);
348
406
  }
349
407
  /**
350
408
  * HOW THE MODEL'S CREDENTIAL REACHES THE BOX, from what the deploy ships and nothing else. The box is the one authority
@@ -391,14 +449,22 @@ async function keptDockerfile(agentDir) {
391
449
  */
392
450
  function checkKeptDockerfile(dockerfileText, agentDir,
393
451
  /** The model the release manifest carries (set only when the value file named it). */
394
- modelFromValueFile, valueFile, report) {
395
- // What the image installs: the packages and apt packages. Variables travel with the value file, whatever the image.
452
+ modelFromValueFile, valueFile,
453
+ /** The release delivers an instance configuration, which the image must copy and name. */
454
+ deliversConfig, report) {
455
+ // What the image installs: the packages and apt packages. Variables are the target's, whatever the image.
396
456
  const environment = loadEnvironment(agentDir);
397
457
  if (Object.keys(environment?.packages ?? {}).length > 0 || (environment?.aptPackages.length ?? 0) > 0) {
398
458
  report.warn(`kept your hand-written Dockerfile — it installs the environment context.json declares only if it says so: ` +
399
459
  `a generated Dockerfile shows the layers (the apt packages, then \`mise install --system\` of the packages ` +
400
460
  `context.json declares).`);
401
461
  }
462
+ // `start` opens dist/. Docker and Fly upload the directory as it is, but `railway up` follows .gitignore, which the
463
+ // scaffold gives `/dist`: without a build step of its own the image has none, and the box does not start.
464
+ if (!/\bfastagent(?:\s+run)?\s+build\b|\bnpm\s+run\s+build\b/.test(dockerfileText)) {
465
+ report.warn(`your Dockerfile does not run \`fastagent build\`: \`start\` opens dist/, which an upload that follows ` +
466
+ `.gitignore (Railway) leaves out. Add \`RUN ./node_modules/.bin/fastagent build\` after copying the agent.`);
467
+ }
402
468
  // The INSTRUCTION is the question, not the file's authorship: `prepareStartWorkspace` returns early without
403
469
  // FASTAGENT_RELEASE_FILE, so a Dockerfile that sets it reads the manifest whoever wrote it. This gates rather
404
470
  // than warns because it is about FastAgent's OWN delivery arriving — the model would be reported here and
@@ -406,12 +472,18 @@ modelFromValueFile, valueFile, report) {
406
472
  // Anywhere in an `ENV` instruction, not just first: `ENV A=1 FASTAGENT_RELEASE_FILE=/app/x` is ordinary
407
473
  // Dockerfile style and hard-refusing it would be a false gate. A backslash continuation still reads as absent
408
474
  // (covering it means joining lines first) — the remaining over-strict edge.
409
- if (modelFromValueFile !== undefined && !/^\s*ENV\s[^\n]*\bFASTAGENT_RELEASE_FILE[=\s]/m.test(dockerfileText)) {
475
+ const readsManifest = /^\s*ENV\s[^\n]*\bFASTAGENT_RELEASE_FILE[=\s]/m.test(dockerfileText);
476
+ if (modelFromValueFile !== undefined && !readsManifest) {
410
477
  const issue = `your Dockerfile does not set FASTAGENT_RELEASE_FILE, and the model comes from ${valueFile} — it travels ` +
411
478
  `in the release manifest, which is only read when that ENV points at it. Add it (see a generated ` +
412
- `Dockerfile), or set \`model\` in fastagent.config.ts so it ships in the config instead.`;
479
+ `Dockerfile), or set \`defaults.model\` in fastagent.config.ts so it ships in the config instead.`;
413
480
  report.issue(issue);
414
481
  }
482
+ if (deliversConfig && !/^\s*ENV\s[^\n]*\bFASTAGENT_CONFIG[=\s]/m.test(dockerfileText)) {
483
+ report.issue(`your Dockerfile does not set FASTAGENT_CONFIG, and --config delivers the instance configuration in the ` +
484
+ `image, which \`start\` reads only where that ENV points: copy ${DELIVERED_CONFIG_DIR}/ into the image and set it ` +
485
+ `(see a generated Dockerfile).`);
486
+ }
415
487
  }
416
488
  /**
417
489
  * WHICH model this deployment runs on, and where that came from.
@@ -0,0 +1,8 @@
1
+ export declare const RAILWAYIGNORE_FILE = ".railwayignore";
2
+ export declare function isGeneratedRailwayignore(content: string): boolean;
3
+ export declare function railwayignore(): string;
4
+ /**
5
+ * Why a `.railwayignore` the author kept would leave the deploy's own directory out of the upload, so the image build
6
+ * could not copy it, or undefined when it lets it through (or there is none to keep).
7
+ */
8
+ export declare function keptRailwayignoreProblem(agentDir: string): string | undefined;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * What `railway up` uploads. It follows `.gitignore`, and only inside a git repository, so a project without one would
3
+ * upload its credentials, and one with one would drop `.fastagent/deploy/`, which `.gitignore` ignores and the image is
4
+ * built from. `.railwayignore` outranks `.gitignore`; a parent that is ignored hides its children, hence the three
5
+ * lines for the one directory (verified against Railway, which reads the same patterns as ripgrep).
6
+ */
7
+ import { readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import ignore from "ignore";
10
+ import { DELIVERED_CONFIG_DIR } from "../container.js";
11
+ export const RAILWAYIGNORE_FILE = ".railwayignore";
12
+ const MARKER = "# Generated by `fastagent deploy railway`";
13
+ export function isGeneratedRailwayignore(content) {
14
+ return content.startsWith(MARKER);
15
+ }
16
+ export function railwayignore() {
17
+ return `${MARKER}. Delete this line to take ownership (deploy then keeps your file).
18
+ # Credentials, state and clones never leave this machine, whatever .gitignore says.
19
+ .secrets
20
+ .state
21
+ /content
22
+ .env
23
+ .env.*
24
+ !.env.example
25
+ # The deploy's own directory is what the image is built from, and .gitignore ignores .fastagent.
26
+ !.fastagent
27
+ .fastagent/*
28
+ !${DELIVERED_CONFIG_DIR}
29
+ `;
30
+ }
31
+ function readIfExists(path) {
32
+ try {
33
+ return readFileSync(path, "utf8");
34
+ }
35
+ catch (error) {
36
+ if (error.code === "ENOENT")
37
+ return "";
38
+ throw error;
39
+ }
40
+ }
41
+ /**
42
+ * Why a `.railwayignore` the author kept would leave the deploy's own directory out of the upload, so the image build
43
+ * could not copy it, or undefined when it lets it through (or there is none to keep).
44
+ */
45
+ export function keptRailwayignoreProblem(agentDir) {
46
+ const kept = readIfExists(join(agentDir, RAILWAYIGNORE_FILE));
47
+ if (kept === "" || isGeneratedRailwayignore(kept))
48
+ return undefined;
49
+ const upload = ignore()
50
+ .add(readIfExists(join(agentDir, ".gitignore")))
51
+ .add(kept);
52
+ if (!upload.ignores(`${DELIVERED_CONFIG_DIR}/fastagent.json`))
53
+ return undefined;
54
+ return (`your ${RAILWAYIGNORE_FILE} (kept), with .gitignore, leaves ${DELIVERED_CONFIG_DIR}/ out of what \`railway up\` ` +
55
+ `uploads, and the image is built from it. Add \`!.fastagent\`, \`.fastagent/*\` and \`!${DELIVERED_CONFIG_DIR}\` ` +
56
+ `to it, or delete it to let deploy generate one.`);
57
+ }
@@ -16,8 +16,7 @@ export interface RailwayPlanInput extends ContainerInput {
16
16
  */
17
17
  channels: readonly DeclaredChannel[];
18
18
  /**
19
- * The runbook's variable list (`deploymentSecrets`): what must have a value, then everything else the value file
20
- * carries.
19
+ * The runbook's variable list (`deploymentSecrets`): the declared names, each hinted by the file that declared it.
21
20
  */
22
21
  secrets?: readonly DeploymentSecret[];
23
22
  /** `schedules/` declares a schedule — one of the things that forbids App Sleeping (deploy/residency.ts). */