@fastagent-sh/fastagent 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +5 -1
  2. package/dist/channels/agentcore-protocol.d.ts +112 -0
  3. package/dist/channels/agentcore-protocol.js +22 -0
  4. package/dist/channels/agentcore-service.d.ts +0 -4
  5. package/dist/channels/agentcore-service.js +1 -6
  6. package/dist/channels/agentcore-state.d.ts +5 -11
  7. package/dist/channels/agentcore-state.js +4 -1
  8. package/dist/channels/agentcore.d.ts +1 -67
  9. package/dist/channels/agentcore.js +90 -89
  10. package/dist/channels/control.d.ts +1 -1
  11. package/dist/channels/control.js +13 -62
  12. package/dist/channels/discover.d.ts +0 -1
  13. package/dist/channels/discover.js +1 -3
  14. package/dist/channels/feishu/context-buffer.d.ts +6 -0
  15. package/dist/channels/feishu/context-buffer.js +0 -38
  16. package/dist/channels/feishu/crypto.d.ts +0 -2
  17. package/dist/channels/feishu/crypto.js +3 -8
  18. package/dist/channels/feishu/feishu-api.js +2 -4
  19. package/dist/channels/feishu/feishu.js +56 -115
  20. package/dist/channels/feishu/parse.d.ts +4 -3
  21. package/dist/channels/feishu/parse.js +5 -4
  22. package/dist/channels/feishu/register-webhook.d.ts +5 -5
  23. package/dist/channels/feishu/register-webhook.js +43 -57
  24. package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
  25. package/dist/channels/feishu/setup-mode.d.ts +30 -0
  26. package/dist/channels/feishu/setup-mode.js +26 -0
  27. package/dist/channels/feishu/shared-api.d.ts +10 -0
  28. package/dist/channels/feishu/shared-api.js +38 -0
  29. package/dist/channels/http.d.ts +0 -8
  30. package/dist/channels/http.js +4 -56
  31. package/dist/channels/kit/turn-runner.d.ts +59 -0
  32. package/dist/channels/kit/turn-runner.js +84 -0
  33. package/dist/channels/lark/scaffold/lark-send.ts +12 -67
  34. package/dist/channels/registration.d.ts +36 -1
  35. package/dist/channels/registration.js +57 -1
  36. package/dist/channels/secret.d.ts +1 -0
  37. package/dist/channels/secret.js +16 -0
  38. package/dist/channels/slack/config-api.d.ts +35 -3
  39. package/dist/channels/slack/config-api.js +51 -9
  40. package/dist/channels/slack/manifest.js +5 -1
  41. package/dist/channels/slack/onboard.d.ts +5 -5
  42. package/dist/channels/slack/onboard.js +41 -20
  43. package/dist/channels/slack/register-webhook.d.ts +3 -2
  44. package/dist/channels/slack/register-webhook.js +34 -14
  45. package/dist/channels/slack/scaffold/channel.ts +3 -10
  46. package/dist/channels/slack/scaffold/slack-send.ts +18 -126
  47. package/dist/channels/slack/shared-api.d.ts +10 -0
  48. package/dist/channels/slack/shared-api.js +34 -0
  49. package/dist/channels/slack/slack-api.d.ts +20 -2
  50. package/dist/channels/slack/slack-api.js +81 -22
  51. package/dist/channels/slack/slack.d.ts +0 -10
  52. package/dist/channels/slack/slack.js +60 -99
  53. package/dist/channels/sse.d.ts +4 -0
  54. package/dist/channels/sse.js +66 -0
  55. package/dist/channels/telegram/register-webhook.d.ts +6 -9
  56. package/dist/channels/telegram/register-webhook.js +44 -42
  57. package/dist/channels/telegram/telegram.js +47 -142
  58. package/dist/channels/wait-health.js +7 -4
  59. package/dist/cli/add-feishu.js +3 -10
  60. package/dist/cli/add-slack.js +7 -20
  61. package/dist/cli/commands/add.d.ts +0 -1
  62. package/dist/cli/commands/add.js +22 -31
  63. package/dist/cli/commands/chat.js +6 -12
  64. package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
  65. package/dist/cli/commands/deploy/agentcore.js +178 -0
  66. package/dist/cli/commands/deploy/docker.d.ts +2 -0
  67. package/dist/cli/commands/deploy/docker.js +119 -0
  68. package/dist/cli/commands/deploy/fly.d.ts +2 -0
  69. package/dist/cli/commands/deploy/fly.js +131 -0
  70. package/dist/cli/commands/deploy/railway.d.ts +2 -0
  71. package/dist/cli/commands/deploy/railway.js +71 -0
  72. package/dist/cli/commands/deploy/shared.d.ts +114 -0
  73. package/dist/cli/commands/deploy/shared.js +124 -0
  74. package/dist/cli/commands/deploy.d.ts +17 -49
  75. package/dist/cli/commands/deploy.js +39 -618
  76. package/dist/cli/commands/dev.js +17 -46
  77. package/dist/cli/commands/fire.js +6 -12
  78. package/dist/cli/commands/info.js +2 -1
  79. package/dist/cli/commands/invoke.js +4 -11
  80. package/dist/cli/commands/schedule.js +1 -1
  81. package/dist/cli/commands/start.js +17 -46
  82. package/dist/cli/commands/tool.js +3 -8
  83. package/dist/cli/kernel.d.ts +0 -2
  84. package/dist/cli/kernel.js +0 -2
  85. package/dist/cli/program.js +7 -12
  86. package/dist/cli/serve.d.ts +36 -19
  87. package/dist/cli/serve.js +91 -7
  88. package/dist/cli/shared.d.ts +13 -18
  89. package/dist/cli/shared.js +20 -4
  90. package/dist/deploy/agentcore/forwarder.js +250 -0
  91. package/dist/deploy/agentcore/plan.d.ts +23 -2
  92. package/dist/deploy/agentcore/plan.js +29 -244
  93. package/dist/deploy/agentcore/run.d.ts +4 -20
  94. package/dist/deploy/agentcore/run.js +22 -7
  95. package/dist/deploy/channel-ingress.js +1 -1
  96. package/dist/deploy/docker/run.d.ts +17 -3
  97. package/dist/deploy/docker/run.js +30 -8
  98. package/dist/deploy/fly/plan.js +7 -0
  99. package/dist/deploy/fly/run.d.ts +27 -0
  100. package/dist/deploy/fly/run.js +102 -20
  101. package/dist/deploy/hosts.d.ts +5 -0
  102. package/dist/deploy/hosts.js +4 -0
  103. package/dist/deploy/preflight.js +3 -3
  104. package/dist/deploy/railway/plan.d.ts +5 -0
  105. package/dist/deploy/railway/plan.js +7 -0
  106. package/dist/deploy/railway/run.d.ts +2 -1
  107. package/dist/deploy/railway/run.js +5 -4
  108. package/dist/deploy/secrets.js +0 -17
  109. package/dist/engines/pi/agent-session-factory.d.ts +37 -17
  110. package/dist/engines/pi/agent-session-factory.js +109 -85
  111. package/dist/engines/pi/config.d.ts +1 -1
  112. package/dist/engines/pi/create.d.ts +22 -39
  113. package/dist/engines/pi/create.js +58 -73
  114. package/dist/engines/pi/definition.js +8 -8
  115. package/dist/engines/pi/invoke-session.js +13 -30
  116. package/dist/engines/pi/open.d.ts +2 -2
  117. package/dist/engines/pi/open.js +31 -41
  118. package/dist/engines/pi/retry-event.d.ts +6 -0
  119. package/dist/engines/pi/retry-event.js +15 -0
  120. package/dist/engines/pi/session-builder.js +26 -113
  121. package/dist/engines/pi/session-control.d.ts +10 -18
  122. package/dist/engines/pi/session-control.js +47 -81
  123. package/dist/engines/pi/session-settings.d.ts +1 -1
  124. package/dist/engines/pi/session-settings.js +1 -1
  125. package/dist/engines/pi/session-store.d.ts +12 -17
  126. package/dist/engines/pi/session-store.js +18 -40
  127. package/dist/engines/pi/tool-context.d.ts +3 -3
  128. package/dist/engines/pi/tool.d.ts +6 -9
  129. package/dist/engines/pi/tool.js +1 -0
  130. package/dist/feishu.d.ts +1 -0
  131. package/dist/feishu.js +1 -0
  132. package/dist/lark.d.ts +1 -0
  133. package/dist/lark.js +1 -0
  134. package/dist/loader.d.ts +2 -0
  135. package/dist/loader.js +5 -0
  136. package/dist/log.d.ts +9 -17
  137. package/dist/log.js +25 -30
  138. package/dist/paths.d.ts +7 -3
  139. package/dist/paths.js +17 -4
  140. package/dist/scaffold/add-channel.d.ts +6 -1
  141. package/dist/scaffold/add-channel.js +48 -65
  142. package/dist/schedule/wake-alarm.d.ts +1 -12
  143. package/dist/schedule/wake-alarm.js +2 -3
  144. package/dist/service.d.ts +23 -22
  145. package/dist/service.js +7 -73
  146. package/dist/session-remote.d.ts +5 -5
  147. package/dist/session-remote.js +25 -28
  148. package/dist/session.d.ts +1 -1
  149. package/dist/slack.d.ts +2 -0
  150. package/dist/slack.js +1 -0
  151. package/dist/tunnel.d.ts +16 -6
  152. package/dist/tunnel.js +53 -12
  153. package/package.json +5 -4
  154. package/dist/channels/slack/bot-auth.d.ts +0 -15
  155. package/dist/channels/slack/bot-auth.js +0 -135
