@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
@@ -2,21 +2,23 @@ import { webhookRunbook } from "../channel-ingress.js";
2
2
  import { containerArtifacts, imageHasGit } from "../container.js";
3
3
  import { deploymentLoginCommand } from "../box-shell.js";
4
4
  import { WAKEUPS_WHEN_ASLEEP, residencyFor } from "../residency.js";
5
+ import { RAILWAYIGNORE_FILE, railwayignore } from "./ignore.js";
6
+ import { stripEdges } from "../../strip-edges.js";
5
7
  /** State root = the volume mount path, kept in lockstep. */
6
8
  const MOUNT = "/data";
7
9
  /** The name this tool gives BOTH the project and the service, derived from the agent directory's name. */
8
10
  export function toRailwayName(basename) {
9
- return basename.replace(/[^a-zA-Z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
11
+ return stripEdges(basename.replace(/[^a-zA-Z0-9-]+/g, "-"), (char) => char === "-") || "agent";
10
12
  }
11
13
  /** Compute the Railway deploy plan from the resolved definition. */
12
14
  export function planRailwayDeploy(input) {
13
15
  const { serviceName, channels } = input;
14
- const artifacts = containerArtifacts(input);
16
+ const artifacts = [...containerArtifacts(input), { path: RAILWAYIGNORE_FILE, content: railwayignore() }];
15
17
  const secrets = input.secrets ?? [];
16
18
  // Order matters, not cosmetics: `railway init` creates a PROJECT with no service, but the volume and variables are
17
19
  // service-scoped and `railway up` deploys THE service.
18
20
  const runbook = [
19
- `# Deploy to Railway. Dockerfile(.dockerignore) are generated above.`,
21
+ `# Deploy to Railway. Dockerfile(.dockerignore, .railwayignore) are generated above.`,
20
22
  `# Prereqs: the Railway CLI (https://docs.railway.com/guides/cli) and \`railway login\`.`,
21
23
  ``,
22
24
  `# One-time setup (init → service → volume → variables). Skip it on redeploy: repeating it creates`,
@@ -38,14 +40,14 @@ export function planRailwayDeploy(input) {
38
40
  `railway variables set FASTAGENT_STATE_DIR=${MOUNT}/.state FASTAGENT_SECRETS_DIR=${MOUNT}/.secrets`,
39
41
  ];
40
42
  if (secrets.length > 0) {
41
- runbook.push(`# Secrets:`, `# ${secrets.map((s) => `${s.name}: ${s.hint}`).join("\n# ")}`, `railway variables set ${secrets.map((s) => `${s.name}=<value>`).join(" ")}`);
43
+ runbook.push(`# Secrets the service needs a value for. \`fastagent secrets push railway\` sets the ones a value file holds and`, `# \`fastagent secrets set railway <NAME>\` one typed without echo; or with the railway CLI:`, `# ${secrets.map((s) => `${s.name}: ${s.hint}`).join("\n# ")}`, `railway variables set ${secrets.map((s) => `${s.name}=<value>`).join(" ")}`);
42
44
  }
43
45
  runbook.push(``, `# Before a new definition release, run \`fastagent deploy railway\` to refresh the release manifest.`, `# Upload the agent directory and build on Railway (no local Docker needed): Railway builds the`, `# Dockerfile at its root. It marks the deploy live once the container starts, without waiting for`, `# /health: \`railway logs\` shows a box that crashes on boot.`, `railway up`);
44
46
  // The credential is created on the box, never carried: the box is then the only holder of its grant.
45
47
  if (input.boxLogin) {
46
48
  runbook.push(``, `# Model auth: once it is up, the deployment logs in to ${input.boxLogin} itself (from the agent directory):`, `${deploymentLoginCommand("railway", input.boxLogin)}`);
47
49
  }
48
- runbook.push(``, `# The volume keeps .state and .secrets across restarts and deploys; each release replaces /data/definition.`, input.shipsGit
50
+ runbook.push(``, `# The volume is the agent's working directory: it keeps its work, .state and .secrets across restarts and deploys.`, input.shipsGit
49
51
  ? `# Railway uploads may strip .git, so the deployed definition may have no history.`
50
52
  : imageHasGit(input)
51
53
  ? `# Git is installed (github content is cloned here, or context.json declares it in aptPackages).`
@@ -53,7 +55,7 @@ export function planRailwayDeploy(input) {
53
55
  // The public URL is minted, not deterministic (unlike Fly's <app>.fly.dev).
54
56
  const steps = webhookRunbook(`https://<your-domain>`, channels);
55
57
  if (steps.length > 0) {
56
- runbook.push(``, `# Public URL — Railway mints a *.up.railway.app domain (NOT deterministic). Generate it, then read`, `# the printed https URL and use it as <your-domain> in the webhook step(s) below:`, `railway domain`, ...steps);
58
+ runbook.push(``, `# Public URL — Railway mints a *.up.railway.app domain (NOT deterministic). Generate it, then read`, `# the printed https URL and use it as <your-domain> in the webhook step(s) below:`, `railway domain`, `# Or, before \`railway up\`, set it as http.publicUrl in the configuration \`deploy --config\` delivers, and`, `# re-run \`fastagent deploy railway\`: the box then points Telegram, Feishu and Lark there itself.`, ...steps);
57
59
  }
58
60
  // Scale-to-zero: App Sleeping is dashboard-only (no CLI/API) — a manual step, not a generated setting. WHY it is
59
61
  // forbidden is residency.ts's to decide; the SETTING and the path through the dashboard are Railway's.
@@ -1,9 +1,12 @@
1
1
  /** `fastagent deploy railway --run` — drive the Railway CLI to completion. */
2
2
  import type { BoxLoginStep } from "../box-shell.ts";
3
- import { type PublicHealthProbe, type Registrars } from "../channel-ingress.ts";
3
+ import { type BoxWebhooksProbe, type UrlRecorder, type PublicHealthProbe, type Registrars } from "../channel-ingress.ts";
4
4
  import type { DeclaredChannel } from "../../channels/discover.ts";
5
5
  import { type CliRunner } from "../runner.ts";
6
+ import type { SecretStore } from "../secret-store.ts";
6
7
  export interface RailwayRunPlan {
8
+ /** At a terminal with a `--config` file: record each URL a person set once the box confirms it. */
9
+ recordUrl?: UrlRecorder;
7
10
  /** Names both the project (`railway init --name`) and the service (`railway add --service`). */
8
11
  name: string;
9
12
  /**
@@ -11,12 +14,11 @@ export interface RailwayRunPlan {
11
14
  * in lockstep).
12
15
  */
13
16
  mountPath: string;
14
- /** `KEY=value` secrets set one-per-`variable set --stdin`: the value file's variables. */
15
- secrets: Record<string, string>;
16
- /** Declared names the value file supplies no value for — the run gates on these before any side effect. */
17
- missingSecrets: string[];
18
- /** That value file, agent-dir-relative, so the gate names the file this deploy actually read. */
19
- valueFile: string;
17
+ /**
18
+ * Make sure the service holds a value for every name the deployment needs, through whoever can supply one
19
+ * (secret-store.ts): resolves a gate line, or undefined. It runs once the service exists.
20
+ */
21
+ provideSecrets: (store: SecretStore) => Promise<string | undefined>;
20
22
  /** Every declared channel and its ingress — the driver asks which of them have a webhook. */
21
23
  channels: readonly DeclaredChannel[];
22
24
  /** Opt-in (CLI `--into-linked`) to provision INTO the project this directory is already linked to. */
@@ -33,6 +35,12 @@ export interface RailwayRunPlan {
33
35
  hostLogin?: () => Promise<string | undefined>;
34
36
  /** The command a coding agent runs to deploy this, its person's steps handed to its user (`--run --json`). */
35
37
  forAgent: string;
38
+ /**
39
+ * Put the service's domain into the configuration the image delivers (`http.publicUrl`), before `railway up` uploads
40
+ * it, so the box points its own channels there. Resolves the URL the configuration then names, the operator's own
41
+ * when it had one.
42
+ */
43
+ deliverPublicUrl: (url: string) => Promise<string>;
36
44
  }
37
45
  /**
38
46
  * Done (with the live URL), or a gate the operator must clear before re-running (printed + non-zero exit by the CLI).
@@ -44,13 +52,6 @@ export type RailwayRunOutcome = {
44
52
  ok: false;
45
53
  gate: string;
46
54
  };
47
- /** Whether `railway status --json` shows a linked project: non-empty stdout. */
48
- export declare function isLinked(stdout: string): boolean;
49
- /**
50
- * The linked project's name for the gate message, or undefined if it can't be read (still linked — `railway status
51
- * --json` puts `name` at the top level on 5.15.0).
52
- */
53
- export declare function linkedName(stdout: string): string | undefined;
54
55
  /** The first Railway-provided domain as an https URL, or undefined if none is present. */
55
56
  export declare function parseDomainUrl(stdout: string): string | undefined;
56
57
  /**
@@ -61,4 +62,9 @@ export declare function parseDomainUrl(stdout: string): string | undefined;
61
62
  * that is not JSON throws: read as "no volume", it would add a second one.
62
63
  */
63
64
  export declare function volumeOn(stdout: string, service: string, mountPath: string): string | undefined;
64
- export declare function deployRailwayRun(plan: RailwayRunPlan, railway: CliRunner, log: (msg: string) => void, registrars: Registrars, healthProbe?: PublicHealthProbe, sleep?: (ms: number) => Promise<void>): Promise<RailwayRunOutcome>;
65
+ export declare function deployRailwayRun(plan: RailwayRunPlan, railway: CliRunner, log: (msg: string) => void,
66
+ /** Slack's, which this machine still sets (its Request URL no credential on the box can change). */
67
+ registrars: Pick<Registrars, "leave">, healthProbe?: PublicHealthProbe, sleep?: (ms: number) => Promise<void>, boxWebhooks?: {
68
+ probe: BoxWebhooksProbe;
69
+ intervalMs?: number;
70
+ }): Promise<RailwayRunOutcome>;
@@ -1,25 +1,7 @@
1
- import { loginGate, publicHealthGate, registerWebhooks, } from "../channel-ingress.js";
1
+ import { confirmWebhooks, loginGate, publicHealthGate, } from "../channel-ingress.js";
2
2
  import { readOutput } from "../runner.js";
3
- import { hostSignedOutGate, missingValuesGate } from "../secrets.js";
4
- /** Whether `railway status --json` shows a linked project: non-empty stdout. */
5
- export function isLinked(stdout) {
6
- return stdout.trim() !== "";
7
- }
8
- /**
9
- * The linked project's name for the gate message, or undefined if it can't be read (still linked — `railway status
10
- * --json` puts `name` at the top level on 5.15.0).
11
- */
12
- export function linkedName(stdout) {
13
- try {
14
- const v = JSON.parse(stdout);
15
- if (typeof v.name === "string")
16
- return v.name;
17
- }
18
- catch {
19
- // non-JSON but non-empty → still linked, just no name to show
20
- }
21
- return undefined;
22
- }
3
+ import { hostSignedOutGate } from "../secrets.js";
4
+ import { ensureRailwayService, railwayStore } from "./store.js";
23
5
  /** Every string leaf of a parsed JSON value. */
24
6
  function jsonStrings(stdout) {
25
7
  const walk = (v) => typeof v === "string"
@@ -72,16 +54,13 @@ export function volumeOn(stdout, service, mountPath) {
72
54
  */
73
55
  const VOLUME_ATTACH_TIMEOUT_MS = 60_000;
74
56
  const VOLUME_POLL_MS = 2_000;
75
- export async function deployRailwayRun(plan, railway, log, registrars, healthProbe, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
57
+ export async function deployRailwayRun(plan, railway, log,
58
+ /** Slack's, which this machine still sets (its Request URL no credential on the box can change). */
59
+ registrars, healthProbe, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), boxWebhooks) {
76
60
  const gate = (g) => ({ ok: false, gate: g });
77
61
  // Every --service below targets plan.name — the name this tool gives BOTH the project and the service (`init
78
62
  // --name` + `add --service`).
79
63
  const svc = ["--service", plan.name];
80
- // 1. Gate missing required secret VALUES before any side effect (no half-created infra), and before asking a
81
- // person to sign in for a deploy that would stop here anyway.
82
- const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
83
- if (missingValues)
84
- return gate(missingValues);
85
64
  // 2.
86
65
  if ((await railway(["whoami"], { capture: true })).code !== 0) {
87
66
  const failed = plan.hostLogin
@@ -91,58 +70,22 @@ export async function deployRailwayRun(plan, railway, log, registrars, healthPro
91
70
  return gate(failed);
92
71
  }
93
72
  // 3.
94
- const status = await railway(["status", "--json"], { capture: true });
95
- if (isLinked(status.stdout)) {
96
- if (!plan.intoLinked) {
97
- const name = linkedName(status.stdout);
98
- return gate(`this directory is already linked to Railway project ${name ? `"${name}"` : "(name unreadable)"}. ` +
99
- "`--run` provisions a NEW project and only runs on an unlinked directory — it won't deploy into an " +
100
- "unrelated one. To redeploy an already-provisioned agent, run `railway up`. To provision the agent " +
101
- "INTO this project, re-run with --into-linked. To start fresh, `railway unlink` first.");
102
- }
103
- log(`provisioning into linked project ${linkedName(status.stdout) ?? plan.name} (--into-linked)`);
104
- }
105
- else {
106
- // --into-linked means "provision into the project this dir is linked to" — but it isn't linked.
107
- if (plan.intoLinked) {
108
- log("warn: --into-linked was passed but this directory isn't linked to any project — creating a fresh one");
109
- }
110
- log(`creating project ${plan.name}…`);
111
- if ((await railway(["init", "--name", plan.name])).code !== 0) {
112
- return gate("`railway init` failed — if you have multiple workspaces, run it once interactively (or pass --workspace) to pick one, then re-run");
113
- }
114
- // Create + link the service (init makes only a project); it precedes the volume, which has no --service flag and
115
- // rides the linked service.
116
- log(`creating service ${plan.name}…`);
117
- // `--variables` with one of the machinery variables set just below: in a terminal, a bare `railway add` stops at
118
- // "Enter a variable <esc to skip>" (5.62.1) and waits for a key nobody was told to press.
119
- const addArgs = ["add", "--service", plan.name, "--variables", `FASTAGENT_STATE_DIR=${plan.mountPath}/.state`];
120
- if ((await railway(addArgs)).code !== 0) {
121
- // Precise recovery, not "fix and re-run": init already created + linked the project, so a plain re-run hits the
122
- // linked-gate, and --into-linked SKIPS `add` and then fails at the volume (no service to ride).
123
- return gate(`\`railway add --service\` failed — \`railway init\` already created the project (this directory is now ` +
124
- `linked) but not the service. Finish with \`railway add --service ${plan.name}\` here, then re-run with ` +
125
- `--into-linked; or \`railway unlink\` to detach and start fresh.`);
126
- }
127
- }
73
+ const projectGate = await ensureRailwayService(railway, { name: plan.name, mountPath: plan.mountPath, intoLinked: plan.intoLinked }, log);
74
+ if (projectGate !== undefined)
75
+ return gate(projectGate);
76
+ // 3a. The names the service holds, the gaps filled.
77
+ const secretsGate = await plan.provideSecrets(railwayStore(railway, plan.name, log, { redeploy: false }));
78
+ if (secretsGate !== undefined)
79
+ return gate(secretsGate);
128
80
  // 3b.
129
81
  const machineryVars = [
130
82
  `FASTAGENT_STATE_DIR=${plan.mountPath}/.state`,
131
83
  `FASTAGENT_SECRETS_DIR=${plan.mountPath}/.secrets`,
132
84
  ];
133
- // The secret NAMES, not a count: `--run` uploads the whole value file, so the operator has to be able to see the
134
- // list on every host.
135
- const secretNames = Object.keys(plan.secrets);
136
- log(`setting ${machineryVars.map((v) => v.split("=")[0]).join("/")} + ${secretNames.length} secret(s)` +
137
- `${secretNames.length > 0 ? `: ${secretNames.join(", ")}` : ""}…`);
85
+ log(`setting ${machineryVars.map((v) => v.split("=")[0]).join("/")}…`);
138
86
  if ((await railway(["variables", "set", ...machineryVars, ...svc])).code !== 0) {
139
87
  return gate("`railway variables set` failed — see the railway output above");
140
88
  }
141
- for (const [k, v] of Object.entries(plan.secrets)) {
142
- if ((await railway(["variables", "set", k, "--stdin", ...svc], { input: v })).code !== 0) {
143
- return gate(`\`railway variables set ${k}\` failed — see the railway output above`);
144
- }
145
- }
146
89
  // 3c.
147
90
  const volumeStatus = () => readOutput(railway, "railway", ["volume", "list", "--json"], (out) => volumeOn(out, plan.name, plan.mountPath));
148
91
  const existing = await volumeStatus();
@@ -174,17 +117,19 @@ export async function deployRailwayRun(plan, railway, log, registrars, healthPro
174
117
  "check `railway volume list`, then re-run with --into-linked once it shows Ready");
175
118
  }
176
119
  }
177
- // 5. Deploy — CI mode streams build logs then exits (no interactive attach). Build runs on Railway.
178
- log("deploying (railway up)…");
179
- if ((await railway(["up", "--ci", ...svc])).code !== 0) {
180
- return gate("`railway up` failed — see the railway output above; fix and re-run");
181
- }
182
- // 6.
120
+ // 5. The domain before the upload: `railway domain` creates the service's on a service never deployed, and answers
121
+ // the existing one when run again, so the image the upload builds names it as its `http.publicUrl`.
183
122
  log("getting the public domain…");
184
123
  const url = parseDomainUrl((await railway(["domain", "--json", ...svc], { capture: true })).stdout);
185
124
  if (!url) {
186
125
  return gate("couldn't read a domain from `railway domain` — run `railway domain` manually, then set any webhook");
187
126
  }
127
+ const publicUrl = await plan.deliverPublicUrl(url);
128
+ // 6. Deploy — CI mode streams build logs then exits (no interactive attach). Build runs on Railway.
129
+ log("deploying (railway up)…");
130
+ if ((await railway(["up", "--ci", ...svc])).code !== 0) {
131
+ return gate("`railway up` failed — see the railway output above; fix and re-run");
132
+ }
188
133
  // 6b. `railway up --ci` exits 0 once the build is accepted, so readiness is asked here and not inferred.
189
134
  const healthGate = await publicHealthGate({
190
135
  baseUrl: url,
@@ -207,12 +152,21 @@ export async function deployRailwayRun(plan, railway, log, registrars, healthPro
207
152
  }));
208
153
  }
209
154
  // 7.
210
- const registrationGateMsg = await registerWebhooks({
155
+ // The box points its own channels at the `http.publicUrl` delivered above; this reads what it made of them.
156
+ const registrationGateMsg = await confirmWebhooks({
211
157
  baseUrl: url,
158
+ publicUrl,
212
159
  channels: plan.channels,
213
160
  registrars,
214
161
  log,
162
+ ...(plan.recordUrl === undefined ? {} : { record: plan.recordUrl }),
215
163
  retryHint: "re-run with --into-linked to retry registration",
164
+ ...(boxWebhooks === undefined
165
+ ? {}
166
+ : {
167
+ probe: boxWebhooks.probe,
168
+ ...(boxWebhooks.intervalMs === undefined ? {} : { intervalMs: boxWebhooks.intervalMs }),
169
+ }),
216
170
  });
217
171
  if (registrationGateMsg)
218
172
  return gate(registrationGateMsg);
@@ -0,0 +1,29 @@
1
+ /** Railway's service variables as a deployment's store: `railway variable list` for the names, `variables set` for values. */
2
+ import type { SecretStore } from "../secret-store.ts";
3
+ import { type CliRunner } from "../runner.ts";
4
+ /** Whether `railway status --json` shows a linked project: non-empty stdout. */
5
+ export declare function isLinked(stdout: string): boolean;
6
+ /**
7
+ * The linked project's name for the gate message, or undefined if it can't be read (still linked — `railway status
8
+ * --json` puts `name` at the top level on 5.15.0).
9
+ */
10
+ export declare function linkedName(stdout: string): string | undefined;
11
+ /**
12
+ * The project and service a deployment's values and image belong to. The project named for the agent (`secrets set` or
13
+ * an earlier `--run` made and linked it) is continued, and given the service when it lacks one. A directory linked to
14
+ * any other project is refused unless `intoLinked` says to provision into it, whatever that project holds: a service of
15
+ * the same name does not make it the agent's. An unlinked directory is given a new project. Resolves the gate line when
16
+ * it cannot be done.
17
+ */
18
+ export declare function ensureRailwayService(railway: CliRunner, plan: {
19
+ name: string;
20
+ mountPath: string;
21
+ intoLinked: boolean;
22
+ }, log: (message: string) => void): Promise<string | undefined>;
23
+ /**
24
+ * `service`'s variables, in the project and environment this directory is linked to. `redeploy`: the service restarts on
25
+ * what is written, once, after the last; off, for a deploy that follows and applies them.
26
+ */
27
+ export declare function railwayStore(railway: CliRunner, service: string, log: (message: string) => void, options: {
28
+ redeploy: boolean;
29
+ }): SecretStore;
@@ -0,0 +1,143 @@
1
+ import { readOutput } from "../runner.js";
2
+ /** Whether `railway status --json` shows a linked project: non-empty stdout. */
3
+ export function isLinked(stdout) {
4
+ return stdout.trim() !== "";
5
+ }
6
+ /**
7
+ * The linked project's name for the gate message, or undefined if it can't be read (still linked — `railway status
8
+ * --json` puts `name` at the top level on 5.15.0).
9
+ */
10
+ export function linkedName(stdout) {
11
+ try {
12
+ const v = JSON.parse(stdout);
13
+ if (typeof v.name === "string")
14
+ return v.name;
15
+ }
16
+ catch {
17
+ // non-JSON but non-empty → still linked, just no name to show
18
+ }
19
+ return undefined;
20
+ }
21
+ function unrelatedProject(name, agent) {
22
+ return (`this directory is already linked to Railway project ${name ? `"${name}"` : "(name unreadable)"}, not to ` +
23
+ `this agent's own (${agent}). \`--run\` won't deploy into an unrelated project. To provision the agent ` +
24
+ "INTO this project, re-run with --into-linked. To start fresh, `railway unlink` first.");
25
+ }
26
+ /** Whether `railway status --json` lists a service called `name`. */
27
+ function hasService(stdout, name) {
28
+ const walk = (value) => {
29
+ if (value === null || typeof value !== "object")
30
+ return false;
31
+ const object = value;
32
+ if (object.serviceName === name)
33
+ return true;
34
+ return Object.values(object).some(walk);
35
+ };
36
+ try {
37
+ return walk(JSON.parse(stdout));
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ /**
44
+ * The project and service a deployment's values and image belong to. The project named for the agent (`secrets set` or
45
+ * an earlier `--run` made and linked it) is continued, and given the service when it lacks one. A directory linked to
46
+ * any other project is refused unless `intoLinked` says to provision into it, whatever that project holds: a service of
47
+ * the same name does not make it the agent's. An unlinked directory is given a new project. Resolves the gate line when
48
+ * it cannot be done.
49
+ */
50
+ export async function ensureRailwayService(railway, plan, log) {
51
+ const status = await railway(["status", "--json"], { capture: true });
52
+ if (isLinked(status.stdout)) {
53
+ const name = linkedName(status.stdout);
54
+ if (!plan.intoLinked && name !== plan.name)
55
+ return unrelatedProject(name, plan.name);
56
+ if (plan.intoLinked)
57
+ log(`provisioning into linked project ${name ?? plan.name} (--into-linked)`);
58
+ if (hasService(status.stdout, plan.name))
59
+ return undefined;
60
+ return addService(railway, plan, log);
61
+ }
62
+ else {
63
+ // --into-linked means "provision into the project this dir is linked to" — but it isn't linked.
64
+ if (plan.intoLinked) {
65
+ log("warn: --into-linked was passed but this directory isn't linked to any project — creating a fresh one");
66
+ }
67
+ log(`creating project ${plan.name}…`);
68
+ if ((await railway(["init", "--name", plan.name])).code !== 0) {
69
+ return "`railway init` failed — if you have multiple workspaces, run it once interactively (or pass --workspace) to pick one, then re-run";
70
+ }
71
+ return addService(railway, plan, log);
72
+ }
73
+ }
74
+ async function addService(railway, plan, log) {
75
+ // `init` makes only a project; the service precedes the volume, which has no --service flag and rides the linked one.
76
+ log(`creating service ${plan.name}…`);
77
+ // `--variables` with one of the machinery variables set just below: in a terminal, a bare `railway add` stops at
78
+ // "Enter a variable <esc to skip>" (5.62.1) and waits for a key nobody was told to press.
79
+ const addArgs = ["add", "--service", plan.name, "--variables", `FASTAGENT_STATE_DIR=${plan.mountPath}/.state`];
80
+ if ((await railway(addArgs)).code !== 0) {
81
+ return (`\`railway add --service\` failed — the project ${plan.name} exists and this directory is linked to it, but ` +
82
+ `it has no service. Run \`railway add --service ${plan.name}\` here, then re-run; or \`railway unlink\` to ` +
83
+ `detach and start fresh.`);
84
+ }
85
+ return undefined;
86
+ }
87
+ /** The names `railway variable list --json` shows: the keys of an object. Its values are never read. */
88
+ function variableNames(stdout) {
89
+ let parsed;
90
+ try {
91
+ parsed = JSON.parse(stdout);
92
+ }
93
+ catch {
94
+ // Not the parser's message: it quotes the text around the error, and this output holds every value.
95
+ throw new Error("the output was not JSON");
96
+ }
97
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
98
+ throw new Error(`expected a JSON object of variables, got ${Array.isArray(parsed) ? "an array" : typeof parsed}`);
99
+ }
100
+ return new Set(Object.keys(parsed));
101
+ }
102
+ /**
103
+ * `service`'s variables, in the project and environment this directory is linked to. `redeploy`: the service restarts on
104
+ * what is written, once, after the last; off, for a deploy that follows and applies them.
105
+ */
106
+ export function railwayStore(railway, service, log, options) {
107
+ const svc = ["--service", service];
108
+ return {
109
+ async exists() {
110
+ const status = (await railway(["status", "--json"], { capture: true })).stdout;
111
+ if (!isLinked(status))
112
+ return false;
113
+ const name = linkedName(status);
114
+ if (name !== service)
115
+ throw new Error(unrelatedProject(name, service));
116
+ return true;
117
+ },
118
+ async ensure() {
119
+ const gate = await ensureRailwayService(railway, { name: service, mountPath: "/data", intoLinked: false }, log);
120
+ if (gate !== undefined)
121
+ throw new Error(gate);
122
+ },
123
+ async names() {
124
+ const listed = await readOutput(railway, "railway", ["variable", "list", "--json", ...svc], variableNames);
125
+ if ("gate" in listed)
126
+ throw new Error(listed.gate);
127
+ return listed.value;
128
+ },
129
+ async write(values) {
130
+ const names = Object.keys(values);
131
+ log(`setting ${names.length} variable(s) on ${service}: ${names.join(", ")}`);
132
+ // One value per call over stdin, never argv. `--skip-deploys` on all but the last of a write that is to take effect.
133
+ for (const [index, name] of names.entries()) {
134
+ const skip = options.redeploy && index === names.length - 1 ? [] : ["--skip-deploys"];
135
+ const result = await railway(["variables", "set", name, "--stdin", ...skip, ...svc], {
136
+ input: values[name],
137
+ });
138
+ if (result.code !== 0)
139
+ throw new Error(`\`railway variables set ${name}\` failed — see the railway output above`);
140
+ }
141
+ },
142
+ };
143
+ }
@@ -30,9 +30,12 @@ export declare function readOutput<T>(run: CliRunner, bin: string, args: string[
30
30
  }>;
31
31
  /**
32
32
  * Production {@link CliRunner}: spawn `bin` in `cwd` (the agent directory, which is the build/upload context). stderr
33
- * is always inherited to the terminal.
33
+ * is always inherited to the terminal. `stdoutToStderr`: what the CLI prints is not this command's result (a command
34
+ * whose stdout is data, `secrets set --generate`), so it goes to this process's stderr, still on the terminal.
34
35
  */
35
- export declare function spawnRunner(bin: string, cwd: string): CliRunner;
36
+ export declare function spawnRunner(bin: string, cwd: string, options?: {
37
+ stdoutToStderr?: boolean;
38
+ }): CliRunner;
36
39
  /**
37
40
  * The AWS CLI, as every command here runs it: with its pager off. Given a terminal, AWS CLI v2 pipes any output it
38
41
  * prints through `less`, so `deploy agentcore --run` stopped at `(END)` after `ecr create-repository` until someone
@@ -25,16 +25,17 @@ export async function readOutput(run, bin, args, read) {
25
25
  }
26
26
  /**
27
27
  * Production {@link CliRunner}: spawn `bin` in `cwd` (the agent directory, which is the build/upload context). stderr
28
- * is always inherited to the terminal.
28
+ * is always inherited to the terminal. `stdoutToStderr`: what the CLI prints is not this command's result (a command
29
+ * whose stdout is data, `secrets set --generate`), so it goes to this process's stderr, still on the terminal.
29
30
  */
30
- export function spawnRunner(bin, cwd) {
31
+ export function spawnRunner(bin, cwd, options = {}) {
31
32
  return (args, opts) => new Promise((res) => {
32
33
  const child = spawn(bin, args, {
33
34
  cwd,
34
35
  env: opts?.env ? { ...process.env, ...opts.env } : process.env,
35
36
  stdio: [
36
37
  opts?.input ? "pipe" : "inherit",
37
- opts?.capture ? "pipe" : "inherit",
38
+ opts?.capture ? "pipe" : options.stdoutToStderr ? 2 : "inherit",
38
39
  opts?.captureStderr ? "pipe" : "inherit",
39
40
  ],
40
41
  });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Where a deployment's values live and how a deploy fills the gaps (agent-artifact.md §6.2). A target keeps its own
3
+ * values; `deploy` checks names and never carries a file. Most stores can say which names they hold and not the values,
4
+ * so that is all the interface asks of them.
5
+ *
6
+ * The gaps are filled from the person's own sources, in the order a deploy can ask least of them: a value file
7
+ * (`.secrets/production/.env` unless another is named) pushes the names it holds in one batch after one question; a name
8
+ * no source has is asked for. A coding agent's run, with nobody to ask, stops naming the commands instead.
9
+ */
10
+ import type { SecretNeed } from "./secrets.ts";
11
+ export interface SecretStore {
12
+ /** Whether the app or service the values belong to exists: absent, it holds no value, and `names()` cannot be asked. */
13
+ exists(): Promise<boolean>;
14
+ /** Make the app or service the values belong to, when it does not exist yet. */
15
+ ensure(): Promise<void>;
16
+ /** The names the target holds a value for. */
17
+ names(): Promise<ReadonlySet<string>>;
18
+ /** Set `values` in one batch. */
19
+ write(values: Readonly<Record<string, string>>): Promise<void>;
20
+ }
21
+ /** How a person at a terminal is asked. */
22
+ export interface SecretAsker {
23
+ confirm(question: string): Promise<boolean>;
24
+ /** One value, typed without echo. */
25
+ secret(need: SecretNeed): Promise<string>;
26
+ }
27
+ /** The commands a gate names, written for the host and agent this run is about. */
28
+ export interface SecretCommands {
29
+ /** Push the names a value file holds: `fastagent secrets push fly`. */
30
+ push: string;
31
+ /** Set one name: `fastagent secrets set fly NAME`. */
32
+ set(name: string): string;
33
+ }
34
+ export interface ProvideSecrets {
35
+ /** What the target is called in what is said: "Fly". */
36
+ host: string;
37
+ store: SecretStore;
38
+ /** What the target must hold. */
39
+ needs: readonly SecretNeed[];
40
+ /** Every name the definition declares: a source's copy of one the target lacks is offered, optional or not. */
41
+ known: readonly string[];
42
+ /** The value file a source reads, when it exists. */
43
+ source?: {
44
+ file: string;
45
+ values: ReadonlyMap<string, string>;
46
+ };
47
+ /** Absent: nobody to ask. */
48
+ asker?: SecretAsker;
49
+ commands: SecretCommands;
50
+ /** Values FastAgent makes itself, set when neither the target nor a source has one (the Agent API's key). */
51
+ mint?: readonly {
52
+ name: string;
53
+ make(): string;
54
+ }[];
55
+ /** Say a line to the person who runs the deploy, for what a deploy with nobody to ask leaves undone. */
56
+ say?: (line: string) => void;
57
+ /** Say what the deploy did by itself, which leaves the person nothing to do. */
58
+ inform?: (line: string) => void;
59
+ /** What was minted, once the target holds it: kept on this machine too, where the person who calls the API looks. */
60
+ onMinted?: (values: Readonly<Record<string, string>>) => Promise<void>;
61
+ }
62
+ /** Resolves a gate line when the target still lacks a required name afterwards, undefined once it holds them all. */
63
+ export declare function provideSecrets(input: ProvideSecrets): Promise<string | undefined>;