@fastagent-sh/fastagent 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +14 -4
  2. package/dist/bind.d.ts +34 -0
  3. package/dist/bind.js +74 -0
  4. package/dist/channels/agentcore-limits.d.ts +7 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-state.d.ts +83 -0
  7. package/dist/channels/agentcore-state.js +266 -0
  8. package/dist/channels/agentcore.d.ts +98 -0
  9. package/dist/channels/agentcore.js +260 -0
  10. package/dist/channels/busy.d.ts +23 -0
  11. package/dist/channels/busy.js +53 -0
  12. package/dist/channels/context-buffer.d.ts +5 -0
  13. package/dist/channels/context-buffer.js +5 -0
  14. package/dist/channels/feishu/context-buffer.d.ts +13 -8
  15. package/dist/channels/feishu/context-buffer.js +60 -6
  16. package/dist/channels/feishu/feishu-api.js +4 -1
  17. package/dist/channels/feishu/feishu.d.ts +0 -12
  18. package/dist/channels/feishu/feishu.js +143 -54
  19. package/dist/channels/feishu/invoke-turn.js +35 -18
  20. package/dist/channels/feishu/model.d.ts +0 -1
  21. package/dist/channels/feishu/normalize.js +0 -1
  22. package/dist/channels/feishu/parse.d.ts +21 -7
  23. package/dist/channels/feishu/parse.js +24 -7
  24. package/dist/channels/feishu/preview.js +3 -2
  25. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  26. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  27. package/dist/channels/feishu/setup-mode.d.ts +30 -1
  28. package/dist/channels/feishu/setup-mode.js +27 -1
  29. package/dist/channels/github/github.js +8 -1
  30. package/dist/channels/http.js +1 -1
  31. package/dist/channels/lark/scaffold/channel.ts +9 -8
  32. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  33. package/dist/channels/preview-kit.d.ts +7 -1
  34. package/dist/channels/preview-kit.js +3 -2
  35. package/dist/channels/slack/parse.d.ts +16 -1
  36. package/dist/channels/slack/parse.js +46 -3
  37. package/dist/channels/slack/preview.d.ts +1 -2
  38. package/dist/channels/slack/preview.js +68 -24
  39. package/dist/channels/slack/scaffold/channel.ts +5 -5
  40. package/dist/channels/slack/slack-api.d.ts +3 -23
  41. package/dist/channels/slack/slack-api.js +6 -22
  42. package/dist/channels/slack/slack.d.ts +13 -20
  43. package/dist/channels/slack/slack.js +95 -50
  44. package/dist/channels/state.d.ts +11 -4
  45. package/dist/channels/state.js +19 -12
  46. package/dist/channels/tasks.d.ts +0 -6
  47. package/dist/channels/tasks.js +16 -1
  48. package/dist/channels/telegram/parse.d.ts +0 -7
  49. package/dist/channels/telegram/parse.js +4 -2
  50. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  51. package/dist/channels/telegram/telegram.js +1 -1
  52. package/dist/channels/text.d.ts +14 -0
  53. package/dist/channels/text.js +14 -0
  54. package/dist/channels/thread-participants.d.ts +21 -0
  55. package/dist/channels/thread-participants.js +132 -0
  56. package/dist/channels/turn-queue.js +7 -0
  57. package/dist/cli/add-feishu.d.ts +7 -4
  58. package/dist/cli/add-feishu.js +57 -37
  59. package/dist/cli/add-slack.d.ts +2 -1
  60. package/dist/cli/add-slack.js +6 -11
  61. package/dist/cli/commands/add.js +50 -51
  62. package/dist/cli/commands/attach.js +8 -4
  63. package/dist/cli/commands/chat.js +8 -8
  64. package/dist/cli/commands/deploy.d.ts +14 -1
  65. package/dist/cli/commands/deploy.js +330 -78
  66. package/dist/cli/commands/dev.d.ts +1 -0
  67. package/dist/cli/commands/dev.js +38 -25
  68. package/dist/cli/commands/fire.js +15 -16
  69. package/dist/cli/commands/info.js +36 -29
  70. package/dist/cli/commands/init.d.ts +1 -1
  71. package/dist/cli/commands/init.js +65 -53
  72. package/dist/cli/commands/invoke.js +9 -6
  73. package/dist/cli/commands/login.js +35 -21
  74. package/dist/cli/commands/schedule.js +6 -8
  75. package/dist/cli/commands/start.d.ts +1 -0
  76. package/dist/cli/commands/start.js +101 -37
  77. package/dist/cli/commands/tool.js +30 -18
  78. package/dist/cli/fail.d.ts +17 -0
  79. package/dist/cli/fail.js +24 -0
  80. package/dist/cli/program.js +66 -36
  81. package/dist/cli/serve.d.ts +52 -8
  82. package/dist/cli/serve.js +133 -32
  83. package/dist/cli/shared.d.ts +21 -2
  84. package/dist/cli/shared.js +44 -18
  85. package/dist/deploy/agentcore/plan.d.ts +117 -0
  86. package/dist/deploy/agentcore/plan.js +721 -0
  87. package/dist/deploy/agentcore/run.d.ts +73 -0
  88. package/dist/deploy/agentcore/run.js +412 -0
  89. package/dist/deploy/agentcore/zip.d.ts +17 -0
  90. package/dist/deploy/agentcore/zip.js +68 -0
  91. package/dist/deploy/container.d.ts +26 -25
  92. package/dist/deploy/container.js +93 -89
  93. package/dist/deploy/docker/plan.d.ts +1 -1
  94. package/dist/deploy/docker/plan.js +12 -17
  95. package/dist/deploy/fly/plan.d.ts +2 -0
  96. package/dist/deploy/fly/plan.js +27 -19
  97. package/dist/deploy/fly/run.d.ts +12 -1
  98. package/dist/deploy/fly/run.js +36 -2
  99. package/dist/deploy/preflight.d.ts +11 -5
  100. package/dist/deploy/preflight.js +235 -65
  101. package/dist/deploy/railway/plan.d.ts +7 -0
  102. package/dist/deploy/railway/plan.js +41 -16
  103. package/dist/deploy/railway/run.d.ts +8 -1
  104. package/dist/deploy/railway/run.js +7 -2
  105. package/dist/deploy/runner.d.ts +5 -2
  106. package/dist/deploy/runner.js +9 -3
  107. package/dist/dev-supervisor.d.ts +11 -8
  108. package/dist/dev-supervisor.js +53 -51
  109. package/dist/engines/pi/auth.d.ts +8 -7
  110. package/dist/engines/pi/auth.js +12 -10
  111. package/dist/engines/pi/channel.d.ts +1 -1
  112. package/dist/engines/pi/channel.js +5 -5
  113. package/dist/engines/pi/chat.js +2 -2
  114. package/dist/engines/pi/config.d.ts +12 -48
  115. package/dist/engines/pi/config.js +29 -110
  116. package/dist/engines/pi/create.d.ts +32 -24
  117. package/dist/engines/pi/create.js +47 -23
  118. package/dist/engines/pi/definition.d.ts +7 -26
  119. package/dist/engines/pi/definition.js +8 -54
  120. package/dist/engines/pi/harness.d.ts +19 -5
  121. package/dist/engines/pi/harness.js +3 -5
  122. package/dist/engines/pi/login.d.ts +1 -1
  123. package/dist/engines/pi/models.d.ts +3 -3
  124. package/dist/engines/pi/models.js +1 -1
  125. package/dist/engines/pi/{workspace.d.ts → open.d.ts} +31 -25
  126. package/dist/engines/pi/{workspace.js → open.js} +27 -29
  127. package/dist/engines/pi/read-image.d.ts +4 -0
  128. package/dist/engines/pi/read-image.js +62 -0
  129. package/dist/engines/pi/search-tools.d.ts +6 -4
  130. package/dist/engines/pi/search-tools.js +3 -1
  131. package/dist/engines/pi/session-builder.d.ts +2 -2
  132. package/dist/engines/pi/session-builder.js +18 -13
  133. package/dist/engines/pi/tool.d.ts +13 -5
  134. package/dist/engines/pi/tool.js +4 -0
  135. package/dist/engines/pi/wake-tool.d.ts +3 -3
  136. package/dist/env.d.ts +16 -4
  137. package/dist/env.js +43 -5
  138. package/dist/host/node.d.ts +4 -2
  139. package/dist/host/node.js +2 -1
  140. package/dist/loader.d.ts +2 -2
  141. package/dist/loader.js +3 -3
  142. package/dist/log.d.ts +1 -1
  143. package/dist/log.js +1 -1
  144. package/dist/paths.d.ts +138 -0
  145. package/dist/paths.js +326 -0
  146. package/dist/pi.d.ts +2 -2
  147. package/dist/pi.js +2 -2
  148. package/dist/runtime.d.ts +7 -5
  149. package/dist/runtime.js +2 -2
  150. package/dist/scaffold/add-channel.d.ts +7 -3
  151. package/dist/scaffold/add-channel.js +55 -29
  152. package/dist/scaffold/init.d.ts +32 -41
  153. package/dist/scaffold/init.js +161 -185
  154. package/dist/scaffold/templates/env.example +15 -6
  155. package/dist/scaffold/templates/fastagent.config.mjs +1 -1
  156. package/dist/scaffold/templates/gitignore +14 -6
  157. package/dist/scaffold/templates/persona.md +4 -2
  158. package/dist/scaffold/templates/secrets.gitignore +5 -0
  159. package/dist/scaffold/templates.d.ts +1 -7
  160. package/dist/scaffold/templates.js +3 -25
  161. package/dist/scaffold/vendor-skill.d.ts +2 -2
  162. package/dist/scaffold/vendor-skill.js +13 -13
  163. package/dist/schedule/discover.js +4 -4
  164. package/dist/schedule/scheduler.d.ts +40 -1
  165. package/dist/schedule/scheduler.js +89 -56
  166. package/dist/schedule/state.js +1 -1
  167. package/dist/schedule/wake-alarm.d.ts +47 -0
  168. package/dist/schedule/wake-alarm.js +136 -0
  169. package/dist/schedule/wakeups.d.ts +1 -0
  170. package/dist/schedule/wakeups.js +18 -0
  171. package/dist/tunnel.d.ts +3 -3
  172. package/dist/tunnel.js +7 -7
  173. package/package.json +7 -4
  174. package/dist/channels/feishu/owned-threads.d.ts +0 -7
  175. package/dist/channels/feishu/owned-threads.js +0 -47
  176. package/dist/channels/slack/owned-threads.d.ts +0 -6
  177. package/dist/channels/slack/owned-threads.js +0 -43
  178. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  179. package/dist/scaffold/templates/gitignore.kit +0 -2
  180. package/dist/workspace.d.ts +0 -9
  181. package/dist/workspace.js +0 -45