@@ -1,7 +1,19 @@
1
1
  import { type LoginIO, type LoginMethod, type LoginResult } from "../engines/pi/login.ts";
2
2
  import type { LoadedDefinition } from "../engines/pi/definition.ts";
3
- import type { ModuleLoadFailure } from "../loader.ts";
3
+ import { type ModuleLoadFailure } from "../loader.ts";
4
4
  import type { ToolCollision } from "../engines/pi/tool.ts";
5
+ import { type ResolvedPlacement } from "../paths.ts";
6
+ /**
7
+ * How every command that runs the model enters its agent directory, in the one order that works:
8
+ * placement decides whose `.env` to read; `.env` may carry the proxy and the provider keys the
9
+ * picker's auth probe needs; the picker runs last. Six commands wrote these steps out by hand, and
10
+ * one carried the order as a comment. The result is the placement every later step reads.
11
+ */
12
+ export declare function enterAgentCommand(dirArg: string, opts: {
13
+ model?: string;
14
+ authPath?: string;
15
+ input?: boolean;
16
+ }): Promise<ResolvedPlacement>;
5
17
  /** What the startup report reads off an opened directory — a structural subset of the opener's return,
6
18
  * spelled out so this module does not depend on that function's whole shape. */
7
19
  export interface ReportableAssembly {
@@ -60,23 +72,6 @@ export declare function parseBind(value: string | undefined): string | undefined
60
72
  * Probes through the AGENT's model surface (`agentDir` carries its models.json), so a custom endpoint is
61
73
  * reported like any built-in rather than as an unknown provider. */
62
74
  export declare function reportAuth(agentDir: string, modelSpec: string, authPath: string): Promise<void>;
63
- /**
64
- * First-run model resolution for every assembly command (dev/start/invoke/fire/chat/deploy): ONE
65
- * funnel, no dead ends. When no model is set (flag/env/config) and we're on a TTY, show the FULL
66
- * catalog annotated per provider — ready (with the credential source, so which account pays is
67
- * visible at the decision point) or login-required — and, when the choice needs auth, run the login
68
- * flow INLINE instead of exiting with "run `fastagent login` and come back". A no-op when a model is
69
- * already set; on a non-TTY (CI, a piped stdin), with `--no-input`, on cancel, or on a failed login
70
- * it stays quiet and lets the caller raise its own clear error (`missing model`, or deploy's
71
- * model-travel gate). The pick is exported to FASTAGENT_MODEL so a spawned `dev` worker inherits it,
72
- * and best-effort written back to the config so the next run is quiet. `agentDir` is the resolved
73
- * AGENT DIR (resolvePlacement().agentDir) — config and auth both live there.
74
- */
75
- export declare function resolveFirstRunModel(agentDir: string, options?: {
76
- model?: string;
77
- authPath?: string;
78
- input?: boolean;
79
- }): Promise<void>;
80
75
  /**
81
76
  * Interactive login with the api_key quick-fail probe closed into a LOOP: a definitively rejected key
82
77
  * (HTTP 401) deletes the bad credential and RE-PROMPTS immediately — the user's hands are on the
@@ -4,7 +4,7 @@
4
4
  * module-scoped flag access (`values.*`) became parameters.
5
5
  */
6
6
  import { readFile, writeFile } from "node:fs/promises";
7
- import { relative } from "node:path";
7
+ import { relative, resolve } 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";
@@ -13,12 +13,28 @@ import { LoginCancelled, loginFlow } from "../engines/pi/login.js";
13
13
  import { createPiModelRuntime, createPiModels, probeApiKey, probeAuthSource, providerAuthStatuses, } from "../engines/pi/models.js";
14
14
  import { formatAuthReport } from "./auth-view.js";
15
15
  import { CODING_TOOL_NAMES } from "../engines/pi/create.js";
16
+ import { reportModuleLoadFailures } from "../loader.js";
16
17
  import { reportFindingsIfChanged, reportToolCollisions } from "../engines/pi/report.js";
17
18
  import { workspaceHint } from "../paths.js";
18
- import { log, reportModuleLoadFailures } from "../log.js";
19
+ import { log } from "../log.js";
20
+ import { loadDotEnv } from "../env.js";
19
21
  import { openExternalUrl } from "../open-url.js";
22
+ import { installProxyFetch } from "../proxy.js";
20
23
  import { bindAddress, isBindAddress } from "../bind.js";
21
- import { failStartup, failUsage } from "./fail.js";
24
+ import { failStartup, failUsage, placementOrExit } from "./fail.js";
25
+ /**
26
+ * How every command that runs the model enters its agent directory, in the one order that works:
27
+ * placement decides whose `.env` to read; `.env` may carry the proxy and the provider keys the
28
+ * picker's auth probe needs; the picker runs last. Six commands wrote these steps out by hand, and
29
+ * one carried the order as a comment. The result is the placement every later step reads.
30
+ */
31
+ export async function enterAgentCommand(dirArg, opts) {
32
+ const placement = placementOrExit(resolve(dirArg));
33
+ loadDotEnv(placement.agentDir);
34
+ installProxyFetch();
35
+ await resolveFirstRunModel(placement.agentDir, opts);
36
+ return placement;
37
+ }
22
38
  /**
23
39
  * The padded label writer for the STARTUP report (`dev`/`start`, stderr via the log level). Hand-spaced
24
40
  * labels drift out of alignment the moment a longer one appears — which is exactly what happened when
@@ -148,7 +164,7 @@ export async function reportAuth(agentDir, modelSpec, authPath) {
148
164
  * and best-effort written back to the config so the next run is quiet. `agentDir` is the resolved
149
165
  * AGENT DIR (resolvePlacement().agentDir) — config and auth both live there.
150
166
  */
151
- export async function resolveFirstRunModel(agentDir, options = {}) {
167
+ async function resolveFirstRunModel(agentDir, options = {}) {
152
168
  const { config, path: configPath } = await loadConfig(agentDir).catch(failStartup);
153
169
  if (resolveModelSpec(options.model, config))
154
170
  return; // already set (flag > FASTAGENT_MODEL > config)
@@ -0,0 +1,250 @@
1
+ // Generated by `fastagent deploy agentcore` — the deployment's only ingress.
2
+ // (Inside fastagent this file IS the source; the sentence below applies to the copy it is emitted as,
3
+ // the workspace's `lambda/index.js`.)
4
+ // REGENERATED ON EVERY DEPLOY; edits here are overwritten and never deployed. `--run` builds the
5
+ // Lambda package from fastagent's own copy of this source, so this file is the readable MIRROR of
6
+ // what runs (and what the manual runbook zips) — not an input you can change.
7
+ // Webhooks (Function URL) and EventBridge Scheduler fires are forwarded as envelopes to the
8
+ // AgentCore Runtime over SigV4 InvokeAgentRuntime, all on ONE fixed ingress session (fastagent
9
+ // channel state is single-writer; one session = at most one microVM). With selfSchedule, this
10
+ // Lambda also OWNS the wake alarms: the container POSTs its pending wake-ups to /__fastagent/
11
+ // wake-alarm (shared secret) and each becomes a self-deleting one-shot EventBridge schedule that
12
+ // pokes this Lambda — which wakes the container, whose wake pump fires the due entry.
13
+ // CommonJS on purpose: the deployment package's entry lands as index.js, where ESM import is invalid.
14
+ "use strict";
15
+ const crypto = require("node:crypto");
16
+ const { BedrockAgentCoreClient, InvokeAgentRuntimeCommand } = require("@aws-sdk/client-bedrock-agentcore");
17
+ const client = new BedrockAgentCoreClient({});
18
+ let ownUrl; // self-resolved once per cold start; rides on every envelope for the wake-alarm callback
19
+
20
+ // Presigned S3 URLs for the container's state snapshot. AgentCore wipes the /mnt/state mount on
21
+ // every runtime version update (= every deploy), so the durable copy lives in S3 — but the
22
+ // container is given NO AWS credentials by the platform, so the only reachable form is a URL that
23
+ // carries its own authorization. SigV4 query signing, node:crypto only (no SDK, nothing to install).
24
+ const enc = (s) => encodeURIComponent(s).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
25
+ const hmac = (key, data) => crypto.createHmac("sha256", key).update(data).digest();
26
+
27
+ // Every shared-secret gate on this public URL goes through here. Constant-time, and an unset
28
+ // expected secret NEVER matches — otherwise a topology that did not configure one would accept a
29
+ // request that sent none. Non-string input is coerced to "" first: Buffer.from(8) allocates eight
30
+ // bytes rather than throwing, which would compare a number against a secret by length alone.
31
+ const secretEq = (given, expected) => {
32
+ const a = Buffer.from(typeof given === "string" ? given : ""),
33
+ b = Buffer.from(expected || "");
34
+ return b.length > 0 && a.length === b.length && crypto.timingSafeEqual(a, b);
35
+ };
36
+
37
+ function presign(method, seconds) {
38
+ const bucket = process.env.STATE_BUCKET,
39
+ key = process.env.STATE_KEY,
40
+ region = process.env.AWS_REGION;
41
+ const host = `${bucket}.s3.${region}.amazonaws.com`;
42
+ const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+/, "");
43
+ const scope = `${stamp.slice(0, 8)}/${region}/s3/aws4_request`;
44
+ const pairs = [
45
+ ["X-Amz-Algorithm", "AWS4-HMAC-SHA256"],
46
+ ["X-Amz-Credential", `${process.env.AWS_ACCESS_KEY_ID}/${scope}`],
47
+ ["X-Amz-Date", stamp],
48
+ ["X-Amz-Expires", String(seconds)],
49
+ ["X-Amz-SignedHeaders", "host"],
50
+ ];
51
+ if (process.env.AWS_SESSION_TOKEN) pairs.push(["X-Amz-Security-Token", process.env.AWS_SESSION_TOKEN]);
52
+ // The canonical query must be byte-identical to the one on the wire — build it ONCE, reuse below.
53
+ const query = pairs
54
+ .map(([k, v]) => [enc(k), enc(v)])
55
+ .sort((a, b) => (a[0] < b[0] ? -1 : 1))
56
+ .map((p) => p.join("="))
57
+ .join("&");
58
+ const path = `/${key.split("/").map(enc).join("/")}`;
59
+ const canonical = [method, path, query, `host:${host}\n`, "host", "UNSIGNED-PAYLOAD"].join("\n");
60
+ const sts = ["AWS4-HMAC-SHA256", stamp, scope, crypto.createHash("sha256").update(canonical).digest("hex")].join(
61
+ "\n",
62
+ );
63
+ let k = hmac(`AWS4${process.env.AWS_SECRET_ACCESS_KEY}`, stamp.slice(0, 8));
64
+ for (const part of [region, "s3", "aws4_request"]) k = hmac(k, part);
65
+ return `https://${host}${path}?${query}&X-Amz-Signature=${hmac(k, sts).toString("hex")}`;
66
+ }
67
+
68
+ async function invoke(envelope) {
69
+ if ((process.env.WAKE_SECRET || process.env.STATE_REFRESH_SECRET) && !ownUrl) {
70
+ const { LambdaClient, GetFunctionUrlConfigCommand } = require("@aws-sdk/client-lambda");
71
+ ownUrl = (
72
+ await new LambdaClient({}).send(
73
+ new GetFunctionUrlConfigCommand({ FunctionName: process.env.AWS_LAMBDA_FUNCTION_NAME }),
74
+ )
75
+ ).FunctionUrl;
76
+ }
77
+ if (ownUrl) envelope.wake = { url: ownUrl };
78
+ // Authenticates this envelope as coming from the forwarder (see the template's FastagentIngressSecret).
79
+ envelope.auth = process.env.INGRESS_SECRET;
80
+ // Keep each capability short-lived. Function-URL deployments also carry an authenticated refresh
81
+ // endpoint, so a background turn settling hours after its webhook never depends on the temporary
82
+ // Lambda credentials that signed the original pair still being alive.
83
+ if (process.env.STATE_BUCKET)
84
+ envelope.state = {
85
+ getUrl: presign("GET", 3600),
86
+ putUrl: presign("PUT", 3600),
87
+ ...(ownUrl && process.env.STATE_REFRESH_SECRET
88
+ ? {
89
+ refresh: {
90
+ url: `${ownUrl.replace(/\/$/, "")}/__fastagent/state-urls`,
91
+ auth: process.env.STATE_REFRESH_SECRET,
92
+ },
93
+ }
94
+ : {}),
95
+ };
96
+ const res = await client.send(
97
+ new InvokeAgentRuntimeCommand({
98
+ agentRuntimeArn: process.env.RUNTIME_ARN,
99
+ runtimeSessionId: process.env.INGRESS_SESSION_ID,
100
+ contentType: "application/json",
101
+ accept: "application/json",
102
+ payload: new TextEncoder().encode(JSON.stringify(envelope)),
103
+ }),
104
+ );
105
+ const body = Buffer.from(await res.response.transformToByteArray());
106
+ return { status: res.statusCode ?? 200, body };
107
+ }
108
+
109
+ // Mirror the container's pending wake-ups into one-shot schedules: at(fireAt), poke me, delete
110
+ // after firing. Upsert (create → conflict → update). The container pre-filters DUE alarms (it is
111
+ // awake handling those), so every failure here is REAL — counted and propagated: a swallowed error
112
+ // would leave a pending wake with no alarm, exactly the reliability hole this mechanism closes.
113
+ // Cancelled wakes are NOT deleted here: their poke fires, finds nothing due, and the schedule
114
+ // self-deletes (lazy cleanup by design).
115
+ async function syncAlarms(alarms, ctx) {
116
+ const { SchedulerClient, CreateScheduleCommand, UpdateScheduleCommand } = require("@aws-sdk/client-scheduler");
117
+ const sch = new SchedulerClient({});
118
+ let failed = 0;
119
+ // Alarm name = a stable hash of the WHOLE wake id. A prefix of the id would collide (two wakes
120
+ // sharing 8 hex chars), and a collision is INDISTINGUISHABLE from the legitimate re-arm below:
121
+ // the second wake would "update" the first's alarm and silently steal its fire time.
122
+ const names = new Map();
123
+ for (const a of alarms) {
124
+ const name = process.env.WAKE_PREFIX + crypto.createHash("sha256").update(a.id).digest("hex").slice(0, 16);
125
+ if (names.has(name)) {
126
+ failed += 1;
127
+ console.log(`alarm name collision ${name}: ${names.get(name)} vs ${a.id}`);
128
+ continue;
129
+ }
130
+ names.set(name, a.id);
131
+ const p = {
132
+ Name: name,
133
+ ScheduleExpression: `at(${a.at.slice(0, 19)})`,
134
+ ScheduleExpressionTimezone: "UTC",
135
+ FlexibleTimeWindow: { Mode: "OFF" },
136
+ ActionAfterCompletion: "DELETE",
137
+ Target: { Arn: ctx.invokedFunctionArn, RoleArn: process.env.WAKE_ROLE_ARN, Input: '{"wakePoke":true}' },
138
+ };
139
+ try {
140
+ await sch.send(new CreateScheduleCommand(p));
141
+ } catch (e) {
142
+ try {
143
+ if (e.name !== "ConflictException") throw e;
144
+ await sch.send(new UpdateScheduleCommand(p));
145
+ } catch (u) {
146
+ failed += 1;
147
+ console.log(`alarm ${p.Name}: ${u}`);
148
+ }
149
+ }
150
+ }
151
+ return failed;
152
+ }
153
+
154
+ exports.handler = async (event, ctx) => {
155
+ // EventBridge wake-up poke: the invocation itself wakes the container; its pump does the rest.
156
+ if (event?.wakePoke) {
157
+ const r = await invoke({ kind: "wake-poke" });
158
+ console.log(`wake-poke: ${r.status}`);
159
+ return { status: r.status };
160
+ }
161
+ // EventBridge Scheduler fire — throw on failure so the miss lands in CloudWatch, never silently.
162
+ if (event?.scheduleFire) {
163
+ const { name, slot } = event.scheduleFire;
164
+ const r = await invoke({ kind: "schedule-fire", name, slot });
165
+ const out = r.body.toString();
166
+ console.log(`schedule-fire ${name} (${slot}): ${r.status} ${out}`);
167
+ if (r.status >= 400) throw new Error(`schedule-fire ${name} failed: ${r.status} ${out}`);
168
+ return { status: r.status };
169
+ }
170
+ const http = event?.requestContext?.http;
171
+ if (!http) throw new Error("unrecognized event shape");
172
+ // Refresh the snapshot capabilities with THIS Lambda invocation's current temporary credentials.
173
+ // The container may settle long after the webhook Lambda (and its credentials) expired.
174
+ if (event.rawPath === "/__fastagent/state-urls") {
175
+ const req = JSON.parse(event.isBase64Encoded ? Buffer.from(event.body, "base64").toString() : event.body || "{}");
176
+ if (!secretEq(req.auth, process.env.STATE_REFRESH_SECRET)) return { statusCode: 403, body: "forbidden\n" };
177
+ return {
178
+ statusCode: 200,
179
+ headers: { "content-type": "application/json" },
180
+ body: JSON.stringify({ getUrl: presign("GET", 3600), putUrl: presign("PUT", 3600) }),
181
+ };
182
+ }
183
+ // The container's wake-alarm callback (reserved path, shared secret) — handled HERE, never forwarded.
184
+ if (event.rawPath === "/__fastagent/wake-alarm") {
185
+ const req = JSON.parse(event.isBase64Encoded ? Buffer.from(event.body, "base64").toString() : event.body || "{}");
186
+ if (!secretEq(req.secret, process.env.WAKE_SECRET)) return { statusCode: 403, body: "forbidden\n" };
187
+ const failed = await syncAlarms(req.alarms || [], ctx);
188
+ // Partial failure IS failure: the container retries the whole (idempotent) set until every
189
+ // pending wake really has its alarm.
190
+ if (failed > 0) return { statusCode: 500, body: `${failed} alarm(s) failed\n` };
191
+ return { statusCode: 200, body: "ok\n" };
192
+ }
193
+ // The deploy driver's probe (reserved path, ingress secret): wake the runtime through the SAME
194
+ // trusted envelope pipeline (state URLs included — a direct InvokeAgentRuntime call could not mint
195
+ // them, and would make the runtime construct against a pre-restore mount) and pass its structured
196
+ // transport-200 verdict back VERBATIM. The ordinary webhook path below folds a non-200 transport
197
+ // into an opaque 502, which would strip exactly the diagnostics the probe exists to carry — and it
198
+ // sits BEFORE the WEBHOOKS_ENABLED gate so schedule-only topologies (whose URLs refuse ordinary
199
+ // public traffic) are probeable too.
200
+ if (event.rawPath === "/__fastagent/probe") {
201
+ const req = JSON.parse(event.isBase64Encoded ? Buffer.from(event.body, "base64").toString() : event.body || "{}");
202
+ if (!secretEq(req.auth, process.env.INGRESS_SECRET)) return { statusCode: 403, body: "forbidden\n" };
203
+ const r = await invoke({ kind: "probe" });
204
+ if (r.status !== 200) {
205
+ console.log(`probe transport error ${r.status}: ${r.body}`);
206
+ return { statusCode: 502, body: "upstream error\n" };
207
+ }
208
+ return { statusCode: 200, headers: { "content-type": "application/json" }, body: r.body.toString() };
209
+ }
210
+ // Enforce the advertised ORIGINAL-body ceiling before base64 adds another 4/3 inside the runtime
211
+ // envelope. This also leaves deterministic room for headers/query/JSON under Lambda's 6 MB cap.
212
+ const webhookBytes =
213
+ event.body === undefined
214
+ ? 0
215
+ : event.isBase64Encoded
216
+ ? Buffer.byteLength(event.body, "base64")
217
+ : Buffer.byteLength(event.body);
218
+ // The ceiling rides in as an env var (the template's MAX_WEBHOOK_BODY_BYTES), so a stack whose
219
+ // template predates it would compare against NaN — every comparison false, the limit silently gone.
220
+ const maxWebhookBytes = Number(process.env.MAX_WEBHOOK_BODY_BYTES);
221
+ if (!(maxWebhookBytes > 0))
222
+ throw new Error("MAX_WEBHOOK_BODY_BYTES is missing or invalid — redeploy to regenerate the stack template");
223
+ if (webhookBytes > maxWebhookBytes) return { statusCode: 413, body: "payload too large\n" };
224
+ // A schedule-only deployment has a Function URL solely for the authenticated refresh callback.
225
+ // Reject arbitrary public traffic BEFORE it can wake AgentCore (cost/DoS) or reach an inner route.
226
+ if (process.env.WEBHOOKS_ENABLED !== "1") return { statusCode: 404, body: "not found\n" };
227
+ // Function URL webhook — forward the original request verbatim (signature material included).
228
+ const r = await invoke({
229
+ kind: "webhook",
230
+ method: http.method,
231
+ path: event.rawPath || "/",
232
+ query: event.rawQueryString || undefined,
233
+ headers: event.headers || {},
234
+ bodyB64:
235
+ event.body === undefined
236
+ ? undefined
237
+ : event.isBase64Encoded
238
+ ? event.body
239
+ : Buffer.from(event.body).toString("base64"),
240
+ });
241
+ if (r.status !== 200) {
242
+ console.log(`transport error ${r.status}: ${r.body}`);
243
+ return { statusCode: 502, body: "upstream error\n" };
244
+ }
245
+ const reply = JSON.parse(r.body.toString()); // { status, headers, bodyB64 } from the adapter
246
+ for (const k of Object.keys(reply.headers)) {
247
+ if (/^(content-length|transfer-encoding|connection)$/i.test(k)) delete reply.headers[k];
248
+ }
249
+ return { statusCode: reply.status, headers: reply.headers, body: reply.bodyB64, isBase64Encoded: true };
250
+ };
@@ -31,6 +31,25 @@ export interface AgentcorePlan {
31
31
  name: string;
32
32
  reason: string;
33
33
  }[];
34
+ /** What the stack contains — the ONE reading the template, the runbook and the `--run` driver share. */
35
+ topology: AgentcoreTopology;
36
+ }
37
+ /**
38
+ * Which resources the definition puts in the stack. Read in one place because it was computed in
39
+ * three — the template, the plan and the CLI — and the third disagreed with the first about a
40
+ * schedule EventBridge cannot express (counted by the CLI, not by the template), which is a
41
+ * `cloudformation deploy` rejecting parameters the template never declared.
42
+ */
43
+ export interface AgentcoreTopology {
44
+ /** A webhook channel: the forwarder relays public Function URL traffic to it. */
45
+ webhooks: boolean;
46
+ /** The forwarder Lambda exists — for webhooks, for EventBridge cron rules, or for wake alarms —
47
+ * with its Function URL: every forwarder needs it as the authenticated refresh channel for the
48
+ * state snapshot's presigned URLs, since a turn can outlive the Lambda credentials that signed
49
+ * them. A schedule-only URL rejects every non-reserved path before invoking AgentCore. */
50
+ forwarder: boolean;
51
+ /** The forwarder mirrors the agent's wake-ups into one-shot EventBridge schedules. */
52
+ wakeAlarms: boolean;
34
53
  }
35
54
  /** SessionStorage mount = FASTAGENT_STATE_DIR (AgentCore requires exactly `/mnt/<one-level>`). It is
36
55
  * a fast LOCAL disk only: the platform wipes it on every runtime version update (= every deploy).
@@ -117,8 +136,10 @@ export declare function toEventBridgeCron(cron: string): {
117
136
  error: string;
118
137
  };
119
138
  /**
120
- * The forwarder Lambda source — the ONLY string both the deployment package and the readable
121
- * `lambda/forwarder.js` artifact are generated from (one source, no drift). Zero-dependency: the
139
+ * The forwarder Lambda source — `forwarder.js` beside this file, the ONE text both the deployment
140
+ * package and the readable `lambda/index.js` artifact are generated from. A real file, not a template
141
+ * literal: it is linted, and it needs no interpolation — the one value it once took from here
142
+ * (`MAX_WEBHOOK_BODY_BYTES`) rides in as a Lambda environment variable. Zero-dependency: the
122
143
  * Lambda Node runtime bundles AWS SDK v3. CommonJS ON PURPOSE: the package's entry lands as
123
144
  * `index.js` ({@link zipSingleFile}), where ESM `import` is a syntax error. Two
124
145
  * event shapes: a Function URL webhook (reconstructed verbatim into a `webhook` envelope; the