@fastagent-sh/fastagent 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/README.md +5 -1
  2. package/dist/atomic-write.d.ts +11 -7
  3. package/dist/atomic-write.js +16 -11
  4. package/dist/channels/agentcore-limits.d.ts +9 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-protocol.d.ts +112 -0
  7. package/dist/channels/agentcore-protocol.js +22 -0
  8. package/dist/channels/agentcore-service.d.ts +12 -15
  9. package/dist/channels/agentcore-service.js +15 -24
  10. package/dist/channels/agentcore-state.d.ts +5 -11
  11. package/dist/channels/agentcore-state.js +4 -1
  12. package/dist/channels/agentcore.d.ts +9 -83
  13. package/dist/channels/agentcore.js +101 -93
  14. package/dist/channels/control.d.ts +50 -12
  15. package/dist/channels/control.js +251 -157
  16. package/dist/channels/discover.d.ts +26 -5
  17. package/dist/channels/discover.js +13 -26
  18. package/dist/channels/feishu/context-buffer.d.ts +6 -0
  19. package/dist/channels/feishu/context-buffer.js +0 -38
  20. package/dist/channels/feishu/crypto.d.ts +0 -2
  21. package/dist/channels/feishu/crypto.js +3 -8
  22. package/dist/channels/feishu/feishu-api.js +7 -12
  23. package/dist/channels/feishu/feishu.js +86 -128
  24. package/dist/channels/feishu/parse.d.ts +4 -3
  25. package/dist/channels/feishu/parse.js +5 -4
  26. package/dist/channels/feishu/register-webhook.d.ts +5 -5
  27. package/dist/channels/feishu/register-webhook.js +43 -57
  28. package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
  29. package/dist/channels/feishu/setup-mode.d.ts +30 -0
  30. package/dist/channels/feishu/setup-mode.js +26 -0
  31. package/dist/channels/feishu/shared-api.d.ts +10 -0
  32. package/dist/channels/feishu/shared-api.js +38 -0
  33. package/dist/channels/http.d.ts +0 -8
  34. package/dist/channels/http.js +4 -56
  35. package/dist/channels/kit/attachment-path.d.ts +12 -0
  36. package/dist/channels/kit/attachment-path.js +43 -0
  37. package/dist/channels/kit/context-buffer.d.ts +9 -0
  38. package/dist/channels/kit/context-buffer.js +11 -0
  39. package/dist/channels/kit/signature.d.ts +12 -0
  40. package/dist/channels/kit/signature.js +17 -0
  41. package/dist/channels/kit/stop-command.js +3 -3
  42. package/dist/channels/kit/tasks.d.ts +1 -1
  43. package/dist/channels/kit/tasks.js +10 -4
  44. package/dist/channels/kit/turn-runner.d.ts +59 -0
  45. package/dist/channels/kit/turn-runner.js +84 -0
  46. package/dist/channels/kit/turn-store.d.ts +19 -3
  47. package/dist/channels/kit/turn-store.js +29 -2
  48. package/dist/channels/lark/scaffold/lark-send.ts +12 -67
  49. package/dist/channels/registration.d.ts +36 -1
  50. package/dist/channels/registration.js +57 -1
  51. package/dist/channels/secret.d.ts +1 -0
  52. package/dist/channels/secret.js +16 -0
  53. package/dist/channels/slack/config-api.d.ts +35 -3
  54. package/dist/channels/slack/config-api.js +51 -9
  55. package/dist/channels/slack/invoke-turn.d.ts +1 -1
  56. package/dist/channels/slack/invoke-turn.js +5 -1
  57. package/dist/channels/slack/manifest.js +5 -1
  58. package/dist/channels/slack/onboard.d.ts +5 -5
  59. package/dist/channels/slack/onboard.js +44 -23
  60. package/dist/channels/slack/onboarding-state.d.ts +7 -3
  61. package/dist/channels/slack/onboarding-state.js +13 -20
  62. package/dist/channels/slack/register-webhook.d.ts +3 -2
  63. package/dist/channels/slack/register-webhook.js +35 -15
  64. package/dist/channels/slack/scaffold/channel.ts +3 -10
  65. package/dist/channels/slack/scaffold/slack-send.ts +18 -141
  66. package/dist/channels/slack/shared-api.d.ts +10 -0
  67. package/dist/channels/slack/shared-api.js +34 -0
  68. package/dist/channels/slack/slack-api.d.ts +20 -2
  69. package/dist/channels/slack/slack-api.js +100 -73
  70. package/dist/channels/slack/slack.d.ts +0 -10
  71. package/dist/channels/slack/slack.js +68 -107
  72. package/dist/channels/sse.d.ts +4 -0
  73. package/dist/channels/sse.js +66 -0
  74. package/dist/channels/telegram/parse.d.ts +21 -1
  75. package/dist/channels/telegram/parse.js +65 -11
  76. package/dist/channels/telegram/register-webhook.d.ts +6 -9
  77. package/dist/channels/telegram/register-webhook.js +44 -42
  78. package/dist/channels/telegram/scaffold/channel.ts +7 -3
  79. package/dist/channels/telegram/telegram-api.js +5 -6
  80. package/dist/channels/telegram/telegram.d.ts +2 -2
  81. package/dist/channels/telegram/telegram.js +97 -204
  82. package/dist/channels/wait-health.js +7 -4
  83. package/dist/cli/add-feishu.js +3 -10
  84. package/dist/cli/add-slack.js +13 -25
  85. package/dist/cli/commands/add.d.ts +0 -1
  86. package/dist/cli/commands/add.js +22 -35
  87. package/dist/cli/commands/attach.d.ts +2 -4
  88. package/dist/cli/commands/attach.js +17 -15
  89. package/dist/cli/commands/chat.js +6 -12
  90. package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
  91. package/dist/cli/commands/deploy/agentcore.js +178 -0
  92. package/dist/cli/commands/deploy/docker.d.ts +2 -0
  93. package/dist/cli/commands/deploy/docker.js +119 -0
  94. package/dist/cli/commands/deploy/fly.d.ts +2 -0
  95. package/dist/cli/commands/deploy/fly.js +131 -0
  96. package/dist/cli/commands/deploy/railway.d.ts +2 -0
  97. package/dist/cli/commands/deploy/railway.js +71 -0
  98. package/dist/cli/commands/deploy/shared.d.ts +114 -0
  99. package/dist/cli/commands/deploy/shared.js +124 -0
  100. package/dist/cli/commands/deploy.d.ts +38 -25
  101. package/dist/cli/commands/deploy.js +101 -631
  102. package/dist/cli/commands/dev.js +20 -76
  103. package/dist/cli/commands/fire.js +6 -12
  104. package/dist/cli/commands/info.js +1 -1
  105. package/dist/cli/commands/init.js +1 -2
  106. package/dist/cli/commands/invoke.js +4 -11
  107. package/dist/cli/commands/schedule.js +1 -1
  108. package/dist/cli/commands/start.js +41 -80
  109. package/dist/cli/commands/tool.js +3 -8
  110. package/dist/cli/kernel.d.ts +0 -2
  111. package/dist/cli/kernel.js +0 -2
  112. package/dist/cli/program.js +7 -12
  113. package/dist/cli/serve.d.ts +42 -34
  114. package/dist/cli/serve.js +107 -28
  115. package/dist/cli/shared.d.ts +52 -27
  116. package/dist/cli/shared.js +69 -6
  117. package/dist/deploy/agentcore/forwarder.js +250 -0
  118. package/dist/deploy/agentcore/logs.d.ts +2 -2
  119. package/dist/deploy/agentcore/logs.js +2 -2
  120. package/dist/deploy/agentcore/plan.d.ts +34 -11
  121. package/dist/deploy/agentcore/plan.js +52 -259
  122. package/dist/deploy/agentcore/run.d.ts +9 -24
  123. package/dist/deploy/agentcore/run.js +35 -43
  124. package/dist/deploy/channel-ingress.d.ts +73 -0
  125. package/dist/deploy/channel-ingress.js +101 -0
  126. package/dist/deploy/docker/plan.d.ts +3 -7
  127. package/dist/deploy/docker/plan.js +4 -15
  128. package/dist/deploy/docker/run.d.ts +35 -4
  129. package/dist/deploy/docker/run.js +40 -8
  130. package/dist/deploy/fly/plan.d.ts +4 -5
  131. package/dist/deploy/fly/plan.js +14 -22
  132. package/dist/deploy/fly/run.d.ts +36 -23
  133. package/dist/deploy/fly/run.js +129 -83
  134. package/dist/deploy/hosts.d.ts +5 -0
  135. package/dist/deploy/hosts.js +4 -0
  136. package/dist/deploy/preflight.d.ts +6 -7
  137. package/dist/deploy/preflight.js +27 -23
  138. package/dist/deploy/railway/plan.d.ts +9 -5
  139. package/dist/deploy/railway/plan.js +18 -26
  140. package/dist/deploy/railway/run.d.ts +11 -10
  141. package/dist/deploy/railway/run.js +44 -43
  142. package/dist/deploy/runner.js +22 -2
  143. package/dist/deploy/secrets.d.ts +18 -9
  144. package/dist/deploy/secrets.js +63 -28
  145. package/dist/dev-supervisor.js +3 -4
  146. package/dist/engines/pi/agent-session-factory.d.ts +37 -17
  147. package/dist/engines/pi/agent-session-factory.js +111 -106
  148. package/dist/engines/pi/auth.js +43 -43
  149. package/dist/engines/pi/config.d.ts +2 -2
  150. package/dist/engines/pi/create.d.ts +22 -27
  151. package/dist/engines/pi/create.js +59 -74
  152. package/dist/engines/pi/definition.d.ts +1 -1
  153. package/dist/engines/pi/definition.js +11 -12
  154. package/dist/engines/pi/invoke-session.js +13 -30
  155. package/dist/engines/pi/login.js +32 -16
  156. package/dist/engines/pi/open.d.ts +3 -4
  157. package/dist/engines/pi/open.js +32 -42
  158. package/dist/engines/pi/retry-event.d.ts +6 -0
  159. package/dist/engines/pi/retry-event.js +15 -0
  160. package/dist/engines/pi/search-tools.js +1 -1
  161. package/dist/engines/pi/service.d.ts +1 -1
  162. package/dist/engines/pi/service.js +8 -0
  163. package/dist/engines/pi/session-builder.js +26 -140
  164. package/dist/engines/pi/session-control.d.ts +27 -22
  165. package/dist/engines/pi/session-control.js +551 -490
  166. package/dist/engines/pi/session-inheritance.d.ts +8 -22
  167. package/dist/engines/pi/session-inheritance.js +95 -76
  168. package/dist/engines/pi/session-markers.d.ts +48 -0
  169. package/dist/engines/pi/session-markers.js +59 -0
  170. package/dist/engines/pi/session-settings.d.ts +5 -5
  171. package/dist/engines/pi/session-settings.js +8 -5
  172. package/dist/engines/pi/session-store.d.ts +91 -26
  173. package/dist/engines/pi/session-store.js +413 -82
  174. package/dist/engines/pi/tool-context.d.ts +24 -11
  175. package/dist/engines/pi/tool-context.js +29 -4
  176. package/dist/engines/pi/tool.d.ts +6 -9
  177. package/dist/engines/pi/tool.js +3 -2
  178. package/dist/env.js +1 -2
  179. package/dist/feishu.d.ts +1 -0
  180. package/dist/feishu.js +1 -0
  181. package/dist/lark.d.ts +1 -0
  182. package/dist/lark.js +1 -0
  183. package/dist/loader.d.ts +51 -7
  184. package/dist/loader.js +84 -18
  185. package/dist/log.d.ts +9 -17
  186. package/dist/log.js +25 -30
  187. package/dist/paths.d.ts +26 -3
  188. package/dist/paths.js +43 -5
  189. package/dist/scaffold/add-channel.d.ts +6 -1
  190. package/dist/scaffold/add-channel.js +50 -67
  191. package/dist/scaffold/init.js +7 -2
  192. package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
  193. package/dist/schedule/discover.js +3 -15
  194. package/dist/schedule/wake-alarm.d.ts +14 -19
  195. package/dist/schedule/wake-alarm.js +89 -48
  196. package/dist/schedule/wakeups.d.ts +1 -1
  197. package/dist/schedule/wakeups.js +10 -7
  198. package/dist/service.d.ts +24 -24
  199. package/dist/service.js +33 -77
  200. package/dist/session-remote.d.ts +12 -7
  201. package/dist/session-remote.js +185 -118
  202. package/dist/session.d.ts +227 -93
  203. package/dist/session.js +61 -23
  204. package/dist/slack.d.ts +2 -0
  205. package/dist/slack.js +1 -0
  206. package/dist/telegram.d.ts +1 -1
  207. package/dist/telegram.js +1 -1
  208. package/dist/tunnel.d.ts +34 -9
  209. package/dist/tunnel.js +83 -50
  210. package/package.json +5 -4
  211. package/dist/channels/slack/bot-auth.d.ts +0 -15
  212. package/dist/channels/slack/bot-auth.js +0 -135