@@ -1,6 +1,7 @@
1
1
  import type { Agent } from "../agent.ts";
2
2
  import { type LoadedLongConnectionChannel } from "../engines/pi/channel.ts";
3
3
  import { type Routes } from "../host/node.ts";
4
+ import type { LoadedSchedule } from "../schedule/schedule.ts";
4
5
  import type { SessionControl } from "../session.ts";
5
6
  export interface ServingSurface {
6
7
  routes: Routes;
@@ -15,9 +16,11 @@ export interface ServingSurface {
15
16
  * The surface this deployment serves: default `GET /health` plus discovered channels, or the default
16
17
  * POST `/invoke` only when neither a route nor a long-connection channel was declared.
17
18
  */
18
- export declare function routesFor(workspaceDir: string, agent: Agent, stateRoot: string, control?: SessionControl): Promise<ServingSurface>;
19
+ export declare function routesFor(agentDir: string, agent: Agent, stateRoot: string, control?: SessionControl, options?: {
20
+ builtinInvoke?: boolean;
21
+ }): Promise<ServingSurface>;
19
22
  /**
20
- * Mount the session control plane (`/control/*`) when the workspace enabled it
23
+ * Mount the session control plane (`/control/*`) when the agent enabled it
21
24
  * (`config.sessionControl`): merge the bearer-authenticated routes and return an announcer that
22
25
  * writes `<stateRoot>/control.json` — `{ url, token }`, 0600 — once the port is known. The file is
23
26
  * the LOCAL discovery channel (`fastagent attach`, a local desktop app); filesystem permissions are
@@ -29,20 +32,61 @@ export declare function routesFor(workspaceDir: string, agent: Agent, stateRoot:
29
32
  export declare function mountSessionControl(routes: Routes, control: SessionControl | undefined, stateRoot: string, options?: {
30
33
  tunnel?: boolean;
31
34
  agent?: Agent;
35
+ host?: string;
32
36
  }): {
33
37
  routes: Routes;
34
38
  announce: (boundPort: number) => void;
35
39
  };
40
+ /**
41
+ * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) over the serving routes —
42
+ * the deployed container's ONLY reachable surface (channels/agentcore.ts). Wired by `start` when
43
+ * `FASTAGENT_AGENTCORE=1` (set by the generated deploy artifacts, never by hand). A channel colliding
44
+ * on either path fails startup, same disposition as the control-plane mount: the adapter's paths are
45
+ * the platform's contract, so a channel shadowing them would silently unserve the whole deployment.
46
+ */
47
+ export declare function mountAgentcore(routes: Routes, options: {
48
+ agent: Agent;
49
+ stateRoot: string;
50
+ schedules: LoadedSchedule[];
51
+ onStateReady?: () => void;
52
+ }): Routes;
53
+ /**
54
+ * Refuse `--tunnel` with a bind that cloudflared cannot reach: it dials the NAME `localhost:<port>`
55
+ * (the dev supervisor's tunnel too), so anything outside `127.0.0.1`/`::1`/wildcard — including a
56
+ * `127.0.0.2` bind, loopback though it is — would leave the tunnel up and 502ing every request.
57
+ * Checked BEFORE the bind (the flag alone pre-spawn in `dev`, again once config is loaded, since
58
+ * `http.host` can carry the address), so the failure is clean rather than a live-but-broken public URL.
59
+ * `source` decides the exit code, per fail.ts: a flag COMBINATION is a usage error (2), a value that
60
+ * came from config is broken runtime configuration (1).
61
+ */
62
+ export declare function assertTunnelBindable(host: string | undefined, tunnel: boolean, source: "flag" | "config"): void;
63
+ /**
64
+ * The startup lines that name WHERE the serve is: the bind report, and the curl the reader copies.
65
+ * ONE function because they are one message — they were two, and `--bind` updated the first while the
66
+ * second went on dialing `localhost`, which is precisely what a non-wildcard bind stops answering. Now
67
+ * neither can be changed without the other in view, and the address has a single derivation.
68
+ *
69
+ * A wildcard bind is every interface, and naming one address there would understate it — but the curl
70
+ * still needs one to dial, which is what `clientHost` gives (loopback for a wildcard, itself otherwise).
71
+ */
72
+ export declare function readyAddressLines(host: string | undefined, boundPort: number, builtinInvoke: boolean): string[];
36
73
  /**
37
74
  * Bind HTTP, open long-connection channels, and report ready only when both forms are usable. Each
38
75
  * adapter owns reconnects; a terminal close rejects `closed` and fails the process visibly. Abort is
39
- * the sole clean-shutdown command.
76
+ * the sole clean-shutdown command. `host` unset binds all interfaces.
40
77
  */
41
- export declare function serve(surface: ServingSurface, port: number, onListening?: (boundPort: number) => void): void;
78
+ export declare function serve(surface: ServingSurface, bind: {
79
+ port: number;
80
+ host?: string;
81
+ }, onListening?: (boundPort: number) => void): void;
42
82
  /** Start a Cloudflare tunnel for route channels only. */
43
- export declare function maybeTunnel(workspaceDir: string, routeChannels: string[], boundPort: number, tunnel: boolean, stateRoot?: string): void;
83
+ export declare function maybeTunnel(agentDir: string, routeChannels: string[], boundPort: number, tunnel: boolean, stateRoot?: string): void;
44
84
  /**
45
- * Load and start the workspace's `schedules/` — a time-trigger firing the agent on each cron. Starts iff
46
- * there are static schedules OR `selfSchedule` is on. Best-effort stop on process signals.
85
+ * Load and start the agent's `schedules/` — a time-trigger firing the agent on each cron. Starts iff
86
+ * there are static schedules OR `selfSchedule` is on. Best-effort stop on process signals. Returns the
87
+ * loaded schedules so a serving surface that needs them (the AgentCore adapter's fire binding) shares
88
+ * ONE load instead of re-discovering. `externalClock` (AgentCore) arms no resident cron timers.
47
89
  */
48
- export declare function startSchedules(workspaceDir: string, agent: Agent, stateRoot: string, selfSchedule: boolean): Promise<void>;
90
+ export declare function startSchedules(agentDir: string, agent: Agent, stateRoot: string, selfSchedule: boolean, options?: {
91
+ externalClock?: boolean;
92
+ }): Promise<LoadedSchedule[]>;
package/dist/cli/serve.js CHANGED
@@ -4,24 +4,28 @@
4
4
  */
5
5
  import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
+ import { createStateSync } from "../channels/agentcore-state.js";
8
+ import { agentcoreRoutes, UnknownScheduleError } from "../channels/agentcore.js";
9
+ import { activeWork } from "../channels/busy.js";
7
10
  import { controlRoutes } from "../channels/control.js";
8
11
  import { INVOKE_EXAMPLE_BODY, createInvokeHandler } from "../channels/http.js";
9
12
  import { text } from "../channels/respond.js";
10
13
  import { loadChannels } from "../engines/pi/channel.js";
11
14
  import { reportModuleLoadFailures } from "../engines/pi/report.js";
15
+ import { answersLocalhost, bindLabel, classifyBind, clientHost } from "../bind.js";
12
16
  import { parseRouteKey, router, serveNode } from "../host/node.js";
13
17
  import { log } from "../log.js";
14
18
  import { openExternalUrl } from "../open-url.js";
15
19
  import { loadSchedules } from "../schedule/discover.js";
16
- import { createScheduler } from "../schedule/scheduler.js";
20
+ import { createScheduler, fireScheduleOnce } from "../schedule/scheduler.js";
17
21
  import { announceWebhooks, startCloudflareTunnel } from "../tunnel.js";
18
- import { failStartup } from "./fail.js";
22
+ import { failStartup, failUsage } from "./fail.js";
19
23
  /**
20
24
  * The surface this deployment serves: default `GET /health` plus discovered channels, or the default
21
25
  * POST `/invoke` only when neither a route nor a long-connection channel was declared.
22
26
  */
23
- export async function routesFor(workspaceDir, agent, stateRoot, control) {
24
- const { routes, longConnections, routeChannels, collisions, failures } = await loadChannels(workspaceDir, {
27
+ export async function routesFor(agentDir, agent, stateRoot, control, options = {}) {
28
+ const { routes, longConnections, routeChannels, collisions, failures } = await loadChannels(agentDir, {
25
29
  agent,
26
30
  stateRoot,
27
31
  control,
@@ -34,7 +38,7 @@ export async function routesFor(workspaceDir, agent, stateRoot, control) {
34
38
  throw new Error(`channel setup is invalid (${failures.length} load failure(s), ${collisions.length} route collision(s)) — ` +
35
39
  `fix it, or rename an intentionally disabled file to *.disabled`);
36
40
  }
37
- const builtinInvoke = Object.keys(routes).length === 0 && longConnections.length === 0;
41
+ const builtinInvoke = options.builtinInvoke !== false && Object.keys(routes).length === 0 && longConnections.length === 0;
38
42
  const channels = builtinInvoke ? { "POST /invoke": createInvokeHandler(agent) } : routes;
39
43
  const healthCovered = Object.keys(channels).some((key) => {
40
44
  const entry = parseRouteKey(key);
@@ -53,7 +57,7 @@ export async function routesFor(workspaceDir, agent, stateRoot, control) {
53
57
  };
54
58
  }
55
59
  /**
56
- * Mount the session control plane (`/control/*`) when the workspace enabled it
60
+ * Mount the session control plane (`/control/*`) when the agent enabled it
57
61
  * (`config.sessionControl`): merge the bearer-authenticated routes and return an announcer that
58
62
  * writes `<stateRoot>/control.json` — `{ url, token }`, 0600 — once the port is known. The file is
59
63
  * the LOCAL discovery channel (`fastagent attach`, a local desktop app); filesystem permissions are
@@ -87,15 +91,22 @@ export function mountSessionControl(routes, control, stateRoot, options = {}) {
87
91
  // Atomic (tmp+rename, the state.ts pattern): attach re-reads this file exactly during the
88
92
  // restart window — a torn read would be misdiagnosed as "serve gone".
89
93
  const tmp = `${path}.tmp`;
90
- writeFileSync(tmp, `${JSON.stringify({ url: `http://127.0.0.1:${boundPort}`, token })}\n`, { mode: 0o600 });
94
+ writeFileSync(tmp, `${JSON.stringify({ url: `http://${clientHost(options.host)}:${boundPort}`, token })}\n`, {
95
+ mode: 0o600,
96
+ });
91
97
  chmodSync(tmp, 0o600); // an existing file keeps its old mode on rewrite — pin it
92
98
  renameSync(tmp, path);
93
99
  log.info(`[fastagent] session control on /control/* (token in ${path})`);
94
- // The serve binds ALL interfaces (containers require it), so /control/* is LAN-reachable
95
- // with the bearer token as the only protection — the tunnel and deploy paths warn loudly,
96
- // and the LAN path must not be the silent third way past the local trust story.
97
- log.warn("[fastagent] the port binds all interfaces: /control/* is reachable on your LAN, protected only by " +
98
- "the bearer token — firewall the port or wrap it for real exposure (docs: design §14)");
100
+ // The serve binds ALL interfaces by DEFAULT (containers require it), so /control/* is
101
+ // LAN-reachable with the bearer token as the only protection — the tunnel and deploy paths warn
102
+ // loudly, and the LAN path must not be the silent third way past the local trust story. A
103
+ // loopback bind closes exactly that reach, so it earns silence.
104
+ const bind = classifyBind(options.host);
105
+ if (bind !== "loopback") {
106
+ log.warn(`[fastagent] the port binds ${bind === "wildcard" ? "all interfaces" : `${options.host} (off this machine)`}: ` +
107
+ "/control/* is reachable on your LAN, protected only by the bearer token — bind loopback " +
108
+ "(--bind 127.0.0.1), firewall the port, or wrap it for real exposure (docs: design §14)");
109
+ }
99
110
  if (options.tunnel) {
100
111
  // Local trust = the token + its file permissions; --tunnel takes the whole port PUBLIC
101
112
  // (beyond even the LAN reach the mount already warned about). The operator asked for the tunnel (webhooks), but must not DISCOVER the control
@@ -132,13 +143,95 @@ export function mountSessionControl(routes, control, stateRoot, options = {}) {
132
143
  },
133
144
  };
134
145
  }
146
+ /**
147
+ * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) over the serving routes —
148
+ * the deployed container's ONLY reachable surface (channels/agentcore.ts). Wired by `start` when
149
+ * `FASTAGENT_AGENTCORE=1` (set by the generated deploy artifacts, never by hand). A channel colliding
150
+ * on either path fails startup, same disposition as the control-plane mount: the adapter's paths are
151
+ * the platform's contract, so a channel shadowing them would silently unserve the whole deployment.
152
+ */
153
+ export function mountAgentcore(routes, options) {
154
+ const { agent, stateRoot, schedules, onStateReady } = options;
155
+ const mounted = agentcoreRoutes({
156
+ routes,
157
+ agent,
158
+ stateRoot,
159
+ isBusy: () => activeWork() > 0,
160
+ // Cross-deploy durability: AgentCore wipes the state mount on every runtime version update, so
161
+ // the state root is restored from (and pushed to) an S3 snapshot through presigned URLs the
162
+ // forwarder mints per envelope. Always wired on this path — the platform gives no other way to
163
+ // keep an agent's memory across a deploy.
164
+ stateSync: createStateSync({ stateRoot }),
165
+ // What separates a forwarder envelope from any IAM principal's InvokeAgentRuntime call. Absent =
166
+ // no forwarder in this topology, so only the public `invoke` kind is servable.
167
+ ingressSecret: process.env.FASTAGENT_INGRESS_SECRET,
168
+ onStateReady,
169
+ fire: schedules.length === 0
170
+ ? undefined
171
+ : (name, slot) => {
172
+ const schedule = schedules.find((s) => s.name === name);
173
+ if (!schedule)
174
+ throw new UnknownScheduleError(name);
175
+ return fireScheduleOnce({ agent, stateRoot, schedule, slot });
176
+ },
177
+ });
178
+ const mountedPaths = new Set(Object.keys(mounted).map((key) => parseRouteKey(key).path));
179
+ const collisions = Object.keys(routes).filter((key) => mountedPaths.has(parseRouteKey(key).path));
180
+ if (collisions.length > 0) {
181
+ throw new Error(`channel route(s) ${collisions.map((key) => `"${key}"`).join(", ")} collide with the AgentCore adapter ` +
182
+ `(/invocations, /ping) — rename the channel route`);
183
+ }
184
+ return { ...routes, ...mounted };
185
+ }
186
+ /**
187
+ * Refuse `--tunnel` with a bind that cloudflared cannot reach: it dials the NAME `localhost:<port>`
188
+ * (the dev supervisor's tunnel too), so anything outside `127.0.0.1`/`::1`/wildcard — including a
189
+ * `127.0.0.2` bind, loopback though it is — would leave the tunnel up and 502ing every request.
190
+ * Checked BEFORE the bind (the flag alone pre-spawn in `dev`, again once config is loaded, since
191
+ * `http.host` can carry the address), so the failure is clean rather than a live-but-broken public URL.
192
+ * `source` decides the exit code, per fail.ts: a flag COMBINATION is a usage error (2), a value that
193
+ * came from config is broken runtime configuration (1).
194
+ */
195
+ export function assertTunnelBindable(host, tunnel, source) {
196
+ if (!tunnel || answersLocalhost(host))
197
+ return;
198
+ // Name the source, not just the exit code: under `config` there is no `--bind` to change and no flag
199
+ // to drop, so flag-only wording would send the reader looking for something they never typed.
200
+ const fix = source === "flag"
201
+ ? "bind 0.0.0.0 (or 127.0.0.1), or drop --tunnel"
202
+ : "set http.host to 0.0.0.0 (or 127.0.0.1) in fastagent.config.*, override it with --bind, or drop --tunnel";
203
+ const message = `--tunnel reaches the serve by dialing localhost, which the bind address ${host} does not answer — ${fix}`;
204
+ if (source === "flag")
205
+ failUsage(message);
206
+ failStartup(new Error(message));
207
+ }
208
+ /**
209
+ * The startup lines that name WHERE the serve is: the bind report, and the curl the reader copies.
210
+ * ONE function because they are one message — they were two, and `--bind` updated the first while the
211
+ * second went on dialing `localhost`, which is precisely what a non-wildcard bind stops answering. Now
212
+ * neither can be changed without the other in view, and the address has a single derivation.
213
+ *
214
+ * A wildcard bind is every interface, and naming one address there would understate it — but the curl
215
+ * still needs one to dial, which is what `clientHost` gives (loopback for a wildcard, itself otherwise).
216
+ */
217
+ export function readyAddressLines(host, boundPort, builtinInvoke) {
218
+ const dial = `${clientHost(host)}:${boundPort}`;
219
+ const lines = [
220
+ `[fastagent] http host on ${classifyBind(host) === "wildcard" ? `:${boundPort} (all interfaces)` : bindLabel(host, boundPort)}`,
221
+ ];
222
+ if (builtinInvoke) {
223
+ lines.push(`[fastagent] try it: curl -s ${dial}/invoke -X POST -H 'content-type: application/json' -d '${INVOKE_EXAMPLE_BODY}'`);
224
+ }
225
+ return lines;
226
+ }
135
227
  /**
136
228
  * Bind HTTP, open long-connection channels, and report ready only when both forms are usable. Each
137
229
  * adapter owns reconnects; a terminal close rejects `closed` and fails the process visibly. Abort is
138
- * the sole clean-shutdown command.
230
+ * the sole clean-shutdown command. `host` unset binds all interfaces.
139
231
  */
140
- export function serve(surface, port, onListening) {
141
- const hosted = serveNode(router(surface.routes), { port });
232
+ export function serve(surface, bind, onListening) {
233
+ const { port, host } = bind;
234
+ const hosted = serveNode(router(surface.routes), { port, host });
142
235
  const abort = new AbortController();
143
236
  let stopping = false;
144
237
  const stop = (exitCode) => {
@@ -193,14 +286,12 @@ export function serve(surface, port, onListening) {
193
286
  port: boundPort,
194
287
  routeChannels: surface.routeChannels,
195
288
  });
196
- log.info(`[fastagent] http host on :${boundPort}`);
289
+ for (const line of readyAddressLines(host, boundPort, surface.builtinInvoke))
290
+ log.info(line);
197
291
  log.info(`[fastagent] routes: ${Object.keys(surface.routes).join(", ") || "(none)"}`);
198
292
  if (surface.longConnections.length > 0) {
199
293
  log.info(`[fastagent] long connections: ${surface.longConnections.map((connection) => connection.name).join(", ")}`);
200
294
  }
201
- if (surface.builtinInvoke) {
202
- log.info(`[fastagent] try it: curl -s localhost:${boundPort}/invoke -X POST -H 'content-type: application/json' -d '${INVOKE_EXAMPLE_BODY}'`);
203
- }
204
295
  onListening?.(boundPort);
205
296
  }
206
297
  catch (error) {
@@ -211,38 +302,48 @@ export function serve(surface, port, onListening) {
211
302
  failStartup(error);
212
303
  }
213
304
  }, (error) => {
214
- if (error.code === "EADDRINUSE")
215
- failStartup(new Error(`port ${port} is already in use; choose another with --port`));
216
- failStartup(new Error(`cannot bind http channel on :${port}: ${error.message}`));
305
+ if (error.code === "EADDRINUSE") {
306
+ // With a bind address the port is only taken ON THAT interface, so moving the bind is as valid
307
+ // a fix as moving the port.
308
+ failStartup(new Error(`${bindLabel(host, port)} is already in use; choose another with ` +
309
+ `--port${classifyBind(host) === "wildcard" ? "" : " or --bind"}`));
310
+ }
311
+ // Through `bindLabel`, like every other message about a bind: hand-concatenating gives `:::8787`
312
+ // for an IPv6 bind and a bare `:8787` for the wildcard, which reads as an explicit bind of nothing.
313
+ failStartup(new Error(`cannot bind http channel on ${bindLabel(host, port)}: ${error.message}`));
217
314
  });
218
315
  }
219
316
  /** Start a Cloudflare tunnel for route channels only. */
220
- export function maybeTunnel(workspaceDir, routeChannels, boundPort, tunnel, stateRoot) {
317
+ export function maybeTunnel(agentDir, routeChannels, boundPort, tunnel, stateRoot) {
221
318
  if (!tunnel || process.env.FASTAGENT_DEV_WORKER === "1")
222
319
  return;
223
320
  void startCloudflareTunnel(boundPort).then((instance) => {
224
321
  if (!instance)
225
322
  return;
226
- void announceWebhooks(workspaceDir, instance.url, { openUrl: openExternalUrl, routeChannels, stateRoot });
323
+ void announceWebhooks(agentDir, instance.url, { openUrl: openExternalUrl, routeChannels, stateRoot });
227
324
  const cleanup = () => instance.close();
228
325
  process.once("SIGINT", cleanup);
229
326
  process.once("SIGTERM", cleanup);
230
327
  });
231
328
  }
232
329
  /**
233
- * Load and start the workspace's `schedules/` — a time-trigger firing the agent on each cron. Starts iff
234
- * there are static schedules OR `selfSchedule` is on. Best-effort stop on process signals.
330
+ * Load and start the agent's `schedules/` — a time-trigger firing the agent on each cron. Starts iff
331
+ * there are static schedules OR `selfSchedule` is on. Best-effort stop on process signals. Returns the
332
+ * loaded schedules so a serving surface that needs them (the AgentCore adapter's fire binding) shares
333
+ * ONE load instead of re-discovering. `externalClock` (AgentCore) arms no resident cron timers.
235
334
  */
236
- export async function startSchedules(workspaceDir, agent, stateRoot, selfSchedule) {
237
- const { schedules, failures } = await loadSchedules(workspaceDir).catch(failStartup);
335
+ export async function startSchedules(agentDir, agent, stateRoot, selfSchedule, options = {}) {
336
+ const { schedules, failures } = await loadSchedules(agentDir).catch(failStartup);
238
337
  reportModuleLoadFailures(failures);
239
338
  if (schedules.length === 0 && !selfSchedule)
240
- return;
241
- const scheduler = createScheduler({ agent, stateRoot, schedules });
339
+ return schedules;
340
+ const scheduler = createScheduler({ agent, stateRoot, schedules, externalClock: options.externalClock });
242
341
  scheduler.start();
243
- if (schedules.length > 0)
244
- log.info(`[fastagent] schedules: ${schedules.map((s) => s.name).join(", ")}`);
342
+ if (schedules.length > 0) {
343
+ log.info(`[fastagent] schedules: ${schedules.map((s) => s.name).join(", ")}${options.externalClock ? " (external clock — no resident cron timers)" : ""}`);
344
+ }
245
345
  const stop = () => scheduler.stop();
246
346
  process.once("SIGINT", stop);
247
347
  process.once("SIGTERM", stop);
348
+ return schedules;
248
349
  }
@@ -1,4 +1,16 @@
1
1
  import { type LoginIO, type LoginMethod, type LoginResult } from "../engines/pi/login.ts";
2
+ /**
3
+ * The padded label writer for the STARTUP report (`dev`/`start`, stderr via the log level). Hand-spaced
4
+ * labels drift out of alignment the moment a longer one appears — which is exactly what happened when
5
+ * `workspace:` joined `config:`/`model:`/`state:`. `info` keeps its own writer on purpose: its report is
6
+ * stdout DATA (pipeable, its own label set, its own width), not a log line — the shared thing is the
7
+ * policy (pad, never hand-space), not a constant.
8
+ */
9
+ export declare function reportLine(label: string, value: string): void;
10
+ /** The workspace hint under the `agent:`/`workspace:` pair, when there is one ({@link workspaceHint}):
11
+ * you pointed at the agent, and the project around it is probably what you meant. A hint, so it renders
12
+ * as one and is silent otherwise — `dev` and `start` both print the pair, so both ask for it. */
13
+ export declare function reportWorkspaceHint(hint: string | undefined): void;
2
14
  /** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
3
15
  export declare function isInteractive(): boolean;
4
16
  /**
@@ -8,6 +20,12 @@ export declare function isInteractive(): boolean;
8
20
  * bad `PORT` env is broken runtime configuration (1).
9
21
  */
10
22
  export declare function parsePort(value: string | undefined, source: string, from: "flag" | "env"): number | undefined;
23
+ /**
24
+ * Parse a `--bind` address: empty/whitespace is "not set" → undefined (the `??` chain falls through to
25
+ * config, then all interfaces). An unbindable string is a USAGE error (2) — caught here rather than as
26
+ * a node bind failure, or worse, as a "the interface you bound" diagnostic downstream.
27
+ */
28
+ export declare function parseBind(value: string | undefined): string | undefined;
11
29
  /** Report which source provides the model's credentials, surfacing a remediation hint at startup. Non-blocking. */
12
30
  export declare function reportAuth(modelSpec: string, authPath: string): Promise<void>;
13
31
  /**
@@ -19,9 +37,10 @@ export declare function reportAuth(modelSpec: string, authPath: string): Promise
19
37
  * already set; on a non-TTY (CI, a piped stdin), with `--no-input`, on cancel, or on a failed login
20
38
  * it stays quiet and lets the caller raise its own clear error (`missing model`, or deploy's
21
39
  * model-travel gate). The pick is exported to FASTAGENT_MODEL so a spawned `dev` worker inherits it,
22
- * and best-effort written back to the config so the next run is quiet.
40
+ * and best-effort written back to the config so the next run is quiet. `agentDir` is the resolved
41
+ * AGENT DIR (resolvePlacement().agentDir) — config and auth both live there.
23
42
  */
24
- export declare function resolveFirstRunModel(workspaceDir: string, options?: {
43
+ export declare function resolveFirstRunModel(agentDir: string, options?: {
25
44
  model?: string;
26
45
  authPath?: string;
27
46
  input?: boolean;
@@ -8,14 +8,31 @@ import { relative } from "node:path";
8
8
  import { autocomplete, isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
9
9
  import { buildModelPickerOptions } from "./models-view.js";
10
10
  import { fastagentCredentialStore } from "../engines/pi/auth.js";
11
- import { isValidPort, listModels, loadConfig, providerOf, resolveAuthPath, resolveModel, resolveModelSpec, resolveStateRoot, rewriteConfigModel, } from "../engines/pi/config.js";
12
- import { ensureStateRootSelfIgnored, isUnderDir } from "../engines/pi/definition.js";
11
+ import { isValidPort, listModels, loadConfig, providerOf, resolveAuthPath, resolveModel, resolveModelSpec, rewriteConfigModel, } from "../engines/pi/config.js";
13
12
  import { LoginCancelled, loginFlow } from "../engines/pi/login.js";
14
13
  import { createPiModels, probeApiKey, probeAuthSource, providerAuthStatuses } from "../engines/pi/models.js";
15
14
  import { formatAuthReport } from "./auth-view.js";
16
15
  import { log } from "../log.js";
17
16
  import { openExternalUrl } from "../open-url.js";
17
+ import { bindAddress, isBindAddress } from "../bind.js";
18
18
  import { failStartup, failUsage } from "./fail.js";
19
+ /**
20
+ * The padded label writer for the STARTUP report (`dev`/`start`, stderr via the log level). Hand-spaced
21
+ * labels drift out of alignment the moment a longer one appears — which is exactly what happened when
22
+ * `workspace:` joined `config:`/`model:`/`state:`. `info` keeps its own writer on purpose: its report is
23
+ * stdout DATA (pipeable, its own label set, its own width), not a log line — the shared thing is the
24
+ * policy (pad, never hand-space), not a constant.
25
+ */
26
+ export function reportLine(label, value) {
27
+ log.info(`[fastagent] ${`${label}:`.padEnd(11)}${value}`);
28
+ }
29
+ /** The workspace hint under the `agent:`/`workspace:` pair, when there is one ({@link workspaceHint}):
30
+ * you pointed at the agent, and the project around it is probably what you meant. A hint, so it renders
31
+ * as one and is silent otherwise — `dev` and `start` both print the pair, so both ask for it. */
32
+ export function reportWorkspaceHint(hint) {
33
+ if (hint)
34
+ reportLine("hint", hint);
35
+ }
19
36
  /** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
20
37
  export function isInteractive() {
21
38
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
@@ -40,6 +57,19 @@ export function parsePort(value, source, from) {
40
57
  }
41
58
  return Number(trimmed);
42
59
  }
60
+ /**
61
+ * Parse a `--bind` address: empty/whitespace is "not set" → undefined (the `??` chain falls through to
62
+ * config, then all interfaces). An unbindable string is a USAGE error (2) — caught here rather than as
63
+ * a node bind failure, or worse, as a "the interface you bound" diagnostic downstream.
64
+ */
65
+ export function parseBind(value) {
66
+ const trimmed = value?.trim();
67
+ if (!trimmed)
68
+ return undefined;
69
+ if (!isBindAddress(trimmed))
70
+ failUsage(`invalid --bind "${value}": must be an IP address or "localhost"`);
71
+ return bindAddress(trimmed); // a name never travels past this point — see bind.ts
72
+ }
43
73
  /** Report which source provides the model's credentials, surfacing a remediation hint at startup. Non-blocking. */
44
74
  export async function reportAuth(modelSpec, authPath) {
45
75
  const provider = providerOf(modelSpec);
@@ -65,23 +95,24 @@ export async function reportAuth(modelSpec, authPath) {
65
95
  * already set; on a non-TTY (CI, a piped stdin), with `--no-input`, on cancel, or on a failed login
66
96
  * it stays quiet and lets the caller raise its own clear error (`missing model`, or deploy's
67
97
  * model-travel gate). The pick is exported to FASTAGENT_MODEL so a spawned `dev` worker inherits it,
68
- * and best-effort written back to the config so the next run is quiet.
98
+ * and best-effort written back to the config so the next run is quiet. `agentDir` is the resolved
99
+ * AGENT DIR (resolvePlacement().agentDir) — config and auth both live there.
69
100
  */
70
- export async function resolveFirstRunModel(workspaceDir, options = {}) {
71
- const { config, path: configPath } = await loadConfig(workspaceDir).catch(failStartup);
101
+ export async function resolveFirstRunModel(agentDir, options = {}) {
102
+ const { config, path: configPath } = await loadConfig(agentDir).catch(failStartup);
72
103
  if (resolveModelSpec(options.model, config))
73
104
  return; // already set (flag > FASTAGENT_MODEL > config)
74
105
  if (options.input === false)
75
106
  return; // --no-input: never prompt (clig) — the opener raises the clear error
76
107
  if (!isInteractive())
77
108
  return; // CI/deploy: the opener throws the actionable missing-model error
78
- const authPath = resolveAuthPath(workspaceDir, options.authPath);
109
+ const authPath = resolveAuthPath(agentDir, options.authPath);
79
110
  const models = createPiModels({ authPath });
80
- const chosen = await pickWithCredentials(workspaceDir, models, authPath);
111
+ const chosen = await pickWithCredentials(models, authPath);
81
112
  if (chosen === undefined)
82
113
  return; // cancelled (or auth probe failed): the caller raises its clear missing-model error
83
114
  process.env.FASTAGENT_MODEL = chosen; // this process + any spawned dev worker inherits it
84
- await persistModelChoice(workspaceDir, configPath, chosen);
115
+ await persistModelChoice(agentDir, configPath, chosen);
85
116
  }
86
117
  /**
87
118
  * The credential-aware pick: full catalog annotated per provider, then the post-pick auth policy —
@@ -89,7 +120,7 @@ export async function resolveFirstRunModel(workspaceDir, options = {}) {
89
120
  * independent of credentials), or the inline login for the rest. Returns the chosen spec, or
90
121
  * undefined when the pick should be discarded (picker cancel, login cancel, a failed auth probe).
91
122
  */
92
- async function pickWithCredentials(workspaceDir, models, authPath) {
123
+ async function pickWithCredentials(models, authPath) {
93
124
  let statuses;
94
125
  try {
95
126
  statuses = await providerAuthStatuses(models);
@@ -124,11 +155,6 @@ async function pickWithCredentials(workspaceDir, models, authPath) {
124
155
  }
125
156
  return chosen;
126
157
  }
127
- // Inline login. Same leak guard as `login`: self-ignore the state root BEFORE a credential
128
- // can land in-tree, so the secret is never untracked-but-committable.
129
- const stateRoot = resolveStateRoot(workspaceDir);
130
- if (isUnderDir(authPath, stateRoot))
131
- await ensureStateRootSelfIgnored(workspaceDir, stateRoot);
132
158
  try {
133
159
  // Verified against the CHOSEN model — the exact request the agent is about to make; a rejected
134
160
  // key re-prompts inside the loop, so reaching here means a usable (or at worst unverifiable) key.
@@ -224,10 +250,10 @@ function terminalLoginIO() {
224
250
  /**
225
251
  * Best-effort persist the picked model so the next run does not prompt. Rewrites the commented
226
252
  * `model:` placeholder the scaffold writes / an existing `model:` line, or re-inserts the line into a
227
- * scaffold-shaped config (the hand-deleted-to-reset case); anything else (zero-config, a hand-shaped
228
- * config) is left untouched with a printed hint. Never throws — persistence is a convenience.
253
+ * scaffold-shaped config (the hand-deleted-to-reset case); anything else (a hand-shaped config) is
254
+ * left untouched with a printed hint. Never throws — persistence is a convenience.
229
255
  */
230
- async function persistModelChoice(workspaceDir, configPath, spec) {
256
+ async function persistModelChoice(agentDir, configPath, spec) {
231
257
  const hint = () => console.error(
232
258
  // No "using it for this run" promise: deploy's model-travel gate rightly ignores the un-persisted pick.
233
259
  `[fastagent] picked ${spec} — set \`model: ${JSON.stringify(spec)}\` in your config to persist`);
@@ -238,7 +264,7 @@ async function persistModelChoice(workspaceDir, configPath, spec) {
238
264
  if (!replaced)
239
265
  return hint();
240
266
  await writeFile(configPath, replaced);
241
- console.error(`[fastagent] saved model ${JSON.stringify(spec)} to ${relative(workspaceDir, configPath)}`);
267
+ console.error(`[fastagent] saved model ${JSON.stringify(spec)} to ${relative(agentDir, configPath)}`);
242
268
  }
243
269
  catch {
244
270
  hint();
@@ -0,0 +1,117 @@
1
+ import type { ChannelKind } from "../../scaffold/add-channel.ts";
2
+ import { type Artifact, type ContainerInput } from "../container.ts";
3
+ /** The one schedule fact the plan needs (from loadSchedules) — name + cron + tz. */
4
+ export interface ScheduleFact {
5
+ name: string;
6
+ cron: string;
7
+ tz?: string;
8
+ }
9
+ export interface AgentcorePlanInput extends ContainerInput {
10
+ /** Base name (dir basename) — shapes the runtime name, stack name, ECR repo, session id. */
11
+ name: string;
12
+ /** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
13
+ modelAuth: string | undefined;
14
+ /** Known first-party channels — each contributes its secret metadata + webhook step. */
15
+ channels: ChannelKind[];
16
+ /** ALL route-channel basenames (customs included) — any of them requires the forwarder. */
17
+ routeChannels: string[];
18
+ /** Extra secret env-var names (fastagent.config deploy.secrets). */
19
+ extraSecrets?: string[];
20
+ /** Static schedules — each becomes an EventBridge Scheduler rule targeting the forwarder. */
21
+ schedules: ScheduleFact[];
22
+ /** Wake tool enabled — DEGRADED here (fires only while a session happens to be awake); warned. */
23
+ selfSchedule: boolean;
24
+ }
25
+ export interface AgentcorePlan {
26
+ /** template + forwarder + Dockerfile/.dockerignore — written by the CLI (kept unless --force). */
27
+ artifacts: Artifact[];
28
+ /** The ordered, values-resolved deploy runbook — printed to stdout. */
29
+ runbook: string[];
30
+ /** Cron expressions EventBridge cannot express — surfaced as runbook warnings, not silent drops. */
31
+ untranslatableSchedules: {
32
+ name: string;
33
+ reason: string;
34
+ }[];
35
+ }
36
+ /** SessionStorage mount = FASTAGENT_STATE_DIR (AgentCore requires exactly `/mnt/<one-level>`). It is
37
+ * a fast LOCAL disk only: the platform wipes it on every runtime version update (= every deploy).
38
+ * Durability across deploys comes from the S3 snapshot (channels/agentcore-state.ts). */
39
+ export declare const MOUNT = "/mnt/state";
40
+ /**
41
+ * How long an idle session keeps its microVM. Memory is billed per second across the WHOLE session
42
+ * — idle included, at the peak level reached — so this tail is the standing cost of every burst of
43
+ * activity, while CPU stops billing the moment the agent stops working. 3 minutes rather than the
44
+ * platform's 15: the tail shrinks 5×, and the cost is a cold start (image + Node + snapshot restore)
45
+ * for anyone who returns after a longer gap. `/ping` reports HealthyBusy while work is in flight, so
46
+ * this timer only ever starts once the agent has genuinely settled — a long turn is never cut short.
47
+ * AWS accepts 60–28800.
48
+ */
49
+ export declare const IDLE_TIMEOUT_SECONDS = 180;
50
+ /** The platform ceiling on one session's compute (8 h). The session ID outlives it: the next invoke
51
+ * simply gets fresh compute with the same storage. */
52
+ export declare const MAX_LIFETIME_SECONDS = 28800;
53
+ /** The state snapshot's object key in the deployment bucket (one object; see agentcore-state.ts). */
54
+ export declare const STATE_KEY = "state/snapshot.json.gz";
55
+ /** The forwarder artifact. Named `index.js` because it IS the Lambda deployment package's entry:
56
+ * zipping it as-is produces a valid package (`Handler: index.handler`), with nothing to rename. */
57
+ export declare const FORWARDER_FILE = "lambda/index.js";
58
+ /** The deployment bucket: forwarder code + the state snapshot. Account-suffixed for S3's GLOBAL
59
+ * namespace, and created OUTSIDE the stack (like the ECR repo) so a `delete-stack` cannot take the
60
+ * agent's memory with it. Bucket names cap at 63 chars; `name` is already gated to 40. */
61
+ export declare function stateBucketName(name: string, account: string): string;
62
+ /** AgentCore env values max 2048 chars — a real OAuth auth.json's base64 exceeds it, so the seed is
63
+ * CHUNKED across FASTAGENT_AUTH_SEED + _2… (collectAuthSeed reassembles at boot). 2000 keeps margin. */
64
+ export declare const AUTH_SEED_CHUNK_SIZE = 2000;
65
+ export declare const AUTH_SEED_MAX_CHUNKS = 4;
66
+ /** The generated template's filename (namespaced under the kit in the agentDir layout). */
67
+ export declare const TEMPLATE_FILE = "agentcore.template.yaml";
68
+ /** The generated template's first-line marker — the ONE source for both the generator and the
69
+ * "did fastagent generate this?" check (deploy's drift gate), so they cannot drift apart. */
70
+ export declare const GENERATED_TEMPLATE_MARKER = "# Generated by `fastagent deploy agentcore`";
71
+ /** Whether an on-disk template is fastagent-generated (vs hand-written — kept, never gated). */
72
+ export declare function isGeneratedAgentcoreTemplate(content: string): boolean;
73
+ /** Runtime name (`[a-zA-Z][a-zA-Z0-9_]{0,47}`) from a dir basename. */
74
+ export declare function toRuntimeName(basename: string): string;
75
+ /** The ONE fixed ingress session id (webhooks + schedule fires) — ≥ 33 chars (the API minimum),
76
+ * deterministic (the Lambda holds it in env), padded so any name clears the floor. */
77
+ export declare function ingressSessionId(name: string): string;
78
+ /** CFN parameter logical id for a secret env-var name: TELEGRAM_BOT_TOKEN → TelegramBotToken
79
+ * (parameter names must be alphanumeric). Deterministic — run.ts builds the same mapping. */
80
+ export declare function cfnParamName(envName: string): string;
81
+ /**
82
+ * Translate a 5-field cron into EventBridge Scheduler's `cron(m h dom mon dow *)`, or say why it
83
+ * can't be. The two dialects disagree exactly where silent translation would misfire:
84
+ * - EventBridge numbers day-of-week 1–7 (1 = Sunday); standard cron uses 0–6 (0/7 = Sunday) —
85
+ * numeric dow values and range endpoints are remapped ({@link mapDowField}); steps and names
86
+ * pass through; a range that wraps under renumbering is refused.
87
+ * - EventBridge requires `?` in dom or dow: a `*` on either side becomes `?`; BOTH restricted is
88
+ * standard cron's OR semantics, which EventBridge cannot express — refused, never approximated.
89
+ * - A 6-field (seconds) expression and L/# day-of-week forms are refused for the same reason.
90
+ */
91
+ export declare function toEventBridgeCron(cron: string): {
92
+ expression: string;
93
+ } | {
94
+ error: string;
95
+ };
96
+ /**
97
+ * The forwarder Lambda source — the ONLY string both the template's inline ZipFile and the readable
98
+ * `lambda/forwarder.js` artifact are generated from (one source, no drift). Zero-dependency: the
99
+ * Lambda Node runtime bundles AWS SDK v3. CommonJS ON PURPOSE: CloudFormation inline code always
100
+ * lands as `index.js`, where ESM `import` is a syntax error (found by the first real deploy). Two
101
+ * event shapes: a Function URL webhook (reconstructed verbatim into a `webhook` envelope; the
102
+ * channel's REAL response rides back inside the transport reply and is re-emitted byte-exact —
103
+ * Feishu's URL-verification challenge depends on it), and an EventBridge Scheduler fire
104
+ * (`{ scheduleFire }`, slot = the scheduled instant — the container's idempotency key). MUST stay
105
+ * under CloudFormation's 4096-byte inline-code cap.
106
+ */
107
+ export declare function forwarderSource(): string;
108
+ /**
109
+ * The EventBridge physical name for a schedule. A schedule's local name is an arbitrary MODULE FILE
110
+ * NAME (`schedules/晨报.ts`, `schedules/deploy check.ts`), while AWS requires `[0-9A-Za-z-_.]+` within
111
+ * 64 chars — and the `fa-<agent>-` prefix already eats up to 44 of them. So: sanitize, bound the
112
+ * readable part, and end with a hash of the ORIGINAL name, which keeps distinct schedules distinct
113
+ * where sanitizing or truncation would have merged them (one rule silently firing for two).
114
+ */
115
+ export declare function scheduleResourceName(agent: string, schedule: string): string;
116
+ /** Compute the AgentCore deploy plan from the resolved definition. */
117
+ export declare function planAgentcoreDeploy(input: AgentcorePlanInput): AgentcorePlan;