@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.
- package/README.md +5 -1
- package/dist/channels/agentcore-protocol.d.ts +112 -0
- package/dist/channels/agentcore-protocol.js +22 -0
- package/dist/channels/agentcore-service.d.ts +0 -4
- package/dist/channels/agentcore-service.js +1 -6
- package/dist/channels/agentcore-state.d.ts +5 -11
- package/dist/channels/agentcore-state.js +4 -1
- package/dist/channels/agentcore.d.ts +1 -67
- package/dist/channels/agentcore.js +90 -89
- package/dist/channels/control.d.ts +1 -1
- package/dist/channels/control.js +13 -62
- package/dist/channels/discover.d.ts +0 -1
- package/dist/channels/discover.js +1 -3
- package/dist/channels/feishu/context-buffer.d.ts +6 -0
- package/dist/channels/feishu/context-buffer.js +0 -38
- package/dist/channels/feishu/crypto.d.ts +0 -2
- package/dist/channels/feishu/crypto.js +3 -8
- package/dist/channels/feishu/feishu-api.js +2 -4
- package/dist/channels/feishu/feishu.js +56 -115
- package/dist/channels/feishu/parse.d.ts +4 -3
- package/dist/channels/feishu/parse.js +5 -4
- package/dist/channels/feishu/register-webhook.d.ts +5 -5
- package/dist/channels/feishu/register-webhook.js +43 -57
- package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
- package/dist/channels/feishu/setup-mode.d.ts +30 -0
- package/dist/channels/feishu/setup-mode.js +26 -0
- package/dist/channels/feishu/shared-api.d.ts +10 -0
- package/dist/channels/feishu/shared-api.js +38 -0
- package/dist/channels/http.d.ts +0 -8
- package/dist/channels/http.js +4 -56
- package/dist/channels/kit/turn-runner.d.ts +59 -0
- package/dist/channels/kit/turn-runner.js +84 -0
- package/dist/channels/lark/scaffold/lark-send.ts +12 -67
- package/dist/channels/registration.d.ts +36 -1
- package/dist/channels/registration.js +57 -1
- package/dist/channels/secret.d.ts +1 -0
- package/dist/channels/secret.js +16 -0
- package/dist/channels/slack/config-api.d.ts +35 -3
- package/dist/channels/slack/config-api.js +51 -9
- package/dist/channels/slack/manifest.js +5 -1
- package/dist/channels/slack/onboard.d.ts +5 -5
- package/dist/channels/slack/onboard.js +41 -20
- package/dist/channels/slack/register-webhook.d.ts +3 -2
- package/dist/channels/slack/register-webhook.js +34 -14
- package/dist/channels/slack/scaffold/channel.ts +3 -10
- package/dist/channels/slack/scaffold/slack-send.ts +18 -126
- package/dist/channels/slack/shared-api.d.ts +10 -0
- package/dist/channels/slack/shared-api.js +34 -0
- package/dist/channels/slack/slack-api.d.ts +20 -2
- package/dist/channels/slack/slack-api.js +81 -22
- package/dist/channels/slack/slack.d.ts +0 -10
- package/dist/channels/slack/slack.js +60 -99
- package/dist/channels/sse.d.ts +4 -0
- package/dist/channels/sse.js +66 -0
- package/dist/channels/telegram/register-webhook.d.ts +6 -9
- package/dist/channels/telegram/register-webhook.js +44 -42
- package/dist/channels/telegram/telegram.js +47 -142
- package/dist/channels/wait-health.js +7 -4
- package/dist/cli/add-feishu.js +3 -10
- package/dist/cli/add-slack.js +7 -20
- package/dist/cli/commands/add.d.ts +0 -1
- package/dist/cli/commands/add.js +22 -31
- package/dist/cli/commands/chat.js +6 -12
- package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
- package/dist/cli/commands/deploy/agentcore.js +178 -0
- package/dist/cli/commands/deploy/docker.d.ts +2 -0
- package/dist/cli/commands/deploy/docker.js +119 -0
- package/dist/cli/commands/deploy/fly.d.ts +2 -0
- package/dist/cli/commands/deploy/fly.js +131 -0
- package/dist/cli/commands/deploy/railway.d.ts +2 -0
- package/dist/cli/commands/deploy/railway.js +71 -0
- package/dist/cli/commands/deploy/shared.d.ts +114 -0
- package/dist/cli/commands/deploy/shared.js +124 -0
- package/dist/cli/commands/deploy.d.ts +17 -49
- package/dist/cli/commands/deploy.js +39 -618
- package/dist/cli/commands/dev.js +17 -46
- package/dist/cli/commands/fire.js +6 -12
- package/dist/cli/commands/info.js +2 -1
- package/dist/cli/commands/invoke.js +4 -11
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +17 -46
- package/dist/cli/commands/tool.js +3 -8
- package/dist/cli/kernel.d.ts +0 -2
- package/dist/cli/kernel.js +0 -2
- package/dist/cli/program.js +7 -12
- package/dist/cli/serve.d.ts +36 -19
- package/dist/cli/serve.js +91 -7
- package/dist/cli/shared.d.ts +13 -18
- package/dist/cli/shared.js +20 -4
- package/dist/deploy/agentcore/forwarder.js +250 -0
- package/dist/deploy/agentcore/plan.d.ts +23 -2
- package/dist/deploy/agentcore/plan.js +29 -244
- package/dist/deploy/agentcore/run.d.ts +4 -20
- package/dist/deploy/agentcore/run.js +22 -7
- package/dist/deploy/channel-ingress.js +1 -1
- package/dist/deploy/docker/run.d.ts +17 -3
- package/dist/deploy/docker/run.js +30 -8
- package/dist/deploy/fly/plan.js +7 -0
- package/dist/deploy/fly/run.d.ts +27 -0
- package/dist/deploy/fly/run.js +102 -20
- package/dist/deploy/hosts.d.ts +5 -0
- package/dist/deploy/hosts.js +4 -0
- package/dist/deploy/preflight.js +3 -3
- package/dist/deploy/railway/plan.d.ts +5 -0
- package/dist/deploy/railway/plan.js +7 -0
- package/dist/deploy/railway/run.d.ts +2 -1
- package/dist/deploy/railway/run.js +5 -4
- package/dist/deploy/secrets.js +0 -17
- package/dist/engines/pi/agent-session-factory.d.ts +37 -17
- package/dist/engines/pi/agent-session-factory.js +109 -85
- package/dist/engines/pi/config.d.ts +1 -1
- package/dist/engines/pi/create.d.ts +22 -39
- package/dist/engines/pi/create.js +58 -73
- package/dist/engines/pi/definition.js +8 -8
- package/dist/engines/pi/invoke-session.js +13 -30
- package/dist/engines/pi/open.d.ts +2 -2
- package/dist/engines/pi/open.js +31 -41
- package/dist/engines/pi/retry-event.d.ts +6 -0
- package/dist/engines/pi/retry-event.js +15 -0
- package/dist/engines/pi/session-builder.js +26 -113
- package/dist/engines/pi/session-control.d.ts +10 -18
- package/dist/engines/pi/session-control.js +47 -81
- package/dist/engines/pi/session-settings.d.ts +1 -1
- package/dist/engines/pi/session-settings.js +1 -1
- package/dist/engines/pi/session-store.d.ts +12 -17
- package/dist/engines/pi/session-store.js +18 -40
- package/dist/engines/pi/tool-context.d.ts +3 -3
- package/dist/engines/pi/tool.d.ts +6 -9
- package/dist/engines/pi/tool.js +1 -0
- package/dist/feishu.d.ts +1 -0
- package/dist/feishu.js +1 -0
- package/dist/lark.d.ts +1 -0
- package/dist/lark.js +1 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +5 -0
- package/dist/log.d.ts +9 -17
- package/dist/log.js +25 -30
- package/dist/paths.d.ts +7 -3
- package/dist/paths.js +17 -4
- package/dist/scaffold/add-channel.d.ts +6 -1
- package/dist/scaffold/add-channel.js +48 -65
- package/dist/schedule/wake-alarm.d.ts +1 -12
- package/dist/schedule/wake-alarm.js +2 -3
- package/dist/service.d.ts +23 -22
- package/dist/service.js +7 -73
- package/dist/session-remote.d.ts +5 -5
- package/dist/session-remote.js +25 -28
- package/dist/session.d.ts +1 -1
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +1 -0
- package/dist/tunnel.d.ts +16 -6
- package/dist/tunnel.js +53 -12
- package/package.json +5 -4
- package/dist/channels/slack/bot-auth.d.ts +0 -15
- package/dist/channels/slack/bot-auth.js +0 -135
package/dist/cli/shared.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/cli/shared.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 —
|
|
121
|
-
* `lambda/
|
|
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
|