@fastagent-sh/fastagent 0.14.0 → 0.16.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 (277) hide show
  1. package/README.md +16 -6
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -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 +258 -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 +35 -0
  13. package/dist/channels/context-buffer.js +103 -0
  14. package/dist/channels/control.d.ts +28 -0
  15. package/dist/channels/control.js +214 -0
  16. package/dist/channels/feishu/card.js +1 -1
  17. package/dist/channels/feishu/context-buffer.d.ts +15 -13
  18. package/dist/channels/feishu/context-buffer.js +62 -69
  19. package/dist/channels/feishu/feishu-api.d.ts +11 -3
  20. package/dist/channels/feishu/feishu-api.js +24 -3
  21. package/dist/channels/feishu/feishu.d.ts +27 -27
  22. package/dist/channels/feishu/feishu.js +318 -207
  23. package/dist/channels/feishu/invoke-turn.d.ts +7 -20
  24. package/dist/channels/feishu/invoke-turn.js +44 -79
  25. package/dist/channels/feishu/model.d.ts +2 -2
  26. package/dist/channels/feishu/normalize.d.ts +2 -1
  27. package/dist/channels/feishu/normalize.js +0 -1
  28. package/dist/channels/feishu/parse.d.ts +22 -8
  29. package/dist/channels/feishu/parse.js +24 -7
  30. package/dist/channels/feishu/preview.d.ts +4 -7
  31. package/dist/channels/feishu/preview.js +27 -142
  32. package/dist/channels/feishu/register-app.d.ts +2 -1
  33. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  34. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  35. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  36. package/dist/channels/feishu/setup-mode.d.ts +37 -0
  37. package/dist/channels/feishu/setup-mode.js +28 -0
  38. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  39. package/dist/channels/feishu/ws-ingress.js +136 -0
  40. package/dist/channels/github/github.js +16 -7
  41. package/dist/channels/http.d.ts +14 -0
  42. package/dist/channels/http.js +36 -3
  43. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  44. package/dist/channels/invoke-turn-kit.js +87 -0
  45. package/dist/channels/lark/lark.d.ts +4 -2
  46. package/dist/channels/lark/lark.js +4 -1
  47. package/dist/channels/lark/onboard.d.ts +8 -4
  48. package/dist/channels/lark/onboard.js +8 -0
  49. package/dist/channels/lark/scaffold/channel.ts +9 -8
  50. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  51. package/dist/channels/preview-kit.d.ts +115 -0
  52. package/dist/channels/preview-kit.js +184 -0
  53. package/dist/channels/seen.d.ts +5 -0
  54. package/dist/channels/seen.js +35 -0
  55. package/dist/channels/slack/bot-auth.d.ts +15 -0
  56. package/dist/channels/slack/bot-auth.js +146 -0
  57. package/dist/channels/slack/config-api.d.ts +60 -0
  58. package/dist/channels/slack/config-api.js +149 -0
  59. package/dist/channels/slack/context-buffer.d.ts +24 -0
  60. package/dist/channels/slack/context-buffer.js +37 -0
  61. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  62. package/dist/channels/slack/invoke-turn.js +63 -0
  63. package/dist/channels/slack/manifest.d.ts +49 -0
  64. package/dist/channels/slack/manifest.js +69 -0
  65. package/dist/channels/slack/model.d.ts +67 -0
  66. package/dist/channels/slack/model.js +2 -0
  67. package/dist/channels/slack/onboard.d.ts +41 -0
  68. package/dist/channels/slack/onboard.js +120 -0
  69. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  70. package/dist/channels/slack/onboarding-state.js +69 -0
  71. package/dist/channels/slack/parse.d.ts +38 -0
  72. package/dist/channels/slack/parse.js +124 -0
  73. package/dist/channels/slack/preview.d.ts +23 -0
  74. package/dist/channels/slack/preview.js +403 -0
  75. package/dist/channels/slack/reaction.d.ts +24 -0
  76. package/dist/channels/slack/reaction.js +62 -0
  77. package/dist/channels/slack/register-webhook.d.ts +10 -0
  78. package/dist/channels/slack/register-webhook.js +49 -0
  79. package/dist/channels/slack/scaffold/channel.ts +33 -0
  80. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  81. package/dist/channels/slack/setup-server.d.ts +17 -0
  82. package/dist/channels/slack/setup-server.js +103 -0
  83. package/dist/channels/slack/slack-api.d.ts +57 -0
  84. package/dist/channels/slack/slack-api.js +399 -0
  85. package/dist/channels/slack/slack.d.ts +51 -0
  86. package/dist/channels/slack/slack.js +496 -0
  87. package/dist/channels/slack/welcomed.d.ts +5 -0
  88. package/dist/channels/slack/welcomed.js +32 -0
  89. package/dist/channels/state.d.ts +11 -4
  90. package/dist/channels/state.js +22 -15
  91. package/dist/channels/stop-command.d.ts +6 -0
  92. package/dist/channels/stop-command.js +36 -0
  93. package/dist/channels/tasks.d.ts +7 -0
  94. package/dist/channels/tasks.js +25 -0
  95. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  96. package/dist/channels/telegram/context-buffer.js +6 -85
  97. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  98. package/dist/channels/telegram/invoke-turn.js +11 -58
  99. package/dist/channels/telegram/parse.d.ts +0 -7
  100. package/dist/channels/telegram/parse.js +4 -2
  101. package/dist/channels/telegram/preview.d.ts +4 -7
  102. package/dist/channels/telegram/preview.js +24 -142
  103. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  104. package/dist/channels/telegram/telegram.js +24 -10
  105. package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
  106. package/dist/channels/{feishu/text.js → text.js} +15 -1
  107. package/dist/channels/thread-participants.d.ts +21 -0
  108. package/dist/channels/thread-participants.js +132 -0
  109. package/dist/channels/turn-queue.js +8 -1
  110. package/dist/channels/turn-store.d.ts +1 -1
  111. package/dist/channels/turn-store.js +2 -3
  112. package/dist/cli/add-feishu.d.ts +30 -0
  113. package/dist/cli/add-feishu.js +366 -0
  114. package/dist/cli/add-slack.d.ts +11 -0
  115. package/dist/cli/add-slack.js +199 -0
  116. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  117. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  118. package/dist/cli/commands/add.d.ts +4 -0
  119. package/dist/cli/commands/add.js +191 -60
  120. package/dist/cli/commands/attach.d.ts +82 -0
  121. package/dist/cli/commands/attach.js +563 -0
  122. package/dist/cli/commands/chat.d.ts +1 -0
  123. package/dist/cli/commands/chat.js +12 -7
  124. package/dist/cli/commands/deploy.d.ts +3 -1
  125. package/dist/cli/commands/deploy.js +384 -82
  126. package/dist/cli/commands/dev.js +34 -26
  127. package/dist/cli/commands/fire.js +17 -18
  128. package/dist/cli/commands/info.js +36 -29
  129. package/dist/cli/commands/init.d.ts +1 -1
  130. package/dist/cli/commands/init.js +65 -53
  131. package/dist/cli/commands/invoke.js +11 -9
  132. package/dist/cli/commands/login.js +48 -45
  133. package/dist/cli/commands/models.js +1 -1
  134. package/dist/cli/commands/schedule.js +6 -8
  135. package/dist/cli/commands/start.js +102 -39
  136. package/dist/cli/commands/tool.js +23 -18
  137. package/dist/cli/fail.d.ts +17 -2
  138. package/dist/cli/fail.js +25 -1
  139. package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
  140. package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
  141. package/dist/cli/kernel.d.ts +3 -2
  142. package/dist/cli/models-view.d.ts +21 -0
  143. package/dist/cli/models-view.js +66 -0
  144. package/dist/cli/program.js +135 -56
  145. package/dist/cli/serve.d.ts +60 -20
  146. package/dist/cli/serve.js +252 -47
  147. package/dist/cli/shared.d.ts +40 -7
  148. package/dist/cli/shared.js +180 -38
  149. package/dist/collect.d.ts +14 -3
  150. package/dist/collect.js +24 -0
  151. package/dist/core.d.ts +3 -1
  152. package/dist/core.js +2 -0
  153. package/dist/deploy/agentcore/plan.d.ts +117 -0
  154. package/dist/deploy/agentcore/plan.js +721 -0
  155. package/dist/deploy/agentcore/run.d.ts +73 -0
  156. package/dist/deploy/agentcore/run.js +412 -0
  157. package/dist/deploy/agentcore/zip.d.ts +17 -0
  158. package/dist/deploy/agentcore/zip.js +68 -0
  159. package/dist/deploy/container.d.ts +26 -25
  160. package/dist/deploy/container.js +93 -89
  161. package/dist/deploy/docker/plan.d.ts +3 -3
  162. package/dist/deploy/docker/plan.js +20 -23
  163. package/dist/deploy/fly/plan.d.ts +5 -1
  164. package/dist/deploy/fly/plan.js +39 -26
  165. package/dist/deploy/fly/run.d.ts +17 -5
  166. package/dist/deploy/fly/run.js +52 -7
  167. package/dist/deploy/preflight.d.ts +20 -8
  168. package/dist/deploy/preflight.js +267 -71
  169. package/dist/deploy/railway/plan.d.ts +10 -1
  170. package/dist/deploy/railway/plan.js +53 -20
  171. package/dist/deploy/railway/run.d.ts +13 -5
  172. package/dist/deploy/railway/run.js +22 -7
  173. package/dist/deploy/runner.d.ts +7 -3
  174. package/dist/deploy/runner.js +9 -3
  175. package/dist/deploy/secrets.d.ts +2 -1
  176. package/dist/deploy/secrets.js +23 -3
  177. package/dist/dev-supervisor.d.ts +11 -10
  178. package/dist/dev-supervisor.js +56 -50
  179. package/dist/engines/pi/auth.d.ts +8 -7
  180. package/dist/engines/pi/auth.js +12 -10
  181. package/dist/engines/pi/channel.d.ts +22 -16
  182. package/dist/engines/pi/channel.js +92 -62
  183. package/dist/engines/pi/chat.d.ts +4 -16
  184. package/dist/engines/pi/chat.js +8 -261
  185. package/dist/engines/pi/config.d.ts +20 -55
  186. package/dist/engines/pi/config.js +49 -121
  187. package/dist/engines/pi/create.d.ts +30 -10
  188. package/dist/engines/pi/create.js +33 -26
  189. package/dist/engines/pi/definition.d.ts +7 -26
  190. package/dist/engines/pi/definition.js +8 -54
  191. package/dist/engines/pi/harness.d.ts +49 -0
  192. package/dist/engines/pi/harness.js +90 -2
  193. package/dist/engines/pi/invoke.d.ts +55 -2
  194. package/dist/engines/pi/invoke.js +304 -19
  195. package/dist/engines/pi/login.d.ts +12 -1
  196. package/dist/engines/pi/login.js +16 -4
  197. package/dist/engines/pi/models.d.ts +59 -13
  198. package/dist/engines/pi/models.js +62 -24
  199. package/dist/engines/pi/open.d.ts +114 -0
  200. package/dist/engines/pi/open.js +127 -0
  201. package/dist/engines/pi/session-builder.d.ts +16 -0
  202. package/dist/engines/pi/session-builder.js +308 -0
  203. package/dist/engines/pi/session-control.d.ts +50 -0
  204. package/dist/engines/pi/session-control.js +604 -0
  205. package/dist/engines/pi/sessions.d.ts +17 -2
  206. package/dist/engines/pi/sessions.js +9 -0
  207. package/dist/engines/pi/tool-context.d.ts +18 -11
  208. package/dist/engines/pi/tool-context.js +3 -9
  209. package/dist/engines/pi/tool.d.ts +6 -8
  210. package/dist/engines/pi/tool.js +10 -1
  211. package/dist/engines/pi/wake-tool.d.ts +0 -3
  212. package/dist/engines/pi/wake-tool.js +9 -7
  213. package/dist/env.d.ts +16 -4
  214. package/dist/env.js +43 -5
  215. package/dist/feishu.d.ts +1 -1
  216. package/dist/feishu.js +1 -1
  217. package/dist/host/node.d.ts +25 -8
  218. package/dist/host/node.js +5 -4
  219. package/dist/index.d.ts +1 -0
  220. package/dist/index.js +1 -0
  221. package/dist/lark.d.ts +1 -1
  222. package/dist/lark.js +1 -1
  223. package/dist/loader.d.ts +2 -2
  224. package/dist/loader.js +3 -3
  225. package/dist/log.d.ts +1 -1
  226. package/dist/log.js +1 -1
  227. package/dist/observe.js +3 -0
  228. package/dist/paths.d.ts +138 -0
  229. package/dist/paths.js +326 -0
  230. package/dist/pi.d.ts +6 -5
  231. package/dist/pi.js +4 -3
  232. package/dist/runtime.d.ts +7 -5
  233. package/dist/runtime.js +2 -2
  234. package/dist/scaffold/add-channel.d.ts +26 -9
  235. package/dist/scaffold/add-channel.js +175 -40
  236. package/dist/scaffold/init.d.ts +32 -41
  237. package/dist/scaffold/init.js +161 -185
  238. package/dist/scaffold/templates/env.example +15 -6
  239. package/dist/scaffold/templates/fastagent.config.mjs +5 -4
  240. package/dist/scaffold/templates/gitignore +14 -6
  241. package/dist/scaffold/templates/persona.md +4 -2
  242. package/dist/scaffold/templates/secrets.gitignore +5 -0
  243. package/dist/scaffold/templates.d.ts +1 -7
  244. package/dist/scaffold/templates.js +3 -25
  245. package/dist/scaffold/vendor-skill.d.ts +2 -2
  246. package/dist/scaffold/vendor-skill.js +13 -13
  247. package/dist/schedule/discover.js +4 -4
  248. package/dist/schedule/scheduler.d.ts +40 -1
  249. package/dist/schedule/scheduler.js +89 -56
  250. package/dist/schedule/state.js +1 -1
  251. package/dist/schedule/wake-alarm.d.ts +47 -0
  252. package/dist/schedule/wake-alarm.js +136 -0
  253. package/dist/schedule/wakeups.d.ts +1 -3
  254. package/dist/schedule/wakeups.js +19 -1
  255. package/dist/session-remote.d.ts +53 -0
  256. package/dist/session-remote.js +336 -0
  257. package/dist/session.d.ts +265 -0
  258. package/dist/session.js +37 -0
  259. package/dist/slack.d.ts +2 -0
  260. package/dist/slack.js +2 -0
  261. package/dist/tunnel.d.ts +7 -4
  262. package/dist/tunnel.js +23 -12
  263. package/package.json +22 -7
  264. package/dist/channels/feishu/owned-threads.d.ts +0 -11
  265. package/dist/channels/feishu/owned-threads.js +0 -47
  266. package/dist/channels/feishu/seen.d.ts +0 -5
  267. package/dist/channels/feishu/seen.js +0 -47
  268. package/dist/cli-add-feishu.d.ts +0 -8
  269. package/dist/cli-add-feishu.js +0 -223
  270. package/dist/cli-models.d.ts +0 -11
  271. package/dist/cli-models.js +0 -20
  272. package/dist/engines/pi/workspace.d.ts +0 -56
  273. package/dist/engines/pi/workspace.js +0 -72
  274. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  275. package/dist/scaffold/templates/gitignore.kit +0 -2
  276. package/dist/workspace.d.ts +0 -9
  277. package/dist/workspace.js +0 -45
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `fastagent deploy agentcore --run` — drive the AWS CLI + Docker to completion. The middle of the
3
+ * deploy the plain runbook hands to the operator; `--run` executes it so a coding agent runs ONE
4
+ * command. Idempotent (ECR check-then-act; `cloudformation deploy` converges the stack) and
5
+ * resumable: it STOPS at a human gate with one actionable line and a non-zero exit.
6
+ *
7
+ * TWO runners, one seam ({@link CliRunner}): `aws` (identity, ECR, CloudFormation) and `docker`
8
+ * (buildx). AgentCore is the ONE host whose image builds on the operator's machine — the platform
9
+ * requires linux/arm64 in the account's ECR and has no remote builder — so a missing Docker/buildx
10
+ * is a first-class gate, not an incidental failure.
11
+ *
12
+ * Secrets ride CloudFormation NoEcho parameters. `--parameter-overrides` on argv would put the
13
+ * values in the process listing (the same reason Fly imports secrets over stdin), so they go through
14
+ * a caller-provided temp parameters file (`file://…`, mode 0600, deleted by the caller) — the write
15
+ * is injected to keep this module pure and the security-sensitive wiring testable.
16
+ */
17
+ import type { RegistrationOutcome } from "../../channels/registration.ts";
18
+ import type { ChannelKind } from "../../scaffold/add-channel.ts";
19
+ import type { CliRunner } from "../runner.ts";
20
+ export interface AgentcoreRunPlan {
21
+ /** The base name — stack `fastagent-<name>`, ECR repo `fastagent/<name>`. */
22
+ name: string;
23
+ /** Template path relative to the run cwd (kit layout: `agent/agentcore.template.yaml`). */
24
+ templatePath: string;
25
+ /** Dockerfile path for `-f` (kit layout only; the default context Dockerfile otherwise). */
26
+ dockerfilePath?: string;
27
+ /** Image tag for this deploy — the CALLER mints it unique (a timestamp): CloudFormation only rolls
28
+ * the runtime when the ImageUri value changes, so a reused tag would deploy nothing. */
29
+ tag: string;
30
+ /** AWS region from the caller's environment (AWS_REGION/AWS_DEFAULT_REGION), else resolved via
31
+ * `aws configure get region` — an unset region is a gate (the ECR registry hostname needs it). */
32
+ region?: string;
33
+ /** Secret env-var name → value (model key or FASTAGENT_AUTH_SEED + channel secrets). Mapped to the
34
+ * template's parameter names via {@link cfnParamName}; delivered via the params file, never argv. */
35
+ secrets: Record<string, string>;
36
+ /** Required secret names with NO local value — gated before any side effect. */
37
+ missingSecrets: string[];
38
+ channels: ChannelKind[];
39
+ /** Whether the topology includes the forwarder Lambda (route channels / schedules / selfSchedule).
40
+ * It owns the state snapshot's presigned URLs, so its absence means an invoke-only deployment
41
+ * with no cross-deploy state to keep. */
42
+ needsForwarder: boolean;
43
+ }
44
+ export type AgentcoreRunOutcome = {
45
+ ok: true;
46
+ runtimeArn: string;
47
+ url?: string;
48
+ } | {
49
+ ok: false;
50
+ gate: string;
51
+ };
52
+ /** Stack outputs (`describe-stacks --query "Stacks[0].Outputs"`) → { OutputKey: OutputValue }. */
53
+ export declare function parseStackOutputs(stdout: string): Record<string, string>;
54
+ /** The `--parameter-overrides file://` payload: a JSON array of "Key=Value" strings. The auth seed
55
+ * is CHUNKED across FastagentAuthSeed(2…) — AgentCore env values cap at 2048 chars and a real OAuth
56
+ * auth.json's base64 exceeds it; `start` reassembles (collectAuthSeed). Every chunk is emitted on
57
+ * every deploy, including empty trailing chunks, so CloudFormation cannot retain stale values. */
58
+ export declare function paramsFileContent(imageUri: string, secrets: Record<string, string>, forwarder?: {
59
+ bucket: string;
60
+ key: string;
61
+ }): string;
62
+ export interface CheckpointReply {
63
+ written: boolean;
64
+ reason?: string;
65
+ }
66
+ /** Parse the runtime's checkpoint acknowledgement without treating malformed output as success. */
67
+ export declare function parseCheckpointReply(stdout: string): CheckpointReply | undefined;
68
+ /**
69
+ * Run the deploy through `aws` + `docker`. `log` reports progress; the injected registrars perform
70
+ * post-deploy webhook steps from the builder machine against the forwarder's Function URL. Every
71
+ * gate is fail-visible; `writeSecretFile` is the caller's 0600-temp-file seam (see the header).
72
+ */
73
+ export declare function deployAgentcoreRun(plan: AgentcoreRunPlan, aws: CliRunner, docker: CliRunner, log: (msg: string) => void, writeSecretFile: (content: string) => Promise<string>, writeForwarderZip: (bytes: Uint8Array) => Promise<string>, registerTelegram: (baseUrl: string) => Promise<RegistrationOutcome>, registerFeishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>, registerSlack?: (baseUrl: string) => Promise<RegistrationOutcome>): Promise<AgentcoreRunOutcome>;
@@ -0,0 +1,412 @@
1
+ import { registrationGate } from "../registration-gate.js";
2
+ import { createHash } from "node:crypto";
3
+ import { Buffer } from "node:buffer";
4
+ import { AUTH_SEED_CHUNK_SIZE, AUTH_SEED_MAX_CHUNKS, cfnParamName, forwarderSource, ingressSessionId, stateBucketName, } from "./plan.js";
5
+ import { zipSingleFile } from "./zip.js";
6
+ /** Stack outputs (`describe-stacks --query "Stacks[0].Outputs"`) → { OutputKey: OutputValue }. */
7
+ export function parseStackOutputs(stdout) {
8
+ try {
9
+ const arr = JSON.parse(stdout);
10
+ if (!Array.isArray(arr))
11
+ return {};
12
+ const out = {};
13
+ for (const o of arr) {
14
+ if (typeof o?.OutputKey === "string" && typeof o?.OutputValue === "string")
15
+ out[o.OutputKey] = o.OutputValue;
16
+ }
17
+ return out;
18
+ }
19
+ catch {
20
+ return {};
21
+ }
22
+ }
23
+ /** The `--parameter-overrides file://` payload: a JSON array of "Key=Value" strings. The auth seed
24
+ * is CHUNKED across FastagentAuthSeed(2…) — AgentCore env values cap at 2048 chars and a real OAuth
25
+ * auth.json's base64 exceeds it; `start` reassembles (collectAuthSeed). Every chunk is emitted on
26
+ * every deploy, including empty trailing chunks, so CloudFormation cannot retain stale values. */
27
+ export function paramsFileContent(imageUri, secrets, forwarder) {
28
+ const params = [`ImageUri=${imageUri}`];
29
+ if (forwarder)
30
+ params.push(`StateBucket=${forwarder.bucket}`, `ForwarderS3Key=${forwarder.key}`);
31
+ for (const [k, v] of Object.entries(secrets)) {
32
+ if (k !== "FASTAGENT_AUTH_SEED")
33
+ params.push(`${cfnParamName(k)}=${v}`);
34
+ }
35
+ const seed = secrets.FASTAGENT_AUTH_SEED ?? "";
36
+ for (let i = 0; i < AUTH_SEED_MAX_CHUNKS; i++) {
37
+ const param = i === 0 ? "FastagentAuthSeed" : `FastagentAuthSeed${i + 1}`;
38
+ params.push(`${param}=${seed.slice(i * AUTH_SEED_CHUNK_SIZE, (i + 1) * AUTH_SEED_CHUNK_SIZE)}`);
39
+ }
40
+ return `${JSON.stringify(params)}\n`;
41
+ }
42
+ /** Parse the runtime's checkpoint acknowledgement without treating malformed output as success. */
43
+ export function parseCheckpointReply(stdout) {
44
+ try {
45
+ const parsed = JSON.parse(stdout.trim());
46
+ if (parsed === null ||
47
+ typeof parsed !== "object" ||
48
+ !("written" in parsed) ||
49
+ typeof parsed.written !== "boolean") {
50
+ return undefined;
51
+ }
52
+ return {
53
+ written: parsed.written,
54
+ reason: "reason" in parsed && typeof parsed.reason === "string" ? parsed.reason : undefined,
55
+ };
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ /**
62
+ * Run the deploy through `aws` + `docker`. `log` reports progress; the injected registrars perform
63
+ * post-deploy webhook steps from the builder machine against the forwarder's Function URL. Every
64
+ * gate is fail-visible; `writeSecretFile` is the caller's 0600-temp-file seam (see the header).
65
+ */
66
+ export async function deployAgentcoreRun(plan, aws, docker, log, writeSecretFile, writeForwarderZip, registerTelegram, registerFeishu, registerSlack) {
67
+ const gate = (g) => ({ ok: false, gate: g });
68
+ const stack = `fastagent-${plan.name}`;
69
+ const repo = `fastagent/${plan.name}`;
70
+ // 1. Identity + region — the two facts everything downstream (registry hostname, stack region)
71
+ // hangs on. `sts get-caller-identity` succeeds with any working credential source.
72
+ const identity = await aws(["sts", "get-caller-identity", "--output", "json"], { capture: true });
73
+ if (identity.code === 127) {
74
+ return gate("aws CLI not found — install AWS CLI v2: https://docs.aws.amazon.com/cli/, then re-run");
75
+ }
76
+ if (identity.code !== 0) {
77
+ return gate("no working AWS credentials — run `aws configure` (or set AWS_ACCESS_KEY_ID/…), then re-run");
78
+ }
79
+ let account;
80
+ try {
81
+ const parsed = JSON.parse(identity.stdout);
82
+ if (typeof parsed.Account !== "string")
83
+ throw new Error("no Account");
84
+ account = parsed.Account;
85
+ }
86
+ catch {
87
+ return gate("could not read the account id from `aws sts get-caller-identity` — see the output above");
88
+ }
89
+ let region = plan.region;
90
+ if (!region) {
91
+ const fromConfig = await aws(["configure", "get", "region"], { capture: true });
92
+ region = fromConfig.stdout.trim() || undefined;
93
+ }
94
+ if (!region) {
95
+ return gate("no AWS region configured — set AWS_REGION (or `aws configure set region <region>`), then re-run");
96
+ }
97
+ // 2. Docker + buildx — this host builds LOCALLY (linux/arm64 into the account's ECR; AgentCore has
98
+ // no remote builder), so their absence is a first-class gate with the install pointer. ANY
99
+ // non-zero gates BEFORE side effects: `docker version` with the daemon down exits non-127, and
100
+ // letting it through would create the ECR repo and then fail the build with a generic error.
101
+ const dockerVersion = await docker(["version"], { capture: true });
102
+ if (dockerVersion.code === 127) {
103
+ return gate("docker not found — install Docker (https://docs.docker.com/get-docker/), then re-run");
104
+ }
105
+ if (dockerVersion.code !== 0) {
106
+ return gate("docker daemon not reachable — start Docker Desktop (or fix your docker context), then re-run");
107
+ }
108
+ if ((await docker(["buildx", "version"], { capture: true })).code !== 0) {
109
+ return gate("docker buildx not available — the image must be linux/arm64 (cross-built); install buildx, then re-run");
110
+ }
111
+ // 3. Gate missing required secret VALUES before any side effect (no half-created infra).
112
+ if (plan.missingSecrets.length > 0) {
113
+ return gate(`no local value for: ${plan.missingSecrets.join(", ")} — set them in .env (or the environment) and re-run`);
114
+ }
115
+ // 3b. AgentCore env values cap at 2048 chars. The auth seed is chunked (paramsFileContent) up to
116
+ // its ceiling; any OTHER oversized value has no chunk lane — gate it instead of a cryptic
117
+ // CloudFormation "maxLength" failure mid-deploy.
118
+ const seed = plan.secrets.FASTAGENT_AUTH_SEED;
119
+ if (seed && seed.length > AUTH_SEED_CHUNK_SIZE * AUTH_SEED_MAX_CHUNKS) {
120
+ return gate(`your auth.json is too large to carry (${seed.length} chars base64 > ${AUTH_SEED_CHUNK_SIZE * AUTH_SEED_MAX_CHUNKS}) — ` +
121
+ `slim it (keep only the model's credential), or set a provider API key in .env instead`);
122
+ }
123
+ for (const [k, v] of Object.entries(plan.secrets)) {
124
+ if (k !== "FASTAGENT_AUTH_SEED" && v.length > 2048) {
125
+ return gate(`secret ${k} is ${v.length} chars — AgentCore environment values cap at 2048; shorten it`);
126
+ }
127
+ }
128
+ // 4. ECR repository — check-then-act. A FAILED describe that isn't "not found" would misreport the
129
+ // create, but ECR's not-found also exits non-zero — so try describe, and on failure attempt the
130
+ // create; a create failing for a REAL reason (permissions) still gates with its own message.
131
+ const registry = `${account}.dkr.ecr.${region}.amazonaws.com`;
132
+ const image = `${registry}/${repo}:${plan.tag}`;
133
+ const described = await aws(["ecr", "describe-repositories", "--repository-names", repo], { capture: true });
134
+ if (described.code === 0) {
135
+ log(`ECR repository ${repo} exists — skipping create`);
136
+ }
137
+ else {
138
+ log(`creating ECR repository ${repo}…`);
139
+ if ((await aws(["ecr", "create-repository", "--repository-name", repo])).code !== 0) {
140
+ return gate("`aws ecr create-repository` failed — see the output above; fix and re-run");
141
+ }
142
+ }
143
+ // 4b. The deployment bucket + the forwarder package. The bucket is created OUTSIDE the stack, on
144
+ // purpose and unlike everything else here: it holds the agent's STATE SNAPSHOT, and AgentCore
145
+ // wipes the /mnt/state mount on every runtime version update (i.e. every deploy). Keeping it
146
+ // out of CloudFormation means a `delete-stack` — or a rolled-back create — cannot take the
147
+ // agent's sessions, channel state and pending wake-ups with it.
148
+ let forwarderParams;
149
+ if (plan.needsForwarder) {
150
+ const bucket = stateBucketName(plan.name, account);
151
+ if ((await aws(["s3api", "head-bucket", "--bucket", bucket], { capture: true })).code !== 0) {
152
+ log(`creating deployment bucket ${bucket}…`);
153
+ // us-east-1 is the ONE region that must not carry a LocationConstraint (the API rejects it).
154
+ const createArgs = ["s3api", "create-bucket", "--bucket", bucket];
155
+ if (region !== "us-east-1")
156
+ createArgs.push("--create-bucket-configuration", `LocationConstraint=${region}`);
157
+ if ((await aws(createArgs)).code !== 0) {
158
+ return gate(`\`aws s3api create-bucket --bucket ${bucket}\` failed — see the output above; fix and re-run`);
159
+ }
160
+ }
161
+ // CONVERGE the properties on EVERY deploy, not just at creation. They are what makes the bucket
162
+ // safe (nothing public) and recoverable (a bad write is not the end of the agent's memory); doing
163
+ // them only in the create branch means a run that failed halfway leaves a bucket that looks
164
+ // finished forever after, and ignoring the exit codes means "deployed" would be reported over a
165
+ // world-readable or unversioned store of the agent's credentials.
166
+ const converge = [
167
+ {
168
+ label: "block public access",
169
+ args: [
170
+ "s3api",
171
+ "put-public-access-block",
172
+ "--bucket",
173
+ bucket,
174
+ "--public-access-block-configuration",
175
+ "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true",
176
+ ],
177
+ },
178
+ {
179
+ label: "enable versioning",
180
+ args: ["s3api", "put-bucket-versioning", "--bucket", bucket, "--versioning-configuration", "Status=Enabled"],
181
+ },
182
+ {
183
+ label: "set the snapshot lifecycle",
184
+ args: [
185
+ "s3api",
186
+ "put-bucket-lifecycle-configuration",
187
+ "--bucket",
188
+ bucket,
189
+ "--lifecycle-configuration",
190
+ JSON.stringify({
191
+ Rules: [
192
+ {
193
+ ID: "fastagent-expire-old-snapshots",
194
+ Status: "Enabled",
195
+ Filter: { Prefix: "state/" },
196
+ NoncurrentVersionExpiration: { NoncurrentDays: 7 },
197
+ },
198
+ ],
199
+ }),
200
+ ],
201
+ },
202
+ ];
203
+ for (const step of converge) {
204
+ // `capture` keeps the CLI's JSON off the deploy log: the exit code is the signal, and a failure
205
+ // gates with the step's name below.
206
+ if ((await aws(step.args, { capture: true })).code !== 0) {
207
+ return gate(`could not ${step.label} on ${bucket} — refusing to store agent state in it; fix and re-run`);
208
+ }
209
+ }
210
+ // Content-hashed key: CloudFormation rolls the function only when a parameter VALUE changes, so
211
+ // identical source must map to an identical key (hence the deterministic zip) and changed source
212
+ // to a new one.
213
+ const zip = zipSingleFile("index.js", Buffer.from(forwarderSource()));
214
+ const key = `forwarder/${createHash("sha256").update(zip).digest("hex").slice(0, 16)}.zip`;
215
+ const zipPath = await writeForwarderZip(zip);
216
+ if ((await aws(["s3", "cp", zipPath, `s3://${bucket}/${key}`])).code !== 0) {
217
+ return gate("uploading the forwarder package to S3 failed — see the output above; fix and re-run");
218
+ }
219
+ forwarderParams = { bucket, key };
220
+ }
221
+ // 5. Registry login — the password flows stdout→stdin between the two runners, never argv.
222
+ const password = await aws(["ecr", "get-login-password"], { capture: true });
223
+ if (password.code !== 0)
224
+ return gate("`aws ecr get-login-password` failed — see the output above");
225
+ if ((await docker(["login", "--username", "AWS", "--password-stdin", registry], { input: password.stdout })).code !== 0) {
226
+ return gate("`docker login` to ECR failed — see the output above");
227
+ }
228
+ // 6. Build (linux/arm64) + push in one step.
229
+ log(`building + pushing ${image} (linux/arm64)…`);
230
+ const buildArgs = ["buildx", "build", "--platform", "linux/arm64", "-t", image, "--push"];
231
+ if (plan.dockerfilePath)
232
+ buildArgs.push("-f", plan.dockerfilePath);
233
+ buildArgs.push(".");
234
+ if ((await docker(buildArgs)).code !== 0) {
235
+ return gate("`docker buildx build` failed — see the output above; fix and re-run");
236
+ }
237
+ // 7. Deploy the stack. Secret values ride the temp params file (file://), never argv.
238
+ // --no-fail-on-empty-changeset: a re-run whose only change already applied must not gate.
239
+ // Self-heal the one un-resumable state first: a FAILED first create leaves the stack in
240
+ // ROLLBACK_COMPLETE, which CloudFormation refuses to update — without this, "fix and re-run"
241
+ // (our own gate advice) would dead-end on a different error. Nothing real is lost by deleting:
242
+ // a ROLLBACK_COMPLETE stack holds no live resources.
243
+ const status = await aws([
244
+ "cloudformation",
245
+ "describe-stacks",
246
+ "--stack-name",
247
+ stack,
248
+ "--query",
249
+ "Stacks[0].StackStatus",
250
+ "--output",
251
+ "text",
252
+ ], { capture: true });
253
+ if (status.code === 0 && status.stdout.trim() === "ROLLBACK_COMPLETE") {
254
+ log(`stack ${stack} is ROLLBACK_COMPLETE (a failed first create) — deleting it before re-creating…`);
255
+ if ((await aws(["cloudformation", "delete-stack", "--stack-name", stack])).code !== 0) {
256
+ return gate("`aws cloudformation delete-stack` failed — see the output above");
257
+ }
258
+ if ((await aws(["cloudformation", "wait", "stack-delete-complete", "--stack-name", stack])).code !== 0) {
259
+ return gate("waiting for the stack delete failed — see the output above; re-run once it is gone");
260
+ }
261
+ }
262
+ log(`deploying stack ${stack}…`);
263
+ const paramsPath = await writeSecretFile(paramsFileContent(image, plan.secrets, forwarderParams));
264
+ const deployed = await aws([
265
+ "cloudformation",
266
+ "deploy",
267
+ "--stack-name",
268
+ stack,
269
+ "--template-file",
270
+ plan.templatePath,
271
+ "--capabilities",
272
+ "CAPABILITY_IAM",
273
+ "--no-fail-on-empty-changeset",
274
+ "--parameter-overrides",
275
+ `file://${paramsPath}`,
276
+ ]);
277
+ if (deployed.code !== 0) {
278
+ return gate("`aws cloudformation deploy` failed — inspect the stack events " +
279
+ `(aws cloudformation describe-stack-events --stack-name ${stack}), fix, and re-run`);
280
+ }
281
+ // 8. Outputs — the runtime ARN (the data plane) and the forwarder URL (the webhook surface).
282
+ const outputsQuery = await aws(["cloudformation", "describe-stacks", "--stack-name", stack, "--query", "Stacks[0].Outputs", "--output", "json"], { capture: true });
283
+ if (outputsQuery.code !== 0)
284
+ return gate("`aws cloudformation describe-stacks` failed — see the output above");
285
+ const outputs = parseStackOutputs(outputsQuery.stdout);
286
+ const runtimeArn = outputs.RuntimeArn;
287
+ if (!runtimeArn)
288
+ return gate("stack has no RuntimeArn output — was the template edited? Regenerate with --force");
289
+ const url = outputs.ForwarderUrl?.replace(/\/$/, ""); // registrars append /<path>; no double slash
290
+ // 8b. Restart the ingress session so the new image serves IMMEDIATELY. A live session keeps its
291
+ // old compute until the idle timeout or the max compute lifetime (8 h) — without this, a
292
+ // redeploy "succeeds" while an actively-chatting session keeps answering from the PREVIOUS
293
+ // image (the exact silent trap the first real deploy hit). Failure is advisory, never a gate:
294
+ // on a first deploy the session does not exist yet, and the stop is an immediacy optimization
295
+ // — the platform's reclaim gets there eventually. An in-flight turn on the old compute is cut;
296
+ // the checkpoint above is what lets a replaying channel re-run it. Only when a forwarder exists
297
+ // (the ingress session is the forwarder's session; pure-invoke deployments have none).
298
+ // Keyed on the FORWARDER: every current forwarder has a callback URL for state-capability refresh,
299
+ // and every forwarder topology has an ingress session whose next event would otherwise land on compute still
300
+ // running the previous image.
301
+ if (plan.needsForwarder) {
302
+ // CHECKPOINT FIRST. The stop cuts whatever turn is running, and that turn's durable intent was
303
+ // written to a mount the version update erases — so without this flush "replay re-runs it" would
304
+ // be false: the intent never reaches S3 and the message is simply gone. Best-effort: a session
305
+ // that is not up has nothing to lose, and a failure here must not block the (already applied)
306
+ // deploy — it only downgrades the promise, so say so.
307
+ const checkpointPayloadPath = await writeSecretFile(`${JSON.stringify({ kind: "checkpoint", auth: plan.secrets.FASTAGENT_INGRESS_SECRET })}\n`);
308
+ const checkpoint = await aws([
309
+ "bedrock-agentcore",
310
+ "invoke-agent-runtime",
311
+ "--agent-runtime-arn",
312
+ runtimeArn,
313
+ "--runtime-session-id",
314
+ ingressSessionId(plan.name),
315
+ "--payload",
316
+ `file://${checkpointPayloadPath}`,
317
+ "--cli-binary-format",
318
+ "raw-in-base64-out",
319
+ "/dev/stdout",
320
+ ], { capture: true, captureStderr: true });
321
+ // Report what the container ACTUALLY did. This line is the only signal an operator has about
322
+ // whether an in-flight turn survived the deploy, so a blanket "checkpointed" — printed even when
323
+ // nothing was written — would be worse than no line at all.
324
+ if (checkpoint.code !== 0) {
325
+ log("note: could not reach the ingress session to checkpoint — if a turn was in flight it is lost " +
326
+ "rather than replayed (see the output above)");
327
+ }
328
+ else {
329
+ const reply = parseCheckpointReply(checkpoint.stdout);
330
+ if (reply?.written) {
331
+ log("checkpointed the ingress session (an interrupted turn can be replayed)");
332
+ }
333
+ else if (reply) {
334
+ // The ordinary case: the session was already idle-reclaimed, so its snapshot was written when
335
+ // it settled and there is nothing in flight to lose.
336
+ log(`note: nothing to checkpoint${reply.reason ? ` — ${reply.reason}` : " (no session was running)"}`);
337
+ }
338
+ else {
339
+ log("warn: ingress session returned an invalid checkpoint response — could not verify the state snapshot");
340
+ }
341
+ }
342
+ log("stopping the ingress session so the new image serves immediately…");
343
+ const stopCommand = [
344
+ "bedrock-agentcore",
345
+ "stop-runtime-session",
346
+ "--agent-runtime-arn",
347
+ runtimeArn,
348
+ "--runtime-session-id",
349
+ ingressSessionId(plan.name),
350
+ ];
351
+ const stopped = await aws(stopCommand, { capture: true, captureStderr: true });
352
+ if (stopped.code !== 0) {
353
+ // Classify, don't guess: "no session yet" (first deploy — expected, quiet note) vs a REAL stop
354
+ // failure (permissions/CLI/network — the old image may keep serving, say so loudly with the
355
+ // manual command). Not a gate: the deploy itself succeeded, and stop is an immediacy
356
+ // optimization — the platform's reclaim converges regardless.
357
+ const stderr = stopped.stderr ?? "";
358
+ if (/ResourceNotFound|not\s*found|does not exist/i.test(stderr)) {
359
+ log("note: no ingress session to stop (first deploy, or already reclaimed)");
360
+ }
361
+ else {
362
+ log(`warn: could not stop the ingress session — an ACTIVE session may keep serving the PREVIOUS ` +
363
+ `image until reclaimed (idle timeout / 8 h ceiling). Stop it manually: aws ${stopCommand.join(" ")}`);
364
+ const firstLine = stderr.trim().split("\n")[0];
365
+ if (firstLine)
366
+ log(`warn: ${firstLine}`);
367
+ }
368
+ }
369
+ }
370
+ // 9. Post-deploy webhook registration — same registrar seam as every host, pointed at the
371
+ // forwarder's Function URL. Gate policy is the shared registration-gate kernel.
372
+ if (plan.channels.length > 0 && !url) {
373
+ return gate("channels are declared but the stack has no ForwarderUrl output — regenerate the template with --force");
374
+ }
375
+ const reg = registrationGate(log, "re-run to retry registration (steps already done are skipped)");
376
+ if (url) {
377
+ if (plan.channels.includes("telegram")) {
378
+ log("registering telegram webhook…");
379
+ reg.track("telegram", await registerTelegram(url));
380
+ }
381
+ if (plan.channels.includes("github")) {
382
+ log(`github: set the webhook in the repo (Settings → Webhooks) → ${url}/webhook`);
383
+ reg.track("github", "manual"); // always a human step — re-surfaced after the registrar output
384
+ }
385
+ if (plan.channels.includes("slack")) {
386
+ if (registerSlack) {
387
+ log("registering slack event URL…");
388
+ reg.track("slack", await registerSlack(url));
389
+ }
390
+ else {
391
+ log(`slack: set Event Subscriptions → Request URL → ${url}/slack`);
392
+ reg.track("slack", "manual");
393
+ }
394
+ }
395
+ for (const kind of ["feishu", "lark"]) {
396
+ if (!plan.channels.includes(kind))
397
+ continue;
398
+ if (registerFeishu) {
399
+ log(`registering ${kind} event URL…`);
400
+ reg.track(kind, await registerFeishu(url, kind));
401
+ }
402
+ else {
403
+ log(`${kind}: set the event Request URL (developer console → Events & Callbacks) → ${url}/${kind}`);
404
+ reg.track(kind, "manual");
405
+ }
406
+ }
407
+ }
408
+ const registrationGateMsg = reg.gate();
409
+ if (registrationGateMsg)
410
+ return gate(registrationGateMsg);
411
+ return { ok: true, runtimeArn, url };
412
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A minimal, DETERMINISTIC single-file ZIP writer — just enough to package the forwarder Lambda.
3
+ *
4
+ * WHY NOT A DEPENDENCY: this runs in the deploy path of a tool whose whole promise is "no build
5
+ * step"; pulling an archiver in for ~40 lines of well-specified format would be the wrong trade.
6
+ * WHY NOT `zip(1)`: it is absent on plenty of machines (and on Windows), and its output embeds the
7
+ * current time — the forwarder's S3 key is CONTENT-HASHED, so identical source must produce
8
+ * identical bytes or every deploy would look like a code change to CloudFormation.
9
+ *
10
+ * STORE (no compression) on purpose: the payload is a few KB of JavaScript, and the format's stored
11
+ * form is trivially verifiable. Fixed 1980-01-01 timestamps keep the output byte-stable.
12
+ */
13
+ import { Buffer } from "node:buffer";
14
+ /**
15
+ * Build a ZIP archive containing exactly one stored entry. Byte-identical for identical inputs.
16
+ */
17
+ export declare function zipSingleFile(name: string, content: Buffer): Buffer;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * A minimal, DETERMINISTIC single-file ZIP writer — just enough to package the forwarder Lambda.
3
+ *
4
+ * WHY NOT A DEPENDENCY: this runs in the deploy path of a tool whose whole promise is "no build
5
+ * step"; pulling an archiver in for ~40 lines of well-specified format would be the wrong trade.
6
+ * WHY NOT `zip(1)`: it is absent on plenty of machines (and on Windows), and its output embeds the
7
+ * current time — the forwarder's S3 key is CONTENT-HASHED, so identical source must produce
8
+ * identical bytes or every deploy would look like a code change to CloudFormation.
9
+ *
10
+ * STORE (no compression) on purpose: the payload is a few KB of JavaScript, and the format's stored
11
+ * form is trivially verifiable. Fixed 1980-01-01 timestamps keep the output byte-stable.
12
+ */
13
+ import { Buffer } from "node:buffer";
14
+ import { crc32 } from "node:zlib";
15
+ /** MS-DOS epoch (1980-01-01 00:00:00) — the earliest the format can express, and a constant. */
16
+ const DOS_DATE = 0x0021;
17
+ const DOS_TIME = 0x0000;
18
+ /**
19
+ * Build a ZIP archive containing exactly one stored entry. Byte-identical for identical inputs.
20
+ */
21
+ export function zipSingleFile(name, content) {
22
+ const nameBytes = Buffer.from(name, "utf8");
23
+ const sum = crc32(content);
24
+ const localHeader = Buffer.alloc(30);
25
+ localHeader.writeUInt32LE(0x04034b50, 0); // local file header signature
26
+ localHeader.writeUInt16LE(20, 4); // version needed to extract (2.0)
27
+ localHeader.writeUInt16LE(0, 6); // general purpose flags
28
+ localHeader.writeUInt16LE(0, 8); // method: 0 = stored
29
+ localHeader.writeUInt16LE(DOS_TIME, 10);
30
+ localHeader.writeUInt16LE(DOS_DATE, 12);
31
+ localHeader.writeUInt32LE(sum, 14);
32
+ localHeader.writeUInt32LE(content.byteLength, 18); // compressed size (= raw, stored)
33
+ localHeader.writeUInt32LE(content.byteLength, 22); // uncompressed size
34
+ localHeader.writeUInt16LE(nameBytes.byteLength, 26);
35
+ localHeader.writeUInt16LE(0, 28); // extra field length
36
+ const centralHeader = Buffer.alloc(46);
37
+ centralHeader.writeUInt32LE(0x02014b50, 0); // central directory header signature
38
+ centralHeader.writeUInt16LE(20, 4); // version made by
39
+ centralHeader.writeUInt16LE(20, 6); // version needed to extract
40
+ centralHeader.writeUInt16LE(0, 8);
41
+ centralHeader.writeUInt16LE(0, 10);
42
+ centralHeader.writeUInt16LE(DOS_TIME, 12);
43
+ centralHeader.writeUInt16LE(DOS_DATE, 14);
44
+ centralHeader.writeUInt32LE(sum, 16);
45
+ centralHeader.writeUInt32LE(content.byteLength, 20);
46
+ centralHeader.writeUInt32LE(content.byteLength, 24);
47
+ centralHeader.writeUInt16LE(nameBytes.byteLength, 28);
48
+ centralHeader.writeUInt16LE(0, 30); // extra field length
49
+ centralHeader.writeUInt16LE(0, 32); // file comment length
50
+ centralHeader.writeUInt16LE(0, 34); // disk number start
51
+ centralHeader.writeUInt16LE(0, 36); // internal attributes
52
+ // External attributes: regular file, rw-r--r-- in the high 16 bits. `>>> 0` because JS bitwise
53
+ // operators are 32-bit SIGNED — the shifted value is negative without it.
54
+ centralHeader.writeUInt32LE((0o100644 << 16) >>> 0, 38);
55
+ centralHeader.writeUInt32LE(0, 42); // local header offset (single entry: always 0)
56
+ const centralSize = centralHeader.byteLength + nameBytes.byteLength;
57
+ const centralOffset = localHeader.byteLength + nameBytes.byteLength + content.byteLength;
58
+ const end = Buffer.alloc(22);
59
+ end.writeUInt32LE(0x06054b50, 0); // end of central directory signature
60
+ end.writeUInt16LE(0, 4); // this disk
61
+ end.writeUInt16LE(0, 6); // disk with central directory
62
+ end.writeUInt16LE(1, 8); // entries on this disk
63
+ end.writeUInt16LE(1, 10); // total entries
64
+ end.writeUInt32LE(centralSize, 12);
65
+ end.writeUInt32LE(centralOffset, 16);
66
+ end.writeUInt16LE(0, 20); // comment length
67
+ return Buffer.concat([localHeader, nameBytes, content, centralHeader, nameBytes, end]);
68
+ }
@@ -1,9 +1,3 @@
1
- /**
2
- * The portable container — Dockerfile + .dockerignore, host-neutral. Every deploy target ships these
3
- * unchanged; only the host config (fly.toml / railway.json) and the CLI sequence differ. The image
4
- * runs `fastagent start /app` on `$PORT` with state under `$FASTAGENT_STATE_DIR` — standard container
5
- * conventions, no host coupling. Extracted from the Fly plan when Railway became the second target.
6
- */
7
1
  export interface Artifact {
8
2
  path: string;
9
3
  content: string;
@@ -17,8 +11,10 @@ export declare const GENERATED_DOCKERFILE_MARKER = "# Generated by `fastagent de
17
11
  * Both the "keep vs flag-as-stale" decision and the apt warning share this; a split would let them
18
12
  * disagree on the same file (e.g. a marker not at byte 0). */
19
13
  export declare function isGeneratedDockerfile(content: string): boolean;
14
+ /** Did fastagent generate this `.dockerignore`? (Same ownership rule as {@link isGeneratedDockerfile}.) */
15
+ export declare function isGeneratedDockerignore(content: string): boolean;
20
16
  export interface ContainerInput {
21
- /** Whether the workspace has a package.json (a code workspace); else a pure markdown/skills agent. */
17
+ /** Whether the agent has a package.json (a code agent); else a pure markdown/skills agent. */
22
18
  hasPackageJson: boolean;
23
19
  /** The package manager the generated image targets: `bun` (packageManager: bun / a bun lockfile) gets an
24
20
  * `oven/bun` base + `bun install` + `bun run fastagent`; otherwise npm on `node:22-slim`. */
@@ -32,25 +28,30 @@ export interface ContainerInput {
32
28
  version: string;
33
29
  /** Extra apt packages (fastagent.config deploy.apt) baked in for the agent's tools — git, ripgrep, …. */
34
30
  apt?: string[];
35
- /**
36
- * Repo-as-workspace layout: the agent kit's subdirectory relative to the repo root (e.g. "agent"
37
- * `config.agentDir` resolved). When set, the image bakes the WHOLE repo as the agent's cwd (§11 bake
38
- * decision), installs the KIT's deps (not the host's the agent installs those in its workspace at
39
- * runtime when its job needs them), ships `.git` (write-back via commit/push needs it), and the
40
- * artifacts are namespaced under the kit so they never collide with the host repo's own
41
- * Dockerfile/.dockerignore. The runtime facts (hasPackageJson/runtime/hasLockfile) describe the KIT.
42
- */
43
- kitDir?: string;
31
+ /** Where deploy's artifacts and the agent's own files sit, relative to the BUILD CONTEXT (which is
32
+ * always the workspace): the agent directory's name plus a slash when it sits INSIDE the workspace,
33
+ * `""` when the agent IS the workspace. ONE derived value an earlier shape branched on a `nested`
34
+ * boolean at fifteen separate sites, and each site was a chance for the two to disagree. */
35
+ agentPrefix: string;
36
+ /** Workspace-relative machinery paths the NAME-based excludes below would miss a
37
+ * `FASTAGENT_SECRETS_DIR`/`FASTAGENT_STATE_DIR` pointed at an in-tree directory not called
38
+ * `.secrets`/`.state`. Preflight resolves them; excluding by PATH is what keeps `COPY . .` from
39
+ * baking a credential (or the build machine's sessions). */
40
+ machineryPaths?: string[];
41
+ /** Whether the baked workspace ships a `.git` (preflight fact). When true, preflight has already
42
+ * merged "git" into `apt` (the write-back loop needs history + binary together); the plans word
43
+ * their runbook's freshness/write-back guidance from the same fact. */
44
+ shipsGit?: boolean;
44
45
  }
45
46
  /**
46
- * The Dockerfile + ignore artifacts — spread into any host's artifact list. Kit layout ({@link
47
- * ContainerInput.kitDir}): the Dockerfile is namespaced under the kit (`agent/Dockerfile`) so it never
48
- * collides with the host repo's own. The ignore ships in TWO forms because context packing is
49
- * host-CLI-owned and inconsistent: (1) a ROOT `.dockerignore` the only form flyctl/railway's own
50
- * context packers reliably read (kept if the host already has one preflight then warns specifically
51
- * about a .git exclude / missing recursive node_modules)and (2) a
52
- * per-Dockerfile `agent/Dockerfile.dockerignore` for plain docker/buildx builds. Whether `.git`
53
- * actually reaches the box is host-CLI-dependent (some strip it from the upload regardless of any
54
- * ignore file) — the runbook's write-back note carries the runtime-clone fallback.
47
+ * The Dockerfile + ignore artifacts — spread into any host's artifact list. Everything is prefixed with
48
+ * {@link ContainerInput.agentPrefix}, so a NESTED agent's Dockerfile lands under `fastagent/` (never
49
+ * colliding with one the workspace already owns) while an agent that IS the workspace lands it at the root, where it IS
50
+ * the agent's own file. The ignore ships in TWO forms because context packing is host-CLI-owned and
51
+ * inconsistent: (1) a ROOT `.dockerignore` the only form flyctl/railway's own context packers
52
+ * reliably read (kept if the workspace already has one preflight then checks the machinery/secret
53
+ * excludes it must carry) — and (2) a per-Dockerfile `Dockerfile.dockerignore` for plain docker/buildx
54
+ * builds. When the agent sits inside the workspace that root file is the ONE write deploy makes outside the agent dir, and
55
+ * only at deploy time without it the host CLI's packer would bake `.secrets/` into the image.
55
56
  */
56
57
  export declare function containerArtifacts(input: ContainerInput): Artifact[];