@@ -21,10 +21,11 @@
21
21
  *
22
22
  * Secrets go in one-per-`variable set --stdin` (value on stdin, never argv/process listing — Railway has
23
23
  * no bulk stdin import like Fly's `secrets import`). Auth needs an ACCOUNT credential (login or
24
- * `RAILWAY_API_KEY`), not a project token: `init` creates a project that a project token can't predate.
24
+ * `RAILWAY_API_TOKEN`), not the project-scoped `RAILWAY_TOKEN`: `init` creates a project that a project
25
+ * token can't predate.
25
26
  */
26
- import type { RegistrationOutcome } from "../../channels/registration.ts";
27
- import type { ChannelKind } from "../../scaffold/add-channel.ts";
27
+ import { type Registrars } from "../channel-ingress.ts";
28
+ import type { DeclaredChannel } from "../../channels/discover.ts";
28
29
  import type { CliRunner } from "../runner.ts";
29
30
  export interface RailwayRunPlan {
30
31
  /** Names both the project (`railway init --name`) and the service (`railway add --service`). Railway
@@ -38,8 +39,8 @@ export interface RailwayRunPlan {
38
39
  secrets: Record<string, string>;
39
40
  /** Required secret names with NO local value — the run gates on these before any side effect. */
40
41
  missingSecrets: string[];
41
- channels: ChannelKind[];
42
- longConnectionChannels?: string[];
42
+ /** Every declared channel and its ingress — the driver asks which of them have a webhook. */
43
+ channels: readonly DeclaredChannel[];
43
44
  /** Opt-in (CLI `--into-linked`) to provision INTO the project this directory is already linked to. Off
44
45
  * by default so `--run` only creates on an unlinked dir and never deploys into a pre-existing (possibly
45
46
  * unrelated/production) project; the flag is the operator's explicit "yes, this project". */
@@ -84,9 +85,9 @@ export declare function parseDomainUrl(stdout: string): string | undefined;
84
85
  */
85
86
  export declare function parseHasVolume(stdout: string, mountPath: string): boolean;
86
87
  /**
87
- * Run the deploy through `railway`. `log` reports progress; the injected Telegram/Feishu/Slack
88
- * registrars perform post-deploy webhook steps from the builder machine (Slack's control credential
89
- * never travels to the host). Absent, the manual console
90
- * instruction is printed. Every gate is fail-visible.
88
+ * Run the deploy through `railway`. `log` reports progress; the injected {@link Registrars} perform
89
+ * the post-deploy webhook steps from the builder machine (Slack's control credential never travels to
90
+ * the host). A registrar the caller did not wire becomes the printed manual step. Every gate is
91
+ * fail-visible.
91
92
  */
92
- export declare function deployRailwayRun(plan: RailwayRunPlan, railway: CliRunner, log: (msg: string) => void, registerTelegram: (baseUrl: string) => Promise<RegistrationOutcome>, registerFeishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>, registerSlack?: (baseUrl: string) => Promise<RegistrationOutcome>): Promise<RailwayRunOutcome>;
93
+ export declare function deployRailwayRun(plan: RailwayRunPlan, railway: CliRunner, log: (msg: string) => void, registrars: Registrars): Promise<RailwayRunOutcome>;
@@ -1,4 +1,30 @@
1
- import { registrationGate } from "../registration-gate.js";
1
+ /**
2
+ * `fastagent deploy railway --run` — drive the Railway CLI to completion. The middle of the deploy the
3
+ * plain runbook hands to the operator; `--run` executes it so a coding agent runs ONE command.
4
+ *
5
+ * Railway's model forces differences from the Fly runner (fly/run.ts), all validated against CLI 5.15.0:
6
+ *
7
+ * - **`--run` PROVISIONS a project and only runs on an UNLINKED directory** — so it can never deploy
8
+ * into a project it didn't create. `railway init` isn't check-then-act (it ALWAYS makes a new project)
9
+ * and `railway status` exits 0 even when unlinked (linkedness is read from stdout: JSON when linked,
10
+ * empty when not). We do NOT track ownership: a pre-existing link is refused (Railway has no globally
11
+ * unique name to give free identity like Fly's app name, and synthesizing one — a machine-local marker
12
+ * — was a large, bug-prone premature optimization). The only "yes, this project" signal is the operator's
13
+ * explicit `--into-linked`, which provisions INTO the linked project; a routine redeploy is just
14
+ * `railway up`. The volume is check-then-act, so `--into-linked` (or a create that failed at volume-add) still gets it.
15
+ * - **Every command needs `--service` explicitly** to stay non-interactive (a bare command prompts to
16
+ * pick a service). `railway volume add` is the exception — it has NO `--service` flag and rides the
17
+ * linked service, so the service must be created (and thus linked) first.
18
+ * - **The public URL is minted, not deterministic** — bare `railway domain --json` returns the service
19
+ * domain (minting one if absent), read for the webhook. We avoid the newer `domain list` subcommand:
20
+ * on a CLI without it, `railway domain list` registers a bogus custom domain named "list" (destructive).
21
+ *
22
+ * Secrets go in one-per-`variable set --stdin` (value on stdin, never argv/process listing — Railway has
23
+ * no bulk stdin import like Fly's `secrets import`). Auth needs an ACCOUNT credential (login or
24
+ * `RAILWAY_API_TOKEN`), not the project-scoped `RAILWAY_TOKEN`: `init` creates a project that a project
25
+ * token can't predate.
26
+ */
27
+ import { registerWebhooks } from "../channel-ingress.js";
2
28
  /**
3
29
  * Whether `railway status --json` shows a linked project: non-empty stdout. Unlinked prints its message
4
30
  * to stderr and leaves stdout EMPTY (the exit code is 0 either way, so it can't be the signal). ANY
@@ -62,12 +88,12 @@ export function parseHasVolume(stdout, mountPath) {
62
88
  return jsonStrings(stdout).includes(mountPath);
63
89
  }
64
90
  /**
65
- * Run the deploy through `railway`. `log` reports progress; the injected Telegram/Feishu/Slack
66
- * registrars perform post-deploy webhook steps from the builder machine (Slack's control credential
67
- * never travels to the host). Absent, the manual console
68
- * instruction is printed. Every gate is fail-visible.
91
+ * Run the deploy through `railway`. `log` reports progress; the injected {@link Registrars} perform
92
+ * the post-deploy webhook steps from the builder machine (Slack's control credential never travels to
93
+ * the host). A registrar the caller did not wire becomes the printed manual step. Every gate is
94
+ * fail-visible.
69
95
  */
70
- export async function deployRailwayRun(plan, railway, log, registerTelegram, registerFeishu, registerSlack) {
96
+ export async function deployRailwayRun(plan, railway, log, registrars) {
71
97
  const gate = (g) => ({ ok: false, gate: g });
72
98
  // Every --service below targets plan.name — the name this tool gives BOTH the project and the service
73
99
  // (`init --name` + `add --service`). On a fresh create they match; on `--into-linked` into a hand-made
@@ -75,10 +101,10 @@ export async function deployRailwayRun(plan, railway, log, registerTelegram, reg
75
101
  // and it's ordered before the volume (which has no --service), so the mismatch fails visibly with no
76
102
  // side effect. (Pre-checking the name means walking status's nested multi-service shape — not worth it.)
77
103
  const svc = ["--service", plan.name];
78
- // 1. Auth needs an ACCOUNT credential (browser login or RAILWAY_API_KEY) — a project token can't
79
- // predate the project `init` creates. `whoami` succeeds with either.
104
+ // 1. Auth needs an ACCOUNT credential (browser login or RAILWAY_API_TOKEN) — the project-scoped
105
+ // RAILWAY_TOKEN can't predate the project `init` creates. `whoami` succeeds with either.
80
106
  if ((await railway(["whoami"], { capture: true })).code !== 0) {
81
- return gate("not logged in to Railway — run `railway login`, or set RAILWAY_API_KEY (an account token), then re-run");
107
+ return gate("not logged in to Railway — run `railway login`, or set RAILWAY_API_TOKEN (an account token), then re-run");
82
108
  }
83
109
  // 2. Gate missing required secret VALUES before any side effect (no half-created infra).
84
110
  if (plan.missingSecrets.length > 0) {
@@ -173,40 +199,15 @@ export async function deployRailwayRun(plan, railway, log, registerTelegram, reg
173
199
  if (!url) {
174
200
  return gate("couldn't read a domain from `railway domain` — run `railway domain` manually, then set any webhook");
175
201
  }
176
- // 7. Post-deploy webhook — gate policy is the shared registration-gate kernel (registrars report
177
- // facts, it owns the policy); all channels are attempted first.
178
- const reg = registrationGate(log, "re-run with --into-linked to retry registration");
179
- if (plan.channels.includes("telegram")) {
180
- log("registering telegram webhook…");
181
- reg.track("telegram", await registerTelegram(url));
182
- }
183
- if (plan.channels.includes("github")) {
184
- log(`github: set the webhook in the repo (Settings → Webhooks) → ${url}/webhook`);
185
- reg.track("github", "manual"); // always a human step — re-surface it after the registrar output
186
- }
187
- if (plan.channels.includes("slack")) {
188
- if (registerSlack) {
189
- log("registering slack event URL…");
190
- reg.track("slack", await registerSlack(url));
191
- }
192
- else {
193
- log(`slack: set Event Subscriptions → Request URL → ${url}/slack`);
194
- reg.track("slack", "manual");
195
- }
196
- }
197
- for (const kind of ["feishu", "lark"]) {
198
- if (!plan.channels.includes(kind) || plan.longConnectionChannels?.includes(kind))
199
- continue;
200
- if (registerFeishu) {
201
- log(`registering ${kind} event URL…`);
202
- reg.track(kind, await registerFeishu(url, kind));
203
- }
204
- else {
205
- log(`${kind}: set the event Request URL in the developer console (Events & Callbacks) → ${url}/${kind} (the service must be running when you save)`);
206
- reg.track(kind, "manual"); // no registrar wired — the console step above is the operator's
207
- }
208
- }
209
- const registrationGateMsg = reg.gate();
202
+ // 7. Post-deploy webhook — which channels, in what words, and the gate policy are all the shared
203
+ // kernel's; Railway contributes the minted URL and how to retry.
204
+ const registrationGateMsg = await registerWebhooks({
205
+ baseUrl: url,
206
+ channels: plan.channels,
207
+ registrars,
208
+ log,
209
+ retryHint: "re-run with --into-linked to retry registration",
210
+ });
210
211
  if (registrationGateMsg)
211
212
  return gate(registrationGateMsg);
212
213
  return { ok: true, url };
@@ -4,6 +4,7 @@
4
4
  * binary and the command sequence differ. Tests inject a fake recorder; production spawns the real CLI.
5
5
  */
6
6
  import { spawn } from "node:child_process";
7
+ import { log } from "../log.js";
7
8
  /**
8
9
  * Production {@link CliRunner}: spawn `bin` in `cwd` (the workspace, so a build/upload context is the
9
10
  * agent). stderr is always inherited to the terminal; stdout is inherited unless `capture`. A spawn
@@ -22,11 +23,30 @@ export function spawnRunner(bin, cwd) {
22
23
  });
23
24
  let out = "";
24
25
  let err = "";
26
+ let stdinError;
25
27
  child.stdout?.on("data", (d) => (out += String(d)));
26
28
  child.stderr?.on("data", (d) => (err += String(d)));
27
- if (opts?.input)
29
+ if (opts?.input) {
30
+ // A host CLI that rejects before reading (bad auth, a refused command) closes stdin under us:
31
+ // the write then emits `error` on a stream with no listener, which Node turns into an uncaught
32
+ // exception — crashing the deploy and losing the gate message the exit code was about to carry.
33
+ // HELD, not dropped: the exit code says it only when the CLI exits non-zero (see below).
34
+ child.stdin?.on("error", (error) => (stdinError ??= error));
28
35
  child.stdin?.end(opts.input);
29
- child.on("close", (code) => res({ code: code ?? 1, stdout: out, stderr: opts?.captureStderr ? err : undefined }));
36
+ }
37
+ child.on("close", (code) => {
38
+ // A CLI that exits 0 having refused part of its stdin took TRUNCATED input — an auth seed runs
39
+ // well past the 64KB pipe buffer, so "success" here would deploy a half-written secret and say
40
+ // nothing. The exit code is what callers read, so the failure has to reach them as one.
41
+ const truncated = stdinError !== undefined && (code ?? 1) === 0;
42
+ if (truncated) {
43
+ // `error`, not `warn`: this line is the ONLY evidence of the failure — the caller's gate says
44
+ // "see the output above", and above it the host CLI printed success. A log level that mutes
45
+ // it (CI often sets `error`) leaves a stopped deploy with no diagnosable cause.
46
+ log.error(`[fastagent] ${bin} exited 0 without reading all of its input — ${stdinError?.message}`);
47
+ }
48
+ res({ code: truncated ? 1 : (code ?? 1), stdout: out, stderr: opts?.captureStderr ? err : undefined });
49
+ });
30
50
  child.on("error", () => res({ code: 127, stdout: "", stderr: opts?.captureStderr ? "" : undefined })); // ENOENT
31
51
  });
32
52
  }
@@ -1,9 +1,4 @@
1
- /**
2
- * The secret set a deployed agent needs, computed from the definition — host-neutral. Required values
3
- * gate every target; optional channel values travel only when configured. Only the SET command differs
4
- * (`fly secrets import` vs `railway variables set`). The runbooks list both classes; `--run` reads local values.
5
- */
6
- import { type ChannelKind } from "../scaffold/add-channel.ts";
1
+ import type { DeclaredChannel } from "../channels/discover.ts";
7
2
  /**
8
3
  * Is this local auth source an env-var API key (→ becomes a deploy secret) vs OAuth / stored / none?
9
4
  * Positive match on the UPPER_SNAKE env-var naming shape, NOT a negative exclude of today's sentinel
@@ -17,7 +12,7 @@ export declare function isEnvKey(source: string | undefined): source is string;
17
12
  * deployment prerequisites. An OAuth/stored login has no env key here — it carries as
18
13
  * `FASTAGENT_AUTH_SEED` on the `--run` path (see each host's run module), not as a named runbook secret.
19
14
  */
20
- export declare function deploymentSecrets(modelAuth: string | undefined, channels: ChannelKind[], extraSecrets?: string[], longConnectionChannels?: string[]): {
15
+ export declare function deploymentSecrets(modelAuth: string | undefined, channels: readonly DeclaredChannel[], extraSecrets?: string[]): {
21
16
  name: string;
22
17
  hint: string;
23
18
  required: boolean;
@@ -42,8 +37,7 @@ export declare function assembleSecrets(input: {
42
37
  * no value to carry and no gate to raise — see {@link modelCredentialCarry}. */
43
38
  modelKeyInDefinition?: boolean;
44
39
  authFile: Buffer | undefined;
45
- channels: ChannelKind[];
46
- longConnectionChannels?: string[];
40
+ channels: readonly DeclaredChannel[];
47
41
  /** Extra secret env-var names from `fastagent.config` deploy.secrets — carried like channel secrets. */
48
42
  extraSecrets?: string[];
49
43
  env: NodeJS.ProcessEnv;
@@ -52,3 +46,18 @@ export declare function assembleSecrets(input: {
52
46
  missingSecrets: string[];
53
47
  needsModelCredential: boolean;
54
48
  };
49
+ /**
50
+ * The bytes to seed to the auth file, or undefined to leave it alone — the pure core of `start`'s
51
+ * FASTAGENT_AUTH_SEED materialization (the read side of {@link assembleSecrets}'s carry). ABSENT-ONLY
52
+ * by design: a present file (a refreshed volume copy) is never overwritten by the stale seed, so a box
53
+ * that ran its own OAuth refresh is not rolled back.
54
+ */
55
+ export declare function authSeedBytes(seed: string | undefined, fileExists: boolean): Buffer | undefined;
56
+ /**
57
+ * Collect the (possibly CHUNKED) auth seed from the environment: `FASTAGENT_AUTH_SEED` plus numbered
58
+ * continuations (`_2`, `_3`, …) concatenated in order. Hosts whose env values carry a small max
59
+ * length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
60
+ * them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
61
+ * Collection stops at the first absent/empty continuation — the writer fills them contiguously.
62
+ */
63
+ export declare function collectAuthSeed(env: NodeJS.ProcessEnv): string | undefined;
@@ -2,8 +2,20 @@
2
2
  * The secret set a deployed agent needs, computed from the definition — host-neutral. Required values
3
3
  * gate every target; optional channel values travel only when configured. Only the SET command differs
4
4
  * (`fly secrets import` vs `railway variables set`). The runbooks list both classes; `--run` reads local values.
5
+ *
6
+ * Both DIRECTIONS of the credential carry live here: the deploy-time assembly below, and the boot-time
7
+ * seed read at the bottom. They were split across a host driver (`fly/run.ts`), which left `start`
8
+ * — a serving path that deploys nothing, on Fly or anywhere — importing from it to boot a container.
5
9
  */
6
- import { channelSetup } from "../scaffold/add-channel.js";
10
+ import { CONTROL_TOKEN_ENV } from "../channels/control.js";
11
+ import { CHANNEL_KINDS, channelSetup } from "../scaffold/add-channel.js";
12
+ /** The declared channels this tool has setup metadata for. A custom channel carries its own secrets;
13
+ * nothing here can name them, and guessing would print a runbook line no one can act on. */
14
+ function firstPartyChannels(channels) {
15
+ return channels.flatMap((channel) => CHANNEL_KINDS.includes(channel.name)
16
+ ? [{ kind: channel.name, ingress: channel.ingress }]
17
+ : []);
18
+ }
7
19
  /**
8
20
  * Is this local auth source an env-var API key (→ becomes a deploy secret) vs OAuth / stored / none?
9
21
  * Positive match on the UPPER_SNAKE env-var naming shape, NOT a negative exclude of today's sentinel
@@ -19,20 +31,30 @@ export function isEnvKey(source) {
19
31
  * deployment prerequisites. An OAuth/stored login has no env key here — it carries as
20
32
  * `FASTAGENT_AUTH_SEED` on the `--run` path (see each host's run module), not as a named runbook secret.
21
33
  */
22
- export function deploymentSecrets(modelAuth, channels, extraSecrets = [], longConnectionChannels = []) {
34
+ export function deploymentSecrets(modelAuth, channels, extraSecrets = []) {
23
35
  const secrets = [];
24
36
  if (isEnvKey(modelAuth))
25
37
  secrets.push({ name: modelAuth, hint: "your model provider key", required: true });
26
- for (const kind of channels) {
27
- const setupMode = longConnectionChannels.includes(kind) ? "websocket" : "webhook";
28
- for (const e of channelSetup(kind, setupMode).env) {
38
+ for (const { kind, ingress } of firstPartyChannels(channels)) {
39
+ for (const e of channelSetup(kind, ingress === "long-connection" ? "websocket" : "webhook").env) {
29
40
  secrets.push({ name: e.name, hint: e.hint, required: e.required });
30
41
  }
31
42
  }
32
43
  // Dedup: a name already covered by the model key / a channel secret must not appear twice in the runbook.
33
44
  for (const name of extraSecrets) {
34
45
  if (!secrets.some((s) => s.name === name)) {
35
- secrets.push({ name, hint: "declared in fastagent.config deploy.secrets", required: true });
46
+ const control = name === CONTROL_TOKEN_ENV;
47
+ secrets.push({
48
+ name,
49
+ hint: control
50
+ ? "the /control/* bearer token — mint one (uuidgen) and give the same value to callers"
51
+ : "declared in fastagent.config deploy.secrets",
52
+ // OPTIONAL, unlike every other extra: unset, the box mints a per-boot token and still serves,
53
+ // and every host with a shell can read it back out of control.json. Gating would stop deploys
54
+ // that work today to enforce a convenience — the pre-flight warning is where that argument
55
+ // belongs.
56
+ required: !control,
57
+ });
36
58
  }
37
59
  }
38
60
  return secrets;
@@ -74,9 +96,8 @@ export function assembleSecrets(input) {
74
96
  else {
75
97
  needsModelCredential = true; // no env key, no auth.json — `fastagent login` remediation
76
98
  }
77
- for (const kind of input.channels) {
78
- const setupMode = input.longConnectionChannels?.includes(kind) ? "websocket" : "webhook";
79
- for (const e of channelSetup(kind, setupMode).env) {
99
+ for (const { kind, ingress } of firstPartyChannels(input.channels)) {
100
+ for (const e of channelSetup(kind, ingress === "long-connection" ? "websocket" : "webhook").env) {
80
101
  const v = input.env[e.name];
81
102
  if (v)
82
103
  secrets[e.name] = v; // optional channel values travel when configured
@@ -85,31 +106,45 @@ export function assembleSecrets(input) {
85
106
  }
86
107
  }
87
108
  }
88
- // Slack bot-token rotation is an all-or-nothing credential bundle. Its fields remain optional so a
89
- // manually configured long-lived token works, but a partial bundle must gate before the container
90
- // reaches slackChannel construction.
91
- if (input.channels.includes("slack")) {
92
- const rotation = [
93
- "SLACK_BOT_REFRESH_TOKEN",
94
- "SLACK_BOT_TOKEN_EXPIRES_AT",
95
- "SLACK_CLIENT_ID",
96
- "SLACK_CLIENT_SECRET",
97
- ];
98
- if (rotation.some((name) => !!input.env[name])) {
99
- for (const name of rotation) {
100
- if (!input.env[name] && !missingSecrets.includes(name))
101
- missingSecrets.push(name);
102
- }
103
- }
104
- }
105
109
  for (const name of input.extraSecrets ?? []) {
106
110
  if (name in secrets || missingSecrets.includes(name))
107
111
  continue; // already covered by model/channel — no dup
108
112
  const v = input.env[name];
109
113
  if (v)
110
114
  secrets[name] = v;
111
- else
112
- missingSecrets.push(name); // declared in config but no local value same .env remediation
115
+ // The control token is CARRIED, never gated — see {@link deploymentSecrets}: unset, the box mints
116
+ // one and serves; every other extra is declared as needed, so its absence is a stop.
117
+ else if (name !== CONTROL_TOKEN_ENV)
118
+ missingSecrets.push(name);
113
119
  }
114
120
  return { secrets, missingSecrets, needsModelCredential };
115
121
  }
122
+ /**
123
+ * The bytes to seed to the auth file, or undefined to leave it alone — the pure core of `start`'s
124
+ * FASTAGENT_AUTH_SEED materialization (the read side of {@link assembleSecrets}'s carry). ABSENT-ONLY
125
+ * by design: a present file (a refreshed volume copy) is never overwritten by the stale seed, so a box
126
+ * that ran its own OAuth refresh is not rolled back.
127
+ */
128
+ export function authSeedBytes(seed, fileExists) {
129
+ return !seed || fileExists ? undefined : Buffer.from(seed, "base64");
130
+ }
131
+ /**
132
+ * Collect the (possibly CHUNKED) auth seed from the environment: `FASTAGENT_AUTH_SEED` plus numbered
133
+ * continuations (`_2`, `_3`, …) concatenated in order. Hosts whose env values carry a small max
134
+ * length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
135
+ * them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
136
+ * Collection stops at the first absent/empty continuation — the writer fills them contiguously.
137
+ */
138
+ export function collectAuthSeed(env) {
139
+ const first = env.FASTAGENT_AUTH_SEED;
140
+ if (!first)
141
+ return undefined;
142
+ let seed = first;
143
+ for (let i = 2;; i++) {
144
+ const part = env[`FASTAGENT_AUTH_SEED_${i}`];
145
+ if (!part)
146
+ break;
147
+ seed += part;
148
+ }
149
+ return seed;
150
+ }
@@ -13,12 +13,12 @@
13
13
  import { spawn } from "node:child_process";
14
14
  import { relative, sep } from "node:path";
15
15
  import { watch as watchTree } from "chokidar";
16
- import { AGENT_CONFIG_NAMES, AGENT_MODELS_FILE, resolveStateRoot } from "./paths.js";
17
- import { isUnderDir } from "./paths.js";
16
+ import { AGENT_CONFIG_NAMES, AGENT_MODELS_FILE, resolveStateRoot, isUnderDir, } from "./paths.js";
18
17
  import { dotEnvPath } from "./env.js";
19
18
  import { log } from "./log.js";
20
19
  import { installProxyFetch } from "./proxy.js";
21
20
  import { openExternalUrl } from "./open-url.js";
21
+ import { declaredChannels } from "./channels/discover.js";
22
22
  import { announceWebhooks, startCloudflareTunnel } from "./tunnel.js";
23
23
  /** What the dev watcher restarts on (agent-dir-relative): the process-bound code inputs only. */
24
24
  /**
@@ -108,9 +108,8 @@ export async function runDevSupervisor(placement, options = {}) {
108
108
  void startCloudflareTunnel(m.port).then((t) => {
109
109
  if (t) {
110
110
  tunnel = t;
111
- void announceWebhooks(placement.agentDir, t.url, {
111
+ void announceWebhooks(placement.agentDir, t.url, declaredChannels(m.routeChannels ?? []), {
112
112
  openUrl: openExternalUrl,
113
- routeChannels: m.routeChannels,
114
113
  stateRoot: resolveStateRoot(placement.agentDir),
115
114
  });
116
115
  }
@@ -1,11 +1,13 @@
1
- import type { ExecutionEnv, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
2
- import type { Model } from "@earendil-works/pi-ai";
3
- import { type AgentSessionServices, type CreateAgentSessionServicesOptions, type ModelRuntime } from "@earendil-works/pi-coding-agent";
1
+ import type { Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+ import { type AgentSessionServices, type CreateAgentSessionServicesOptions, type ModelRuntime, type SessionManager, createAgentSessionFromServices } from "@earendil-works/pi-coding-agent";
4
3
  import type { PiAgentSessionFactory } from "./invoke-session.ts";
5
4
  import type { PiSessionRecordStore } from "./session-store.ts";
6
5
  import { type MountedTool } from "./tool.ts";
7
- /** pi's Model with the API-shape generic erased; fastagent only passes models through. */
8
- type AnyModel = Model<any>;
6
+ import { type AnyModel } from "./models.ts";
7
+ interface PiSessionDefinition {
8
+ systemPrompt?: string;
9
+ skills: Skill[];
10
+ }
9
11
  export interface PiAgentSessionFactoryOptions {
10
12
  /** Where conversations live. Continuity = same store + same session id. */
11
13
  sessions: PiSessionRecordStore;
@@ -23,16 +25,8 @@ export interface PiAgentSessionFactoryOptions {
23
25
  }>;
24
26
  thinkingLevel?: ThinkingLevel;
25
27
  tools?: MountedTool[];
26
- /** Final assembled prompt, or a factory re-evaluated per turn. */
27
- systemPrompt?: string | (() => string);
28
- skills?: Skill[];
29
- /** Per-turn source for the prompt+skills PAIR; supersedes the two above. This is what keeps
30
- * "the directory is the agent, LIVE" true on a shared `services`: the ResourceLoader is built
31
- * once, but what it serves is re-read here. */
32
- live?: () => Promise<{
33
- systemPrompt?: string;
34
- skills?: Skill[];
35
- }>;
28
+ /** Read once per binding; prompt and skills come from the same definition read. */
29
+ readDefinition: () => PiSessionDefinition | Promise<PiSessionDefinition>;
36
30
  /** The agent's working directory — what fastagent-defined tools see as `cwd`. */
37
31
  cwd: string;
38
32
  /**
@@ -66,9 +60,35 @@ export interface PiAgentSessionFactoryOptions {
66
60
  extensionPaths?: string[];
67
61
  /** Built-ins omitted by an explicit lower-level tool list. */
68
62
  excludedToolNames?: readonly string[];
69
- /** Filesystem/process environment: definition loading, and the turn context for tools that read one. */
70
- env: ExecutionEnv;
71
63
  }
64
+ export interface BindPiSessionOptions {
65
+ services: AgentSessionServices;
66
+ sessionManager: SessionManager;
67
+ /** Chat only: pi's runtime hands the resumed/new session its start event. */
68
+ sessionStartEvent?: Parameters<typeof createAgentSessionFromServices>[0]["sessionStartEvent"];
69
+ model: AnyModel;
70
+ thinkingLevel: ThinkingLevel | undefined;
71
+ tools: MountedTool[];
72
+ /** The agent's working directory — what fastagent-defined tools see as `cwd`. */
73
+ cwd: string;
74
+ /** Built-ins omitted by an explicit lower-level tool list. */
75
+ excludedToolNames?: readonly string[];
76
+ /** The CALLER's session id — what a tool asking which conversation it is in hears. Defaults to
77
+ * pi's own id, which is right where the caller has none (chat). */
78
+ sessionId?: string;
79
+ /** Record each discovered activation on the session, so the next bind restores it. Off for chat:
80
+ * pi's own session has nowhere to put one, so a resumed chat re-discovers. */
81
+ recordActivations: boolean;
82
+ }
83
+ /**
84
+ * Bind ONE pi session to a record: the definition's tools as pi definitions over one turn context,
85
+ * pi's own tool copies kept off, and deferral applied. The per-invoke factory and the resident chat
86
+ * runtime both bind here — they differ in what they hand in (a shared vs a per-session `services`,
87
+ * record-resolved vs configured settings) and in whether a discovered activation has a record to
88
+ * land in, and in nothing else. Two copies of this drifted once (the tool adapter and the deferral
89
+ * narrowing each existed twice, identical but for those parameters).
90
+ */
91
+ export declare function bindPiSession(options: BindPiSessionOptions): ReturnType<typeof createAgentSessionFromServices>;
72
92
  /**
73
93
  * Announce extensions pi failed to load. pi collects them into `LoadExtensionsResult.errors` and
74
94
  * carries on with the rest — sound for a TUI that shows them, silent for a server that never looks.